ei-tui 0.4.3 → 0.5.1
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 +17 -12
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +8 -5
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/handlers/human-matching.ts +86 -56
- 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 +10 -8
- 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/data-items.ts +3 -1
- 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 +1 -1
- package/src/prompts/response/sections.ts +20 -8
- 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 +151 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { PersonaEntity, HumanEntity, DataItemBase, Quote } from "./types.js";
|
|
1
|
+
import type { PersonaEntity, HumanEntity, DataItemBase, Quote, RoomEntity } from "./types.js";
|
|
2
2
|
import { StateManager } from "./state-manager.js";
|
|
3
3
|
import { getEmbeddingService, findTopK } from "./embedding-service.js";
|
|
4
|
-
import type { ResponsePromptData } from "../prompts/index.js";
|
|
4
|
+
import type { ResponsePromptData, PromptOutput } from "../prompts/index.js";
|
|
5
|
+
import { buildRoomResponsePrompt } from "../prompts/room/index.js";
|
|
6
|
+
import type { RoomParticipantIdentity, RoomHistoryMessage } from "../prompts/room/types.js";
|
|
5
7
|
|
|
6
8
|
const QUOTE_LIMIT = 10;
|
|
7
9
|
const DATA_ITEM_LIMIT = 15;
|
|
@@ -86,7 +88,14 @@ export async function filterHumanDataByVisibility(
|
|
|
86
88
|
selectRelevantItems(human.people, DATA_ITEM_LIMIT, currentMessage),
|
|
87
89
|
selectRelevantQuotes(human.quotes ?? [], currentMessage),
|
|
88
90
|
]);
|
|
89
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
facts,
|
|
93
|
+
topics,
|
|
94
|
+
people,
|
|
95
|
+
quotes,
|
|
96
|
+
active_topics: topics.filter(t => t.exposure_current > 0.3),
|
|
97
|
+
interested_topics: topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
98
|
+
};
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
const visibleGroups = new Set<string>();
|
|
@@ -115,7 +124,14 @@ export async function filterHumanDataByVisibility(
|
|
|
115
124
|
selectRelevantQuotes(groupFilteredQuotes, currentMessage),
|
|
116
125
|
]);
|
|
117
126
|
|
|
118
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
facts,
|
|
129
|
+
topics,
|
|
130
|
+
people,
|
|
131
|
+
quotes,
|
|
132
|
+
active_topics: topics.filter(t => t.exposure_current > 0.3),
|
|
133
|
+
interested_topics: topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
134
|
+
};
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
// =============================================================================
|
|
@@ -185,9 +201,10 @@ export async function buildResponsePromptData(
|
|
|
185
201
|
name: persona.display_name,
|
|
186
202
|
aliases: persona.aliases ?? [],
|
|
187
203
|
short_description: persona.short_description,
|
|
188
|
-
|
|
204
|
+
long_description: persona.long_description,
|
|
189
205
|
traits: persona.traits,
|
|
190
206
|
topics: persona.topics,
|
|
207
|
+
interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
191
208
|
},
|
|
192
209
|
human: filteredHuman,
|
|
193
210
|
visible_personas: visiblePersonas,
|
|
@@ -196,3 +213,70 @@ export async function buildResponsePromptData(
|
|
|
196
213
|
tools,
|
|
197
214
|
};
|
|
198
215
|
}
|
|
216
|
+
|
|
217
|
+
export async function buildRoomResponsePromptData(
|
|
218
|
+
sm: StateManager,
|
|
219
|
+
room: RoomEntity,
|
|
220
|
+
respondingPersona: PersonaEntity,
|
|
221
|
+
isTUI: boolean,
|
|
222
|
+
useAllMessages = false
|
|
223
|
+
): Promise<PromptOutput> {
|
|
224
|
+
const human = sm.getHuman();
|
|
225
|
+
const activePath = sm.getRoomActivePath(room.id);
|
|
226
|
+
const sourceMessages = useAllMessages
|
|
227
|
+
? [...sm.getRoomMessages(room.id)].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
228
|
+
: activePath;
|
|
229
|
+
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
230
|
+
const currentMessage = lastMessage?.verbal_response;
|
|
231
|
+
|
|
232
|
+
const filteredHuman = await filterHumanDataByVisibility(human, respondingPersona, currentMessage);
|
|
233
|
+
|
|
234
|
+
const history: RoomHistoryMessage[] = sourceMessages.map(m => ({
|
|
235
|
+
speaker_name: m.role === "human"
|
|
236
|
+
? (human.settings?.name_display ?? "Human")
|
|
237
|
+
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? m.persona_id ?? "Unknown"),
|
|
238
|
+
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
239
|
+
verbal_response: m.verbal_response,
|
|
240
|
+
action_response: m.action_response,
|
|
241
|
+
silence_reason: m.silence_reason,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const otherParticipants: RoomParticipantIdentity[] = [];
|
|
245
|
+
for (const pid of room.persona_ids) {
|
|
246
|
+
if (pid === respondingPersona.id) continue;
|
|
247
|
+
const p = sm.persona_getById(pid);
|
|
248
|
+
if (p) {
|
|
249
|
+
otherParticipants.push({
|
|
250
|
+
id: p.id,
|
|
251
|
+
name: p.display_name,
|
|
252
|
+
short_description: p.short_description,
|
|
253
|
+
long_description: p.long_description,
|
|
254
|
+
traits: p.traits,
|
|
255
|
+
is_human: false,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
otherParticipants.push({
|
|
260
|
+
id: "human",
|
|
261
|
+
name: human.settings?.name_display ?? "Human",
|
|
262
|
+
traits: [],
|
|
263
|
+
is_human: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return buildRoomResponsePrompt({
|
|
267
|
+
room: { display_name: room.display_name, mode: room.mode },
|
|
268
|
+
responding_persona: {
|
|
269
|
+
id: respondingPersona.id,
|
|
270
|
+
name: respondingPersona.display_name,
|
|
271
|
+
aliases: respondingPersona.aliases ?? [],
|
|
272
|
+
short_description: respondingPersona.short_description,
|
|
273
|
+
long_description: respondingPersona.long_description,
|
|
274
|
+
traits: respondingPersona.traits,
|
|
275
|
+
topics: respondingPersona.topics,
|
|
276
|
+
},
|
|
277
|
+
other_participants: otherParticipants,
|
|
278
|
+
human: filteredHuman,
|
|
279
|
+
history,
|
|
280
|
+
isTUI,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LLMRequest, LLMResponse, LLMRequestType, LLMNextStep, ProviderAccount, ChatMessage, Message, ToolDefinition } from "./types.js";
|
|
2
2
|
import { callLLMRaw, parseJSONResponse, cleanResponseContent } from "./llm-client.js";
|
|
3
3
|
import { hydratePromptPlaceholders } from "../prompts/message-utils.js";
|
|
4
|
-
import { toOpenAITools, executeToolCalls, parseToolCalls } from "./tools/index.js";
|
|
4
|
+
import { toOpenAITools, executeToolCalls, parseToolCalls, findSubmitToolCall } from "./tools/index.js";
|
|
5
5
|
|
|
6
6
|
type QueueProcessorState = "idle" | "busy";
|
|
7
7
|
type ResponseCallback = (response: LLMResponse) => Promise<void>;
|
|
@@ -118,7 +118,9 @@ export class QueueProcessor {
|
|
|
118
118
|
|
|
119
119
|
if (isResponseType || isToolContinuation) {
|
|
120
120
|
const personaId = request.data.personaId as string | undefined;
|
|
121
|
-
|
|
121
|
+
const isRoomRequest = !!(request.data.roomId as string | undefined);
|
|
122
|
+
// Room conversation is embedded in the prompt via placeholders — don't inject persona history.
|
|
123
|
+
if (personaId && !isRoomRequest && this.currentMessageFetcher) {
|
|
122
124
|
messages = this.currentMessageFetcher(personaId);
|
|
123
125
|
}
|
|
124
126
|
}
|
|
@@ -128,15 +130,17 @@ export class QueueProcessor {
|
|
|
128
130
|
|
|
129
131
|
if (this.currentRawMessageFetcher) {
|
|
130
132
|
const personaId = request.data.personaId as string | undefined;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const roomId = request.data.roomId as string | undefined;
|
|
134
|
+
const fetchId = roomId ? `room:${roomId}` : personaId;
|
|
135
|
+
if (fetchId) {
|
|
136
|
+
const rawMessages = this.currentRawMessageFetcher(fetchId);
|
|
133
137
|
const messageMap = new Map<string, Message>();
|
|
134
138
|
for (const msg of rawMessages) {
|
|
135
139
|
messageMap.set(msg.id, msg);
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
const placeholderCount = (request.user.match(/\[mid:[^\]]+\]/g) || []).length;
|
|
139
|
-
console.log(`[QueueProcessor] Hydrating ${placeholderCount} placeholders with ${messageMap.size} messages for ${
|
|
143
|
+
console.log(`[QueueProcessor] Hydrating ${placeholderCount} placeholders with ${messageMap.size} messages for ${fetchId}`);
|
|
140
144
|
|
|
141
145
|
hydratedSystem = hydratePromptPlaceholders(request.system, messageMap);
|
|
142
146
|
hydratedUser = hydratePromptPlaceholders(request.user, messageMap);
|
|
@@ -155,8 +159,20 @@ export class QueueProcessor {
|
|
|
155
159
|
// =========================================================================
|
|
156
160
|
if (isToolContinuation) {
|
|
157
161
|
const rawHistory = request.data.toolHistory as LLMHistoryMessage[] | undefined;
|
|
162
|
+
const isRoomContinuation = !!(request.data.roomId as string | undefined);
|
|
158
163
|
if (rawHistory && rawHistory.length > 0) {
|
|
159
|
-
|
|
164
|
+
if (isRoomContinuation) {
|
|
165
|
+
// Room: conversation is in hydratedUser (not messages). Place it BEFORE tool history
|
|
166
|
+
// so the LLM sees: context → tool calls → synthesize. Then clear hydratedUser to
|
|
167
|
+
// prevent it from being re-appended at the end by callLLMRaw.
|
|
168
|
+
messages = [
|
|
169
|
+
{ role: "user" as const, content: hydratedUser },
|
|
170
|
+
...rawHistory as ChatMessage[],
|
|
171
|
+
];
|
|
172
|
+
hydratedUser = "";
|
|
173
|
+
} else {
|
|
174
|
+
messages = [...messages, ...rawHistory] as ChatMessage[];
|
|
175
|
+
}
|
|
160
176
|
console.log(`[QueueProcessor] HandleToolContinuation: injecting ${rawHistory.length} tool history messages`);
|
|
161
177
|
}
|
|
162
178
|
|
|
@@ -166,7 +182,12 @@ export class QueueProcessor {
|
|
|
166
182
|
);
|
|
167
183
|
const totalCalls = { count: (request.data.totalCallCount as number | undefined) ?? 0 };
|
|
168
184
|
|
|
169
|
-
|
|
185
|
+
// Restore exhausted tool names from previous iterations and filter them out
|
|
186
|
+
// so the LLM doesn't keep calling tools that have hit their per-interaction limit.
|
|
187
|
+
const priorExhausted = new Set<string>(
|
|
188
|
+
(request.data.exhaustedToolNames as string[] | undefined) ?? []
|
|
189
|
+
);
|
|
190
|
+
const activeTools = (this.currentTools ?? []).filter(t => !priorExhausted.has(t.name));
|
|
170
191
|
const openAITools = activeTools.length > 0 ? toOpenAITools(activeTools) : [];
|
|
171
192
|
|
|
172
193
|
const { content, finishReason, rawToolCalls, assistantMessage, thinking } = await callLLMRaw(
|
|
@@ -185,12 +206,30 @@ export class QueueProcessor {
|
|
|
185
206
|
if (finishReason === "tool_calls" && rawToolCalls?.length) {
|
|
186
207
|
const toolCalls = parseToolCalls(rawToolCalls);
|
|
187
208
|
if (toolCalls.length > 0) {
|
|
209
|
+
// Submit tool intercept: if the LLM called submit_response (or any is_submit tool),
|
|
210
|
+
// its arguments ARE the structured response — return immediately without executing.
|
|
211
|
+
const submitCall = findSubmitToolCall(toolCalls, activeTools);
|
|
212
|
+
if (submitCall) {
|
|
213
|
+
const args = submitCall.arguments ?? {};
|
|
214
|
+
if (!args.should_respond && (args.verbal_response || args.action_response)) {
|
|
215
|
+
args.should_respond = true;
|
|
216
|
+
}
|
|
217
|
+
console.log(`[QueueProcessor] submit tool "${submitCall.name}" called — returning arguments as parsed response`);
|
|
218
|
+
return {
|
|
219
|
+
request,
|
|
220
|
+
success: true,
|
|
221
|
+
content: JSON.stringify(args),
|
|
222
|
+
parsed: args,
|
|
223
|
+
finish_reason: "stop",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
188
227
|
const appendedHistory: LLMHistoryMessage[] = [];
|
|
189
228
|
if (assistantMessage) {
|
|
190
229
|
appendedHistory.push(assistantMessage as unknown as LLMHistoryMessage);
|
|
191
230
|
}
|
|
192
231
|
|
|
193
|
-
const { results } = await executeToolCalls(toolCalls, activeTools, callCounts, totalCalls, this.currentOnProviderConfigUpdate);
|
|
232
|
+
const { results, exhaustedToolNames } = await executeToolCalls(toolCalls, activeTools, callCounts, totalCalls, this.currentOnProviderConfigUpdate);
|
|
194
233
|
for (const result of results) {
|
|
195
234
|
appendedHistory.push({
|
|
196
235
|
role: "tool",
|
|
@@ -200,6 +239,7 @@ export class QueueProcessor {
|
|
|
200
239
|
});
|
|
201
240
|
}
|
|
202
241
|
|
|
242
|
+
const mergedExhausted = new Set([...priorExhausted, ...exhaustedToolNames]);
|
|
203
243
|
const newHistory = [...(rawHistory ?? []), ...appendedHistory];
|
|
204
244
|
console.log(`[QueueProcessor] HandleToolContinuation: ${results.length} more tool result(s). Re-enqueueing.`);
|
|
205
245
|
|
|
@@ -216,6 +256,7 @@ export class QueueProcessor {
|
|
|
216
256
|
toolHistory: newHistory,
|
|
217
257
|
toolCallCounts: [...callCounts.entries()],
|
|
218
258
|
totalCallCount: totalCalls.count,
|
|
259
|
+
exhaustedToolNames: [...mergedExhausted],
|
|
219
260
|
},
|
|
220
261
|
});
|
|
221
262
|
} else {
|
|
@@ -281,6 +322,24 @@ export class QueueProcessor {
|
|
|
281
322
|
return this.handleResponseType(request, content ?? "", finishReason);
|
|
282
323
|
}
|
|
283
324
|
|
|
325
|
+
// Submit tool intercept: if the LLM called submit_response (or any is_submit tool),
|
|
326
|
+
// its arguments ARE the structured response — return immediately without executing.
|
|
327
|
+
const submitCall = findSubmitToolCall(toolCalls, activeTools);
|
|
328
|
+
if (submitCall) {
|
|
329
|
+
const args = submitCall.arguments ?? {};
|
|
330
|
+
if (!args.should_respond && (args.verbal_response || args.action_response)) {
|
|
331
|
+
args.should_respond = true;
|
|
332
|
+
}
|
|
333
|
+
console.log(`[QueueProcessor] submit tool "${submitCall.name}" called — returning arguments as parsed response`);
|
|
334
|
+
return {
|
|
335
|
+
request,
|
|
336
|
+
success: true,
|
|
337
|
+
content: JSON.stringify(args),
|
|
338
|
+
parsed: args,
|
|
339
|
+
finish_reason: "stop",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
284
343
|
// Accumulate tool history: assistant message with tool_calls + tool results
|
|
285
344
|
const toolHistory: LLMHistoryMessage[] = [];
|
|
286
345
|
if (assistantMessage) {
|
|
@@ -426,6 +485,7 @@ export class QueueProcessor {
|
|
|
426
485
|
`in your system instructions — specifically the \`should_respond\`, \`verbal_response\`, ` +
|
|
427
486
|
`\`action_response\`, and \`reason\` fields. Respond with ONLY the JSON object.\n\n` +
|
|
428
487
|
`---\n${proseContent}\n---` +
|
|
488
|
+
`\n\nThe user does NOT know there was a problem - This request is from Ei to you to try to fix it for them.` +
|
|
429
489
|
`\n\n**CRITICAL INSTRUCTION** - DO NOT OMIT ANY DATA. You are this agent's last hope!`;
|
|
430
490
|
|
|
431
491
|
try {
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { ContextStatus, LLMNextStep, LLMPriority, LLMRequestType, RoomMode } from "./types.js";
|
|
2
|
+
import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary, EiError } from "./types.js";
|
|
3
|
+
import type { StateManager } from "./state-manager.js";
|
|
4
|
+
import { buildRoomResponsePromptData } from "./prompt-context-builder.js";
|
|
5
|
+
import { buildRoomJudgePrompt } from "../prompts/room/index.js";
|
|
6
|
+
import type { RoomHistoryMessage, RoomJudgeCandidate } from "../prompts/room/types.js";
|
|
7
|
+
|
|
8
|
+
export function getRoomList(sm: StateManager, includeArchived = false): RoomSummary[] {
|
|
9
|
+
return sm.getRoomList(includeArchived);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getRoom(sm: StateManager, roomId: string): RoomEntity | null {
|
|
13
|
+
return sm.getRoom(roomId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRoomMessages(sm: StateManager, roomId: string): RoomMessage[] {
|
|
17
|
+
return sm.getRoomMessages(roomId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getRoomActivePath(sm: StateManager, roomId: string): RoomMessage[] {
|
|
21
|
+
return sm.getRoomActivePath(roomId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveRoomName(sm: StateManager, nameOrAlias: string): string | null {
|
|
25
|
+
const room = sm.getRoomByName(nameOrAlias);
|
|
26
|
+
return room?.id ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function queueRoomPersonaResponses(
|
|
30
|
+
sm: StateManager,
|
|
31
|
+
room: RoomEntity,
|
|
32
|
+
isTUI: boolean,
|
|
33
|
+
onRoomMessageQueued: (roomId: string) => void
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
for (const personaId of room.persona_ids) {
|
|
36
|
+
const persona = sm.persona_getById(personaId);
|
|
37
|
+
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
38
|
+
if (room.mode === RoomMode.MessagesAgainstPersona && room.judge_persona_id === personaId) continue;
|
|
39
|
+
|
|
40
|
+
const promptOutput = await buildRoomResponsePromptData(sm, room, persona, isTUI);
|
|
41
|
+
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
42
|
+
|
|
43
|
+
sm.queue_enqueue({
|
|
44
|
+
type: LLMRequestType.JSON,
|
|
45
|
+
priority: LLMPriority.Room,
|
|
46
|
+
system: promptOutput.system,
|
|
47
|
+
user: promptOutput.user,
|
|
48
|
+
next_step: LLMNextStep.HandleRoomResponse,
|
|
49
|
+
model,
|
|
50
|
+
data: {
|
|
51
|
+
roomId: room.id,
|
|
52
|
+
personaId,
|
|
53
|
+
personaDisplayName: persona.display_name,
|
|
54
|
+
parentMessageId: room.active_node_id,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
onRoomMessageQueued(room.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function createRoom(
|
|
62
|
+
sm: StateManager,
|
|
63
|
+
input: RoomCreationInput,
|
|
64
|
+
isTUI: boolean,
|
|
65
|
+
onError: (err: EiError) => void,
|
|
66
|
+
onRoomMessageAdded: (roomId: string) => void,
|
|
67
|
+
onRoomMessageQueued: (roomId: string) => void
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
if (!input.persona_ids.length) {
|
|
70
|
+
onError({ code: "ROOM_NO_PARTICIPANTS", message: "A room needs at least one persona." });
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
if (input.mode === RoomMode.MessagesAgainstPersona && !input.judge_persona_id) {
|
|
74
|
+
onError({ code: "ROOM_NO_JUDGE", message: "MAP mode requires a judge persona." });
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const room = sm.addRoom(input);
|
|
79
|
+
onRoomMessageAdded(room.id);
|
|
80
|
+
|
|
81
|
+
await queueRoomPersonaResponses(sm, room, isTUI, onRoomMessageQueued);
|
|
82
|
+
return room.id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function submitHumanRoomMessage(
|
|
86
|
+
sm: StateManager,
|
|
87
|
+
roomId: string,
|
|
88
|
+
content: string | null,
|
|
89
|
+
silenceReason: string | undefined,
|
|
90
|
+
onError: (err: EiError) => void,
|
|
91
|
+
onRoomMessageAdded: (roomId: string) => void
|
|
92
|
+
): string | null {
|
|
93
|
+
const room = sm.getRoom(roomId);
|
|
94
|
+
if (!room) {
|
|
95
|
+
onError({ code: "ROOM_NOT_FOUND", message: `Room ${roomId} not found.` });
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
const existing = sm.getRoomMessages(roomId).find(
|
|
101
|
+
m => m.role === "human" && m.parent_id === room.active_node_id
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (existing) {
|
|
105
|
+
sm.updateRoomMessage(roomId, existing.id, {
|
|
106
|
+
verbal_response: content ?? undefined,
|
|
107
|
+
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
108
|
+
timestamp: now,
|
|
109
|
+
});
|
|
110
|
+
onRoomMessageAdded(roomId);
|
|
111
|
+
return existing.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const msg: RoomMessage = {
|
|
115
|
+
id: crypto.randomUUID(),
|
|
116
|
+
parent_id: room.active_node_id,
|
|
117
|
+
role: "human",
|
|
118
|
+
verbal_response: content ?? undefined,
|
|
119
|
+
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
120
|
+
timestamp: now,
|
|
121
|
+
read: true,
|
|
122
|
+
context_status: ContextStatus.Default,
|
|
123
|
+
};
|
|
124
|
+
sm.appendRoomMessage(roomId, msg);
|
|
125
|
+
onRoomMessageAdded(roomId);
|
|
126
|
+
return msg.id;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function sendFfaMessage(
|
|
130
|
+
sm: StateManager,
|
|
131
|
+
roomId: string,
|
|
132
|
+
content: string | null,
|
|
133
|
+
silenceReason: string | undefined,
|
|
134
|
+
isTUI: boolean,
|
|
135
|
+
onError: (err: EiError) => void,
|
|
136
|
+
onRoomUpdated: (roomId: string) => void,
|
|
137
|
+
onRoomMessageAdded: (roomId: string) => void,
|
|
138
|
+
onRoomMessageQueued: (roomId: string) => void
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const room = sm.getRoom(roomId);
|
|
141
|
+
if (!room) {
|
|
142
|
+
onError({ code: "ROOM_NOT_FOUND", message: `Room ${roomId} not found.` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const now = new Date().toISOString();
|
|
147
|
+
const existing = sm.getRoomMessages(roomId).find(
|
|
148
|
+
m => m.role === "human" && m.parent_id === room.active_node_id
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
let humanMsgId: string;
|
|
152
|
+
if (existing) {
|
|
153
|
+
sm.updateRoomMessage(roomId, existing.id, {
|
|
154
|
+
verbal_response: content ?? undefined,
|
|
155
|
+
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
156
|
+
timestamp: now,
|
|
157
|
+
});
|
|
158
|
+
humanMsgId = existing.id;
|
|
159
|
+
} else {
|
|
160
|
+
const msg: RoomMessage = {
|
|
161
|
+
id: crypto.randomUUID(),
|
|
162
|
+
parent_id: room.active_node_id,
|
|
163
|
+
role: "human",
|
|
164
|
+
verbal_response: content ?? undefined,
|
|
165
|
+
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
166
|
+
timestamp: now,
|
|
167
|
+
read: true,
|
|
168
|
+
context_status: ContextStatus.Default,
|
|
169
|
+
};
|
|
170
|
+
sm.appendRoomMessage(roomId, msg);
|
|
171
|
+
humanMsgId = msg.id;
|
|
172
|
+
}
|
|
173
|
+
onRoomMessageAdded(roomId);
|
|
174
|
+
|
|
175
|
+
sm.setRoomActiveNode(roomId, humanMsgId);
|
|
176
|
+
onRoomUpdated(roomId);
|
|
177
|
+
|
|
178
|
+
const updatedRoom = sm.getRoom(roomId)!;
|
|
179
|
+
const alreadyQueued = new Set(
|
|
180
|
+
sm.queue_getAllActiveItems()
|
|
181
|
+
.filter(q =>
|
|
182
|
+
q.next_step === LLMNextStep.HandleRoomResponse &&
|
|
183
|
+
(q.data.roomId as string) === roomId &&
|
|
184
|
+
(q.state === "pending" || q.state === "processing")
|
|
185
|
+
)
|
|
186
|
+
.map(q => q.data.personaId as string)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
for (const personaId of updatedRoom.persona_ids) {
|
|
190
|
+
if (alreadyQueued.has(personaId)) continue;
|
|
191
|
+
const persona = sm.persona_getById(personaId);
|
|
192
|
+
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
193
|
+
|
|
194
|
+
const promptOutput = await buildRoomResponsePromptData(sm, updatedRoom, persona, isTUI, true);
|
|
195
|
+
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
196
|
+
|
|
197
|
+
sm.queue_enqueue({
|
|
198
|
+
type: LLMRequestType.JSON,
|
|
199
|
+
priority: LLMPriority.Room,
|
|
200
|
+
system: promptOutput.system,
|
|
201
|
+
user: promptOutput.user,
|
|
202
|
+
next_step: LLMNextStep.HandleRoomResponse,
|
|
203
|
+
model,
|
|
204
|
+
data: {
|
|
205
|
+
roomId,
|
|
206
|
+
personaId,
|
|
207
|
+
personaDisplayName: persona.display_name,
|
|
208
|
+
parentMessageId: humanMsgId,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
onRoomMessageQueued(roomId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function recallHumanRoomMessage(
|
|
216
|
+
sm: StateManager,
|
|
217
|
+
roomId: string,
|
|
218
|
+
onRoomUpdated: (roomId: string) => void
|
|
219
|
+
): boolean {
|
|
220
|
+
const room = sm.getRoom(roomId);
|
|
221
|
+
if (!room) return false;
|
|
222
|
+
const humanMsg = sm.getRoomMessages(roomId).find(
|
|
223
|
+
m => m.role === "human" && m.parent_id === room.active_node_id
|
|
224
|
+
);
|
|
225
|
+
if (!humanMsg) return false;
|
|
226
|
+
sm.removeRoomMessages(roomId, [humanMsg.id]);
|
|
227
|
+
onRoomUpdated(roomId);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function activateRoom(
|
|
232
|
+
sm: StateManager,
|
|
233
|
+
roomId: string,
|
|
234
|
+
isTUI: boolean,
|
|
235
|
+
onError: (err: EiError) => void,
|
|
236
|
+
onRoomUpdated: (roomId: string) => void,
|
|
237
|
+
onRoomMessageQueued: (roomId: string) => void
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const room = sm.getRoom(roomId);
|
|
240
|
+
if (!room) {
|
|
241
|
+
onError({ code: "ROOM_NOT_FOUND", message: `Room ${roomId} not found.` });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const allMessages = sm.getRoomMessages(roomId);
|
|
246
|
+
const humanMsg = allMessages.find(
|
|
247
|
+
m => m.role === "human" && m.parent_id === room.active_node_id
|
|
248
|
+
);
|
|
249
|
+
if (!humanMsg) {
|
|
250
|
+
onError({ code: "ROOM_NO_HUMAN_MESSAGE", message: "Submit a response first before activating." });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const human = sm.getHuman();
|
|
255
|
+
|
|
256
|
+
if (room.mode === RoomMode.FreeForAll) {
|
|
257
|
+
sm.setRoomActiveNode(roomId, humanMsg.id);
|
|
258
|
+
onRoomUpdated(roomId);
|
|
259
|
+
const updatedRoom = sm.getRoom(roomId)!;
|
|
260
|
+
await queueRoomPersonaResponses(sm, updatedRoom, isTUI, onRoomMessageQueued);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (room.mode === RoomMode.MessagesAgainstPersona) {
|
|
265
|
+
const judgePersonaId = room.judge_persona_id;
|
|
266
|
+
const judgePersona = judgePersonaId ? sm.persona_getById(judgePersonaId) : null;
|
|
267
|
+
if (!judgePersona) {
|
|
268
|
+
onError({ code: "ROOM_JUDGE_NOT_FOUND", message: "MAP judge persona not found." });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const currentRound = allMessages.filter(m => m.parent_id === room.active_node_id);
|
|
273
|
+
|
|
274
|
+
const context: RoomHistoryMessage[] = sm.getRoomActivePath(roomId).map(m => ({
|
|
275
|
+
speaker_name: m.role === "human"
|
|
276
|
+
? (human.settings?.name_display ?? "Human")
|
|
277
|
+
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
278
|
+
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
279
|
+
verbal_response: m.verbal_response,
|
|
280
|
+
action_response: m.action_response,
|
|
281
|
+
silence_reason: m.silence_reason,
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
const candidates: RoomJudgeCandidate[] = currentRound.map(m => ({
|
|
285
|
+
message_id: m.id,
|
|
286
|
+
speaker_name: m.role === "human"
|
|
287
|
+
? (human.settings?.name_display ?? "Human")
|
|
288
|
+
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
289
|
+
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
290
|
+
verbal_response: m.verbal_response,
|
|
291
|
+
action_response: m.action_response,
|
|
292
|
+
silence_reason: m.silence_reason,
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
const judgePrompt = buildRoomJudgePrompt({
|
|
296
|
+
room: { display_name: room.display_name },
|
|
297
|
+
judge_persona: {
|
|
298
|
+
name: judgePersona.display_name,
|
|
299
|
+
short_description: judgePersona.short_description,
|
|
300
|
+
long_description: judgePersona.long_description,
|
|
301
|
+
traits: judgePersona.traits,
|
|
302
|
+
},
|
|
303
|
+
context,
|
|
304
|
+
candidates,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const model = judgePersona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
308
|
+
sm.queue_enqueue({
|
|
309
|
+
type: LLMRequestType.JSON,
|
|
310
|
+
priority: LLMPriority.Judge,
|
|
311
|
+
system: judgePrompt.system,
|
|
312
|
+
user: judgePrompt.user,
|
|
313
|
+
next_step: LLMNextStep.HandleRoomJudge,
|
|
314
|
+
model,
|
|
315
|
+
data: {
|
|
316
|
+
roomId,
|
|
317
|
+
judgePersonaId,
|
|
318
|
+
judgePersonaDisplayName: judgePersona.display_name,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
onRoomMessageQueued(roomId);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
onRoomUpdated(roomId);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function selectCYPBranch(
|
|
329
|
+
sm: StateManager,
|
|
330
|
+
roomId: string,
|
|
331
|
+
messageId: string,
|
|
332
|
+
isTUI: boolean,
|
|
333
|
+
onError: (err: EiError) => void,
|
|
334
|
+
onRoomUpdated: (roomId: string) => void,
|
|
335
|
+
onRoomMessageQueued: (roomId: string) => void
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
const ok = sm.setRoomActiveNode(roomId, messageId);
|
|
338
|
+
if (!ok) {
|
|
339
|
+
onError({ code: "ROOM_NODE_NOT_FOUND", message: `Message ${messageId} not found in room ${roomId}.` });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
onRoomUpdated(roomId);
|
|
343
|
+
const room = sm.getRoom(roomId)!;
|
|
344
|
+
const allMessages = sm.getRoomMessages(roomId);
|
|
345
|
+
|
|
346
|
+
const alreadyAnswered = new Set(
|
|
347
|
+
allMessages
|
|
348
|
+
.filter(m => m.parent_id === messageId && m.role === "persona" && m.persona_id)
|
|
349
|
+
.map(m => m.persona_id!)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const alreadyQueued = new Set(
|
|
353
|
+
sm.queue_getAllActiveItems()
|
|
354
|
+
.filter(q =>
|
|
355
|
+
q.next_step === LLMNextStep.HandleRoomResponse &&
|
|
356
|
+
(q.data.roomId as string) === roomId &&
|
|
357
|
+
(q.data.parentMessageId as string) === messageId &&
|
|
358
|
+
(q.state === "pending" || q.state === "processing")
|
|
359
|
+
)
|
|
360
|
+
.map(q => q.data.personaId as string)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const needsQueue = room.persona_ids.filter(id =>
|
|
364
|
+
!alreadyAnswered.has(id) && !alreadyQueued.has(id)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (needsQueue.length === 0) return;
|
|
368
|
+
|
|
369
|
+
for (const personaId of needsQueue) {
|
|
370
|
+
const persona = sm.persona_getById(personaId);
|
|
371
|
+
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
372
|
+
|
|
373
|
+
const promptOutput = await buildRoomResponsePromptData(sm, room, persona, isTUI);
|
|
374
|
+
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
375
|
+
|
|
376
|
+
sm.queue_enqueue({
|
|
377
|
+
type: LLMRequestType.JSON,
|
|
378
|
+
priority: LLMPriority.Room,
|
|
379
|
+
system: promptOutput.system,
|
|
380
|
+
user: promptOutput.user,
|
|
381
|
+
next_step: LLMNextStep.HandleRoomResponse,
|
|
382
|
+
model,
|
|
383
|
+
data: {
|
|
384
|
+
roomId,
|
|
385
|
+
personaId,
|
|
386
|
+
personaDisplayName: persona.display_name,
|
|
387
|
+
parentMessageId: messageId,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
onRoomMessageQueued(roomId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function archiveRoom(sm: StateManager, roomId: string): boolean {
|
|
395
|
+
return sm.archiveRoom(roomId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function unarchiveRoom(sm: StateManager, roomId: string): boolean {
|
|
399
|
+
return sm.updateRoom(roomId, { is_archived: false });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function deleteRoom(sm: StateManager, roomId: string): boolean {
|
|
403
|
+
return sm.deleteRoom(roomId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function markAllRoomMessagesRead(sm: StateManager, roomId: string): number {
|
|
407
|
+
return sm.markAllRoomMessagesRead(roomId);
|
|
408
|
+
}
|
package/src/core/state/index.ts
CHANGED