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.
Files changed (44) hide show
  1. package/README.md +36 -35
  2. package/package.json +6 -2
  3. package/src/README.md +85 -1
  4. package/src/cli/README.md +30 -20
  5. package/src/cli/retrieval.ts +5 -17
  6. package/src/cli.ts +69 -0
  7. package/src/core/handlers/index.ts +195 -172
  8. package/src/core/orchestrators/ceremony.ts +4 -4
  9. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  10. package/src/core/processor.ts +245 -77
  11. package/src/core/queue-processor.ts +3 -26
  12. package/src/core/state/checkpoints.ts +4 -0
  13. package/src/core/state/personas.ts +13 -1
  14. package/src/core/state/queue.ts +80 -23
  15. package/src/core/state-manager.ts +36 -10
  16. package/src/core/types.ts +23 -11
  17. package/src/core/utils/crossFind.ts +44 -0
  18. package/src/core/utils/index.ts +4 -0
  19. package/src/integrations/opencode/importer.ts +118 -691
  20. package/src/prompts/heartbeat/check.ts +27 -13
  21. package/src/prompts/heartbeat/ei.ts +65 -136
  22. package/src/prompts/heartbeat/types.ts +47 -17
  23. package/src/prompts/human/item-update.ts +20 -8
  24. package/src/prompts/index.ts +2 -5
  25. package/src/prompts/message-utils.ts +42 -3
  26. package/src/prompts/response/index.ts +13 -6
  27. package/src/prompts/response/sections.ts +65 -12
  28. package/src/prompts/response/types.ts +10 -0
  29. package/tui/README.md +89 -4
  30. package/tui/src/commands/dlq.ts +75 -0
  31. package/tui/src/commands/editor.tsx +1 -1
  32. package/tui/src/commands/queue.ts +77 -0
  33. package/tui/src/components/CommandSuggest.tsx +50 -0
  34. package/tui/src/components/MessageList.tsx +12 -2
  35. package/tui/src/components/PromptInput.tsx +118 -30
  36. package/tui/src/components/Sidebar.tsx +6 -2
  37. package/tui/src/components/StatusBar.tsx +12 -5
  38. package/tui/src/context/ei.tsx +43 -3
  39. package/tui/src/context/keyboard.tsx +90 -2
  40. package/tui/src/util/clipboard.ts +73 -0
  41. package/tui/src/util/yaml-serializers.ts +81 -11
  42. package/src/prompts/validation/ei.ts +0 -93
  43. package/src/prompts/validation/index.ts +0 -6
  44. 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
- padding={1}
82
- marginBottom={0.5}
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
- return `Processing (${status.pending_count})`;
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.state === "paused") {
22
- return "Paused";
28
+ if (status.dlq_count > 0) {
29
+ label += ` [DLQ:${status.dlq_count}]`;
23
30
  }
24
- return "Ready";
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>
@@ -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
- content: string;
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
- content: m.content,
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,6 +0,0 @@
1
- export { buildEiValidationPrompt } from "./ei.js";
2
- export type {
3
- EiValidationPromptData,
4
- EiValidationResult,
5
- PromptOutput,
6
- } from "./types.js";
@@ -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
- }