ei-tui 0.4.3 → 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.
Files changed (101) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +17 -12
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +2 -2
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +8 -5
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/handlers/human-matching.ts +53 -35
  11. package/src/core/handlers/index.ts +5 -0
  12. package/src/core/handlers/persona-preview.ts +7 -0
  13. package/src/core/handlers/persona-topics.ts +3 -2
  14. package/src/core/handlers/rooms.ts +176 -0
  15. package/src/core/handlers/utils.ts +55 -3
  16. package/src/core/heartbeat-manager.ts +3 -1
  17. package/src/core/llm-client.ts +1 -1
  18. package/src/core/message-manager.ts +10 -8
  19. package/src/core/orchestrators/human-extraction.ts +15 -2
  20. package/src/core/orchestrators/index.ts +1 -0
  21. package/src/core/orchestrators/persona-generation.ts +4 -0
  22. package/src/core/orchestrators/persona-topics.ts +2 -1
  23. package/src/core/orchestrators/room-extraction.ts +318 -0
  24. package/src/core/persona-manager.ts +16 -5
  25. package/src/core/personas/opencode-agent.ts +12 -2
  26. package/src/core/processor.ts +520 -4
  27. package/src/core/prompt-context-builder.ts +89 -5
  28. package/src/core/queue-processor.ts +68 -8
  29. package/src/core/room-manager.ts +408 -0
  30. package/src/core/state/index.ts +1 -0
  31. package/src/core/state/personas.ts +12 -2
  32. package/src/core/state/queue.ts +2 -2
  33. package/src/core/state/rooms.ts +182 -0
  34. package/src/core/state-manager.ts +124 -2
  35. package/src/core/tool-manager.ts +1 -1
  36. package/src/core/tools/index.ts +15 -0
  37. package/src/core/types/enums.ts +11 -0
  38. package/src/core/types/integrations.ts +10 -2
  39. package/src/core/types/llm.ts +3 -0
  40. package/src/core/types/rooms.ts +59 -0
  41. package/src/core/types.ts +1 -0
  42. package/src/core/utils/decay.ts +14 -8
  43. package/src/core/utils/exposure.ts +14 -0
  44. package/src/integrations/claude-code/importer.ts +23 -10
  45. package/src/integrations/cursor/importer.ts +22 -10
  46. package/src/integrations/opencode/importer.ts +30 -13
  47. package/src/prompts/ceremony/dedup.ts +2 -2
  48. package/src/prompts/generation/from-person.ts +85 -0
  49. package/src/prompts/generation/index.ts +2 -0
  50. package/src/prompts/generation/persona.ts +14 -10
  51. package/src/prompts/generation/seeds.ts +4 -29
  52. package/src/prompts/generation/types.ts +13 -0
  53. package/src/prompts/heartbeat/check.ts +1 -1
  54. package/src/prompts/heartbeat/ei.ts +4 -4
  55. package/src/prompts/heartbeat/types.ts +1 -0
  56. package/src/prompts/index.ts +15 -0
  57. package/src/prompts/message-utils.ts +2 -2
  58. package/src/prompts/persona/topics-match.ts +7 -6
  59. package/src/prompts/persona/topics-update.ts +8 -11
  60. package/src/prompts/persona/types.ts +2 -1
  61. package/src/prompts/response/index.ts +1 -1
  62. package/src/prompts/response/sections.ts +20 -8
  63. package/src/prompts/response/types.ts +6 -0
  64. package/src/prompts/room/index.ts +115 -0
  65. package/src/prompts/room/sections.ts +150 -0
  66. package/src/prompts/room/types.ts +93 -0
  67. package/tui/README.md +20 -0
  68. package/tui/src/app.tsx +3 -2
  69. package/tui/src/commands/activate.tsx +98 -0
  70. package/tui/src/commands/archive.tsx +54 -25
  71. package/tui/src/commands/capture.tsx +50 -0
  72. package/tui/src/commands/dedupe.tsx +2 -7
  73. package/tui/src/commands/delete.tsx +48 -0
  74. package/tui/src/commands/details.tsx +7 -0
  75. package/tui/src/commands/persona.tsx +271 -9
  76. package/tui/src/commands/room.tsx +261 -0
  77. package/tui/src/commands/silence.tsx +29 -0
  78. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  79. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  80. package/tui/src/components/ConflictOverlay.tsx +6 -0
  81. package/tui/src/components/HelpOverlay.tsx +6 -1
  82. package/tui/src/components/LoadingOverlay.tsx +51 -0
  83. package/tui/src/components/MessageList.tsx +1 -18
  84. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  85. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  86. package/tui/src/components/PromptInput.tsx +141 -8
  87. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  88. package/tui/src/components/QuotesOverlay.tsx +5 -1
  89. package/tui/src/components/RoomMessageList.tsx +179 -0
  90. package/tui/src/components/Sidebar.tsx +54 -2
  91. package/tui/src/components/StatusBar.tsx +99 -8
  92. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  93. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  94. package/tui/src/context/ei.tsx +252 -1
  95. package/tui/src/context/keyboard.tsx +48 -12
  96. package/tui/src/util/cyp-editor.tsx +152 -0
  97. package/tui/src/util/quote-utils.ts +19 -0
  98. package/tui/src/util/room-editor.tsx +164 -0
  99. package/tui/src/util/room-logic.ts +8 -0
  100. package/tui/src/util/room-parser.ts +70 -0
  101. package/tui/src/util/yaml-serializers.ts +151 -0
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  LLMNextStep,
3
+ LLMRequestType,
4
+ LLMPriority,
3
5
  type LLMRequest,
4
6
  type Ei_Interface,
5
7
  type PersonaSummary,
@@ -21,18 +23,23 @@ import {
21
23
  type ToolDefinition,
22
24
  type ToolProvider,
23
25
  } from "./types.js";
26
+ import { buildPersonaFromPersonPrompt } from "../prompts/index.js";
27
+ import type { PersonaGenerationResult } from "../prompts/generation/types.js";
28
+
24
29
  import type { Storage } from "../storage/interface.js";
25
30
  import { remoteSync } from "../storage/remote.js";
26
31
  import { yoloMerge } from "../storage/merge.js";
27
32
  import { StateManager } from "./state-manager.js";
28
33
  import { QueueProcessor } from "./queue-processor.js";
29
34
  import { handlers } from "./handlers/index.js";
35
+ import { normalizeRoomMessages } from "./handlers/utils.js";
30
36
  import { ContextStatus as ContextStatusEnum } from "./types.js";
31
37
  import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/index.js";
32
38
  import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
33
39
  import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
34
- import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueUserDedupRequest } from "./orchestrators/index.js";
40
+ import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction } from "./orchestrators/index.js";
35
41
  import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
42
+ import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
36
43
 
37
44
  // Static module imports
38
45
  import {
@@ -103,6 +110,24 @@ import {
103
110
  clearQueue,
104
111
  submitOneShot,
105
112
  } from "./queue-manager.js";
113
+ import {
114
+ getRoomList,
115
+ getRoom,
116
+ getRoomMessages,
117
+ getRoomActivePath,
118
+ resolveRoomName,
119
+ createRoom,
120
+ submitHumanRoomMessage,
121
+ recallHumanRoomMessage,
122
+ sendFfaMessage,
123
+ activateRoom,
124
+ selectCYPBranch,
125
+ archiveRoom,
126
+ unarchiveRoom,
127
+ deleteRoom,
128
+ markAllRoomMessagesRead,
129
+ } from "./room-manager.js";
130
+ import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary } from "./types.js";
106
131
 
107
132
  const DEFAULT_LOOP_INTERVAL_MS = 100;
108
133
  const DEFAULT_OPENCODE_POLLING_MS = 60000;
@@ -133,6 +158,7 @@ export class Processor {
133
158
  private pendingConflict: StateConflictData | null = null;
134
159
  private storage: Storage | null = null;
135
160
  private importAbortController = new AbortController();
161
+ private personaPreviewResolvers = new Map<string, { resolve: (r: PersonaGenerationResult) => void; reject: (e: Error) => void }>();
136
162
 
137
163
  constructor(ei: Ei_Interface) {
138
164
  this.interface = ei;
@@ -601,6 +627,180 @@ export class Processor {
601
627
  max_calls_per_interaction: 1,
602
628
  });
603
629
  }
630
+
631
+ // submit_response tool — auto-injected for HandlePersonaResponse and HandleRoomResponse.
632
+ // Not user-configurable; invisible in the tools UI. Terminates the tool loop immediately
633
+ // when called; its arguments become response.parsed.
634
+ if (!this.stateManager.tools_getByName("submit_response")) {
635
+ this.stateManager.tools_add({
636
+ id: crypto.randomUUID(),
637
+ provider_id: "ei",
638
+ name: "submit_response",
639
+ display_name: "Submit Response",
640
+ description: "Submit your response to the conversation. Call this when you are ready to respond — after any research or tool use is complete.",
641
+ input_schema: {
642
+ type: "object",
643
+ properties: {
644
+ should_respond: {
645
+ type: "boolean",
646
+ description: "Whether you are responding (true) or staying silent (false)",
647
+ },
648
+ verbal_response: {
649
+ type: "string",
650
+ description: "What you say out loud. Required when should_respond is true (unless action_response is provided).",
651
+ },
652
+ action_response: {
653
+ type: "string",
654
+ description: "What you do — rendered as italics stage directions. Optional alongside verbal_response.",
655
+ },
656
+ reason: {
657
+ type: "string",
658
+ description: "Why you are staying silent. Only used when should_respond is false.",
659
+ },
660
+ },
661
+ required: ["should_respond"],
662
+ additionalProperties: false,
663
+ },
664
+ runtime: "any",
665
+ builtin: true,
666
+ enabled: true,
667
+ is_submit: true,
668
+ max_calls_per_interaction: 1,
669
+ created_at: now,
670
+ });
671
+ }
672
+
673
+ if (!this.stateManager.tools_getByName("submit_heartbeat_check")) {
674
+ this.stateManager.tools_add({
675
+ id: crypto.randomUUID(),
676
+ provider_id: "ei",
677
+ name: "submit_heartbeat_check",
678
+ display_name: "Submit Heartbeat Decision",
679
+ description: "Submit your decision on whether to reach out with a message. Call this when you have decided.",
680
+ input_schema: {
681
+ type: "object",
682
+ properties: {
683
+ should_respond: { type: "boolean", description: "Whether you want to initiate a message" },
684
+ topic: { type: "string", description: "The specific topic you want to discuss (when should_respond is true)" },
685
+ message: { type: "string", description: "Your actual message to them (when should_respond is true)" },
686
+ },
687
+ required: ["should_respond"],
688
+ additionalProperties: false,
689
+ },
690
+ runtime: "any",
691
+ builtin: true,
692
+ enabled: true,
693
+ is_submit: true,
694
+ max_calls_per_interaction: 1,
695
+ created_at: now,
696
+ });
697
+ }
698
+
699
+ if (!this.stateManager.tools_getByName("submit_ei_heartbeat")) {
700
+ this.stateManager.tools_add({
701
+ id: crypto.randomUUID(),
702
+ provider_id: "ei",
703
+ name: "submit_ei_heartbeat",
704
+ display_name: "Submit Ei Heartbeat Decision",
705
+ description: "Submit your choice of item to follow up on, or indicate nothing warrants reaching out.",
706
+ input_schema: {
707
+ type: "object",
708
+ properties: {
709
+ should_respond: { type: "boolean", description: "Whether Ei wants to check in about an item" },
710
+ id: { type: "string", description: "ID of the item you chose (when should_respond is true)" },
711
+ my_response: { type: "string", description: "The check-in message (for Person/Topic/Persona items)" },
712
+ },
713
+ required: ["should_respond"],
714
+ additionalProperties: false,
715
+ },
716
+ runtime: "any",
717
+ builtin: true,
718
+ enabled: true,
719
+ is_submit: true,
720
+ max_calls_per_interaction: 1,
721
+ created_at: now,
722
+ });
723
+ }
724
+
725
+ if (!this.stateManager.tools_getByName("submit_dedup_decisions")) {
726
+ this.stateManager.tools_add({
727
+ id: crypto.randomUUID(),
728
+ provider_id: "ei",
729
+ name: "submit_dedup_decisions",
730
+ display_name: "Submit Dedup Decisions",
731
+ description: "Submit your merge, remove, and add decisions for this cluster of records.",
732
+ input_schema: {
733
+ type: "object",
734
+ properties: {
735
+ update: {
736
+ type: "array",
737
+ description: "Records to update with merged data. Must include at least one (the canonical record).",
738
+ items: {
739
+ type: "object",
740
+ properties: {
741
+ id: { type: "string" },
742
+ type: { type: "string", enum: ["topic", "person", "trait"] },
743
+ name: { type: "string" },
744
+ description: { type: "string" },
745
+ sentiment: { type: "number" },
746
+ strength: { type: "number" },
747
+ confidence: { type: "number" },
748
+ exposure_current: { type: "number" },
749
+ exposure_desired: { type: "number" },
750
+ relationship: { type: "string" },
751
+ category: { type: "string" },
752
+ last_updated: { type: "string" },
753
+ },
754
+ required: ["id", "type", "name", "description"],
755
+ additionalProperties: false,
756
+ },
757
+ },
758
+ remove: {
759
+ type: "array",
760
+ description: "Duplicates to remove. Each must reference its canonical record via replaced_by.",
761
+ items: {
762
+ type: "object",
763
+ properties: {
764
+ to_be_removed: { type: "string" },
765
+ replaced_by: { type: "string" },
766
+ },
767
+ required: ["to_be_removed", "replaced_by"],
768
+ additionalProperties: false,
769
+ },
770
+ },
771
+ add: {
772
+ type: "array",
773
+ description: "New records to create. Only when merging reveals a missing concept.",
774
+ items: {
775
+ type: "object",
776
+ properties: {
777
+ type: { type: "string", enum: ["topic", "person", "trait"] },
778
+ name: { type: "string" },
779
+ description: { type: "string" },
780
+ sentiment: { type: "number" },
781
+ strength: { type: "number" },
782
+ confidence: { type: "number" },
783
+ exposure_current: { type: "number" },
784
+ exposure_desired: { type: "number" },
785
+ relationship: { type: "string" },
786
+ category: { type: "string" },
787
+ },
788
+ required: ["type", "name", "description"],
789
+ additionalProperties: false,
790
+ },
791
+ },
792
+ },
793
+ required: ["update", "remove", "add"],
794
+ additionalProperties: false,
795
+ },
796
+ runtime: "any",
797
+ builtin: true,
798
+ enabled: true,
799
+ is_submit: true,
800
+ max_calls_per_interaction: 1,
801
+ created_at: now,
802
+ });
803
+ }
604
804
  }
605
805
 
606
806
  /**
@@ -837,8 +1037,14 @@ export class Processor {
837
1037
  this.interface.onMessageProcessing?.(personaId);
838
1038
  }
839
1039
 
1040
+ const roomId = request.data.roomId as string | undefined;
1041
+ if (roomId && (request.next_step === LLMNextStep.HandleRoomResponse || request.next_step === LLMNextStep.HandleRoomJudge)) {
1042
+ this.interface.onRoomMessageProcessing?.(roomId);
1043
+ }
1044
+
840
1045
  const toolNextSteps = new Set([
841
1046
  LLMNextStep.HandlePersonaResponse,
1047
+ LLMNextStep.HandleRoomResponse,
842
1048
  LLMNextStep.HandleHeartbeatCheck,
843
1049
  LLMNextStep.HandleEiHeartbeat,
844
1050
  LLMNextStep.HandleToolContinuation,
@@ -866,6 +1072,25 @@ const toolNextSteps = new Set([
866
1072
  } else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
867
1073
  tools = this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI);
868
1074
  }
1075
+
1076
+ // Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
1077
+ const submitToolByStep: Partial<Record<string, string>> = {
1078
+ [LLMNextStep.HandlePersonaResponse]: "submit_response",
1079
+ [LLMNextStep.HandleRoomResponse]: "submit_response",
1080
+ [LLMNextStep.HandleHeartbeatCheck]: "submit_heartbeat_check",
1081
+ [LLMNextStep.HandleEiHeartbeat]: "submit_ei_heartbeat",
1082
+ [LLMNextStep.HandleDedupCurate]: "submit_dedup_decisions",
1083
+ };
1084
+ const effectiveStep = request.next_step === LLMNextStep.HandleToolContinuation
1085
+ ? (request.data.originalNextStep as string | undefined)
1086
+ : request.next_step;
1087
+ const submitToolName = effectiveStep ? submitToolByStep[effectiveStep] : undefined;
1088
+ if (submitToolName) {
1089
+ const submitTool = this.stateManager.tools_getByName(submitToolName);
1090
+ if (submitTool?.enabled && !tools.find(t => t.name === submitToolName)) {
1091
+ tools = [...tools, submitTool];
1092
+ }
1093
+ }
869
1094
 
870
1095
  console.log(
871
1096
  `[Tools] Dispatch for ${request.next_step} persona=${toolPersonaId ?? "none"}: ${tools.length} tool(s) attached`
@@ -882,7 +1107,13 @@ const toolNextSteps = new Set([
882
1107
  {
883
1108
  accounts: this.stateManager.getHuman().settings?.accounts,
884
1109
  messageFetcher: (pName) => fetchMessagesForLLM(this.stateManager, pName),
885
- rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
1110
+ rawMessageFetcher: (id) => {
1111
+ if (id.startsWith("room:")) {
1112
+ const roomId = id.slice(5);
1113
+ return normalizeRoomMessages(this.stateManager.getRoomMessages(roomId), this.stateManager);
1114
+ }
1115
+ return this.stateManager.messages_get(id);
1116
+ },
886
1117
  tools: tools.length > 0 ? tools : undefined,
887
1118
  onEnqueue: (req) => this.stateManager.queue_enqueue(req),
888
1119
  onProviderConfigUpdate: (providerId, updates) => {
@@ -1206,6 +1437,15 @@ const toolNextSteps = new Set([
1206
1437
  message += ` (attempt ${response.request.attempts}, retrying in ${Math.round(result.retryDelay / 1000)}s)`;
1207
1438
  } else if (result.dropped) {
1208
1439
  message += " (permanent failure \u2014 request removed)";
1440
+ if (response.request.next_step === LLMNextStep.HandlePersonaPreview) {
1441
+ const guid = response.request.data.guid as string;
1442
+ const entry = this.personaPreviewResolvers.get(guid);
1443
+ if (entry) {
1444
+ this.personaPreviewResolvers.delete(guid);
1445
+ entry.reject(new Error("Persona preview generation failed after max retries"));
1446
+ return;
1447
+ }
1448
+ }
1209
1449
  if (response.request.next_step === LLMNextStep.HandleOneShot) {
1210
1450
  const guid = response.request.data.guid as string;
1211
1451
  this.interface.onOneShotReturned?.(guid, "");
@@ -1255,6 +1495,88 @@ const toolNextSteps = new Set([
1255
1495
  this.interface.onOneShotReturned?.(guid, content);
1256
1496
  }
1257
1497
 
1498
+ if (response.request.next_step === LLMNextStep.HandlePersonaPreview) {
1499
+ const guid = response.request.data.guid as string;
1500
+ const loopCounter = (response.request.data.loop_counter as number) ?? 0;
1501
+ const existingPersonaId = response.request.data.personaId as string | undefined;
1502
+ const MAX_PREVIEW_LOOPS = 3;
1503
+ const entry = this.personaPreviewResolvers.get(guid);
1504
+ if (!entry) return;
1505
+
1506
+ if (!response.success || !response.parsed) {
1507
+ return;
1508
+ }
1509
+
1510
+ let result = response.parsed as import("../prompts/generation/types.js").PersonaGenerationResult;
1511
+ const isComplete =
1512
+ result.traits && result.traits.length >= 3 &&
1513
+ result.topics && result.topics.length >= 3 &&
1514
+ result.long_description && result.short_description;
1515
+
1516
+ const hydrateWithExisting = (r: typeof result): typeof result => {
1517
+ if (!existingPersonaId) return r;
1518
+ const existing = this.stateManager.persona_getById(existingPersonaId);
1519
+ if (!existing) return r;
1520
+
1521
+ const existingTraitNames = new Set(existing.traits.map(t => t.name.toLowerCase().trim()));
1522
+ const newTraits = r.traits.filter(t => !existingTraitNames.has(t.name.toLowerCase().trim()));
1523
+ const mergedTraits = [
1524
+ ...existing.traits.map(t => ({
1525
+ name: t.name,
1526
+ description: t.description,
1527
+ strength: t.strength ?? 0.5,
1528
+ sentiment: t.sentiment,
1529
+ })),
1530
+ ...newTraits,
1531
+ ];
1532
+
1533
+ const existingTopicNames = new Set(existing.topics.map(t => t.name.toLowerCase().trim()));
1534
+ const newTopics = r.topics.filter(t => !existingTopicNames.has(t.name.toLowerCase().trim()));
1535
+ const mergedTopics = [...existing.topics, ...newTopics];
1536
+
1537
+ return {
1538
+ ...r,
1539
+ traits: mergedTraits,
1540
+ topics: mergedTopics,
1541
+ previous_long_description: existing.long_description,
1542
+ previous_short_description: existing.short_description,
1543
+ aliases: existing.aliases ?? [],
1544
+ };
1545
+ };
1546
+
1547
+ if (isComplete) {
1548
+ this.personaPreviewResolvers.delete(guid);
1549
+ const hydratedComplete = hydrateWithExisting(result);
1550
+ const seedTraitNamesComplete = new Set(hydratedComplete.traits.map((t: { name: string }) => t.name.toLowerCase().trim()));
1551
+ DEFAULT_SEED_TRAITS
1552
+ .filter(s => !seedTraitNamesComplete.has(s.name.toLowerCase().trim()))
1553
+ .forEach(s => hydratedComplete.traits.push({ name: s.name, description: s.description, sentiment: s.sentiment, strength: s.strength }));
1554
+ entry.resolve(hydratedComplete);
1555
+ return;
1556
+ }
1557
+
1558
+ if (loopCounter < MAX_PREVIEW_LOOPS) {
1559
+ this.stateManager.queue_enqueue({
1560
+ type: LLMRequestType.JSON,
1561
+ priority: LLMPriority.High,
1562
+ system: response.request.system,
1563
+ user: response.request.user,
1564
+ next_step: LLMNextStep.HandlePersonaPreview,
1565
+ model: response.request.model,
1566
+ data: { guid, loop_counter: loopCounter + 1, personaId: existingPersonaId },
1567
+ });
1568
+ return;
1569
+ }
1570
+
1571
+ this.personaPreviewResolvers.delete(guid);
1572
+ const hydratedFallback = hydrateWithExisting(result);
1573
+ const seedTraitNamesFallback = new Set(hydratedFallback.traits.map((t: { name: string }) => t.name.toLowerCase().trim()));
1574
+ DEFAULT_SEED_TRAITS
1575
+ .filter(s => !seedTraitNamesFallback.has(s.name.toLowerCase().trim()))
1576
+ .forEach(s => hydratedFallback.traits.push({ name: s.name, description: s.description, sentiment: s.sentiment, strength: s.strength }));
1577
+ entry.resolve(hydratedFallback);
1578
+ }
1579
+
1258
1580
  if (response.request.next_step === LLMNextStep.HandlePersonaGeneration) {
1259
1581
  const personaId = response.request.data.personaId as string;
1260
1582
  if (personaId) {
@@ -1307,6 +1629,27 @@ const toolNextSteps = new Set([
1307
1629
  this.interface.onHumanUpdated?.();
1308
1630
  }
1309
1631
 
1632
+ if (response.request.next_step === LLMNextStep.HandleDedupCurate) {
1633
+ this.interface.onHumanUpdated?.();
1634
+ }
1635
+
1636
+ const isRoomResponse =
1637
+ response.request.next_step === LLMNextStep.HandleRoomResponse ||
1638
+ (response.request.next_step === LLMNextStep.HandleToolContinuation &&
1639
+ response.request.data.originalNextStep === LLMNextStep.HandleRoomResponse);
1640
+ if (isRoomResponse) {
1641
+ const roomId = response.request.data.roomId as string;
1642
+ if (roomId) {
1643
+ this.interface.onRoomMessageAdded?.(roomId);
1644
+ checkAndQueueRoomExtraction(this.stateManager, roomId);
1645
+ }
1646
+ }
1647
+
1648
+ if (response.request.next_step === LLMNextStep.HandleRoomJudge) {
1649
+ const roomId = response.request.data.roomId as string;
1650
+ if (roomId) this.interface.onRoomUpdated?.(roomId);
1651
+ }
1652
+
1310
1653
  if (typeof response.request.data.ceremony_progress === "number") {
1311
1654
  handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
1312
1655
  }
@@ -1377,6 +1720,11 @@ const toolNextSteps = new Set([
1377
1720
  if (ok) this.interface.onPersonaUpdated?.(personaId);
1378
1721
  }
1379
1722
 
1723
+ async updateRoom(roomId: string, updates: Partial<RoomEntity>): Promise<void> {
1724
+ const ok = this.stateManager.updateRoom(roomId, updates);
1725
+ if (ok) this.interface.onRoomUpdated?.(roomId);
1726
+ }
1727
+
1380
1728
  async getGroupList(): Promise<string[]> {
1381
1729
  return getGroupList(this.stateManager);
1382
1730
  }
@@ -1408,7 +1756,7 @@ const toolNextSteps = new Set([
1408
1756
  );
1409
1757
  }
1410
1758
 
1411
- async sendMessage(personaId: string, content: string): Promise<void> {
1759
+ async sendMessage(personaId: string, content: string | null, silenceReason?: string): Promise<void> {
1412
1760
  return sendMessage(
1413
1761
  this.stateManager,
1414
1762
  this.queueProcessor,
@@ -1419,7 +1767,8 @@ const toolNextSteps = new Set([
1419
1767
  (id) => getModelForPersona(this.stateManager, id),
1420
1768
  (err) => this.interface.onError?.(err),
1421
1769
  (id) => this.interface.onMessageAdded?.(id),
1422
- (id) => this.interface.onMessageQueued?.(id)
1770
+ (id) => this.interface.onMessageQueued?.(id),
1771
+ silenceReason
1423
1772
  );
1424
1773
  }
1425
1774
 
@@ -1590,6 +1939,14 @@ const toolNextSteps = new Set([
1590
1939
  queueUserDedupRequest(this.stateManager, itemType, entityIds);
1591
1940
  }
1592
1941
 
1942
+ capturePersona(personaId: string): void {
1943
+ queuePersonaCapture(this.stateManager, personaId);
1944
+ }
1945
+
1946
+ captureRoom(roomId: string): void {
1947
+ queueRoomCapture(this.stateManager, roomId);
1948
+ }
1949
+
1593
1950
  async submitOneShot(guid: string, systemPrompt: string, userPrompt: string): Promise<void> {
1594
1951
  return submitOneShot(
1595
1952
  this.stateManager,
@@ -1600,6 +1957,45 @@ const toolNextSteps = new Set([
1600
1957
  );
1601
1958
  }
1602
1959
 
1960
+ async generatePersonaPreview(
1961
+ name: string,
1962
+ description: string,
1963
+ relationship?: string,
1964
+ personaId?: string
1965
+ ): Promise<PersonaGenerationResult> {
1966
+ let existing_trait_names: string[] | undefined;
1967
+ let existing_topic_names: string[] | undefined;
1968
+
1969
+ if (personaId) {
1970
+ const existing = this.stateManager.persona_getById(personaId);
1971
+ if (existing) {
1972
+ existing_trait_names = existing.traits.map((t) => t.name);
1973
+ existing_topic_names = existing.topics.map((t) => t.name);
1974
+ }
1975
+ }
1976
+
1977
+ const prompt = buildPersonaFromPersonPrompt({
1978
+ name,
1979
+ description,
1980
+ relationship,
1981
+ existing_trait_names,
1982
+ existing_topic_names,
1983
+ });
1984
+ const guid = crypto.randomUUID();
1985
+ return new Promise<PersonaGenerationResult>((resolve, reject) => {
1986
+ this.personaPreviewResolvers.set(guid, { resolve, reject });
1987
+ this.stateManager.queue_enqueue({
1988
+ type: LLMRequestType.JSON,
1989
+ priority: LLMPriority.High,
1990
+ system: prompt.system,
1991
+ user: prompt.user,
1992
+ next_step: LLMNextStep.HandlePersonaPreview,
1993
+ model: getOneshotModel(this.stateManager),
1994
+ data: { guid, loop_counter: 0, personaId },
1995
+ });
1996
+ });
1997
+ }
1998
+
1603
1999
  // ==========================================================================
1604
2000
  // TOOL API
1605
2001
  // ==========================================================================
@@ -1662,6 +2058,126 @@ const toolNextSteps = new Set([
1662
2058
  return result;
1663
2059
  }
1664
2060
 
2061
+ // ==========================================================================
2062
+ // ROOM API
2063
+ // ==========================================================================
2064
+
2065
+ getRoomList(includeArchived = false): RoomSummary[] {
2066
+ return getRoomList(this.stateManager, includeArchived);
2067
+ }
2068
+
2069
+ getRoom(roomId: string): RoomEntity | null {
2070
+ return getRoom(this.stateManager, roomId);
2071
+ }
2072
+
2073
+ getRoomMessages(roomId: string): RoomMessage[] {
2074
+ return getRoomMessages(this.stateManager, roomId);
2075
+ }
2076
+
2077
+ getRoomActivePath(roomId: string): RoomMessage[] {
2078
+ return getRoomActivePath(this.stateManager, roomId);
2079
+ }
2080
+
2081
+ resolveRoomName(nameOrAlias: string): string | null {
2082
+ return resolveRoomName(this.stateManager, nameOrAlias);
2083
+ }
2084
+
2085
+ async createRoom(input: RoomCreationInput): Promise<string> {
2086
+ const id = await createRoom(
2087
+ this.stateManager,
2088
+ input,
2089
+ this.isTUI,
2090
+ (err) => this.interface.onError?.(err),
2091
+ (id) => this.interface.onRoomMessageAdded?.(id),
2092
+ (id) => this.interface.onRoomMessageQueued?.(id)
2093
+ );
2094
+ if (id) this.interface.onRoomAdded?.();
2095
+ return id;
2096
+ }
2097
+
2098
+ submitHumanRoomMessage(
2099
+ roomId: string,
2100
+ content: string | null,
2101
+ silenceReason?: string
2102
+ ): string | null {
2103
+ return submitHumanRoomMessage(
2104
+ this.stateManager,
2105
+ roomId,
2106
+ content,
2107
+ silenceReason,
2108
+ (err) => this.interface.onError?.(err),
2109
+ (id) => this.interface.onRoomMessageAdded?.(id)
2110
+ );
2111
+ }
2112
+
2113
+ recallHumanRoomMessage(roomId: string): boolean {
2114
+ return recallHumanRoomMessage(
2115
+ this.stateManager,
2116
+ roomId,
2117
+ (id) => this.interface.onRoomUpdated?.(id)
2118
+ );
2119
+ }
2120
+
2121
+ async activateRoom(roomId: string): Promise<void> {
2122
+ return activateRoom(
2123
+ this.stateManager,
2124
+ roomId,
2125
+ this.isTUI,
2126
+ (err) => this.interface.onError?.(err),
2127
+ (id) => this.interface.onRoomUpdated?.(id),
2128
+ (id) => this.interface.onRoomMessageQueued?.(id)
2129
+ );
2130
+ }
2131
+
2132
+ async sendFfaMessage(
2133
+ roomId: string,
2134
+ content: string | null,
2135
+ silenceReason?: string
2136
+ ): Promise<void> {
2137
+ return sendFfaMessage(
2138
+ this.stateManager,
2139
+ roomId,
2140
+ content,
2141
+ silenceReason,
2142
+ this.isTUI,
2143
+ (err) => this.interface.onError?.(err),
2144
+ (id) => this.interface.onRoomUpdated?.(id),
2145
+ (id) => this.interface.onRoomMessageAdded?.(id),
2146
+ (id) => this.interface.onRoomMessageQueued?.(id)
2147
+ );
2148
+ }
2149
+
2150
+ async selectCYPBranch(roomId: string, messageId: string): Promise<void> {
2151
+ return selectCYPBranch(
2152
+ this.stateManager,
2153
+ roomId,
2154
+ messageId,
2155
+ this.isTUI,
2156
+ (err) => this.interface.onError?.(err),
2157
+ (id) => this.interface.onRoomUpdated?.(id),
2158
+ (id) => this.interface.onRoomMessageQueued?.(id)
2159
+ );
2160
+ }
2161
+
2162
+ async archiveRoom(roomId: string): Promise<void> {
2163
+ const ok = archiveRoom(this.stateManager, roomId);
2164
+ if (ok) this.interface.onRoomRemoved?.();
2165
+ }
2166
+
2167
+ async unarchiveRoom(roomId: string): Promise<void> {
2168
+ const ok = unarchiveRoom(this.stateManager, roomId);
2169
+ if (ok) this.interface.onRoomAdded?.();
2170
+ }
2171
+
2172
+ async deleteRoom(roomId: string): Promise<void> {
2173
+ const ok = deleteRoom(this.stateManager, roomId);
2174
+ if (ok) this.interface.onRoomRemoved?.();
2175
+ }
2176
+
2177
+ async markAllRoomMessagesRead(roomId: string): Promise<number> {
2178
+ return markAllRoomMessagesRead(this.stateManager, roomId);
2179
+ }
2180
+
1665
2181
  // ==========================================================================
1666
2182
  // DEBUG / TESTING UTILITIES
1667
2183
  // ==========================================================================