ei-tui 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +21 -14
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +6 -5
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +21 -19
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/format-utils.ts +23 -0
- package/src/core/handlers/human-matching.ts +53 -35
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +13 -9
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/entities.ts +1 -0
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +4 -11
- package/src/prompts/response/sections.ts +22 -10
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +154 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PersonaTrait, Message, PersonaTopic } from "../../core/types.js";
|
|
2
|
+
import type { ExposureImpact } from "../human/types.js";
|
|
2
3
|
|
|
3
4
|
export interface PromptOutput {
|
|
4
5
|
system: string;
|
|
@@ -70,6 +71,6 @@ export interface PersonaTopicUpdateResult {
|
|
|
70
71
|
approach: string; // How they engage - populate if clear signal
|
|
71
72
|
personal_stake: string; // Why it matters - populate if clear signal
|
|
72
73
|
sentiment: number;
|
|
73
|
-
|
|
74
|
+
exposure_impact: ExposureImpact;
|
|
74
75
|
exposure_desired: number;
|
|
75
76
|
}
|
|
@@ -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}
|
|
@@ -84,7 +81,7 @@ ${conversationState}
|
|
|
84
81
|
- Format your response as specified in the Response Format section above.`
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
const RESPONSE_FORMAT_INSTRUCTION = `
|
|
84
|
+
const RESPONSE_FORMAT_INSTRUCTION = `Call the \`submit_response\` tool with your response. If the tool is unavailable, use the JSON format specified in the Response Format section.`;
|
|
88
85
|
|
|
89
86
|
/**
|
|
90
87
|
* Standard system prompt for non-Ei personas
|
|
@@ -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
|
|
|
@@ -154,7 +155,7 @@ export function buildHumanSection(human: ResponsePromptData["human"]): string {
|
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
// Active topics (exposure_current > 0.3)
|
|
157
|
-
const activeTopics = human.
|
|
158
|
+
const activeTopics = human.active_topics;
|
|
158
159
|
if (activeTopics.length > 0) {
|
|
159
160
|
const topics = activeTopics
|
|
160
161
|
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
@@ -216,8 +217,7 @@ export function buildPrioritiesSection(
|
|
|
216
217
|
): string {
|
|
217
218
|
const priorities: string[] = [];
|
|
218
219
|
|
|
219
|
-
const yourNeeds = persona.
|
|
220
|
-
.filter(t => t.exposure_desired - t.exposure_current > 0.2)
|
|
220
|
+
const yourNeeds = persona.interested_topics
|
|
221
221
|
.slice(0, 3)
|
|
222
222
|
.map(t => `- Bring up "${t.name}" - ${t.perspective || t.name}`);
|
|
223
223
|
|
|
@@ -226,8 +226,7 @@ export function buildPrioritiesSection(
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Their needs (topics they might want to discuss)
|
|
229
|
-
const theirNeeds = human.
|
|
230
|
-
.filter(t => t.exposure_desired - t.exposure_current > 0.2)
|
|
229
|
+
const theirNeeds = human.interested_topics
|
|
231
230
|
.slice(0, 3)
|
|
232
231
|
.map(t => `- They might want to talk about "${t.name}"`);
|
|
233
232
|
|
|
@@ -264,8 +263,7 @@ export function getConversationStateText(delayMs: number): string {
|
|
|
264
263
|
// =============================================================================
|
|
265
264
|
|
|
266
265
|
function formatDate(isoString: string): string {
|
|
267
|
-
|
|
268
|
-
return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
266
|
+
return formatTimestamp(isoString);
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["human"]): string {
|
|
@@ -312,6 +310,9 @@ export function buildSystemKnowledgeSection(isTUI: boolean): string {
|
|
|
312
310
|
const editorNotes = isTUI ? "Ctrl+E to open their editor" : "Ctrl+L to focus the input box";
|
|
313
311
|
const helpNotes = isTUI ? "`/h[elp]` to see all the commands" : "'Help' is in the Upper-right menu";
|
|
314
312
|
const settingsAction = isTUI ? "`/settings` command" : "Hamburger Menu, Top-Right of screen";
|
|
313
|
+
const createRoomAction = isTUI
|
|
314
|
+
? "Use the `/r[oom] new` command. A YAML editor will open where you set the room name, mode, and which personas participate."
|
|
315
|
+
: "Click the [+] button in the Rooms panel on the left.";
|
|
315
316
|
const leftPanelNotes = isTUI ? "\n- Can be hidden with Ctrl+B" : `
|
|
316
317
|
- Hover over a persona to see controls: pause, edit (Pencil), archive, delete (Trash)
|
|
317
318
|
- Click a persona to switch conversations
|
|
@@ -354,6 +355,16 @@ Additionally, if the user wants a Persona to feel "Fresh" without prior knowledg
|
|
|
354
355
|
- Shows all personas (you're always at the top)
|
|
355
356
|
${leftPanelNotes}
|
|
356
357
|
|
|
358
|
+
## Rooms
|
|
359
|
+
|
|
360
|
+
Rooms are shared multi-persona conversations — a space where the Human and multiple personas talk in the same thread. Three modes are available:
|
|
361
|
+
|
|
362
|
+
- **Free For All (FFA)**: Everyone responds to every message. The conversation builds naturally from all voices.
|
|
363
|
+
- **Choose Your Path (CYP)**: At each fork, the conversation branches. The Human navigates which path to follow, choosing which response moves the story forward.
|
|
364
|
+
- **Messages Against Persona (MAP)**: Everyone — personas and Human — submits a response, and a designated Judge persona picks which one continues the conversation. Personas must stay true to their identity; the Human has no such constraint.
|
|
365
|
+
|
|
366
|
+
**To create a room**: ${createRoomAction}
|
|
367
|
+
|
|
357
368
|
## Learning About the Human
|
|
358
369
|
As the human chats, the system learns about them:
|
|
359
370
|
- **Facts**: Objective information (job, location, family members)
|
|
@@ -380,7 +391,8 @@ ${externalImportNotes}
|
|
|
380
391
|
### Tips You Can Share
|
|
381
392
|
- If they want to talk to a persona privately, tell them about the "Groups" functionality
|
|
382
393
|
- If they want you to remember something specific, tell them about the quote capture feature (${viewQuotesAction})
|
|
383
|
-
- Pausing the system (Escape) immediately stops AI processing but preserves messages
|
|
394
|
+
- Pausing the system (Escape) immediately stops AI processing but preserves messages
|
|
395
|
+
- Rooms are a great way to get multiple perspectives on the same question at once — especially MAP mode, where the Human can play to a persona's known preferences`;
|
|
384
396
|
}
|
|
385
397
|
|
|
386
398
|
// =============================================================================
|
|
@@ -419,7 +431,7 @@ export function buildResponseFormatSection(): string {
|
|
|
419
431
|
|
|
420
432
|
return `## Response Format
|
|
421
433
|
|
|
422
|
-
|
|
434
|
+
When you are ready to respond, call the \`submit_response\` tool with one of these forms:
|
|
423
435
|
|
|
424
436
|
**Words only** (most common):
|
|
425
437
|
\`\`\`json
|
|
@@ -447,7 +459,7 @@ Rules:
|
|
|
447
459
|
- \`reason\` is only used when \`should_respond\` is false
|
|
448
460
|
- Do NOT include \`<thinking>\` blocks or analysis outside the JSON
|
|
449
461
|
- The JSON must be valid - use double quotes, no trailing commas
|
|
450
|
-
-
|
|
462
|
+
- If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
// =============================================================================
|
|
@@ -17,12 +17,18 @@ export interface ResponsePromptData {
|
|
|
17
17
|
long_description?: string;
|
|
18
18
|
traits: PersonaTrait[];
|
|
19
19
|
topics: PersonaTopic[];
|
|
20
|
+
/** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
|
|
21
|
+
interested_topics: PersonaTopic[];
|
|
20
22
|
};
|
|
21
23
|
human: {
|
|
22
24
|
facts: Fact[];
|
|
23
25
|
topics: Topic[];
|
|
24
26
|
people: Person[];
|
|
25
27
|
quotes: Quote[];
|
|
28
|
+
/** Pre-filtered: topics where exposure_current > 0.3 */
|
|
29
|
+
active_topics: Topic[];
|
|
30
|
+
/** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
|
|
31
|
+
interested_topics: Topic[];
|
|
26
32
|
};
|
|
27
33
|
visible_personas: Array<{ name: string; short_description?: string }>;
|
|
28
34
|
delay_ms: number;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room Prompt Builders
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RoomResponsePromptData, RoomJudgePromptData, PromptOutput } from "./types.js";
|
|
6
|
+
import { formatCurrentTime } from "../../core/format-utils.js";
|
|
7
|
+
import {
|
|
8
|
+
buildRoomParticipantsSection,
|
|
9
|
+
buildRoomHistorySection,
|
|
10
|
+
buildRoomGuidelinesSection,
|
|
11
|
+
buildRoomTraitsSection,
|
|
12
|
+
buildRoomTopicsSection,
|
|
13
|
+
buildRoomResponseFormatSection,
|
|
14
|
+
buildJudgeCandidatesSection,
|
|
15
|
+
buildJudgeDecisionFormatSection,
|
|
16
|
+
} from "./sections.js";
|
|
17
|
+
import {
|
|
18
|
+
buildHumanSection,
|
|
19
|
+
buildQuotesSection,
|
|
20
|
+
buildToolsSection,
|
|
21
|
+
} from "../response/sections.js";
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
RoomResponsePromptData,
|
|
25
|
+
RoomJudgePromptData,
|
|
26
|
+
RoomJudgeResult,
|
|
27
|
+
RoomParticipantIdentity,
|
|
28
|
+
RoomHistoryMessage,
|
|
29
|
+
RoomJudgeCandidate,
|
|
30
|
+
PersonaResponseResult,
|
|
31
|
+
PromptOutput,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOutput {
|
|
35
|
+
const { responding_persona: persona, room, other_participants, human, history, tools } = data;
|
|
36
|
+
|
|
37
|
+
const name = persona.name;
|
|
38
|
+
const desc = persona.long_description || persona.short_description || "a conversational participant";
|
|
39
|
+
const aliasText = persona.aliases.length > 0 ? ` (also known as: ${persona.aliases.join(", ")})` : "";
|
|
40
|
+
|
|
41
|
+
const identity = `You are ${name}${aliasText}.\n\n${desc}`;
|
|
42
|
+
const traits = buildRoomTraitsSection(persona.traits);
|
|
43
|
+
const topics = buildRoomTopicsSection(persona.topics);
|
|
44
|
+
const humanSection = buildHumanSection(human);
|
|
45
|
+
const quotesSection = buildQuotesSection(human.quotes, human);
|
|
46
|
+
const participants = buildRoomParticipantsSection(other_participants);
|
|
47
|
+
const guidelines = buildRoomGuidelinesSection(name, data.room.mode);
|
|
48
|
+
const responseFormat = buildRoomResponseFormatSection();
|
|
49
|
+
const toolsSection = tools && tools.length > 0 ? buildToolsSection() : "";
|
|
50
|
+
const currentTime = formatCurrentTime();
|
|
51
|
+
|
|
52
|
+
const system = [
|
|
53
|
+
identity,
|
|
54
|
+
traits,
|
|
55
|
+
topics,
|
|
56
|
+
humanSection,
|
|
57
|
+
quotesSection,
|
|
58
|
+
participants,
|
|
59
|
+
`## The Room: ${room.display_name}`,
|
|
60
|
+
`You are participating in a shared multi-persona conversation. Speak as yourself — everyone else in the room can read your words.`,
|
|
61
|
+
guidelines,
|
|
62
|
+
responseFormat,
|
|
63
|
+
toolsSection,
|
|
64
|
+
`Current time: ${currentTime}`,
|
|
65
|
+
].filter(Boolean).join("\n\n");
|
|
66
|
+
|
|
67
|
+
const user = buildRoomHistorySection(history) +
|
|
68
|
+
`\n\nRespond to the conversation above as ${name}. Call the \`submit_response\` tool with your response. If the tool is unavailable, use the JSON format in the Response Format section.`;
|
|
69
|
+
|
|
70
|
+
return { system, user };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildRoomJudgePrompt(data: RoomJudgePromptData): PromptOutput {
|
|
74
|
+
const { judge_persona: judge, room, context, candidates } = data;
|
|
75
|
+
|
|
76
|
+
const desc = judge.long_description || judge.short_description || "a discerning judge";
|
|
77
|
+
const topTraits = judge.traits
|
|
78
|
+
.sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5))
|
|
79
|
+
.slice(0, 8)
|
|
80
|
+
.map(t => `- **${t.name}**: ${t.description}`)
|
|
81
|
+
.join("\n");
|
|
82
|
+
|
|
83
|
+
const traitsBlock = topTraits ? `## Your Character\n\nYour personality is your rubric. Let it guide your choice.\n\n${topTraits}` : "";
|
|
84
|
+
|
|
85
|
+
const roleSection = `## Your Role in "${room.display_name}"
|
|
86
|
+
|
|
87
|
+
The conversation has reached a fork. Multiple participants have responded to the same moment, and it falls to you to decide which response the conversation continues from.
|
|
88
|
+
|
|
89
|
+
There is no objectively correct answer. Pick the response you find most interesting, surprising, true, or fitting — the one that feels most alive to you, given who you are.
|
|
90
|
+
|
|
91
|
+
**The MAP dynamic**: Every participant — personas and the Human alike — can see your description and traits. They have been crafting their responses specifically to appeal to your tastes. Personas are also constrained to stay true to their own identities; the Human is not. Factor that in if you choose to.`;
|
|
92
|
+
|
|
93
|
+
const contextSection = context.length > 0
|
|
94
|
+
? buildRoomHistorySection(context)
|
|
95
|
+
: "";
|
|
96
|
+
|
|
97
|
+
const candidatesSection = buildJudgeCandidatesSection(candidates);
|
|
98
|
+
const decisionSection = buildJudgeDecisionFormatSection();
|
|
99
|
+
const currentTime = formatCurrentTime();
|
|
100
|
+
|
|
101
|
+
const system = [
|
|
102
|
+
`You are ${judge.name}.\n\n${desc}`,
|
|
103
|
+
traitsBlock,
|
|
104
|
+
roleSection,
|
|
105
|
+
`Current time: ${currentTime}`,
|
|
106
|
+
].filter(Boolean).join("\n\n");
|
|
107
|
+
|
|
108
|
+
const user = [
|
|
109
|
+
contextSection,
|
|
110
|
+
candidatesSection,
|
|
111
|
+
decisionSection,
|
|
112
|
+
].filter(Boolean).join("\n\n");
|
|
113
|
+
|
|
114
|
+
return { system, user };
|
|
115
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room Prompt Section Builders
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RoomParticipantIdentity, RoomHistoryMessage, RoomJudgeCandidate } from "./types.js";
|
|
6
|
+
import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
|
|
7
|
+
import { RoomMode } from "../../core/types.js";
|
|
8
|
+
|
|
9
|
+
const DESCRIPTION_MAX_CHARS = 500;
|
|
10
|
+
|
|
11
|
+
function truncate(s: string): string {
|
|
12
|
+
return s.length <= DESCRIPTION_MAX_CHARS ? s : s.slice(0, DESCRIPTION_MAX_CHARS) + "…";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildRoomParticipantsSection(participants: RoomParticipantIdentity[]): string {
|
|
16
|
+
if (participants.length === 0) return "";
|
|
17
|
+
|
|
18
|
+
const lines = participants.map(p => {
|
|
19
|
+
const desc = p.long_description || p.short_description;
|
|
20
|
+
const topTraits = p.traits
|
|
21
|
+
.sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5))
|
|
22
|
+
.slice(0, 5)
|
|
23
|
+
.map(t => ` - **${t.name}**: ${truncate(t.description)}`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
|
|
26
|
+
const header = p.is_human ? `### Human` : `### ${p.name}`;
|
|
27
|
+
const descLine = desc ? `\n${truncate(desc)}` : "";
|
|
28
|
+
const traitBlock = topTraits ? `\n${topTraits}` : "";
|
|
29
|
+
return `${header}${descLine}${traitBlock}`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return `## Others in the Room\n\n${lines.join("\n\n")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildRoomHistorySection(history: RoomHistoryMessage[]): string {
|
|
36
|
+
if (history.length === 0) return "";
|
|
37
|
+
|
|
38
|
+
const lines = history.map(msg => {
|
|
39
|
+
const speaker = msg.speaker_id === "human" ? "Human" : msg.speaker_name;
|
|
40
|
+
if (msg.silence_reason) {
|
|
41
|
+
return `**${speaker}**: *[chose not to respond: ${msg.silence_reason}]*`;
|
|
42
|
+
}
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (msg.verbal_response) parts.push(msg.verbal_response);
|
|
45
|
+
if (msg.action_response) parts.push(`*${msg.action_response}*`);
|
|
46
|
+
return `**${speaker}**: ${parts.join(" ")}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return `## Conversation So Far\n\n${lines.join("\n\n")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildRoomGuidelinesSection(personaName: string, mode?: RoomMode): string {
|
|
53
|
+
const baseGuidelines = `## Guidelines
|
|
54
|
+
|
|
55
|
+
- You are one voice among several — contribute authentically, not performatively
|
|
56
|
+
- Respond to the thread as a whole; you may address specific participants by name
|
|
57
|
+
- You may agree with, challenge, build on, or be surprised by what others have said
|
|
58
|
+
- Match the conversational energy — a brief exchange doesn't call for a monologue
|
|
59
|
+
- **Silence is always valid.** Your silence reason will be visible to all participants, so make it honest
|
|
60
|
+
- Be yourself — don't suppress your perspective just because others are present
|
|
61
|
+
- NEVER repeat or echo what was just said. Start directly with your own reaction.
|
|
62
|
+
- Format your response as specified in the Response Format section.
|
|
63
|
+
- ${personaName.toLowerCase() === "ei" ? "As Ei, you can see the full room dynamic and may help facilitate if things get stuck." : "Don't break character to comment on the room mechanics."}`;
|
|
64
|
+
|
|
65
|
+
if (mode === RoomMode.MessagesAgainstPersona) {
|
|
66
|
+
return baseGuidelines + `
|
|
67
|
+
- **MAP Mode — Messages Against Persona**: This is a competition. Everyone (including you) is trying to craft the response the Judge selects to continue the conversation.
|
|
68
|
+
- Your primary goal: provide the message the Judge thinks best moves the conversation forward. Study their description and traits above — understand what resonates with them.
|
|
69
|
+
- Your submission must be true to your identity. You cannot abandon who you are to win.
|
|
70
|
+
- Your advantage: you have direct access to the Judge's full description and personality traits. Use them. The Human's advantage: they are not bound by the identity constraint above.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return baseGuidelines;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildRoomTraitsSection(traits: PersonaTrait[]): string {
|
|
77
|
+
if (traits.length === 0) return "";
|
|
78
|
+
const sorted = [...traits].sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5)).slice(0, 12);
|
|
79
|
+
const lines = sorted.map(t => {
|
|
80
|
+
const pct = t.strength !== undefined ? ` (${Math.round(t.strength * 100)}%)` : "";
|
|
81
|
+
return `- **${t.name}**${pct}: ${truncate(t.description)}`;
|
|
82
|
+
});
|
|
83
|
+
return `## Your Personality\n\n${lines.join("\n")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildRoomTopicsSection(topics: PersonaTopic[]): string {
|
|
87
|
+
if (topics.length === 0) return "";
|
|
88
|
+
const sorted = [...topics]
|
|
89
|
+
.sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
|
|
90
|
+
.slice(0, 8);
|
|
91
|
+
const lines = sorted.map(t => `- **${t.name}**: ${t.perspective}`);
|
|
92
|
+
return `## Your Interests\n\n${lines.join("\n")}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildRoomResponseFormatSection(): string {
|
|
96
|
+
return `## Response Format
|
|
97
|
+
|
|
98
|
+
When you are ready to respond, call the \`submit_response\` tool. Silence reasons are visible to everyone in the room, so be honest.
|
|
99
|
+
|
|
100
|
+
**Words:**
|
|
101
|
+
\`\`\`json
|
|
102
|
+
{ "should_respond": true, "verbal_response": "What you say" }
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
**Action:**
|
|
106
|
+
\`\`\`json
|
|
107
|
+
{ "should_respond": true, "action_response": "What you do (rendered in italics)" }
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
**Words and action:**
|
|
111
|
+
\`\`\`json
|
|
112
|
+
{ "should_respond": true, "verbal_response": "What you say", "action_response": "What you do" }
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
**Silent:**
|
|
116
|
+
\`\`\`json
|
|
117
|
+
{ "should_respond": false, "reason": "Why you're not speaking — this will be shown to other participants" }
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
Rules:
|
|
121
|
+
- At least one of verbal_response or action_response must be present when should_respond is true
|
|
122
|
+
- reason is only used when should_respond is false
|
|
123
|
+
- If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[]): string {
|
|
127
|
+
const lines = candidates.map((c, i) => {
|
|
128
|
+
const speaker = c.speaker_id === "human" ? "Human" : c.speaker_name;
|
|
129
|
+
const content = c.silence_reason
|
|
130
|
+
? `*[chose not to respond: ${c.silence_reason}]*`
|
|
131
|
+
: [c.verbal_response, c.action_response ? `*${c.action_response}*` : ""].filter(Boolean).join(" ");
|
|
132
|
+
return `### Option ${i + 1} — ${speaker}\nMessage ID: \`${c.message_id}\`\n\n${content}`;
|
|
133
|
+
});
|
|
134
|
+
return `## The Responses\n\n${lines.join("\n\n")}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildJudgeDecisionFormatSection(): string {
|
|
138
|
+
return `## Your Decision
|
|
139
|
+
|
|
140
|
+
Return a JSON object selecting the response you want to follow:
|
|
141
|
+
|
|
142
|
+
\`\`\`json
|
|
143
|
+
{
|
|
144
|
+
"winner_message_id": "the-exact-message-id-from-above",
|
|
145
|
+
"reason": "Why this one — optional but appreciated"
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
Your entire reply must be this JSON object.`;
|
|
150
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room Prompt Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic, ToolDefinition } from "../../core/types.js";
|
|
6
|
+
import type { RoomMode } from "../../core/types.js";
|
|
7
|
+
|
|
8
|
+
export interface RoomParticipantIdentity {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
short_description?: string;
|
|
12
|
+
long_description?: string;
|
|
13
|
+
traits: PersonaTrait[];
|
|
14
|
+
is_human: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RoomHistoryMessage {
|
|
18
|
+
speaker_name: string;
|
|
19
|
+
speaker_id: string;
|
|
20
|
+
verbal_response?: string;
|
|
21
|
+
action_response?: string;
|
|
22
|
+
silence_reason?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RoomResponsePromptData {
|
|
26
|
+
room: {
|
|
27
|
+
display_name: string;
|
|
28
|
+
mode: RoomMode;
|
|
29
|
+
};
|
|
30
|
+
responding_persona: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
aliases: string[];
|
|
34
|
+
short_description?: string;
|
|
35
|
+
long_description?: string;
|
|
36
|
+
traits: PersonaTrait[];
|
|
37
|
+
topics: PersonaTopic[];
|
|
38
|
+
};
|
|
39
|
+
other_participants: RoomParticipantIdentity[];
|
|
40
|
+
human: {
|
|
41
|
+
facts: Fact[];
|
|
42
|
+
topics: Topic[];
|
|
43
|
+
people: Person[];
|
|
44
|
+
quotes: Quote[];
|
|
45
|
+
/** Pre-filtered: topics where exposure_current > 0.3 */
|
|
46
|
+
active_topics: Topic[];
|
|
47
|
+
/** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
|
|
48
|
+
interested_topics: Topic[];
|
|
49
|
+
};
|
|
50
|
+
history: RoomHistoryMessage[];
|
|
51
|
+
isTUI: boolean;
|
|
52
|
+
tools?: ToolDefinition[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RoomJudgeCandidate {
|
|
56
|
+
message_id: string;
|
|
57
|
+
speaker_name: string;
|
|
58
|
+
speaker_id: string;
|
|
59
|
+
verbal_response?: string;
|
|
60
|
+
action_response?: string;
|
|
61
|
+
silence_reason?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RoomJudgePromptData {
|
|
65
|
+
room: {
|
|
66
|
+
display_name: string;
|
|
67
|
+
};
|
|
68
|
+
judge_persona: {
|
|
69
|
+
name: string;
|
|
70
|
+
short_description?: string;
|
|
71
|
+
long_description?: string;
|
|
72
|
+
traits: PersonaTrait[];
|
|
73
|
+
};
|
|
74
|
+
context: RoomHistoryMessage[];
|
|
75
|
+
candidates: RoomJudgeCandidate[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RoomJudgeResult {
|
|
79
|
+
winner_message_id: string;
|
|
80
|
+
reason?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PersonaResponseResult {
|
|
84
|
+
should_respond: boolean;
|
|
85
|
+
verbal_response?: string;
|
|
86
|
+
action_response?: string;
|
|
87
|
+
reason?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PromptOutput {
|
|
91
|
+
system: string;
|
|
92
|
+
user: string;
|
|
93
|
+
}
|
package/tui/README.md
CHANGED
|
@@ -59,6 +59,26 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
|
|
|
59
59
|
| `/resume` | `/unpause` | Resume the current paused persona |
|
|
60
60
|
| `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
|
|
61
61
|
|
|
62
|
+
### Rooms
|
|
63
|
+
|
|
64
|
+
| Command | Aliases | Description |
|
|
65
|
+
|---------|---------|-------------|
|
|
66
|
+
| `/room` | `/r` | Open room picker overlay |
|
|
67
|
+
| `/room <name>` | `/r <name>` | Switch to a room by name |
|
|
68
|
+
| `/room new` | | Create a new room (opens `$EDITOR`) |
|
|
69
|
+
| `/room new <name>` | | Create a new room with a pre-filled name |
|
|
70
|
+
| `/capture` | | Force-extract quotes, topics, and people from current room now (bypasses threshold) |
|
|
71
|
+
| `/archive <name>` | | Archive a room by name |
|
|
72
|
+
| `/archive` | | List archived rooms (Enter to unarchive) |
|
|
73
|
+
|
|
74
|
+
Rooms have three modes, set at creation time:
|
|
75
|
+
|
|
76
|
+
| Mode | Badge | Description |
|
|
77
|
+
|------|-------|-------------|
|
|
78
|
+
| Free For All | `[FFA]` | All personas respond to every message |
|
|
79
|
+
| Choose Your Path | `[CYP]` | The conversation branches at each response; you navigate which path to follow |
|
|
80
|
+
| Messages Against Persona | `[MAP]` | Everyone submits a response; a Judge persona picks which one continues |
|
|
81
|
+
|
|
62
82
|
### Providers & Models
|
|
63
83
|
|
|
64
84
|
| Command | Aliases | Description |
|
package/tui/src/app.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { OverlayProvider, useOverlay } from "./context/overlay";
|
|
|
5
5
|
import { Layout } from "./components/Layout";
|
|
6
6
|
import { Sidebar } from "./components/Sidebar";
|
|
7
7
|
import { MessageList } from "./components/MessageList";
|
|
8
|
+
import { RoomMessageList } from "./components/RoomMessageList";
|
|
8
9
|
import { PromptInput } from "./components/PromptInput";
|
|
9
10
|
import { StatusBar } from "./components/StatusBar";
|
|
10
11
|
import { Show } from "solid-js";
|
|
@@ -14,7 +15,7 @@ import { useRenderer } from "@opentui/solid";
|
|
|
14
15
|
|
|
15
16
|
function AppContent() {
|
|
16
17
|
const { overlayRenderer, showOverlay } = useOverlay();
|
|
17
|
-
const { showWelcomeOverlay, dismissWelcomeOverlay } = useEi();
|
|
18
|
+
const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId } = useEi();
|
|
18
19
|
const renderer = useRenderer();
|
|
19
20
|
// Show welcome overlay when LLM detection determines no provider is configured
|
|
20
21
|
createEffect(() => {
|
|
@@ -33,7 +34,7 @@ function AppContent() {
|
|
|
33
34
|
<box flexDirection="column" width="100%" height="100%">
|
|
34
35
|
<Layout
|
|
35
36
|
sidebar={<Sidebar />}
|
|
36
|
-
messages={<MessageList />}
|
|
37
|
+
messages={<Show when={activeRoomId()} fallback={<MessageList />}><RoomMessageList /></Show>}
|
|
37
38
|
input={<PromptInput />}
|
|
38
39
|
/>
|
|
39
40
|
<StatusBar />
|