palmier 0.4.3 → 0.4.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.
- package/README.md +5 -3
- package/dist/commands/request-input.d.ts +1 -2
- package/dist/commands/request-input.js +7 -21
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +43 -53
- package/dist/commands/serve.js +31 -33
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +37 -12
- package/dist/rpc-handler.js +177 -30
- package/dist/task.d.ts +13 -13
- package/dist/task.js +59 -51
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/commands/request-input.ts +7 -21
- package/src/commands/run.ts +43 -56
- package/src/commands/serve.ts +34 -41
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +36 -13
- package/src/rpc-handler.ts +195 -34
- package/src/task.ts +60 -55
- package/src/types.ts +2 -2
package/dist/rpc-handler.js
CHANGED
|
@@ -4,13 +4,15 @@ import * as path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory,
|
|
7
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
8
8
|
import { getPlatform } from "./platform/index.js";
|
|
9
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
|
+
import crossSpawn from "cross-spawn";
|
|
10
11
|
import { getAgent } from "./agents/agent.js";
|
|
11
12
|
import { validateSession } from "./session-store.js";
|
|
12
13
|
import { publishHostEvent } from "./events.js";
|
|
13
14
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
15
|
+
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
14
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
16
18
|
/**
|
|
@@ -28,13 +30,26 @@ function parseResultFrontmatter(raw) {
|
|
|
28
30
|
meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
|
|
29
31
|
}
|
|
30
32
|
const messages = parseConversationMessages(fmMatch[2]);
|
|
33
|
+
// Derive state from status messages — just look at the last one
|
|
34
|
+
const statusMessages = messages.filter((m) => m.role === "status");
|
|
35
|
+
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
36
|
+
const startedMsg = statusMessages.find((m) => m.type === "started");
|
|
37
|
+
const terminalStates = ["finished", "failed", "aborted"];
|
|
38
|
+
const terminalMsg = [...statusMessages].reverse().find((m) => terminalStates.includes(m.type ?? ""));
|
|
39
|
+
// If last status is "started", determine if it's a task run or follow-up
|
|
40
|
+
let runningState;
|
|
41
|
+
if (lastStatus?.type === "started") {
|
|
42
|
+
runningState = terminalMsg ? "followup" : "started";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
runningState = lastStatus?.type;
|
|
46
|
+
}
|
|
31
47
|
return {
|
|
32
48
|
messages,
|
|
33
49
|
task_name: meta.task_name,
|
|
34
|
-
running_state:
|
|
35
|
-
start_time:
|
|
36
|
-
end_time:
|
|
37
|
-
task_file: meta.task_file,
|
|
50
|
+
running_state: runningState,
|
|
51
|
+
start_time: startedMsg?.time || undefined,
|
|
52
|
+
end_time: terminalMsg?.time || undefined,
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
55
|
/**
|
|
@@ -101,6 +116,8 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
|
101
116
|
}
|
|
102
117
|
return { name, body };
|
|
103
118
|
}
|
|
119
|
+
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
120
|
+
const activeFollowups = new Map();
|
|
104
121
|
/**
|
|
105
122
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
106
123
|
*/
|
|
@@ -242,8 +259,8 @@ export function createRpcHandler(config, nc) {
|
|
|
242
259
|
writeTaskFile(taskDir, task);
|
|
243
260
|
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
244
261
|
// Create initial result file so it appears in runs list immediately
|
|
245
|
-
const
|
|
246
|
-
appendHistory(config.projectRoot, { task_id: id,
|
|
262
|
+
const runId = createRunDir(taskDir, name, Date.now());
|
|
263
|
+
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
247
264
|
// Spawn `palmier run <id>` directly as a detached process
|
|
248
265
|
const script = process.argv[1] || "palmier";
|
|
249
266
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
@@ -252,7 +269,7 @@ export function createRpcHandler(config, nc) {
|
|
|
252
269
|
windowsHide: true,
|
|
253
270
|
});
|
|
254
271
|
child.unref();
|
|
255
|
-
return { ok: true, task_id: id,
|
|
272
|
+
return { ok: true, task_id: id, run_id: runId };
|
|
256
273
|
}
|
|
257
274
|
case "task.run": {
|
|
258
275
|
const params = request.params;
|
|
@@ -260,10 +277,10 @@ export function createRpcHandler(config, nc) {
|
|
|
260
277
|
// Create initial result file so it appears in runs list immediately
|
|
261
278
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
262
279
|
const runTask = parseTaskFile(runTaskDir);
|
|
263
|
-
const
|
|
264
|
-
appendHistory(config.projectRoot, { task_id: params.id,
|
|
280
|
+
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
281
|
+
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
265
282
|
await getPlatform().startTask(params.id);
|
|
266
|
-
return { ok: true, task_id: params.id,
|
|
283
|
+
return { ok: true, task_id: params.id, run_id: taskRunId };
|
|
267
284
|
}
|
|
268
285
|
catch (err) {
|
|
269
286
|
const e = err;
|
|
@@ -271,15 +288,145 @@ export function createRpcHandler(config, nc) {
|
|
|
271
288
|
return { error: `Failed to start task: ${e.stderr || e.message}` };
|
|
272
289
|
}
|
|
273
290
|
}
|
|
291
|
+
case "task.followup": {
|
|
292
|
+
const params = request.params;
|
|
293
|
+
if (!params.run_id || !params.message?.trim()) {
|
|
294
|
+
return { error: "run_id and message are required" };
|
|
295
|
+
}
|
|
296
|
+
const followupKey = `${params.id}:${params.run_id}`;
|
|
297
|
+
if (activeFollowups.has(followupKey)) {
|
|
298
|
+
return { error: "A follow-up is already running for this run" };
|
|
299
|
+
}
|
|
300
|
+
const followupTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
301
|
+
const followupTask = parseTaskFile(followupTaskDir);
|
|
302
|
+
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
303
|
+
// Append user message + started status
|
|
304
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
305
|
+
role: "user",
|
|
306
|
+
time: Date.now(),
|
|
307
|
+
content: params.message,
|
|
308
|
+
});
|
|
309
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
310
|
+
role: "status",
|
|
311
|
+
time: Date.now(),
|
|
312
|
+
content: "",
|
|
313
|
+
type: "started",
|
|
314
|
+
});
|
|
315
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
316
|
+
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
317
|
+
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
318
|
+
const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(followupTask, params.message, followupTask.frontmatter.permissions);
|
|
319
|
+
// Spawn directly via crossSpawn so we can track and kill the child
|
|
320
|
+
const child = crossSpawn(cmd, cmdArgs, {
|
|
321
|
+
cwd: followupRunDir,
|
|
322
|
+
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
323
|
+
env: { ...process.env, PALMIER_TASK_ID: params.id },
|
|
324
|
+
windowsHide: true,
|
|
325
|
+
});
|
|
326
|
+
if (stdin != null)
|
|
327
|
+
child.stdin.end(stdin);
|
|
328
|
+
activeFollowups.set(followupKey, child);
|
|
329
|
+
// Collect output
|
|
330
|
+
const chunks = [];
|
|
331
|
+
child.stdout?.on("data", (d) => chunks.push(d));
|
|
332
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
333
|
+
child.on("close", async (code) => {
|
|
334
|
+
activeFollowups.delete(followupKey);
|
|
335
|
+
// If killed by stop_followup, the stopped status is already written
|
|
336
|
+
if (child.killed)
|
|
337
|
+
return;
|
|
338
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
339
|
+
const outcome = code !== 0 ? "failed" : parseTaskOutcome(output);
|
|
340
|
+
const reportFiles = parseReportFiles(output);
|
|
341
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
342
|
+
role: "assistant",
|
|
343
|
+
time: Date.now(),
|
|
344
|
+
content: stripPalmierMarkers(output),
|
|
345
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
346
|
+
});
|
|
347
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
348
|
+
role: "status",
|
|
349
|
+
time: Date.now(),
|
|
350
|
+
content: "",
|
|
351
|
+
type: outcome,
|
|
352
|
+
});
|
|
353
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
354
|
+
});
|
|
355
|
+
child.on("error", async (err) => {
|
|
356
|
+
activeFollowups.delete(followupKey);
|
|
357
|
+
console.error(`Follow-up failed for ${followupKey}:`, err);
|
|
358
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
359
|
+
role: "status",
|
|
360
|
+
time: Date.now(),
|
|
361
|
+
content: "",
|
|
362
|
+
type: "failed",
|
|
363
|
+
});
|
|
364
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
365
|
+
});
|
|
366
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
367
|
+
}
|
|
368
|
+
case "task.stop_followup": {
|
|
369
|
+
const params = request.params;
|
|
370
|
+
if (!params.run_id) {
|
|
371
|
+
return { error: "run_id is required" };
|
|
372
|
+
}
|
|
373
|
+
const stopKey = `${params.id}:${params.run_id}`;
|
|
374
|
+
const child = activeFollowups.get(stopKey);
|
|
375
|
+
if (!child) {
|
|
376
|
+
return { error: "No active follow-up for this run" };
|
|
377
|
+
}
|
|
378
|
+
// Kill the child process tree
|
|
379
|
+
if (process.platform === "win32" && child.pid) {
|
|
380
|
+
try {
|
|
381
|
+
const { execFileSync } = await import("child_process");
|
|
382
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
383
|
+
}
|
|
384
|
+
catch { /* may have already exited */ }
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
child.kill();
|
|
388
|
+
}
|
|
389
|
+
// Append stopped status (child.killed prevents the close handler from writing)
|
|
390
|
+
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
391
|
+
appendRunMessage(stopTaskDir, params.run_id, {
|
|
392
|
+
role: "status",
|
|
393
|
+
time: Date.now(),
|
|
394
|
+
content: "",
|
|
395
|
+
type: "stopped",
|
|
396
|
+
});
|
|
397
|
+
activeFollowups.delete(stopKey);
|
|
398
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
399
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
400
|
+
}
|
|
274
401
|
case "task.abort": {
|
|
275
402
|
const params = request.params;
|
|
403
|
+
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
404
|
+
// Read the PID before overwriting status — stopTask needs it to
|
|
405
|
+
// kill the entire process tree on Windows.
|
|
406
|
+
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
276
407
|
// Write abort status BEFORE killing so the dying process's signal
|
|
277
408
|
// handler can detect this was RPC-initiated and skip publishing.
|
|
278
|
-
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
279
409
|
writeTaskStatus(abortTaskDir, {
|
|
280
410
|
running_state: "aborted",
|
|
281
411
|
time_stamp: Date.now(),
|
|
412
|
+
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
282
413
|
});
|
|
414
|
+
// Append aborted status to the latest run
|
|
415
|
+
try {
|
|
416
|
+
const runDirs = fs.readdirSync(abortTaskDir)
|
|
417
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
418
|
+
.sort();
|
|
419
|
+
const latestRunId = runDirs[runDirs.length - 1];
|
|
420
|
+
if (latestRunId) {
|
|
421
|
+
appendRunMessage(abortTaskDir, latestRunId, {
|
|
422
|
+
role: "status",
|
|
423
|
+
time: Date.now(),
|
|
424
|
+
content: "",
|
|
425
|
+
type: "aborted",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch { /* best-effort */ }
|
|
283
430
|
try {
|
|
284
431
|
await getPlatform().stopTask(params.id);
|
|
285
432
|
}
|
|
@@ -304,25 +451,26 @@ export function createRpcHandler(config, nc) {
|
|
|
304
451
|
}
|
|
305
452
|
case "task.result": {
|
|
306
453
|
const params = request.params;
|
|
307
|
-
if (!params.
|
|
308
|
-
return { error: "
|
|
454
|
+
if (!params.run_id) {
|
|
455
|
+
return { error: "run_id is required" };
|
|
309
456
|
}
|
|
310
|
-
const
|
|
457
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", params.id, params.run_id, "TASKRUN.md");
|
|
311
458
|
try {
|
|
312
|
-
const raw = fs.readFileSync(
|
|
459
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
313
460
|
const meta = parseResultFrontmatter(raw);
|
|
314
461
|
return { task_id: params.id, ...meta };
|
|
315
462
|
}
|
|
316
463
|
catch {
|
|
317
|
-
return { task_id: params.id, error: "
|
|
464
|
+
return { task_id: params.id, error: "Run not found" };
|
|
318
465
|
}
|
|
319
466
|
}
|
|
320
467
|
case "task.reports": {
|
|
321
468
|
const params = request.params;
|
|
322
|
-
if (!Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
323
|
-
return { error: "
|
|
469
|
+
if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
470
|
+
return { error: "run_id and report_files are required" };
|
|
324
471
|
}
|
|
325
472
|
const reports = [];
|
|
473
|
+
const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
|
|
326
474
|
for (const file of params.report_files) {
|
|
327
475
|
if (!file.endsWith(".md")) {
|
|
328
476
|
reports.push({ file, error: "must end with .md" });
|
|
@@ -333,7 +481,7 @@ export function createRpcHandler(config, nc) {
|
|
|
333
481
|
reports.push({ file, error: "must be a plain filename" });
|
|
334
482
|
continue;
|
|
335
483
|
}
|
|
336
|
-
const reportPath = path.join(
|
|
484
|
+
const reportPath = path.join(runDir, basename);
|
|
337
485
|
try {
|
|
338
486
|
const content = fs.readFileSync(reportPath, "utf-8");
|
|
339
487
|
reports.push({ file, content });
|
|
@@ -355,7 +503,7 @@ export function createRpcHandler(config, nc) {
|
|
|
355
503
|
console.log(`[task.user_input] ${params.id} → ${params.value}`);
|
|
356
504
|
return { ok: true };
|
|
357
505
|
}
|
|
358
|
-
case "
|
|
506
|
+
case "taskrun.list": {
|
|
359
507
|
const params = request.params;
|
|
360
508
|
const { entries, total } = readHistory(config.projectRoot, {
|
|
361
509
|
offset: params.offset ?? 0,
|
|
@@ -363,30 +511,29 @@ export function createRpcHandler(config, nc) {
|
|
|
363
511
|
task_id: params.task_id,
|
|
364
512
|
});
|
|
365
513
|
const enriched = entries.map((entry) => {
|
|
366
|
-
const
|
|
514
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.run_id, "TASKRUN.md");
|
|
367
515
|
try {
|
|
368
|
-
const raw = fs.readFileSync(
|
|
516
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
369
517
|
const meta = parseResultFrontmatter(raw);
|
|
370
|
-
// Exclude messages from list response
|
|
371
518
|
const { messages: _, ...rest } = meta;
|
|
372
519
|
return { ...entry, ...rest };
|
|
373
520
|
}
|
|
374
521
|
catch {
|
|
375
|
-
return { ...entry, error: "
|
|
522
|
+
return { ...entry, error: "Run not found" };
|
|
376
523
|
}
|
|
377
524
|
});
|
|
378
525
|
return { entries: enriched, total };
|
|
379
526
|
}
|
|
380
|
-
case "
|
|
527
|
+
case "taskrun.delete": {
|
|
381
528
|
const params = request.params;
|
|
382
|
-
if (!params.task_id || !params.
|
|
383
|
-
return { error: "task_id and
|
|
529
|
+
if (!params.task_id || !params.run_id) {
|
|
530
|
+
return { error: "task_id and run_id are required" };
|
|
384
531
|
}
|
|
385
|
-
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.
|
|
532
|
+
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
|
|
386
533
|
if (!deleted) {
|
|
387
534
|
return { error: "History entry not found" };
|
|
388
535
|
}
|
|
389
|
-
return { ok: true, task_id: params.task_id,
|
|
536
|
+
return { ok: true, task_id: params.task_id, run_id: params.run_id };
|
|
390
537
|
}
|
|
391
538
|
case "host.update": {
|
|
392
539
|
const error = await performUpdate();
|
package/dist/task.d.ts
CHANGED
|
@@ -39,31 +39,31 @@ export declare function writeTaskStatus(taskDir: string, status: TaskStatus): vo
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
41
41
|
/**
|
|
42
|
-
* Create
|
|
43
|
-
*
|
|
44
|
-
* Returns the result file name.
|
|
42
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
43
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
45
44
|
*/
|
|
46
|
-
export declare function
|
|
45
|
+
export declare function createRunDir(taskDir: string, taskName: string, startTime: number): string;
|
|
47
46
|
/**
|
|
48
|
-
*
|
|
47
|
+
* Get the path to a run directory.
|
|
49
48
|
*/
|
|
50
|
-
export declare function
|
|
49
|
+
export declare function getRunDir(taskDir: string, runId: string): string;
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
51
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
53
52
|
*/
|
|
54
|
-
export declare function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
export declare function appendRunMessage(taskDir: string, runId: string, msg: ConversationMessage): void;
|
|
54
|
+
/**
|
|
55
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readRunMessages(taskDir: string, runId: string): ConversationMessage[];
|
|
58
58
|
/**
|
|
59
59
|
* Append a history entry to the project-level history.jsonl file.
|
|
60
60
|
*/
|
|
61
61
|
export declare function appendHistory(projectRoot: string, entry: HistoryEntry): void;
|
|
62
62
|
/**
|
|
63
|
-
* Delete a history entry and its associated
|
|
63
|
+
* Delete a history entry and its associated run directory.
|
|
64
64
|
* Returns true if the entry was found and removed.
|
|
65
65
|
*/
|
|
66
|
-
export declare function deleteHistoryEntry(projectRoot: string, taskId: string,
|
|
66
|
+
export declare function deleteHistoryEntry(projectRoot: string, taskId: string, runId: string): boolean;
|
|
67
67
|
/**
|
|
68
68
|
* Read history entries from history.jsonl with pagination.
|
|
69
69
|
* Returns entries sorted most-recent-first.
|
package/dist/task.js
CHANGED
|
@@ -130,21 +130,27 @@ export function readTaskStatus(taskDir) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Create
|
|
134
|
-
*
|
|
135
|
-
* Returns the result file name.
|
|
133
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
134
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
136
135
|
*/
|
|
137
|
-
export function
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
export function createRunDir(taskDir, taskName, startTime) {
|
|
137
|
+
const runId = String(startTime);
|
|
138
|
+
const runDir = path.join(taskDir, runId);
|
|
139
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
140
|
+
const content = `---\ntask_name: ${taskName}\n---\n\n`;
|
|
141
|
+
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
142
|
+
return runId;
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
145
|
+
* Get the path to a run directory.
|
|
146
146
|
*/
|
|
147
|
-
export function
|
|
147
|
+
export function getRunDir(taskDir, runId) {
|
|
148
|
+
return path.join(taskDir, runId);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
152
|
+
*/
|
|
153
|
+
export function appendRunMessage(taskDir, runId, msg) {
|
|
148
154
|
const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
|
|
149
155
|
if (msg.type)
|
|
150
156
|
attrs.push(`type="${msg.type}"`);
|
|
@@ -152,29 +158,41 @@ export function appendResultMessage(taskDir, resultFile, msg) {
|
|
|
152
158
|
attrs.push(`attachments="${msg.attachments.join(",")}"`);
|
|
153
159
|
const delimiter = `<!-- palmier:message ${attrs.join(" ")} -->`;
|
|
154
160
|
const entry = `${delimiter}\n\n${msg.content}\n\n`;
|
|
155
|
-
fs.appendFileSync(path.join(taskDir,
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
*
|
|
159
|
-
*/
|
|
160
|
-
export function
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
165
|
+
*/
|
|
166
|
+
export function readRunMessages(taskDir, runId) {
|
|
167
|
+
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
168
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
169
|
+
if (!fmMatch)
|
|
170
|
+
return [];
|
|
171
|
+
const body = fmMatch[1];
|
|
172
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
173
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
174
|
+
if (matches.length === 0)
|
|
175
|
+
return [];
|
|
176
|
+
const messages = [];
|
|
177
|
+
for (let i = 0; i < matches.length; i++) {
|
|
178
|
+
const match = matches[i];
|
|
179
|
+
const attrs = match[1];
|
|
180
|
+
const start = match.index + match[0].length;
|
|
181
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
|
|
182
|
+
const content = body.slice(start, end).trim();
|
|
183
|
+
const roleAttr = attrs.match(/role="([^"]*)"/)?.[1] ?? "assistant";
|
|
184
|
+
const timeAttr = attrs.match(/time="([^"]*)"/)?.[1] ?? "0";
|
|
185
|
+
const typeAttr = attrs.match(/type="([^"]*)"/)?.[1];
|
|
186
|
+
const attachmentsAttr = attrs.match(/attachments="([^"]*)"/)?.[1];
|
|
187
|
+
messages.push({
|
|
188
|
+
role: roleAttr,
|
|
189
|
+
time: Number(timeAttr),
|
|
190
|
+
content,
|
|
191
|
+
...(typeAttr ? { type: typeAttr } : {}),
|
|
192
|
+
...(attachmentsAttr ? { attachments: attachmentsAttr.split(",").map((f) => f.trim()).filter(Boolean) } : {}),
|
|
193
|
+
});
|
|
176
194
|
}
|
|
177
|
-
|
|
195
|
+
return messages;
|
|
178
196
|
}
|
|
179
197
|
/**
|
|
180
198
|
* Append a history entry to the project-level history.jsonl file.
|
|
@@ -184,10 +202,10 @@ export function appendHistory(projectRoot, entry) {
|
|
|
184
202
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
185
203
|
}
|
|
186
204
|
/**
|
|
187
|
-
* Delete a history entry and its associated
|
|
205
|
+
* Delete a history entry and its associated run directory.
|
|
188
206
|
* Returns true if the entry was found and removed.
|
|
189
207
|
*/
|
|
190
|
-
export function deleteHistoryEntry(projectRoot, taskId,
|
|
208
|
+
export function deleteHistoryEntry(projectRoot, taskId, runId) {
|
|
191
209
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
192
210
|
if (!fs.existsSync(historyPath))
|
|
193
211
|
return false;
|
|
@@ -197,9 +215,9 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
197
215
|
for (const line of lines) {
|
|
198
216
|
try {
|
|
199
217
|
const entry = JSON.parse(line);
|
|
200
|
-
if (entry.task_id === taskId && entry.
|
|
218
|
+
if (entry.task_id === taskId && entry.run_id === runId) {
|
|
201
219
|
found = true;
|
|
202
|
-
continue;
|
|
220
|
+
continue;
|
|
203
221
|
}
|
|
204
222
|
}
|
|
205
223
|
catch { /* keep malformed lines */ }
|
|
@@ -207,21 +225,11 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
207
225
|
}
|
|
208
226
|
if (!found)
|
|
209
227
|
return false;
|
|
210
|
-
// Rewrite history.jsonl without the deleted entry
|
|
211
228
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
212
|
-
// Delete the
|
|
213
|
-
const
|
|
214
|
-
if (fs.existsSync(
|
|
215
|
-
fs.
|
|
216
|
-
}
|
|
217
|
-
// Delete the corresponding task snapshot (TASK-<timestamp>.md)
|
|
218
|
-
const tsMatch = resultFile.match(/^RESULT-(\d+)\.md$/);
|
|
219
|
-
if (tsMatch) {
|
|
220
|
-
const snapshotFile = `TASK-${tsMatch[1]}.md`;
|
|
221
|
-
const snapshotPath = path.join(projectRoot, "tasks", taskId, snapshotFile);
|
|
222
|
-
if (fs.existsSync(snapshotPath)) {
|
|
223
|
-
fs.unlinkSync(snapshotPath);
|
|
224
|
-
}
|
|
229
|
+
// Delete the run directory
|
|
230
|
+
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
231
|
+
if (fs.existsSync(runDir)) {
|
|
232
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
225
233
|
}
|
|
226
234
|
return true;
|
|
227
235
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface TaskStatus {
|
|
|
61
61
|
}
|
|
62
62
|
export interface HistoryEntry {
|
|
63
63
|
task_id: string;
|
|
64
|
-
|
|
64
|
+
run_id: string;
|
|
65
65
|
}
|
|
66
66
|
export interface RequiredPermission {
|
|
67
67
|
name: string;
|
|
@@ -71,7 +71,7 @@ export interface ConversationMessage {
|
|
|
71
71
|
role: "assistant" | "user" | "status";
|
|
72
72
|
time: number;
|
|
73
73
|
content: string;
|
|
74
|
-
type?: "input" | "permission" | "confirmation" | "started" | "finished" | "failed" | "aborted";
|
|
74
|
+
type?: "input" | "permission" | "confirmation" | "started" | "finished" | "failed" | "aborted" | "stopped";
|
|
75
75
|
attachments?: string[];
|
|
76
76
|
}
|
|
77
77
|
export interface RpcMessage {
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { connectNats } from "../nats-client.js";
|
|
3
|
-
import { getTaskDir, parseTaskFile,
|
|
3
|
+
import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
|
|
4
4
|
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Request input from the user and print responses to stdout.
|
|
8
8
|
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
9
9
|
*
|
|
10
|
-
* Requires PALMIER_TASK_ID
|
|
11
|
-
* Outputs each response on its own line: "description: value"
|
|
10
|
+
* Requires PALMIER_TASK_ID and PALMIER_RUN_DIR environment variables.
|
|
12
11
|
*/
|
|
13
12
|
export async function requestInputCommand(opts: { description: string[] }): Promise<void> {
|
|
14
13
|
const taskId = process.env.PALMIER_TASK_ID;
|
|
@@ -21,36 +20,23 @@ export async function requestInputCommand(opts: { description: string[] }): Prom
|
|
|
21
20
|
const nc = await connectNats(config);
|
|
22
21
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
23
22
|
const task = parseTaskFile(taskDir);
|
|
23
|
+
const runId = process.env.PALMIER_RUN_DIR?.split(/[/\\]/).pop();
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
26
|
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
|
|
27
27
|
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
28
28
|
|
|
29
29
|
if (response === "aborted") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (resultFile) {
|
|
33
|
-
appendResultMessage(taskDir, resultFile, {
|
|
34
|
-
role: "user",
|
|
35
|
-
time: Date.now(),
|
|
36
|
-
content: "Input request aborted.",
|
|
37
|
-
type: "input",
|
|
38
|
-
});
|
|
30
|
+
if (runId) {
|
|
31
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
|
|
39
32
|
}
|
|
40
33
|
console.error("User aborted the input request.");
|
|
41
34
|
process.exit(1);
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
46
|
-
if (resultFile) {
|
|
37
|
+
if (runId) {
|
|
47
38
|
const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
|
|
48
|
-
|
|
49
|
-
role: "user",
|
|
50
|
-
time: Date.now(),
|
|
51
|
-
content: lines.join("\n"),
|
|
52
|
-
type: "input",
|
|
53
|
-
});
|
|
39
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
|
|
54
40
|
}
|
|
55
41
|
|
|
56
42
|
for (let i = 0; i < opts.description.length; i++) {
|