ei-tui 0.1.3 → 0.1.4

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.
@@ -1,3 +1,5 @@
1
+ import { createEffect, createSignal } from "solid-js";
2
+ import { getAllCommands } from "../commands/registry";
1
3
  import type { TextareaRenderable, KeyBinding } from "@opentui/core";
2
4
  import { useEi } from "../context/ei";
3
5
  import { useKeyboardNav } from "../context/keyboard";
@@ -20,6 +22,9 @@ import { quotesCommand } from "../commands/quotes";
20
22
  import { providerCommand } from "../commands/provider";
21
23
  import { setSyncCommand } from "../commands/setsync";
22
24
  import { useOverlay } from "../context/overlay";
25
+ import { CommandSuggest } from "./CommandSuggest";
26
+ import { useKeyboard } from "@opentui/solid";
27
+ import type { KeyEvent } from "@opentui/core";
23
28
 
24
29
  const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
25
30
  { name: "return", action: "submit" },
@@ -30,7 +35,7 @@ const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
30
35
  export function PromptInput() {
31
36
  const ei = useEi();
32
37
  const { sendMessage, activePersonaId, stopProcessor, showNotification } = ei;
33
- const { registerTextarea, registerEditorHandler, exitApp, renderer } = useKeyboardNav();
38
+ const { registerTextarea, registerEditorHandler, exitApp, renderer, resetHistoryIndex } = useKeyboardNav();
34
39
  const { showOverlay, hideOverlay, overlayRenderer } = useOverlay();
35
40
 
36
41
  registerCommand(helpCommand);
@@ -54,6 +59,65 @@ export function PromptInput() {
54
59
 
55
60
  let textareaRef: TextareaRenderable | undefined;
56
61
 
62
+ const [inputText, setInputText] = createSignal("");
63
+ const [suggestIndex, setSuggestIndex] = createSignal(0);
64
+
65
+ const suggestMatches = () => {
66
+ const raw = inputText().trim();
67
+ if (!raw.startsWith("/")) return [];
68
+ const query = raw.slice(1).split(/\s/)[0].replace(/!$/, "").toLowerCase();
69
+ return getAllCommands().filter(
70
+ (cmd) =>
71
+ cmd.name.startsWith(query) ||
72
+ cmd.aliases.some((a) => a.startsWith(query))
73
+ );
74
+ };
75
+
76
+ const suggestVisible = () => suggestMatches().length > 0 && !overlayRenderer();
77
+
78
+ createEffect(() => {
79
+ inputText();
80
+ setSuggestIndex(0);
81
+ });
82
+
83
+ createEffect(() => {
84
+ activePersonaId();
85
+ resetHistoryIndex();
86
+ });
87
+
88
+ useKeyboard((event: KeyEvent) => {
89
+ if (!suggestVisible()) return;
90
+
91
+ if (event.name === "up") {
92
+ event.preventDefault();
93
+ setSuggestIndex(i => Math.max(0, i - 1));
94
+ return;
95
+ }
96
+ if (event.name === "down") {
97
+ event.preventDefault();
98
+ setSuggestIndex(i => Math.min(suggestMatches().length - 1, i + 1));
99
+ return;
100
+ }
101
+ if (event.name === "tab" || event.name === "right") {
102
+ event.preventDefault();
103
+ const match = suggestMatches()[suggestIndex()];
104
+ if (match) {
105
+ textareaRef?.setText(`/${match.name} `);
106
+ setInputText(`/${match.name} `);
107
+ textareaRef?.gotoBufferEnd();
108
+ setSuggestIndex(0);
109
+ }
110
+ return;
111
+ }
112
+ if (event.name === "escape") {
113
+ event.preventDefault();
114
+ textareaRef?.clear();
115
+ setInputText("");
116
+ setSuggestIndex(0);
117
+ return;
118
+ }
119
+ });
120
+
57
121
  const getCommandContext = (): CommandContext => ({
58
122
  showOverlay,
59
123
  hideOverlay,
@@ -62,21 +126,24 @@ export function PromptInput() {
62
126
  stopProcessor,
63
127
  ei,
64
128
  renderer,
65
- setInputText: (text: string) => textareaRef?.setText(text),
129
+ setInputText: (text: string) => {
130
+ textareaRef?.setText(text);
131
+ setInputText(text);
132
+ },
66
133
  getInputText: () => textareaRef?.plainText || "",
67
134
  });
68
135
 
69
136
  const handleSubmit = async () => {
70
137
  const text = textareaRef?.plainText?.trim();
71
138
  if (!text) return;
72
-
139
+
73
140
  if (text.startsWith("/")) {
74
141
  const isEditorCmd = text.startsWith("/editor") ||
75
142
  text.startsWith("/edit") ||
76
143
  text.startsWith("/e ") ||
77
144
  text === "/e";
78
- const opensEditorForData = text.startsWith("/me") ||
79
- text.startsWith("/details") ||
145
+ const opensEditorForData = text.startsWith("/me") ||
146
+ text.startsWith("/details") ||
80
147
  text.startsWith("/d ") ||
81
148
  text === "/d" ||
82
149
  text.startsWith("/settings") ||
@@ -87,18 +154,24 @@ export function PromptInput() {
87
154
  text.startsWith("/q ") ||
88
155
  text.startsWith("/context") ||
89
156
  text.startsWith("/messages");
90
-
157
+
91
158
  if (!isEditorCmd && !opensEditorForData) {
92
159
  textareaRef?.clear();
160
+ setInputText("");
93
161
  }
94
162
  await parseAndExecute(text, getCommandContext());
95
163
  if (opensEditorForData) {
96
164
  textareaRef?.clear();
165
+ setInputText("");
97
166
  }
167
+ setSuggestIndex(0);
98
168
  return;
99
169
  }
100
-
170
+
101
171
  textareaRef?.clear();
172
+ setInputText("");
173
+ resetHistoryIndex();
174
+ setSuggestIndex(0);
102
175
  if (!activePersonaId()) return;
103
176
  await sendMessage(text);
104
177
  };
@@ -115,31 +188,40 @@ export function PromptInput() {
115
188
  };
116
189
 
117
190
  return (
118
- <box
191
+ <box
192
+ flexDirection="column"
119
193
  flexShrink={0}
120
- border={["top"]}
121
- borderStyle="single"
122
- backgroundColor="#0f3460"
123
- paddingLeft={1}
124
- paddingRight={1}
125
- paddingTop={0.5}
126
- paddingBottom={0.5}
127
194
  >
128
- <textarea
129
- ref={(r: TextareaRenderable) => {
130
- textareaRef = r;
131
- registerTextarea(r);
132
- }}
133
- focused={!overlayRenderer()}
134
- onSubmit={() => void handleSubmit()}
135
- placeholder={getPlaceholder()}
136
- textColor="#eee8d5"
137
- backgroundColor="#0f3460"
138
- cursorColor="#eee8d5"
139
- minHeight={1}
140
- maxHeight={6}
141
- keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
195
+ <CommandSuggest
196
+ input={inputText}
197
+ highlightIndex={suggestIndex}
142
198
  />
199
+ <box
200
+ border={["top"]}
201
+ borderStyle="single"
202
+ backgroundColor="#0f3460"
203
+ paddingLeft={1}
204
+ paddingRight={1}
205
+ paddingTop={0.5}
206
+ paddingBottom={0.5}
207
+ >
208
+ <textarea
209
+ ref={(r: TextareaRenderable) => {
210
+ textareaRef = r;
211
+ registerTextarea(r);
212
+ }}
213
+ focused={!overlayRenderer()}
214
+ onSubmit={() => void handleSubmit()}
215
+ onContentChange={() => setInputText(textareaRef?.plainText ?? "")}
216
+ placeholder={getPlaceholder()}
217
+ textColor="#eee8d5"
218
+ backgroundColor="#0f3460"
219
+ cursorColor="#eee8d5"
220
+ minHeight={1}
221
+ maxHeight={6}
222
+ keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
223
+ />
224
+ </box>
143
225
  </box>
144
226
  );
145
227
  }
@@ -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
  }}
@@ -98,6 +98,7 @@ export interface EiContextValue {
98
98
  dismissWelcomeOverlay: () => void;
99
99
  deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
100
100
  setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
101
+ recallPendingMessages: () => Promise<string>;
101
102
  }
102
103
 
103
104
  const EiContext = createContext<EiContextValue>();
@@ -378,6 +379,14 @@ export const EiProvider: ParentComponent = (props) => {
378
379
  await processor.setMessageContextStatus(personaId, messageId, status);
379
380
  };
380
381
 
382
+ const recallPendingMessages = async (): Promise<string> => {
383
+ if (!processor) return "";
384
+ const personaId = store.activePersonaId;
385
+ if (!personaId) return "";
386
+ return processor.recallPendingMessages(personaId);
387
+ };
388
+
389
+
381
390
  const searchHumanData = async (
382
391
  query: string,
383
392
  options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
@@ -591,6 +600,7 @@ export const EiProvider: ParentComponent = (props) => {
591
600
  dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
592
601
  deleteMessages,
593
602
  setMessageContextStatus,
603
+ recallPendingMessages,
594
604
  };
595
605
 
596
606
  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.content);
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.content);
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
+ }
@@ -220,7 +220,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[]): str
220
220
  : persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
221
221
  name, perspective, approach, personal_stake, exposure_current, exposure_desired
222
222
  })),
223
- heartbeat_delay_ms: persona.heartbeat_delay_ms,
223
+ heartbeat_delay_ms: persona.heartbeat_delay_ms || 'default',
224
224
  context_window_hours: persona.context_window_hours,
225
225
  is_paused: persona.is_paused || undefined,
226
226
  pause_until: persona.pause_until,
@@ -322,7 +322,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity): P
322
322
  groups_visible: groupsVisible,
323
323
  traits,
324
324
  topics,
325
- heartbeat_delay_ms: data.heartbeat_delay_ms,
325
+ heartbeat_delay_ms: stripPlaceholder(data.heartbeat_delay_ms, 'default'),
326
326
  context_window_hours: data.context_window_hours,
327
327
  is_paused: data.is_paused ?? false,
328
328
  pause_until: data.pause_until,
@@ -347,7 +347,7 @@ export function humanToYAML(human: HumanEntity): string {
347
347
 
348
348
  return YAML.stringify(data, {
349
349
  lineWidth: 0,
350
- });
350
+ }).replace(/^(\s+validated:\s+\S+)$/mg, '$1 # none | ei | human');
351
351
  }
352
352
 
353
353
  export interface HumanYAMLResult {
@@ -640,6 +640,10 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
640
640
  data.default_model = undefined;
641
641
  }
642
642
 
643
+ if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
644
+ 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.`);
645
+ }
646
+
643
647
  return {
644
648
  id: crypto.randomUUID(),
645
649
  name: data.name,
@@ -687,6 +691,10 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
687
691
  throw new Error("Provider URL is required");
688
692
  }
689
693
 
694
+ if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
695
+ 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.`);
696
+ }
697
+
690
698
  return {
691
699
  id: original.id,
692
700
  name: data.name,
@@ -752,4 +760,4 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
752
760
  }
753
761
 
754
762
  return { messages, deletedMessageIds };
755
- }
763
+ }
@@ -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
- }