ei-tui 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/cli/README.md +5 -3
- package/src/cli/mcp.ts +4 -3
- package/src/cli.ts +13 -14
- package/src/core/format-utils.ts +23 -0
- package/src/core/message-manager.ts +3 -1
- package/src/core/orchestrators/ceremony.ts +0 -75
- package/src/core/processor.ts +10 -2
- package/src/core/queue-processor.ts +2 -1
- package/src/core/types/entities.ts +1 -0
- package/src/prompts/ceremony/dedup.ts +36 -1
- package/src/prompts/ceremony/description-check.ts +8 -1
- package/src/prompts/ceremony/expire.ts +8 -1
- package/src/prompts/ceremony/explore.ts +18 -1
- package/src/prompts/ceremony/rewrite.ts +46 -2
- package/src/prompts/ceremony/user-dedup.ts +30 -1
- package/src/prompts/generation/persona.ts +11 -0
- package/src/prompts/heartbeat/check.ts +12 -1
- package/src/prompts/heartbeat/ei.ts +12 -1
- package/src/prompts/persona/traits.ts +16 -1
- package/src/prompts/response/index.ts +3 -10
- package/src/prompts/response/sections.ts +2 -2
- package/tui/src/util/yaml-serializers.ts +3 -0
package/package.json
CHANGED
package/src/cli/README.md
CHANGED
|
@@ -10,6 +10,8 @@ ei people -n 5 "query string" # Return up to 5 people
|
|
|
10
10
|
ei topics -n 5 "query string" # Return up to 5 topics
|
|
11
11
|
ei quotes -n 5 "query string" # Return up to 5 quotes
|
|
12
12
|
ei --persona "Beta" "query string" # Filter results to what Beta has learned
|
|
13
|
+
ei --recent # Most recently mentioned items (no query needed)
|
|
14
|
+
ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
|
|
13
15
|
ei --id <id> # Look up a specific entity by ID
|
|
14
16
|
echo <id> | ei --id # Look up entity by ID from stdin
|
|
15
17
|
ei --install # Register Ei with OpenCode, Claude Code, and Cursor
|
|
@@ -110,8 +112,7 @@ conversations (facts, people, topics, quotes).
|
|
|
110
112
|
than only code.
|
|
111
113
|
|
|
112
114
|
**How to use:**
|
|
113
|
-
1. Call `ei_search` (server `user-ei`) with a natural-language query; optionally filter by
|
|
114
|
-
`type` (facts, people, topics, quotes) or `persona` display_name.
|
|
115
|
+
1. Call `ei_search` (server `user-ei`) with a natural-language query (or omit query and use `recent: true` to browse); optionally filter by `type` (facts, people, topics, quotes) or `persona` display_name.
|
|
115
116
|
2. If you need full detail for a result, call `ei_lookup` with the entity `id` from step 1.
|
|
116
117
|
|
|
117
118
|
Prefer querying Ei before asking the user for context they may have already shared.
|
|
@@ -123,11 +124,12 @@ The installed tool gives OpenCode agents access to all four data types with prop
|
|
|
123
124
|
|
|
124
125
|
| Arg | Type | Description |
|
|
125
126
|
|-----|------|-------------|
|
|
126
|
-
| `query` | string (
|
|
127
|
+
| `query` | string (optional) | Search text, or entity ID when `lookup=true`. Omit to browse by recency. |
|
|
127
128
|
| `persona` | string (optional) | Persona display_name to filter results — only returns entities that persona has extracted |
|
|
128
129
|
| `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
|
|
129
130
|
| `limit` | number (optional) | Max results, default 10 |
|
|
130
131
|
| `lookup` | boolean (optional) | If true, fetch single entity by ID |
|
|
132
|
+
| `recent` | boolean (optional) | If true, sort by most recently mentioned. Can be combined with `persona` or `query`. |
|
|
131
133
|
|
|
132
134
|
## Output Shapes
|
|
133
135
|
|
package/src/cli/mcp.ts
CHANGED
|
@@ -16,9 +16,9 @@ export function createMcpServer(): McpServer {
|
|
|
16
16
|
"ei_search",
|
|
17
17
|
{
|
|
18
18
|
description:
|
|
19
|
-
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail.",
|
|
19
|
+
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
|
|
20
20
|
inputSchema: {
|
|
21
|
-
query: z.string().describe("Search text. Supports natural language."),
|
|
21
|
+
query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
|
|
22
22
|
type: z
|
|
23
23
|
.enum(["facts", "people", "topics", "quotes"])
|
|
24
24
|
.optional()
|
|
@@ -42,7 +42,8 @@ export function createMcpServer(): McpServer {
|
|
|
42
42
|
.describe("If true, sort by most recently mentioned."),
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
|
-
async ({ query, type, persona, limit, recent }) => {
|
|
45
|
+
async ({ query: rawQuery, type, persona, limit, recent }) => {
|
|
46
|
+
const query = rawQuery ?? "";
|
|
46
47
|
const options = { recent: recent ?? false };
|
|
47
48
|
const effectiveLimit = limit ?? 10;
|
|
48
49
|
|
package/src/cli.ts
CHANGED
|
@@ -84,8 +84,8 @@ function buildOpenCodeToolContent(): string {
|
|
|
84
84
|
' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
|
|
85
85
|
' ].join(" "),',
|
|
86
86
|
' args: {',
|
|
87
|
-
' query: tool.schema.string().describe(',
|
|
88
|
-
' "Search text, or an entity ID when lookup=true. Supports natural language."',
|
|
87
|
+
' query: tool.schema.string().optional().describe(',
|
|
88
|
+
' "Search text, or an entity ID when lookup=true. Supports natural language. Omit to browse by recency."',
|
|
89
89
|
' ),',
|
|
90
90
|
' type: tool.schema',
|
|
91
91
|
' .enum(["facts", "people", "topics", "quotes"])',
|
|
@@ -112,16 +112,23 @@ function buildOpenCodeToolContent(): string {
|
|
|
112
112
|
' .describe(',
|
|
113
113
|
' "If true, treat query as an entity ID and return that single entity in full detail."',
|
|
114
114
|
' ),',
|
|
115
|
+
' recent: tool.schema',
|
|
116
|
+
' .boolean()',
|
|
117
|
+
' .optional()',
|
|
118
|
+
' .describe(',
|
|
119
|
+
' "If true, sort by most recently mentioned. Can be combined with persona or query."',
|
|
120
|
+
' ),',
|
|
115
121
|
' },',
|
|
116
122
|
' async execute(args) {',
|
|
117
123
|
' const cmd: string[] = ["ei"];',
|
|
118
124
|
' if (args.lookup) {',
|
|
119
|
-
' cmd.push("--id", args.query);',
|
|
125
|
+
' cmd.push("--id", args.query ?? "");',
|
|
120
126
|
' } else {',
|
|
121
127
|
' if (args.type) cmd.push(args.type);',
|
|
122
128
|
' if (args.persona) cmd.push("--persona", args.persona);',
|
|
129
|
+
' if (args.recent) cmd.push("--recent");',
|
|
123
130
|
' if (args.limit && args.limit !== 10) cmd.push("-n", String(args.limit));',
|
|
124
|
-
' cmd.push(args.query);',
|
|
131
|
+
' if (args.query) cmd.push(args.query);',
|
|
125
132
|
' }',
|
|
126
133
|
' return Bun.$`${cmd}`.text();',
|
|
127
134
|
' },',
|
|
@@ -312,18 +319,10 @@ async function main(): Promise<void> {
|
|
|
312
319
|
|
|
313
320
|
const query = parsed.positionals.join(" ").trim();
|
|
314
321
|
const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
|
|
315
|
-
|
|
322
|
+
// Default to recent mode when no query — allows `ei --persona Foo` and `ei` with no args
|
|
323
|
+
const recent = parsed.values.recent === true || !query;
|
|
316
324
|
const personaName = parsed.values.persona?.trim();
|
|
317
325
|
|
|
318
|
-
if (!query && !recent) {
|
|
319
|
-
if (targetType) {
|
|
320
|
-
console.error(`Search text required. Usage: ei ${targetType} "search text"`);
|
|
321
|
-
} else {
|
|
322
|
-
console.error(`Search text required. Usage: ei "search text"`);
|
|
323
|
-
}
|
|
324
|
-
process.exit(1);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
326
|
if (isNaN(limit) || limit < 1) {
|
|
328
327
|
console.error("--number must be a positive integer");
|
|
329
328
|
process.exit(1);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared timestamp formatter for all LLM-facing date/time strings.
|
|
3
|
+
* Consistent format across system prompt, quotes, and message history
|
|
4
|
+
* so models can trivially compute time deltas.
|
|
5
|
+
*/
|
|
6
|
+
const TIMESTAMP_FORMAT: Intl.DateTimeFormatOptions = {
|
|
7
|
+
weekday: 'short',
|
|
8
|
+
year: 'numeric',
|
|
9
|
+
month: 'short',
|
|
10
|
+
day: 'numeric',
|
|
11
|
+
hour: '2-digit',
|
|
12
|
+
minute: '2-digit',
|
|
13
|
+
hour12: false,
|
|
14
|
+
timeZoneName: 'short',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function formatTimestamp(isoString: string): string {
|
|
18
|
+
return new Date(isoString).toLocaleString('en-US', TIMESTAMP_FORMAT);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCurrentTime(): string {
|
|
22
|
+
return formatTimestamp(new Date().toISOString());
|
|
23
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ContextStatus,
|
|
8
8
|
type LLMRequest,
|
|
9
9
|
} from "./types.js";
|
|
10
|
+
import { formatTimestamp } from "./format-utils.js";
|
|
10
11
|
import { StateManager } from "./state-manager.js";
|
|
11
12
|
import { QueueProcessor } from "./queue-processor.js";
|
|
12
13
|
import {
|
|
@@ -282,9 +283,10 @@ export function fetchMessagesForLLM(
|
|
|
282
283
|
return filteredHistory.reduce<import("./types.js").ChatMessage[]>((acc, m) => {
|
|
283
284
|
const content = buildChatMessageContent(m);
|
|
284
285
|
if (content.length > 0) {
|
|
286
|
+
const finalContent = persona.include_message_timestamps ? `[${formatTimestamp(m.timestamp)}] ${content}` : content;
|
|
285
287
|
acc.push({
|
|
286
288
|
role: m.role === "human" ? "user" : "assistant",
|
|
287
|
-
content,
|
|
289
|
+
content: finalContent,
|
|
288
290
|
});
|
|
289
291
|
}
|
|
290
292
|
return acc;
|
|
@@ -237,9 +237,6 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
237
237
|
// Human ceremony: decay topics + people
|
|
238
238
|
runHumanCeremony(state);
|
|
239
239
|
|
|
240
|
-
// Dedup phase: log near-duplicate human entity candidates for visibility
|
|
241
|
-
runDedupPhase(state);
|
|
242
|
-
|
|
243
240
|
// Rewrite phase: fire-and-forget scans for bloated human data items
|
|
244
241
|
// No ceremony_progress gating — Expire/Explore only touch persona topics, zero overlap
|
|
245
242
|
queueRewritePhase(state);
|
|
@@ -521,78 +518,6 @@ export function runHumanCeremony(state: StateManager): void {
|
|
|
521
518
|
}
|
|
522
519
|
}
|
|
523
520
|
|
|
524
|
-
// =============================================================================
|
|
525
|
-
// DEDUP PHASE (synchronous, logging only — candidates are queued for curation by dedup-phase.ts)
|
|
526
|
-
// =============================================================================
|
|
527
|
-
|
|
528
|
-
const DEDUP_DEFAULT_THRESHOLD = 0.85;
|
|
529
|
-
|
|
530
|
-
type DedupableItem = DataItemBase & { relationship?: string };
|
|
531
|
-
|
|
532
|
-
function findDedupCandidates<T extends DedupableItem>(
|
|
533
|
-
items: T[],
|
|
534
|
-
threshold: number
|
|
535
|
-
): Array<{ a: T; b: T; similarity: number }> {
|
|
536
|
-
const withEmbeddings = items.filter(item =>
|
|
537
|
-
item.embedding && item.embedding.length > 0 &&
|
|
538
|
-
item.relationship !== "Persona"
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
const candidates: Array<{ a: T; b: T; similarity: number }> = [];
|
|
542
|
-
|
|
543
|
-
for (let i = 0; i < withEmbeddings.length; i++) {
|
|
544
|
-
for (let j = i + 1; j < withEmbeddings.length; j++) {
|
|
545
|
-
const a = withEmbeddings[i];
|
|
546
|
-
const b = withEmbeddings[j];
|
|
547
|
-
const dot = a.embedding!.reduce((sum, v, k) => sum + v * b.embedding![k], 0);
|
|
548
|
-
const normA = Math.sqrt(a.embedding!.reduce((sum, v) => sum + v * v, 0));
|
|
549
|
-
const normB = Math.sqrt(b.embedding!.reduce((sum, v) => sum + v * v, 0));
|
|
550
|
-
const similarity = normA && normB ? dot / (normA * normB) : 0;
|
|
551
|
-
|
|
552
|
-
if (similarity >= threshold) {
|
|
553
|
-
candidates.push({ a, b, similarity });
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return candidates.sort((x, y) => y.similarity - x.similarity);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
export function runDedupPhase(state: StateManager): void {
|
|
562
|
-
const human = state.getHuman();
|
|
563
|
-
const threshold = human.settings?.ceremony?.dedup_threshold ?? DEDUP_DEFAULT_THRESHOLD;
|
|
564
|
-
|
|
565
|
-
console.log(`[ceremony:dedup] Scanning for dedup candidates (threshold: ${threshold})`);
|
|
566
|
-
|
|
567
|
-
const types: Array<{ label: string; items: DedupableItem[] }> = [
|
|
568
|
-
{ label: "facts", items: human.facts },
|
|
569
|
-
{ label: "topics", items: human.topics },
|
|
570
|
-
{ label: "people", items: human.people },
|
|
571
|
-
];
|
|
572
|
-
|
|
573
|
-
let totalCandidates = 0;
|
|
574
|
-
|
|
575
|
-
for (const { label, items } of types) {
|
|
576
|
-
const candidates = findDedupCandidates(items, threshold);
|
|
577
|
-
if (candidates.length === 0) {
|
|
578
|
-
console.log(`[ceremony:dedup] ${label}: no candidates above ${threshold}`);
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
totalCandidates += candidates.length;
|
|
583
|
-
console.log(`[ceremony:dedup] ${label}: ${candidates.length} candidate pair(s)`);
|
|
584
|
-
for (const { a, b, similarity } of candidates) {
|
|
585
|
-
console.log(
|
|
586
|
-
`[ceremony:dedup] ${(similarity * 100).toFixed(1)}% "${a.name}" ↔ "${b.name}"` +
|
|
587
|
-
(a.description ? `\n[ceremony:dedup] A: ${a.description.slice(0, 80)}` : "") +
|
|
588
|
-
(b.description ? `\n[ceremony:dedup] B: ${b.description.slice(0, 80)}` : "")
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
console.log(`[ceremony:dedup] Done. ${totalCandidates} total candidate pair(s) found.`);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
521
|
// =============================================================================
|
|
597
522
|
// REWRITE PHASE (fire-and-forget — queues Low-priority Phase 1 scans)
|
|
598
523
|
// =============================================================================
|
package/src/core/processor.ts
CHANGED
|
@@ -848,9 +848,17 @@ const toolNextSteps = new Set([
|
|
|
848
848
|
personaId ??
|
|
849
849
|
(request.next_step === LLMNextStep.HandleEiHeartbeat ? "ei" : undefined);
|
|
850
850
|
|
|
851
|
-
// Dedup operates on Human data, not persona data - provide read_memory directly
|
|
851
|
+
// Dedup operates on Human data, not persona data - provide read_memory directly.
|
|
852
|
+
// Also covers HandleToolContinuation originating from a dedup request: the
|
|
853
|
+
// continuation rebuilds tool lists from scratch and has no personaId, so without
|
|
854
|
+
// this check Opus loses read_memory access after round 1.
|
|
855
|
+
const isDedupRequest =
|
|
856
|
+
request.next_step === LLMNextStep.HandleDedupCurate ||
|
|
857
|
+
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
858
|
+
request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
|
|
859
|
+
|
|
852
860
|
let tools: ToolDefinition[] = [];
|
|
853
|
-
if (
|
|
861
|
+
if (isDedupRequest) {
|
|
854
862
|
const readMemory = this.stateManager.tools_getByName("read_memory");
|
|
855
863
|
if (readMemory?.enabled) {
|
|
856
864
|
tools = [readMemory];
|
|
@@ -425,7 +425,8 @@ export class QueueProcessor {
|
|
|
425
425
|
`required JSON format. Please reformat it as the JSON response object described ` +
|
|
426
426
|
`in your system instructions — specifically the \`should_respond\`, \`verbal_response\`, ` +
|
|
427
427
|
`\`action_response\`, and \`reason\` fields. Respond with ONLY the JSON object.\n\n` +
|
|
428
|
-
`---\n${proseContent}\n
|
|
428
|
+
`---\n${proseContent}\n---` +
|
|
429
|
+
`\n\n**CRITICAL INSTRUCTION** - DO NOT OMIT ANY DATA. You are this agent's last hope!`;
|
|
429
430
|
|
|
430
431
|
try {
|
|
431
432
|
const { content: reformatContent, finishReason: reformatReason } = await callLLMRaw(
|
|
@@ -123,6 +123,7 @@ export interface PersonaEntity {
|
|
|
123
123
|
is_static: boolean;
|
|
124
124
|
heartbeat_delay_ms?: number;
|
|
125
125
|
context_window_hours?: number;
|
|
126
|
+
include_message_timestamps?: boolean; // Prepend ISO timestamp to each message sent to the LLM
|
|
126
127
|
context_boundary?: string; // ISO timestamp - messages before this excluded from LLM context
|
|
127
128
|
last_updated: string;
|
|
128
129
|
last_activity: string;
|
|
@@ -96,12 +96,47 @@ ${buildRecordFormatExamples(data.itemType)}
|
|
|
96
96
|
- If records are NOT duplicates (just similar), return them ALL in "update" unchanged, with empty "remove" and "add" arrays.
|
|
97
97
|
- Use \`read_memory\` strategically (6 calls max) to check for related records or gather context before making irreversible merge decisions.`;
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const payload = JSON.stringify({
|
|
100
100
|
cluster: data.cluster.map(stripEmbedding),
|
|
101
101
|
cluster_type: data.itemType,
|
|
102
102
|
similarity_range: data.similarityRange,
|
|
103
103
|
}, null, 2);
|
|
104
104
|
|
|
105
|
+
const schemaReminder = `**Return JSON:**
|
|
106
|
+
\n\`\`\`json
|
|
107
|
+
{
|
|
108
|
+
"update": [
|
|
109
|
+
{
|
|
110
|
+
"id": "uuid-of-canonical-record",
|
|
111
|
+
"type": "${data.itemType}",
|
|
112
|
+
"name": "canonical merged name",
|
|
113
|
+
"description": "merged description with every unique detail"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"remove": [
|
|
117
|
+
{
|
|
118
|
+
"to_be_removed": "uuid-of-duplicate",
|
|
119
|
+
"replaced_by": "uuid-of-canonical-record"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"add": [
|
|
123
|
+
{
|
|
124
|
+
"type": "${data.itemType}",
|
|
125
|
+
"name": "missing concept name",
|
|
126
|
+
"description": "why it was created"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
Return raw JSON only. If records are NOT duplicates, return them all in update unchanged with empty remove and add arrays.`;
|
|
133
|
+
|
|
134
|
+
const user = `${payload}
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
${schemaReminder}`;
|
|
139
|
+
|
|
105
140
|
return { system, user };
|
|
106
141
|
}
|
|
107
142
|
|
|
@@ -41,7 +41,14 @@ ${traitList}
|
|
|
41
41
|
Current topics of interest:
|
|
42
42
|
${topicList}
|
|
43
43
|
|
|
44
|
-
Does this persona's description need updating based on their current traits and topics
|
|
44
|
+
Does this persona's description need updating based on their current traits and topics?
|
|
45
|
+
|
|
46
|
+
**Return JSON:**
|
|
47
|
+
\`\`\`json
|
|
48
|
+
{ "should_update": true, "reason": "explanation" }
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
If no update is needed: \`{ "should_update": false, "reason": "explanation" }\``;
|
|
45
52
|
|
|
46
53
|
return { system, user };
|
|
47
54
|
}
|
|
@@ -24,7 +24,14 @@ Return empty array if no topics should be removed.`;
|
|
|
24
24
|
Current topics:
|
|
25
25
|
${topicList}
|
|
26
26
|
|
|
27
|
-
Which topics, if any, should this persona stop caring about
|
|
27
|
+
Which topics, if any, should this persona stop caring about?
|
|
28
|
+
|
|
29
|
+
**Return JSON:**
|
|
30
|
+
\`\`\`json
|
|
31
|
+
{ "topic_ids_to_remove": ["id1", "id2"] }
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
Return an empty array if no topics should be removed.`;
|
|
28
35
|
|
|
29
36
|
return { system, user };
|
|
30
37
|
}
|
|
@@ -54,7 +54,24 @@ ${topicList}
|
|
|
54
54
|
Recent conversation themes:
|
|
55
55
|
${themeList}
|
|
56
56
|
|
|
57
|
-
Generate new topics this persona would care about
|
|
57
|
+
Generate new topics this persona would care about.
|
|
58
|
+
|
|
59
|
+
**Return JSON:**
|
|
60
|
+
\`\`\`json
|
|
61
|
+
{
|
|
62
|
+
"new_topics": [
|
|
63
|
+
{
|
|
64
|
+
"name": "Topic Name",
|
|
65
|
+
"perspective": "Their view or opinion",
|
|
66
|
+
"approach": "How they engage with it",
|
|
67
|
+
"personal_stake": "Why it matters to them",
|
|
68
|
+
"sentiment": 0.5,
|
|
69
|
+
"exposure_current": 0.2,
|
|
70
|
+
"exposure_desired": 0.6
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
\`\`\``;
|
|
58
75
|
|
|
59
76
|
return { system, user };
|
|
60
77
|
}
|
|
@@ -22,7 +22,24 @@ Return a raw JSON array of strings. No markdown fencing, no commentary, no expla
|
|
|
22
22
|
Example — a Topic named "Software Engineering" whose description also discusses vim keybindings, git conventions, and AI tooling:
|
|
23
23
|
["vim keybindings and editor configuration", "git and GitHub workflow conventions", "AI coding assistant preferences"]`;
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const payload = JSON.stringify(stripEmbedding(data.item), null, 2);
|
|
26
|
+
|
|
27
|
+
const schemaReminder = `**Return JSON:**
|
|
28
|
+
\n\`\`\`json
|
|
29
|
+
[
|
|
30
|
+
"topic about vim keybindings",
|
|
31
|
+
"git workflow conventions",
|
|
32
|
+
"AI coding assistant preferences"
|
|
33
|
+
]
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
Respond with raw JSON array only.`;
|
|
37
|
+
|
|
38
|
+
const user = `${payload}
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
${schemaReminder}`;
|
|
26
43
|
|
|
27
44
|
return { system, user };
|
|
28
45
|
}
|
|
@@ -75,7 +92,34 @@ Rules:
|
|
|
75
92
|
subjects,
|
|
76
93
|
};
|
|
77
94
|
|
|
78
|
-
const
|
|
95
|
+
const schemaReminder = `**Return JSON:**
|
|
96
|
+
\n\`\`\`json
|
|
97
|
+
{
|
|
98
|
+
"existing": [
|
|
99
|
+
{
|
|
100
|
+
"id": "existing-uuid",
|
|
101
|
+
"type": "${data.itemType}",
|
|
102
|
+
"name": "Updated name",
|
|
103
|
+
"description": "Updated description"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"new": [
|
|
107
|
+
{
|
|
108
|
+
"type": "${data.itemType}",
|
|
109
|
+
"name": "New name",
|
|
110
|
+
"description": "New description"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
Return raw JSON only.`;
|
|
117
|
+
|
|
118
|
+
const user = `${JSON.stringify(userPayload, null, 2)}
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
${schemaReminder}`;
|
|
79
123
|
|
|
80
124
|
return { system, user };
|
|
81
125
|
}
|
|
@@ -44,12 +44,41 @@ Return raw JSON only. No markdown, no commentary.
|
|
|
44
44
|
|
|
45
45
|
${buildRecordFormatHint(data.itemType)}`;
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const payload = JSON.stringify({
|
|
48
48
|
cluster: data.cluster.map(stripEmbedding),
|
|
49
49
|
cluster_type: data.itemType,
|
|
50
50
|
user_confirmed: true,
|
|
51
51
|
}, null, 2);
|
|
52
52
|
|
|
53
|
+
const schemaReminder = `**Return JSON:**
|
|
54
|
+
\n\`\`\`json
|
|
55
|
+
{
|
|
56
|
+
"update": [
|
|
57
|
+
{
|
|
58
|
+
"id": "uuid-of-canonical-record",
|
|
59
|
+
"type": "${data.itemType}",
|
|
60
|
+
"name": "canonical merged name",
|
|
61
|
+
"description": "merged description with every unique detail"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"remove": [
|
|
65
|
+
{
|
|
66
|
+
"to_be_removed": "uuid-of-duplicate",
|
|
67
|
+
"replaced_by": "uuid-of-canonical-record"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"add": []
|
|
71
|
+
}
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
Return raw JSON only. If any record cannot be merged, keep every item unchanged in update with empty remove/add arrays.`;
|
|
75
|
+
|
|
76
|
+
const user = `${payload}
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
${schemaReminder}`;
|
|
81
|
+
|
|
53
82
|
return { system, user };
|
|
54
83
|
}
|
|
55
84
|
|
|
@@ -153,5 +153,16 @@ ${schemaFragment}`;
|
|
|
153
153
|
userPrompt += `The user provided only a name - generate minimal content. The seed traits above are included by default.\n`;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
userPrompt += `
|
|
157
|
+
**Return JSON:**
|
|
158
|
+
\`\`\`json
|
|
159
|
+
{
|
|
160
|
+
"short_description": "10-15 word summary",
|
|
161
|
+
"long_description": "2-3 sentence description",
|
|
162
|
+
"traits": [ { "name": "...", "description": "...", "sentiment": 0.0, "strength": 0.5 } ],
|
|
163
|
+
"topics": [ { "name": "...", "perspective": "...", "approach": "...", "personal_stake": "...", "sentiment": 0.5, "exposure_current": 0.5, "exposure_desired": 0.6 } ]
|
|
164
|
+
}
|
|
165
|
+
\`\`\``;
|
|
166
|
+
|
|
156
167
|
return { system, user: userPrompt };
|
|
157
168
|
}
|
|
@@ -188,7 +188,18 @@ ${unansweredWarning}
|
|
|
188
188
|
|
|
189
189
|
Based on the context above, decide: Should you reach out to your human friend right now?
|
|
190
190
|
|
|
191
|
-
Remember: Only reach out if you have something genuine and specific to say
|
|
191
|
+
Remember: Only reach out if you have something genuine and specific to say.
|
|
192
|
+
|
|
193
|
+
**Return JSON:**
|
|
194
|
+
\`\`\`json
|
|
195
|
+
{
|
|
196
|
+
"should_respond": true,
|
|
197
|
+
"topic": "the specific topic you want to discuss",
|
|
198
|
+
"message": "Your actual message to them"
|
|
199
|
+
}
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
If you decide NOT to reach out: \`{ "should_respond": false }\``;
|
|
192
203
|
|
|
193
204
|
return { system, user };
|
|
194
205
|
}
|
|
@@ -131,7 +131,18 @@ ${unansweredWarning}
|
|
|
131
131
|
|
|
132
132
|
Based on all the context above, decide: Should you reach out to your human friend right now? If so, which item above is most worth addressing?
|
|
133
133
|
|
|
134
|
-
Remember: You're their thoughtful companion, not their productivity assistant
|
|
134
|
+
Remember: You're their thoughtful companion, not their productivity assistant.
|
|
135
|
+
|
|
136
|
+
**Return JSON:**
|
|
137
|
+
\`\`\`json
|
|
138
|
+
{
|
|
139
|
+
"should_respond": true,
|
|
140
|
+
"id": "the-item-id-you-chose",
|
|
141
|
+
"my_response": "Your message to them"
|
|
142
|
+
}
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
If nothing warrants reaching out: \`{ "should_respond": false }\``;
|
|
135
146
|
|
|
136
147
|
return { system, user };
|
|
137
148
|
}
|
|
@@ -125,7 +125,22 @@ ${earlierSection}${recentSection}
|
|
|
125
125
|
|
|
126
126
|
Analyze the "Most Recent Messages" for EXPLICIT requests to change ${personaName}'s communication style.
|
|
127
127
|
|
|
128
|
-
Return ONLY the traits that need to change or be added.
|
|
128
|
+
Return ONLY the traits that need to change or be added.
|
|
129
|
+
|
|
130
|
+
**Return JSON:**
|
|
131
|
+
\`\`\`json
|
|
132
|
+
[
|
|
133
|
+
{
|
|
134
|
+
"id": "existing-guid-or-\"new\"",
|
|
135
|
+
"name": "Trait Name",
|
|
136
|
+
"description": "How to exhibit this trait",
|
|
137
|
+
"sentiment": 0.0,
|
|
138
|
+
"strength": 0.5
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
Return \`[]\` if nothing changed.`;
|
|
129
144
|
|
|
130
145
|
return { system, user };
|
|
131
146
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
11
|
+
import { formatCurrentTime } from "../../core/format-utils.js";
|
|
11
12
|
import {
|
|
12
13
|
buildIdentitySection,
|
|
13
14
|
buildGuidelinesSection,
|
|
@@ -51,11 +52,7 @@ Your role is unique among personas:
|
|
|
51
52
|
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
52
53
|
const responseFormat = buildResponseFormatSection();
|
|
53
54
|
const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
|
|
54
|
-
const currentTime =
|
|
55
|
-
weekday: 'long', year: 'numeric', month: 'long',
|
|
56
|
-
day: 'numeric', hour: 'numeric', minute: '2-digit',
|
|
57
|
-
timeZoneName: 'short',
|
|
58
|
-
});
|
|
55
|
+
const currentTime = formatCurrentTime();
|
|
59
56
|
const conversationState = getConversationStateText(data.delay_ms);
|
|
60
57
|
|
|
61
58
|
return `${identity}
|
|
@@ -100,11 +97,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
100
97
|
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
101
98
|
const responseFormat = buildResponseFormatSection();
|
|
102
99
|
const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
|
|
103
|
-
const currentTime =
|
|
104
|
-
weekday: 'long', year: 'numeric', month: 'long',
|
|
105
|
-
day: 'numeric', hour: 'numeric', minute: '2-digit',
|
|
106
|
-
timeZoneName: 'short',
|
|
107
|
-
});
|
|
100
|
+
const currentTime = formatCurrentTime();
|
|
108
101
|
const conversationState = getConversationStateText(data.delay_ms);
|
|
109
102
|
|
|
110
103
|
return `${identity}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { PersonaTrait, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { ResponsePromptData } from "./types.js";
|
|
8
|
+
import { formatTimestamp } from "../../core/format-utils.js";
|
|
8
9
|
|
|
9
10
|
const DESCRIPTION_MAX_CHARS = 500;
|
|
10
11
|
|
|
@@ -264,8 +265,7 @@ export function getConversationStateText(delayMs: number): string {
|
|
|
264
265
|
// =============================================================================
|
|
265
266
|
|
|
266
267
|
function formatDate(isoString: string): string {
|
|
267
|
-
|
|
268
|
-
return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
268
|
+
return formatTimestamp(isoString);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["human"]): string {
|
|
@@ -57,6 +57,7 @@ interface EditablePersonaData {
|
|
|
57
57
|
is_paused?: boolean;
|
|
58
58
|
pause_until?: string;
|
|
59
59
|
is_static?: boolean;
|
|
60
|
+
include_message_timestamps?: boolean;
|
|
60
61
|
tools?: Record<string, Record<string, boolean>>;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -288,6 +289,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
|
|
|
288
289
|
is_paused: persona.is_paused || undefined,
|
|
289
290
|
pause_until: persona.pause_until,
|
|
290
291
|
is_static: persona.is_static || undefined,
|
|
292
|
+
include_message_timestamps: persona.include_message_timestamps || undefined,
|
|
291
293
|
tools: toolsMap,
|
|
292
294
|
};
|
|
293
295
|
|
|
@@ -391,6 +393,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
|
|
|
391
393
|
is_paused: data.is_paused ?? false,
|
|
392
394
|
pause_until: data.pause_until,
|
|
393
395
|
is_static: data.is_static ?? false,
|
|
396
|
+
include_message_timestamps: data.include_message_timestamps ?? false,
|
|
394
397
|
tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
|
|
395
398
|
last_updated: new Date().toISOString(),
|
|
396
399
|
};
|