ei-tui 0.1.4 → 0.1.6
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 +6 -2
- package/package.json +3 -3
- package/src/cli/README.md +2 -0
- package/src/core/handlers/index.ts +106 -16
- package/src/core/orchestrators/ceremony.ts +3 -3
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +74 -33
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +79 -13
- package/src/core/state-manager.ts +35 -3
- package/src/core/types.ts +19 -6
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +5 -4
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/message-utils.ts +38 -1
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +90 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +12 -1
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +7 -1
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +33 -3
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/yaml-serializers.ts +70 -8
package/src/core/state/queue.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LLMRequest, QueueFailResult } from "../types.js";
|
|
2
|
+
import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS } from "../types.js";
|
|
2
3
|
|
|
3
4
|
const BASE_BACKOFF_MS = 2_000;
|
|
4
5
|
const MAX_BACKOFF_MS = 30_000;
|
|
@@ -27,35 +28,55 @@ export class QueueState {
|
|
|
27
28
|
private paused = false;
|
|
28
29
|
|
|
29
30
|
load(queue: LLMRequest[]): void {
|
|
30
|
-
|
|
31
|
+
// Reset any items stuck in 'processing' from a previous session
|
|
32
|
+
this.queue = queue.map(r =>
|
|
33
|
+
r.state === "processing" ? { ...r, state: "pending" } : r
|
|
34
|
+
);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export(): LLMRequest[] {
|
|
34
38
|
return this.queue;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
|
|
41
|
+
enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
|
|
38
42
|
const id = crypto.randomUUID();
|
|
39
43
|
const fullRequest: LLMRequest = {
|
|
40
44
|
...request,
|
|
41
45
|
id,
|
|
42
46
|
created_at: new Date().toISOString(),
|
|
43
47
|
attempts: 0,
|
|
48
|
+
state: "pending",
|
|
44
49
|
};
|
|
45
50
|
this.queue.push(fullRequest);
|
|
46
51
|
return id;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
claimHighest(): LLMRequest | null {
|
|
50
55
|
if (this.paused || this.queue.length === 0) return null;
|
|
51
|
-
const
|
|
52
|
-
const available = this.queue.filter(r => !r.retry_after || r.retry_after <= now);
|
|
56
|
+
const available = this.queue.filter(r => r.state === "pending");
|
|
53
57
|
if (available.length === 0) return null;
|
|
54
58
|
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
55
59
|
const sorted = [...available].sort(
|
|
56
60
|
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
57
61
|
);
|
|
58
|
-
|
|
62
|
+
const item = sorted[0] ?? null;
|
|
63
|
+
if (item) {
|
|
64
|
+
item.state = "processing";
|
|
65
|
+
}
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Returns the retry_after of the highest-priority pending item, or null if ready now.
|
|
70
|
+
// Used by the processor loop to decide whether to sleep instead of claiming.
|
|
71
|
+
nextItemRetryAfter(): string | null {
|
|
72
|
+
if (this.paused || this.queue.length === 0) return null;
|
|
73
|
+
const available = this.queue.filter(r => r.state === "pending");
|
|
74
|
+
if (available.length === 0) return null;
|
|
75
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
76
|
+
const sorted = [...available].sort(
|
|
77
|
+
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
78
|
+
);
|
|
79
|
+
return sorted[0]?.retry_after ?? null;
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
complete(id: string): void {
|
|
@@ -76,18 +97,21 @@ export class QueueState {
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
// No error string and not flagged permanent = just increment, no classification
|
|
100
|
+
// Still reset to pending in case it was claimed (processing state)
|
|
79
101
|
if (!error && !permanent) {
|
|
102
|
+
request.state = "pending";
|
|
80
103
|
return { dropped: false };
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
const shouldDrop = permanent || (error ? isPermanentError(error) : false);
|
|
84
107
|
|
|
85
108
|
if (shouldDrop) {
|
|
86
|
-
|
|
109
|
+
request.state = "dlq";
|
|
87
110
|
return { dropped: true };
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
// Transient error —
|
|
113
|
+
// Transient error — reset to pending with backoff timer
|
|
114
|
+
request.state = "pending";
|
|
91
115
|
const delay = calculateBackoff(request.attempts);
|
|
92
116
|
request.retry_after = new Date(Date.now() + delay).toISOString();
|
|
93
117
|
return { dropped: false, retryDelay: delay };
|
|
@@ -98,7 +122,7 @@ export class QueueState {
|
|
|
98
122
|
clearPersonaResponses(personaId: string, nextStep: string): string[] {
|
|
99
123
|
const removedIds: string[] = [];
|
|
100
124
|
this.queue = this.queue.filter((r) => {
|
|
101
|
-
if (r.next_step === nextStep && r.data.personaId === personaId) {
|
|
125
|
+
if (r.state !== "dlq" && r.next_step === nextStep && r.data.personaId === personaId) {
|
|
102
126
|
removedIds.push(r.id);
|
|
103
127
|
return false;
|
|
104
128
|
}
|
|
@@ -108,7 +132,49 @@ export class QueueState {
|
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
length(): number {
|
|
111
|
-
return this.queue.length;
|
|
135
|
+
return this.queue.filter(r => r.state === "pending" || r.state === "processing").length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dlqLength(): number {
|
|
139
|
+
return this.queue.filter(r => r.state === "dlq").length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hasProcessingItem(): boolean {
|
|
143
|
+
return this.queue.some(r => r.state === "processing");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getDLQItems(): LLMRequest[] {
|
|
147
|
+
return this.queue.filter(r => r.state === "dlq");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getAllActiveItems(): LLMRequest[] {
|
|
151
|
+
return this.queue.filter(r => r.state !== "dlq");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateItem(id: string, updates: Partial<LLMRequest>): boolean {
|
|
155
|
+
const idx = this.queue.findIndex(r => r.id === id);
|
|
156
|
+
if (idx < 0) return false;
|
|
157
|
+
this.queue[idx] = { ...this.queue[idx], ...updates };
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
trimDLQ(): number {
|
|
162
|
+
const dlqItems = this.queue.filter(r => r.state === "dlq");
|
|
163
|
+
const cutoff = new Date();
|
|
164
|
+
cutoff.setDate(cutoff.getDate() - DLQ_MAX_AGE_DAYS);
|
|
165
|
+
const cutoffISO = cutoff.toISOString();
|
|
166
|
+
|
|
167
|
+
// Remove by age first
|
|
168
|
+
const afterAgeTrim = dlqItems.filter(r => r.created_at >= cutoffISO);
|
|
169
|
+
|
|
170
|
+
// Then enforce count limit (oldest first)
|
|
171
|
+
const sorted = afterAgeTrim.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
172
|
+
const kept = sorted.slice(-DLQ_MAX_COUNT);
|
|
173
|
+
const keptIds = new Set(kept.map(r => r.id));
|
|
174
|
+
|
|
175
|
+
const before = this.queue.length;
|
|
176
|
+
this.queue = this.queue.filter(r => r.state !== "dlq" || keptIds.has(r.id));
|
|
177
|
+
return before - this.queue.length;
|
|
112
178
|
}
|
|
113
179
|
|
|
114
180
|
pause(): void {
|
|
@@ -124,12 +190,12 @@ export class QueueState {
|
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
hasPendingCeremonies(): boolean {
|
|
127
|
-
return this.queue.some(r => r.data.ceremony_progress === true);
|
|
193
|
+
return this.queue.some(r => r.state !== "dlq" && r.data.ceremony_progress === true);
|
|
128
194
|
}
|
|
129
195
|
|
|
130
196
|
clear(): number {
|
|
131
|
-
const count = this.queue.length;
|
|
132
|
-
this.queue =
|
|
197
|
+
const count = this.queue.filter(r => r.state !== "dlq").length;
|
|
198
|
+
this.queue = this.queue.filter(r => r.state === "dlq");
|
|
133
199
|
return count;
|
|
134
200
|
}
|
|
135
201
|
}
|
|
@@ -250,7 +250,7 @@ export class StateManager {
|
|
|
250
250
|
return result;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
|
|
253
|
+
queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
|
|
254
254
|
const requestWithModel = {
|
|
255
255
|
...request,
|
|
256
256
|
model: request.model ?? this.humanState.get().settings?.default_model,
|
|
@@ -260,8 +260,8 @@ export class StateManager {
|
|
|
260
260
|
return id;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
return this.queueState.
|
|
263
|
+
queue_claimHighest(): LLMRequest | null {
|
|
264
|
+
return this.queueState.claimHighest();
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
queue_complete(id: string): void {
|
|
@@ -287,6 +287,14 @@ export class StateManager {
|
|
|
287
287
|
return this.queueState.length();
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
queue_hasProcessingItem(): boolean {
|
|
291
|
+
return this.queueState.hasProcessingItem();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
queue_nextItemRetryAfter(): string | null {
|
|
295
|
+
return this.queueState.nextItemRetryAfter();
|
|
296
|
+
}
|
|
297
|
+
|
|
290
298
|
queue_pause(): void {
|
|
291
299
|
this.queueState.pause();
|
|
292
300
|
this.scheduleSave();
|
|
@@ -311,6 +319,30 @@ export class StateManager {
|
|
|
311
319
|
return result;
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
queue_dlqLength(): number {
|
|
323
|
+
return this.queueState.dlqLength();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
queue_getDLQItems(): LLMRequest[] {
|
|
327
|
+
return this.queueState.getDLQItems();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
queue_getAllActiveItems(): LLMRequest[] {
|
|
331
|
+
return this.queueState.getAllActiveItems();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
queue_updateItem(id: string, updates: Partial<LLMRequest>): boolean {
|
|
335
|
+
const result = this.queueState.updateItem(id, updates);
|
|
336
|
+
if (result) this.scheduleSave();
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
queue_trimDLQ(): number {
|
|
341
|
+
const result = this.queueState.trimDLQ();
|
|
342
|
+
if (result > 0) this.scheduleSave();
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
314
346
|
async flush(): Promise<void> {
|
|
315
347
|
await this.persistenceState.flush();
|
|
316
348
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -256,6 +256,10 @@ export interface PersonaCreationInput {
|
|
|
256
256
|
export const MESSAGE_MIN_COUNT = 200;
|
|
257
257
|
export const MESSAGE_MAX_AGE_DAYS = 14;
|
|
258
258
|
|
|
259
|
+
// DLQ rolloff thresholds
|
|
260
|
+
export const DLQ_MAX_COUNT = 50;
|
|
261
|
+
export const DLQ_MAX_AGE_DAYS = 14;
|
|
262
|
+
|
|
259
263
|
// Reserved persona names (command keywords that conflict with /persona subcommands)
|
|
260
264
|
export const RESERVED_PERSONA_NAMES = ["new", "clone"] as const;
|
|
261
265
|
export type ReservedPersonaName = typeof RESERVED_PERSONA_NAMES[number];
|
|
@@ -271,14 +275,19 @@ export function isReservedPersonaName(name: string): boolean {
|
|
|
271
275
|
export interface Message {
|
|
272
276
|
id: string;
|
|
273
277
|
role: "human" | "system";
|
|
274
|
-
|
|
278
|
+
verbal_response?: string; // Human text or persona's spoken reply
|
|
279
|
+
action_response?: string; // Stage direction / action the persona performs
|
|
280
|
+
silence_reason?: string; // Why the persona chose not to respond (not shown to LLM)
|
|
275
281
|
timestamp: string;
|
|
276
|
-
read: boolean;
|
|
282
|
+
read: boolean; // Has human seen this system message?
|
|
277
283
|
context_status: ContextStatus;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
|
|
285
|
+
// Extraction completion flags (omit when false to save space)
|
|
286
|
+
// Single-letter names minimize storage overhead for large message histories
|
|
287
|
+
f?: boolean; // Fact extraction completed
|
|
288
|
+
r?: boolean; // tRait extraction completed
|
|
289
|
+
p?: boolean; // Person extraction completed
|
|
290
|
+
o?: boolean; // tOpic extraction completed
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
export interface ChatMessage {
|
|
@@ -290,12 +299,15 @@ export interface ChatMessage {
|
|
|
290
299
|
// LLM TYPES
|
|
291
300
|
// =============================================================================
|
|
292
301
|
|
|
302
|
+
export type LLMRequestState = "pending" | "processing" | "dlq";
|
|
303
|
+
|
|
293
304
|
export interface LLMRequest {
|
|
294
305
|
id: string;
|
|
295
306
|
created_at: string;
|
|
296
307
|
attempts: number;
|
|
297
308
|
last_attempt?: string;
|
|
298
309
|
retry_after?: string;
|
|
310
|
+
state: LLMRequestState;
|
|
299
311
|
type: LLMRequestType;
|
|
300
312
|
priority: LLMPriority;
|
|
301
313
|
system: string;
|
|
@@ -345,6 +357,7 @@ export interface MessageQueryOptions {
|
|
|
345
357
|
export interface QueueStatus {
|
|
346
358
|
state: "idle" | "busy" | "paused";
|
|
347
359
|
pending_count: number;
|
|
360
|
+
dlq_count: number;
|
|
348
361
|
current_operation?: string;
|
|
349
362
|
}
|
|
350
363
|
|
|
@@ -47,7 +47,7 @@ function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
|
47
47
|
return {
|
|
48
48
|
id: ocMsg.id,
|
|
49
49
|
role: ocMsg.role === "user" ? "human" : "system",
|
|
50
|
-
|
|
50
|
+
verbal_response: ocMsg.content,
|
|
51
51
|
timestamp: ocMsg.timestamp,
|
|
52
52
|
read: true,
|
|
53
53
|
context_status: "default" as ContextStatus,
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HeartbeatCheckPromptData, PromptOutput } from "./types.js";
|
|
9
|
-
import type
|
|
10
|
-
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
11
|
-
|
|
9
|
+
import { type Message, type Topic, type Person } from "../../core/types.js";
|
|
10
|
+
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
12
11
|
function formatTopicsWithGaps(topics: Topic[]): string {
|
|
13
12
|
if (topics.length === 0) return "(No topics with engagement gaps)";
|
|
14
13
|
|
|
@@ -31,23 +30,37 @@ function formatPeopleWithGaps(people: Person[]): string {
|
|
|
31
30
|
.join('\n');
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A "real" persona message is one the persona actually said to the human.
|
|
35
|
+
* silence_reason messages (persona chose not to speak) and action-only messages
|
|
36
|
+
* (no verbal_response) don't count as conversational outreach.
|
|
37
|
+
*/
|
|
38
|
+
function isConversationalMessage(m: Message): boolean {
|
|
39
|
+
if (m.role !== 'system') return false;
|
|
40
|
+
if (m.silence_reason !== undefined) return false;
|
|
41
|
+
// Action-only: has action but no verbal response
|
|
42
|
+
if (!m.verbal_response) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
function countTrailingPersonaMessages(history: Message[]): number {
|
|
35
47
|
if (history.length === 0) return 0;
|
|
36
48
|
|
|
37
49
|
let count = 0;
|
|
38
50
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
51
|
+
const msg = history[i];
|
|
52
|
+
if (msg.role === 'human') break;
|
|
53
|
+
if (isConversationalMessage(msg)) count++;
|
|
54
|
+
// Skip non-conversational system messages and keep looking back
|
|
45
55
|
}
|
|
46
56
|
return count;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
function getLastPersonaMessage(history: Message[]): Message | undefined {
|
|
50
|
-
|
|
60
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
61
|
+
if (isConversationalMessage(history[i])) return history[i];
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -148,9 +161,10 @@ ${formatMessagesAsPlaceholders(data.recent_history, personaName)}`;
|
|
|
148
161
|
|
|
149
162
|
let unansweredWarning = '';
|
|
150
163
|
if (lastPersonaMsg && consecutiveMessages >= 1) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
const rawPreview = getMessageDisplayText(lastPersonaMsg) ?? "";
|
|
165
|
+
const preview = rawPreview.length > 100
|
|
166
|
+
? rawPreview.substring(0, 100) + "..."
|
|
167
|
+
: rawPreview;
|
|
154
168
|
|
|
155
169
|
unansweredWarning = `
|
|
156
170
|
### CRITICAL: You Already Reached Out
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
|
|
2
2
|
import type { Message } from "../../core/types.js";
|
|
3
|
-
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
3
|
+
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
4
4
|
|
|
5
5
|
function formatItem(item: EiHeartbeatItem): string {
|
|
6
6
|
switch (item.type) {
|
|
@@ -105,9 +105,10 @@ ${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
|
|
|
105
105
|
|
|
106
106
|
let unansweredWarning = "";
|
|
107
107
|
if (lastEiMsg && consecutiveMessages >= 1) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const rawPreview = getMessageDisplayText(lastEiMsg) ?? "";
|
|
109
|
+
const preview = rawPreview.length > 100
|
|
110
|
+
? rawPreview.substring(0, 100) + "..."
|
|
111
|
+
: rawPreview;
|
|
111
112
|
|
|
112
113
|
unansweredWarning = `
|
|
113
114
|
### CRITICAL: You Already Reached Out
|
|
@@ -240,13 +240,26 @@ In addition to updating the ${typeLabel}, identify any **memorable, funny, impor
|
|
|
240
240
|
- Phrases that reveal personality or communication style
|
|
241
241
|
- Things you'd quote back to them later to make them laugh
|
|
242
242
|
- Unique expressions, malaphors, or turns of phrase
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
- Quotable moments from EITHER speaker — humans AND AI personas both say memorable things
|
|
244
|
+
|
|
245
|
+
**NEVER extract these — they are NOT quotes:**
|
|
246
|
+
- Technical identifiers: ARNs, URLs, file paths, UUIDs, config keys, environment variable values, role/policy names
|
|
247
|
+
- AI agent self-talk: "I notice I'm in Plan Mode", "I'll start by...", "Let me help you with...", status updates about the agent's own process
|
|
248
|
+
- AI apologies or acknowledgments: "You're absolutely right", "I apologize for that overreach", "Good decision to revert"
|
|
249
|
+
- Generic AI instructions or tips: "Remember to include X in your prompts", tool usage advice, workflow suggestions
|
|
250
|
+
- Dry technical facts: infrastructure descriptions, process status, batch sizes, system architecture summaries
|
|
251
|
+
- Status updates or process descriptions: "We're running a batch of...", "The pipeline is...", "I'm currently working on..."
|
|
252
|
+
- Generic statements that could come from anyone or any AI session
|
|
253
|
+
- Credentials, secrets, connection strings, or anything that looks like an access token
|
|
254
|
+
|
|
255
|
+
**The litmus test**: Would you bring this up at a bar with a friend? Would it make someone laugh, think, or feel something?
|
|
256
|
+
- "Does the Pope shit in his hat?" → YES. Hilarious malaphor.
|
|
257
|
+
- "AWSReservedSSO_cmidp-nihl-sandbox-adm_db7b191e026bdd85" → NO. That's a credential.
|
|
258
|
+
- "Slow is smooth. Smooth is fast." → YES (once). Pithy wisdom.
|
|
259
|
+
- "The authentication flow is working correctly now" → NO. Status update.
|
|
260
|
+
- "I built this, and now it's live." → YES. Pride and accomplishment.
|
|
261
|
+
|
|
262
|
+
**When in doubt, leave it out.** An empty quotes array is always acceptable.
|
|
250
263
|
|
|
251
264
|
Return them in the \`quotes\` array:
|
|
252
265
|
|
|
@@ -266,7 +279,6 @@ Return them in the \`quotes\` array:
|
|
|
266
279
|
|
|
267
280
|
**CRITICAL**: Return the EXACT text as it appears in the message (spacing, punctuation, formatting, etc.). WE CAN ONLY USE IT IF WE FIND IT IN THE TEXT.
|
|
268
281
|
|
|
269
|
-
|
|
270
282
|
# CRITICAL INSTRUCTIONS
|
|
271
283
|
|
|
272
284
|
ONLY ANALYZE the "Most Recent Messages" in the following conversation. The "Earlier Conversation" is provided for your context and has already been processed!
|
|
@@ -2,6 +2,42 @@ import type { Message } from "../core/types.js";
|
|
|
2
2
|
|
|
3
3
|
const MESSAGE_PLACEHOLDER_REGEX = /\[mid:([a-zA-Z0-9_-]+):([^\]]+)\]/g;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Returns the display text for a message from its structured fields.
|
|
7
|
+
* - action_response as _italics_
|
|
8
|
+
* - verbal_response as plain text
|
|
9
|
+
* - silence_reason shown so the user understands why a persona stayed silent
|
|
10
|
+
*/
|
|
11
|
+
export function getMessageDisplayText(message: Message): string | null {
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
14
|
+
if (message.verbal_response) parts.push(message.verbal_response);
|
|
15
|
+
if (message.silence_reason) {
|
|
16
|
+
const name = 'Persona'; // Caller doesn't pass persona name; frontends can override
|
|
17
|
+
parts.push(`[${name} chose not to respond because: ${message.silence_reason}]`);
|
|
18
|
+
}
|
|
19
|
+
if (parts.length === 0) return null;
|
|
20
|
+
return parts.join('\n\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the content string for a ChatMessage sent to the LLM.
|
|
25
|
+
* Unlike getMessageDisplayText (which is for frontend rendering and skips silence),
|
|
26
|
+
* this includes ALL structured fields so the persona has full conversational context:
|
|
27
|
+
* - action_response as _italics_
|
|
28
|
+
* - verbal_response as plain text
|
|
29
|
+
* - silence_reason as "You chose not to respond because: ..."
|
|
30
|
+
*/
|
|
31
|
+
export function buildChatMessageContent(message: Message): string {
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
34
|
+
if (message.verbal_response) parts.push(message.verbal_response);
|
|
35
|
+
if (message.silence_reason) {
|
|
36
|
+
parts.push(`You chose not to respond because: ${message.silence_reason}`);
|
|
37
|
+
}
|
|
38
|
+
return parts.join('\n\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
export function formatMessageAsPlaceholder(message: Message, personaName: string): string {
|
|
6
42
|
const role = message.role === "human" ? "human" : personaName;
|
|
7
43
|
return `[mid:${message.id}:${role}]`;
|
|
@@ -22,7 +58,8 @@ export function hydratePromptPlaceholders(
|
|
|
22
58
|
return `[${role}]: [message not found]`;
|
|
23
59
|
}
|
|
24
60
|
const displayRole = message.role === "human" ? "[human]" : `[${role}]`;
|
|
25
|
-
|
|
61
|
+
const text = getMessageDisplayText(message) ?? "[no content]";
|
|
62
|
+
return `${displayRole}: ${text}`;
|
|
26
63
|
});
|
|
27
64
|
}
|
|
28
65
|
|
|
@@ -19,9 +19,10 @@ import {
|
|
|
19
19
|
buildQuotesSection,
|
|
20
20
|
buildSystemKnowledgeSection,
|
|
21
21
|
getConversationStateText,
|
|
22
|
+
buildResponseFormatSection,
|
|
22
23
|
} from "./sections.js";
|
|
23
24
|
|
|
24
|
-
export type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
25
|
+
export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./types.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Special system prompt for Ei (the system guide persona)
|
|
@@ -47,6 +48,7 @@ Your role is unique among personas:
|
|
|
47
48
|
const associatesSection = buildAssociatesSection(data.visible_personas);
|
|
48
49
|
const systemKnowledge = buildSystemKnowledgeSection(data.isTUI);
|
|
49
50
|
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
51
|
+
const responseFormat = buildResponseFormatSection();
|
|
50
52
|
const currentTime = new Date().toISOString();
|
|
51
53
|
|
|
52
54
|
return `${identity}
|
|
@@ -63,16 +65,19 @@ ${associatesSection}
|
|
|
63
65
|
${systemKnowledge}
|
|
64
66
|
${priorities}
|
|
65
67
|
|
|
68
|
+
${responseFormat}
|
|
69
|
+
|
|
66
70
|
Current time: ${currentTime}
|
|
67
71
|
|
|
68
72
|
## Final Instructions
|
|
69
73
|
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
70
74
|
- The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
|
|
71
75
|
- If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
|
|
72
|
-
-
|
|
73
|
-
- If you decide not to respond, say exactly: No Message`;
|
|
76
|
+
- Your entire reply must be the JSON object. No prose before or after it.`
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
const RESPONSE_FORMAT_INSTRUCTION = `Respond to the conversation above using the JSON format specified in the Response Format section.`;
|
|
80
|
+
|
|
76
81
|
/**
|
|
77
82
|
* Standard system prompt for non-Ei personas
|
|
78
83
|
*/
|
|
@@ -85,6 +90,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
85
90
|
const quotesSection = buildQuotesSection(data.human.quotes, data.human);
|
|
86
91
|
const associatesSection = buildAssociatesSection(data.visible_personas);
|
|
87
92
|
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
93
|
+
const responseFormat = buildResponseFormatSection();
|
|
88
94
|
const currentTime = new Date().toISOString();
|
|
89
95
|
|
|
90
96
|
return `${identity}
|
|
@@ -100,12 +106,13 @@ ${quotesSection}
|
|
|
100
106
|
${associatesSection}
|
|
101
107
|
${priorities}
|
|
102
108
|
|
|
109
|
+
${responseFormat}
|
|
110
|
+
|
|
103
111
|
Current time: ${currentTime}
|
|
104
112
|
|
|
105
113
|
## Final Instructions
|
|
106
114
|
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
107
|
-
-
|
|
108
|
-
- If you decide not to respond, say exactly: No Message`;
|
|
115
|
+
- Your entire reply must be the JSON object. No prose before or after it.`
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
function buildUserPrompt(data: ResponsePromptData): string {
|
|
@@ -113,7 +120,7 @@ function buildUserPrompt(data: ResponsePromptData): string {
|
|
|
113
120
|
|
|
114
121
|
return `${conversationState}
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
${RESPONSE_FORMAT_INSTRUCTION}`;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|