palmier 0.5.1 → 0.5.2
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/dist/agents/agent-instructions.md +7 -11
- package/dist/agents/agent.d.ts +8 -3
- package/dist/agents/agent.js +7 -1
- package/dist/agents/claude.d.ts +2 -1
- package/dist/agents/claude.js +10 -5
- package/dist/agents/codex.d.ts +2 -1
- package/dist/agents/codex.js +10 -6
- package/dist/agents/copilot.d.ts +2 -1
- package/dist/agents/copilot.js +10 -3
- package/dist/agents/gemini.d.ts +2 -1
- package/dist/agents/gemini.js +11 -7
- package/dist/agents/kimi.d.ts +9 -0
- package/dist/agents/kimi.js +35 -0
- package/dist/agents/openclaw.d.ts +2 -1
- package/dist/agents/openclaw.js +3 -1
- package/dist/agents/qwen.d.ts +9 -0
- package/dist/agents/qwen.js +32 -0
- package/dist/agents/shared-prompt.d.ts +1 -1
- package/dist/agents/shared-prompt.js +6 -2
- package/dist/commands/run.js +12 -3
- package/dist/rpc-handler.js +7 -3
- package/dist/task.d.ts +13 -3
- package/dist/task.js +39 -7
- package/dist/transports/http-transport.js +23 -6
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +7 -11
- package/src/agents/agent.ts +16 -4
- package/src/agents/claude.ts +11 -6
- package/src/agents/codex.ts +11 -7
- package/src/agents/copilot.ts +10 -4
- package/src/agents/gemini.ts +12 -8
- package/src/agents/kimi.ts +37 -0
- package/src/agents/openclaw.ts +4 -2
- package/src/agents/qwen.ts +34 -0
- package/src/agents/shared-prompt.ts +6 -2
- package/src/commands/run.ts +14 -3
- package/src/rpc-handler.ts +9 -3
- package/src/task.ts +43 -8
- package/src/transports/http-transport.ts +25 -6
- package/src/types.ts +1 -0
- package/test/agent-instructions.test.ts +31 -0
|
@@ -3,7 +3,8 @@ import * as os from "os";
|
|
|
3
3
|
import { StringCodec } from "nats";
|
|
4
4
|
import { validateSession, addSession } from "../session-store.js";
|
|
5
5
|
import { registerPending } from "../pending-requests.js";
|
|
6
|
-
import
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
7
8
|
const PWA_ORIGIN = "https://app.palmier.me";
|
|
8
9
|
const assetCache = new Map();
|
|
9
10
|
/** Paths currently being fetched (dedup concurrent requests). */
|
|
@@ -78,6 +79,18 @@ export function detectLanIp() {
|
|
|
78
79
|
}
|
|
79
80
|
return "127.0.0.1";
|
|
80
81
|
}
|
|
82
|
+
/** Find the latest (highest-numbered) run directory for a task. */
|
|
83
|
+
function findLatestRunId(taskDir) {
|
|
84
|
+
try {
|
|
85
|
+
const dirs = fs.readdirSync(taskDir)
|
|
86
|
+
.filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
|
|
87
|
+
.sort();
|
|
88
|
+
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
81
94
|
/**
|
|
82
95
|
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
83
96
|
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
@@ -242,6 +255,8 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
242
255
|
}
|
|
243
256
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
244
257
|
const task = parseTaskFile(taskDir);
|
|
258
|
+
// Resolve runId: use provided value, otherwise find the latest run directory
|
|
259
|
+
const effectiveRunId = runId ?? findLatestRunId(taskDir);
|
|
245
260
|
const pendingPromise = registerPending(taskId, "input", descriptions);
|
|
246
261
|
await publishEvent(taskId, {
|
|
247
262
|
event_type: "input-request",
|
|
@@ -250,18 +265,20 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
250
265
|
name: task.frontmatter.name,
|
|
251
266
|
});
|
|
252
267
|
const response = await pendingPromise;
|
|
268
|
+
const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
|
|
253
269
|
if (response.length === 1 && response[0] === "aborted") {
|
|
254
270
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
|
|
255
|
-
if (
|
|
256
|
-
|
|
271
|
+
if (effectiveRunId) {
|
|
272
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
|
|
273
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
257
274
|
}
|
|
258
275
|
sendJson(res, 200, { aborted: true });
|
|
259
276
|
}
|
|
260
277
|
else {
|
|
261
278
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
279
|
+
if (effectiveRunId) {
|
|
280
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
|
|
281
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
265
282
|
}
|
|
266
283
|
sendJson(res, 200, { values: response });
|
|
267
284
|
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -2,23 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
|
|
|
2
2
|
|
|
3
3
|
## Reporting Output
|
|
4
4
|
|
|
5
|
-
If you generate report or output files, print each file path on its own line
|
|
6
|
-
[PALMIER_REPORT]
|
|
7
|
-
[PALMIER_REPORT] summary.md
|
|
5
|
+
If you generate report or output files, print each file path on its own line using this exact format (no code block):
|
|
6
|
+
`[PALMIER_REPORT] <filename>`
|
|
8
7
|
|
|
9
8
|
## Completion
|
|
10
9
|
|
|
11
|
-
When you are done, output exactly one of these markers as the very last line:
|
|
12
|
-
- Success: [PALMIER_TASK_SUCCESS]
|
|
13
|
-
- Failure: [PALMIER_TASK_FAILURE]
|
|
14
|
-
Do not wrap them in code blocks or add text on the same line.
|
|
10
|
+
When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
|
|
11
|
+
- Success: `[PALMIER_TASK_SUCCESS]`
|
|
12
|
+
- Failure: `[PALMIER_TASK_FAILURE]`
|
|
15
13
|
|
|
16
14
|
## Permissions
|
|
17
15
|
|
|
18
|
-
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line
|
|
19
|
-
[PALMIER_PERMISSION]
|
|
20
|
-
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
21
|
-
[PALMIER_PERMISSION] Write | Write generated output files
|
|
16
|
+
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format (no code block):
|
|
17
|
+
`[PALMIER_PERMISSION] <tool_name> | <description>`
|
|
22
18
|
|
|
23
19
|
## HTTP Endpoints
|
|
24
20
|
|
package/src/agents/agent.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { GeminiAgent } from "./gemini.js";
|
|
|
4
4
|
import { CodexAgent } from "./codex.js";
|
|
5
5
|
import { OpenClawAgent } from "./openclaw.js";
|
|
6
6
|
import { CopilotAgent } from "./copilot.js";
|
|
7
|
+
import { QwenAgent } from "./qwen.js";
|
|
8
|
+
import { KimiAgent } from "./kimi.js";
|
|
7
9
|
|
|
8
10
|
export interface CommandLine {
|
|
9
11
|
command: string;
|
|
@@ -21,9 +23,14 @@ export interface AgentTool {
|
|
|
21
23
|
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
22
24
|
|
|
23
25
|
/** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
|
|
24
|
-
* and treat it as a continuation of the original run (reuse the same session, etc).
|
|
25
|
-
* permissions granted for this run only
|
|
26
|
-
|
|
26
|
+
* and treat it as a continuation of the original run (reuse the same session, etc).
|
|
27
|
+
* extraPermissions: pass an array of RequiredPermission for transient permissions granted for this run only,
|
|
28
|
+
* or pass `"yolo"` to enable yolo mode (auto-approve all tools, skip permission instructions). */
|
|
29
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine;
|
|
30
|
+
|
|
31
|
+
/** Whether this agent supports permission overrides (e.g. --allowedTools).
|
|
32
|
+
* If false, the permissions section is omitted from agent instructions. */
|
|
33
|
+
supportsPermissions: boolean;
|
|
27
34
|
|
|
28
35
|
/** Detect whether the agent CLI is available and perform any agent-specific
|
|
29
36
|
* initialization. Returns true if the agent was detected and initialized successfully. */
|
|
@@ -36,6 +43,8 @@ const agentRegistry: Record<string, AgentTool> = {
|
|
|
36
43
|
codex: new CodexAgent(),
|
|
37
44
|
openclaw: new OpenClawAgent(),
|
|
38
45
|
copilot: new CopilotAgent(),
|
|
46
|
+
qwen: new QwenAgent(),
|
|
47
|
+
kimi: new KimiAgent(),
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
const agentLabels: Record<string, string> = {
|
|
@@ -44,11 +53,14 @@ const agentLabels: Record<string, string> = {
|
|
|
44
53
|
codex: "Codex CLI",
|
|
45
54
|
openclaw: "OpenClaw",
|
|
46
55
|
copilot: "Copilot CLI",
|
|
56
|
+
qwen: "Qwen Code",
|
|
57
|
+
kimi: "Kimi Code",
|
|
47
58
|
};
|
|
48
59
|
|
|
49
60
|
export interface DetectedAgent {
|
|
50
61
|
key: string;
|
|
51
62
|
label: string;
|
|
63
|
+
supportsPermissions: boolean;
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
export async function detectAgents(): Promise<DetectedAgent[]> {
|
|
@@ -56,7 +68,7 @@ export async function detectAgents(): Promise<DetectedAgent[]> {
|
|
|
56
68
|
for (const [key, agent] of Object.entries(agentRegistry)) {
|
|
57
69
|
const label = agentLabels[key] ?? key;
|
|
58
70
|
const ok = await agent.init();
|
|
59
|
-
if (ok) detected.push({ key, label });
|
|
71
|
+
if (ok) detected.push({ key, label, supportsPermissions: agent.supportsPermissions });
|
|
60
72
|
}
|
|
61
73
|
return detected;
|
|
62
74
|
}
|
package/src/agents/claude.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class ClaudeAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = true;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "claude",
|
|
@@ -12,13 +13,17 @@ export class ClaudeAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
if (!yolo) {
|
|
22
|
+
args.push("--allowedTools", "WebFetch");
|
|
23
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
24
|
+
for (const p of allPerms) {
|
|
25
|
+
args.push("--allowedTools", p.name);
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
if (followupPrompt) {args.push("-c");} // continue mode for followups
|
package/src/agents/codex.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class CodexAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = true;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "codex",
|
|
@@ -12,15 +13,18 @@ export class CodexAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
17
19
|
// Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
|
|
18
|
-
const args = ["exec", "--
|
|
20
|
+
const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (!yolo) {
|
|
23
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
24
|
+
for (const p of allPerms) {
|
|
25
|
+
args.push("--config");
|
|
26
|
+
args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
|
|
26
30
|
args.push("-"); // read prompt from stdin
|
package/src/agents/copilot.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class CopilotAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "copilot",
|
|
@@ -12,12 +13,17 @@ export class CopilotAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
17
19
|
const args = ["-p", prompt];
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--yolo");
|
|
23
|
+
} else {
|
|
24
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
25
|
+
args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
|
|
26
|
+
}
|
|
21
27
|
if (followupPrompt) { args.push("--continue"); }
|
|
22
28
|
return { command: "copilot", args};
|
|
23
29
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class GeminiAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = true;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "gemini",
|
|
@@ -12,21 +13,24 @@ export class GeminiAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if (!yolo) {
|
|
22
|
+
const tools = ["run_shell_command", "web_fetch"];
|
|
23
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
21
24
|
for (const p of allPerms) {
|
|
22
|
-
|
|
25
|
+
tools.push(p.name);
|
|
23
26
|
}
|
|
27
|
+
args.push("--allowed-tools", tools.join(","));
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
27
|
-
args.push("--prompt",
|
|
31
|
+
args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
|
|
28
32
|
|
|
29
|
-
return { command: "gemini", args };
|
|
33
|
+
return { command: "gemini", args, stdin: prompt };
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
async init(): Promise<boolean> {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class KimiAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "kimi",
|
|
12
|
+
args: ["-p", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = [];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--yolo");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) { args.push("--continue"); }
|
|
25
|
+
args.push("-p", prompt);
|
|
26
|
+
return { command: "kimi", args };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init(): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
execSync("kimi --version", { stdio: "ignore", shell: SHELL });
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentTool, CommandLine } from "./agent.js";
|
|
|
4
4
|
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
6
|
export class OpenClawAgent implements AgentTool {
|
|
7
|
+
supportsPermissions = false;
|
|
7
8
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
9
|
return {
|
|
9
10
|
command: "openclaw",
|
|
@@ -11,8 +12,9 @@ export class OpenClawAgent implements AgentTool {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
-
const
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
16
|
+
const yolo = extraPermissions === "yolo";
|
|
17
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
16
18
|
// OpenClaw does not support stdin as prompt.
|
|
17
19
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
18
20
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class QwenAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "qwen",
|
|
12
|
+
args: ["-p", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
|
|
20
|
+
|
|
21
|
+
if (followupPrompt) { args.push("-c"); }
|
|
22
|
+
args.push("-p", prompt);
|
|
23
|
+
return { command: "qwen", args };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async init(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
execSync("qwen --version", { stdio: "ignore", shell: SHELL });
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -13,11 +13,15 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
13
13
|
/**
|
|
14
14
|
* Agent instructions with the serve daemon's HTTP port and task ID baked in.
|
|
15
15
|
*/
|
|
16
|
-
export function getAgentInstructions(taskId: string): string {
|
|
16
|
+
export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
17
17
|
const port = loadConfig().httpPort ?? 7400;
|
|
18
|
-
|
|
18
|
+
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
19
19
|
.replace(/\{\{PORT\}\}/g, String(port))
|
|
20
20
|
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
21
|
+
if (skipPermissions) {
|
|
22
|
+
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
23
|
+
}
|
|
24
|
+
return instructions;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
package/src/commands/run.ts
CHANGED
|
@@ -62,7 +62,12 @@ async function invokeAgentWithRetries(
|
|
|
62
62
|
}, 500);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
|
|
65
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
|
|
66
|
+
invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
|
|
67
|
+
);
|
|
68
|
+
const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
|
|
69
|
+
const displayArgs = args.map((a) => truncate(a));
|
|
70
|
+
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
66
71
|
const result = await spawnCommand(command, args, {
|
|
67
72
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
68
73
|
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
|
|
@@ -531,8 +536,14 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
531
536
|
*/
|
|
532
537
|
export function parseTaskOutcome(output: string): TaskRunningState {
|
|
533
538
|
const lastChunk = output.slice(-500);
|
|
534
|
-
|
|
535
|
-
|
|
539
|
+
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
|
|
540
|
+
let last: string | null = null;
|
|
541
|
+
let match;
|
|
542
|
+
while ((match = regex.exec(lastChunk)) !== null) {
|
|
543
|
+
last = match[0];
|
|
544
|
+
}
|
|
545
|
+
if (last === TASK_FAILURE_MARKER) return "failed";
|
|
546
|
+
if (last === TASK_SUCCESS_MARKER) return "finished";
|
|
536
547
|
return "finished";
|
|
537
548
|
}
|
|
538
549
|
|
package/src/rpc-handler.ts
CHANGED
|
@@ -188,6 +188,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
188
188
|
triggers?: Array<{ type: "cron" | "once"; value: string }>;
|
|
189
189
|
triggers_enabled?: boolean;
|
|
190
190
|
requires_confirmation?: boolean;
|
|
191
|
+
yolo_mode?: boolean;
|
|
191
192
|
command?: string;
|
|
192
193
|
};
|
|
193
194
|
|
|
@@ -218,6 +219,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
218
219
|
triggers: params.triggers ?? [],
|
|
219
220
|
triggers_enabled: params.triggers_enabled ?? true,
|
|
220
221
|
requires_confirmation: params.requires_confirmation ?? true,
|
|
222
|
+
...(params.yolo_mode ? { yolo_mode: true } : {}),
|
|
221
223
|
...(params.command ? { command: params.command } : {}),
|
|
222
224
|
},
|
|
223
225
|
body,
|
|
@@ -238,6 +240,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
238
240
|
triggers?: Array<{ type: "cron" | "once"; value: string }>;
|
|
239
241
|
triggers_enabled?: boolean;
|
|
240
242
|
requires_confirmation?: boolean;
|
|
243
|
+
yolo_mode?: boolean;
|
|
241
244
|
command?: string;
|
|
242
245
|
};
|
|
243
246
|
|
|
@@ -256,6 +259,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
256
259
|
if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
257
260
|
if (params.requires_confirmation !== undefined)
|
|
258
261
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
262
|
+
if (params.yolo_mode !== undefined) existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
|
|
259
263
|
if (params.command !== undefined) {
|
|
260
264
|
if (params.command) {
|
|
261
265
|
existing.frontmatter.command = params.command;
|
|
@@ -302,6 +306,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
302
306
|
user_prompt: string;
|
|
303
307
|
agent: string;
|
|
304
308
|
requires_confirmation?: boolean;
|
|
309
|
+
yolo_mode?: boolean;
|
|
305
310
|
command?: string;
|
|
306
311
|
};
|
|
307
312
|
|
|
@@ -317,6 +322,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
317
322
|
triggers: [],
|
|
318
323
|
triggers_enabled: false,
|
|
319
324
|
requires_confirmation: params.requires_confirmation ?? false,
|
|
325
|
+
...(params.yolo_mode ? { yolo_mode: true } : {}),
|
|
320
326
|
...(params.command ? { command: params.command } : {}),
|
|
321
327
|
},
|
|
322
328
|
body: "",
|
|
@@ -390,7 +396,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
390
396
|
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
391
397
|
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
392
398
|
const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(
|
|
393
|
-
followupTask, params.message, followupTask.frontmatter.permissions,
|
|
399
|
+
followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
|
|
394
400
|
);
|
|
395
401
|
|
|
396
402
|
// Spawn directly via crossSpawn so we can track and kill the child
|
|
@@ -564,8 +570,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
564
570
|
const reports: Array<{ file: string; content?: string; error?: string }> = [];
|
|
565
571
|
const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
|
|
566
572
|
for (const file of params.report_files) {
|
|
567
|
-
if (!file.endsWith(".md")) {
|
|
568
|
-
reports.push({ file, error: "must end with .md" });
|
|
573
|
+
if (!file.endsWith(".md") && !file.endsWith(".txt")) {
|
|
574
|
+
reports.push({ file, error: "must end with .md or .txt" });
|
|
569
575
|
continue;
|
|
570
576
|
}
|
|
571
577
|
const basename = path.basename(file);
|
package/src/task.ts
CHANGED
|
@@ -202,31 +202,66 @@ export function beginStreamingMessage(
|
|
|
202
202
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
203
203
|
const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
|
|
204
204
|
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
205
|
-
return new StreamingMessageWriter(filePath
|
|
205
|
+
return new StreamingMessageWriter(filePath);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
export class StreamingMessageWriter {
|
|
209
|
-
private
|
|
210
|
-
constructor(private filePath: string, delimiter: string) {
|
|
211
|
-
this.delimiter = delimiter;
|
|
212
|
-
}
|
|
209
|
+
constructor(private filePath: string) {}
|
|
213
210
|
|
|
214
211
|
/** Append a chunk of content to the current message. */
|
|
215
212
|
write(chunk: string): void {
|
|
216
213
|
fs.appendFileSync(this.filePath, chunk, "utf-8");
|
|
217
214
|
}
|
|
218
215
|
|
|
219
|
-
/** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
|
|
216
|
+
/** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
|
|
220
217
|
end(attachments?: string[]): void {
|
|
221
218
|
fs.appendFileSync(this.filePath, "\n\n", "utf-8");
|
|
222
219
|
if (attachments?.length) {
|
|
223
220
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
// Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
|
|
222
|
+
const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
|
|
223
|
+
let lastMatch: RegExpExecArray | null = null;
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = pattern.exec(raw)) !== null) lastMatch = m;
|
|
226
|
+
if (lastMatch) {
|
|
227
|
+
const before = raw.slice(0, lastMatch.index);
|
|
228
|
+
const after = raw.slice(lastMatch.index + lastMatch[0].length);
|
|
229
|
+
const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
|
|
230
|
+
fs.writeFileSync(this.filePath, updated, "utf-8");
|
|
231
|
+
}
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Splice a user message into a running assistant stream.
|
|
238
|
+
* Ends the current assistant block, writes the user message,
|
|
239
|
+
* then opens a new assistant block — all as direct file appends.
|
|
240
|
+
* The existing StreamingMessageWriter keeps working because its
|
|
241
|
+
* write() is just appendFileSync, so subsequent chunks land in
|
|
242
|
+
* the new assistant block.
|
|
243
|
+
*/
|
|
244
|
+
export function spliceUserMessage(
|
|
245
|
+
taskDir: string,
|
|
246
|
+
runId: string,
|
|
247
|
+
userMsg: ConversationMessage,
|
|
248
|
+
/** Optional text to append to the current assistant block before ending it. */
|
|
249
|
+
assistantAppend?: string,
|
|
250
|
+
): void {
|
|
251
|
+
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
252
|
+
// 1. Optionally append to the current assistant block (e.g. the input questions)
|
|
253
|
+
if (assistantAppend) {
|
|
254
|
+
fs.appendFileSync(filePath, assistantAppend, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
// 2. End the current assistant block
|
|
257
|
+
fs.appendFileSync(filePath, "\n\n", "utf-8");
|
|
258
|
+
// 3. Write the user message
|
|
259
|
+
appendRunMessage(taskDir, runId, userMsg);
|
|
260
|
+
// 4. Open a new assistant block for subsequent agent output
|
|
261
|
+
const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
|
|
262
|
+
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
/**
|
|
231
266
|
* Read conversation messages from a run's TASKRUN.md file.
|
|
232
267
|
*/
|