ei-tui 1.6.8 → 1.6.9
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/commands/personas.ts +46 -1
- package/src/cli/install.ts +135 -132
- package/src/cli.ts +17 -0
- package/src/core/handlers/human-extraction.ts +8 -2
- package/src/core/llm-client.ts +7 -1
- package/src/core/orchestrators/human-extraction.ts +1 -0
- package/src/core/personas/opencode-agent.ts +1 -3
- package/src/core/types/entities.ts +1 -0
- package/src/integrations/pi/importer.ts +142 -50
- package/src/integrations/pi/reader.ts +1 -0
- package/src/integrations/pi/types.ts +4 -0
- package/tui/src/util/provider-detection.ts +4 -2
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadLatestState, retrievePersonas, retrievePersonasSemantic } from "../retrieval";
|
|
1
|
+
import { loadLatestState, retrievePersonas, retrievePersonasSemantic, mapPersona } from "../retrieval";
|
|
2
2
|
import { getEmbeddingService } from "../../core/embedding-service";
|
|
3
3
|
import type { PersonaResult } from "../retrieval";
|
|
4
4
|
|
|
@@ -14,8 +14,53 @@ export async function execute(query: string, limit: number, options: { recent?:
|
|
|
14
14
|
return nameResults;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// BUG-2 fix: query may be longer than the stored persona name
|
|
18
|
+
// (e.g. "Beta — QA Goddess" vs stored "Beta"). Try reverse containment
|
|
19
|
+
// before falling to semantic search, which requires an embedding.
|
|
20
|
+
const queryLower = query.toLowerCase();
|
|
21
|
+
const reverseResults = Object.values(state.personas)
|
|
22
|
+
.map((p) => p.entity)
|
|
23
|
+
.filter((p) => queryLower.includes(p.display_name.toLowerCase()))
|
|
24
|
+
.slice(0, limit)
|
|
25
|
+
.map(mapPersona);
|
|
26
|
+
if (reverseResults.length > 0) {
|
|
27
|
+
return reverseResults;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
const embeddingService = getEmbeddingService();
|
|
18
31
|
const queryVector = await embeddingService.embed(query);
|
|
19
32
|
const semanticResults = await retrievePersonasSemantic(queryVector, state, limit);
|
|
20
33
|
return semanticResults;
|
|
21
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format a PersonaResult as a <ei-relationship> block for injection into
|
|
38
|
+
* AI system prompts. Equivalent to the jq formatter in .zshenv.omp and
|
|
39
|
+
* the inline builder previously in the OpenCode plugin — consolidated here
|
|
40
|
+
* so all integrations can call `ei personas <name> --format prompt`.
|
|
41
|
+
*/
|
|
42
|
+
export function buildEiRelationshipBlock(persona: PersonaResult): string {
|
|
43
|
+
const strongTraits = (persona.traits ?? [])
|
|
44
|
+
.filter((t) => t.strength >= 0.7)
|
|
45
|
+
.sort((a, b) => b.strength - a.strength)
|
|
46
|
+
.map((t) => `**${t.name}** (${Math.round(t.strength * 100)}%): ${t.description}`)
|
|
47
|
+
.join("\n");
|
|
48
|
+
const sortedTopics = [...(persona.topics ?? [])]
|
|
49
|
+
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
50
|
+
.map((t) => `**${t.name}**: ${t.perspective} — ${t.approach}`)
|
|
51
|
+
.join("\n");
|
|
52
|
+
return [
|
|
53
|
+
"<!-- ei-relationship-injected -->",
|
|
54
|
+
"<ei-relationship>",
|
|
55
|
+
"## Ei: Relationship Context",
|
|
56
|
+
"",
|
|
57
|
+
persona.base_prompt ?? "",
|
|
58
|
+
"",
|
|
59
|
+
"### Working Style",
|
|
60
|
+
strongTraits || "(no traits above threshold)",
|
|
61
|
+
"",
|
|
62
|
+
"### Shared Context",
|
|
63
|
+
sortedTopics || "(no topics)",
|
|
64
|
+
"</ei-relationship>",
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
package/src/cli/install.ts
CHANGED
|
@@ -130,43 +130,45 @@ async function installCodexHooks(): Promise<void> {
|
|
|
130
130
|
const scriptContent = `#!/usr/bin/env bun
|
|
131
131
|
import { $ } from "bun";
|
|
132
132
|
|
|
133
|
-
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
134
|
-
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
135
|
-
const searchArgs = ["-n", "8"];
|
|
136
|
-
|
|
137
|
-
const sessionArgs = [];
|
|
138
|
-
if (input.transcript_path) {
|
|
139
|
-
sessionArgs.push("--transcript", input.transcript_path);
|
|
140
|
-
}
|
|
141
|
-
if (input.session_id) {
|
|
142
|
-
sessionArgs.push("--session", input.session_id, "--hook-source", "codex");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const args = raw ? [...searchArgs, ...sessionArgs, raw] : ["--recent", ...searchArgs];
|
|
146
|
-
|
|
147
133
|
async function runEi(commandArgs) {
|
|
148
134
|
const direct = await $\`ei \${commandArgs}\`.quiet().text().catch(() => "");
|
|
149
135
|
if (direct.trim()) return direct;
|
|
150
136
|
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
151
137
|
}
|
|
152
138
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
139
|
+
if (import.meta.main) {
|
|
140
|
+
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
141
|
+
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
142
|
+
const searchArgs = ["-n", "8"];
|
|
143
|
+
|
|
144
|
+
const sessionArgs = [];
|
|
145
|
+
if (input.transcript_path) {
|
|
146
|
+
sessionArgs.push("--transcript", input.transcript_path);
|
|
147
|
+
}
|
|
148
|
+
if (input.session_id) {
|
|
149
|
+
sessionArgs.push("--session", input.session_id, "--hook-source", "codex");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const args = raw ? [...searchArgs, ...sessionArgs, raw] : ["--recent", ...searchArgs];
|
|
153
|
+
|
|
154
|
+
const output = await runEi(args);
|
|
155
|
+
if (output.trim()) {
|
|
156
|
+
const heading = [
|
|
157
|
+
"## Ei Memory Context",
|
|
158
|
+
"*(The user cannot see this block. It is injected automatically before their message.)*",
|
|
159
|
+
"*(If you reference anything from it, briefly explain where it came from — e.g. \\"Ei shows you've been working on X\\" — so the user isn't confused by knowledge that appeared from nowhere.)*",
|
|
160
|
+
"",
|
|
161
|
+
"Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.",
|
|
162
|
+
"The following memories MAY be relevant to your current task — use \`ei_search\` or \`ei_lookup\` for targeted queries.",
|
|
163
|
+
].join("\\n");
|
|
164
|
+
|
|
165
|
+
process.stdout.write(JSON.stringify({
|
|
166
|
+
hookSpecificOutput: {
|
|
167
|
+
hookEventName: "UserPromptSubmit",
|
|
168
|
+
additionalContext: \`\\n\${heading}\\n\${output.trim()}\\n\`,
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
173
|
`;
|
|
172
174
|
|
|
@@ -279,7 +281,14 @@ async function installClaudeCodeHooks(): Promise<void> {
|
|
|
279
281
|
const scriptContent = `#!/usr/bin/env bun
|
|
280
282
|
import { $ } from "bun";
|
|
281
283
|
|
|
282
|
-
|
|
284
|
+
async function runEi(commandArgs) {
|
|
285
|
+
const direct = await $\`ei \${commandArgs}\`.quiet().text().catch(() => "");
|
|
286
|
+
if (direct.trim()) return direct;
|
|
287
|
+
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (import.meta.main) {
|
|
291
|
+
const heading = \`
|
|
283
292
|
## Ei Memory Context
|
|
284
293
|
*(The user cannot see this block. It is injected automatically before their message.)*
|
|
285
294
|
*(If you reference anything from it, briefly explain where it came from — e.g. "Ei shows you've been working on X" — so the user isn't confused by knowledge that appeared from nowhere.)*
|
|
@@ -288,25 +297,21 @@ Ei is a personal knowledge base built from the user's coding sessions, Slack, do
|
|
|
288
297
|
The following items MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
|
|
289
298
|
\`;
|
|
290
299
|
|
|
291
|
-
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
292
|
-
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
300
|
+
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
301
|
+
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
293
302
|
|
|
294
|
-
const sessionArgs = [];
|
|
295
|
-
if (input.session_id && input.hook_source) {
|
|
296
|
-
|
|
297
|
-
} else if (input.transcript_path) {
|
|
298
|
-
|
|
299
|
-
}
|
|
303
|
+
const sessionArgs = [];
|
|
304
|
+
if (input.session_id && input.hook_source) {
|
|
305
|
+
sessionArgs.push("--session", input.session_id, "--hook-source", input.hook_source);
|
|
306
|
+
} else if (input.transcript_path) {
|
|
307
|
+
sessionArgs.push("--transcript", input.transcript_path);
|
|
308
|
+
}
|
|
300
309
|
|
|
301
|
-
const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
|
|
310
|
+
const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
|
|
302
311
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (direct.trim()) return direct;
|
|
306
|
-
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
312
|
+
const output = await runEi(args);
|
|
313
|
+
if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
|
|
307
314
|
}
|
|
308
|
-
const output = await runEi(args);
|
|
309
|
-
if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
|
|
310
315
|
`;
|
|
311
316
|
|
|
312
317
|
await Bun.write(scriptPath, scriptContent);
|
|
@@ -558,7 +563,6 @@ async function installOmp(): Promise<void> {
|
|
|
558
563
|
const home = process.env.HOME || "~";
|
|
559
564
|
|
|
560
565
|
const extensionContent = `import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
561
|
-
import { Type } from "typebox";
|
|
562
566
|
import { $ } from "bun";
|
|
563
567
|
|
|
564
568
|
const runEi = async (cmdArgs: string[]): Promise<string> => {
|
|
@@ -567,8 +571,46 @@ const runEi = async (cmdArgs: string[]): Promise<string> => {
|
|
|
567
571
|
return $\`bunx ei-tui@latest \${cmdArgs}\`.quiet().text().catch(() => "");
|
|
568
572
|
};
|
|
569
573
|
|
|
574
|
+
// WHO block deduplication: Promise identity reuse — resolving is synchronous on subsequent calls.
|
|
575
|
+
const personaBlockFetch = new Map<string, Promise<string | null>>();
|
|
576
|
+
|
|
577
|
+
async function fetchPersonaBlock(name: string): Promise<string | null> {
|
|
578
|
+
try {
|
|
579
|
+
const block = await runEi(["personas", "--format", "prompt", "--", name]);
|
|
580
|
+
return block.trim() || null;
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
570
585
|
|
|
571
586
|
export default function eiIntegration(pi: ExtensionAPI) {
|
|
587
|
+
// WHO: inject <ei-relationship> block for the active primary persona.
|
|
588
|
+
// Prefer ctx.activePersonaName (OMP >= persona-tab-cycle PR); fall back to
|
|
589
|
+
// parsing "You are \\"<Name>\\"" from the HOW block in event.systemPrompt.
|
|
590
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
591
|
+
const joined = ((event as any).systemPrompt as string[] | undefined)?.join("\\n") ?? "";
|
|
592
|
+
const quoted = joined.match(/You are "([^"]+)"/);
|
|
593
|
+
const personaName: string | null =
|
|
594
|
+
(ctx as any).activePersonaName ??
|
|
595
|
+
(quoted?.[1]?.trim() || null);
|
|
596
|
+
if (!personaName) return undefined;
|
|
597
|
+
|
|
598
|
+
if (!personaBlockFetch.has(personaName)) {
|
|
599
|
+
personaBlockFetch.set(personaName, fetchPersonaBlock(personaName));
|
|
600
|
+
}
|
|
601
|
+
const block = await personaBlockFetch.get(personaName)!;
|
|
602
|
+
if (!block) return undefined;
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
message: {
|
|
606
|
+
customType: "ei-persona-who",
|
|
607
|
+
content: block,
|
|
608
|
+
display: false,
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// MEMORY: inject relevant Ei context based on the current prompt.
|
|
572
614
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
573
615
|
const entries = ctx.sessionManager.getEntries();
|
|
574
616
|
const recentMsgs = entries
|
|
@@ -584,12 +626,8 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
584
626
|
.join("\\n");
|
|
585
627
|
|
|
586
628
|
const prompt = event.prompt ?? "";
|
|
587
|
-
const args = prompt
|
|
588
|
-
? ["-n", "5", "--", prompt]
|
|
589
|
-
: ["--recent", "-n", "5"];
|
|
590
|
-
|
|
629
|
+
const args = prompt ? ["-n", "5", "--", prompt] : ["--recent", "-n", "5"];
|
|
591
630
|
const output = await runEi(args).catch(() => "");
|
|
592
|
-
|
|
593
631
|
if (!output.trim()) return undefined;
|
|
594
632
|
|
|
595
633
|
const heading = [
|
|
@@ -610,22 +648,25 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
610
648
|
};
|
|
611
649
|
});
|
|
612
650
|
|
|
651
|
+
// Tools use plain JSON Schema — no typebox import needed (not available in source mode).
|
|
613
652
|
pi.registerTool({
|
|
614
653
|
name: "ei_search",
|
|
615
654
|
label: "Search Ei Memory",
|
|
616
655
|
description: "Semantic search of Ei's personal knowledge base — facts, topics, people, quotes across all sources. Use when you need context about the user, their work, or anything Ei has learned.",
|
|
617
656
|
promptSnippet: "Search Ei's personal memory for relevant facts, topics, people, or quotes.",
|
|
618
|
-
parameters:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
657
|
+
parameters: {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {
|
|
660
|
+
query: { type: "string", description: "Natural language search query" },
|
|
661
|
+
type: {
|
|
662
|
+
type: "string",
|
|
663
|
+
enum: ["facts", "topics", "people", "quotes", "personas"],
|
|
664
|
+
description: "Filter to a specific data type. Omit for balanced results across all types.",
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
required: ["query"],
|
|
668
|
+
},
|
|
669
|
+
async execute(_id, params: { query: string; type?: string }, _signal, _onUpdate, _ctx) {
|
|
629
670
|
const args = params.type
|
|
630
671
|
? [params.type, "-n", "5", "--", params.query]
|
|
631
672
|
: ["-n", "5", "--", params.query];
|
|
@@ -641,10 +682,14 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
641
682
|
name: "ei_lookup",
|
|
642
683
|
label: "Lookup Ei Entity",
|
|
643
684
|
description: "Full-record lookup for a specific Ei entity (Fact, Topic, Person, Quote, or Persona) by ID. Use after ei_search to retrieve complete details for an item.",
|
|
644
|
-
parameters:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
685
|
+
parameters: {
|
|
686
|
+
type: "object",
|
|
687
|
+
properties: {
|
|
688
|
+
id: { type: "string", description: "Entity ID from ei_search results" },
|
|
689
|
+
},
|
|
690
|
+
required: ["id"],
|
|
691
|
+
},
|
|
692
|
+
async execute(_id, params: { id: string }, _signal, _onUpdate, _ctx) {
|
|
648
693
|
const output = await runEi(["--id", params.id]).catch(() => "");
|
|
649
694
|
return {
|
|
650
695
|
content: [{ type: "text" as const, text: output.trim() || "Not found" }],
|
|
@@ -675,8 +720,8 @@ async function installOpenCodePlugin(): Promise<void> {
|
|
|
675
720
|
import { join } from "path"
|
|
676
721
|
import { appendFileSync } from "fs"
|
|
677
722
|
|
|
678
|
-
|
|
679
|
-
const
|
|
723
|
+
// Deduplication: the Promise itself is re-awaited on subsequent calls (synchronous once resolved).
|
|
724
|
+
const personaFetch = new Map<string, Promise<string | null>>()
|
|
680
725
|
|
|
681
726
|
const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? "~", ".local", "share", "ei"), "ei-persona-plugin.log")
|
|
682
727
|
|
|
@@ -686,11 +731,7 @@ function log(msg: string) {
|
|
|
686
731
|
} catch {}
|
|
687
732
|
}
|
|
688
733
|
|
|
689
|
-
|
|
690
|
-
type PersonaTopic = { name: string; perspective: string; approach: string; exposure_current: number }
|
|
691
|
-
type PersonaResult = { display_name: string; base_prompt?: string; traits?: PersonaTrait[]; topics?: PersonaTopic[] }
|
|
692
|
-
|
|
693
|
-
// Pulls the agent name from the system prompt. Handles OMO's multiple formats:
|
|
734
|
+
// Pulls the agent name from the system prompt. Handles OMO/OMP formats:
|
|
694
735
|
// You are "Sisyphus" - ... (quoted, dash)
|
|
695
736
|
// You are "Sisyphus - Ultraworker" (quoted, dash in name)
|
|
696
737
|
// You are Atlas - ... (unquoted, dash)
|
|
@@ -704,51 +745,26 @@ export function extractAgentName(systemPrompt: string): string | null {
|
|
|
704
745
|
return null
|
|
705
746
|
}
|
|
706
747
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
748
|
+
const runEi = async (cmdArgs: string[]): Promise<string> => {
|
|
749
|
+
const direct = await $\`ei \${cmdArgs}\`.quiet().text().catch(() => "")
|
|
750
|
+
if (direct.trim()) return direct
|
|
751
|
+
return $\`bunx ei-tui@latest \${cmdArgs}\`.quiet().text().catch(() => "")
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Fetch the <ei-relationship> block for a named persona via the Ei CLI.
|
|
755
|
+
// Delegates all formatting to \`ei personas <name> --format prompt\` so
|
|
756
|
+
// the block format is maintained in one place.
|
|
757
|
+
async function fetchRelationshipBlock(rawName: string): Promise<string | null> {
|
|
710
758
|
try {
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const rawLower = rawName.toLowerCase()
|
|
716
|
-
const match = candidates.find((p) => {
|
|
717
|
-
const nameLower = p.display_name.toLowerCase()
|
|
718
|
-
return rawLower.includes(nameLower) || nameLower.includes(rawLower)
|
|
719
|
-
})
|
|
720
|
-
return match ?? null
|
|
759
|
+
const block = await runEi(["personas", "--format", "prompt", "--", rawName])
|
|
760
|
+
if (!block.trim() || block.includes("No saved state")) return null
|
|
761
|
+
log(\`ei-persona: injecting block for \${rawName}\`)
|
|
762
|
+
return block.trim()
|
|
721
763
|
} catch {
|
|
722
764
|
return null
|
|
723
765
|
}
|
|
724
766
|
}
|
|
725
767
|
|
|
726
|
-
function buildEiRelationshipBlock(persona: PersonaResult): string {
|
|
727
|
-
const strongTraits = (persona.traits ?? [])
|
|
728
|
-
.filter((t) => t.strength >= 0.7)
|
|
729
|
-
.sort((a, b) => b.strength - a.strength)
|
|
730
|
-
.map((t) => \`**\${t.name}** (\${Math.round(t.strength * 100)}%): \${t.description}\`)
|
|
731
|
-
.join("\\n")
|
|
732
|
-
const sortedTopics = [...(persona.topics ?? [])]
|
|
733
|
-
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
734
|
-
.map((t) => \`**\${t.name}**: \${t.perspective} — \${t.approach}\`)
|
|
735
|
-
.join("\\n")
|
|
736
|
-
return [
|
|
737
|
-
"<!-- ei-relationship-injected -->",
|
|
738
|
-
"<ei-relationship>",
|
|
739
|
-
"## Ei: Relationship Context",
|
|
740
|
-
"",
|
|
741
|
-
persona.base_prompt ?? "",
|
|
742
|
-
"",
|
|
743
|
-
"### Working Style",
|
|
744
|
-
strongTraits || "(no traits above threshold)",
|
|
745
|
-
"",
|
|
746
|
-
"### Shared Context",
|
|
747
|
-
sortedTopics || "(no topics)",
|
|
748
|
-
"</ei-relationship>",
|
|
749
|
-
].join("\\n")
|
|
750
|
-
}
|
|
751
|
-
|
|
752
768
|
export default async function EiPersonaPlugin() {
|
|
753
769
|
return {
|
|
754
770
|
name: "ei-persona",
|
|
@@ -760,26 +776,13 @@ export default async function EiPersonaPlugin() {
|
|
|
760
776
|
const rawName = extractAgentName(output.system[0])
|
|
761
777
|
if (!rawName) return
|
|
762
778
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
if (cached !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
768
|
-
output.system[0] = output.system[0] + "\\n\\n" + cached
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (!sessionFetch.has(cacheKey)) {
|
|
773
|
-
sessionFetch.set(cacheKey, (async () => {
|
|
774
|
-
const persona = await resolveEiPersona(rawName)
|
|
775
|
-
if (!persona) return null
|
|
776
|
-
log(\`ei-persona: injecting \${persona.display_name}\`)
|
|
777
|
-
return buildEiRelationshipBlock(persona)
|
|
778
|
-
})())
|
|
779
|
+
// Cache per persona name (not per session) — block only changes when the
|
|
780
|
+
// persona's Ei data changes, which is infrequent.
|
|
781
|
+
if (!personaFetch.has(rawName)) {
|
|
782
|
+
personaFetch.set(rawName, fetchRelationshipBlock(rawName))
|
|
779
783
|
}
|
|
780
784
|
|
|
781
|
-
const block = await
|
|
782
|
-
sessionCache.set(cacheKey, block)
|
|
785
|
+
const block = await personaFetch.get(rawName)!
|
|
783
786
|
if (block !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
784
787
|
output.system[0] = output.system[0] + "\\n\\n" + block
|
|
785
788
|
},
|
package/src/cli.ts
CHANGED
|
@@ -283,6 +283,7 @@ async function main(): Promise<void> {
|
|
|
283
283
|
session: { type: "string" },
|
|
284
284
|
"hook-source": { type: "string" },
|
|
285
285
|
transcript: { type: "string" },
|
|
286
|
+
format: { type: "string", short: "f" },
|
|
286
287
|
},
|
|
287
288
|
allowPositionals: true,
|
|
288
289
|
strict: true,
|
|
@@ -336,6 +337,8 @@ async function main(): Promise<void> {
|
|
|
336
337
|
? [...recentMessages, query].join(" ").trim()
|
|
337
338
|
: query;
|
|
338
339
|
|
|
340
|
+
const format = parsed.values.format?.trim();
|
|
341
|
+
|
|
339
342
|
let result;
|
|
340
343
|
if (targetType) {
|
|
341
344
|
const module = await import(`./cli/commands/${targetType}.js`);
|
|
@@ -346,6 +349,20 @@ async function main(): Promise<void> {
|
|
|
346
349
|
if (sourcePrefix && state) {
|
|
347
350
|
result = filterTypeSpecificBySource(result, state, sourcePrefix, targetType);
|
|
348
351
|
}
|
|
352
|
+
|
|
353
|
+
// --format prompt: output a formatted text block instead of JSON.
|
|
354
|
+
// Currently supported for personas only; other types tracked in GitHub issue #77.
|
|
355
|
+
if (format === "prompt" && targetType === "personas") {
|
|
356
|
+
// BUG-1 fix: when no persona matches, emit nothing and exit clean.
|
|
357
|
+
// Do NOT fall through to JSON — callers check block.trim() truthiness
|
|
358
|
+
// and "[]".trim() is truthy, corrupting system prompts.
|
|
359
|
+
if (!Array.isArray(result) || result.length === 0) {
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
const { buildEiRelationshipBlock } = await import("./cli/commands/personas.js");
|
|
363
|
+
process.stdout.write(buildEiRelationshipBlock(result[0]) + "\n");
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
349
366
|
} else {
|
|
350
367
|
result = await retrieveBalanced(enrichedQuery, limit, options);
|
|
351
368
|
if (personaId && state) {
|
|
@@ -169,7 +169,10 @@ export async function handleHumanTopicScan(response: LLMResponse, state: StateMa
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const context =
|
|
172
|
+
const context = {
|
|
173
|
+
...(response.request.data as unknown as ExtractionContext),
|
|
174
|
+
channelDisplayName: (response.request.data as Record<string, unknown>).personaDisplayName as string,
|
|
175
|
+
};
|
|
173
176
|
if (!context?.personaId) return;
|
|
174
177
|
|
|
175
178
|
const extractionModel = (response.request.data as Record<string, unknown>).extraction_model as string | undefined;
|
|
@@ -347,7 +350,10 @@ export async function handleEventScan(response: LLMResponse, state: StateManager
|
|
|
347
350
|
return;
|
|
348
351
|
}
|
|
349
352
|
|
|
350
|
-
const context =
|
|
353
|
+
const context = {
|
|
354
|
+
...(response.request.data as unknown as ExtractionContext),
|
|
355
|
+
channelDisplayName: (response.request.data as Record<string, unknown>).personaDisplayName as string,
|
|
356
|
+
};
|
|
351
357
|
if (!context?.personaId) return;
|
|
352
358
|
|
|
353
359
|
const extractionModel = (response.request.data as Record<string, unknown>).extraction_model as string | undefined;
|
package/src/core/llm-client.ts
CHANGED
|
@@ -310,10 +310,16 @@ export async function callLLMRaw(
|
|
|
310
310
|
headers["anthropic-dangerous-direct-browser-access"] = "true";
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// Omit temperature for models that don't accept it (e.g. Anthropic extended-thinking models).
|
|
314
|
+
// Also omit when thinking_budget > 0: Anthropic rejects temperature alongside thinking params.
|
|
315
|
+
const sendTemperature =
|
|
316
|
+
!modelConfig?.temperature_disabled &&
|
|
317
|
+
!(modelConfig?.thinking_budget !== undefined && modelConfig.thinking_budget > 0);
|
|
318
|
+
|
|
313
319
|
const requestBody: Record<string, unknown> = {
|
|
314
320
|
...(model !== undefined && { model }),
|
|
315
321
|
messages: finalMessages,
|
|
316
|
-
temperature,
|
|
322
|
+
...(sendTemperature && { temperature }),
|
|
317
323
|
max_tokens: modelConfig?.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
318
324
|
};
|
|
319
325
|
|
|
@@ -479,6 +479,7 @@ export function queueTopicUpdate(
|
|
|
479
479
|
next_step: LLMNextStep.HandleTopicUpdate,
|
|
480
480
|
data: {
|
|
481
481
|
...context,
|
|
482
|
+
personaDisplayName: context.channelDisplayName,
|
|
482
483
|
isNewItem,
|
|
483
484
|
existingItemId: existingItem?.id,
|
|
484
485
|
candidateName: isNewItem ? context.candidateName : undefined,
|
|
@@ -3,7 +3,6 @@ import type { PersonaTrait } from "../types.js";
|
|
|
3
3
|
import type { StateManager } from "../state-manager.js";
|
|
4
4
|
import type { IOpenCodeReader } from "../../integrations/opencode/types.js";
|
|
5
5
|
import { AGENT_ALIASES } from "../../integrations/opencode/types.js";
|
|
6
|
-
import { createOpenCodeReader } from "../../integrations/opencode/reader-factory.js";
|
|
7
6
|
import { DEFAULT_SEED_TRAITS } from "../constants/seed-traits.js";
|
|
8
7
|
|
|
9
8
|
const OPENCODE_GROUP = "OpenCode";
|
|
@@ -52,8 +51,7 @@ export async function ensureAgentPersona(
|
|
|
52
51
|
return existing;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const
|
|
56
|
-
const agentInfo = await agentReader.getAgentInfo(canonical);
|
|
54
|
+
const agentInfo = reader ? await reader.getAgentInfo(canonical) : null;
|
|
57
55
|
|
|
58
56
|
const now = new Date().toISOString();
|
|
59
57
|
const personaId = crypto.randomUUID();
|
|
@@ -60,6 +60,7 @@ export interface ModelConfig {
|
|
|
60
60
|
token_limit?: number; // Input token limit (user sets effective limit)
|
|
61
61
|
max_output_tokens?: number; // Output token limit (API-enforced)
|
|
62
62
|
thinking_budget?: number; // Thinking token budget: 0 = disabled, N = enable with N tokens, undefined = don't send
|
|
63
|
+
temperature_disabled?: boolean; // Set true for models that reject temperature (e.g. Anthropic extended-thinking models)
|
|
63
64
|
total_calls?: number; // Usage counter
|
|
64
65
|
total_tokens_in?: number; // Usage counter
|
|
65
66
|
total_tokens_out?: number; // Usage counter
|
|
@@ -20,11 +20,16 @@ import {
|
|
|
20
20
|
type IPiReader,
|
|
21
21
|
} from "./types.js";
|
|
22
22
|
import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
|
|
23
|
+
import {
|
|
24
|
+
ensureAgentPersona,
|
|
25
|
+
resolveCanonicalAgent,
|
|
26
|
+
} from "../../core/personas/opencode-agent.js";
|
|
23
27
|
|
|
24
28
|
export interface PiImportResult {
|
|
25
29
|
sessionsProcessed: number;
|
|
26
30
|
messagesImported: number;
|
|
27
31
|
personaCreated: boolean;
|
|
32
|
+
personasCreated: string[];
|
|
28
33
|
extractionScansQueued: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -121,6 +126,7 @@ export async function importPiSessions(options: PiImporterOptions): Promise<PiIm
|
|
|
121
126
|
sessionsProcessed: 0,
|
|
122
127
|
messagesImported: 0,
|
|
123
128
|
personaCreated: false,
|
|
129
|
+
personasCreated: [],
|
|
124
130
|
extractionScansQueued: 0,
|
|
125
131
|
};
|
|
126
132
|
|
|
@@ -167,56 +173,142 @@ export async function importPiSessions(options: PiImporterOptions): Promise<PiIm
|
|
|
167
173
|
|
|
168
174
|
if (signal?.aborted) return result;
|
|
169
175
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
176
|
+
const hasAgentAttribution = messages.some((m) => m.agent != null);
|
|
177
|
+
|
|
178
|
+
if (!hasAgentAttribution) {
|
|
179
|
+
// ─── Single-persona path (vanilla Pi / OMP without active agent) ────────
|
|
180
|
+
const personaExistedBefore = stateManager.persona_getByName(PI_PERSONA_NAME) !== null;
|
|
181
|
+
const persona = ensurePiPersona(stateManager, eiInterface);
|
|
182
|
+
result.personaCreated = !personaExistedBefore;
|
|
183
|
+
|
|
184
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
185
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
186
|
+
if (externalIds.length > 0) {
|
|
187
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
191
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
192
|
+
const toAnalyze: Message[] = [];
|
|
193
|
+
|
|
194
|
+
for (const msg of messages) {
|
|
195
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
196
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
197
|
+
const eiMsg = isOld
|
|
198
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
|
|
199
|
+
: convertToEiMessage(msg, targetSession.id, qualify);
|
|
200
|
+
|
|
201
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
202
|
+
result.messagesImported++;
|
|
203
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
stateManager.messages_sort(persona.id);
|
|
207
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
208
|
+
|
|
209
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
210
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
211
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
212
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
213
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
214
|
+
|
|
215
|
+
const context: ExtractionContext = {
|
|
216
|
+
personaId: persona.id,
|
|
217
|
+
channelDisplayName: persona.display_name,
|
|
218
|
+
messages_context: contextMsgs,
|
|
219
|
+
messages_analyze: toAnalyze,
|
|
220
|
+
sources: [`pi:${getMachineId()}:${targetSession.id}`],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
queuePersonRewritePhase(stateManager);
|
|
224
|
+
queueTopicRewritePhase(stateManager);
|
|
225
|
+
queueAllScans(context, stateManager, {
|
|
226
|
+
extraction_model: human.settings?.pi?.extraction_model,
|
|
227
|
+
external_filter: "only",
|
|
228
|
+
});
|
|
229
|
+
result.extractionScansQueued += 4;
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// ─── Multi-agent path (OMP sessions with agent attribution) ─────────────
|
|
233
|
+
const byPersonaId = new Map<string, { persona: NonNullable<ReturnType<typeof stateManager.persona_getByName>>; msgs: typeof messages; agentName: string }>();
|
|
234
|
+
|
|
235
|
+
for (const msg of messages) {
|
|
236
|
+
const agentName = msg.agent ?? PI_PERSONA_NAME;
|
|
237
|
+
let persona = stateManager.persona_getByName(agentName);
|
|
238
|
+
if (!persona) {
|
|
239
|
+
const { canonical } = resolveCanonicalAgent(agentName);
|
|
240
|
+
persona = stateManager.persona_getByName(canonical);
|
|
241
|
+
}
|
|
242
|
+
if (!persona) {
|
|
243
|
+
persona = await ensureAgentPersona(agentName, {
|
|
244
|
+
stateManager,
|
|
245
|
+
interface: eiInterface,
|
|
246
|
+
reader: undefined,
|
|
247
|
+
});
|
|
248
|
+
result.personasCreated.push(agentName);
|
|
249
|
+
}
|
|
250
|
+
const bucket = byPersonaId.get(persona.id);
|
|
251
|
+
if (bucket) {
|
|
252
|
+
bucket.msgs.push(msg);
|
|
253
|
+
} else {
|
|
254
|
+
byPersonaId.set(persona.id, { persona, msgs: [msg], agentName });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
259
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
260
|
+
let anyPersonaHasChanges = false;
|
|
261
|
+
|
|
262
|
+
for (const [, { persona, msgs: agentMsgs }] of byPersonaId) {
|
|
263
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
264
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
265
|
+
if (externalIds.length > 0) {
|
|
266
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const toAnalyze: Message[] = [];
|
|
270
|
+
for (const msg of agentMsgs) {
|
|
271
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
272
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
273
|
+
const eiMsg = isOld
|
|
274
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
|
|
275
|
+
: convertToEiMessage(msg, targetSession.id, qualify);
|
|
276
|
+
|
|
277
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
278
|
+
result.messagesImported++;
|
|
279
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stateManager.messages_sort(persona.id);
|
|
283
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
284
|
+
|
|
285
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
286
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
287
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
288
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
289
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
290
|
+
|
|
291
|
+
const context: ExtractionContext = {
|
|
292
|
+
personaId: persona.id,
|
|
293
|
+
channelDisplayName: persona.display_name,
|
|
294
|
+
messages_context: contextMsgs,
|
|
295
|
+
messages_analyze: toAnalyze,
|
|
296
|
+
sources: [`pi:${getMachineId()}:${targetSession.id}`],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
anyPersonaHasChanges = true;
|
|
300
|
+
queueAllScans(context, stateManager, {
|
|
301
|
+
extraction_model: human.settings?.pi?.extraction_model,
|
|
302
|
+
external_filter: "only",
|
|
303
|
+
});
|
|
304
|
+
result.extractionScansQueued += 4;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (anyPersonaHasChanges && !signal?.aborted) {
|
|
309
|
+
queuePersonRewritePhase(stateManager);
|
|
310
|
+
queueTopicRewritePhase(stateManager);
|
|
311
|
+
}
|
|
220
312
|
}
|
|
221
313
|
|
|
222
314
|
result.sessionsProcessed = 1;
|
|
@@ -49,6 +49,8 @@ export interface PiMessageEntry {
|
|
|
49
49
|
id: string;
|
|
50
50
|
parentId?: string;
|
|
51
51
|
timestamp: string;
|
|
52
|
+
/** Active agent definition name, present only for OMP sessions. Mirrors the `agent` field on OpenCode message rows. */
|
|
53
|
+
agent?: string;
|
|
52
54
|
message: PiMessagePayload;
|
|
53
55
|
[key: string]: unknown;
|
|
54
56
|
}
|
|
@@ -112,6 +114,8 @@ export interface PiMessage {
|
|
|
112
114
|
role: "user" | "assistant";
|
|
113
115
|
content: string;
|
|
114
116
|
timestamp: string;
|
|
117
|
+
/** Active agent definition name when this message was recorded; undefined for vanilla Pi or OMP without an active agent. */
|
|
118
|
+
agent?: string;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
// ============================================================================
|
|
@@ -63,8 +63,10 @@ export const ALL_PROVIDER_NAMES: ReadonlyArray<string> = [
|
|
|
63
63
|
// For example, Haiku's advertised context is 200k but real-world extraction quality degrades
|
|
64
64
|
// above ~100k, so we cap it there. When adding new models, prefer conservative values based
|
|
65
65
|
// on actual usage over marketing specs.
|
|
66
|
-
export const KNOWN_MODEL_LIMITS: Readonly<Record<string, { token_limit?: number; max_output_tokens?: number }>> = {
|
|
66
|
+
export const KNOWN_MODEL_LIMITS: Readonly<Record<string, { token_limit?: number; max_output_tokens?: number; temperature_disabled?: boolean }>> = {
|
|
67
67
|
// Anthropic — claude-opus-4.x
|
|
68
|
+
// Models from 4-8 onward always use extended thinking and reject the temperature parameter.
|
|
69
|
+
"claude-opus-4-8": { token_limit: 200000, max_output_tokens: 128000, temperature_disabled: true },
|
|
68
70
|
"claude-opus-4-7": { token_limit: 200000, max_output_tokens: 128000 },
|
|
69
71
|
"claude-opus-4-6": { token_limit: 200000, max_output_tokens: 128000 },
|
|
70
72
|
"claude-opus-4-5-20251101": { token_limit: 200000, max_output_tokens: 64000 },
|
|
@@ -361,9 +363,9 @@ export function buildProviderAccounts(
|
|
|
361
363
|
name: modelName,
|
|
362
364
|
...(limits?.token_limit !== undefined && { token_limit: limits.token_limit }),
|
|
363
365
|
...(limits?.max_output_tokens !== undefined && { max_output_tokens: limits.max_output_tokens }),
|
|
366
|
+
...(limits?.temperature_disabled === true && { temperature_disabled: true }),
|
|
364
367
|
};
|
|
365
368
|
};
|
|
366
|
-
|
|
367
369
|
const seenNames = new Set<string>();
|
|
368
370
|
const models: ModelConfig[] = [];
|
|
369
371
|
|