palmier 1.1.23 → 1.1.25
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/DISCLAIMER.md +4 -2
- package/README.md +6 -1
- package/dist/agent-tools.js +3 -60
- package/dist/agents/agent-instructions.md +7 -5
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/agent.js +0 -5
- package/dist/agents/claude.js +0 -1
- package/dist/agents/shared-prompt.d.ts +3 -1
- package/dist/agents/shared-prompt.js +6 -3
- package/dist/agents/wizard.js +0 -1
- package/dist/commands/run.d.ts +2 -3
- package/dist/commands/run.js +144 -183
- package/dist/commands/serve.js +4 -2
- package/dist/pending-requests.d.ts +3 -5
- package/dist/pwa/assets/index-D64fpB5p.js +117 -0
- package/dist/pwa/assets/index-DWgTuvMe.css +1 -0
- package/dist/pwa/assets/{web-o_D8jqAS.js → web-Flj1nZsf.js} +1 -1
- package/dist/pwa/assets/{web-miCfAJBc.js → web-YFetWCPM.js} +1 -1
- package/dist/pwa/assets/{web-BQ0rMZ_X.js → web-YYfGpkCq.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.js +12 -5
- package/dist/task.d.ts +3 -0
- package/dist/task.js +29 -0
- package/dist/transports/http-transport.js +0 -37
- package/dist/types.d.ts +6 -9
- package/package.json +1 -1
- package/dist/agents/aider.d.ts +0 -2
- package/dist/agents/aider.js +0 -13
- package/dist/agents/cline.d.ts +0 -2
- package/dist/agents/cline.js +0 -14
- package/dist/pwa/assets/index-BPB3G_2J.css +0 -1
- package/dist/pwa/assets/index-De8hDN5I.js +0 -117
package/dist/commands/run.js
CHANGED
|
@@ -2,157 +2,116 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { spawnCommand } from "../spawn-command.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
-
import { parseTaskFile, taskDisplayName, getTaskDir,
|
|
5
|
+
import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, claimPendingRun, appendRunMessage, getRunDir, beginStreamingMessage, formatAgentModelComment, takeUserReply } from "../task.js";
|
|
6
6
|
import { getAgent } from "../agents/agent.js";
|
|
7
7
|
import { getPlatform } from "../platform/index.js";
|
|
8
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER,
|
|
8
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_NEED_INPUT_MARKER, AGENT_MODEL_PREFIX, getResumePrompt } from "../agents/shared-prompt.js";
|
|
9
9
|
import { extractFileLinks, resolveTaskFile } from "../task-files.js";
|
|
10
10
|
import { publishHostEvent } from "../events.js";
|
|
11
11
|
/**
|
|
12
|
-
* Invoke the agent CLI in a continuation loop to handle permission requests.
|
|
13
12
|
* `invokeTask` is the ParsedTask whose prompt is passed to the agent (for
|
|
14
13
|
* triggered tasks this is the per-trigger augmented task).
|
|
15
14
|
*/
|
|
16
|
-
async function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
notifyPending =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function ensureWriter(stream) {
|
|
38
|
-
if (writer && activeStream === stream)
|
|
39
|
-
return writer;
|
|
40
|
-
if (writer)
|
|
41
|
-
writer.end();
|
|
42
|
-
writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), stream);
|
|
43
|
-
activeStream = stream;
|
|
15
|
+
async function invokeAgent(ctx, invokeTask, followupPrompt) {
|
|
16
|
+
let writer;
|
|
17
|
+
let activeStream;
|
|
18
|
+
const lineBufs = { stdout: "", stderr: "" };
|
|
19
|
+
let suppressedStderr = "";
|
|
20
|
+
let notifyPending = false;
|
|
21
|
+
let notifyTimer;
|
|
22
|
+
let agentModel;
|
|
23
|
+
let agentModelWritten = false;
|
|
24
|
+
const captureCliModel = createCliModelCapture(ctx.agent);
|
|
25
|
+
function throttledNotify() {
|
|
26
|
+
if (notifyPending)
|
|
27
|
+
return;
|
|
28
|
+
notifyPending = true;
|
|
29
|
+
notifyTimer = setTimeout(() => {
|
|
30
|
+
notifyPending = false;
|
|
31
|
+
publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
32
|
+
}, 500);
|
|
33
|
+
}
|
|
34
|
+
function ensureWriter(stream) {
|
|
35
|
+
if (writer && activeStream === stream)
|
|
44
36
|
return writer;
|
|
37
|
+
if (writer)
|
|
38
|
+
writer.end();
|
|
39
|
+
writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), stream);
|
|
40
|
+
activeStream = stream;
|
|
41
|
+
return writer;
|
|
42
|
+
}
|
|
43
|
+
function flushAgentModel() {
|
|
44
|
+
if (!agentModel || agentModelWritten || !writer)
|
|
45
|
+
return;
|
|
46
|
+
writer.write(formatAgentModelComment(agentModel));
|
|
47
|
+
agentModelWritten = true;
|
|
48
|
+
throttledNotify();
|
|
49
|
+
}
|
|
50
|
+
function onStderr(chunk) {
|
|
51
|
+
if (ctx.agent.suppressStdErr)
|
|
52
|
+
suppressedStderr += chunk;
|
|
53
|
+
else
|
|
54
|
+
emit("stderr", chunk);
|
|
55
|
+
const cliModel = captureCliModel(chunk);
|
|
56
|
+
if (cliModel && !agentModelWritten)
|
|
57
|
+
agentModel = cliModel;
|
|
58
|
+
flushAgentModel();
|
|
59
|
+
}
|
|
60
|
+
function emit(stream, chunk) {
|
|
61
|
+
lineBufs[stream] += chunk;
|
|
62
|
+
const lines = lineBufs[stream].split("\n");
|
|
63
|
+
lineBufs[stream] = lines.pop() ?? "";
|
|
64
|
+
if (stream === "stdout" && !agentModel) {
|
|
65
|
+
agentModel = parseAgentModel(lines.join("\n"));
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
writer.write(formatAgentModelComment(agentModel));
|
|
50
|
-
agentModelWritten = true;
|
|
67
|
+
const filtered = lines.filter((l) => !l.startsWith("[PALMIER"));
|
|
68
|
+
if (filtered.length > 0) {
|
|
69
|
+
ensureWriter(stream).write(filtered.join("\n") + "\n");
|
|
51
70
|
throttledNotify();
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (cliModel && !agentModelWritten)
|
|
60
|
-
agentModel = cliModel;
|
|
61
|
-
flushAgentModel();
|
|
62
|
-
}
|
|
63
|
-
function emit(stream, chunk) {
|
|
64
|
-
lineBufs[stream] += chunk;
|
|
65
|
-
const lines = lineBufs[stream].split("\n");
|
|
66
|
-
lineBufs[stream] = lines.pop() ?? "";
|
|
67
|
-
if (stream === "stdout" && !agentModel) {
|
|
68
|
-
agentModel = parseAgentModel(lines.join("\n"));
|
|
69
|
-
}
|
|
70
|
-
const filtered = lines.filter((l) => !l.startsWith("[PALMIER"));
|
|
71
|
-
if (filtered.length > 0) {
|
|
72
|
-
ensureWriter(stream).write(filtered.join("\n") + "\n");
|
|
73
|
-
throttledNotify();
|
|
74
|
-
}
|
|
75
|
-
flushAgentModel();
|
|
76
|
-
}
|
|
77
|
-
function flushTrailingLine(stream) {
|
|
78
|
-
const trailing = lineBufs[stream];
|
|
79
|
-
if (trailing && !trailing.startsWith("[PALMIER")) {
|
|
80
|
-
ensureWriter(stream).write(trailing);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask);
|
|
84
|
-
const command = ctx.agent.command;
|
|
85
|
-
const runDir = getRunDir(ctx.taskDir, ctx.runId);
|
|
86
|
-
if (files) {
|
|
87
|
-
for (const f of files)
|
|
88
|
-
fs.writeFileSync(path.join(runDir, f.path), f.content, "utf-8");
|
|
72
|
+
flushAgentModel();
|
|
73
|
+
}
|
|
74
|
+
function flushTrailingLine(stream) {
|
|
75
|
+
const trailing = lineBufs[stream];
|
|
76
|
+
if (trailing && !trailing.startsWith("[PALMIER")) {
|
|
77
|
+
ensureWriter(stream).write(trailing);
|
|
89
78
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
79
|
+
}
|
|
80
|
+
const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt);
|
|
81
|
+
const command = ctx.agent.command;
|
|
82
|
+
const runDir = getRunDir(ctx.taskDir, ctx.runId);
|
|
83
|
+
if (files) {
|
|
84
|
+
for (const f of files)
|
|
85
|
+
fs.writeFileSync(path.join(runDir, f.path), f.content, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
const result = await spawnCommand(command, args, {
|
|
88
|
+
cwd: runDir,
|
|
89
|
+
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: runDir, PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
|
|
90
|
+
echoStdout: true,
|
|
91
|
+
resolveOnFailure: true,
|
|
92
|
+
stdin,
|
|
93
|
+
onStdout: (chunk) => emit("stdout", chunk),
|
|
94
|
+
onStderr,
|
|
95
|
+
});
|
|
96
|
+
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
97
|
+
const linkedFiles = listLinkedTaskFiles(ctx.taskDir, ctx.runId, result.output);
|
|
98
|
+
flushTrailingLine("stdout");
|
|
99
|
+
if (outcome === "failed")
|
|
100
|
+
emit("stderr", suppressedStderr);
|
|
101
|
+
flushTrailingLine("stderr");
|
|
102
|
+
if (notifyTimer)
|
|
103
|
+
clearTimeout(notifyTimer);
|
|
104
|
+
writer?.end();
|
|
105
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
106
|
+
if (linkedFiles.length > 0) {
|
|
107
|
+
await publishHostEvent(ctx.taskId, {
|
|
108
|
+
event_type: "report-generated",
|
|
109
|
+
run_id: ctx.runId,
|
|
110
|
+
name: taskDisplayName(ctx.task.frontmatter),
|
|
111
|
+
report_files: linkedFiles,
|
|
98
112
|
});
|
|
99
|
-
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
100
|
-
const linkedFiles = listLinkedTaskFiles(ctx.taskDir, ctx.runId, result.output);
|
|
101
|
-
const requiredPermissions = parsePermissions(result.output);
|
|
102
|
-
flushTrailingLine("stdout");
|
|
103
|
-
if (outcome === "failed")
|
|
104
|
-
emit("stderr", suppressedStderr);
|
|
105
|
-
flushTrailingLine("stderr");
|
|
106
|
-
if (requiredPermissions.length > 0) {
|
|
107
|
-
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
108
|
-
ensureWriter("stdout").write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
109
|
-
}
|
|
110
|
-
if (notifyTimer)
|
|
111
|
-
clearTimeout(notifyTimer);
|
|
112
|
-
writer?.end();
|
|
113
|
-
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
114
|
-
if (linkedFiles.length > 0) {
|
|
115
|
-
await publishHostEvent(ctx.taskId, {
|
|
116
|
-
event_type: "report-generated",
|
|
117
|
-
run_id: ctx.runId,
|
|
118
|
-
name: taskDisplayName(ctx.task.frontmatter),
|
|
119
|
-
report_files: linkedFiles,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (requiredPermissions.length > 0) {
|
|
123
|
-
const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
124
|
-
if (response === "aborted") {
|
|
125
|
-
await appendAndNotify(ctx, {
|
|
126
|
-
role: "user",
|
|
127
|
-
time: Date.now(),
|
|
128
|
-
content: "Deny & Abort Task",
|
|
129
|
-
type: "permission",
|
|
130
|
-
});
|
|
131
|
-
return { outcome: "failed" };
|
|
132
|
-
}
|
|
133
|
-
const newPerms = requiredPermissions.filter((rp) => !ctx.task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
134
|
-
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name));
|
|
135
|
-
await appendAndNotify(ctx, {
|
|
136
|
-
role: "user",
|
|
137
|
-
time: Date.now(),
|
|
138
|
-
content: response === "granted_all" ? "Allow Always" : "Allow Once",
|
|
139
|
-
type: "permission",
|
|
140
|
-
});
|
|
141
|
-
if (response === "granted_all") {
|
|
142
|
-
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
143
|
-
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
144
|
-
writeTaskFile(ctx.taskDir, ctx.task);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
148
|
-
}
|
|
149
|
-
// Retry with the new permissions if the agent failed.
|
|
150
|
-
if (outcome === "failed") {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { outcome };
|
|
155
113
|
}
|
|
114
|
+
return { outcome };
|
|
156
115
|
}
|
|
157
116
|
export function stripPalmierMarkers(output) {
|
|
158
117
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
@@ -171,6 +130,47 @@ function resolveOutcome(taskDir, outcome) {
|
|
|
171
130
|
return "aborted";
|
|
172
131
|
return outcome;
|
|
173
132
|
}
|
|
133
|
+
const USER_REPLY_POLL_MS = 500;
|
|
134
|
+
// A recurring task can't fire again while this run is waiting, so an unanswered question can't hold it forever.
|
|
135
|
+
const USER_REPLY_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
136
|
+
async function waitForUserReply(runDir) {
|
|
137
|
+
const deadline = Date.now() + USER_REPLY_TIMEOUT_MS;
|
|
138
|
+
while (Date.now() < deadline) {
|
|
139
|
+
const reply = takeUserReply(runDir);
|
|
140
|
+
if (reply)
|
|
141
|
+
return reply;
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, USER_REPLY_POLL_MS));
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Only the agent CLI exits while the user answers. This process stays alive because
|
|
148
|
+
* the OS scheduler would otherwise clean up what the agent left running, like its browser.
|
|
149
|
+
*/
|
|
150
|
+
async function invokeAgentUntilDone(ctx, invokeTask) {
|
|
151
|
+
const taskName = taskDisplayName(ctx.task.frontmatter);
|
|
152
|
+
const runDir = getRunDir(ctx.taskDir, ctx.runId);
|
|
153
|
+
let result = await invokeAgent(ctx, invokeTask);
|
|
154
|
+
while (resolveOutcome(ctx.taskDir, result.outcome) === "needs_input") {
|
|
155
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "needs_input" });
|
|
156
|
+
await publishTaskEvent(ctx.taskDir, ctx.taskId, "needs_input", taskName, ctx.runId);
|
|
157
|
+
const reply = await waitForUserReply(runDir);
|
|
158
|
+
if (!reply) {
|
|
159
|
+
await appendAndNotify(ctx, {
|
|
160
|
+
role: "assistant",
|
|
161
|
+
time: Date.now(),
|
|
162
|
+
content: "The task stopped because no reply arrived within 24 hours.",
|
|
163
|
+
});
|
|
164
|
+
return { outcome: "failed" };
|
|
165
|
+
}
|
|
166
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "user", time: Date.now(), content: reply, type: "input" });
|
|
167
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
168
|
+
await publishTaskEvent(ctx.taskDir, ctx.taskId, "started", taskName, ctx.runId);
|
|
169
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
170
|
+
result = await invokeAgent(ctx, invokeTask, getResumePrompt(reply));
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
174
|
export async function runCommand(taskId) {
|
|
175
175
|
const config = loadConfig();
|
|
176
176
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
@@ -217,7 +217,6 @@ export async function runCommand(taskId) {
|
|
|
217
217
|
const agent = getAgent(task.frontmatter.agent);
|
|
218
218
|
const ctx = {
|
|
219
219
|
agent, task, taskDir, runId, guiEnv, config, taskId,
|
|
220
|
-
transientPermissions: [],
|
|
221
220
|
};
|
|
222
221
|
// Command-output and on_new_* tasks share the same trigger machinery: the
|
|
223
222
|
// daemon owns the trigger source (the shell command's stdout / a NATS
|
|
@@ -238,7 +237,7 @@ export async function runCommand(taskId) {
|
|
|
238
237
|
time: Date.now(),
|
|
239
238
|
content: task.frontmatter.user_prompt,
|
|
240
239
|
});
|
|
241
|
-
const result = await
|
|
240
|
+
const result = await invokeAgentUntilDone(ctx, task);
|
|
242
241
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
243
242
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
244
243
|
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
@@ -299,7 +298,7 @@ async function runEventTriggeredMode(ctx) {
|
|
|
299
298
|
const perEventTask = {
|
|
300
299
|
frontmatter: { ...ctx.task.frontmatter, user_prompt: perEventPrompt },
|
|
301
300
|
};
|
|
302
|
-
const result = await
|
|
301
|
+
const result = await invokeAgentUntilDone(ctx, perEventTask);
|
|
303
302
|
lastOutcome = result.outcome;
|
|
304
303
|
}
|
|
305
304
|
}
|
|
@@ -315,7 +314,8 @@ async function publishTaskEvent(taskDir, taskId, eventType, taskName, runId) {
|
|
|
315
314
|
writeTaskStatus(taskDir, {
|
|
316
315
|
running_state: eventType,
|
|
317
316
|
time_stamp: Date.now(),
|
|
318
|
-
...(eventType === "started" ? { pid: process.pid } : {}),
|
|
317
|
+
...(eventType === "started" || eventType === "needs_input" ? { pid: process.pid } : {}),
|
|
318
|
+
...(eventType === "needs_input" ? { run_id: runId } : {}),
|
|
319
319
|
});
|
|
320
320
|
const payload = { event_type: "running-state", running_state: eventType };
|
|
321
321
|
if (taskName)
|
|
@@ -324,28 +324,6 @@ async function publishTaskEvent(taskDir, taskId, eventType, taskName, runId) {
|
|
|
324
324
|
payload.run_id = runId;
|
|
325
325
|
await publishHostEvent(taskId, payload);
|
|
326
326
|
}
|
|
327
|
-
async function requestPermission(config, task, taskDir, requiredPermissions) {
|
|
328
|
-
const port = config.httpPort ?? 7256;
|
|
329
|
-
const res = await fetch(`http://localhost:${port}/request-permission`, {
|
|
330
|
-
method: "POST",
|
|
331
|
-
headers: { "Content-Type": "application/json" },
|
|
332
|
-
body: JSON.stringify({
|
|
333
|
-
taskId: task.frontmatter.id,
|
|
334
|
-
taskName: taskDisplayName(task.frontmatter),
|
|
335
|
-
permissions: requiredPermissions,
|
|
336
|
-
}),
|
|
337
|
-
});
|
|
338
|
-
const body = await res.json();
|
|
339
|
-
const response = body.response;
|
|
340
|
-
if (!response || !["granted", "granted_all", "aborted"].includes(response)) {
|
|
341
|
-
throw new Error(`Permission request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
|
|
342
|
-
}
|
|
343
|
-
writeTaskStatus(taskDir, {
|
|
344
|
-
running_state: response === "aborted" ? "aborted" : "started",
|
|
345
|
-
time_stamp: Date.now(),
|
|
346
|
-
});
|
|
347
|
-
return response;
|
|
348
|
-
}
|
|
349
327
|
async function requestConfirmation(config, task, taskDir) {
|
|
350
328
|
const port = config.httpPort ?? 7256;
|
|
351
329
|
const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
|
|
@@ -376,25 +354,6 @@ export function listLinkedTaskFiles(taskDir, runId, output) {
|
|
|
376
354
|
}
|
|
377
355
|
});
|
|
378
356
|
}
|
|
379
|
-
export function parsePermissions(output) {
|
|
380
|
-
const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
|
|
381
|
-
const perms = [];
|
|
382
|
-
let match;
|
|
383
|
-
while ((match = regex.exec(output)) !== null) {
|
|
384
|
-
const raw = match[1].trim();
|
|
385
|
-
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
|
|
386
|
-
if (raw.startsWith("<"))
|
|
387
|
-
continue;
|
|
388
|
-
const sep = raw.indexOf("|");
|
|
389
|
-
if (sep !== -1) {
|
|
390
|
-
perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
perms.push({ name: raw, description: "" });
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return perms;
|
|
397
|
-
}
|
|
398
357
|
export function parseAgentModel(output) {
|
|
399
358
|
const regex = new RegExp(`^\\${AGENT_MODEL_PREFIX}[ \\t]+(.+)$`, "gm");
|
|
400
359
|
let match;
|
|
@@ -427,10 +386,10 @@ function sanitizeAgentModel(raw) {
|
|
|
427
386
|
return undefined;
|
|
428
387
|
return model;
|
|
429
388
|
}
|
|
430
|
-
/** Falls back to "finished" if no
|
|
389
|
+
/** Falls back to "finished" if no outcome marker is found. */
|
|
431
390
|
export function parseTaskOutcome(output) {
|
|
432
391
|
const lastChunk = output.slice(-500);
|
|
433
|
-
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
|
|
392
|
+
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$|^\\${TASK_NEED_INPUT_MARKER}$`, "gm");
|
|
434
393
|
let last = null;
|
|
435
394
|
let match;
|
|
436
395
|
while ((match = regex.exec(lastChunk)) !== null) {
|
|
@@ -438,6 +397,8 @@ export function parseTaskOutcome(output) {
|
|
|
438
397
|
}
|
|
439
398
|
if (last === TASK_FAILURE_MARKER)
|
|
440
399
|
return "failed";
|
|
400
|
+
if (last === TASK_NEED_INPUT_MARKER)
|
|
401
|
+
return "needs_input";
|
|
441
402
|
if (last === TASK_SUCCESS_MARKER)
|
|
442
403
|
return "finished";
|
|
443
404
|
return "finished";
|
package/dist/commands/serve.js
CHANGED
|
@@ -42,7 +42,9 @@ async function checkStaleTasks(config) {
|
|
|
42
42
|
catch {
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Follow-up turns also end in needs_input, but no process is held open for them.
|
|
46
|
+
const isWaitingRun = status.running_state === "needs_input" && status.pid !== undefined;
|
|
47
|
+
if ((status.running_state === "started" || isWaitingRun) && !platform.isTaskRunning(taskId)) {
|
|
46
48
|
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
47
49
|
const endTime = Date.now();
|
|
48
50
|
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
@@ -64,7 +66,7 @@ async function checkStaleTasks(config) {
|
|
|
64
66
|
name: taskDisplayName(task.frontmatter) || taskId,
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
|
-
if (task.frontmatter.one_off && status.running_state !== "started") {
|
|
69
|
+
if (task.frontmatter.one_off && status.running_state !== "started" && !isWaitingRun) {
|
|
68
70
|
try {
|
|
69
71
|
platform.removeTaskTimer(taskId);
|
|
70
72
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type { RequiredPermission } from "./types.js";
|
|
2
1
|
export interface PendingRequestMeta {
|
|
3
2
|
/** The task the request belongs to. */
|
|
4
3
|
session_id?: string;
|
|
5
|
-
/** Task name, set for permission requests. */
|
|
6
4
|
session_name?: string;
|
|
7
5
|
description?: string;
|
|
8
6
|
input_questions?: string[];
|
|
@@ -10,10 +8,10 @@ export interface PendingRequestMeta {
|
|
|
10
8
|
secret?: boolean;
|
|
11
9
|
}
|
|
12
10
|
export interface PendingRequest {
|
|
13
|
-
type: "confirmation" | "
|
|
11
|
+
type: "confirmation" | "input" | "choice";
|
|
14
12
|
resolve: (value: string[]) => void;
|
|
15
|
-
/**
|
|
16
|
-
params?:
|
|
13
|
+
/** Input descriptions (for 'input') or option labels (for 'choice'). */
|
|
14
|
+
params?: string[];
|
|
17
15
|
/** Display context for PWAs that connect while this request is already open. */
|
|
18
16
|
meta?: PendingRequestMeta;
|
|
19
17
|
}
|