ei-tui 0.1.3 → 0.1.5
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 +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -71,6 +71,7 @@ export function Sidebar() {
|
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
73
|
<box
|
|
74
|
+
flexDirection="column"
|
|
74
75
|
backgroundColor={
|
|
75
76
|
isActive() && highlightedPersona() === persona.id
|
|
76
77
|
? "#3d5a80"
|
|
@@ -78,12 +79,15 @@ export function Sidebar() {
|
|
|
78
79
|
? "#2d3748"
|
|
79
80
|
: "transparent"
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
marginBottom={
|
|
82
|
+
paddingX={1}
|
|
83
|
+
marginBottom={1}
|
|
83
84
|
>
|
|
84
85
|
<text fg={textColor()}>
|
|
85
86
|
{getLabel()}
|
|
86
87
|
</text>
|
|
88
|
+
<text fg="#586e75" wrapMode="word" height={2} visible={!!persona.short_description}>
|
|
89
|
+
{persona.short_description ?? ""}
|
|
90
|
+
</text>
|
|
87
91
|
</box>
|
|
88
92
|
);
|
|
89
93
|
}}
|
|
@@ -15,13 +15,20 @@ export function StatusBar() {
|
|
|
15
15
|
|
|
16
16
|
const getQueueIndicator = () => {
|
|
17
17
|
const status = queueStatus();
|
|
18
|
+
let label: string;
|
|
18
19
|
if (status.state === "busy") {
|
|
19
|
-
|
|
20
|
+
label = `Processing (${status.pending_count})`;
|
|
21
|
+
} else if (status.state === "paused") {
|
|
22
|
+
label = `Paused (${status.pending_count})`;
|
|
23
|
+
} else if (status.pending_count > 0) {
|
|
24
|
+
label = `Waiting (${status.pending_count})`;
|
|
25
|
+
} else {
|
|
26
|
+
label = "Ready";
|
|
20
27
|
}
|
|
21
|
-
if (status.
|
|
22
|
-
|
|
28
|
+
if (status.dlq_count > 0) {
|
|
29
|
+
label += ` [DLQ:${status.dlq_count}]`;
|
|
23
30
|
}
|
|
24
|
-
return
|
|
31
|
+
return label;
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
const getFocusIndicator = () => {
|
|
@@ -69,7 +76,7 @@ export function StatusBar() {
|
|
|
69
76
|
</text>
|
|
70
77
|
</Show>
|
|
71
78
|
|
|
72
|
-
<text fg={queueStatus().state === "busy" ? "#b58900" : "#586e75"}>
|
|
79
|
+
<text fg={queueStatus().state === "busy" ? "#b58900" : queueStatus().dlq_count > 0 ? "#dc322f" : queueStatus().pending_count > 0 ? "#2aa198" : "#586e75"}>
|
|
73
80
|
{getQueueIndicator()}
|
|
74
81
|
</text>
|
|
75
82
|
</box>
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
StateConflictData,
|
|
33
33
|
StateConflictResolution,
|
|
34
34
|
ContextStatus,
|
|
35
|
+
LLMRequest,
|
|
35
36
|
} from "../../../src/core/types.js";
|
|
36
37
|
|
|
37
38
|
interface EiStore {
|
|
@@ -57,6 +58,10 @@ export interface EiContextValue {
|
|
|
57
58
|
refreshMessages: () => Promise<void>;
|
|
58
59
|
abortCurrentOperation: () => Promise<void>;
|
|
59
60
|
resumeQueue: () => Promise<void>;
|
|
61
|
+
pauseQueue: () => void;
|
|
62
|
+
getQueueActiveItems: () => LLMRequest[];
|
|
63
|
+
getDLQItems: () => LLMRequest[];
|
|
64
|
+
updateQueueItem: (id: string, updates: Partial<LLMRequest>) => Promise<boolean>;
|
|
60
65
|
stopProcessor: () => Promise<void>;
|
|
61
66
|
saveAndExit: () => Promise<{ success: boolean; error?: string }>;
|
|
62
67
|
showNotification: (message: string, level: "error" | "warn" | "info") => void;
|
|
@@ -98,6 +103,7 @@ export interface EiContextValue {
|
|
|
98
103
|
dismissWelcomeOverlay: () => void;
|
|
99
104
|
deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
|
|
100
105
|
setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
106
|
+
recallPendingMessages: () => Promise<string>;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
const EiContext = createContext<EiContextValue>();
|
|
@@ -109,7 +115,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
109
115
|
activePersonaId: null,
|
|
110
116
|
activeContextBoundary: undefined,
|
|
111
117
|
messages: [],
|
|
112
|
-
queueStatus: { state: "idle", pending_count: 0 },
|
|
118
|
+
queueStatus: { state: "idle", pending_count: 0, dlq_count: 0 },
|
|
113
119
|
notification: null,
|
|
114
120
|
});
|
|
115
121
|
|
|
@@ -209,6 +215,27 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
209
215
|
await processor.resumeQueue();
|
|
210
216
|
};
|
|
211
217
|
|
|
218
|
+
const pauseQueue = () => {
|
|
219
|
+
if (!processor) return;
|
|
220
|
+
logger.info("Pausing queue");
|
|
221
|
+
processor.pauseQueue();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const getQueueActiveItems = (): LLMRequest[] => {
|
|
225
|
+
if (!processor) return [];
|
|
226
|
+
return processor.getQueueActiveItems();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const getDLQItems = (): LLMRequest[] => {
|
|
230
|
+
if (!processor) return [];
|
|
231
|
+
return processor.getDLQItems();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const updateQueueItem = async (id: string, updates: Partial<LLMRequest>): Promise<boolean> => {
|
|
235
|
+
if (!processor) return false;
|
|
236
|
+
return processor.updateQueueItem(id, updates);
|
|
237
|
+
};
|
|
238
|
+
|
|
212
239
|
const stopProcessor = async () => {
|
|
213
240
|
if (processor) {
|
|
214
241
|
await processor.stop();
|
|
@@ -378,6 +405,14 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
378
405
|
await processor.setMessageContextStatus(personaId, messageId, status);
|
|
379
406
|
};
|
|
380
407
|
|
|
408
|
+
const recallPendingMessages = async (): Promise<string> => {
|
|
409
|
+
if (!processor) return "";
|
|
410
|
+
const personaId = store.activePersonaId;
|
|
411
|
+
if (!personaId) return "";
|
|
412
|
+
return processor.recallPendingMessages(personaId);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
|
|
381
416
|
const searchHumanData = async (
|
|
382
417
|
query: string,
|
|
383
418
|
options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
|
|
@@ -497,11 +532,11 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
497
532
|
logger.debug(`onQueueStateChanged called with state: ${state}`);
|
|
498
533
|
if (processor) {
|
|
499
534
|
processor.getQueueStatus().then((status) => {
|
|
500
|
-
setStore("queueStatus", { state: status.state, pending_count: status.pending_count });
|
|
535
|
+
setStore("queueStatus", { state: status.state, pending_count: status.pending_count, dlq_count: status.dlq_count });
|
|
501
536
|
logger.debug(`store.queueStatus after setStore:`, store.queueStatus);
|
|
502
537
|
});
|
|
503
538
|
} else {
|
|
504
|
-
setStore("queueStatus", { state, pending_count: 0 });
|
|
539
|
+
setStore("queueStatus", { state, pending_count: 0, dlq_count: 0 });
|
|
505
540
|
}
|
|
506
541
|
},
|
|
507
542
|
onContextBoundaryChanged: (personaId) => {
|
|
@@ -559,6 +594,10 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
559
594
|
refreshMessages,
|
|
560
595
|
abortCurrentOperation,
|
|
561
596
|
resumeQueue,
|
|
597
|
+
pauseQueue,
|
|
598
|
+
getQueueActiveItems,
|
|
599
|
+
getDLQItems,
|
|
600
|
+
updateQueueItem,
|
|
562
601
|
stopProcessor,
|
|
563
602
|
saveAndExit,
|
|
564
603
|
showNotification,
|
|
@@ -591,6 +630,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
591
630
|
dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
|
|
592
631
|
deleteMessages,
|
|
593
632
|
setMessageContextStatus,
|
|
633
|
+
recallPendingMessages,
|
|
594
634
|
};
|
|
595
635
|
|
|
596
636
|
return (
|
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
type ParentComponent,
|
|
6
6
|
type Accessor,
|
|
7
7
|
} from "solid-js";
|
|
8
|
-
import { useKeyboard, useRenderer } from "@opentui/solid";
|
|
8
|
+
import { useKeyboard, useRenderer, useSelectionHandler } from "@opentui/solid";
|
|
9
9
|
import type { ScrollBoxRenderable, KeyEvent, TextareaRenderable, CliRenderer } from "@opentui/core";
|
|
10
10
|
import type { PersonaSummary } from "../../../src/core/types.js";
|
|
11
11
|
import { useEi } from "./ei";
|
|
12
12
|
import { logger } from "../util/logger";
|
|
13
|
+
import { copyToClipboard } from "../util/clipboard";
|
|
13
14
|
|
|
14
15
|
export type Panel = "sidebar" | "messages" | "input";
|
|
15
16
|
|
|
@@ -23,6 +24,7 @@ interface KeyboardContextValue {
|
|
|
23
24
|
toggleSidebar: () => void;
|
|
24
25
|
exitApp: () => Promise<void>;
|
|
25
26
|
renderer: CliRenderer;
|
|
27
|
+
resetHistoryIndex: () => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const KeyboardContext = createContext<KeyboardContextValue>();
|
|
@@ -31,11 +33,13 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
31
33
|
const [focusedPanel, setFocusedPanel] = createSignal<Panel>("input");
|
|
32
34
|
const [sidebarVisible, setSidebarVisible] = createSignal(true);
|
|
33
35
|
const renderer = useRenderer();
|
|
34
|
-
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification } = useEi();
|
|
36
|
+
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages } = useEi();
|
|
35
37
|
|
|
36
38
|
let messageScrollRef: ScrollBoxRenderable | null = null;
|
|
37
39
|
let textareaRef: TextareaRenderable | null = null;
|
|
38
40
|
let editorHandler: (() => Promise<void>) | null = null;
|
|
41
|
+
let historyIndex = -1; // -1 = not browsing history
|
|
42
|
+
let savedDraft = ""; // input text saved when history browsing starts
|
|
39
43
|
|
|
40
44
|
const registerMessageScroll = (scrollbox: ScrollBoxRenderable) => {
|
|
41
45
|
messageScrollRef = scrollbox;
|
|
@@ -123,6 +127,67 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
|
|
131
|
+
if (event.name === "up" && !event.ctrl && !event.shift && !event.meta) {
|
|
132
|
+
if (!textareaRef) return;
|
|
133
|
+
const cursor = textareaRef.logicalCursor;
|
|
134
|
+
// Only intercept when cursor is at the very beginning (row 0, col 0)
|
|
135
|
+
if (cursor.row !== 0 || cursor.col !== 0) return;
|
|
136
|
+
// Don't intercept when slash-command suggest panel is visible
|
|
137
|
+
if (textareaRef.plainText.startsWith("/")) return;
|
|
138
|
+
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
// First Up from fresh state: check for pending (unread) messages to recall
|
|
141
|
+
if (historyIndex === -1) {
|
|
142
|
+
const hasPending = messages().some(m => m.role === "human" && !m.read);
|
|
143
|
+
if (hasPending) {
|
|
144
|
+
savedDraft = textareaRef.plainText;
|
|
145
|
+
void recallPendingMessages().then(recalled => {
|
|
146
|
+
if (recalled) {
|
|
147
|
+
textareaRef!.setText(recalled);
|
|
148
|
+
textareaRef!.gotoBufferHome();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Navigate backward through sent-message history
|
|
155
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.verbal_response ?? ''));
|
|
156
|
+
if (history.length === 0) return;
|
|
157
|
+
if (historyIndex === -1) {
|
|
158
|
+
savedDraft = textareaRef.plainText;
|
|
159
|
+
}
|
|
160
|
+
historyIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
161
|
+
// history is newest-last; index 0 = most recent
|
|
162
|
+
const entry = history[history.length - 1 - historyIndex];
|
|
163
|
+
textareaRef.setText(entry);
|
|
164
|
+
textareaRef.gotoBufferHome(); // cursor at start so next Up continues backward
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.name === "down" && !event.ctrl && !event.shift && !event.meta) {
|
|
169
|
+
if (!textareaRef || historyIndex === -1) return;
|
|
170
|
+
// Only intercept when cursor is at the very end
|
|
171
|
+
if (textareaRef.cursorOffset !== textareaRef.plainText.length) return;
|
|
172
|
+
// Don't intercept when slash-command suggest panel is visible
|
|
173
|
+
if (textareaRef.plainText.startsWith("/")) return;
|
|
174
|
+
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
if (historyIndex === 0) {
|
|
177
|
+
// Back to the draft
|
|
178
|
+
historyIndex = -1;
|
|
179
|
+
textareaRef.setText(savedDraft);
|
|
180
|
+
textareaRef.gotoBufferEnd();
|
|
181
|
+
} else {
|
|
182
|
+
historyIndex -= 1;
|
|
183
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.verbal_response ?? ''));
|
|
184
|
+
const entry = history[history.length - 1 - historyIndex];
|
|
185
|
+
textareaRef.setText(entry);
|
|
186
|
+
textareaRef.gotoBufferEnd(); // cursor at end so next Down continues forward
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
126
191
|
if (!messageScrollRef) return;
|
|
127
192
|
|
|
128
193
|
const scrollAmount = messageScrollRef.height;
|
|
@@ -136,6 +201,28 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
136
201
|
}
|
|
137
202
|
});
|
|
138
203
|
|
|
204
|
+
|
|
205
|
+
useSelectionHandler((selection) => {
|
|
206
|
+
const text = selection.getSelectedText();
|
|
207
|
+
if (!text || text.length === 0) return;
|
|
208
|
+
logger.info(`Selection detected: ${text.length} chars, copying...`);
|
|
209
|
+
void copyToClipboard(text)
|
|
210
|
+
.then(() => {
|
|
211
|
+
showNotification(`Copied ${text.length} chars`, "info");
|
|
212
|
+
renderer.clearSelection();
|
|
213
|
+
logger.info(`Clipboard copy succeeded`);
|
|
214
|
+
})
|
|
215
|
+
.catch((err: unknown) => {
|
|
216
|
+
logger.error(`Clipboard copy failed: ${String(err)}`);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const resetHistoryIndex = () => {
|
|
221
|
+
historyIndex = -1;
|
|
222
|
+
savedDraft = "";
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
|
|
139
226
|
const value: KeyboardContextValue = {
|
|
140
227
|
focusedPanel,
|
|
141
228
|
setFocusedPanel,
|
|
@@ -146,6 +233,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
146
233
|
toggleSidebar,
|
|
147
234
|
exitApp,
|
|
148
235
|
renderer,
|
|
236
|
+
resetHistoryIndex,
|
|
149
237
|
};
|
|
150
238
|
|
|
151
239
|
return (
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { platform } from "os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write OSC 52 escape sequence to stdout.
|
|
5
|
+
* Works in terminals that support it (Kitty, Alacritty, etc.).
|
|
6
|
+
* Apple Terminal.app does NOT support OSC 52 — use copyNative() for macOS.
|
|
7
|
+
*/
|
|
8
|
+
function writeOsc52(text: string): void {
|
|
9
|
+
if (!process.stdout.isTTY) return;
|
|
10
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
11
|
+
const osc52 = `\x1b]52;c;${base64}\x07`;
|
|
12
|
+
const inTmux = process.env["TMUX"] || process.env["STY"];
|
|
13
|
+
const sequence = inTmux ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52;
|
|
14
|
+
process.stdout.write(sequence);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy text to clipboard using the best available native method.
|
|
19
|
+
* Mirrors OpenCode's clipboard.ts approach.
|
|
20
|
+
*/
|
|
21
|
+
export async function copyToClipboard(text: string): Promise<void> {
|
|
22
|
+
// Always attempt OSC 52 (works over SSH, in supported terminals)
|
|
23
|
+
writeOsc52(text);
|
|
24
|
+
|
|
25
|
+
const os = platform();
|
|
26
|
+
|
|
27
|
+
if (os === "darwin") {
|
|
28
|
+
// osascript is the reliable path on macOS (works in Apple Terminal + tmux)
|
|
29
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
30
|
+
const proc = Bun.spawn(
|
|
31
|
+
["osascript", "-e", `set the clipboard to "${escaped}"`],
|
|
32
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
33
|
+
);
|
|
34
|
+
await proc.exited.catch(() => {});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (os === "linux") {
|
|
39
|
+
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
|
40
|
+
const proc = Bun.spawn(["wl-copy"], {
|
|
41
|
+
stdin: "pipe",
|
|
42
|
+
stdout: "ignore",
|
|
43
|
+
stderr: "ignore",
|
|
44
|
+
});
|
|
45
|
+
proc.stdin.write(text);
|
|
46
|
+
proc.stdin.end();
|
|
47
|
+
await proc.exited.catch(() => {});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (Bun.which("xclip")) {
|
|
51
|
+
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
|
|
52
|
+
stdin: "pipe",
|
|
53
|
+
stdout: "ignore",
|
|
54
|
+
stderr: "ignore",
|
|
55
|
+
});
|
|
56
|
+
proc.stdin.write(text);
|
|
57
|
+
proc.stdin.end();
|
|
58
|
+
await proc.exited.catch(() => {});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Bun.which("xsel")) {
|
|
62
|
+
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
|
|
63
|
+
stdin: "pipe",
|
|
64
|
+
stdout: "ignore",
|
|
65
|
+
stderr: "ignore",
|
|
66
|
+
});
|
|
67
|
+
proc.stdin.write(text);
|
|
68
|
+
proc.stdin.end();
|
|
69
|
+
await proc.exited.catch(() => {});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import YAML from "yaml";
|
|
2
|
-
import type {
|
|
3
|
-
PersonaEntity,
|
|
4
|
-
HumanEntity,
|
|
2
|
+
import type {
|
|
3
|
+
PersonaEntity,
|
|
4
|
+
HumanEntity,
|
|
5
5
|
HumanSettings,
|
|
6
6
|
CeremonyConfig,
|
|
7
7
|
OpenCodeSettings,
|
|
8
|
-
Fact,
|
|
9
|
-
Trait,
|
|
10
|
-
Topic,
|
|
8
|
+
Fact,
|
|
9
|
+
Trait,
|
|
10
|
+
Topic,
|
|
11
11
|
Person,
|
|
12
12
|
PersonaTopic,
|
|
13
13
|
ProviderAccount,
|
|
14
14
|
ProviderType,
|
|
15
15
|
Quote,
|
|
16
16
|
Message,
|
|
17
|
+
LLMRequest,
|
|
18
|
+
LLMRequestState,
|
|
19
|
+
LLMPriority,
|
|
17
20
|
} from "../../../src/core/types.js";
|
|
18
21
|
import { ContextStatus } from "../../../src/core/types.js";
|
|
19
22
|
|
|
@@ -220,7 +223,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[]): str
|
|
|
220
223
|
: persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
|
|
221
224
|
name, perspective, approach, personal_stake, exposure_current, exposure_desired
|
|
222
225
|
})),
|
|
223
|
-
heartbeat_delay_ms: persona.heartbeat_delay_ms,
|
|
226
|
+
heartbeat_delay_ms: persona.heartbeat_delay_ms || 'default',
|
|
224
227
|
context_window_hours: persona.context_window_hours,
|
|
225
228
|
is_paused: persona.is_paused || undefined,
|
|
226
229
|
pause_until: persona.pause_until,
|
|
@@ -322,7 +325,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity): P
|
|
|
322
325
|
groups_visible: groupsVisible,
|
|
323
326
|
traits,
|
|
324
327
|
topics,
|
|
325
|
-
heartbeat_delay_ms: data.heartbeat_delay_ms,
|
|
328
|
+
heartbeat_delay_ms: stripPlaceholder(data.heartbeat_delay_ms, 'default'),
|
|
326
329
|
context_window_hours: data.context_window_hours,
|
|
327
330
|
is_paused: data.is_paused ?? false,
|
|
328
331
|
pause_until: data.pause_until,
|
|
@@ -347,7 +350,7 @@ export function humanToYAML(human: HumanEntity): string {
|
|
|
347
350
|
|
|
348
351
|
return YAML.stringify(data, {
|
|
349
352
|
lineWidth: 0,
|
|
350
|
-
});
|
|
353
|
+
}).replace(/^(\s+validated:\s+\S+)$/mg, '$1 # none | ei | human');
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
export interface HumanYAMLResult {
|
|
@@ -640,6 +643,10 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
|
640
643
|
data.default_model = undefined;
|
|
641
644
|
}
|
|
642
645
|
|
|
646
|
+
if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
|
|
647
|
+
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
648
|
+
}
|
|
649
|
+
|
|
643
650
|
return {
|
|
644
651
|
id: crypto.randomUUID(),
|
|
645
652
|
name: data.name,
|
|
@@ -687,6 +694,10 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
687
694
|
throw new Error("Provider URL is required");
|
|
688
695
|
}
|
|
689
696
|
|
|
697
|
+
if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
|
|
698
|
+
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
699
|
+
}
|
|
700
|
+
|
|
690
701
|
return {
|
|
691
702
|
id: original.id,
|
|
692
703
|
name: data.name,
|
|
@@ -711,13 +722,17 @@ interface EditableMessage {
|
|
|
711
722
|
timestamp: string;
|
|
712
723
|
context_status: ContextStatus;
|
|
713
724
|
_delete?: boolean;
|
|
714
|
-
|
|
725
|
+
// verbal_response | action_response | silence_reason
|
|
726
|
+
verbal_response?: string;
|
|
727
|
+
action_response?: string;
|
|
728
|
+
silence_reason?: string;
|
|
715
729
|
}
|
|
716
730
|
|
|
717
731
|
export function contextToYAML(messages: Message[]): string {
|
|
718
732
|
const header = [
|
|
719
733
|
"# context_status: default | always | never",
|
|
720
734
|
"# _delete: true — permanently removes the message",
|
|
735
|
+
"# verbal_response | action_response | silence_reason",
|
|
721
736
|
].join("\n");
|
|
722
737
|
|
|
723
738
|
const data: EditableMessage[] = messages.map((m) => ({
|
|
@@ -726,7 +741,9 @@ export function contextToYAML(messages: Message[]): string {
|
|
|
726
741
|
timestamp: m.timestamp,
|
|
727
742
|
context_status: m.context_status,
|
|
728
743
|
_delete: false,
|
|
729
|
-
|
|
744
|
+
verbal_response: m.verbal_response,
|
|
745
|
+
action_response: m.action_response,
|
|
746
|
+
silence_reason: m.silence_reason,
|
|
730
747
|
}));
|
|
731
748
|
|
|
732
749
|
return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
|
|
@@ -752,4 +769,57 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
|
752
769
|
}
|
|
753
770
|
|
|
754
771
|
return { messages, deletedMessageIds };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
// =============================================================================
|
|
776
|
+
// QUEUE ITEM YAML
|
|
777
|
+
// =============================================================================
|
|
778
|
+
|
|
779
|
+
export function queueItemsToYAML(items: LLMRequest[]): string {
|
|
780
|
+
const data = items.map(item => ({
|
|
781
|
+
id: item.id,
|
|
782
|
+
state: item.state,
|
|
783
|
+
created_at: item.created_at,
|
|
784
|
+
attempts: item.attempts,
|
|
785
|
+
last_attempt: item.last_attempt,
|
|
786
|
+
retry_after: item.retry_after,
|
|
787
|
+
type: item.type,
|
|
788
|
+
priority: item.priority,
|
|
789
|
+
next_step: item.next_step,
|
|
790
|
+
model: item.model,
|
|
791
|
+
data: item.data,
|
|
792
|
+
// NOTE: system/user prompts omitted (large); to requeue: set state='pending', attempts=0
|
|
793
|
+
}));
|
|
794
|
+
return YAML.stringify(data, { lineWidth: 0 });
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export interface QueueItemUpdate {
|
|
798
|
+
id: string;
|
|
799
|
+
state: LLMRequestState;
|
|
800
|
+
attempts: number;
|
|
801
|
+
model?: string;
|
|
802
|
+
priority?: LLMPriority;
|
|
803
|
+
data?: Record<string, unknown>;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export function queueItemsFromYAML(yamlContent: string): QueueItemUpdate[] {
|
|
807
|
+
const data = YAML.parse(yamlContent) as QueueItemUpdate[];
|
|
808
|
+
if (!Array.isArray(data)) throw new Error("Expected a YAML array of queue items");
|
|
809
|
+
return data.map(item => {
|
|
810
|
+
if (!item.id) throw new Error(`Queue item missing 'id' field`);
|
|
811
|
+
if (!item.state) throw new Error(`Queue item ${item.id} missing 'state' field`);
|
|
812
|
+
const validStates: LLMRequestState[] = ["pending", "processing", "dlq"];
|
|
813
|
+
if (!validStates.includes(item.state)) {
|
|
814
|
+
throw new Error(`Queue item ${item.id} has invalid state '${item.state}'. Valid: ${validStates.join(", ")}`);
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
id: item.id,
|
|
818
|
+
state: item.state,
|
|
819
|
+
attempts: typeof item.attempts === "number" ? item.attempts : 0,
|
|
820
|
+
model: item.model,
|
|
821
|
+
priority: item.priority,
|
|
822
|
+
data: item.data,
|
|
823
|
+
};
|
|
824
|
+
});
|
|
755
825
|
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { EiValidationPromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import type { DataItemBase } from "../../core/types.js";
|
|
3
|
-
|
|
4
|
-
function formatDataItem(item: DataItemBase, label: string): string {
|
|
5
|
-
return `### ${label}
|
|
6
|
-
- **Name**: ${item.name}
|
|
7
|
-
- **Description**: ${item.description}
|
|
8
|
-
- **Sentiment**: ${item.sentiment}
|
|
9
|
-
- **Last Updated**: ${item.last_updated}
|
|
10
|
-
${item.learned_by ? `- **Learned By**: ${item.learned_by}` : ""}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function buildEiValidationPrompt(data: EiValidationPromptData): PromptOutput {
|
|
14
|
-
if (!data.item_name || !data.data_type) {
|
|
15
|
-
throw new Error("buildEiValidationPrompt: item_name and data_type are required");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const roleFragment = `You are Ei, the system guide and arbiter of truth for the human's data.
|
|
19
|
-
|
|
20
|
-
When other personas learn things about the human, those changes come to you for validation. Your job is to ensure data quality and consistency.`;
|
|
21
|
-
|
|
22
|
-
const contextFragment = `# Validation Request
|
|
23
|
-
|
|
24
|
-
**Type**: ${data.data_type.toUpperCase()}
|
|
25
|
-
**Item**: "${data.item_name}"
|
|
26
|
-
**Source**: ${data.source_persona}
|
|
27
|
-
**Context**: ${data.context}`;
|
|
28
|
-
|
|
29
|
-
const dataFragment = data.current_item
|
|
30
|
-
? `# Data Comparison
|
|
31
|
-
|
|
32
|
-
${formatDataItem(data.current_item, "Current (existing data)")}
|
|
33
|
-
|
|
34
|
-
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}`
|
|
35
|
-
: `# New Data
|
|
36
|
-
|
|
37
|
-
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}
|
|
38
|
-
|
|
39
|
-
*(This is a NEW ${data.data_type} - no existing data to compare)*`;
|
|
40
|
-
|
|
41
|
-
const guidelinesFragment = `# Validation Guidelines
|
|
42
|
-
|
|
43
|
-
## ACCEPT if:
|
|
44
|
-
- Change is factual and well-evidenced
|
|
45
|
-
- New information is consistent with what you know about the human
|
|
46
|
-
- Source persona's interpretation seems reasonable
|
|
47
|
-
- Data improves understanding of the human
|
|
48
|
-
|
|
49
|
-
## MODIFY if:
|
|
50
|
-
- Partially correct but needs refinement
|
|
51
|
-
- Description could be clearer or more accurate
|
|
52
|
-
- Sentiment or other fields seem off
|
|
53
|
-
- Good information but poorly expressed
|
|
54
|
-
|
|
55
|
-
## REJECT if:
|
|
56
|
-
- Contradicts known facts
|
|
57
|
-
- Seems like a hallucination or misunderstanding
|
|
58
|
-
- Would misrepresent the human
|
|
59
|
-
- Source persona lacks context to make this claim
|
|
60
|
-
|
|
61
|
-
## Considerations
|
|
62
|
-
- ${data.source_persona} may have context you don't
|
|
63
|
-
- The human's data should be accurate, not just convenient
|
|
64
|
-
- When in doubt, lean toward accepting with modifications`;
|
|
65
|
-
|
|
66
|
-
const outputFragment = `# Response Format
|
|
67
|
-
|
|
68
|
-
\`\`\`json
|
|
69
|
-
{
|
|
70
|
-
"decision": "accept" | "modify" | "reject",
|
|
71
|
-
"reason": "Brief explanation of your decision",
|
|
72
|
-
"modified_item": { ... } // Only if decision is "modify"
|
|
73
|
-
}
|
|
74
|
-
\`\`\`
|
|
75
|
-
|
|
76
|
-
If modifying, include the corrected item with all fields.`;
|
|
77
|
-
|
|
78
|
-
const system = `${roleFragment}
|
|
79
|
-
|
|
80
|
-
${contextFragment}
|
|
81
|
-
|
|
82
|
-
${dataFragment}
|
|
83
|
-
|
|
84
|
-
${guidelinesFragment}
|
|
85
|
-
|
|
86
|
-
${outputFragment}`;
|
|
87
|
-
|
|
88
|
-
const user = `Review the ${data.data_type} "${data.item_name}" proposed by ${data.source_persona}.
|
|
89
|
-
|
|
90
|
-
Should this change be accepted, modified, or rejected?`;
|
|
91
|
-
|
|
92
|
-
return { system, user };
|
|
93
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { DataItemBase } from "../../core/types.js";
|
|
2
|
-
|
|
3
|
-
export interface PromptOutput {
|
|
4
|
-
system: string;
|
|
5
|
-
user: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface EiValidationPromptData {
|
|
9
|
-
validation_type: "cross_persona";
|
|
10
|
-
item_name: string;
|
|
11
|
-
data_type: "fact" | "trait" | "topic" | "person";
|
|
12
|
-
context: string;
|
|
13
|
-
source_persona: string;
|
|
14
|
-
current_item?: DataItemBase;
|
|
15
|
-
proposed_item: DataItemBase;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface EiValidationResult {
|
|
19
|
-
decision: "accept" | "modify" | "reject";
|
|
20
|
-
reason: string;
|
|
21
|
-
modified_item?: DataItemBase;
|
|
22
|
-
}
|