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