ei-tui 0.9.4 → 1.0.1

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 (58) hide show
  1. package/README.md +22 -3
  2. package/package.json +5 -1
  3. package/src/README.md +9 -25
  4. package/src/core/handlers/document-segmentation.ts +113 -0
  5. package/src/core/handlers/human-extraction.ts +16 -16
  6. package/src/core/handlers/index.ts +2 -0
  7. package/src/core/handlers/rewrite.ts +13 -9
  8. package/src/core/heartbeat-manager.ts +2 -2
  9. package/src/core/llm-client.ts +66 -6
  10. package/src/core/message-manager.ts +20 -18
  11. package/src/core/orchestrators/ceremony.ts +83 -40
  12. package/src/core/orchestrators/human-extraction.ts +5 -1
  13. package/src/core/persona-manager.ts +4 -0
  14. package/src/core/processor.ts +90 -1
  15. package/src/core/queue-manager.ts +35 -0
  16. package/src/core/queue-processor.ts +13 -13
  17. package/src/core/state/queue.ts +9 -1
  18. package/src/core/state-manager.ts +10 -6
  19. package/src/core/types/entities.ts +15 -0
  20. package/src/core/types/enums.ts +1 -0
  21. package/src/core/types/integrations.ts +2 -0
  22. package/src/core/types/llm.ts +9 -0
  23. package/src/integrations/document/chunker.ts +88 -0
  24. package/src/integrations/document/importer.ts +82 -0
  25. package/src/integrations/document/index.ts +2 -0
  26. package/src/integrations/document/invoice.ts +63 -0
  27. package/src/integrations/document/types.ts +16 -0
  28. package/src/integrations/document/unsource.ts +164 -0
  29. package/src/integrations/persona-history/importer.ts +197 -0
  30. package/src/integrations/persona-history/index.ts +3 -0
  31. package/src/integrations/persona-history/types.ts +7 -0
  32. package/src/prompts/ceremony/dedup.ts +7 -3
  33. package/src/prompts/ceremony/index.ts +2 -1
  34. package/src/prompts/ceremony/people-rewrite.ts +190 -0
  35. package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
  36. package/src/prompts/human/person-scan.ts +13 -4
  37. package/src/prompts/human/topic-scan.ts +16 -2
  38. package/src/prompts/human/topic-update.ts +36 -4
  39. package/src/prompts/human/types.ts +1 -0
  40. package/src/storage/indexed.ts +4 -0
  41. package/src/storage/interface.ts +1 -0
  42. package/src/storage/local.ts +4 -0
  43. package/src/templates/emmett.ts +49 -0
  44. package/tui/README.md +25 -2
  45. package/tui/src/app.tsx +9 -6
  46. package/tui/src/commands/delete.tsx +7 -1
  47. package/tui/src/commands/import.tsx +30 -0
  48. package/tui/src/commands/unsource.tsx +115 -0
  49. package/tui/src/components/PromptInput.tsx +4 -0
  50. package/tui/src/components/WelcomeOverlay.tsx +58 -32
  51. package/tui/src/context/ei.tsx +80 -60
  52. package/tui/src/index.tsx +14 -0
  53. package/tui/src/storage/file.ts +11 -5
  54. package/tui/src/util/e2e-flags.ts +4 -3
  55. package/tui/src/util/help-content.ts +20 -0
  56. package/tui/src/util/logger.ts +1 -1
  57. package/tui/src/util/provider-detection.ts +251 -0
  58. package/tui/src/util/yaml-human.ts +7 -1
@@ -89,7 +89,7 @@ ${buildRecordFormatExamples(data.itemType)}
89
89
 
90
90
  ### Rules:
91
91
  - Do NOT invent information. Only redistribute what exists in the cluster.
92
- - Descriptions should be concise—ideally under 300 characters, never over 500.
92
+ - Descriptions should be concise ideally under 300 characters, never over 500 for regular topics. Technical topics (category: "Technical") may go up to 900 characters — preserve their specific gotchas, decisions, and open questions.
93
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.
94
94
  - Every removed record MUST have "replaced_by" pointing to the canonical record that absorbed its data.
95
95
  - The "update" array should contain AT LEAST ONE record (the canonical/merged one), even if all others are removed.
@@ -165,6 +165,8 @@ Similarity of meaning is not the same as identity. "Concern about job security"
165
165
 
166
166
  Ask yourself: *If a persona referenced the established record in conversation, would the newcomer feel like a repeat? Or would it feel like something different being said?*
167
167
 
168
+ **Default to keeping both.** Merge only when you are certain these describe the same concept — thematic overlap, shared vocabulary, or similar domain are not sufficient. A false merge destroys information permanently; a false keep is harmless.
169
+
168
170
  If they are the same thing: **merge**. Preserve every unique detail from both. The newcomer's description is synthesized and current — weight it, but don't discard what the established record learned first.
169
171
 
170
172
  If they are distinct: **keep both**. Return them both in \`update\` unchanged. Leave \`remove\` and \`add\` empty.
@@ -183,7 +185,8 @@ Rules:
183
185
  - \`add\` is always empty here. We are not creating new records from this decision.
184
186
  - If merging: the merged record goes in \`update\`, the absorbed record goes in \`remove\`.
185
187
  - If keeping both: return both in \`update\` exactly as received. Do not modify either.
186
- - Descriptions must stay concise — under 300 characters, never over 500. Synthesize; don't concatenate.
188
+ - Descriptions must stay concise — under 300 characters, never over 500 for regular topics. **Technical topics** (category: "Technical") may go up to 900 characters — they are knowledge bases, not summaries. Synthesize regular topics; preserve detail in Technical ones.
189
+ - For Technical topics: two records about the same technology but different aspects (e.g., "Uniform composition model" vs "Uniform preview setup") are **NOT duplicates** — keep both. Only merge if they are genuinely the same concept described twice.
187
190
  - When merging numeric fields: take the HIGHER value for \`exposure_current\`, \`exposure_desired\`, \`strength\`, \`confidence\`. Average \`sentiment\`.
188
191
  - Do NOT invent information. Only what exists in these two records.
189
192
 
@@ -297,7 +300,7 @@ function buildTopicExamples(): string {
297
300
  "name": "Software Architecture", // REQUIRED
298
301
  "description": "System design patterns, microservices, event-driven architecture. Passionate about scalability and maintainability.", // REQUIRED
299
302
  "sentiment": 0.8, // -1.0 to 1.0 (average when merging)
300
- "category": "Interest", // REQUIRED - Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project (pick most common)
303
+ "category": "Interest", // REQUIRED - Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project, Event, Technical (pick most common)
301
304
  "exposure_current": 0.6, // 0.0 to 1.0, how recently discussed (take HIGHER when merging)
302
305
  "exposure_desired": 0.9, // 0.0 to 1.0, how much they want to discuss (take HIGHER when merging)
303
306
  "last_ei_asked": "2024-03-10T08:00:00Z", // OPTIONAL - ISO timestamp or null
@@ -330,6 +333,7 @@ CATEGORIES explained:
330
333
  - Goal: Things they want to achieve
331
334
  - Concern/Fear: Things that worry them
332
335
  - Plan/Project: Active work or intentions
336
+ - Technical: Tools, platforms, frameworks, or technical concepts being learned or used — knowledge base entries, NOT summaries
333
337
 
334
338
  GOOD vs BAD descriptions:
335
339
  ✅ GOOD: "Functional programming paradigm. Loves immutability and pure functions. Uses in side projects."
@@ -1,4 +1,5 @@
1
- export { buildRewriteScanPrompt, buildRewritePrompt } from "./rewrite.js";
1
+ export { buildPersonRewriteScanPrompt, buildPersonRewriteSplitPrompt } from "./people-rewrite.js";
2
+ export { buildTopicRewriteScanPrompt, buildTopicRewriteSplitPrompt } from "./topic-rewrite.js";
2
3
  export { buildDedupPrompt, buildValidatePrompt } from "./dedup.js";
3
4
  export { buildUserDedupPrompt } from "./user-dedup.js";
4
5
  export type {
@@ -0,0 +1,190 @@
1
+ import type { RewriteScanPromptData, RewritePromptData } from "./types.js";
2
+
3
+ // =============================================================================
4
+ // What belongs in a Person record (shared reference for both prompts)
5
+ // =============================================================================
6
+ //
7
+ // A Person record is a RELATIONSHIP PROFILE — who this person is, how they
8
+ // relate to the human user, and anything a persona would use to meaningfully
9
+ // reference them in conversation 6+ months from now.
10
+ //
11
+ // A Person record is NOT:
12
+ // - A project status log
13
+ // - A record of ticket numbers, PR numbers, or sprint assignments
14
+ // - A biography of their personal habits and hobbies
15
+ // - A shared-interest tracker (those are Topics)
16
+ //
17
+ // The test: "Would this still be true and useful if you ran into this person
18
+ // at a coffee shop, unrelated to any current project?"
19
+
20
+ const PERSON_CONTRACT = `A Person record is a **relationship profile** — who this person IS, how they relate to the human user, their character and communication style, and anything that makes them recognizable across time and context.
21
+
22
+ It is NOT:
23
+ - A project status log (ticket numbers, PR references, sprint assignments)
24
+ - A record of shared interests that could stand alone as a Topic
25
+ - Personal biography unrelated to the relationship (commute, hobbies, hometown)
26
+ - Technical knowledge attributed to them rather than about them
27
+
28
+ **The test**: Would this detail still be true and useful if you ran into this person at a coffee shop, unrelated to any current project, in six months?`;
29
+
30
+ // =============================================================================
31
+ // PHASE 1: SCAN — Identify subjects that don't belong in a Person record
32
+ // =============================================================================
33
+
34
+ export function buildPersonRewriteScanPrompt(data: RewriteScanPromptData): { system: string; user: string } {
35
+ const system = `You are auditing a Person record in a personal knowledge base.
36
+
37
+ ${PERSON_CONTRACT}
38
+
39
+ Your job: identify **subjects buried in this description that fail the test above**.
40
+
41
+ For each subject that doesn't belong, return a short phrase (3-8 words) that describes it — specific enough to search for matching records. These phrases will be used to find existing Topics this content might belong in.
42
+
43
+ Rules:
44
+ - Do NOT include the relationship profile itself — who they are, their role, how you know them, their character
45
+ - Be specific: "React performance patterns" beats "technical stuff"
46
+ - If the record is clean — everything in it passes the test — return an empty array
47
+
48
+ Return a raw JSON array of strings. No markdown fencing, no commentary.
49
+
50
+ Example — a Person named "Nicholas" whose description includes sprint ticket numbers:
51
+ ["CMIDP sprint ticket assignments", "ASU Data Lake access provisioning details"]`;
52
+
53
+ const payload = JSON.stringify({
54
+ name: (data.item as { name?: string }).name,
55
+ description: data.item.description,
56
+ relationship: (data.item as { relationship?: string }).relationship,
57
+ }, null, 2);
58
+
59
+ const user = `${payload}
60
+
61
+ ---
62
+
63
+ Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean.`;
64
+
65
+ return { system, user };
66
+ }
67
+
68
+ // =============================================================================
69
+ // PHASE 2: SPLIT — Slim the Person, redistribute subjects to Topics
70
+ // =============================================================================
71
+
72
+ function buildPersonExistingExample(): string {
73
+ return `{
74
+ "id": "existing-uuid",
75
+ "type": "person",
76
+ "name": "Nicholas",
77
+ "description": "Backend engineer on the CMIDP team. Thoughtful code reviewer who flags architectural concerns — specifically around concurrency and queue isolation. Direct point of contact for Data Lake access provisioning.",
78
+ "relationship": "coworker"
79
+ }`;
80
+ }
81
+
82
+ function buildPersonNewTopicExample(): string {
83
+ return `{
84
+ "type": "topic",
85
+ "name": "CMIDP Sprint 86 work",
86
+ "description": "Nicholas owns 4 tickets in Sprint 86 including course list ordering bugs (CMIDP-2604, CMIDP-2441, CMIDP-2686) and course sequencing (CMIDP-2624).",
87
+ "sentiment": 0.5,
88
+ "category": "Project"
89
+ }`;
90
+ }
91
+
92
+ export function buildPersonRewriteSplitPrompt(data: RewritePromptData): { system: string; user: string } {
93
+ const system = `You are reorganizing a Person record in a personal knowledge base.
94
+
95
+ ${PERSON_CONTRACT}
96
+
97
+ An earlier scan identified subjects in this Person record that don't belong there. For each subject, we searched the knowledge base for existing Topics that might already cover it.
98
+
99
+ Your job:
100
+ 1. **Slim the Person** — remove the identified subjects AND any other content that fails the relationship profile test (personal trivia, lifestyle details, biographical facts unrelated to the relationship). Keep only: who they are, their role, their character, how the human user knows and works with them.
101
+ 2. **Redistribute each identified subject** — if a matching Topic exists in the search results, move the content there. If not, create a new Topic.
102
+ 3. **Discard what isn't worth a Topic** — personal trivia (hobbies, commute, hometown) that has no standalone value doesn't need to become a Topic. Just remove it from the Person.
103
+ 4. **Lose NO relationship data** — everything about how this person relates to the human user must survive.
104
+
105
+ Record format for the Person (MUST keep "id", type stays "person"):
106
+ ${buildPersonExistingExample()}
107
+
108
+ Record format for a new Topic created from extracted content:
109
+ ${buildPersonNewTopicExample()}
110
+
111
+ Rules:
112
+ - The original Person record (id: "${data.item.id}") MUST appear in "existing", slimmed down
113
+ - Person description after slimming: 2-4 sentences, relationship profile only. **If it still contains city, commute, hobbies, or lifestyle details after slimming — remove them.** Those are not relationship data.
114
+ - Topics created from person content: use the most appropriate category (Technical, Project, Interest, etc.)
115
+ - People MUST include "relationship"
116
+ - Topics MUST include "category"
117
+ - Do NOT invent information — only redistribute what exists in the original record
118
+ - Do NOT remove the person's relationship, role, character, or how the human user knows them — only the non-person content
119
+
120
+ **What to KEEP in the Person description**: role, expertise, *why* the human user works with them (their operational function in the relationship), how they communicate, character traits, how the human user knows them.
121
+ **What to REMOVE from the Person description**: current project status, ticket/PR numbers, shared interests (→ Topic), city/commute/hobbies (→ discard).
122
+
123
+ The distinction:
124
+ - "Data Lake bucket owner responsible for access provisioning" → KEEP (operational role in the relationship)
125
+ - "Currently owns 4 tickets in Sprint 86" → REMOVE (current sprint status, not who they are)
126
+ - "Left detailed comments on PR #1644 identifying architectural concerns around concurrency" → KEEP the insight, DROP the PR reference: "Flags architectural concerns around concurrency and queue isolation" belongs in the description; "PR #1644" does not.
127
+
128
+ Return raw JSON with exactly two keys:
129
+ {
130
+ "existing": [ /* slimmed Person + any existing Topics being updated */ ],
131
+ "new": [ /* new Topics for subjects with no existing match */ ]
132
+ }
133
+
134
+ No markdown fencing, no commentary.`;
135
+
136
+ const subjects = data.subjects.map(s => ({
137
+ search_term: s.searchTerm,
138
+ matches: s.matches.map(m => ({
139
+ id: (m as { id?: string }).id,
140
+ name: (m as { name?: string }).name,
141
+ description: m.description,
142
+ category: (m as { category?: string }).category,
143
+ })),
144
+ }));
145
+
146
+ const payload = JSON.stringify({
147
+ original_person: {
148
+ id: data.item.id,
149
+ name: (data.item as { name?: string }).name,
150
+ description: data.item.description,
151
+ relationship: (data.item as { relationship?: string }).relationship,
152
+ sentiment: data.item.sentiment,
153
+ },
154
+ subjects_to_extract: subjects,
155
+ }, null, 2);
156
+
157
+ const schemaReminder = `**Return JSON:**
158
+ \`\`\`json
159
+ {
160
+ "existing": [
161
+ {
162
+ "id": "uuid-of-person",
163
+ "type": "person",
164
+ "name": "Person Name",
165
+ "description": "Slimmed relationship profile only",
166
+ "relationship": "coworker"
167
+ }
168
+ ],
169
+ "new": [
170
+ {
171
+ "type": "topic",
172
+ "name": "Subject Name",
173
+ "description": "Content extracted from person record",
174
+ "sentiment": 0.5,
175
+ "category": "Project|Technical|Interest|etc."
176
+ }
177
+ ]
178
+ }
179
+ \`\`\`
180
+
181
+ Return raw JSON only.`;
182
+
183
+ const user = `${payload}
184
+
185
+ ---
186
+
187
+ ${schemaReminder}`;
188
+
189
+ return { system, user };
190
+ }
@@ -1,23 +1,41 @@
1
1
  import type { RewriteScanPromptData, RewritePromptData } from "./types.js";
2
2
 
3
3
  // =============================================================================
4
- // PHASE 1: SCAN — Identify distinct subjects in a bloated item
4
+ // PHASE 1: SCAN — Identify subjects that don't belong in a Topic record
5
5
  // =============================================================================
6
6
 
7
- export function buildRewriteScanPrompt(data: RewriteScanPromptData): { system: string; user: string } {
8
- const typeLabel = data.itemType.charAt(0).toUpperCase() + data.itemType.slice(1);
7
+ function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "embedding"> {
8
+ const { embedding: _, ...rest } = item;
9
+ return rest as Omit<T, "embedding">;
10
+ }
9
11
 
10
- const system = `You are auditing a personal knowledge base. A single ${typeLabel} record has grown too large because unrelated information was repeatedly appended to it over time. The record's Name suggests its intended subject, but its Description now covers many additional, unrelated subjects.
12
+ export function buildTopicRewriteScanPrompt(data: RewriteScanPromptData): { system: string; user: string } {
13
+ const isTechnical = (data.item as { category?: string }).category === "Technical";
11
14
 
12
- Your job: identify the **additional** subjects buried in this record that do NOT belong under the record's Name.
15
+ const technicalGuidance = isTechnical
16
+ ? `
17
+ ## Technical Topic Guidance
13
18
 
14
- Rules:
15
- - Do NOT include the record's primary subject (what its Name describes) — only the extra, unrelated subjects.
16
- - Each subject should be a succinct phrase (2-8 words) that could serve as a search query.
17
- - Be specific. "Technical preferences" is too vague. "TypeScript coding conventions" is better.
18
- - If the record is actually cohesive and on-topic despite its length, return an empty array.
19
+ This is a Technical topic — a knowledge base for a specific technology, platform, or tool. Technical topics are ALLOWED to be dense and detailed.
20
+
21
+ Only flag subjects that are about a **different** technology or workflow than the one named in this record. For example:
22
+ - A Uniform topic containing Turborepo setup details flag "Turborepo monorepo setup"
23
+ - A Uniform topic containing Vercel preview gotchas do NOT flag (that's core Uniform knowledge)
24
+ - An AWS Bedrock topic containing Twilio integration details → flag "Twilio integration"
25
+ `
26
+ : "";
19
27
 
20
- Return a raw JSON array of strings. No markdown fencing, no commentary, no explanation. Just the array.
28
+ const system = `You are auditing a Topic record in a personal knowledge base. A single Topic record has grown too large because unrelated information was repeatedly added over time. The record's Name suggests its intended subject, but its Description now covers additional, unrelated subjects.
29
+
30
+ Your job: identify the **extra subjects** buried in this record that do NOT belong under the record's Name.
31
+
32
+ Rules:
33
+ - Do NOT include the record's primary subject (what its Name describes) — only the extra, unrelated subjects
34
+ - Each subject should be a succinct phrase (2-8 words) that could serve as a search query
35
+ - Be specific: "TypeScript coding conventions" beats "technical preferences"
36
+ - If the record is cohesive and on-topic despite its length, return an empty array
37
+ ${technicalGuidance}
38
+ Return a raw JSON array of strings. No markdown fencing, no commentary.
21
39
 
22
40
  Example — a Topic named "Software Engineering" whose description also discusses vim keybindings, git conventions, and AI tooling:
23
41
  ["vim keybindings and editor configuration", "git and GitHub workflow conventions", "AI coding assistant preferences"]`;
@@ -25,10 +43,10 @@ Example — a Topic named "Software Engineering" whose description also discusse
25
43
  const payload = JSON.stringify(stripEmbedding(data.item), null, 2);
26
44
 
27
45
  const schemaReminder = `**Return JSON:**
28
- \n\`\`\`json
46
+ \`\`\`json
29
47
  [
30
- "topic about vim keybindings",
31
- "git workflow conventions",
48
+ "vim keybindings and editor configuration",
49
+ "git and GitHub workflow conventions",
32
50
  "AI coding assistant preferences"
33
51
  ]
34
52
  \`\`\`
@@ -45,13 +63,70 @@ ${schemaReminder}`;
45
63
  }
46
64
 
47
65
  // =============================================================================
48
- // PHASE 2: REWRITEReorganize data across existing and new items
66
+ // PHASE 2: SPLITSlim the Topic, redistribute subjects to new/existing records
49
67
  // =============================================================================
50
68
 
51
- export function buildRewritePrompt(data: RewritePromptData): { system: string; user: string } {
52
- const typeLabel = data.itemType.charAt(0).toUpperCase() + data.itemType.slice(1);
69
+ function buildTopicExistingExample(): string {
70
+ return `Topic:
71
+ {
72
+ "id": "existing-uuid",
73
+ "type": "topic",
74
+ "name": "Topic Name",
75
+ "description": "Updated topic description",
76
+ "category": "Interest"
77
+ }
78
+
79
+ Person:
80
+ {
81
+ "id": "existing-uuid",
82
+ "type": "person",
83
+ "name": "Person Name",
84
+ "description": "Updated person description",
85
+ "relationship": "coworker"
86
+ }`;
87
+ }
88
+
89
+ function buildTopicNewExample(isTechnical: boolean): string {
90
+ const categoryHint = isTechnical
91
+ ? `"category": "Technical" // Split topics from a Technical record inherit Technical category unless clearly different`
92
+ : `"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical"`;
93
+
94
+ return `Topic:
95
+ {
96
+ "type": "topic",
97
+ "name": "New Topic Name",
98
+ "description": "Concise topic description",
99
+ "sentiment": 0.0,
100
+ ${categoryHint}
101
+ }
102
+
103
+ Person:
104
+ {
105
+ "type": "person",
106
+ "name": "New Person Name",
107
+ "description": "Concise person description",
108
+ "sentiment": 0.0,
109
+ "relationship": "friend"
110
+ }`;
111
+ }
112
+
113
+ export function buildTopicRewriteSplitPrompt(data: RewritePromptData): { system: string; user: string } {
114
+ const isTechnical = (data.item as { category?: string }).category === "Technical";
115
+
116
+ const descriptionGuidance = isTechnical
117
+ ? `ideally under 600 characters, never over 900 — Technical topics are knowledge bases that preserve specific gotchas, decisions, and open questions`
118
+ : `ideally under 300 characters, never over 500`;
53
119
 
54
- const system = `You are reorganizing a personal knowledge base. A ${typeLabel} record has become a catch-all for several unrelated subjects. An earlier analysis identified the extra subjects, and we searched our knowledge base for potentially matching existing records.
120
+ const technicalCategoryNote = isTechnical
121
+ ? `\n- Topics split from a Technical record should inherit category "Technical" unless the subject is clearly a different type (e.g., a personal interest extracted from a technical topic)`
122
+ : "";
123
+
124
+ const noSubjectsGate = data.subjects.length === 0
125
+ ? `\n**IMPORTANT: No extra subjects were identified for this record. The correct response is to return the original record unchanged in "existing" with an empty "new" array. Do NOT create new records. Do NOT modify the description.**\n`
126
+ : "";
127
+
128
+ const system = `You are reorganizing a personal knowledge base. A Topic record has become a catch-all for several unrelated subjects. An earlier analysis identified the extra subjects, and we searched the knowledge base for potentially matching existing records.
129
+ ${noSubjectsGate}
55
130
 
56
131
  The search results under each subject are our **best guesses** — they may not be accurate matches. Only merge data into an existing record if the subject matter genuinely overlaps. Similar names with different meanings should produce a NEW record instead.
57
132
 
@@ -60,26 +135,26 @@ Your job:
60
135
  2. **Create new records**: For subjects with no appropriate match among the search results, create a new record.
61
136
  3. **Slim the original**: Remove all data from the original record that now lives elsewhere. The original should contain ONLY information directly relevant to its Name.
62
137
 
63
- Return raw JSON with exactly two keys. No markdown fencing, no commentary. Just the JSON object:
138
+ Return raw JSON with exactly two keys. No markdown fencing, no commentary:
64
139
  {
65
140
  "existing": [ /* updated records, including the slimmed-down original */ ],
66
141
  "new": [ /* brand-new records for subjects with no match */ ]
67
142
  }
68
143
 
69
144
  Record format for "existing" entries (MUST include "id" and "type"):
70
- ${buildExistingExamples()}
145
+ ${buildTopicExistingExample()}
71
146
 
72
147
  Record format for "new" entries (NO "id" field — the system assigns one):
73
- ${buildNewExamples()}
148
+ ${buildTopicNewExample(isTechnical)}
74
149
 
75
150
  Rules:
76
- - The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down.
77
- - Descriptions should be concise: ${data.itemType === 'topic' ? 'ideally under 300 characters, never over 500' : 'ideally under 600 characters, never over 1000'}.
78
- - Preserve sentiment, strength, confidence, and other numeric values from the source record where applicable.
79
- - "type" must be one of: "topic", "person".
80
- - 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.
151
+ - The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down
152
+ - Descriptions should be concise: ${descriptionGuidance}
153
+ - Preserve sentiment and other numeric values from the source record where applicable
154
+ - "type" must be one of: "topic", "person"
155
+ - Topics MUST include "category" — one of: Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project, Event, Technical. For Event topics, the description should be a narrative account of a specific moment, not a general summary. For Technical topics, split by distinct technical concept (e.g., "Uniform Composition Model" vs "Uniform Preview Setup") — preserve specificity over brevity
81
156
  - People MUST include "relationship" — a short label like "coworker", "friend", "mentor", etc.
82
- - Do NOT invent information. Only redistribute what exists in the original record.`;
157
+ - Do NOT invent information. Only redistribute what exists in the original record${technicalCategoryNote}`;
83
158
 
84
159
  const subjects = data.subjects.map(s => ({
85
160
  search_term: s.searchTerm,
@@ -93,7 +168,7 @@ Rules:
93
168
  };
94
169
 
95
170
  const schemaReminder = `**Return JSON:**
96
- \n\`\`\`json
171
+ \`\`\`json
97
172
  {
98
173
  "existing": [
99
174
  {
@@ -123,53 +198,3 @@ ${schemaReminder}`;
123
198
 
124
199
  return { system, user };
125
200
  }
126
-
127
- // =============================================================================
128
- // Helpers
129
- // =============================================================================
130
-
131
- /** Strip embedding arrays from items before putting them in prompts — they're huge and useless to the LLM. */
132
- function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "embedding"> {
133
- const { embedding: _, ...rest } = item;
134
- return rest as Omit<T, "embedding">;
135
- }
136
-
137
- function buildExistingExamples(): string {
138
- return `Topic:
139
- {
140
- "id": "existing-uuid",
141
- "type": "topic",
142
- "name": "Topic Name",
143
- "description": "Updated topic description",
144
- "category": "Interest"
145
- }
146
-
147
- Person:
148
- {
149
- "id": "existing-uuid",
150
- "type": "person",
151
- "name": "Person Name",
152
- "description": "Updated person description",
153
- "relationship": "coworker"
154
- }`;
155
- }
156
-
157
- function buildNewExamples(): string {
158
- return `Topic:
159
- {
160
- "type": "topic",
161
- "name": "New Topic Name",
162
- "description": "Concise topic description",
163
- "sentiment": 0.0,
164
- "category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event"
165
- }
166
-
167
- Person:
168
- {
169
- "type": "person",
170
- "name": "New Person Name",
171
- "description": "Concise person description",
172
- "sentiment": 0.0,
173
- "relationship": "friend"
174
- }`;
175
- }
@@ -43,6 +43,8 @@ Flag a PERSON when they were meaningfully discussed — not just mentioned in pa
43
43
 
44
44
  Be **conservative**: ignore one-off mentions, greetings, small talk, or jokes. Only flag people who matter to the human user's life.
45
45
 
46
+ A person is **not worth flagging** if they have no name AND appear only to attribute a single event ("a coworker showed me this band", "a friend told me about it", "some guy I know"). The human user having a contact who did one thing is not a meaningful discussion of that person.
47
+
46
48
  ## What a PERSON Is
47
49
 
48
50
  Someone in the human user's world.
@@ -68,11 +70,18 @@ Use the specific value where possible (e.g. "Father", "Brother", "Coworker"). Av
68
70
 
69
71
  ## When Identity Is Unclear
70
72
 
71
- If you can't identify which "Bob" or which "Brother" the user means, use "Unknown" and explain in the reason field. This triggers a later step to resolve ambiguity.
73
+ "Unknown" is ONLY for people who are **meaningfully and repeatedly discussed** but whose name isn't given. It is NOT a catch-all for any nameless mention.
74
+
75
+ ✓ USE "Unknown":
76
+ - name: "Unknown", relationship: "Brother", reason: "User talked at length about their brother across multiple messages without naming him"
77
+
78
+ ✗ DO NOT USE "Unknown" for one-off attributions:
79
+ - "a coworker showed me this band" → **skip entirely** — not a person, just attribution
80
+ - "a friend told me about it" → **skip entirely**
81
+ - "some guy I know" → **skip entirely**
82
+ - "a coworker at [company name]" with no personal name → **skip entirely** — a company name is NOT a person's name
72
83
 
73
- Examples:
74
- - name: "Alice from work", relationship: "Coworker", description: "Mentioned but not described further", reason: "User referenced a work colleague named Alice"
75
- - name: "Unknown", relationship: "Sibling", description: "User mentioned a sibling but did not give a name", reason: "User said 'my brother' without further context"
84
+ If someone has no personal name and appears only to explain how the user found something or heard about something, they are not a person in the user's life worth tracking. Do not extract them. A single interaction — even a meaningful one — does not make someone a contact.
76
85
 
77
86
  ## Identifiers (optional)
78
87
 
@@ -14,6 +14,17 @@ function participantContextSection(ctx: ParticipantContext | undefined): string
14
14
  return lines.join("\n");
15
15
  }
16
16
 
17
+ function technicalContextSection(technical_context: boolean | undefined): string {
18
+ if (!technical_context) return "";
19
+ return `## Technical Context
20
+
21
+ This conversation originates from a technical source (coding tool session, developer workflow). The human is likely a developer or technical user.
22
+
23
+ **Treat Technical as a priority category** for topics that are tools, platforms, frameworks, libraries, or technical concepts being actively learned, evaluated, or built with. Flag these even if they seem like passing mentions — technical knowledge compounds and is worth preserving.
24
+
25
+ `;
26
+ }
27
+
17
28
  export function buildHumanTopicScanPrompt(data: TopicScanPromptData): PromptOutput {
18
29
  if (!data.persona_name) {
19
30
  throw new Error("buildHumanTopicScanPrompt: persona_name is required");
@@ -56,9 +67,12 @@ Assign each TOPIC one category. Pick the closest fit:
56
67
  - **Plan** — concrete intentions with steps in mind
57
68
  - **Project** — active undertakings with real progress
58
69
  - **Event** — a specific, significant moment that either party might reference later ("remember when...")
70
+ - **Technical** — a tool, platform, framework, library, or technical concept being actively learned, evaluated, or built with
59
71
 
60
72
  When in doubt, pick the closest match. The update step will refine it.
61
73
 
74
+ ${technicalContextSection(data.technical_context)}
75
+
62
76
  ## Output Format
63
77
 
64
78
  \`\`\`json
@@ -67,7 +81,7 @@ When in doubt, pick the closest match. The update step will refine it.
67
81
  {
68
82
  "name": "Short label for the topic (10-75 characters)",
69
83
  "description": "1-2 sentences: what this topic is and why it matters to the user",
70
- "category": "One of the categories above",
84
+ "category": "One of the categories above (Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical)",
71
85
  "reason": "Evidence from the conversation that justified flagging this topic"
72
86
  }
73
87
  ]
@@ -104,7 +118,7 @@ Scan the "Most Recent Messages" for TOPICS of interest to the human user.
104
118
  {
105
119
  "name": "Short label for the topic (10-75 characters)",
106
120
  "description": "1-2 sentences: what this topic is and why it matters to the user",
107
- "category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event",
121
+ "category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical",
108
122
  "reason": "Evidence from the conversation that justified flagging this topic"
109
123
  }
110
124
  ]