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.
- package/README.md +42 -0
- package/package.json +1 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +4 -5
- package/src/cli/retrieval.ts +3 -25
- package/src/cli.ts +3 -7
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +10 -16
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +95 -30
- package/src/core/handlers/human-matching.ts +326 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -29
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +5 -27
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +50 -39
- package/src/core/orchestrators/dedup-phase.ts +0 -1
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +99 -17
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +7 -8
- package/src/core/types/data-items.ts +2 -4
- package/src/core/types/entities.ts +6 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +8 -4
- package/src/integrations/claude-code/types.ts +2 -0
- package/src/integrations/opencode/importer.ts +7 -3
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/rewrite.ts +3 -22
- package/src/prompts/ceremony/types.ts +3 -3
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/commands/me.tsx +5 -14
- package/tui/src/commands/settings.tsx +15 -0
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/util/yaml-serializers.ts +48 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { HumanEntity, PersonaEntity, Fact,
|
|
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 } &
|
|
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
|
-
|
|
57
|
+
t: true,
|
|
58
58
|
p: true,
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
t: true,
|
|
61
61
|
p: true,
|
|
62
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/prompts/AGENTS.md
CHANGED
|
@@ -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", "
|
|
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 {
|
|
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:
|
|
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:
|
|
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 {
|
|
2
|
+
import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
|
|
3
3
|
|
|
4
|
-
function formatTraitsForPrompt(traits:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
6
|
-
export {
|
|
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
|
-
|
|
28
|
+
FactFindResult,
|
|
20
29
|
TopicScanResult,
|
|
21
30
|
PersonScanResult,
|
|
22
|
-
|
|
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";
|