@shipers-dev/multi 0.5.0 → 0.6.0
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/index.js +280 -41
- package/package.json +1 -1
- package/src/acp-runner.ts +15 -3
- package/src/acpx-runner.ts +136 -0
- package/src/index.ts +91 -36
package/dist/index.js
CHANGED
|
@@ -5198,6 +5198,7 @@ async function runAcp(opts) {
|
|
|
5198
5198
|
const stream2 = ndJsonStream(output, input);
|
|
5199
5199
|
let activeSessionId = opts.sessionId || null;
|
|
5200
5200
|
let recording = false;
|
|
5201
|
+
const alwaysAllow = new Set;
|
|
5201
5202
|
const client = {
|
|
5202
5203
|
async sessionUpdate(params) {
|
|
5203
5204
|
if (!recording)
|
|
@@ -5205,7 +5206,7 @@ async function runAcp(opts) {
|
|
|
5205
5206
|
await handleSessionUpdate(params, opts);
|
|
5206
5207
|
},
|
|
5207
5208
|
async requestPermission(params) {
|
|
5208
|
-
return await handleRequestPermission(params, opts);
|
|
5209
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
5209
5210
|
},
|
|
5210
5211
|
async readTextFile(params) {
|
|
5211
5212
|
try {
|
|
@@ -5325,8 +5326,14 @@ ${entries}` } });
|
|
|
5325
5326
|
break;
|
|
5326
5327
|
}
|
|
5327
5328
|
}
|
|
5328
|
-
async function handleRequestPermission(params, o) {
|
|
5329
|
+
async function handleRequestPermission(params, o, allowCache) {
|
|
5329
5330
|
const tc = params.toolCall || {};
|
|
5331
|
+
const toolKey = `${tc.toolName || tc.title || ""}|${tc.kind || ""}`.toLowerCase();
|
|
5332
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
5333
|
+
const allowOpt = params.options.find((op) => /allow/i.test(op.kind || "") || /allow/i.test(op.name || ""));
|
|
5334
|
+
if (allowOpt)
|
|
5335
|
+
return { outcome: { outcome: "selected", optionId: allowOpt.optionId } };
|
|
5336
|
+
}
|
|
5330
5337
|
const created = await apiClient.post(`${o.apiUrl}/api/permissions`, {
|
|
5331
5338
|
issue_id: o.issueId,
|
|
5332
5339
|
device_id: o.deviceId,
|
|
@@ -5346,6 +5353,10 @@ ${entries}` } });
|
|
|
5346
5353
|
if (!row)
|
|
5347
5354
|
break;
|
|
5348
5355
|
if (row.status === "resolved" && row.chosen) {
|
|
5356
|
+
const chosenOpt = params.options.find((op) => op.optionId === row.chosen);
|
|
5357
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || "") || /allow_always/i.test(chosenOpt.kind || ""));
|
|
5358
|
+
if (isAlways && toolKey)
|
|
5359
|
+
allowCache.add(toolKey);
|
|
5349
5360
|
return { outcome: { outcome: "selected", optionId: row.chosen } };
|
|
5350
5361
|
}
|
|
5351
5362
|
if (row.status === "cancelled") {
|
|
@@ -5369,6 +5380,145 @@ function extractText(content) {
|
|
|
5369
5380
|
return "";
|
|
5370
5381
|
}
|
|
5371
5382
|
|
|
5383
|
+
// src/acpx-runner.ts
|
|
5384
|
+
async function runAcpx(opts) {
|
|
5385
|
+
const args = ["acpx", "--format", "json", "--json-strict", "--approve-all"];
|
|
5386
|
+
if (opts.cwd)
|
|
5387
|
+
args.push("--cwd", opts.cwd);
|
|
5388
|
+
args.push(opts.agentType);
|
|
5389
|
+
if (opts.sessionName) {
|
|
5390
|
+
const ensureArgs = ["acpx", ...opts.cwd ? ["--cwd", opts.cwd] : [], opts.agentType, "sessions", "ensure", "--name", opts.sessionName];
|
|
5391
|
+
try {
|
|
5392
|
+
const ensure = Bun.spawn(ensureArgs, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
5393
|
+
await ensure.exited;
|
|
5394
|
+
} catch {}
|
|
5395
|
+
args.push("prompt", "-s", opts.sessionName);
|
|
5396
|
+
} else {
|
|
5397
|
+
args.push("prompt");
|
|
5398
|
+
}
|
|
5399
|
+
args.push(opts.prompt);
|
|
5400
|
+
const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
5401
|
+
let stopReason = "end_turn";
|
|
5402
|
+
const reader = proc.stdout.getReader();
|
|
5403
|
+
const dec = new TextDecoder;
|
|
5404
|
+
let buf = "";
|
|
5405
|
+
while (true) {
|
|
5406
|
+
const { value, done } = await reader.read();
|
|
5407
|
+
if (done)
|
|
5408
|
+
break;
|
|
5409
|
+
buf += dec.decode(value, { stream: true });
|
|
5410
|
+
let nl;
|
|
5411
|
+
while ((nl = buf.indexOf(`
|
|
5412
|
+
`)) !== -1) {
|
|
5413
|
+
const line = buf.slice(0, nl).trim();
|
|
5414
|
+
buf = buf.slice(nl + 1);
|
|
5415
|
+
if (!line)
|
|
5416
|
+
continue;
|
|
5417
|
+
const events = parseAcpLine(line, (r) => {
|
|
5418
|
+
stopReason = r;
|
|
5419
|
+
});
|
|
5420
|
+
for (const ev of events)
|
|
5421
|
+
await opts.onEvent(ev);
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
if (buf.trim()) {
|
|
5425
|
+
const events = parseAcpLine(buf.trim(), (r) => {
|
|
5426
|
+
stopReason = r;
|
|
5427
|
+
});
|
|
5428
|
+
for (const ev of events)
|
|
5429
|
+
await opts.onEvent(ev);
|
|
5430
|
+
}
|
|
5431
|
+
const code = await proc.exited;
|
|
5432
|
+
if (code !== 0 && code !== 130) {
|
|
5433
|
+
await opts.onEvent({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
|
|
5434
|
+
}
|
|
5435
|
+
return { stopReason };
|
|
5436
|
+
}
|
|
5437
|
+
function parseAcpLine(line, setStop) {
|
|
5438
|
+
let msg;
|
|
5439
|
+
try {
|
|
5440
|
+
msg = JSON.parse(line);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return [];
|
|
5443
|
+
}
|
|
5444
|
+
const out = [];
|
|
5445
|
+
if (msg.method === "session/update" && msg.params?.sessionUpdate) {
|
|
5446
|
+
return handleSessionUpdate(msg.params);
|
|
5447
|
+
}
|
|
5448
|
+
if (msg.id && msg.result && typeof msg.result === "object" && "stopReason" in msg.result) {
|
|
5449
|
+
setStop(msg.result.stopReason);
|
|
5450
|
+
out.push({ event_type: "result", payload: { stopReason: msg.result.stopReason } });
|
|
5451
|
+
return out;
|
|
5452
|
+
}
|
|
5453
|
+
if (msg.error) {
|
|
5454
|
+
out.push({ event_type: "error", payload: { message: msg.error.message || JSON.stringify(msg.error) } });
|
|
5455
|
+
return out;
|
|
5456
|
+
}
|
|
5457
|
+
return out;
|
|
5458
|
+
}
|
|
5459
|
+
function handleSessionUpdate(params) {
|
|
5460
|
+
const u = params.update || {};
|
|
5461
|
+
const kind = u.sessionUpdate;
|
|
5462
|
+
const out = [];
|
|
5463
|
+
switch (kind) {
|
|
5464
|
+
case "agent_message_chunk":
|
|
5465
|
+
case "agent_thought_chunk": {
|
|
5466
|
+
const text = extractText2(u.content);
|
|
5467
|
+
if (text)
|
|
5468
|
+
out.push({ event_type: "assistant_text", payload: { text } });
|
|
5469
|
+
break;
|
|
5470
|
+
}
|
|
5471
|
+
case "tool_call":
|
|
5472
|
+
case "tool_call_update": {
|
|
5473
|
+
out.push({ event_type: "tool_call", payload: {
|
|
5474
|
+
id: u.toolCallId || u.id,
|
|
5475
|
+
tool: u.title || u.toolName || "tool",
|
|
5476
|
+
kind: u.kind,
|
|
5477
|
+
status: u.status,
|
|
5478
|
+
input: u.rawInput,
|
|
5479
|
+
locations: u.locations
|
|
5480
|
+
} });
|
|
5481
|
+
if (Array.isArray(u.content)) {
|
|
5482
|
+
for (const c of u.content) {
|
|
5483
|
+
if (c.type === "content" && c.content?.type === "text") {
|
|
5484
|
+
out.push({ event_type: "tool_result", payload: { tool_use_id: u.toolCallId || u.id, content: String(c.content.text).slice(0, 4000) } });
|
|
5485
|
+
} else if (c.type === "diff") {
|
|
5486
|
+
const diff = `--- ${c.path}
|
|
5487
|
+
${c.oldText || ""}
|
|
5488
|
+
+++ ${c.path}
|
|
5489
|
+
${c.newText || ""}`.slice(0, 4000);
|
|
5490
|
+
out.push({ event_type: "tool_result", payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
break;
|
|
5495
|
+
}
|
|
5496
|
+
case "plan": {
|
|
5497
|
+
const entries = (u.entries || []).map((e, i) => `${i + 1}. [${e.status || "pending"}] ${e.content}`).join(`
|
|
5498
|
+
`);
|
|
5499
|
+
if (entries)
|
|
5500
|
+
out.push({ event_type: "assistant_text", payload: { text: `**Plan**
|
|
5501
|
+
|
|
5502
|
+
${entries}` } });
|
|
5503
|
+
break;
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
return out;
|
|
5507
|
+
}
|
|
5508
|
+
function extractText2(content) {
|
|
5509
|
+
if (!content)
|
|
5510
|
+
return "";
|
|
5511
|
+
if (typeof content === "string")
|
|
5512
|
+
return content;
|
|
5513
|
+
if (Array.isArray(content))
|
|
5514
|
+
return content.map(extractText2).join("");
|
|
5515
|
+
if (content.type === "text" && typeof content.text === "string")
|
|
5516
|
+
return content.text;
|
|
5517
|
+
if (content.text)
|
|
5518
|
+
return content.text;
|
|
5519
|
+
return "";
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5372
5522
|
// src/index.ts
|
|
5373
5523
|
import { parseArgs } from "util";
|
|
5374
5524
|
import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync, unlinkSync, readdirSync, statSync } from "fs";
|
|
@@ -5381,7 +5531,7 @@ var LOG_PATH = join(MULTI_DIR, "logs", "agent.log");
|
|
|
5381
5531
|
var SKILLS_DIR = join(MULTI_DIR, "skills");
|
|
5382
5532
|
var STOP_PATH = join(MULTI_DIR, "stop.flag");
|
|
5383
5533
|
var TASKS_DB_PATH = join(MULTI_DIR, "tasks.db");
|
|
5384
|
-
var VERSION = "0.
|
|
5534
|
+
var VERSION = "0.6.0";
|
|
5385
5535
|
var COMMANDS = {
|
|
5386
5536
|
setup: "Register this device with a workspace",
|
|
5387
5537
|
connect: "Connect device to realtime hub and execute assigned tasks",
|
|
@@ -5833,29 +5983,71 @@ async function handleRunTask(apiUrl, deviceId, task, detected) {
|
|
|
5833
5983
|
} catch {}
|
|
5834
5984
|
};
|
|
5835
5985
|
const turn = {
|
|
5836
|
-
|
|
5837
|
-
toolOrder: [],
|
|
5986
|
+
blocks: [],
|
|
5838
5987
|
tools: new Map,
|
|
5839
5988
|
plans: [],
|
|
5840
5989
|
progress: [],
|
|
5841
5990
|
result: null,
|
|
5842
5991
|
error: null
|
|
5843
5992
|
};
|
|
5993
|
+
const appendText = (text) => {
|
|
5994
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
5995
|
+
if (last && last.kind === "text")
|
|
5996
|
+
last.text += text;
|
|
5997
|
+
else
|
|
5998
|
+
turn.blocks.push({ kind: "text", text });
|
|
5999
|
+
};
|
|
6000
|
+
const upsertTool = (id, patch) => {
|
|
6001
|
+
const existing = turn.tools.get(id);
|
|
6002
|
+
if (existing) {
|
|
6003
|
+
if (patch.tool && patch.tool !== "tool")
|
|
6004
|
+
existing.tool = patch.tool;
|
|
6005
|
+
if (patch.kind)
|
|
6006
|
+
existing.kind = patch.kind;
|
|
6007
|
+
if (patch.status)
|
|
6008
|
+
existing.status = patch.status;
|
|
6009
|
+
if (patch.input !== undefined)
|
|
6010
|
+
existing.input = patch.input;
|
|
6011
|
+
} else {
|
|
6012
|
+
turn.tools.set(id, { id, tool: patch.tool || "tool", kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
6013
|
+
turn.blocks.push({ kind: "tool", id });
|
|
6014
|
+
}
|
|
6015
|
+
};
|
|
6016
|
+
const toolLabel = (t) => {
|
|
6017
|
+
const clean = stripMd(t.tool);
|
|
6018
|
+
if (clean && clean !== "tool")
|
|
6019
|
+
return clean;
|
|
6020
|
+
if (t.input && typeof t.input === "object") {
|
|
6021
|
+
if (t.input.command)
|
|
6022
|
+
return `Bash: ${String(t.input.command).split(`
|
|
6023
|
+
`)[0].slice(0, 60)}`;
|
|
6024
|
+
if (t.input.file_path)
|
|
6025
|
+
return `${t.kind === "edit" ? "Edit" : "Read"}: ${t.input.file_path}`;
|
|
6026
|
+
if (t.input.pattern)
|
|
6027
|
+
return `Grep: ${t.input.pattern}`;
|
|
6028
|
+
if (t.input.url)
|
|
6029
|
+
return `Fetch: ${t.input.url}`;
|
|
6030
|
+
}
|
|
6031
|
+
return t.kind ? `${t.kind}` : "tool";
|
|
6032
|
+
};
|
|
5844
6033
|
const render = () => {
|
|
5845
6034
|
const parts = [];
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
6035
|
+
for (const b of turn.blocks) {
|
|
6036
|
+
if (b.kind === "text") {
|
|
6037
|
+
if (b.text)
|
|
6038
|
+
parts.push(b.text);
|
|
6039
|
+
continue;
|
|
6040
|
+
}
|
|
6041
|
+
const t = turn.tools.get(b.id);
|
|
5850
6042
|
if (!t)
|
|
5851
6043
|
continue;
|
|
5852
6044
|
const icon = statusIcon(t.status);
|
|
5853
|
-
const
|
|
5854
|
-
const head = `${icon} ${
|
|
6045
|
+
const label = toolLabel(t);
|
|
6046
|
+
const head = `${icon} ${label}${t.status && t.status !== "completed" ? ` \xB7 ${t.status}` : ""}`;
|
|
5855
6047
|
const body = [];
|
|
5856
6048
|
if (t.input !== undefined && t.input !== null) {
|
|
5857
6049
|
const inputStr = typeof t.input === "object" ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
5858
|
-
body.push("```json\n" + inputStr + "\n```");
|
|
6050
|
+
body.push("```json\n" + inputStr.slice(0, 2000) + "\n```");
|
|
5859
6051
|
}
|
|
5860
6052
|
if (t.results.length) {
|
|
5861
6053
|
const joined = t.results.join(`
|
|
@@ -5917,7 +6109,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5917
6109
|
}
|
|
5918
6110
|
case "assistant_text": {
|
|
5919
6111
|
await ensureLiveComment();
|
|
5920
|
-
|
|
6112
|
+
appendText(p.text);
|
|
5921
6113
|
hasAssistantText = true;
|
|
5922
6114
|
schedulePatch();
|
|
5923
6115
|
break;
|
|
@@ -5925,8 +6117,8 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5925
6117
|
case "stdout": {
|
|
5926
6118
|
if (p.line) {
|
|
5927
6119
|
await ensureLiveComment();
|
|
5928
|
-
|
|
5929
|
-
` : "") + p.line;
|
|
6120
|
+
appendText((turn.blocks.length ? `
|
|
6121
|
+
` : "") + p.line);
|
|
5930
6122
|
hasAssistantText = true;
|
|
5931
6123
|
schedulePatch();
|
|
5932
6124
|
}
|
|
@@ -5934,36 +6126,24 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5934
6126
|
}
|
|
5935
6127
|
case "tool_call": {
|
|
5936
6128
|
await ensureLiveComment();
|
|
5937
|
-
const id = p.id || `anon-${turn.
|
|
5938
|
-
|
|
5939
|
-
if (existing) {
|
|
5940
|
-
if (p.tool)
|
|
5941
|
-
existing.tool = p.tool;
|
|
5942
|
-
if (p.kind)
|
|
5943
|
-
existing.kind = p.kind;
|
|
5944
|
-
if (p.status)
|
|
5945
|
-
existing.status = p.status;
|
|
5946
|
-
if (p.input !== undefined)
|
|
5947
|
-
existing.input = p.input;
|
|
5948
|
-
} else {
|
|
5949
|
-
turn.toolOrder.push(id);
|
|
5950
|
-
turn.tools.set(id, { id, tool: p.tool || "tool", kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
5951
|
-
}
|
|
6129
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
6130
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
5952
6131
|
schedulePatch();
|
|
5953
6132
|
break;
|
|
5954
6133
|
}
|
|
5955
6134
|
case "tool_result": {
|
|
5956
6135
|
await ensureLiveComment();
|
|
5957
|
-
const
|
|
6136
|
+
const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
|
|
6137
|
+
const id = p.tool_use_id || lastToolId;
|
|
5958
6138
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
5959
6139
|
const content = String(p.content ?? "").trim();
|
|
5960
6140
|
if (entry && content) {
|
|
5961
6141
|
entry.results.push(content);
|
|
5962
6142
|
schedulePatch();
|
|
5963
6143
|
} else if (content) {
|
|
5964
|
-
const pid = `result-${turn.
|
|
5965
|
-
|
|
5966
|
-
turn.tools.
|
|
6144
|
+
const pid = `result-${turn.tools.size}`;
|
|
6145
|
+
upsertTool(pid, { tool: "tool result" });
|
|
6146
|
+
turn.tools.get(pid).results.push(content);
|
|
5967
6147
|
schedulePatch();
|
|
5968
6148
|
}
|
|
5969
6149
|
break;
|
|
@@ -5973,7 +6153,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5973
6153
|
if (p.is_error)
|
|
5974
6154
|
hadError = true;
|
|
5975
6155
|
if (!hasAssistantText && p.result) {
|
|
5976
|
-
|
|
6156
|
+
appendText(p.result);
|
|
5977
6157
|
hasAssistantText = true;
|
|
5978
6158
|
}
|
|
5979
6159
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|
|
@@ -5996,6 +6176,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5996
6176
|
} catch {}
|
|
5997
6177
|
const acpCapable = detected.filter((d) => d.type === "claude-code");
|
|
5998
6178
|
const useAcp = preferType !== "pi" && acpCapable.length > 0 && process.env.MULTI_LEGACY !== "1";
|
|
6179
|
+
const useAcpx = !useAcp && preferType && ["pi", "codex", "openclaw"].includes(preferType) && process.env.MULTI_LEGACY !== "1";
|
|
5999
6180
|
try {
|
|
6000
6181
|
if (useAcp) {
|
|
6001
6182
|
const base = `${task.title}
|
|
@@ -6083,14 +6264,72 @@ ${userPart}` : userPart;
|
|
|
6083
6264
|
}
|
|
6084
6265
|
});
|
|
6085
6266
|
log(` acp session ${sessionId.slice(0, 8)}`);
|
|
6086
|
-
} else {
|
|
6087
|
-
let
|
|
6267
|
+
} else if (useAcpx) {
|
|
6268
|
+
let preamble = "";
|
|
6088
6269
|
try {
|
|
6089
|
-
const
|
|
6090
|
-
|
|
6091
|
-
|
|
6270
|
+
const agentRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
|
|
6271
|
+
const agent = agentRes.data;
|
|
6272
|
+
if (agent?.prompt)
|
|
6273
|
+
preamble += `# Agent instructions
|
|
6274
|
+
|
|
6275
|
+
${agent.prompt}
|
|
6276
|
+
|
|
6277
|
+
`;
|
|
6278
|
+
const skillsRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}/skills`);
|
|
6279
|
+
const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
|
|
6280
|
+
if (skillList.length) {
|
|
6281
|
+
preamble += `# Attached skills (${skillList.length})
|
|
6282
|
+
|
|
6283
|
+
`;
|
|
6284
|
+
for (const s of skillList) {
|
|
6285
|
+
const body = String(s.body || s.description || "").trim();
|
|
6286
|
+
if (body)
|
|
6287
|
+
preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
|
|
6288
|
+
|
|
6289
|
+
${body}
|
|
6290
|
+
|
|
6291
|
+
`;
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6092
6294
|
} catch {}
|
|
6093
|
-
const
|
|
6295
|
+
const base = `${task.title}
|
|
6296
|
+
|
|
6297
|
+
${task.description || ""}`.trim();
|
|
6298
|
+
let userPart = task.followup ? `${task.followup}
|
|
6299
|
+
|
|
6300
|
+
---
|
|
6301
|
+
Context (${task.key}): ${task.title}` : base || task.title;
|
|
6302
|
+
userPart = stripSelfMention(userPart, preferType);
|
|
6303
|
+
if (attachmentRefs.length) {
|
|
6304
|
+
const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
|
|
6305
|
+
`);
|
|
6306
|
+
userPart += `
|
|
6307
|
+
|
|
6308
|
+
---
|
|
6309
|
+
Attached files:
|
|
6310
|
+
${lines}
|
|
6311
|
+
|
|
6312
|
+
Write generated files to: ${outDir}`;
|
|
6313
|
+
} else {
|
|
6314
|
+
userPart += `
|
|
6315
|
+
|
|
6316
|
+
---
|
|
6317
|
+
Write generated files to: ${outDir}`;
|
|
6318
|
+
}
|
|
6319
|
+
const full = preamble ? `${preamble}
|
|
6320
|
+
---
|
|
6321
|
+
|
|
6322
|
+
${userPart}` : userPart;
|
|
6323
|
+
log(` acpx runner: ${preferType}`);
|
|
6324
|
+
await runAcpx({
|
|
6325
|
+
agentType: preferType,
|
|
6326
|
+
prompt: full,
|
|
6327
|
+
cwd: workingDir,
|
|
6328
|
+
sessionName: `issue-${issueId}`,
|
|
6329
|
+
onEvent: eventHandler
|
|
6330
|
+
});
|
|
6331
|
+
} else {
|
|
6332
|
+
const runner = pickRunner(detected, preferType);
|
|
6094
6333
|
for await (const event of runner(task))
|
|
6095
6334
|
await eventHandler(event);
|
|
6096
6335
|
}
|
package/package.json
CHANGED
package/src/acp-runner.ts
CHANGED
|
@@ -54,6 +54,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
54
54
|
|
|
55
55
|
let activeSessionId: string | null = opts.sessionId || null;
|
|
56
56
|
let recording = false; // only forward events after prompt() starts
|
|
57
|
+
const alwaysAllow = new Set<string>(); // keys: toolName|kind that user said "always allow"
|
|
57
58
|
|
|
58
59
|
const client: Client = {
|
|
59
60
|
async sessionUpdate(params: SessionNotification): Promise<void> {
|
|
@@ -61,7 +62,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
61
62
|
await handleSessionUpdate(params, opts);
|
|
62
63
|
},
|
|
63
64
|
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
64
|
-
return await handleRequestPermission(params, opts);
|
|
65
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
65
66
|
},
|
|
66
67
|
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
67
68
|
try {
|
|
@@ -170,8 +171,16 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts): Promise<RequestPermissionResponse> {
|
|
174
|
+
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts, allowCache: Set<string>): Promise<RequestPermissionResponse> {
|
|
174
175
|
const tc: any = params.toolCall || {};
|
|
176
|
+
const toolKey = `${tc.toolName || tc.title || ''}|${tc.kind || ''}`.toLowerCase();
|
|
177
|
+
|
|
178
|
+
// Auto-approve if user previously chose "always allow" for same tool/kind
|
|
179
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
180
|
+
const allowOpt = (params.options as any[]).find(op => /allow/i.test(op.kind || '') || /allow/i.test(op.name || ''));
|
|
181
|
+
if (allowOpt) return { outcome: { outcome: 'selected', optionId: allowOpt.optionId } as any };
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
const created = await apiClient.post<any>(`${o.apiUrl}/api/permissions`, {
|
|
176
185
|
issue_id: o.issueId,
|
|
177
186
|
device_id: o.deviceId,
|
|
@@ -183,7 +192,6 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
183
192
|
const permId = created.data?.id;
|
|
184
193
|
if (!permId) return { outcome: { outcome: 'cancelled' } as any };
|
|
185
194
|
|
|
186
|
-
// Poll every 500ms up to 5 min
|
|
187
195
|
const deadline = Date.now() + 5 * 60 * 1000;
|
|
188
196
|
while (Date.now() < deadline) {
|
|
189
197
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -191,6 +199,10 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
191
199
|
const row = got.data;
|
|
192
200
|
if (!row) break;
|
|
193
201
|
if (row.status === 'resolved' && row.chosen) {
|
|
202
|
+
// If the chosen option is an "always" variant, cache it
|
|
203
|
+
const chosenOpt = (params.options as any[]).find(op => op.optionId === row.chosen);
|
|
204
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || '') || /allow_always/i.test(chosenOpt.kind || ''));
|
|
205
|
+
if (isAlways && toolKey) allowCache.add(toolKey);
|
|
194
206
|
return { outcome: { outcome: 'selected', optionId: row.chosen } as any };
|
|
195
207
|
}
|
|
196
208
|
if (row.status === 'cancelled') {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// acpx-runner — delegate to `acpx` CLI for pi/codex/openclaw agents.
|
|
2
|
+
// acpx emits raw ACP JSON-RPC NDJSON on stdout with --format json; we reuse the
|
|
3
|
+
// same parse semantics as our direct ACP runner to produce AcpEvent stream.
|
|
4
|
+
//
|
|
5
|
+
// Requires: `npm install -g acpx` on device host (or npx acpx@latest).
|
|
6
|
+
// Runs with --approve-all for now (no permission UI forwarding).
|
|
7
|
+
|
|
8
|
+
import type { AcpEvent } from './acp-runner';
|
|
9
|
+
|
|
10
|
+
export interface AcpxRunOpts {
|
|
11
|
+
agentType: 'pi' | 'codex' | 'openclaw' | 'claude' | string;
|
|
12
|
+
prompt: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
sessionName?: string; // reuse same session across follow-ups (e.g. "issue-<id>")
|
|
15
|
+
onEvent: (ev: AcpEvent) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runAcpx(opts: AcpxRunOpts): Promise<{ stopReason: string }> {
|
|
19
|
+
const args = ['acpx', '--format', 'json', '--json-strict', '--approve-all'];
|
|
20
|
+
if (opts.cwd) args.push('--cwd', opts.cwd);
|
|
21
|
+
args.push(opts.agentType);
|
|
22
|
+
// Ensure a session exists for this name (idempotent). ensure = get-or-create.
|
|
23
|
+
// We run it as a separate invocation, then prompt.
|
|
24
|
+
if (opts.sessionName) {
|
|
25
|
+
const ensureArgs = ['acpx', ...(opts.cwd ? ['--cwd', opts.cwd] : []), opts.agentType, 'sessions', 'ensure', '--name', opts.sessionName];
|
|
26
|
+
try {
|
|
27
|
+
const ensure = Bun.spawn(ensureArgs, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
|
28
|
+
await ensure.exited;
|
|
29
|
+
} catch {}
|
|
30
|
+
args.push('prompt', '-s', opts.sessionName);
|
|
31
|
+
} else {
|
|
32
|
+
args.push('prompt');
|
|
33
|
+
}
|
|
34
|
+
args.push(opts.prompt);
|
|
35
|
+
|
|
36
|
+
const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
|
37
|
+
let stopReason = 'end_turn';
|
|
38
|
+
|
|
39
|
+
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
40
|
+
const dec = new TextDecoder();
|
|
41
|
+
let buf = '';
|
|
42
|
+
|
|
43
|
+
while (true) {
|
|
44
|
+
const { value, done } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
buf += dec.decode(value, { stream: true });
|
|
47
|
+
let nl: number;
|
|
48
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
49
|
+
const line = buf.slice(0, nl).trim();
|
|
50
|
+
buf = buf.slice(nl + 1);
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
const events = parseAcpLine(line, (r) => { stopReason = r; });
|
|
53
|
+
for (const ev of events) await opts.onEvent(ev);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (buf.trim()) {
|
|
57
|
+
const events = parseAcpLine(buf.trim(), (r) => { stopReason = r; });
|
|
58
|
+
for (const ev of events) await opts.onEvent(ev);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const code = await proc.exited;
|
|
62
|
+
if (code !== 0 && code !== 130) {
|
|
63
|
+
await opts.onEvent({ event_type: 'error', payload: { message: `acpx exited with code ${code}` } });
|
|
64
|
+
}
|
|
65
|
+
return { stopReason };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseAcpLine(line: string, setStop: (r: string) => void): AcpEvent[] {
|
|
69
|
+
let msg: any;
|
|
70
|
+
try { msg = JSON.parse(line); } catch { return []; }
|
|
71
|
+
const out: AcpEvent[] = [];
|
|
72
|
+
|
|
73
|
+
// session/update notification
|
|
74
|
+
if (msg.method === 'session/update' && msg.params?.sessionUpdate) {
|
|
75
|
+
return handleSessionUpdate(msg.params);
|
|
76
|
+
}
|
|
77
|
+
// prompt result
|
|
78
|
+
if (msg.id && msg.result && typeof msg.result === 'object' && 'stopReason' in msg.result) {
|
|
79
|
+
setStop(msg.result.stopReason);
|
|
80
|
+
out.push({ event_type: 'result', payload: { stopReason: msg.result.stopReason } });
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
// errors
|
|
84
|
+
if (msg.error) {
|
|
85
|
+
out.push({ event_type: 'error', payload: { message: msg.error.message || JSON.stringify(msg.error) } });
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleSessionUpdate(params: any): AcpEvent[] {
|
|
92
|
+
const u = params.update || {};
|
|
93
|
+
const kind = u.sessionUpdate;
|
|
94
|
+
const out: AcpEvent[] = [];
|
|
95
|
+
switch (kind) {
|
|
96
|
+
case 'agent_message_chunk':
|
|
97
|
+
case 'agent_thought_chunk': {
|
|
98
|
+
const text = extractText(u.content);
|
|
99
|
+
if (text) out.push({ event_type: 'assistant_text', payload: { text } });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'tool_call':
|
|
103
|
+
case 'tool_call_update': {
|
|
104
|
+
out.push({ event_type: 'tool_call', payload: {
|
|
105
|
+
id: u.toolCallId || u.id, tool: u.title || u.toolName || 'tool',
|
|
106
|
+
kind: u.kind, status: u.status, input: u.rawInput, locations: u.locations,
|
|
107
|
+
}});
|
|
108
|
+
if (Array.isArray(u.content)) {
|
|
109
|
+
for (const c of u.content) {
|
|
110
|
+
if (c.type === 'content' && c.content?.type === 'text') {
|
|
111
|
+
out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: String(c.content.text).slice(0, 4000) } });
|
|
112
|
+
} else if (c.type === 'diff') {
|
|
113
|
+
const diff = `--- ${c.path}\n${c.oldText || ''}\n+++ ${c.path}\n${c.newText || ''}`.slice(0, 4000);
|
|
114
|
+
out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'plan': {
|
|
121
|
+
const entries = (u.entries || []).map((e: any, i: number) => `${i + 1}. [${e.status || 'pending'}] ${e.content}`).join('\n');
|
|
122
|
+
if (entries) out.push({ event_type: 'assistant_text', payload: { text: `**Plan**\n\n${entries}` } });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractText(content: any): string {
|
|
130
|
+
if (!content) return '';
|
|
131
|
+
if (typeof content === 'string') return content;
|
|
132
|
+
if (Array.isArray(content)) return content.map(extractText).join('');
|
|
133
|
+
if (content.type === 'text' && typeof content.text === 'string') return content.text;
|
|
134
|
+
if (content.text) return content.text;
|
|
135
|
+
return '';
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { detectAgents } from './detect';
|
|
|
4
4
|
import { apiClient, setAuthToken } from './client';
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
6
6
|
import { runAcp } from './acp-runner';
|
|
7
|
+
import { runAcpx } from './acpx-runner';
|
|
7
8
|
import { parseArgs } from 'util';
|
|
8
9
|
import { mkdirSync, existsSync, writeFileSync, readFileSync, appendFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
9
10
|
import { join, dirname } from 'path';
|
|
@@ -16,7 +17,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
|
|
|
16
17
|
const SKILLS_DIR = join(MULTI_DIR, 'skills');
|
|
17
18
|
const STOP_PATH = join(MULTI_DIR, 'stop.flag');
|
|
18
19
|
const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
|
|
19
|
-
const VERSION = '0.
|
|
20
|
+
const VERSION = '0.6.0';
|
|
20
21
|
|
|
21
22
|
const COMMANDS = {
|
|
22
23
|
setup: 'Register this device with a workspace',
|
|
@@ -448,9 +449,9 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
448
449
|
};
|
|
449
450
|
|
|
450
451
|
type ToolEntry = { id: string; tool: string; kind?: string; status?: string; input?: any; results: string[] };
|
|
452
|
+
type Block = { kind: 'text'; text: string } | { kind: 'tool'; id: string };
|
|
451
453
|
const turn = {
|
|
452
|
-
|
|
453
|
-
toolOrder: [] as string[],
|
|
454
|
+
blocks: [] as Block[],
|
|
454
455
|
tools: new Map<string, ToolEntry>(),
|
|
455
456
|
plans: [] as string[],
|
|
456
457
|
progress: [] as string[],
|
|
@@ -458,20 +459,54 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
458
459
|
error: null as null | string,
|
|
459
460
|
};
|
|
460
461
|
|
|
462
|
+
const appendText = (text: string) => {
|
|
463
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
464
|
+
if (last && last.kind === 'text') last.text += text;
|
|
465
|
+
else turn.blocks.push({ kind: 'text', text });
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const upsertTool = (id: string, patch: Partial<ToolEntry>) => {
|
|
469
|
+
const existing = turn.tools.get(id);
|
|
470
|
+
if (existing) {
|
|
471
|
+
if (patch.tool && patch.tool !== 'tool') existing.tool = patch.tool;
|
|
472
|
+
if (patch.kind) existing.kind = patch.kind;
|
|
473
|
+
if (patch.status) existing.status = patch.status;
|
|
474
|
+
if (patch.input !== undefined) existing.input = patch.input;
|
|
475
|
+
} else {
|
|
476
|
+
turn.tools.set(id, { id, tool: patch.tool || 'tool', kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
477
|
+
turn.blocks.push({ kind: 'tool', id });
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const toolLabel = (t: ToolEntry): string => {
|
|
482
|
+
const clean = stripMd(t.tool);
|
|
483
|
+
if (clean && clean !== 'tool') return clean;
|
|
484
|
+
// Fallback from input shape
|
|
485
|
+
if (t.input && typeof t.input === 'object') {
|
|
486
|
+
if (t.input.command) return `Bash: ${String(t.input.command).split('\n')[0].slice(0, 60)}`;
|
|
487
|
+
if (t.input.file_path) return `${t.kind === 'edit' ? 'Edit' : 'Read'}: ${t.input.file_path}`;
|
|
488
|
+
if (t.input.pattern) return `Grep: ${t.input.pattern}`;
|
|
489
|
+
if (t.input.url) return `Fetch: ${t.input.url}`;
|
|
490
|
+
}
|
|
491
|
+
return (t.kind ? `${t.kind}` : 'tool');
|
|
492
|
+
};
|
|
493
|
+
|
|
461
494
|
const render = (): string => {
|
|
462
495
|
const parts: string[] = [];
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
496
|
+
for (const b of turn.blocks) {
|
|
497
|
+
if (b.kind === 'text') {
|
|
498
|
+
if (b.text) parts.push(b.text);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const t = turn.tools.get(b.id);
|
|
467
502
|
if (!t) continue;
|
|
468
503
|
const icon = statusIcon(t.status);
|
|
469
|
-
const
|
|
470
|
-
const head = `${icon} ${
|
|
504
|
+
const label = toolLabel(t);
|
|
505
|
+
const head = `${icon} ${label}${t.status && t.status !== 'completed' ? ` · ${t.status}` : ''}`;
|
|
471
506
|
const body: string[] = [];
|
|
472
507
|
if (t.input !== undefined && t.input !== null) {
|
|
473
508
|
const inputStr = typeof t.input === 'object' ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
474
|
-
body.push('```json\n' + inputStr + '\n```');
|
|
509
|
+
body.push('```json\n' + inputStr.slice(0, 2000) + '\n```');
|
|
475
510
|
}
|
|
476
511
|
if (t.results.length) {
|
|
477
512
|
const joined = t.results.join('\n').slice(-2000);
|
|
@@ -481,7 +516,6 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
481
516
|
}
|
|
482
517
|
|
|
483
518
|
if (turn.plans.length) parts.push('**Plan**\n\n' + turn.plans[turn.plans.length - 1]);
|
|
484
|
-
|
|
485
519
|
if (turn.error) parts.push(`> ❌ ${turn.error}`);
|
|
486
520
|
|
|
487
521
|
if (turn.result) {
|
|
@@ -517,16 +551,15 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
517
551
|
}
|
|
518
552
|
case 'assistant_text': {
|
|
519
553
|
await ensureLiveComment();
|
|
520
|
-
|
|
554
|
+
appendText(p.text);
|
|
521
555
|
hasAssistantText = true;
|
|
522
556
|
schedulePatch();
|
|
523
557
|
break;
|
|
524
558
|
}
|
|
525
559
|
case 'stdout': {
|
|
526
|
-
// Legacy (non-ACP) runners emit stdout — route to live comment too.
|
|
527
560
|
if (p.line) {
|
|
528
561
|
await ensureLiveComment();
|
|
529
|
-
|
|
562
|
+
appendText((turn.blocks.length ? '\n' : '') + p.line);
|
|
530
563
|
hasAssistantText = true;
|
|
531
564
|
schedulePatch();
|
|
532
565
|
}
|
|
@@ -534,33 +567,24 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
534
567
|
}
|
|
535
568
|
case 'tool_call': {
|
|
536
569
|
await ensureLiveComment();
|
|
537
|
-
const id = p.id || `anon-${turn.
|
|
538
|
-
|
|
539
|
-
if (existing) {
|
|
540
|
-
if (p.tool) existing.tool = p.tool;
|
|
541
|
-
if (p.kind) existing.kind = p.kind;
|
|
542
|
-
if (p.status) existing.status = p.status;
|
|
543
|
-
if (p.input !== undefined) existing.input = p.input;
|
|
544
|
-
} else {
|
|
545
|
-
turn.toolOrder.push(id);
|
|
546
|
-
turn.tools.set(id, { id, tool: p.tool || 'tool', kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
547
|
-
}
|
|
570
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
571
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
548
572
|
schedulePatch();
|
|
549
573
|
break;
|
|
550
574
|
}
|
|
551
575
|
case 'tool_result': {
|
|
552
576
|
await ensureLiveComment();
|
|
553
|
-
const
|
|
577
|
+
const lastToolId = [...turn.blocks].reverse().find(b => b.kind === 'tool')?.id;
|
|
578
|
+
const id = p.tool_use_id || lastToolId;
|
|
554
579
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
555
580
|
const content = String(p.content ?? '').trim();
|
|
556
581
|
if (entry && content) {
|
|
557
582
|
entry.results.push(content);
|
|
558
583
|
schedulePatch();
|
|
559
584
|
} else if (content) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
turn.
|
|
563
|
-
turn.tools.set(pid, { id: pid, tool: 'tool result', results: [content] });
|
|
585
|
+
const pid = `result-${turn.tools.size}`;
|
|
586
|
+
upsertTool(pid, { tool: 'tool result' });
|
|
587
|
+
turn.tools.get(pid)!.results.push(content);
|
|
564
588
|
schedulePatch();
|
|
565
589
|
}
|
|
566
590
|
break;
|
|
@@ -569,7 +593,7 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
569
593
|
await ensureLiveComment();
|
|
570
594
|
if (p.is_error) hadError = true;
|
|
571
595
|
if (!hasAssistantText && p.result) {
|
|
572
|
-
|
|
596
|
+
appendText(p.result);
|
|
573
597
|
hasAssistantText = true;
|
|
574
598
|
}
|
|
575
599
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|
|
@@ -593,6 +617,8 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
593
617
|
} catch {}
|
|
594
618
|
const acpCapable = detected.filter(d => d.type === 'claude-code');
|
|
595
619
|
const useAcp = preferType !== 'pi' && acpCapable.length > 0 && process.env.MULTI_LEGACY !== '1';
|
|
620
|
+
// Route pi/codex/openclaw through acpx (handles these agent types natively)
|
|
621
|
+
const useAcpx = !useAcp && preferType && ['pi', 'codex', 'openclaw'].includes(preferType) && process.env.MULTI_LEGACY !== '1';
|
|
596
622
|
|
|
597
623
|
try {
|
|
598
624
|
if (useAcp) {
|
|
@@ -653,13 +679,42 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
653
679
|
},
|
|
654
680
|
});
|
|
655
681
|
log(` acp session ${sessionId.slice(0, 8)}`);
|
|
656
|
-
} else {
|
|
657
|
-
//
|
|
658
|
-
let
|
|
682
|
+
} else if (useAcpx) {
|
|
683
|
+
// Build prompt with preamble (same logic as ACP path, but as one string)
|
|
684
|
+
let preamble = '';
|
|
659
685
|
try {
|
|
660
|
-
const
|
|
661
|
-
|
|
686
|
+
const agentRes = await apiClient.get<any>(`${apiUrl}/api/agents/${task.agent_id}`);
|
|
687
|
+
const agent = agentRes.data;
|
|
688
|
+
if (agent?.prompt) preamble += `# Agent instructions\n\n${agent.prompt}\n\n`;
|
|
689
|
+
const skillsRes = await apiClient.get<any[]>(`${apiUrl}/api/agents/${task.agent_id}/skills`);
|
|
690
|
+
const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
|
|
691
|
+
if (skillList.length) {
|
|
692
|
+
preamble += `# Attached skills (${skillList.length})\n\n`;
|
|
693
|
+
for (const s of skillList) {
|
|
694
|
+
const body = String(s.body || s.description || '').trim();
|
|
695
|
+
if (body) preamble += `## ${s.name}${s.version ? ` v${s.version}` : ''}\n\n${body}\n\n`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
662
698
|
} catch {}
|
|
699
|
+
const base = `${task.title}\n\n${task.description || ''}`.trim();
|
|
700
|
+
let userPart = task.followup ? `${task.followup}\n\n---\nContext (${task.key}): ${task.title}` : (base || task.title);
|
|
701
|
+
userPart = stripSelfMention(userPart, preferType);
|
|
702
|
+
if (attachmentRefs.length) {
|
|
703
|
+
const lines = attachmentRefs.map(a => `- ${a.filename}: ${a.path}`).join('\n');
|
|
704
|
+
userPart += `\n\n---\nAttached files:\n${lines}\n\nWrite generated files to: ${outDir}`;
|
|
705
|
+
} else {
|
|
706
|
+
userPart += `\n\n---\nWrite generated files to: ${outDir}`;
|
|
707
|
+
}
|
|
708
|
+
const full = preamble ? `${preamble}\n---\n\n${userPart}` : userPart;
|
|
709
|
+
log(` acpx runner: ${preferType}`);
|
|
710
|
+
await runAcpx({
|
|
711
|
+
agentType: preferType!,
|
|
712
|
+
prompt: full,
|
|
713
|
+
cwd: workingDir,
|
|
714
|
+
sessionName: `issue-${issueId}`,
|
|
715
|
+
onEvent: eventHandler,
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
663
718
|
const runner = pickRunner(detected, preferType);
|
|
664
719
|
for await (const event of runner(task)) await eventHandler(event);
|
|
665
720
|
}
|