ei-tui 0.1.25 → 0.2.0

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 (78) hide show
  1. package/README.md +42 -0
  2. package/package.json +1 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +4 -5
  5. package/src/cli/retrieval.ts +3 -25
  6. package/src/cli.ts +3 -7
  7. package/src/core/AGENTS.md +1 -1
  8. package/src/core/constants/built-in-facts.ts +49 -0
  9. package/src/core/constants/index.ts +1 -0
  10. package/src/core/context-utils.ts +0 -1
  11. package/src/core/embedding-service.ts +8 -0
  12. package/src/core/handlers/dedup.ts +10 -16
  13. package/src/core/handlers/heartbeat.ts +2 -3
  14. package/src/core/handlers/human-extraction.ts +95 -30
  15. package/src/core/handlers/human-matching.ts +326 -248
  16. package/src/core/handlers/index.ts +8 -6
  17. package/src/core/handlers/persona-generation.ts +8 -8
  18. package/src/core/handlers/rewrite.ts +4 -29
  19. package/src/core/handlers/utils.ts +23 -1
  20. package/src/core/heartbeat-manager.ts +2 -4
  21. package/src/core/human-data-manager.ts +5 -27
  22. package/src/core/message-manager.ts +10 -10
  23. package/src/core/orchestrators/ceremony.ts +50 -39
  24. package/src/core/orchestrators/dedup-phase.ts +0 -1
  25. package/src/core/orchestrators/human-extraction.ts +351 -207
  26. package/src/core/orchestrators/index.ts +6 -4
  27. package/src/core/orchestrators/persona-generation.ts +3 -3
  28. package/src/core/processor.ts +99 -17
  29. package/src/core/prompt-context-builder.ts +4 -6
  30. package/src/core/state/human.ts +1 -26
  31. package/src/core/state/personas.ts +2 -2
  32. package/src/core/state-manager.ts +107 -14
  33. package/src/core/tools/builtin/read-memory.ts +7 -8
  34. package/src/core/types/data-items.ts +2 -4
  35. package/src/core/types/entities.ts +6 -4
  36. package/src/core/types/enums.ts +6 -9
  37. package/src/core/types/llm.ts +2 -2
  38. package/src/core/utils/crossFind.ts +2 -5
  39. package/src/core/utils/event-windows.ts +31 -0
  40. package/src/integrations/claude-code/importer.ts +8 -4
  41. package/src/integrations/claude-code/types.ts +2 -0
  42. package/src/integrations/opencode/importer.ts +7 -3
  43. package/src/prompts/AGENTS.md +73 -1
  44. package/src/prompts/ceremony/rewrite.ts +3 -22
  45. package/src/prompts/ceremony/types.ts +3 -3
  46. package/src/prompts/generation/descriptions.ts +2 -2
  47. package/src/prompts/generation/types.ts +2 -2
  48. package/src/prompts/heartbeat/types.ts +2 -2
  49. package/src/prompts/human/event-scan.ts +122 -0
  50. package/src/prompts/human/fact-find.ts +106 -0
  51. package/src/prompts/human/fact-scan.ts +0 -2
  52. package/src/prompts/human/index.ts +17 -10
  53. package/src/prompts/human/person-match.ts +65 -0
  54. package/src/prompts/human/person-scan.ts +52 -59
  55. package/src/prompts/human/person-update.ts +241 -0
  56. package/src/prompts/human/topic-match.ts +65 -0
  57. package/src/prompts/human/topic-scan.ts +51 -71
  58. package/src/prompts/human/topic-update.ts +295 -0
  59. package/src/prompts/human/types.ts +63 -40
  60. package/src/prompts/index.ts +4 -8
  61. package/src/prompts/persona/topics-update.ts +2 -2
  62. package/src/prompts/persona/traits.ts +2 -2
  63. package/src/prompts/persona/types.ts +3 -3
  64. package/src/prompts/response/index.ts +1 -1
  65. package/src/prompts/response/sections.ts +9 -12
  66. package/src/prompts/response/types.ts +2 -3
  67. package/src/storage/embeddings.ts +1 -1
  68. package/src/storage/index.ts +1 -0
  69. package/src/storage/indexed.ts +174 -0
  70. package/src/storage/merge.ts +67 -2
  71. package/tui/src/commands/me.tsx +5 -14
  72. package/tui/src/commands/settings.tsx +15 -0
  73. package/tui/src/context/ei.tsx +5 -14
  74. package/tui/src/util/yaml-serializers.ts +48 -33
  75. package/src/cli/commands/traits.ts +0 -25
  76. package/src/prompts/human/item-match.ts +0 -74
  77. package/src/prompts/human/item-update.ts +0 -364
  78. package/src/prompts/human/trait-scan.ts +0 -115
@@ -1,13 +1,12 @@
1
- import type { HumanEntity, PersonaEntity, Fact, Trait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
1
+ import type { HumanEntity, PersonaEntity, Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
2
2
  export type CrossFindResult =
3
3
  | { type: "fact" } & Fact
4
- | { type: "trait" } & Trait
5
4
  | { type: "topic" } & Topic
6
5
  | { type: "person" } & Person
7
6
  | { type: "quote" } & Quote
8
7
  | { type: "persona" } & PersonaEntity
9
8
  | { type: "personaTopic"; personaId: string } & PersonaTopic
10
- | { type: "personaTrait"; personaId: string } & Trait;
9
+ | { type: "personaTrait"; personaId: string } & PersonaTrait;
11
10
 
12
11
  export function crossFind(
13
12
  id: string,
@@ -18,8 +17,6 @@ export function crossFind(
18
17
  const fact = human.facts.find(f => f.id === id);
19
18
  if (fact) return { type: "fact", ...fact };
20
19
 
21
- const trait = human.traits.find(t => t.id === id);
22
- if (trait) return { type: "trait", ...trait };
23
20
 
24
21
  const person = human.people.find(p => p.id === id);
25
22
  if (person) return { type: "person", ...person };
@@ -0,0 +1,31 @@
1
+ import type { Message } from "../types.js";
2
+
3
+ const DEFAULT_EVENT_WINDOW_HOURS = 8;
4
+
5
+ export function buildEventWindows(
6
+ messages: Message[],
7
+ gapHours: number = DEFAULT_EVENT_WINDOW_HOURS
8
+ ): Message[][] {
9
+ if (messages.length === 0) return [];
10
+
11
+ const gapMs = gapHours * 60 * 60 * 1000;
12
+ const windows: Message[][] = [];
13
+ let currentWindow: Message[] = [messages[0]];
14
+
15
+ for (let i = 1; i < messages.length; i++) {
16
+ const prev = new Date(messages[i - 1].timestamp).getTime();
17
+ const curr = new Date(messages[i].timestamp).getTime();
18
+
19
+ if (curr - prev >= gapMs) {
20
+ windows.push(currentWindow);
21
+ currentWindow = [];
22
+ }
23
+ currentWindow.push(messages[i]);
24
+ }
25
+
26
+ if (currentWindow.length > 0) {
27
+ windows.push(currentWindow);
28
+ }
29
+
30
+ return windows;
31
+ }
@@ -54,9 +54,9 @@ function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage): Message {
54
54
  return {
55
55
  ...convertToEiMessage(msg),
56
56
  f: true,
57
- r: true,
57
+ t: true,
58
58
  p: true,
59
- o: true,
59
+ e: true,
60
60
  };
61
61
  }
62
62
 
@@ -130,7 +130,7 @@ function ensureSessionTopic(
130
130
  exposure_current: 0.5,
131
131
  exposure_desired: 0.3,
132
132
  persona_groups: CLAUDE_CODE_TOPIC_GROUPS,
133
- learned_by: CLAUDE_CODE_PERSONA_NAME,
133
+ learned_by: stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME)?.id ?? undefined,
134
134
  last_updated: new Date().toISOString(),
135
135
  };
136
136
 
@@ -305,7 +305,11 @@ export async function importClaudeCodeSessions(
305
305
  messages_analyze: toAnalyze,
306
306
  };
307
307
 
308
- queueAllScans(context, stateManager);
308
+ const ccSettings = stateManager.getHuman().settings?.claudeCode;
309
+ queueAllScans(context, stateManager, {
310
+ extraction_model: ccSettings?.extraction_model,
311
+ extraction_token_limit: ccSettings?.extraction_token_limit,
312
+ });
309
313
  result.extractionScansQueued += 4;
310
314
  }
311
315
 
@@ -158,6 +158,8 @@ export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
158
158
  export interface ClaudeCodeSettings {
159
159
  integration?: boolean;
160
160
  polling_interval_ms?: number; // Default: 1800000 (30 min)
161
+ extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
162
+ extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
161
163
  last_sync?: string; // ISO timestamp
162
164
  processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
163
165
  }
@@ -57,9 +57,9 @@ function convertToPreMarkedEiMessage(ocMsg: OpenCodeMessage): Message {
57
57
  return {
58
58
  ...convertToEiMessage(ocMsg),
59
59
  f: true,
60
- r: true,
60
+ t: true,
61
61
  p: true,
62
- o: true,
62
+ e: true,
63
63
  };
64
64
  }
65
65
 
@@ -229,7 +229,11 @@ export async function importOpenCodeSessions(
229
229
  };
230
230
 
231
231
  if (!signal?.aborted) {
232
- queueAllScans(context, stateManager);
232
+ const openCodeSettings = stateManager.getHuman().settings?.opencode;
233
+ queueAllScans(context, stateManager, {
234
+ extraction_model: openCodeSettings?.extraction_model,
235
+ extraction_token_limit: openCodeSettings?.extraction_token_limit,
236
+ });
233
237
  result.extractionScansQueued += 4;
234
238
  }
235
239
  }
@@ -29,7 +29,7 @@ interface PromptBuilder<T> {
29
29
 
30
30
  **Rules**:
31
31
  1. **Synchronous** - No async, no fetching
32
- 2. **Pure** - Same input → same output
32
+ 2. **Pure** - Same input → same output. No state reads, no side effects.
33
33
  3. **Pre-processed data** - Processor fetches/filters before calling
34
34
  4. **Minimal logic** - String interpolation, not computation
35
35
 
@@ -60,3 +60,75 @@ interface PromptBuilder<T> {
60
60
  **Prompt engineering lives here. Code logic lives in Processor.**
61
61
 
62
62
  When modifying persona behavior, check prompts first—the "personality" is in the English, not the TypeScript.
63
+
64
+ ---
65
+
66
+ ## VIOLATIONS
67
+
68
+ These are wrong. If you see them, fix them.
69
+
70
+ ### Prompt strings defined outside `src/prompts/`
71
+
72
+ **Violation:**
73
+ ```typescript
74
+ // ❌ In src/core/handlers/something.ts
75
+ const system = `You are an expert at JSON. Return only valid JSON with no commentary.`;
76
+ const user = `Fix this broken JSON: ${badJson}`;
77
+ const response = await llmClient.call({ system, user });
78
+ ```
79
+
80
+ **Correct:** Move the prompt to `src/prompts/[purpose]/index.ts`. The handler calls the builder with pre-fetched data; the builder returns `{ system, user }`.
81
+
82
+ ### Exception: JSON recovery prompt in `queue-processor.ts`
83
+
84
+ There is one deliberate exception to the "all prompts live in `src/prompts/`" rule: the JSON repair retry prompt in `queue-processor.ts`.
85
+
86
+ **Why it's an exception**: This is a *repair heuristic* — it fires after a JSON parse failure to ask the LLM to fix its own malformed output. It has no domain knowledge, no persona data, and no human data. It's infrastructure-level error recovery, not a domain prompt. Moving it to `src/prompts/` would create a degenerate prompt builder with a one-line body and no meaningful data contract.
87
+
88
+ **Criteria for a legitimate exception** (all must be true):
89
+ 1. The prompt contains zero domain knowledge (no persona names, no human data, no Ei concepts)
90
+ 2. It's error recovery or infrastructure glue, not business logic
91
+ 3. Moving it to `src/prompts/` would produce a builder with no real `types.ts` (no input data shape worth naming)
92
+
93
+ If your use case doesn't meet all three criteria, it belongs in `src/prompts/`.
94
+
95
+ ### Prompt builders that do computation
96
+
97
+ **Violation:**
98
+ ```typescript
99
+ // ❌ Prompt builder filtering its own data
100
+ function buildResponsePrompt(data: ResponsePromptData) {
101
+ const relevantFacts = data.facts.filter(f => f.sentiment > 0.5); // ← WRONG
102
+ const recentTopics = data.topics.slice(-5); // ← WRONG
103
+ return { system: `...${relevantFacts}...`, user: `...` };
104
+ }
105
+ ```
106
+
107
+ **Correct:** The Processor filters and slices before calling the builder. The builder receives already-filtered data and does string interpolation only.
108
+
109
+ > If you're writing a loop, a `.filter()`, or a `.map()` inside a prompt builder for anything other than formatting a string — stop. That logic belongs in the Processor or a pre-processing utility.
110
+
111
+ ### Async prompt builders
112
+
113
+ **Violation:**
114
+ ```typescript
115
+ // ❌ Async prompt builder
116
+ async function buildHeartbeatPrompt(personaId: string) {
117
+ const persona = await stateManager.getPersona(personaId); // ← WRONG
118
+ return { system: `...`, user: `...` };
119
+ }
120
+ ```
121
+
122
+ **Correct:** Processor calls `stateManager.getPersona()` first, then passes the result to the synchronous builder.
123
+
124
+ ---
125
+
126
+ ## Prompt Creep Warning
127
+
128
+ Prompt builders are the most-edited files in the codebase. Watch for these failure modes:
129
+
130
+ - **Logic creep**: A builder that started as pure string interpolation slowly accumulates conditionals, date math, or data filtering. Each addition seems small. After six changes it's a 200-line function that requires mocking to test. If you're adding branching logic to a prompt builder — reconsider. Move it to the Processor.
131
+
132
+ - **Data shape expansion**: A builder's input type grows to include things it doesn't actually use in the prompt string. This means the Processor is fetching data the prompt doesn't need. Audit `types.ts` when the data shape grows.
133
+
134
+ - **Responsibility leakage**: A prompt builder that calls another prompt builder, or calls a utility that reads from state, or has a side effect on logging that depends on runtime context. Builders must be standalone: same input, same output, every time, in any order.
@@ -59,10 +59,9 @@ Rules:
59
59
  - The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down.
60
60
  - Descriptions should be concise — ideally under 300 characters, never over 500.
61
61
  - Preserve sentiment, strength, confidence, and other numeric values from the source record where applicable.
62
- - "type" must be one of: "fact", "trait", "topic", "person".
63
- - Topics MUST include "category" — one of: Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project.
62
+ - "type" must be one of: "fact", "topic", "person".
63
+ - Topics MUST include "category" — one of: Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project, Event. For Event topics, the description should be a narrative account of a specific moment, not a general summary.
64
64
  - People MUST include "relationship" — a short label like "coworker", "friend", "mentor", etc.
65
- - Traits MUST include "strength" (0.0-1.0).
66
65
  - Do NOT invent information. Only redistribute what exists in the original record.`;
67
66
 
68
67
  const subjects = data.subjects.map(s => ({
@@ -100,15 +99,6 @@ function buildExistingExamples(): string {
100
99
  "description": "Updated description with incorporated data"
101
100
  }
102
101
 
103
- Trait:
104
- {
105
- "id": "existing-uuid",
106
- "type": "trait",
107
- "name": "Trait Name",
108
- "description": "Updated trait description",
109
- "strength": 0.7
110
- }
111
-
112
102
  Topic:
113
103
  {
114
104
  "id": "existing-uuid",
@@ -137,22 +127,13 @@ function buildNewExamples(): string {
137
127
  "sentiment": 0.0
138
128
  }
139
129
 
140
- Trait:
141
- {
142
- "type": "trait",
143
- "name": "New Trait Name",
144
- "description": "Concise trait description",
145
- "sentiment": 0.0,
146
- "strength": 0.5
147
- }
148
-
149
130
  Topic:
150
131
  {
151
132
  "type": "topic",
152
133
  "name": "New Topic Name",
153
134
  "description": "Concise topic description",
154
135
  "sentiment": 0.0,
155
- "category": "Interest"
136
+ "category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event"
156
137
  }
157
138
 
158
139
  Person:
@@ -1,4 +1,4 @@
1
- import type { Trait, PersonaTopic, DataItemBase } from "../../core/types.js";
1
+ import type { PersonaTrait, PersonaTopic, DataItemBase } from "../../core/types.js";
2
2
 
3
3
  export interface PersonaExpirePromptData {
4
4
  persona_name: string;
@@ -11,7 +11,7 @@ export interface PersonaExpireResult {
11
11
 
12
12
  export interface PersonaExplorePromptData {
13
13
  persona_name: string;
14
- traits: Trait[];
14
+ traits: PersonaTrait[];
15
15
  remaining_topics: PersonaTopic[];
16
16
  recent_conversation_themes: string[];
17
17
  }
@@ -32,7 +32,7 @@ export interface DescriptionCheckPromptData {
32
32
  persona_name: string;
33
33
  current_short_description?: string;
34
34
  current_long_description?: string;
35
- traits: Trait[];
35
+ traits: PersonaTrait[];
36
36
  topics: PersonaTopic[];
37
37
  }
38
38
 
@@ -1,7 +1,7 @@
1
1
  import type { PersonaDescriptionsPromptData, PromptOutput } from "./types.js";
2
- import type { Trait, PersonaTopic } from "../../core/types.js";
2
+ import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
3
3
 
4
- function formatTraitsForPrompt(traits: Trait[]): string {
4
+ function formatTraitsForPrompt(traits: PersonaTrait[]): string {
5
5
  if (traits.length === 0) return "(No traits defined)";
6
6
 
7
7
  return traits.map(t => {
@@ -1,4 +1,4 @@
1
- import type { Trait, PersonaTopic } from "../../core/types.js";
1
+ import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
2
2
 
3
3
  export interface PromptOutput {
4
4
  system: string;
@@ -36,7 +36,7 @@ export interface PersonaGenerationResult {
36
36
  export interface PersonaDescriptionsPromptData {
37
37
  name: string;
38
38
  aliases: string[];
39
- traits: Trait[];
39
+ traits: PersonaTrait[];
40
40
  topics: PersonaTopic[];
41
41
  }
42
42
 
@@ -3,7 +3,7 @@
3
3
  * Based on CONTRACTS.md specifications
4
4
  */
5
5
 
6
- import type { Trait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
6
+ import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
7
7
 
8
8
  /**
9
9
  * Common prompt output structure
@@ -19,7 +19,7 @@ export interface PromptOutput {
19
19
  export interface HeartbeatCheckPromptData {
20
20
  persona: {
21
21
  name: string;
22
- traits: Trait[];
22
+ traits: PersonaTrait[];
23
23
  topics: PersonaTopic[];
24
24
  };
25
25
  human: {
@@ -0,0 +1,122 @@
1
+ import type { PromptOutput, ParticipantContext } from "./types.js";
2
+ import type { Message } from "../../core/types.js";
3
+ import { formatMessagesAsPlaceholders } from "../message-utils.js";
4
+
5
+ function participantContextSection(ctx: ParticipantContext | undefined): string {
6
+ if (!ctx) return "";
7
+ const lines: string[] = ["# Participant Context", "The following may help you understand what themes and moments are meaningful in this conversation.", ""];
8
+ lines.push(`## Persona: ${ctx.persona_name}`);
9
+ if (ctx.persona_description) lines.push(ctx.persona_description);
10
+ lines.push("");
11
+ lines.push("## Human");
12
+ if (ctx.human_name) lines.push(`Name: ${ctx.human_name}`);
13
+ if (ctx.human_age !== undefined) lines.push(`Age: ${ctx.human_age}`);
14
+ lines.push("");
15
+ return lines.join("\n");
16
+ }
17
+
18
+ export interface EventScanPromptData {
19
+ persona_name: string;
20
+ messages_context: Message[];
21
+ messages_analyze: Message[];
22
+ window_start?: string;
23
+ window_end?: string;
24
+ participant_context?: ParticipantContext;
25
+ }
26
+
27
+ export function buildEventScanPrompt(data: EventScanPromptData): PromptOutput {
28
+ if (!data.persona_name) {
29
+ throw new Error("buildEventScanPrompt: persona_name is required");
30
+ }
31
+
32
+ const personaName = data.persona_name;
33
+
34
+ const system = `# Task
35
+
36
+ You are scanning a conversation window to identify EPIC EVENTS worth preserving as long-term memories.
37
+
38
+ An EPIC EVENT is a significant, bounded moment — something either participant would reference months later with recognition. Not a topic. Not a theme. A specific thing that happened.
39
+
40
+ ## The Test
41
+
42
+ Ask yourself: "Would this moment get a section heading in a campaign recap document?"
43
+
44
+ - "The Night We Debugged Beta's CPU" → YES
45
+ - "First session with filesystem access" → YES
46
+ - "The time the health check cached the API response forever" → YES
47
+ - "We talked about AI" → NO (that's a Topic) return empty
48
+ - "Flare asked some questions" → NO (too vague) return empty
49
+ - Normal conversation without a notable arc → NO (not epic) return empty
50
+
51
+ ## What Makes an EPIC EVENT
52
+
53
+ - A conflict encountered and (possibly) resolved
54
+ - A discovery or breakthrough moment
55
+ - A memorable failure or unexpected outcome
56
+ - A significant "first" in the relationship
57
+ - A pivotal decision or turning point
58
+ - A moment either party would say "oh THAT session" about
59
+
60
+ ## What Is NOT an EPIC EVENT
61
+
62
+ - Ongoing themes or interests (those are Topics)
63
+ - Casual check-ins without a notable arc
64
+ - Technical facts without a story around them
65
+ - Anything that's already an ongoing trend rather than a bounded moment
66
+
67
+ ## Output Format
68
+
69
+ \`\`\`json
70
+ {
71
+ "events": [
72
+ {
73
+ "name": "Short evocative label (5-50 characters)",
74
+ "description": "1-2 sentences: what happened, why it mattered. Write it like a friend describing the moment to someone who wasn't there.",
75
+ "reason": "Evidence from the conversation — what made this rise to Epic Event level"
76
+ }
77
+ ]
78
+ }
79
+ \`\`\`
80
+
81
+ Return an empty array if nothing qualifies. An empty array is the most common expected response.
82
+
83
+ **Return JSON only. Be conservative. One memorable moment per window is the norm.**
84
+
85
+ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided for context only — it has already been processed.
86
+
87
+ ${participantContextSection(data.participant_context)}`;
88
+
89
+ const earlierSection = data.messages_context.length > 0
90
+ ? `## Earlier Conversation
91
+ ${formatMessagesAsPlaceholders(data.messages_context, personaName)}
92
+
93
+ `
94
+ : '';
95
+
96
+ const recentSection = `## Most Recent Messages
97
+ ${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
98
+
99
+ const user = `# Conversation Window
100
+ ${earlierSection}${recentSection}
101
+
102
+ ---
103
+
104
+ Scan this conversation window for EPIC EVENTS — specific, memorable moments worth preserving long-term.
105
+
106
+ **Return JSON:**
107
+ \`\`\`json
108
+ {
109
+ "events": [
110
+ {
111
+ "name": "Short evocative label",
112
+ "description": "What happened and why it mattered",
113
+ "reason": "Evidence from the conversation"
114
+ }
115
+ ]
116
+ }
117
+ \`\`\`
118
+
119
+ Return empty array if nothing qualifies. Be conservative.`;
120
+
121
+ return { system, user };
122
+ }
@@ -0,0 +1,106 @@
1
+ import type { FactFindPromptData, PromptOutput } from "./types.js";
2
+ import { formatMessagesAsPlaceholders } from "../message-utils.js";
3
+
4
+ export function buildFactFindPrompt(data: FactFindPromptData): PromptOutput {
5
+ if (!data.persona_name) {
6
+ throw new Error("buildFactFindPrompt: persona_name is required");
7
+ }
8
+ if (!data.missing_fact_names || data.missing_fact_names.length === 0) {
9
+ throw new Error("buildFactFindPrompt: missing_fact_names is required and must not be empty");
10
+ }
11
+
12
+ const personaName = data.persona_name;
13
+
14
+ const taskFragment = `# Task
15
+
16
+ The system is missing some facts about the user. The user is not obligated to EVER provide ANY facts, so it is very likely that these facts are NOT present in this conversation. Your ONLY job is to search for EXPLICIT statements of fact for these SPECIFIC items, and return any matches in the provided JSON format.`;
17
+
18
+ const missingFactsFragment = `## Missing Facts
19
+
20
+ The system is looking for the following facts:
21
+
22
+ ${data.missing_fact_names.map(name => `- ${name}`).join('\n')}
23
+
24
+ Again - 99.99999% of the time, you will return no data — don't try to force it.`;
25
+
26
+ const guidelinesFragment = `# Guidelines
27
+
28
+ 1. **Explicitness:**
29
+ * **Focus only on what the user *explicitly states*.** Do not infer, assume, or guess based on context or general knowledge.
30
+ * **Prioritize direct statements.** "I was born in 1985" is a fact. "I feel old now that it's 3030" isn't an explicit statement of their birth year.
31
+ 2. **Objectivity and Verifiability:**
32
+ * **Facts are objective and generally verifiable.** They are not subjective opinions, feelings, or temporary states.
33
+ * **Focus on unchangeable or enduring attributes/events.**
34
+ 3. **Specificity over Generality:**
35
+ * If the user says "I live in a big city," do not extract "Current Location: big city." If they say "I live in New York," extract "Current Location: New York."
36
+ 4. **Avoid Inference:**
37
+ * If a user talks extensively about cooking, it's an interest, not a Fact like "Current Job Title: Chef" unless they explicitly state they ARE a chef.
38
+ 5. **CRITICAL - Entity Attribution:**
39
+ * ONLY extract facts about THE HUMAN USER THEMSELVES, not facts about other people they mention.
40
+ * **Extract**: "I was born in 1984" → User's birthday
41
+ * **Extract**: "I'm a software engineer" → User's job
42
+ * **DO NOT Extract**: "My wife was a theater major" → This is about the wife, NOT the user
43
+ * **DO NOT Extract**: "My daughter is 10 years old" → This is about the daughter, NOT the user
44
+ * **DO NOT Extract**: "My brother lives in Texas" → This is about the brother, NOT the user
45
+ * If the user shares information about someone else, that is NOT a fact about the user.`;
46
+
47
+ const criticalFragment = `# CRITICAL INSTRUCTIONS
48
+
49
+ ONLY ANALYZE the "Most Recent Messages" in the following conversation. The "Earlier Conversation" is provided for your context and has already been processed!
50
+
51
+ The JSON format is:
52
+
53
+ \`\`\`json
54
+ {
55
+ "facts": [
56
+ {
57
+ "name": "One of the missing fact names from above",
58
+ "value": "The exact value of the fact",
59
+ "evidence": "Direct quote or reference showing where this fact was stated"
60
+ }
61
+ ]
62
+ }
63
+ \`\`\`
64
+
65
+ **Return JSON only.**`;
66
+
67
+ const system = `${taskFragment}
68
+
69
+ ${missingFactsFragment}
70
+
71
+ ${guidelinesFragment}
72
+
73
+ ${criticalFragment}`;
74
+
75
+ const earlierSection = data.messages_context.length > 0
76
+ ? `## Earlier Conversation
77
+ ${formatMessagesAsPlaceholders(data.messages_context, personaName)}
78
+
79
+ `
80
+ : '';
81
+
82
+ const recentSection = `## Most Recent Messages
83
+ ${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
84
+
85
+ const user = `# Conversation
86
+ ${earlierSection}${recentSection}
87
+
88
+ ---
89
+
90
+ Scan the "Most Recent Messages" for FACTS about the human user.
91
+
92
+ **Return JSON:**
93
+ \`\`\`json
94
+ {
95
+ "facts": [
96
+ {
97
+ "name": "One of the missing fact names from above",
98
+ "value": "The exact value of the fact",
99
+ "evidence": "Direct quote or reference showing where this fact was stated"
100
+ }
101
+ ]
102
+ }
103
+ \`\`\``;
104
+
105
+ return { system, user };
106
+ }
@@ -75,8 +75,6 @@ Your job is to quickly identify:
75
75
  > They are details OF facts (e.g., { "type_of_fact": "Birthday", "value_of_fact": "August 15th" }).
76
76
 
77
77
  **FACTS ARE NOT**
78
- - Trait: Personality patterns, communication style, behavioral tendencies
79
- - These are tracked separately
80
78
  - General Topic: Interests, hobbies, general subjects
81
79
  - These are tracked separately
82
80
  - Relationships: Wife, Husband, Daughter, Son, etc.
@@ -1,32 +1,39 @@
1
1
  export { buildHumanFactScanPrompt } from "./fact-scan.js";
2
- export { buildHumanTraitScanPrompt } from "./trait-scan.js";
2
+ export { buildFactFindPrompt } from "./fact-find.js";
3
3
  export { buildHumanTopicScanPrompt } from "./topic-scan.js";
4
4
  export { buildHumanPersonScanPrompt } from "./person-scan.js";
5
- export { buildHumanItemMatchPrompt } from "./item-match.js";
6
- export { buildHumanItemUpdatePrompt } from "./item-update.js";
5
+ export { buildTopicMatchPrompt } from "./topic-match.js";
6
+ export { buildTopicUpdatePrompt } from "./topic-update.js";
7
+ export { buildPersonMatchPrompt } from "./person-match.js";
8
+ export { buildPersonUpdatePrompt } from "./person-update.js";
9
+ export { buildEventScanPrompt } from "./event-scan.js";
10
+ export type { EventScanPromptData } from "./event-scan.js";
11
+
12
+ export type { TopicMatchPromptData } from "./topic-match.js";
13
+ export type { TopicUpdatePromptData } from "./topic-update.js";
14
+ export type { PersonMatchPromptData } from "./person-match.js";
15
+ export type { PersonUpdatePromptData } from "./person-update.js";
7
16
 
8
17
  export type {
9
18
  PromptOutput,
19
+ ParticipantContext,
10
20
  FactScanPromptData,
11
- TraitScanPromptData,
12
21
  TopicScanPromptData,
13
22
  PersonScanPromptData,
23
+ FactFindPromptData,
14
24
  FactScanCandidate,
15
- TraitScanCandidate,
16
25
  TopicScanCandidate,
17
26
  PersonScanCandidate,
18
27
  FactScanResult,
19
- TraitScanResult,
28
+ FactFindResult,
20
29
  TopicScanResult,
21
30
  PersonScanResult,
22
- ItemMatchPromptData,
31
+ EventScanCandidate,
32
+ EventScanResult,
23
33
  ItemMatchResult,
24
- ItemUpdatePromptData,
25
34
  ExposureImpact,
26
35
  ItemUpdateResultBase,
27
36
  FactUpdateResult,
28
- TraitUpdateResult,
29
- TopicUpdateResult,
30
37
  PersonUpdateResult,
31
38
  ItemUpdateResult,
32
39
  } from "./types.js";