palmier 0.2.4 → 0.2.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/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +288 -223
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +15 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +14 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +15 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/info.js +17 -10
- package/dist/commands/init.d.ts +2 -10
- package/dist/commands/init.js +171 -163
- package/dist/commands/mcpserver.js +11 -21
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +295 -114
- package/dist/commands/serve.js +83 -4
- package/dist/commands/sessions.js +1 -4
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.js +29 -0
- package/dist/index.js +14 -7
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/linux.d.ts +13 -0
- package/dist/platform/linux.js +207 -0
- package/dist/platform/platform.d.ts +24 -0
- package/dist/platform/platform.js +2 -0
- package/dist/platform/windows.d.ts +14 -0
- package/dist/platform/windows.js +218 -0
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +53 -55
- package/dist/spawn-command.d.ts +32 -6
- package/dist/spawn-command.js +38 -13
- package/dist/transports/http-transport.js +2 -7
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +4 -2
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -39
- package/src/agents/codex.ts +14 -12
- package/src/agents/gemini.ts +15 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/info.ts +18 -10
- package/src/commands/init.ts +201 -186
- package/src/commands/mcpserver.ts +11 -22
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +367 -133
- package/src/commands/serve.ts +101 -5
- package/src/commands/sessions.ts +1 -4
- package/src/config.ts +1 -1
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +38 -0
- package/src/index.ts +16 -7
- package/src/platform/index.ts +16 -0
- package/src/platform/linux.ts +231 -0
- package/src/platform/platform.ts +31 -0
- package/src/platform/windows.ts +234 -0
- package/src/rpc-handler.ts +56 -63
- package/src/spawn-command.ts +120 -78
- package/src/transports/http-transport.ts +2 -5
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -2
- package/src/systemd.ts +0 -164
package/src/commands/run.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import
|
|
3
|
+
import * as readline from "readline";
|
|
4
|
+
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
4
5
|
import { loadConfig } from "../config.js";
|
|
5
6
|
import { connectNats } from "../nats-client.js";
|
|
6
7
|
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory } from "../task.js";
|
|
7
8
|
import { getAgent } from "../agents/agent.js";
|
|
8
|
-
import {
|
|
9
|
+
import { getPlatform } from "../platform/index.js";
|
|
10
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX, TASK_INPUT_PREFIX } from "../agents/shared-prompt.js";
|
|
11
|
+
import type { AgentTool } from "../agents/agent.js";
|
|
12
|
+
import { publishHostEvent } from "../events.js";
|
|
9
13
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
10
14
|
import type { NatsConnection } from "nats";
|
|
11
|
-
import { StringCodec } from "nats";
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Write a time-stamped RESULT file with frontmatter.
|
|
@@ -33,6 +36,113 @@ function writeResult(
|
|
|
33
36
|
return resultFileName;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Shared context for agent invocation retry loops.
|
|
41
|
+
* Passed around to avoid threading many individual parameters.
|
|
42
|
+
*/
|
|
43
|
+
interface InvocationContext {
|
|
44
|
+
agent: AgentTool;
|
|
45
|
+
task: ParsedTask;
|
|
46
|
+
taskDir: string;
|
|
47
|
+
guiEnv: Record<string, string>;
|
|
48
|
+
nc: NatsConnection | undefined;
|
|
49
|
+
config: HostConfig;
|
|
50
|
+
taskId: string;
|
|
51
|
+
useHttp: boolean;
|
|
52
|
+
/** Mutable — accumulates across invocations within a run. */
|
|
53
|
+
transientPermissions: RequiredPermission[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface InvocationResult {
|
|
57
|
+
output: string;
|
|
58
|
+
outcome: TaskRunningState;
|
|
59
|
+
reportFiles: string[];
|
|
60
|
+
requiredPermissions: RequiredPermission[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Invoke the agent CLI with a retry loop for permissions and user input.
|
|
65
|
+
*
|
|
66
|
+
* Both standard and command-triggered execution use this.
|
|
67
|
+
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
68
|
+
* (for command-triggered mode this is the per-line augmented task).
|
|
69
|
+
*/
|
|
70
|
+
async function invokeAgentWithRetry(
|
|
71
|
+
ctx: InvocationContext,
|
|
72
|
+
invokeTask: ParsedTask,
|
|
73
|
+
): Promise<InvocationResult> {
|
|
74
|
+
let retryPrompt: string | undefined;
|
|
75
|
+
// eslint-disable-next-line no-constant-condition
|
|
76
|
+
while (true) {
|
|
77
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
78
|
+
const result = await spawnCommand(command, args, {
|
|
79
|
+
cwd: ctx.taskDir,
|
|
80
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
81
|
+
echoStdout: true,
|
|
82
|
+
resolveOnFailure: true,
|
|
83
|
+
stdin,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const outcome: TaskRunningState = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
87
|
+
const reportFiles = parseReportFiles(result.output);
|
|
88
|
+
const requiredPermissions = parsePermissions(result.output);
|
|
89
|
+
|
|
90
|
+
// Permission retry
|
|
91
|
+
if (outcome === "failed" && requiredPermissions.length > 0) {
|
|
92
|
+
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions, ctx.useHttp);
|
|
93
|
+
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response, ctx.useHttp);
|
|
94
|
+
|
|
95
|
+
if (response === "aborted") {
|
|
96
|
+
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newPerms = requiredPermissions.filter(
|
|
100
|
+
(rp) => !ctx.task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
101
|
+
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (response === "granted_all") {
|
|
105
|
+
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
106
|
+
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
107
|
+
writeTaskFile(ctx.taskDir, ctx.task);
|
|
108
|
+
} else {
|
|
109
|
+
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
retryPrompt = "Permissions granted, please continue.";
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Input retry
|
|
117
|
+
const inputRequests = parseInputRequests(result.output);
|
|
118
|
+
if (outcome === "failed" && inputRequests.length > 0) {
|
|
119
|
+
const response = await requestUserInput(ctx.nc, ctx.config, ctx.task, ctx.taskDir, inputRequests, ctx.useHttp);
|
|
120
|
+
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided", ctx.useHttp);
|
|
121
|
+
|
|
122
|
+
if (response === "aborted") {
|
|
123
|
+
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputLines = inputRequests.map((desc, i) => `- ${desc} → ${response[i]}`).join("\n");
|
|
127
|
+
retryPrompt = `The user provided the following inputs:\n${inputLines}\nPlease continue with these values.`;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Normal completion (success or non-retryable failure)
|
|
132
|
+
return { output: result.output, outcome, reportFiles, requiredPermissions };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
|
|
138
|
+
* respect that instead of overwriting with the process's own outcome.
|
|
139
|
+
*/
|
|
140
|
+
function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunningState {
|
|
141
|
+
const current = readTaskStatus(taskDir);
|
|
142
|
+
if (current?.running_state === "aborted") return "aborted";
|
|
143
|
+
return outcome;
|
|
144
|
+
}
|
|
145
|
+
|
|
36
146
|
/**
|
|
37
147
|
* Execute a task by ID.
|
|
38
148
|
*/
|
|
@@ -60,26 +170,13 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
60
170
|
}
|
|
61
171
|
};
|
|
62
172
|
|
|
63
|
-
// Handle signals
|
|
64
|
-
const onSignal = async () => {
|
|
65
|
-
console.log("Received signal, cleaning up...");
|
|
66
|
-
const endTime = Date.now();
|
|
67
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "abort", startTime, endTime, "", [], []);
|
|
68
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
69
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "abort", useHttp, taskName);
|
|
70
|
-
await cleanup();
|
|
71
|
-
process.exit(1);
|
|
72
|
-
};
|
|
73
|
-
process.on("SIGINT", onSignal);
|
|
74
|
-
process.on("SIGTERM", onSignal);
|
|
75
|
-
|
|
76
173
|
try {
|
|
77
174
|
if (useNats) {
|
|
78
175
|
nc = await connectNats(config);
|
|
79
176
|
}
|
|
80
177
|
|
|
81
178
|
// Mark as started immediately
|
|
82
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "
|
|
179
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", useHttp, taskName);
|
|
83
180
|
|
|
84
181
|
// If requires_confirmation, notify clients and wait
|
|
85
182
|
if (task.frontmatter.requires_confirmation) {
|
|
@@ -89,129 +186,213 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
89
186
|
if (!confirmed) {
|
|
90
187
|
console.log("Task aborted by user.");
|
|
91
188
|
const endTime = Date.now();
|
|
92
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "
|
|
189
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "aborted", startTime, endTime, "", [], []);
|
|
93
190
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
94
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "
|
|
191
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", useHttp, taskName);
|
|
95
192
|
await cleanup();
|
|
96
193
|
return;
|
|
97
194
|
}
|
|
98
195
|
console.log("Task confirmed by user.");
|
|
99
196
|
}
|
|
100
197
|
|
|
101
|
-
//
|
|
102
|
-
const guiEnv = getGuiEnv();
|
|
198
|
+
// Shared invocation context
|
|
199
|
+
const guiEnv = getPlatform().getGuiEnv();
|
|
103
200
|
const agent = getAgent(task.frontmatter.agent);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await publishPermissionResolved(nc, config, taskId, response, useHttp);
|
|
137
|
-
|
|
138
|
-
if (response === "aborted") {
|
|
139
|
-
console.log("Permission request aborted by user.");
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const newPerms = lastRequiredPermissions.filter(
|
|
144
|
-
(rp) => !task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
145
|
-
&& !transientPermissions.some((ep) => ep.name === rp.name),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
if (response === "granted_all") {
|
|
149
|
-
// Persist permissions to task frontmatter for all future runs
|
|
150
|
-
task.frontmatter.permissions = [...(task.frontmatter.permissions ?? []), ...newPerms];
|
|
151
|
-
writeTaskFile(taskDir, task);
|
|
152
|
-
} else {
|
|
153
|
-
// "granted" — allow for this run only
|
|
154
|
-
transientPermissions = [...transientPermissions, ...newPerms];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
console.log(`Permissions granted, retrying task ${taskId}...`);
|
|
158
|
-
retryPrompt = "Permissions granted, please continue.";
|
|
159
|
-
continue;
|
|
201
|
+
const ctx: InvocationContext = {
|
|
202
|
+
agent, task, taskDir, guiEnv, nc, config, taskId, useHttp,
|
|
203
|
+
transientPermissions: [],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (task.frontmatter.command) {
|
|
207
|
+
// Command-triggered mode
|
|
208
|
+
const result = await runCommandTriggeredMode(ctx);
|
|
209
|
+
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
210
|
+
const resultFileName = writeResult(
|
|
211
|
+
taskDir, taskName, taskSnapshotName, outcome,
|
|
212
|
+
startTime, result.endTime, result.output, [], [],
|
|
213
|
+
);
|
|
214
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
215
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
216
|
+
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
217
|
+
} else {
|
|
218
|
+
// Standard execution
|
|
219
|
+
const result = await invokeAgentWithRetry(ctx, task);
|
|
220
|
+
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
221
|
+
|
|
222
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, outcome, startTime, Date.now(), result.output, result.reportFiles, result.requiredPermissions);
|
|
223
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
224
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
225
|
+
|
|
226
|
+
if (result.reportFiles.length > 0) {
|
|
227
|
+
await publishHostEvent(nc, config, taskId, {
|
|
228
|
+
event_type: "report-generated",
|
|
229
|
+
name: taskName,
|
|
230
|
+
report_files: result.reportFiles,
|
|
231
|
+
running_state: outcome,
|
|
232
|
+
}, useHttp);
|
|
160
233
|
}
|
|
161
234
|
|
|
162
|
-
|
|
163
|
-
break;
|
|
235
|
+
console.log(`Task ${taskId} completed.`);
|
|
164
236
|
}
|
|
165
|
-
|
|
166
|
-
// Write result and history once after the loop
|
|
167
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, lastOutcome, startTime, lastEndTime, lastOutput, lastReportFiles, lastRequiredPermissions);
|
|
168
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
169
|
-
await publishTaskEvent(nc, config, taskDir, taskId, lastOutcome, useHttp, taskName);
|
|
170
|
-
console.log(`Task ${taskId} completed.`);
|
|
171
237
|
} catch (err) {
|
|
172
238
|
console.error(`Task ${taskId} failed:`, err);
|
|
173
239
|
const endTime = Date.now();
|
|
240
|
+
const outcome = resolveOutcome(taskDir, "failed");
|
|
174
241
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
175
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName,
|
|
242
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, outcome, startTime, endTime, errorMsg, [], []);
|
|
176
243
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
177
|
-
await publishTaskEvent(nc, config, taskDir, taskId,
|
|
244
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
178
245
|
process.exitCode = 1;
|
|
179
246
|
} finally {
|
|
180
247
|
await cleanup();
|
|
181
248
|
}
|
|
182
249
|
}
|
|
183
250
|
|
|
184
|
-
const
|
|
251
|
+
const MAX_QUEUE_SIZE = 100;
|
|
252
|
+
const MAX_LOG_ENTRIES = 1000;
|
|
253
|
+
/** Max input line length (chars). Long emails can take up to 200k chars. */
|
|
254
|
+
const MAX_LINE_LENGTH = 200_000;
|
|
185
255
|
|
|
186
256
|
/**
|
|
187
|
-
*
|
|
257
|
+
* Command-triggered execution mode.
|
|
258
|
+
*
|
|
259
|
+
* Spawns a long-running shell command and, for each line of stdout,
|
|
260
|
+
* invokes the agent CLI with the user's prompt augmented by that line.
|
|
261
|
+
* Processes lines sequentially with a bounded queue.
|
|
188
262
|
*/
|
|
189
|
-
async function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
263
|
+
async function runCommandTriggeredMode(
|
|
264
|
+
ctx: InvocationContext,
|
|
265
|
+
): Promise<{ outcome: TaskRunningState; endTime: number; output: string }> {
|
|
266
|
+
const commandStr = ctx.task.frontmatter.command!;
|
|
267
|
+
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
268
|
+
|
|
269
|
+
const child = spawnStreamingCommand(commandStr, {
|
|
270
|
+
cwd: ctx.taskDir,
|
|
271
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Stats
|
|
275
|
+
let linesProcessed = 0;
|
|
276
|
+
let invocationsSucceeded = 0;
|
|
277
|
+
let invocationsFailed = 0;
|
|
278
|
+
|
|
279
|
+
// Bounded queue for incoming lines
|
|
280
|
+
const lineQueue: string[] = [];
|
|
281
|
+
let processing = false;
|
|
282
|
+
let commandExited = false;
|
|
283
|
+
let resolveWhenDone: (() => void) | undefined;
|
|
284
|
+
|
|
285
|
+
// Rolling log of per-line agent outputs
|
|
286
|
+
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
287
|
+
function appendLog(line: string, agentOutput: string, outcome: string) {
|
|
288
|
+
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
289
|
+
fs.appendFileSync(logPath, entry, "utf-8");
|
|
197
290
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
291
|
+
// Trim log if too large (keep last MAX_LOG_ENTRIES entries)
|
|
292
|
+
try {
|
|
293
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
294
|
+
const entries = content.split("\n---\n").filter(Boolean);
|
|
295
|
+
if (entries.length > MAX_LOG_ENTRIES) {
|
|
296
|
+
const trimmed = entries.slice(-MAX_LOG_ENTRIES).join("\n---\n") + "\n---\n";
|
|
297
|
+
fs.writeFileSync(logPath, trimmed, "utf-8");
|
|
298
|
+
}
|
|
299
|
+
} catch { /* ignore trim errors */ }
|
|
201
300
|
}
|
|
202
301
|
|
|
203
|
-
|
|
302
|
+
async function processLine(line: string): Promise<void> {
|
|
303
|
+
linesProcessed++;
|
|
304
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
305
|
+
console.warn(`[command-triggered] Skipping line #${linesProcessed}: ${line.length} chars exceeds limit`);
|
|
306
|
+
invocationsFailed++;
|
|
307
|
+
appendLog(line.slice(0, 200) + "...(truncated)", "", "skipped");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
console.log(`[command-triggered] Processing line #${linesProcessed}: ${line}`);
|
|
311
|
+
|
|
312
|
+
const perLinePrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${line}`;
|
|
313
|
+
const perLineTask: ParsedTask = {
|
|
314
|
+
frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
|
|
315
|
+
body: "",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const result = await invokeAgentWithRetry(ctx, perLineTask);
|
|
319
|
+
if (result.outcome === "finished") {
|
|
320
|
+
invocationsSucceeded++;
|
|
321
|
+
} else {
|
|
322
|
+
invocationsFailed++;
|
|
323
|
+
}
|
|
324
|
+
appendLog(line, result.output, result.outcome);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function drainQueue(): Promise<void> {
|
|
328
|
+
if (processing) return;
|
|
329
|
+
processing = true;
|
|
204
330
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
331
|
+
while (lineQueue.length > 0) {
|
|
332
|
+
const line = lineQueue.shift()!;
|
|
333
|
+
await processLine(line);
|
|
334
|
+
}
|
|
335
|
+
} finally {
|
|
336
|
+
processing = false;
|
|
337
|
+
if (commandExited && lineQueue.length === 0 && resolveWhenDone) {
|
|
338
|
+
resolveWhenDone();
|
|
339
|
+
}
|
|
213
340
|
}
|
|
214
341
|
}
|
|
342
|
+
|
|
343
|
+
// Read stdout line by line
|
|
344
|
+
const rl = readline.createInterface({ input: child.stdout! });
|
|
345
|
+
rl.on("line", (line: string) => {
|
|
346
|
+
if (!line.trim()) return; // skip empty lines
|
|
347
|
+
if (lineQueue.length >= MAX_QUEUE_SIZE) {
|
|
348
|
+
console.warn(`[command-triggered] Queue full, dropping oldest line.`);
|
|
349
|
+
lineQueue.shift();
|
|
350
|
+
}
|
|
351
|
+
lineQueue.push(line);
|
|
352
|
+
drainQueue().catch((err) => {
|
|
353
|
+
console.error(`[command-triggered] Error processing line:`, err);
|
|
354
|
+
invocationsFailed++;
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Log stderr
|
|
359
|
+
child.stderr?.on("data", (d: Buffer) => process.stderr.write(d));
|
|
360
|
+
|
|
361
|
+
// Wait for command to exit
|
|
362
|
+
const exitCode = await new Promise<number | null>((resolve) => {
|
|
363
|
+
child.on("close", (code: number | null) => {
|
|
364
|
+
commandExited = true;
|
|
365
|
+
rl.close();
|
|
366
|
+
resolve(code);
|
|
367
|
+
});
|
|
368
|
+
child.on("error", (err: Error) => {
|
|
369
|
+
console.error(`[command-triggered] Command error:`, err);
|
|
370
|
+
commandExited = true;
|
|
371
|
+
rl.close();
|
|
372
|
+
resolve(1);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Wait for any remaining queued lines to finish processing
|
|
377
|
+
if (lineQueue.length > 0 || processing) {
|
|
378
|
+
await new Promise<void>((resolve) => {
|
|
379
|
+
resolveWhenDone = resolve;
|
|
380
|
+
drainQueue();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const endTime = Date.now();
|
|
385
|
+
const summary = [
|
|
386
|
+
`Command: ${commandStr}`,
|
|
387
|
+
`Exit code: ${exitCode}`,
|
|
388
|
+
`Lines processed: ${linesProcessed}`,
|
|
389
|
+
`Agent invocations succeeded: ${invocationsSucceeded}`,
|
|
390
|
+
`Agent invocations failed: ${invocationsFailed}`,
|
|
391
|
+
].join("\n");
|
|
392
|
+
|
|
393
|
+
// Command-triggered tasks run until the command exits — any exit is a normal finish.
|
|
394
|
+
const outcome: TaskRunningState = "finished";
|
|
395
|
+
return { outcome, endTime, output: summary };
|
|
215
396
|
}
|
|
216
397
|
|
|
217
398
|
async function publishTaskEvent(
|
|
@@ -274,11 +455,11 @@ async function requestPermission(
|
|
|
274
455
|
return new Promise<"granted" | "granted_all" | "aborted">((resolve) => {
|
|
275
456
|
const watcher = fs.watch(statusPath, () => {
|
|
276
457
|
const status = readTaskStatus(taskDir);
|
|
277
|
-
if (!status || status.user_input
|
|
458
|
+
if (!status || !status.user_input?.length) return;
|
|
278
459
|
watcher.close();
|
|
279
|
-
const response = status.user_input as "granted" | "granted_all" | "aborted";
|
|
460
|
+
const response = status.user_input[0] as "granted" | "granted_all" | "aborted";
|
|
280
461
|
writeTaskStatus(taskDir, {
|
|
281
|
-
running_state: response === "aborted" ? "
|
|
462
|
+
running_state: response === "aborted" ? "aborted" : "started",
|
|
282
463
|
time_stamp: Date.now(),
|
|
283
464
|
});
|
|
284
465
|
resolve(response);
|
|
@@ -300,6 +481,58 @@ async function publishPermissionResolved(
|
|
|
300
481
|
}, useHttp);
|
|
301
482
|
}
|
|
302
483
|
|
|
484
|
+
async function requestUserInput(
|
|
485
|
+
nc: NatsConnection | undefined,
|
|
486
|
+
config: HostConfig,
|
|
487
|
+
task: ParsedTask,
|
|
488
|
+
taskDir: string,
|
|
489
|
+
inputDescriptions: string[],
|
|
490
|
+
useHttp: boolean,
|
|
491
|
+
): Promise<string[] | "aborted"> {
|
|
492
|
+
const taskId = task.frontmatter.id;
|
|
493
|
+
const statusPath = path.join(taskDir, "status.json");
|
|
494
|
+
|
|
495
|
+
const currentStatus = readTaskStatus(taskDir)!;
|
|
496
|
+
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
497
|
+
|
|
498
|
+
await publishHostEvent(nc, config, taskId, {
|
|
499
|
+
event_type: "input-request",
|
|
500
|
+
host_id: config.hostId,
|
|
501
|
+
input_descriptions: inputDescriptions,
|
|
502
|
+
name: task.frontmatter.name,
|
|
503
|
+
}, useHttp);
|
|
504
|
+
|
|
505
|
+
return new Promise<string[] | "aborted">((resolve) => {
|
|
506
|
+
const watcher = fs.watch(statusPath, () => {
|
|
507
|
+
const status = readTaskStatus(taskDir);
|
|
508
|
+
if (!status || !status.user_input?.length) return;
|
|
509
|
+
watcher.close();
|
|
510
|
+
const response = status.user_input;
|
|
511
|
+
if (response.length === 1 && response[0] === "aborted") {
|
|
512
|
+
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
513
|
+
resolve("aborted");
|
|
514
|
+
} else {
|
|
515
|
+
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
516
|
+
resolve(response);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function publishInputResolved(
|
|
523
|
+
nc: NatsConnection | undefined,
|
|
524
|
+
config: HostConfig,
|
|
525
|
+
taskId: string,
|
|
526
|
+
status: "provided" | "aborted",
|
|
527
|
+
useHttp: boolean,
|
|
528
|
+
): Promise<void> {
|
|
529
|
+
await publishHostEvent(nc, config, taskId, {
|
|
530
|
+
event_type: "input-resolved",
|
|
531
|
+
host_id: config.hostId,
|
|
532
|
+
status,
|
|
533
|
+
}, useHttp);
|
|
534
|
+
}
|
|
535
|
+
|
|
303
536
|
async function requestConfirmation(
|
|
304
537
|
nc: NatsConnection | undefined,
|
|
305
538
|
config: HostConfig,
|
|
@@ -324,12 +557,12 @@ async function requestConfirmation(
|
|
|
324
557
|
return new Promise<boolean>((resolve) => {
|
|
325
558
|
const watcher = fs.watch(statusPath, () => {
|
|
326
559
|
const status = readTaskStatus(taskDir);
|
|
327
|
-
if (!status || status.user_input
|
|
560
|
+
if (!status || !status.user_input?.length) return; // still pending
|
|
328
561
|
watcher.close();
|
|
329
|
-
const confirmed = status.user_input === "confirmed";
|
|
562
|
+
const confirmed = status.user_input[0] === "confirmed";
|
|
330
563
|
// Clear pending_confirmation/user_input and update running_state
|
|
331
564
|
writeTaskStatus(taskDir, {
|
|
332
|
-
running_state: confirmed ? "
|
|
565
|
+
running_state: confirmed ? "started" : "aborted",
|
|
333
566
|
time_stamp: Date.now(),
|
|
334
567
|
});
|
|
335
568
|
resolve(confirmed);
|
|
@@ -373,27 +606,28 @@ function parsePermissions(output: string): RequiredPermission[] {
|
|
|
373
606
|
}
|
|
374
607
|
|
|
375
608
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
609
|
+
* Extract user input requests from agent output.
|
|
610
|
+
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
378
611
|
*/
|
|
379
|
-
function
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
612
|
+
function parseInputRequests(output: string): string[] {
|
|
613
|
+
const regex = new RegExp(`^\\${TASK_INPUT_PREFIX}\\s+(.+)$`, "gm");
|
|
614
|
+
const inputs: string[] = [];
|
|
615
|
+
let match;
|
|
616
|
+
while ((match = regex.exec(output)) !== null) {
|
|
617
|
+
const desc = match[1].trim();
|
|
618
|
+
if (desc) inputs.push(desc);
|
|
619
|
+
}
|
|
620
|
+
return inputs;
|
|
384
621
|
}
|
|
385
622
|
|
|
386
623
|
/**
|
|
387
|
-
*
|
|
624
|
+
* Parse the agent's output for success/failure markers.
|
|
625
|
+
* Falls back to "finished" if no marker is found.
|
|
388
626
|
*/
|
|
389
|
-
function
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
DISPLAY: ":0",
|
|
397
|
-
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
398
|
-
};
|
|
627
|
+
function parseTaskOutcome(output: string): TaskRunningState {
|
|
628
|
+
const lastChunk = output.slice(-500);
|
|
629
|
+
if (lastChunk.includes(TASK_FAILURE_MARKER)) return "failed";
|
|
630
|
+
if (lastChunk.includes(TASK_SUCCESS_MARKER)) return "finished";
|
|
631
|
+
return "finished";
|
|
399
632
|
}
|
|
633
|
+
|