palmier 0.5.6 → 0.5.8
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/dist/commands/run.js +10 -2
- package/dist/rpc-handler.js +5 -3
- package/dist/spawn-command.js +0 -1
- package/package.json +1 -1
- package/src/commands/run.ts +12 -2
- package/src/rpc-handler.ts +5 -3
- package/src/spawn-command.ts +0 -1
- package/test/taskrun-messages.test.ts +224 -0
package/dist/commands/run.js
CHANGED
|
@@ -85,7 +85,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
85
85
|
await appendAndNotify(ctx, {
|
|
86
86
|
role: "user",
|
|
87
87
|
time: Date.now(),
|
|
88
|
-
content: "
|
|
88
|
+
content: "Deny & Abort Task",
|
|
89
89
|
type: "permission",
|
|
90
90
|
});
|
|
91
91
|
return { outcome: "failed" };
|
|
@@ -95,7 +95,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
95
95
|
await appendAndNotify(ctx, {
|
|
96
96
|
role: "user",
|
|
97
97
|
time: Date.now(),
|
|
98
|
-
content: response === "granted_all" ? "
|
|
98
|
+
content: response === "granted_all" ? "Allow Always" : "Allow Once",
|
|
99
99
|
type: "permission",
|
|
100
100
|
});
|
|
101
101
|
if (response === "granted_all") {
|
|
@@ -181,14 +181,19 @@ export async function runCommand(taskId) {
|
|
|
181
181
|
// If requires_confirmation, notify clients and wait
|
|
182
182
|
if (task.frontmatter.requires_confirmation) {
|
|
183
183
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
184
|
+
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
185
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: Date.now(), content: confirmPrompt, type: "confirmation" });
|
|
186
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
184
187
|
if (!confirmed) {
|
|
185
188
|
console.log("Task aborted by user.");
|
|
189
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Aborted", type: "confirmation" });
|
|
186
190
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
187
191
|
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
188
192
|
await cleanup();
|
|
189
193
|
return;
|
|
190
194
|
}
|
|
191
195
|
console.log("Task confirmed by user.");
|
|
196
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Confirmed", type: "confirmation" });
|
|
192
197
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
193
198
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
194
199
|
}
|
|
@@ -302,6 +307,9 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
302
307
|
invocationsFailed++;
|
|
303
308
|
}
|
|
304
309
|
appendLog(line, "", result.outcome);
|
|
310
|
+
// Append monitoring status so the UI shows the task is waiting for more input
|
|
311
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
312
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
305
313
|
}
|
|
306
314
|
async function drainQueue() {
|
|
307
315
|
if (processing)
|
package/dist/rpc-handler.js
CHANGED
|
@@ -37,10 +37,12 @@ function parseResultFrontmatter(raw) {
|
|
|
37
37
|
const startedMsg = statusMessages.find((m) => m.type === "started");
|
|
38
38
|
const terminalStates = ["finished", "failed", "aborted"];
|
|
39
39
|
const terminalMsg = [...statusMessages].reverse().find((m) => terminalStates.includes(m.type ?? ""));
|
|
40
|
-
// If last status is "started"
|
|
40
|
+
// If last status is "started" (or continuation like "confirmation"/"monitoring"),
|
|
41
|
+
// determine if it's a task run or follow-up
|
|
42
|
+
const activeStates = ["started", "monitoring", "confirmation"];
|
|
41
43
|
let runningState;
|
|
42
|
-
if (lastStatus?.type
|
|
43
|
-
runningState = terminalMsg ? "followup" :
|
|
44
|
+
if (activeStates.includes(lastStatus?.type ?? "")) {
|
|
45
|
+
runningState = terminalMsg ? "followup" : "started";
|
|
44
46
|
}
|
|
45
47
|
else {
|
|
46
48
|
runningState = lastStatus?.type;
|
package/dist/spawn-command.js
CHANGED
package/package.json
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -123,7 +123,7 @@ async function invokeAgentWithRetries(
|
|
|
123
123
|
await appendAndNotify(ctx, {
|
|
124
124
|
role: "user",
|
|
125
125
|
time: Date.now(),
|
|
126
|
-
content: "
|
|
126
|
+
content: "Deny & Abort Task",
|
|
127
127
|
type: "permission",
|
|
128
128
|
});
|
|
129
129
|
return { outcome: "failed" };
|
|
@@ -137,7 +137,7 @@ async function invokeAgentWithRetries(
|
|
|
137
137
|
await appendAndNotify(ctx, {
|
|
138
138
|
role: "user",
|
|
139
139
|
time: Date.now(),
|
|
140
|
-
content: response === "granted_all" ? "
|
|
140
|
+
content: response === "granted_all" ? "Allow Always" : "Allow Once",
|
|
141
141
|
type: "permission",
|
|
142
142
|
});
|
|
143
143
|
|
|
@@ -237,14 +237,20 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
237
237
|
// If requires_confirmation, notify clients and wait
|
|
238
238
|
if (task.frontmatter.requires_confirmation) {
|
|
239
239
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
240
|
+
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
241
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: Date.now(), content: confirmPrompt, type: "confirmation" });
|
|
242
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
243
|
+
|
|
240
244
|
if (!confirmed) {
|
|
241
245
|
console.log("Task aborted by user.");
|
|
246
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Aborted", type: "confirmation" });
|
|
242
247
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
243
248
|
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
244
249
|
await cleanup();
|
|
245
250
|
return;
|
|
246
251
|
}
|
|
247
252
|
console.log("Task confirmed by user.");
|
|
253
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Confirmed", type: "confirmation" });
|
|
248
254
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
249
255
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
250
256
|
}
|
|
@@ -369,6 +375,10 @@ async function runCommandTriggeredMode(
|
|
|
369
375
|
invocationsFailed++;
|
|
370
376
|
}
|
|
371
377
|
appendLog(line, "", result.outcome);
|
|
378
|
+
|
|
379
|
+
// Append monitoring status so the UI shows the task is waiting for more input
|
|
380
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
381
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
372
382
|
}
|
|
373
383
|
|
|
374
384
|
async function drainQueue(): Promise<void> {
|
package/src/rpc-handler.ts
CHANGED
|
@@ -47,10 +47,12 @@ function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
47
47
|
const terminalStates = ["finished", "failed", "aborted"];
|
|
48
48
|
const terminalMsg = [...statusMessages].reverse().find((m: ConversationMessage) => terminalStates.includes(m.type ?? ""));
|
|
49
49
|
|
|
50
|
-
// If last status is "started"
|
|
50
|
+
// If last status is "started" (or continuation like "confirmation"/"monitoring"),
|
|
51
|
+
// determine if it's a task run or follow-up
|
|
52
|
+
const activeStates = ["started", "monitoring", "confirmation"];
|
|
51
53
|
let runningState: string | undefined;
|
|
52
|
-
if (lastStatus?.type
|
|
53
|
-
runningState = terminalMsg ? "followup" :
|
|
54
|
+
if (activeStates.includes(lastStatus?.type ?? "")) {
|
|
55
|
+
runningState = terminalMsg ? "followup" : "started";
|
|
54
56
|
} else {
|
|
55
57
|
runningState = lastStatus?.type;
|
|
56
58
|
}
|
package/src/spawn-command.ts
CHANGED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import {
|
|
7
|
+
createRunDir,
|
|
8
|
+
appendRunMessage,
|
|
9
|
+
readRunMessages,
|
|
10
|
+
beginStreamingMessage,
|
|
11
|
+
spliceUserMessage,
|
|
12
|
+
} from "../src/task.js";
|
|
13
|
+
|
|
14
|
+
let taskDir: string;
|
|
15
|
+
let runId: string;
|
|
16
|
+
|
|
17
|
+
function setup() {
|
|
18
|
+
taskDir = fs.mkdtempSync(path.join(os.tmpdir(), "palmier-test-"));
|
|
19
|
+
runId = createRunDir(taskDir, "Test Task", 1000, "claude");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("appendRunMessage + readRunMessages", () => {
|
|
23
|
+
beforeEach(setup);
|
|
24
|
+
|
|
25
|
+
it("writes and reads a user message", () => {
|
|
26
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Hello" });
|
|
27
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
28
|
+
assert.equal(msgs.length, 1);
|
|
29
|
+
assert.equal(msgs[0].role, "user");
|
|
30
|
+
assert.equal(msgs[0].content, "Hello");
|
|
31
|
+
assert.equal(msgs[0].time, 1001);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("writes and reads an assistant message", () => {
|
|
35
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: 1002, content: "Hi there" });
|
|
36
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
37
|
+
assert.equal(msgs.length, 1);
|
|
38
|
+
assert.equal(msgs[0].role, "assistant");
|
|
39
|
+
assert.equal(msgs[0].content, "Hi there");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("writes and reads a status message", () => {
|
|
43
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "started" });
|
|
44
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
45
|
+
assert.equal(msgs.length, 1);
|
|
46
|
+
assert.equal(msgs[0].role, "status");
|
|
47
|
+
assert.equal(msgs[0].type, "started");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("preserves message type", () => {
|
|
51
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1004, content: "Confirmed", type: "confirmation" });
|
|
52
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
53
|
+
assert.equal(msgs[0].type, "confirmation");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("preserves attachments", () => {
|
|
57
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: 1005, content: "Done", attachments: ["report.md", "chart.png"] });
|
|
58
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
59
|
+
assert.deepEqual(msgs[0].attachments, ["report.md", "chart.png"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reads multiple messages in order", () => {
|
|
63
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
64
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Do something" });
|
|
65
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: 1002, content: "Done" });
|
|
66
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "finished" });
|
|
67
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
68
|
+
assert.equal(msgs.length, 4);
|
|
69
|
+
assert.equal(msgs[0].type, "started");
|
|
70
|
+
assert.equal(msgs[1].role, "user");
|
|
71
|
+
assert.equal(msgs[2].role, "assistant");
|
|
72
|
+
assert.equal(msgs[3].type, "finished");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("confirmation flow", () => {
|
|
77
|
+
beforeEach(setup);
|
|
78
|
+
|
|
79
|
+
it("records confirmation with assistant prompt, user response, and status", () => {
|
|
80
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
81
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: 1001, content: '**Task Confirmation**\n\nRun task "My Task"?', type: "confirmation" });
|
|
82
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1002, content: "Confirmed", type: "confirmation" });
|
|
83
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "confirmation" });
|
|
84
|
+
|
|
85
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
86
|
+
assert.equal(msgs.length, 4);
|
|
87
|
+
assert.equal(msgs[1].role, "assistant");
|
|
88
|
+
assert.ok(msgs[1].content.includes("Task Confirmation"));
|
|
89
|
+
assert.equal(msgs[2].role, "user");
|
|
90
|
+
assert.equal(msgs[2].content, "Confirmed");
|
|
91
|
+
assert.equal(msgs[3].role, "status");
|
|
92
|
+
assert.equal(msgs[3].type, "confirmation");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("records aborted confirmation", () => {
|
|
96
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
97
|
+
appendRunMessage(taskDir, runId, { role: "assistant", time: 1001, content: '**Task Confirmation**\n\nRun task "My Task"?', type: "confirmation" });
|
|
98
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1002, content: "Aborted", type: "confirmation" });
|
|
99
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "aborted" });
|
|
100
|
+
|
|
101
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
102
|
+
assert.equal(msgs.length, 4);
|
|
103
|
+
assert.equal(msgs[2].content, "Aborted");
|
|
104
|
+
assert.equal(msgs[3].type, "aborted");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("beginStreamingMessage", () => {
|
|
109
|
+
beforeEach(setup);
|
|
110
|
+
|
|
111
|
+
it("streams chunks and finalizes", () => {
|
|
112
|
+
const writer = beginStreamingMessage(taskDir, runId, 2000);
|
|
113
|
+
writer.write("Hello ");
|
|
114
|
+
writer.write("world");
|
|
115
|
+
writer.end();
|
|
116
|
+
|
|
117
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
118
|
+
assert.equal(msgs.length, 1);
|
|
119
|
+
assert.equal(msgs[0].role, "assistant");
|
|
120
|
+
assert.equal(msgs[0].content, "Hello world");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("attaches report files to the last assistant message", () => {
|
|
124
|
+
const writer = beginStreamingMessage(taskDir, runId, 2000);
|
|
125
|
+
writer.write("Generated report.");
|
|
126
|
+
writer.end(["report.md", "chart.png"]);
|
|
127
|
+
|
|
128
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
129
|
+
assert.equal(msgs.length, 1);
|
|
130
|
+
assert.deepEqual(msgs[0].attachments, ["report.md", "chart.png"]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("spliceUserMessage", () => {
|
|
135
|
+
beforeEach(setup);
|
|
136
|
+
|
|
137
|
+
it("splits assistant stream for user input", () => {
|
|
138
|
+
const writer = beginStreamingMessage(taskDir, runId, 2000);
|
|
139
|
+
writer.write("Working on it...");
|
|
140
|
+
|
|
141
|
+
spliceUserMessage(taskDir, runId, { role: "user", time: 2001, content: "my-api-key", type: "input" });
|
|
142
|
+
|
|
143
|
+
writer.write("Continuing with key.");
|
|
144
|
+
writer.end();
|
|
145
|
+
|
|
146
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
147
|
+
assert.equal(msgs.length, 3);
|
|
148
|
+
assert.equal(msgs[0].role, "assistant");
|
|
149
|
+
assert.equal(msgs[0].content, "Working on it...");
|
|
150
|
+
assert.equal(msgs[1].role, "user");
|
|
151
|
+
assert.equal(msgs[1].content, "my-api-key");
|
|
152
|
+
assert.equal(msgs[1].type, "input");
|
|
153
|
+
assert.equal(msgs[2].role, "assistant");
|
|
154
|
+
assert.equal(msgs[2].content, "Continuing with key.");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("appends assistant text before splicing", () => {
|
|
158
|
+
const writer = beginStreamingMessage(taskDir, runId, 2000);
|
|
159
|
+
writer.write("Processing");
|
|
160
|
+
|
|
161
|
+
spliceUserMessage(
|
|
162
|
+
taskDir, runId,
|
|
163
|
+
{ role: "user", time: 2001, content: "answer1", type: "input" },
|
|
164
|
+
"\n\n**What is your key?**",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
writer.write("Done.");
|
|
168
|
+
writer.end();
|
|
169
|
+
|
|
170
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
171
|
+
assert.equal(msgs.length, 3);
|
|
172
|
+
assert.ok(msgs[0].content.includes("What is your key?"));
|
|
173
|
+
assert.equal(msgs[1].content, "answer1");
|
|
174
|
+
assert.equal(msgs[2].content, "Done.");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("attaches reports to last assistant message after splice", () => {
|
|
178
|
+
const writer = beginStreamingMessage(taskDir, runId, 2000);
|
|
179
|
+
writer.write("Part 1");
|
|
180
|
+
|
|
181
|
+
spliceUserMessage(taskDir, runId, { role: "user", time: 2001, content: "input", type: "input" });
|
|
182
|
+
|
|
183
|
+
writer.write("Part 2");
|
|
184
|
+
writer.end(["report.md"]);
|
|
185
|
+
|
|
186
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
187
|
+
// Attachments should be on the last assistant message (after splice), not the first
|
|
188
|
+
assert.equal(msgs[0].attachments, undefined);
|
|
189
|
+
assert.deepEqual(msgs[2].attachments, ["report.md"]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("permission flow", () => {
|
|
194
|
+
beforeEach(setup);
|
|
195
|
+
|
|
196
|
+
it("records permission grant as user message", () => {
|
|
197
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
198
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Do something" });
|
|
199
|
+
// Simulate agent output with permission request (via streaming)
|
|
200
|
+
const writer = beginStreamingMessage(taskDir, runId, 1002);
|
|
201
|
+
writer.write("I need permission.\n\n**Permissions requested:**\n- **Read** Read files\n");
|
|
202
|
+
writer.end();
|
|
203
|
+
// Permission granted
|
|
204
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1003, content: "Granted", type: "permission" });
|
|
205
|
+
|
|
206
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
207
|
+
assert.equal(msgs.length, 4);
|
|
208
|
+
assert.equal(msgs[3].role, "user");
|
|
209
|
+
assert.equal(msgs[3].content, "Granted");
|
|
210
|
+
assert.equal(msgs[3].type, "permission");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("records permission denial", () => {
|
|
214
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Do something" });
|
|
215
|
+
const writer = beginStreamingMessage(taskDir, runId, 1002);
|
|
216
|
+
writer.write("Need permission.");
|
|
217
|
+
writer.end();
|
|
218
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1003, content: "Denied", type: "permission" });
|
|
219
|
+
|
|
220
|
+
const msgs = readRunMessages(taskDir, runId);
|
|
221
|
+
assert.equal(msgs[2].content, "Denied");
|
|
222
|
+
assert.equal(msgs[2].type, "permission");
|
|
223
|
+
});
|
|
224
|
+
});
|