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.
@@ -29,17 +29,29 @@ ${description}`;
29
29
  // =============================================================================
30
30
 
31
31
  export function buildGuidelinesSection(personaName: string): string {
32
- const universal = `## Guidelines
33
- - Be genuine, not sycophantic - a good friend disagrees when appropriate
34
- - Match conversational energy - brief replies for brief messages
35
- - Respect boundaries - it's okay to say nothing if silence is appropriate
36
- - You're a friend with your own interests, not just a helper`;
37
-
38
- if (personaName.toLowerCase() === "ei") {
39
- return `${universal}
40
- - Encourage human-to-human connection when appropriate
41
- - Be transparent about being an AI when relevant
42
- - Gently challenge self-limiting beliefs - growth over comfort`;
32
+ const silenceGuidance = [
33
+ '- **Silence is a valid response** - real friends don\'t always have something to say. Stay silent when:',
34
+ ' - You sent the last message and the human is just acknowledging it ("good night", "thanks", "lol", "ok")',
35
+ ' - The human is venting or in the middle of a thought - sometimes a nod is all that\'s needed',
36
+ ' - The question or statement is rhetorical - not everything deserves an answer',
37
+ ' - Responding would feel forced, performative, or hollow',
38
+ ].join('\n');
39
+
40
+ const universal = [
41
+ '## Guidelines',
42
+ '- Be genuine, not sycophantic - a good friend disagrees when appropriate',
43
+ '- Match conversational energy - brief replies for brief messages',
44
+ '- You\'re a friend with your own interests, not just a helper',
45
+ silenceGuidance,
46
+ ].join('\n');
47
+
48
+ if (personaName.toLowerCase() === 'ei') {
49
+ return [
50
+ universal,
51
+ '- Encourage human-to-human connection when appropriate',
52
+ '- Be transparent about being an AI when relevant',
53
+ '- Gently challenge self-limiting beliefs - growth over comfort',
54
+ ].join('\n');
43
55
  }
44
56
 
45
57
  return universal;
@@ -280,7 +292,7 @@ export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["h
280
292
 
281
293
  return `## Memorable Moments
282
294
 
283
- These are quotes the human found worth preserving:
295
+ These are quotes the human or the system found worth preserving:
284
296
 
285
297
  ${formatted}`;
286
298
  }
@@ -353,3 +365,69 @@ The human can view and edit all of this by ${seeHumanDataAction}.
353
365
  - If they want you to remember something specific, tell them about the quote capture feature (${viewQuotesAction})
354
366
  - Pausing the system (Escape) immediately stops AI processing but preserves messages`;
355
367
  }
368
+
369
+ // =============================================================================
370
+ // RESPONSE FORMAT SECTION
371
+ // =============================================================================
372
+
373
+ export function buildResponseFormatSection(): string {
374
+ const jsonVerbalOnly = [
375
+ '{',
376
+ ' "should_respond": true,',
377
+ ' "verbal_response": "What you would say out loud"',
378
+ '}'
379
+ ].join('\n');
380
+
381
+ const jsonActionOnly = [
382
+ '{',
383
+ ' "should_respond": true,',
384
+ ' "action_response": "What you would do (rendered in italics, like stage directions)"',
385
+ '}'
386
+ ].join('\n');
387
+
388
+ const jsonBoth = [
389
+ '{',
390
+ ' "should_respond": true,',
391
+ ' "verbal_response": "What you would say out loud",',
392
+ ' "action_response": "What you would do (rendered in italics, like stage directions)"',
393
+ '}'
394
+ ].join('\n');
395
+
396
+ const jsonSilent = [
397
+ '{',
398
+ ' "should_respond": false,',
399
+ ' "reason": "Brief explanation of why silence is the right choice here"',
400
+ '}'
401
+ ].join('\n');
402
+
403
+ return `## Response Format
404
+
405
+ Always respond with JSON. You have four valid forms:
406
+
407
+ **Words only** (most common):
408
+ \`\`\`json
409
+ ${jsonVerbalOnly}
410
+ \`\`\`
411
+
412
+ **Action only** (a gesture, expression, or physical reaction with no words):
413
+ \`\`\`json
414
+ ${jsonActionOnly}
415
+ \`\`\`
416
+
417
+ **Words and action** (speaking while doing something):
418
+ \`\`\`json
419
+ ${jsonBoth}
420
+ \`\`\`
421
+
422
+ **Silent** (choosing not to respond):
423
+ \`\`\`json
424
+ ${jsonSilent}
425
+ \`\`\`
426
+
427
+ Rules:
428
+ - Use whichever combination fits the moment — both fields are optional, but at least one must be present when \`should_respond\` is true
429
+ - \`action_response\` alone is valid — a smile, a shrug, or a thoughtful pause can speak volumes
430
+ - \`reason\` is only used when \`should_respond\` is false
431
+ - Do NOT include \`<thinking>\` blocks or analysis outside the JSON
432
+ - The JSON must be valid - use double quotes, no trailing commas`;
433
+ }
@@ -29,6 +29,16 @@ export interface ResponsePromptData {
29
29
  isTUI: boolean;
30
30
  }
31
31
 
32
+ /**
33
+ * Structured response from LLM (new JSON schema)
34
+ */
35
+ export interface PersonaResponseResult {
36
+ should_respond: boolean;
37
+ verbal_response?: string;
38
+ action_response?: string;
39
+ reason?: string;
40
+ }
41
+
32
42
  /**
33
43
  * Prompt output structure (all prompts return this)
34
44
  */
package/tui/README.md CHANGED
@@ -6,7 +6,11 @@ OpenCode integration: import via `/settings` (`opencode.integration: true`) · e
6
6
 
7
7
  # Installation
8
8
 
9
- ```
9
+ ```bash
10
+ # Install Bun (if you don't have it)
11
+ curl -fsSL https://bun.sh/install | bash
12
+
13
+ # Install Ei
10
14
  npm install -g ei-tui
11
15
  ```
12
16
 
@@ -78,6 +82,13 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
78
82
  |---------|---------|-------------|
79
83
  | `/editor` | `/e`, `/edit` | Open current input text in `$EDITOR`, update on save |
80
84
 
85
+ ### Queue & Debugging
86
+
87
+ | Command | Aliases | Description |
88
+ |---------|---------|-------------|
89
+ | `/queue` | | Pause queue and inspect/edit active items in `$EDITOR` |
90
+ | `/dlq` | | Inspect and recover failed (dead-letter) queue items in `$EDITOR` |
91
+
81
92
  ### Keybindings
82
93
 
83
94
  | Key | Action |
@@ -0,0 +1,75 @@
1
+ import type { Command } from "./registry.js";
2
+ import { spawnEditor } from "../util/editor.js";
3
+ import { queueItemsToYAML, queueItemsFromYAML } from "../util/yaml-serializers.js";
4
+ import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
5
+
6
+ export const dlqCommand: Command = {
7
+ name: "dlq",
8
+ aliases: [],
9
+ description: "Open dead-letter queue $EDITOR",
10
+ usage: "/dlq - Inspect and recover failed queue items",
11
+
12
+ async execute(_args, ctx) {
13
+ const items = ctx.ei.getDLQItems();
14
+
15
+ if (items.length === 0) {
16
+ ctx.showNotification("DLQ is empty", "info");
17
+ return;
18
+ }
19
+
20
+ let yamlContent = queueItemsToYAML(items);
21
+
22
+ while (true) {
23
+ const result = await spawnEditor({
24
+ initialContent: yamlContent,
25
+ filename: "ei-dlq.yaml",
26
+ renderer: ctx.renderer,
27
+ });
28
+
29
+ if (result.aborted || !result.success) {
30
+ ctx.showNotification("DLQ edit cancelled", "info");
31
+ return;
32
+ }
33
+
34
+ if (result.content === null) {
35
+ ctx.showNotification("No changes made", "info");
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const updates = queueItemsFromYAML(result.content);
41
+ let recovered = 0;
42
+ for (const update of updates) {
43
+ await ctx.ei.updateQueueItem(update.id, update);
44
+ if (update.state === "pending") recovered++;
45
+ }
46
+ const msg = recovered > 0
47
+ ? `DLQ updated — ${recovered} item(s) requeued`
48
+ : `DLQ updated (no items requeued)`;
49
+ ctx.showNotification(msg, "info");
50
+ return;
51
+ } catch (parseError) {
52
+ const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
53
+
54
+ const shouldReEdit = await new Promise<boolean>((resolve) => {
55
+ ctx.showOverlay((hideOverlay) =>
56
+ ConfirmOverlay({
57
+ message: `YAML error:\n${errorMsg}\n\nRe-edit?`,
58
+ onConfirm: () => { hideOverlay(); resolve(true); },
59
+ onCancel: () => { hideOverlay(); resolve(false); },
60
+ })
61
+ );
62
+ });
63
+
64
+ if (shouldReEdit) {
65
+ yamlContent = result.content;
66
+ await new Promise(r => setTimeout(r, 50));
67
+ continue;
68
+ }
69
+
70
+ ctx.showNotification("Changes discarded", "warn");
71
+ return;
72
+ }
73
+ }
74
+ },
75
+ };
@@ -0,0 +1,77 @@
1
+ import type { Command } from "./registry.js";
2
+ import { spawnEditor } from "../util/editor.js";
3
+ import { queueItemsToYAML, queueItemsFromYAML } from "../util/yaml-serializers.js";
4
+ import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
5
+
6
+ export const queueCommand: Command = {
7
+ name: "queue",
8
+ aliases: [],
9
+ description: "Pause & open queue items in $EDITOR",
10
+ usage: "/queue - Inspect and edit active queue items",
11
+
12
+ async execute(_args, ctx) {
13
+ const items = ctx.ei.getQueueActiveItems();
14
+
15
+ if (items.length === 0) {
16
+ ctx.showNotification("Queue is empty", "info");
17
+ return;
18
+ }
19
+
20
+ ctx.ei.pauseQueue();
21
+ ctx.showNotification(`Queue paused (${items.length} items)`, "info");
22
+
23
+ let yamlContent = queueItemsToYAML(items);
24
+
25
+ while (true) {
26
+ const result = await spawnEditor({
27
+ initialContent: yamlContent,
28
+ filename: "ei-queue.yaml",
29
+ renderer: ctx.renderer,
30
+ });
31
+
32
+ if (result.aborted || !result.success) {
33
+ ctx.ei.resumeQueue();
34
+ ctx.showNotification("Queue resumed (no changes)", "info");
35
+ return;
36
+ }
37
+
38
+ if (result.content === null) {
39
+ ctx.ei.resumeQueue();
40
+ ctx.showNotification("No changes — queue resumed", "info");
41
+ return;
42
+ }
43
+
44
+ try {
45
+ const updates = queueItemsFromYAML(result.content);
46
+ for (const update of updates) {
47
+ await ctx.ei.updateQueueItem(update.id, update);
48
+ }
49
+ ctx.ei.resumeQueue();
50
+ ctx.showNotification(`Queue updated (${updates.length} items) — resumed`, "info");
51
+ return;
52
+ } catch (parseError) {
53
+ const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
54
+
55
+ const shouldReEdit = await new Promise<boolean>((resolve) => {
56
+ ctx.showOverlay((hideOverlay) =>
57
+ ConfirmOverlay({
58
+ message: `YAML error:\n${errorMsg}\n\nRe-edit?`,
59
+ onConfirm: () => { hideOverlay(); resolve(true); },
60
+ onCancel: () => { hideOverlay(); resolve(false); },
61
+ })
62
+ );
63
+ });
64
+
65
+ if (shouldReEdit) {
66
+ yamlContent = result.content;
67
+ await new Promise(r => setTimeout(r, 50));
68
+ continue;
69
+ }
70
+
71
+ ctx.ei.resumeQueue();
72
+ ctx.showNotification("Changes discarded — queue resumed", "warn");
73
+ return;
74
+ }
75
+ }
76
+ },
77
+ };
@@ -18,6 +18,16 @@ function formatTime(timestamp: string): string {
18
18
  return `${hours}:${minutes}`;
19
19
  }
20
20
 
21
+ function buildMessageText(message: Message): string {
22
+ if (message.silence_reason !== undefined) {
23
+ return `[chose not to respond: ${message.silence_reason}]`;
24
+ }
25
+ const parts: string[] = [];
26
+ if (message.action_response) parts.push(`_${message.action_response}_`);
27
+ if (message.verbal_response) parts.push(message.verbal_response);
28
+ return parts.join('\n\n');
29
+ }
30
+
21
31
  function insertQuoteMarkers(content: string, quotes: Quote[]): string {
22
32
  const validQuotes = quotes
23
33
  .filter(q => q.end !== null && q.end !== undefined)
@@ -30,7 +40,7 @@ function insertQuoteMarkers(content: string, quotes: Quote[]): string {
30
40
  while (insertPos > 0 && (result[insertPos - 1] === '\n' || result[insertPos - 1] === ' ')) {
31
41
  insertPos--;
32
42
  }
33
- result = result.slice(0, insertPos) + "" + result.slice(insertPos);
43
+ result = result.slice(0, insertPos) + "\u207a" + result.slice(insertPos);
34
44
  }
35
45
  }
36
46
  return result;
@@ -118,7 +128,7 @@ export function MessageList() {
118
128
 
119
129
  const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`;
120
130
 
121
- const displayContent = insertQuoteMarkers(message.content, message._quotes);
131
+ const displayContent = insertQuoteMarkers(buildMessageText(message), message._quotes);
122
132
 
123
133
  const showDivider = () => {
124
134
  const boundary = activeContextBoundary();
@@ -21,6 +21,8 @@ import { deleteCommand } from "../commands/delete";
21
21
  import { quotesCommand } from "../commands/quotes";
22
22
  import { providerCommand } from "../commands/provider";
23
23
  import { setSyncCommand } from "../commands/setsync";
24
+ import { queueCommand } from "../commands/queue";
25
+ import { dlqCommand } from "../commands/dlq";
24
26
  import { useOverlay } from "../context/overlay";
25
27
  import { CommandSuggest } from "./CommandSuggest";
26
28
  import { useKeyboard } from "@opentui/solid";
@@ -56,6 +58,8 @@ export function PromptInput() {
56
58
  registerCommand(setSyncCommand);
57
59
  registerCommand(contextCommand);
58
60
  registerCommand(deleteCommand);
61
+ registerCommand(queueCommand);
62
+ registerCommand(dlqCommand);
59
63
 
60
64
  let textareaRef: TextareaRenderable | undefined;
61
65
 
@@ -153,7 +157,9 @@ export function PromptInput() {
153
157
  text.startsWith("/quotes") ||
154
158
  text.startsWith("/q ") ||
155
159
  text.startsWith("/context") ||
156
- text.startsWith("/messages");
160
+ text.startsWith("/messages") ||
161
+ text === "/queue" ||
162
+ text === "/dlq";
157
163
 
158
164
  if (!isEditorCmd && !opensEditorForData) {
159
165
  textareaRef?.clear();
@@ -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;
@@ -110,7 +115,7 @@ export const EiProvider: ParentComponent = (props) => {
110
115
  activePersonaId: null,
111
116
  activeContextBoundary: undefined,
112
117
  messages: [],
113
- queueStatus: { state: "idle", pending_count: 0 },
118
+ queueStatus: { state: "idle", pending_count: 0, dlq_count: 0 },
114
119
  notification: null,
115
120
  });
116
121
 
@@ -210,6 +215,27 @@ export const EiProvider: ParentComponent = (props) => {
210
215
  await processor.resumeQueue();
211
216
  };
212
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
+
213
239
  const stopProcessor = async () => {
214
240
  if (processor) {
215
241
  await processor.stop();
@@ -506,11 +532,11 @@ export const EiProvider: ParentComponent = (props) => {
506
532
  logger.debug(`onQueueStateChanged called with state: ${state}`);
507
533
  if (processor) {
508
534
  processor.getQueueStatus().then((status) => {
509
- 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 });
510
536
  logger.debug(`store.queueStatus after setStore:`, store.queueStatus);
511
537
  });
512
538
  } else {
513
- setStore("queueStatus", { state, pending_count: 0 });
539
+ setStore("queueStatus", { state, pending_count: 0, dlq_count: 0 });
514
540
  }
515
541
  },
516
542
  onContextBoundaryChanged: (personaId) => {
@@ -568,6 +594,10 @@ export const EiProvider: ParentComponent = (props) => {
568
594
  refreshMessages,
569
595
  abortCurrentOperation,
570
596
  resumeQueue,
597
+ pauseQueue,
598
+ getQueueActiveItems,
599
+ getDLQItems,
600
+ updateQueueItem,
571
601
  stopProcessor,
572
602
  saveAndExit,
573
603
  showNotification,
@@ -152,7 +152,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
152
152
  }
153
153
  }
154
154
  // Navigate backward through sent-message history
155
- const history = messages().filter(m => m.role === "human").map(m => m.content);
155
+ const history = messages().filter(m => m.role === "human").map(m => (m.verbal_response ?? ''));
156
156
  if (history.length === 0) return;
157
157
  if (historyIndex === -1) {
158
158
  savedDraft = textareaRef.plainText;
@@ -180,7 +180,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
180
180
  textareaRef.gotoBufferEnd();
181
181
  } else {
182
182
  historyIndex -= 1;
183
- const history = messages().filter(m => m.role === "human").map(m => m.content);
183
+ const history = messages().filter(m => m.role === "human").map(m => (m.verbal_response ?? ''));
184
184
  const entry = history[history.length - 1 - historyIndex];
185
185
  textareaRef.setText(entry);
186
186
  textareaRef.gotoBufferEnd(); // cursor at end so next Down continues forward
@@ -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
 
@@ -719,13 +722,17 @@ interface EditableMessage {
719
722
  timestamp: string;
720
723
  context_status: ContextStatus;
721
724
  _delete?: boolean;
722
- content: string;
725
+ // verbal_response | action_response | silence_reason
726
+ verbal_response?: string;
727
+ action_response?: string;
728
+ silence_reason?: string;
723
729
  }
724
730
 
725
731
  export function contextToYAML(messages: Message[]): string {
726
732
  const header = [
727
733
  "# context_status: default | always | never",
728
734
  "# _delete: true — permanently removes the message",
735
+ "# verbal_response | action_response | silence_reason",
729
736
  ].join("\n");
730
737
 
731
738
  const data: EditableMessage[] = messages.map((m) => ({
@@ -734,7 +741,9 @@ export function contextToYAML(messages: Message[]): string {
734
741
  timestamp: m.timestamp,
735
742
  context_status: m.context_status,
736
743
  _delete: false,
737
- content: m.content,
744
+ verbal_response: m.verbal_response,
745
+ action_response: m.action_response,
746
+ silence_reason: m.silence_reason,
738
747
  }));
739
748
 
740
749
  return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
@@ -761,3 +770,56 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
761
770
 
762
771
  return { messages, deletedMessageIds };
763
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
+ });
825
+ }