palmier 0.4.3 → 0.4.5
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 +7 -4
- package/dist/agents/agent.d.ts +2 -2
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +4 -4
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +4 -4
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +3 -3
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +4 -4
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +2 -2
- 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 +57 -64
- 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/agents/agent.ts +2 -2
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +3 -3
- package/src/agents/copilot.ts +3 -3
- package/src/agents/gemini.ts +3 -3
- package/src/agents/openclaw.ts +2 -2
- package/src/commands/request-input.ts +7 -21
- package/src/commands/run.ts +57 -67
- 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/test/agent-output-parsing.test.ts +1 -14
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
package/src/agents/agent.ts
CHANGED
|
@@ -20,10 +20,10 @@ export interface AgentTool {
|
|
|
20
20
|
/** Return the command and args used to generate a plan from a prompt. */
|
|
21
21
|
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
22
22
|
|
|
23
|
-
/** Return the command and args used to run a task. If
|
|
23
|
+
/** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
|
|
24
24
|
* and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
|
|
25
25
|
* permissions granted for this run only (not persisted in frontmatter). */
|
|
26
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
26
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
27
27
|
|
|
28
28
|
/** Detect whether the agent CLI is available and perform any agent-specific
|
|
29
29
|
* initialization. Returns true if the agent was detected and initialized successfully. */
|
package/src/agents/claude.ts
CHANGED
|
@@ -12,8 +12,8 @@ export class ClaudeAgent implements AgentTool {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
16
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
17
|
const args = ["--permission-mode", "acceptEdits", "-p"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
@@ -21,7 +21,7 @@ export class ClaudeAgent implements AgentTool {
|
|
|
21
21
|
args.push("--allowedTools", p.name);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (followupPrompt) {args.push("-c");} // continue mode for followups
|
|
25
25
|
return { command: "claude", args, stdin: prompt };
|
|
26
26
|
}
|
|
27
27
|
|
package/src/agents/codex.ts
CHANGED
|
@@ -12,8 +12,8 @@ export class CodexAgent implements AgentTool {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
16
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
17
|
// Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
|
|
18
18
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ export class CodexAgent implements AgentTool {
|
|
|
24
24
|
}
|
|
25
25
|
args.push("-"); // read prompt from stdin
|
|
26
26
|
|
|
27
|
-
if (
|
|
27
|
+
if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
|
|
28
28
|
return { command: "codex", args, stdin: prompt };
|
|
29
29
|
}
|
|
30
30
|
|
package/src/agents/copilot.ts
CHANGED
|
@@ -12,8 +12,8 @@ export class CopilotAgent implements AgentTool {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
16
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
17
|
const args = ["-p", prompt];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
@@ -21,7 +21,7 @@ export class CopilotAgent implements AgentTool {
|
|
|
21
21
|
args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (followupPrompt) { args.push("--continue"); }
|
|
25
25
|
return { command: "copilot", args};
|
|
26
26
|
}
|
|
27
27
|
|
package/src/agents/gemini.ts
CHANGED
|
@@ -12,8 +12,8 @@ export class GeminiAgent implements AgentTool {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
16
|
-
const prompt =
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
+
const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
17
17
|
const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
|
|
18
18
|
const args = ["--prompt", "-"];
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ export class GeminiAgent implements AgentTool {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (
|
|
28
|
+
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
29
29
|
return { command: "gemini", args, stdin: fullPrompt };
|
|
30
30
|
}
|
|
31
31
|
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -11,8 +11,8 @@ export class OpenClawAgent implements AgentTool {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
15
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
14
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
16
16
|
// OpenClaw does not support stdin as prompt.
|
|
17
17
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
18
18
|
|
|
@@ -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++) {
|
package/src/commands/run.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as readline from "readline";
|
|
|
4
4
|
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory,
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
@@ -22,7 +22,7 @@ interface InvocationContext {
|
|
|
22
22
|
agent: AgentTool;
|
|
23
23
|
task: ParsedTask;
|
|
24
24
|
taskDir: string;
|
|
25
|
-
|
|
25
|
+
runId: string;
|
|
26
26
|
guiEnv: Record<string, string>;
|
|
27
27
|
nc: NatsConnection | undefined;
|
|
28
28
|
config: HostConfig;
|
|
@@ -36,23 +36,23 @@ interface InvocationResult {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Invoke the agent CLI with a
|
|
39
|
+
* Invoke the agent CLI with a continuation loop for permissions and user input.
|
|
40
40
|
*
|
|
41
41
|
* Both standard and command-triggered execution use this.
|
|
42
42
|
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
43
43
|
* (for command-triggered mode this is the per-line augmented task).
|
|
44
44
|
*/
|
|
45
|
-
async function
|
|
45
|
+
async function invokeAgentWithContinuation(
|
|
46
46
|
ctx: InvocationContext,
|
|
47
47
|
invokeTask: ParsedTask,
|
|
48
48
|
): Promise<InvocationResult> {
|
|
49
|
-
let
|
|
49
|
+
let followupPrompt: string | undefined;
|
|
50
50
|
// eslint-disable-next-line no-constant-condition
|
|
51
51
|
while (true) {
|
|
52
|
-
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask,
|
|
52
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
|
|
53
53
|
const result = await spawnCommand(command, args, {
|
|
54
|
-
cwd: ctx.taskDir,
|
|
55
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id,
|
|
54
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
55
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
56
56
|
echoStdout: true,
|
|
57
57
|
resolveOnFailure: true,
|
|
58
58
|
stdin,
|
|
@@ -70,8 +70,8 @@ async function invokeAgentWithRetry(
|
|
|
70
70
|
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
// Permission
|
|
74
|
-
if (
|
|
73
|
+
// Permission handling — agent requested permissions
|
|
74
|
+
if (requiredPermissions.length > 0) {
|
|
75
75
|
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
76
76
|
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
|
|
77
77
|
|
|
@@ -106,11 +106,14 @@ async function invokeAgentWithRetry(
|
|
|
106
106
|
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// If the agent actually failed, retry with the new permissions
|
|
110
|
+
if (outcome === "failed") {
|
|
111
|
+
followupPrompt = "Permissions granted, please continue.";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
// Normal completion (success or
|
|
116
|
+
// Normal completion (success or terminal failure)
|
|
114
117
|
return { outcome };
|
|
115
118
|
}
|
|
116
119
|
}
|
|
@@ -118,7 +121,7 @@ async function invokeAgentWithRetry(
|
|
|
118
121
|
/**
|
|
119
122
|
* Strip [PALMIER_*] marker lines from agent output.
|
|
120
123
|
*/
|
|
121
|
-
function stripPalmierMarkers(output: string): string {
|
|
124
|
+
export function stripPalmierMarkers(output: string): string {
|
|
122
125
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -127,22 +130,24 @@ function stripPalmierMarkers(output: string): string {
|
|
|
127
130
|
*/
|
|
128
131
|
async function appendAndNotify(
|
|
129
132
|
ctx: InvocationContext,
|
|
130
|
-
msg: Parameters<typeof
|
|
133
|
+
msg: Parameters<typeof appendRunMessage>[2],
|
|
131
134
|
): Promise<void> {
|
|
132
|
-
|
|
133
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
|
|
135
|
+
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
136
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
137
|
-
* Find
|
|
140
|
+
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
138
141
|
*/
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
function findLatestPendingRunId(taskDir: string): string | null {
|
|
143
|
+
const dirs = fs.readdirSync(taskDir)
|
|
144
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
145
|
+
.sort();
|
|
146
|
+
if (dirs.length === 0) return null;
|
|
147
|
+
const latest = dirs[dirs.length - 1];
|
|
148
|
+
const messages = readRunMessages(taskDir, latest);
|
|
149
|
+
const hasStatus = messages.some((m) => m.role === "status");
|
|
150
|
+
return hasStatus ? null : latest;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
|
@@ -167,15 +172,11 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
167
172
|
let nc: NatsConnection | undefined;
|
|
168
173
|
const taskName = task.frontmatter.name;
|
|
169
174
|
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Snapshot the task file at run time
|
|
176
|
-
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
177
|
-
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
178
|
-
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
175
|
+
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
176
|
+
const existingRunId = findLatestPendingRunId(taskDir);
|
|
177
|
+
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
|
|
178
|
+
if (!existingRunId) {
|
|
179
|
+
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
const cleanup = async () => {
|
|
@@ -184,19 +185,12 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
184
185
|
}
|
|
185
186
|
};
|
|
186
187
|
|
|
187
|
-
if (!existingResult) {
|
|
188
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
188
|
try {
|
|
192
189
|
nc = await connectNats(config);
|
|
193
190
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Status: started
|
|
198
|
-
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
199
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
191
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
192
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
193
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
200
194
|
|
|
201
195
|
// If requires_confirmation, notify clients and wait
|
|
202
196
|
if (task.frontmatter.requires_confirmation) {
|
|
@@ -205,22 +199,21 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
205
199
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
206
200
|
if (!confirmed) {
|
|
207
201
|
console.log("Task aborted by user.");
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
202
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
203
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
211
204
|
await cleanup();
|
|
212
205
|
return;
|
|
213
206
|
}
|
|
214
207
|
console.log("Task confirmed by user.");
|
|
215
|
-
|
|
216
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
208
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
209
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
217
210
|
}
|
|
218
211
|
|
|
219
212
|
// Shared invocation context
|
|
220
213
|
const guiEnv = getPlatform().getGuiEnv();
|
|
221
214
|
const agent = getAgent(task.frontmatter.agent);
|
|
222
215
|
const ctx: InvocationContext = {
|
|
223
|
-
agent, task, taskDir,
|
|
216
|
+
agent, task, taskDir, runId, guiEnv, nc, config, taskId,
|
|
224
217
|
transientPermissions: [],
|
|
225
218
|
};
|
|
226
219
|
|
|
@@ -228,9 +221,8 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
228
221
|
// Command-triggered mode
|
|
229
222
|
const result = await runCommandTriggeredMode(ctx);
|
|
230
223
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
224
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
225
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
234
226
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
235
227
|
} else {
|
|
236
228
|
// Standard execution — add user prompt as first message
|
|
@@ -240,25 +232,23 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
240
232
|
content: task.body || task.frontmatter.user_prompt,
|
|
241
233
|
});
|
|
242
234
|
|
|
243
|
-
const result = await
|
|
235
|
+
const result = await invokeAgentWithContinuation(ctx, task);
|
|
244
236
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
237
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
238
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
248
239
|
console.log(`Task ${taskId} completed.`);
|
|
249
240
|
}
|
|
250
241
|
} catch (err) {
|
|
251
242
|
console.error(`Task ${taskId} failed:`, err);
|
|
252
243
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
253
244
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
254
|
-
|
|
245
|
+
appendRunMessage(taskDir, runId, {
|
|
255
246
|
role: "assistant",
|
|
256
247
|
time: Date.now(),
|
|
257
248
|
content: errorMsg,
|
|
258
249
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
250
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
251
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
262
252
|
process.exitCode = 1;
|
|
263
253
|
} finally {
|
|
264
254
|
await cleanup();
|
|
@@ -284,8 +274,8 @@ async function runCommandTriggeredMode(
|
|
|
284
274
|
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
285
275
|
|
|
286
276
|
const child = spawnStreamingCommand(commandStr, {
|
|
287
|
-
cwd: ctx.taskDir,
|
|
288
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
277
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
278
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
289
279
|
});
|
|
290
280
|
|
|
291
281
|
let linesProcessed = 0;
|
|
@@ -297,7 +287,7 @@ async function runCommandTriggeredMode(
|
|
|
297
287
|
let commandExited = false;
|
|
298
288
|
let resolveWhenDone: (() => void) | undefined;
|
|
299
289
|
|
|
300
|
-
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
290
|
+
const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
|
|
301
291
|
function appendLog(line: string, agentOutput: string, outcome: string) {
|
|
302
292
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
303
293
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
@@ -329,7 +319,7 @@ async function runCommandTriggeredMode(
|
|
|
329
319
|
body: "",
|
|
330
320
|
};
|
|
331
321
|
|
|
332
|
-
const result = await
|
|
322
|
+
const result = await invokeAgentWithContinuation(ctx, perLineTask);
|
|
333
323
|
if (result.outcome === "finished") {
|
|
334
324
|
invocationsSucceeded++;
|
|
335
325
|
} else {
|
|
@@ -404,7 +394,7 @@ async function publishTaskEvent(
|
|
|
404
394
|
taskId: string,
|
|
405
395
|
eventType: TaskRunningState,
|
|
406
396
|
taskName?: string,
|
|
407
|
-
|
|
397
|
+
runId?: string,
|
|
408
398
|
): Promise<void> {
|
|
409
399
|
writeTaskStatus(taskDir, {
|
|
410
400
|
running_state: eventType,
|
|
@@ -414,7 +404,7 @@ async function publishTaskEvent(
|
|
|
414
404
|
|
|
415
405
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
|
|
416
406
|
if (taskName) payload.name = taskName;
|
|
417
|
-
if (
|
|
407
|
+
if (runId) payload.run_id = runId;
|
|
418
408
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
419
409
|
}
|
|
420
410
|
|
package/src/commands/serve.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus,
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
|
|
8
8
|
import { publishHostEvent } from "../events.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { detectAgents } from "../agents/agent.js";
|
|
@@ -17,46 +17,11 @@ const POLL_INTERVAL_MS = 30_000;
|
|
|
17
17
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* and broadcast the failure event.
|
|
22
|
-
*/
|
|
23
|
-
async function markTaskFailed(
|
|
24
|
-
config: HostConfig,
|
|
25
|
-
nc: NatsConnection | undefined,
|
|
26
|
-
taskId: string,
|
|
27
|
-
reason: string,
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
30
|
-
const status = readTaskStatus(taskDir);
|
|
31
|
-
if (!status || status.running_state !== "started") return;
|
|
32
|
-
|
|
33
|
-
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
34
|
-
const endTime = Date.now();
|
|
35
|
-
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
36
|
-
|
|
37
|
-
let taskName = taskId;
|
|
38
|
-
try {
|
|
39
|
-
const task = parseTaskFile(taskDir);
|
|
40
|
-
taskName = task.frontmatter.name || taskId;
|
|
41
|
-
} catch { /* use taskId as fallback */ }
|
|
42
|
-
|
|
43
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
44
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
|
|
45
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
46
|
-
appendResultMessage(taskDir, resultFileName, {
|
|
47
|
-
role: "assistant",
|
|
48
|
-
time: endTime,
|
|
49
|
-
content: reason,
|
|
50
|
-
});
|
|
51
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
52
|
-
|
|
53
|
-
const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
54
|
-
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
20
|
+
* Scan all tasks for any stuck in "started" state whose process is no longer alive.
|
|
59
21
|
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
22
|
+
*
|
|
23
|
+
* Since run.ts creates the RESULT file and history entry at start, we just need to
|
|
24
|
+
* finalize the existing RESULT file, append a failed status entry, and broadcast.
|
|
60
25
|
*/
|
|
61
26
|
async function checkStaleTasks(
|
|
62
27
|
config: HostConfig,
|
|
@@ -80,7 +45,35 @@ async function checkStaleTasks(
|
|
|
80
45
|
// Ask the system scheduler if the task is still running
|
|
81
46
|
if (platform.isTaskRunning(taskId)) continue;
|
|
82
47
|
|
|
83
|
-
|
|
48
|
+
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
49
|
+
const endTime = Date.now();
|
|
50
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
51
|
+
|
|
52
|
+
// Find the latest run directory (created by run.ts at start)
|
|
53
|
+
const runId = fs.readdirSync(taskDir)
|
|
54
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
55
|
+
.sort()
|
|
56
|
+
.pop();
|
|
57
|
+
|
|
58
|
+
if (runId) {
|
|
59
|
+
appendRunMessage(taskDir, runId, {
|
|
60
|
+
role: "status",
|
|
61
|
+
time: endTime,
|
|
62
|
+
content: "",
|
|
63
|
+
type: "failed",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let taskName = taskId;
|
|
68
|
+
try {
|
|
69
|
+
taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
|
|
70
|
+
} catch { /* use taskId as fallback */ }
|
|
71
|
+
|
|
72
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
73
|
+
event_type: "running-state",
|
|
74
|
+
running_state: "failed",
|
|
75
|
+
name: taskName,
|
|
76
|
+
});
|
|
84
77
|
}
|
|
85
78
|
}
|
|
86
79
|
|
package/src/platform/linux.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { execSync, exec } from "child_process";
|
|
|
5
5
|
import { promisify } from "util";
|
|
6
6
|
import type { PlatformService } from "./platform.js";
|
|
7
7
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
8
|
+
import { loadConfig } from "../config.js";
|
|
9
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
10
|
|
|
9
11
|
const execAsync = promisify(exec);
|
|
10
12
|
|
|
@@ -230,20 +232,28 @@ WantedBy=timers.target
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
isTaskRunning(taskId: string): boolean {
|
|
235
|
+
// Check systemd first (for scheduled/on-demand runs)
|
|
233
236
|
const serviceName = getServiceName(taskId);
|
|
234
237
|
try {
|
|
235
|
-
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
236
|
-
// the state is "activating" while running, which exits non-zero.
|
|
237
|
-
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
238
238
|
const out = execSync(
|
|
239
239
|
`systemctl --user show -p ActiveState --value ${serviceName}`,
|
|
240
240
|
{ encoding: "utf-8" },
|
|
241
241
|
);
|
|
242
242
|
const state = out.trim();
|
|
243
|
-
|
|
244
|
-
} catch {
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (state === "active" || state === "activating") return true;
|
|
244
|
+
} catch { /* service may not exist */ }
|
|
245
|
+
|
|
246
|
+
// Fall back to PID check (for follow-up runs spawned directly)
|
|
247
|
+
try {
|
|
248
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
249
|
+
const status = readTaskStatus(taskDir);
|
|
250
|
+
if (status?.pid) {
|
|
251
|
+
process.kill(status.pid, 0); // signal 0 = check if process exists
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
} catch { /* process not running or config unavailable */ }
|
|
255
|
+
|
|
256
|
+
return false;
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
getGuiEnv(): Record<string, string> {
|