ei-tui 0.1.24 → 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 +34 -14
- 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 +60 -46
- package/src/core/orchestrators/dedup-phase.ts +11 -5
- 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 +113 -22
- 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/dedup.ts +41 -7
- 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/app.tsx +7 -5
- package/tui/src/commands/archive.tsx +2 -2
- package/tui/src/commands/context.tsx +3 -4
- package/tui/src/commands/delete.tsx +4 -4
- package/tui/src/commands/dlq.ts +3 -4
- package/tui/src/commands/help.tsx +1 -1
- package/tui/src/commands/me.tsx +8 -18
- package/tui/src/commands/persona.tsx +2 -2
- package/tui/src/commands/provider.tsx +3 -5
- package/tui/src/commands/queue.ts +3 -4
- package/tui/src/commands/quotes.tsx +6 -8
- package/tui/src/commands/registry.ts +1 -1
- package/tui/src/commands/setsync.tsx +2 -2
- package/tui/src/commands/settings.tsx +18 -4
- package/tui/src/commands/spotify-auth.ts +0 -1
- package/tui/src/commands/tools.tsx +4 -5
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/context/overlay.tsx +17 -6
- package/tui/src/util/editor.ts +22 -11
- package/tui/src/util/persona-editor.tsx +6 -8
- package/tui/src/util/provider-editor.tsx +6 -8
- package/tui/src/util/toolkit-editor.tsx +3 -4
- 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
package/src/core/types/enums.ts
CHANGED
|
@@ -9,11 +9,6 @@ export enum ContextStatus {
|
|
|
9
9
|
Never = "never",
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export enum ValidationLevel {
|
|
13
|
-
None = "none", // Fresh data, never acknowledged
|
|
14
|
-
Ei = "ei", // Ei mentioned it to user (don't mention again)
|
|
15
|
-
Human = "human", // User explicitly confirmed (locked)
|
|
16
|
-
}
|
|
17
12
|
export enum LLMRequestType {
|
|
18
13
|
Response = "response",
|
|
19
14
|
JSON = "json",
|
|
@@ -30,12 +25,13 @@ export enum LLMNextStep {
|
|
|
30
25
|
HandlePersonaResponse = "handlePersonaResponse",
|
|
31
26
|
HandlePersonaGeneration = "handlePersonaGeneration",
|
|
32
27
|
HandlePersonaDescriptions = "handlePersonaDescriptions",
|
|
33
|
-
|
|
34
|
-
HandleHumanTraitScan = "handleHumanTraitScan",
|
|
28
|
+
HandleFactFind = "handleFactFind",
|
|
35
29
|
HandleHumanTopicScan = "handleHumanTopicScan",
|
|
36
30
|
HandleHumanPersonScan = "handleHumanPersonScan",
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
HandleTopicMatch = "handleTopicMatch",
|
|
32
|
+
HandleTopicUpdate = "handleTopicUpdate",
|
|
33
|
+
HandlePersonMatch = "handlePersonMatch",
|
|
34
|
+
HandlePersonUpdate = "handlePersonUpdate",
|
|
39
35
|
HandlePersonaTraitExtraction = "handlePersonaTraitExtraction",
|
|
40
36
|
HandlePersonaTopicScan = "handlePersonaTopicScan",
|
|
41
37
|
HandlePersonaTopicMatch = "handlePersonaTopicMatch",
|
|
@@ -54,6 +50,7 @@ export enum LLMNextStep {
|
|
|
54
50
|
HandleRewriteScan = "handleRewriteScan",
|
|
55
51
|
HandleRewriteRewrite = "handleRewriteRewrite",
|
|
56
52
|
HandleDedupCurate = "handleDedupCurate",
|
|
53
|
+
HandleEventScan = "handleEventScan",
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
export enum ProviderType {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -18,9 +18,9 @@ export interface Message {
|
|
|
18
18
|
// Extraction completion flags (omit when false to save space)
|
|
19
19
|
// Single-letter names minimize storage overhead for large message histories
|
|
20
20
|
f?: boolean; // Fact extraction completed
|
|
21
|
-
|
|
21
|
+
t?: boolean; // Topic extraction completed
|
|
22
22
|
p?: boolean; // Person extraction completed
|
|
23
|
-
|
|
23
|
+
e?: boolean; // Event (epic) extraction completed
|
|
24
24
|
// Image generation fields (web-only, ephemeral)
|
|
25
25
|
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
26
|
|
|
@@ -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.
|
|
@@ -15,22 +15,56 @@ import type { DedupPromptData } from "./types.js";
|
|
|
15
15
|
export function buildDedupPrompt(data: DedupPromptData): { system: string; user: string } {
|
|
16
16
|
const typeLabel = data.itemType.charAt(0).toUpperCase() + data.itemType.slice(1);
|
|
17
17
|
|
|
18
|
-
const system =
|
|
18
|
+
const system = `## HARD RULES (Non-Negotiable — Override All Other Instructions)
|
|
19
|
+
|
|
20
|
+
You are working with Opus 4.6 constraints. These rules prevent overthinking and ensure decisive action:
|
|
21
|
+
|
|
22
|
+
### 1. TOOL BUDGET
|
|
23
|
+
- You have **6 \`read_memory\` calls** for this cluster
|
|
24
|
+
- Prioritize: verify ambiguous relationships > check parent concepts > validate new entities
|
|
25
|
+
- After 6 calls, make decisions with available information
|
|
26
|
+
- Do NOT waste calls re-checking pairs you already examined
|
|
27
|
+
|
|
28
|
+
### 2. SATISFICING MODE (Good Enough > Perfect)
|
|
29
|
+
- If two items share **85%+ semantic similarity** on core meaning → merge them
|
|
30
|
+
- Do NOT re-examine after deciding to merge
|
|
31
|
+
- Do NOT explore alternative groupings
|
|
32
|
+
- First valid match wins — stop searching for "better" options
|
|
33
|
+
|
|
34
|
+
### 3. FORBIDDEN PATTERNS (Signs of Overthinking)
|
|
35
|
+
If you find yourself writing these phrases, **STOP IMMEDIATELY**:
|
|
36
|
+
- ❌ "On the other hand..." / "However, there's another angle..."
|
|
37
|
+
- ❌ "Let me reconsider..." / "But what if..."
|
|
38
|
+
- ❌ "This could be interpreted as..."
|
|
39
|
+
- ❌ Re-analyzing the same pair after making a decision
|
|
40
|
+
|
|
41
|
+
Output format when you catch overthinking:
|
|
42
|
+
\`\`\`
|
|
43
|
+
[OVERTHINKING DETECTED]
|
|
44
|
+
Decision: [Yes/No to merge]
|
|
45
|
+
Reason: [1 sentence]
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## YOUR TASK
|
|
51
|
+
|
|
52
|
+
You are acting as the curator for a user's internal database. You have been given a cluster of ${typeLabel} records that our system believes may be duplicates (based on semantic similarity >= 0.90).
|
|
19
53
|
|
|
20
54
|
**YOUR PRIME DIRECTIVE IS TO LOSE _NO_ DATA.**
|
|
21
55
|
|
|
22
56
|
Your secondary directive is to ORGANIZE IT into small, non-repetitive components. The user NEEDS the data, but the data is used by AI agents, so duplication limits usefulness—agents waste tokens re-reading the same information under different names.
|
|
23
57
|
|
|
24
|
-
You have access to a tool called \`read_memory\`
|
|
58
|
+
You have access to a tool called \`read_memory\` (6 calls max — see HARD RULES above). Use it strategically to verify relationships, check for related records, or gather context before making merge decisions.
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
1. **Identify true duplicates**: Examine each record. Are these genuinely the same thing with different wording, or are they distinct but related concepts?
|
|
60
|
+
### Decision Process:
|
|
61
|
+
1. **Identify true duplicates**: Examine each record. Are these genuinely the same thing with different wording (85%+ core meaning overlap), or are they distinct but related concepts?
|
|
28
62
|
2. **Merge where appropriate**: For TRUE duplicates, consolidate all unique information into ONE canonical record. Pick the best "name" (most descriptive, most commonly used). Merge all descriptions—every unique detail must be preserved.
|
|
29
63
|
3. **Keep distinct concepts separate**: Similar ≠ duplicate. "Software Engineering" and "Software Architecture" may be related but are NOT the same. "Job at Company X" and "Profession: Software Engineer" are related but distinct. Do NOT merge these.
|
|
30
64
|
4. **Track what was merged**: For removed records, indicate which record absorbed their data (via "replaced_by" field).
|
|
31
65
|
5. **Add new records if needed**: If consolidating reveals a MISSING intermediate concept (e.g., merging "Python Developer" and "Backend Engineer" reveals we're missing "Software Engineering" as a parent topic), create it.
|
|
32
66
|
|
|
33
|
-
|
|
67
|
+
### Output Format:
|
|
34
68
|
{
|
|
35
69
|
"update": [
|
|
36
70
|
/* Full ${typeLabel} record payloads with all fields preserved */
|
|
@@ -53,14 +87,14 @@ Record format for "${typeLabel}" (based on type):
|
|
|
53
87
|
|
|
54
88
|
${buildRecordFormatExamples(data.itemType)}
|
|
55
89
|
|
|
56
|
-
Rules:
|
|
90
|
+
### Rules:
|
|
57
91
|
- Do NOT invent information. Only redistribute what exists in the cluster.
|
|
58
92
|
- Descriptions should be concise—ideally under 300 characters, never over 500.
|
|
59
93
|
- Preserve all numeric values (sentiment, strength, confidence, exposure, etc.) from source records. When merging, take the HIGHER value for strength/confidence, AVERAGE for sentiment.
|
|
60
94
|
- Every removed record MUST have "replaced_by" pointing to the canonical record that absorbed its data.
|
|
61
95
|
- The "update" array should contain AT LEAST ONE record (the canonical/merged one), even if all others are removed.
|
|
62
96
|
- If records are NOT duplicates (just similar), return them ALL in "update" unchanged, with empty "remove" and "add" arrays.
|
|
63
|
-
- Use \`read_memory\` to check for related records or gather context before making irreversible merge decisions.`;
|
|
97
|
+
- Use \`read_memory\` strategically (6 calls max) to check for related records or gather context before making irreversible merge decisions.`;
|
|
64
98
|
|
|
65
99
|
const user = JSON.stringify({
|
|
66
100
|
cluster: data.cluster.map(stripEmbedding),
|
|
@@ -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
|
+
}
|