ei-tui 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/package.json +3 -3
- package/src/cli/README.md +2 -0
- package/src/core/handlers/index.ts +106 -16
- package/src/core/orchestrators/ceremony.ts +3 -3
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +74 -33
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +79 -13
- package/src/core/state-manager.ts +35 -3
- package/src/core/types.ts +19 -6
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +5 -4
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/message-utils.ts +38 -1
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +90 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +12 -1
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +7 -1
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +33 -3
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/yaml-serializers.ts +70 -8
|
@@ -29,17 +29,29 @@ ${description}`;
|
|
|
29
29
|
// =============================================================================
|
|
30
30
|
|
|
31
31
|
export function buildGuidelinesSection(personaName: string): string {
|
|
32
|
-
const
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
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) + "
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|