ei-tui 1.6.7 → 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 +253 -143
- 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
|
@@ -34,15 +34,26 @@ export async function installMcpClients(): Promise<void> {
|
|
|
34
34
|
console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const hasPi =
|
|
37
|
+
const hasPi =
|
|
38
|
+
await Bun.file(join(home, ".pi", "agent", "settings.json")).exists() ||
|
|
38
39
|
await Bun.file(join(home, ".pi", "agent", "auth.json")).exists();
|
|
39
|
-
const hasOmp = await Bun.file(join(home, ".omp", "agent", "settings.json")).exists() ||
|
|
40
|
-
await Bun.file(join(home, ".omp", "agent", "auth.json")).exists();
|
|
41
40
|
|
|
42
|
-
if (hasPi
|
|
41
|
+
if (hasPi) {
|
|
43
42
|
await installPi();
|
|
44
43
|
} else {
|
|
45
|
-
console.log(`ℹ️ Pi
|
|
44
|
+
console.log(`ℹ️ Pi not detected — skipping Pi extension install.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasOmp =
|
|
48
|
+
await Bun.file(join(home, ".omp", "agent", "settings.json")).exists() ||
|
|
49
|
+
await Bun.file(join(home, ".omp", "agent", "auth.json")).exists() ||
|
|
50
|
+
await Bun.file(join(home, ".omp", "agent", "config.yml")).exists() ||
|
|
51
|
+
await Bun.file(join(home, ".omp", "agent", "agent.db")).exists();
|
|
52
|
+
|
|
53
|
+
if (hasOmp) {
|
|
54
|
+
await installOmp();
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`ℹ️ OMP not detected — skipping OMP extension install.`);
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
|
|
@@ -119,43 +130,45 @@ async function installCodexHooks(): Promise<void> {
|
|
|
119
130
|
const scriptContent = `#!/usr/bin/env bun
|
|
120
131
|
import { $ } from "bun";
|
|
121
132
|
|
|
122
|
-
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
123
|
-
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
124
|
-
const searchArgs = ["-n", "8"];
|
|
125
|
-
|
|
126
|
-
const sessionArgs = [];
|
|
127
|
-
if (input.transcript_path) {
|
|
128
|
-
sessionArgs.push("--transcript", input.transcript_path);
|
|
129
|
-
}
|
|
130
|
-
if (input.session_id) {
|
|
131
|
-
sessionArgs.push("--session", input.session_id, "--hook-source", "codex");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const args = raw ? [...searchArgs, ...sessionArgs, raw] : ["--recent", ...searchArgs];
|
|
135
|
-
|
|
136
133
|
async function runEi(commandArgs) {
|
|
137
134
|
const direct = await $\`ei \${commandArgs}\`.quiet().text().catch(() => "");
|
|
138
135
|
if (direct.trim()) return direct;
|
|
139
136
|
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
172
|
}
|
|
160
173
|
`;
|
|
161
174
|
|
|
@@ -268,7 +281,14 @@ async function installClaudeCodeHooks(): Promise<void> {
|
|
|
268
281
|
const scriptContent = `#!/usr/bin/env bun
|
|
269
282
|
import { $ } from "bun";
|
|
270
283
|
|
|
271
|
-
|
|
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 = \`
|
|
272
292
|
## Ei Memory Context
|
|
273
293
|
*(The user cannot see this block. It is injected automatically before their message.)*
|
|
274
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.)*
|
|
@@ -277,20 +297,21 @@ Ei is a personal knowledge base built from the user's coding sessions, Slack, do
|
|
|
277
297
|
The following items MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
|
|
278
298
|
\`;
|
|
279
299
|
|
|
280
|
-
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
281
|
-
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();
|
|
282
302
|
|
|
283
|
-
const sessionArgs = [];
|
|
284
|
-
if (input.session_id && input.hook_source) {
|
|
285
|
-
|
|
286
|
-
} else if (input.transcript_path) {
|
|
287
|
-
|
|
288
|
-
}
|
|
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
|
+
}
|
|
289
309
|
|
|
290
|
-
const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
|
|
310
|
+
const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
|
|
291
311
|
|
|
292
|
-
const output = await
|
|
293
|
-
if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
|
|
312
|
+
const output = await runEi(args);
|
|
313
|
+
if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
|
|
314
|
+
}
|
|
294
315
|
`;
|
|
295
316
|
|
|
296
317
|
await Bun.write(scriptPath, scriptContent);
|
|
@@ -431,12 +452,18 @@ exit 0
|
|
|
431
452
|
|
|
432
453
|
async function installPi(): Promise<void> {
|
|
433
454
|
const home = process.env.HOME || "~";
|
|
434
|
-
const dataPath = process.env.EI_DATA_PATH ?? join(home, ".local", "share", "ei");
|
|
435
455
|
|
|
436
456
|
const extensionContent = `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
437
457
|
import { Type } from "typebox";
|
|
438
458
|
import { $ } from "bun";
|
|
439
459
|
|
|
460
|
+
const runEi = async (cmdArgs: string[]): Promise<string> => {
|
|
461
|
+
const direct = await $\`ei \${cmdArgs}\`.quiet().text().catch(() => "");
|
|
462
|
+
if (direct.trim()) return direct;
|
|
463
|
+
return $\`bunx ei-tui@latest \${cmdArgs}\`.quiet().text().catch(() => "");
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
|
|
440
467
|
export default function eiIntegration(pi: ExtensionAPI) {
|
|
441
468
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
442
469
|
const entries = ctx.sessionManager.getEntries();
|
|
@@ -457,11 +484,7 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
457
484
|
? ["-n", "5", "--", prompt]
|
|
458
485
|
: ["--recent", "-n", "5"];
|
|
459
486
|
|
|
460
|
-
const output = await
|
|
461
|
-
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
462
|
-
.quiet()
|
|
463
|
-
.text()
|
|
464
|
-
.catch(() => "");
|
|
487
|
+
const output = await runEi(args).catch(() => "");
|
|
465
488
|
|
|
466
489
|
if (!output.trim()) return undefined;
|
|
467
490
|
|
|
@@ -502,11 +525,7 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
502
525
|
const args = params.type
|
|
503
526
|
? [params.type, "-n", "5", "--", params.query]
|
|
504
527
|
: ["-n", "5", "--", params.query];
|
|
505
|
-
const output = await
|
|
506
|
-
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
507
|
-
.quiet()
|
|
508
|
-
.text()
|
|
509
|
-
.catch(() => "No results found");
|
|
528
|
+
const output = await runEi(args).catch(() => "");
|
|
510
529
|
return {
|
|
511
530
|
content: [{ type: "text" as const, text: output.trim() || "No results found" }],
|
|
512
531
|
details: {},
|
|
@@ -522,11 +541,7 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
522
541
|
id: Type.String({ description: "Entity ID from ei_search results" }),
|
|
523
542
|
}),
|
|
524
543
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
525
|
-
const output = await
|
|
526
|
-
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
527
|
-
.quiet()
|
|
528
|
-
.text()
|
|
529
|
-
.catch(() => "Not found");
|
|
544
|
+
const output = await runEi(["--id", params.id]).catch(() => "");
|
|
530
545
|
return {
|
|
531
546
|
content: [{ type: "text" as const, text: output.trim() || "Not found" }],
|
|
532
547
|
details: {},
|
|
@@ -536,28 +551,163 @@ export default function eiIntegration(pi: ExtensionAPI) {
|
|
|
536
551
|
}
|
|
537
552
|
`;
|
|
538
553
|
|
|
539
|
-
const
|
|
540
|
-
const ompExtDir = join(home, ".omp", "agent", "extensions");
|
|
554
|
+
const extDir = join(home, ".pi", "agent", "extensions");
|
|
541
555
|
const extFilename = "ei-integration.ts";
|
|
542
556
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
557
|
+
await Bun.$`mkdir -p ${extDir}`;
|
|
558
|
+
await Bun.write(join(extDir, extFilename), extensionContent);
|
|
559
|
+
console.log(`✓ Installed Ei extension to ~/.pi/agent/extensions/${extFilename}`);
|
|
560
|
+
}
|
|
547
561
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
562
|
+
async function installOmp(): Promise<void> {
|
|
563
|
+
const home = process.env.HOME || "~";
|
|
564
|
+
|
|
565
|
+
const extensionContent = `import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
566
|
+
import { $ } from "bun";
|
|
567
|
+
|
|
568
|
+
const runEi = async (cmdArgs: string[]): Promise<string> => {
|
|
569
|
+
const direct = await $\`ei \${cmdArgs}\`.quiet().text().catch(() => "");
|
|
570
|
+
if (direct.trim()) return direct;
|
|
571
|
+
return $\`bunx ei-tui@latest \${cmdArgs}\`.quiet().text().catch(() => "");
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// WHO block deduplication: Promise identity reuse — resolving is synchronous on subsequent calls.
|
|
575
|
+
const personaBlockFetch = new Map<string, Promise<string | null>>();
|
|
553
576
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
await
|
|
557
|
-
|
|
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;
|
|
558
583
|
}
|
|
559
584
|
}
|
|
560
585
|
|
|
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.
|
|
614
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
615
|
+
const entries = ctx.sessionManager.getEntries();
|
|
616
|
+
const recentMsgs = entries
|
|
617
|
+
.filter((e: any) => e.type === "message" && (e.message?.role === "user" || e.message?.role === "assistant"))
|
|
618
|
+
.slice(-5)
|
|
619
|
+
.map((e: any) => {
|
|
620
|
+
const role = e.message?.role ?? "unknown";
|
|
621
|
+
const text = Array.isArray(e.message?.content)
|
|
622
|
+
? e.message.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join(" ")
|
|
623
|
+
: (e.message?.content ?? "");
|
|
624
|
+
return \`\${role}: \${text.slice(0, 200)}\`;
|
|
625
|
+
})
|
|
626
|
+
.join("\\n");
|
|
627
|
+
|
|
628
|
+
const prompt = event.prompt ?? "";
|
|
629
|
+
const args = prompt ? ["-n", "5", "--", prompt] : ["--recent", "-n", "5"];
|
|
630
|
+
const output = await runEi(args).catch(() => "");
|
|
631
|
+
if (!output.trim()) return undefined;
|
|
632
|
+
|
|
633
|
+
const heading = [
|
|
634
|
+
"## Ei Memory Context",
|
|
635
|
+
"*(The user cannot see this block. It is injected automatically before their message.)*",
|
|
636
|
+
"*(If you reference anything from it, briefly explain where it came from.)*",
|
|
637
|
+
"",
|
|
638
|
+
"Ei is a personal knowledge base built from your coding sessions, Slack, documents, and conversations.",
|
|
639
|
+
"The following items MAY be relevant to your current task — use ei_search or ei_lookup for targeted queries.",
|
|
640
|
+
].join("\\n");
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
message: {
|
|
644
|
+
customType: "ei-context",
|
|
645
|
+
content: \`\${heading}\\n\\n\${output.trim()}\`,
|
|
646
|
+
display: false,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Tools use plain JSON Schema — no typebox import needed (not available in source mode).
|
|
652
|
+
pi.registerTool({
|
|
653
|
+
name: "ei_search",
|
|
654
|
+
label: "Search Ei Memory",
|
|
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.",
|
|
656
|
+
promptSnippet: "Search Ei's personal memory for relevant facts, topics, people, or quotes.",
|
|
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) {
|
|
670
|
+
const args = params.type
|
|
671
|
+
? [params.type, "-n", "5", "--", params.query]
|
|
672
|
+
: ["-n", "5", "--", params.query];
|
|
673
|
+
const output = await runEi(args).catch(() => "");
|
|
674
|
+
return {
|
|
675
|
+
content: [{ type: "text" as const, text: output.trim() || "No results found" }],
|
|
676
|
+
details: {},
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
pi.registerTool({
|
|
682
|
+
name: "ei_lookup",
|
|
683
|
+
label: "Lookup Ei Entity",
|
|
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.",
|
|
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) {
|
|
693
|
+
const output = await runEi(["--id", params.id]).catch(() => "");
|
|
694
|
+
return {
|
|
695
|
+
content: [{ type: "text" as const, text: output.trim() || "Not found" }],
|
|
696
|
+
details: {},
|
|
697
|
+
};
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
const extDir = join(home, ".omp", "agent", "extensions");
|
|
704
|
+
const extFilename = "ei-integration.ts";
|
|
705
|
+
|
|
706
|
+
await Bun.$`mkdir -p ${extDir}`;
|
|
707
|
+
await Bun.write(join(extDir, extFilename), extensionContent);
|
|
708
|
+
console.log(`✓ Installed Ei extension to ~/.omp/agent/extensions/${extFilename}`);
|
|
709
|
+
}
|
|
710
|
+
|
|
561
711
|
async function installOpenCodePlugin(): Promise<void> {
|
|
562
712
|
const home = process.env.HOME || "~";
|
|
563
713
|
const opencodeDir = join(home, ".config", "opencode");
|
|
@@ -570,8 +720,8 @@ async function installOpenCodePlugin(): Promise<void> {
|
|
|
570
720
|
import { join } from "path"
|
|
571
721
|
import { appendFileSync } from "fs"
|
|
572
722
|
|
|
573
|
-
|
|
574
|
-
const
|
|
723
|
+
// Deduplication: the Promise itself is re-awaited on subsequent calls (synchronous once resolved).
|
|
724
|
+
const personaFetch = new Map<string, Promise<string | null>>()
|
|
575
725
|
|
|
576
726
|
const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? "~", ".local", "share", "ei"), "ei-persona-plugin.log")
|
|
577
727
|
|
|
@@ -581,11 +731,7 @@ function log(msg: string) {
|
|
|
581
731
|
} catch {}
|
|
582
732
|
}
|
|
583
733
|
|
|
584
|
-
|
|
585
|
-
type PersonaTopic = { name: string; perspective: string; approach: string; exposure_current: number }
|
|
586
|
-
type PersonaResult = { display_name: string; base_prompt?: string; traits?: PersonaTrait[]; topics?: PersonaTopic[] }
|
|
587
|
-
|
|
588
|
-
// 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:
|
|
589
735
|
// You are "Sisyphus" - ... (quoted, dash)
|
|
590
736
|
// You are "Sisyphus - Ultraworker" (quoted, dash in name)
|
|
591
737
|
// You are Atlas - ... (unquoted, dash)
|
|
@@ -599,50 +745,26 @@ export function extractAgentName(systemPrompt: string): string | null {
|
|
|
599
745
|
return null
|
|
600
746
|
}
|
|
601
747
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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> {
|
|
605
758
|
try {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const match = candidates.find((p) => {
|
|
611
|
-
const nameLower = p.display_name.toLowerCase()
|
|
612
|
-
return rawLower.includes(nameLower) || nameLower.includes(rawLower)
|
|
613
|
-
})
|
|
614
|
-
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()
|
|
615
763
|
} catch {
|
|
616
764
|
return null
|
|
617
765
|
}
|
|
618
766
|
}
|
|
619
767
|
|
|
620
|
-
function buildEiRelationshipBlock(persona: PersonaResult): string {
|
|
621
|
-
const strongTraits = (persona.traits ?? [])
|
|
622
|
-
.filter((t) => t.strength >= 0.7)
|
|
623
|
-
.sort((a, b) => b.strength - a.strength)
|
|
624
|
-
.map((t) => \`**\${t.name}** (\${Math.round(t.strength * 100)}%): \${t.description}\`)
|
|
625
|
-
.join("\\n")
|
|
626
|
-
const sortedTopics = [...(persona.topics ?? [])]
|
|
627
|
-
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
628
|
-
.map((t) => \`**\${t.name}**: \${t.perspective} — \${t.approach}\`)
|
|
629
|
-
.join("\\n")
|
|
630
|
-
return [
|
|
631
|
-
"<!-- ei-relationship-injected -->",
|
|
632
|
-
"<ei-relationship>",
|
|
633
|
-
"## Ei: Relationship Context",
|
|
634
|
-
"",
|
|
635
|
-
persona.base_prompt ?? "",
|
|
636
|
-
"",
|
|
637
|
-
"### Working Style",
|
|
638
|
-
strongTraits || "(no traits above threshold)",
|
|
639
|
-
"",
|
|
640
|
-
"### Shared Context",
|
|
641
|
-
sortedTopics || "(no topics)",
|
|
642
|
-
"</ei-relationship>",
|
|
643
|
-
].join("\\n")
|
|
644
|
-
}
|
|
645
|
-
|
|
646
768
|
export default async function EiPersonaPlugin() {
|
|
647
769
|
return {
|
|
648
770
|
name: "ei-persona",
|
|
@@ -650,29 +772,17 @@ export default async function EiPersonaPlugin() {
|
|
|
650
772
|
input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } },
|
|
651
773
|
output: { system: string[] },
|
|
652
774
|
): Promise<void> => {
|
|
653
|
-
|
|
775
|
+
if (!Array.isArray(output.system) || typeof output.system[0] !== "string") return
|
|
776
|
+
const rawName = extractAgentName(output.system[0])
|
|
654
777
|
if (!rawName) return
|
|
655
778
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
if (cached !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
661
|
-
output.system[0] = output.system[0] + "\\n\\n" + cached
|
|
662
|
-
return
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (!sessionFetch.has(cacheKey)) {
|
|
666
|
-
sessionFetch.set(cacheKey, (async () => {
|
|
667
|
-
const persona = await resolveEiPersona(rawName)
|
|
668
|
-
if (!persona) return null
|
|
669
|
-
log(\`ei-persona: injecting \${persona.display_name}\`)
|
|
670
|
-
return buildEiRelationshipBlock(persona)
|
|
671
|
-
})())
|
|
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))
|
|
672
783
|
}
|
|
673
784
|
|
|
674
|
-
const block = await
|
|
675
|
-
sessionCache.set(cacheKey, block)
|
|
785
|
+
const block = await personaFetch.get(rawName)!
|
|
676
786
|
if (block !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
677
787
|
output.system[0] = output.system[0] + "\\n\\n" + block
|
|
678
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
|
|