@nathapp/nax 0.42.4 → 0.42.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nax.js +93 -48
- package/package.json +1 -1
- package/src/agents/acp/adapter.ts +17 -6
- package/src/agents/acp/spawn-client.ts +87 -39
- package/src/agents/claude-complete.ts +3 -1
- package/src/agents/types-extended.ts +2 -0
- package/src/agents/types.ts +6 -0
- package/src/cli/plan.ts +6 -1
package/dist/nax.js
CHANGED
|
@@ -3244,7 +3244,10 @@ async function executeComplete(binary, prompt, options) {
|
|
|
3244
3244
|
if (options?.jsonMode) {
|
|
3245
3245
|
cmd.push("--output-format", "json");
|
|
3246
3246
|
}
|
|
3247
|
-
const
|
|
3247
|
+
const spawnOpts = { stdout: "pipe", stderr: "pipe" };
|
|
3248
|
+
if (options?.workdir)
|
|
3249
|
+
spawnOpts.cwd = options.workdir;
|
|
3250
|
+
const proc = _completeDeps.spawn(cmd, spawnOpts);
|
|
3248
3251
|
const exitCode = await proc.exited;
|
|
3249
3252
|
const stdout = await new Response(proc.stdout).text();
|
|
3250
3253
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -18960,6 +18963,8 @@ class SpawnAcpSession {
|
|
|
18960
18963
|
timeoutSeconds;
|
|
18961
18964
|
permissionMode;
|
|
18962
18965
|
env;
|
|
18966
|
+
pidRegistry;
|
|
18967
|
+
activeProc = null;
|
|
18963
18968
|
constructor(opts) {
|
|
18964
18969
|
this.agentName = opts.agentName;
|
|
18965
18970
|
this.sessionName = opts.sessionName;
|
|
@@ -18968,6 +18973,7 @@ class SpawnAcpSession {
|
|
|
18968
18973
|
this.timeoutSeconds = opts.timeoutSeconds;
|
|
18969
18974
|
this.permissionMode = opts.permissionMode;
|
|
18970
18975
|
this.env = opts.env;
|
|
18976
|
+
this.pidRegistry = opts.pidRegistry;
|
|
18971
18977
|
}
|
|
18972
18978
|
async prompt(text) {
|
|
18973
18979
|
const cmd = [
|
|
@@ -18994,35 +19000,50 @@ class SpawnAcpSession {
|
|
|
18994
19000
|
stderr: "pipe",
|
|
18995
19001
|
env: this.env
|
|
18996
19002
|
});
|
|
18997
|
-
proc
|
|
18998
|
-
proc.
|
|
18999
|
-
|
|
19000
|
-
const stdout = await new Response(proc.stdout).text();
|
|
19001
|
-
const stderr = await new Response(proc.stderr).text();
|
|
19002
|
-
if (exitCode !== 0) {
|
|
19003
|
-
getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
|
|
19004
|
-
stderr: stderr.slice(0, 200)
|
|
19005
|
-
});
|
|
19006
|
-
return {
|
|
19007
|
-
messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
|
|
19008
|
-
stopReason: "error"
|
|
19009
|
-
};
|
|
19010
|
-
}
|
|
19003
|
+
this.activeProc = proc;
|
|
19004
|
+
const processPid = proc.pid;
|
|
19005
|
+
await this.pidRegistry?.register(processPid);
|
|
19011
19006
|
try {
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19007
|
+
proc.stdin.write(text);
|
|
19008
|
+
proc.stdin.end();
|
|
19009
|
+
const exitCode = await proc.exited;
|
|
19010
|
+
const stdout = await new Response(proc.stdout).text();
|
|
19011
|
+
const stderr = await new Response(proc.stderr).text();
|
|
19012
|
+
if (exitCode !== 0) {
|
|
19013
|
+
getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
|
|
19014
|
+
stderr: stderr.slice(0, 200)
|
|
19015
|
+
});
|
|
19016
|
+
return {
|
|
19017
|
+
messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
|
|
19018
|
+
stopReason: "error"
|
|
19019
|
+
};
|
|
19020
|
+
}
|
|
19021
|
+
try {
|
|
19022
|
+
const parsed = parseAcpxJsonOutput(stdout);
|
|
19023
|
+
return {
|
|
19024
|
+
messages: [{ role: "assistant", content: parsed.text || "" }],
|
|
19025
|
+
stopReason: "end_turn",
|
|
19026
|
+
cumulative_token_usage: parsed.tokenUsage
|
|
19027
|
+
};
|
|
19028
|
+
} catch (err) {
|
|
19029
|
+
getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
|
|
19030
|
+
stderr: stderr.slice(0, 200)
|
|
19031
|
+
});
|
|
19032
|
+
throw err;
|
|
19033
|
+
}
|
|
19034
|
+
} finally {
|
|
19035
|
+
this.activeProc = null;
|
|
19036
|
+
await this.pidRegistry?.unregister(processPid);
|
|
19023
19037
|
}
|
|
19024
19038
|
}
|
|
19025
19039
|
async close() {
|
|
19040
|
+
if (this.activeProc) {
|
|
19041
|
+
try {
|
|
19042
|
+
this.activeProc.kill(15);
|
|
19043
|
+
getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
|
|
19044
|
+
} catch {}
|
|
19045
|
+
this.activeProc = null;
|
|
19046
|
+
}
|
|
19026
19047
|
const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
|
|
19027
19048
|
getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
|
|
19028
19049
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
@@ -19036,6 +19057,12 @@ class SpawnAcpSession {
|
|
|
19036
19057
|
}
|
|
19037
19058
|
}
|
|
19038
19059
|
async cancelActivePrompt() {
|
|
19060
|
+
if (this.activeProc) {
|
|
19061
|
+
try {
|
|
19062
|
+
this.activeProc.kill(15);
|
|
19063
|
+
getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
|
|
19064
|
+
} catch {}
|
|
19065
|
+
}
|
|
19039
19066
|
const cmd = ["acpx", this.agentName, "cancel"];
|
|
19040
19067
|
getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
|
|
19041
19068
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
@@ -19049,20 +19076,26 @@ class SpawnAcpClient {
|
|
|
19049
19076
|
cwd;
|
|
19050
19077
|
timeoutSeconds;
|
|
19051
19078
|
env;
|
|
19052
|
-
|
|
19079
|
+
pidRegistry;
|
|
19080
|
+
constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
|
|
19053
19081
|
const parts = cmdStr.split(/\s+/);
|
|
19054
19082
|
const modelIdx = parts.indexOf("--model");
|
|
19055
19083
|
this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
|
|
19056
|
-
|
|
19084
|
+
const lastToken = parts[parts.length - 1];
|
|
19085
|
+
if (!lastToken || lastToken.startsWith("-")) {
|
|
19086
|
+
throw new Error(`[acp-adapter] Could not parse agentName from cmdStr: "${cmdStr}"`);
|
|
19087
|
+
}
|
|
19088
|
+
this.agentName = lastToken;
|
|
19057
19089
|
this.cwd = cwd || process.cwd();
|
|
19058
19090
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
19059
19091
|
this.env = buildAllowedEnv2();
|
|
19092
|
+
this.pidRegistry = pidRegistry;
|
|
19060
19093
|
}
|
|
19061
19094
|
async start() {}
|
|
19062
19095
|
async createSession(opts) {
|
|
19063
19096
|
const sessionName = opts.sessionName || `nax-${Date.now()}`;
|
|
19064
|
-
const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "
|
|
19065
|
-
getSafeLogger()?.debug("acp-adapter", `
|
|
19097
|
+
const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
|
|
19098
|
+
getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
|
|
19066
19099
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
19067
19100
|
const exitCode = await proc.exited;
|
|
19068
19101
|
if (exitCode !== 0) {
|
|
@@ -19076,30 +19109,32 @@ class SpawnAcpClient {
|
|
|
19076
19109
|
model: this.model,
|
|
19077
19110
|
timeoutSeconds: this.timeoutSeconds,
|
|
19078
19111
|
permissionMode: opts.permissionMode,
|
|
19079
|
-
env: this.env
|
|
19112
|
+
env: this.env,
|
|
19113
|
+
pidRegistry: this.pidRegistry
|
|
19080
19114
|
});
|
|
19081
19115
|
}
|
|
19082
|
-
async loadSession(sessionName) {
|
|
19083
|
-
const cmd = ["acpx", "--cwd", this.cwd,
|
|
19116
|
+
async loadSession(sessionName, agentName) {
|
|
19117
|
+
const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
|
|
19084
19118
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
19085
19119
|
const exitCode = await proc.exited;
|
|
19086
19120
|
if (exitCode !== 0) {
|
|
19087
19121
|
return null;
|
|
19088
19122
|
}
|
|
19089
19123
|
return new SpawnAcpSession({
|
|
19090
|
-
agentName
|
|
19124
|
+
agentName,
|
|
19091
19125
|
sessionName,
|
|
19092
19126
|
cwd: this.cwd,
|
|
19093
19127
|
model: this.model,
|
|
19094
19128
|
timeoutSeconds: this.timeoutSeconds,
|
|
19095
19129
|
permissionMode: "approve-all",
|
|
19096
|
-
env: this.env
|
|
19130
|
+
env: this.env,
|
|
19131
|
+
pidRegistry: this.pidRegistry
|
|
19097
19132
|
});
|
|
19098
19133
|
}
|
|
19099
19134
|
async close() {}
|
|
19100
19135
|
}
|
|
19101
|
-
function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
|
|
19102
|
-
return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
|
|
19136
|
+
function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
|
|
19137
|
+
return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
|
|
19103
19138
|
}
|
|
19104
19139
|
var _spawnClientDeps;
|
|
19105
19140
|
var init_spawn_client = __esm(() => {
|
|
@@ -19177,9 +19212,12 @@ function buildSessionName(workdir, featureName, storyId, sessionRole) {
|
|
|
19177
19212
|
return parts.join("-");
|
|
19178
19213
|
}
|
|
19179
19214
|
async function ensureAcpSession(client, sessionName, agentName, permissionMode) {
|
|
19215
|
+
if (!agentName) {
|
|
19216
|
+
throw new Error("[acp-adapter] agentName is required for ensureAcpSession");
|
|
19217
|
+
}
|
|
19180
19218
|
if (client.loadSession) {
|
|
19181
19219
|
try {
|
|
19182
|
-
const existing = await client.loadSession(sessionName);
|
|
19220
|
+
const existing = await client.loadSession(sessionName, agentName);
|
|
19183
19221
|
if (existing) {
|
|
19184
19222
|
getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
|
|
19185
19223
|
return existing;
|
|
@@ -19357,7 +19395,7 @@ class AcpAgentAdapter {
|
|
|
19357
19395
|
}
|
|
19358
19396
|
async _runWithClient(options, startTime) {
|
|
19359
19397
|
const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
|
|
19360
|
-
const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
|
|
19398
|
+
const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
|
|
19361
19399
|
await client.start();
|
|
19362
19400
|
let sessionName = options.acpSessionName;
|
|
19363
19401
|
if (!sessionName && options.featureName && options.storyId) {
|
|
@@ -19445,10 +19483,11 @@ class AcpAgentAdapter {
|
|
|
19445
19483
|
const model = _options?.model ?? "default";
|
|
19446
19484
|
const timeoutMs = _options?.timeoutMs ?? 120000;
|
|
19447
19485
|
const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
|
|
19486
|
+
const workdir = _options?.workdir;
|
|
19448
19487
|
let lastError;
|
|
19449
19488
|
for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
19450
19489
|
const cmdStr = `acpx --model ${model} ${this.name}`;
|
|
19451
|
-
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
19490
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
19452
19491
|
await client.start();
|
|
19453
19492
|
let session = null;
|
|
19454
19493
|
try {
|
|
@@ -19528,7 +19567,8 @@ class AcpAgentAdapter {
|
|
|
19528
19567
|
maxInteractionTurns: options.maxInteractionTurns,
|
|
19529
19568
|
featureName: options.featureName,
|
|
19530
19569
|
storyId: options.storyId,
|
|
19531
|
-
sessionRole: options.sessionRole
|
|
19570
|
+
sessionRole: options.sessionRole,
|
|
19571
|
+
pidRegistry: options.pidRegistry
|
|
19532
19572
|
});
|
|
19533
19573
|
if (!result.success) {
|
|
19534
19574
|
throw new Error(`[acp-adapter] plan() failed: ${result.output}`);
|
|
@@ -19598,8 +19638,8 @@ var init_adapter = __esm(() => {
|
|
|
19598
19638
|
async sleep(ms) {
|
|
19599
19639
|
await Bun.sleep(ms);
|
|
19600
19640
|
},
|
|
19601
|
-
createClient(cmdStr, cwd, timeoutSeconds) {
|
|
19602
|
-
return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
|
|
19641
|
+
createClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
|
|
19642
|
+
return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
|
|
19603
19643
|
}
|
|
19604
19644
|
};
|
|
19605
19645
|
});
|
|
@@ -21859,7 +21899,7 @@ var package_default;
|
|
|
21859
21899
|
var init_package = __esm(() => {
|
|
21860
21900
|
package_default = {
|
|
21861
21901
|
name: "@nathapp/nax",
|
|
21862
|
-
version: "0.42.
|
|
21902
|
+
version: "0.42.6",
|
|
21863
21903
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21864
21904
|
type: "module",
|
|
21865
21905
|
bin: {
|
|
@@ -21932,8 +21972,8 @@ var init_version = __esm(() => {
|
|
|
21932
21972
|
NAX_VERSION = package_default.version;
|
|
21933
21973
|
NAX_COMMIT = (() => {
|
|
21934
21974
|
try {
|
|
21935
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
21936
|
-
return "
|
|
21975
|
+
if (/^[0-9a-f]{6,10}$/.test("deb8333"))
|
|
21976
|
+
return "deb8333";
|
|
21937
21977
|
} catch {}
|
|
21938
21978
|
try {
|
|
21939
21979
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -65591,6 +65631,7 @@ init_registry();
|
|
|
65591
65631
|
import { existsSync as existsSync9 } from "fs";
|
|
65592
65632
|
import { join as join10 } from "path";
|
|
65593
65633
|
import { createInterface } from "readline";
|
|
65634
|
+
init_pid_registry();
|
|
65594
65635
|
init_logger2();
|
|
65595
65636
|
|
|
65596
65637
|
// src/prd/schema.ts
|
|
@@ -65781,7 +65822,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
65781
65822
|
const cliAdapter = _deps2.getAgent(agentName);
|
|
65782
65823
|
if (!cliAdapter)
|
|
65783
65824
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65784
|
-
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true });
|
|
65825
|
+
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
|
|
65785
65826
|
try {
|
|
65786
65827
|
const envelope = JSON.parse(rawResponse);
|
|
65787
65828
|
if (envelope?.type === "result" && typeof envelope?.result === "string") {
|
|
@@ -65794,6 +65835,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
65794
65835
|
if (!adapter)
|
|
65795
65836
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65796
65837
|
const interactionBridge = createCliInteractionBridge();
|
|
65838
|
+
const pidRegistry = new PidRegistry(workdir);
|
|
65797
65839
|
logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
|
|
65798
65840
|
try {
|
|
65799
65841
|
await adapter.plan({
|
|
@@ -65805,9 +65847,12 @@ async function planCommand(workdir, config2, options) {
|
|
|
65805
65847
|
config: config2,
|
|
65806
65848
|
modelTier: config2?.plan?.model ?? "balanced",
|
|
65807
65849
|
dangerouslySkipPermissions: config2?.execution?.dangerouslySkipPermissions ?? false,
|
|
65808
|
-
maxInteractionTurns: config2?.agent?.maxInteractionTurns
|
|
65850
|
+
maxInteractionTurns: config2?.agent?.maxInteractionTurns,
|
|
65851
|
+
featureName: options.feature,
|
|
65852
|
+
pidRegistry
|
|
65809
65853
|
});
|
|
65810
65854
|
} finally {
|
|
65855
|
+
await pidRegistry.killAll().catch(() => {});
|
|
65811
65856
|
logger?.info("plan", "Interactive session ended");
|
|
65812
65857
|
}
|
|
65813
65858
|
if (!_deps2.existsSync(outputPath)) {
|
package/package.json
CHANGED
|
@@ -92,7 +92,7 @@ export interface AcpClient {
|
|
|
92
92
|
start(): Promise<void>;
|
|
93
93
|
createSession(opts: { agentName: string; permissionMode: string; sessionName?: string }): Promise<AcpSession>;
|
|
94
94
|
/** Resume an existing named session. Returns null if the session is not found. */
|
|
95
|
-
loadSession?(sessionName: string): Promise<AcpSession | null>;
|
|
95
|
+
loadSession?(sessionName: string, agentName: string): Promise<AcpSession | null>;
|
|
96
96
|
close(): Promise<void>;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -114,8 +114,13 @@ export const _acpAdapterDeps = {
|
|
|
114
114
|
* Default: spawn-based client (shells out to acpx CLI).
|
|
115
115
|
* Override in tests via: _acpAdapterDeps.createClient = mock(...)
|
|
116
116
|
*/
|
|
117
|
-
createClient(
|
|
118
|
-
|
|
117
|
+
createClient(
|
|
118
|
+
cmdStr: string,
|
|
119
|
+
cwd?: string,
|
|
120
|
+
timeoutSeconds?: number,
|
|
121
|
+
pidRegistry?: import("../../execution/pid-registry").PidRegistry,
|
|
122
|
+
): AcpClient {
|
|
123
|
+
return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
|
|
119
124
|
},
|
|
120
125
|
};
|
|
121
126
|
|
|
@@ -180,10 +185,14 @@ export async function ensureAcpSession(
|
|
|
180
185
|
agentName: string,
|
|
181
186
|
permissionMode: string,
|
|
182
187
|
): Promise<AcpSession> {
|
|
188
|
+
if (!agentName) {
|
|
189
|
+
throw new Error("[acp-adapter] agentName is required for ensureAcpSession");
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
// Try to resume existing session first
|
|
184
193
|
if (client.loadSession) {
|
|
185
194
|
try {
|
|
186
|
-
const existing = await client.loadSession(sessionName);
|
|
195
|
+
const existing = await client.loadSession(sessionName, agentName);
|
|
187
196
|
if (existing) {
|
|
188
197
|
getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
|
|
189
198
|
return existing;
|
|
@@ -432,7 +441,7 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
432
441
|
|
|
433
442
|
private async _runWithClient(options: AgentRunOptions, startTime: number): Promise<AgentResult> {
|
|
434
443
|
const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
|
|
435
|
-
const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
|
|
444
|
+
const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
|
|
436
445
|
await client.start();
|
|
437
446
|
|
|
438
447
|
// 1. Resolve session name: explicit > sidecar > derived
|
|
@@ -554,12 +563,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
554
563
|
const model = _options?.model ?? "default";
|
|
555
564
|
const timeoutMs = _options?.timeoutMs ?? 120_000; // 2-min safety net by default
|
|
556
565
|
const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
|
|
566
|
+
const workdir = _options?.workdir;
|
|
557
567
|
|
|
558
568
|
let lastError: Error | undefined;
|
|
559
569
|
|
|
560
570
|
for (let attempt = 0; attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
561
571
|
const cmdStr = `acpx --model ${model} ${this.name}`;
|
|
562
|
-
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
572
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
563
573
|
await client.start();
|
|
564
574
|
|
|
565
575
|
let session: AcpSession | null = null;
|
|
@@ -668,6 +678,7 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
668
678
|
featureName: options.featureName,
|
|
669
679
|
storyId: options.storyId,
|
|
670
680
|
sessionRole: options.sessionRole,
|
|
681
|
+
pidRegistry: options.pidRegistry,
|
|
671
682
|
});
|
|
672
683
|
|
|
673
684
|
if (!result.success) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* acpx <agent> cancel → session.cancelActivePrompt()
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { PidRegistry } from "../../execution/pid-registry";
|
|
15
16
|
import { getSafeLogger } from "../../logger";
|
|
16
17
|
import type { AcpClient, AcpSession, AcpSessionResponse } from "./adapter";
|
|
17
18
|
import { parseAcpxJsonOutput } from "./parser";
|
|
@@ -96,6 +97,8 @@ class SpawnAcpSession implements AcpSession {
|
|
|
96
97
|
private readonly timeoutSeconds: number;
|
|
97
98
|
private readonly permissionMode: string;
|
|
98
99
|
private readonly env: Record<string, string | undefined>;
|
|
100
|
+
private readonly pidRegistry?: PidRegistry;
|
|
101
|
+
private activeProc: { pid: number; kill(signal?: number): void } | null = null;
|
|
99
102
|
|
|
100
103
|
constructor(opts: {
|
|
101
104
|
agentName: string;
|
|
@@ -105,6 +108,7 @@ class SpawnAcpSession implements AcpSession {
|
|
|
105
108
|
timeoutSeconds: number;
|
|
106
109
|
permissionMode: string;
|
|
107
110
|
env: Record<string, string | undefined>;
|
|
111
|
+
pidRegistry?: PidRegistry;
|
|
108
112
|
}) {
|
|
109
113
|
this.agentName = opts.agentName;
|
|
110
114
|
this.sessionName = opts.sessionName;
|
|
@@ -113,6 +117,7 @@ class SpawnAcpSession implements AcpSession {
|
|
|
113
117
|
this.timeoutSeconds = opts.timeoutSeconds;
|
|
114
118
|
this.permissionMode = opts.permissionMode;
|
|
115
119
|
this.env = opts.env;
|
|
120
|
+
this.pidRegistry = opts.pidRegistry;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async prompt(text: string): Promise<AcpSessionResponse> {
|
|
@@ -143,40 +148,60 @@ class SpawnAcpSession implements AcpSession {
|
|
|
143
148
|
env: this.env,
|
|
144
149
|
});
|
|
145
150
|
|
|
146
|
-
proc
|
|
147
|
-
proc.
|
|
151
|
+
this.activeProc = proc;
|
|
152
|
+
const processPid = proc.pid;
|
|
153
|
+
await this.pidRegistry?.register(processPid);
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
try {
|
|
156
|
+
proc.stdin.write(text);
|
|
157
|
+
proc.stdin.end();
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
// Return error response so the adapter can handle it
|
|
158
|
-
return {
|
|
159
|
-
messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
|
|
160
|
-
stopReason: "error",
|
|
161
|
-
};
|
|
162
|
-
}
|
|
159
|
+
const exitCode = await proc.exited;
|
|
160
|
+
const stdout = await new Response(proc.stdout).text();
|
|
161
|
+
const stderr = await new Response(proc.stderr).text();
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
if (exitCode !== 0) {
|
|
164
|
+
getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
|
|
165
|
+
stderr: stderr.slice(0, 200),
|
|
166
|
+
});
|
|
167
|
+
// Return error response so the adapter can handle it
|
|
168
|
+
return {
|
|
169
|
+
messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
|
|
170
|
+
stopReason: "error",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const parsed = parseAcpxJsonOutput(stdout);
|
|
176
|
+
return {
|
|
177
|
+
messages: [{ role: "assistant", content: parsed.text || "" }],
|
|
178
|
+
stopReason: "end_turn",
|
|
179
|
+
cumulative_token_usage: parsed.tokenUsage,
|
|
180
|
+
};
|
|
181
|
+
} catch (err) {
|
|
182
|
+
getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
|
|
183
|
+
stderr: stderr.slice(0, 200),
|
|
184
|
+
});
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
this.activeProc = null;
|
|
189
|
+
await this.pidRegistry?.unregister(processPid);
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
async close(): Promise<void> {
|
|
194
|
+
// Kill in-flight prompt process first (if any)
|
|
195
|
+
if (this.activeProc) {
|
|
196
|
+
try {
|
|
197
|
+
this.activeProc.kill(15); // SIGTERM
|
|
198
|
+
getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
|
|
199
|
+
} catch {
|
|
200
|
+
// Process may have already exited
|
|
201
|
+
}
|
|
202
|
+
this.activeProc = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
180
205
|
const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
|
|
181
206
|
getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
|
|
182
207
|
|
|
@@ -193,6 +218,16 @@ class SpawnAcpSession implements AcpSession {
|
|
|
193
218
|
}
|
|
194
219
|
|
|
195
220
|
async cancelActivePrompt(): Promise<void> {
|
|
221
|
+
// Kill in-flight prompt process directly (faster than acpx cancel)
|
|
222
|
+
if (this.activeProc) {
|
|
223
|
+
try {
|
|
224
|
+
this.activeProc.kill(15); // SIGTERM
|
|
225
|
+
getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
|
|
226
|
+
} catch {
|
|
227
|
+
// Process may have already exited
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
196
231
|
const cmd = ["acpx", this.agentName, "cancel"];
|
|
197
232
|
getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
|
|
198
233
|
|
|
@@ -211,7 +246,7 @@ class SpawnAcpSession implements AcpSession {
|
|
|
211
246
|
* The cmdStr is parsed to extract --model and agent name:
|
|
212
247
|
* "acpx --model claude-sonnet-4-5 claude" → model=claude-sonnet-4-5, agent=claude
|
|
213
248
|
*
|
|
214
|
-
* createSession() spawns: acpx
|
|
249
|
+
* createSession() spawns: acpx <agent> sessions ensure --name <name>
|
|
215
250
|
* loadSession() tries to resume an existing named session.
|
|
216
251
|
*/
|
|
217
252
|
export class SpawnAcpClient implements AcpClient {
|
|
@@ -220,17 +255,23 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
220
255
|
private readonly cwd: string;
|
|
221
256
|
private readonly timeoutSeconds: number;
|
|
222
257
|
private readonly env: Record<string, string | undefined>;
|
|
258
|
+
private readonly pidRegistry?: PidRegistry;
|
|
223
259
|
|
|
224
|
-
constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number) {
|
|
260
|
+
constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number, pidRegistry?: PidRegistry) {
|
|
225
261
|
// Parse: "acpx --model <model> <agentName>"
|
|
226
262
|
const parts = cmdStr.split(/\s+/);
|
|
227
263
|
const modelIdx = parts.indexOf("--model");
|
|
228
264
|
this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
|
|
229
|
-
// Agent name is the last non-flag token
|
|
230
|
-
|
|
265
|
+
// Agent name is the last non-flag token — must be present and not a flag
|
|
266
|
+
const lastToken = parts[parts.length - 1];
|
|
267
|
+
if (!lastToken || lastToken.startsWith("-")) {
|
|
268
|
+
throw new Error(`[acp-adapter] Could not parse agentName from cmdStr: "${cmdStr}"`);
|
|
269
|
+
}
|
|
270
|
+
this.agentName = lastToken;
|
|
231
271
|
this.cwd = cwd || process.cwd();
|
|
232
272
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
233
273
|
this.env = buildAllowedEnv();
|
|
274
|
+
this.pidRegistry = pidRegistry;
|
|
234
275
|
}
|
|
235
276
|
|
|
236
277
|
async start(): Promise<void> {
|
|
@@ -244,9 +285,9 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
244
285
|
}): Promise<AcpSession> {
|
|
245
286
|
const sessionName = opts.sessionName || `nax-${Date.now()}`;
|
|
246
287
|
|
|
247
|
-
//
|
|
248
|
-
const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "
|
|
249
|
-
getSafeLogger()?.debug("acp-adapter", `
|
|
288
|
+
// Ensure session exists via CLI
|
|
289
|
+
const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
|
|
290
|
+
getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
|
|
250
291
|
|
|
251
292
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
252
293
|
const exitCode = await proc.exited;
|
|
@@ -264,12 +305,13 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
264
305
|
timeoutSeconds: this.timeoutSeconds,
|
|
265
306
|
permissionMode: opts.permissionMode,
|
|
266
307
|
env: this.env,
|
|
308
|
+
pidRegistry: this.pidRegistry,
|
|
267
309
|
});
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
async loadSession(sessionName: string): Promise<AcpSession | null> {
|
|
312
|
+
async loadSession(sessionName: string, agentName: string): Promise<AcpSession | null> {
|
|
271
313
|
// Try to ensure session exists — if it does, acpx returns success
|
|
272
|
-
const cmd = ["acpx", "--cwd", this.cwd,
|
|
314
|
+
const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
|
|
273
315
|
|
|
274
316
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
275
317
|
const exitCode = await proc.exited;
|
|
@@ -279,13 +321,14 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
279
321
|
}
|
|
280
322
|
|
|
281
323
|
return new SpawnAcpSession({
|
|
282
|
-
agentName
|
|
324
|
+
agentName,
|
|
283
325
|
sessionName,
|
|
284
326
|
cwd: this.cwd,
|
|
285
327
|
model: this.model,
|
|
286
328
|
timeoutSeconds: this.timeoutSeconds,
|
|
287
329
|
permissionMode: "approve-all", // Default for resumed sessions
|
|
288
330
|
env: this.env,
|
|
331
|
+
pidRegistry: this.pidRegistry,
|
|
289
332
|
});
|
|
290
333
|
}
|
|
291
334
|
|
|
@@ -302,6 +345,11 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
302
345
|
* Create a spawn-based ACP client. This is the default production factory.
|
|
303
346
|
* The cmdStr format is: "acpx --model <model> <agentName>"
|
|
304
347
|
*/
|
|
305
|
-
export function createSpawnAcpClient(
|
|
306
|
-
|
|
348
|
+
export function createSpawnAcpClient(
|
|
349
|
+
cmdStr: string,
|
|
350
|
+
cwd?: string,
|
|
351
|
+
timeoutSeconds?: number,
|
|
352
|
+
pidRegistry?: PidRegistry,
|
|
353
|
+
): AcpClient {
|
|
354
|
+
return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
|
|
307
355
|
}
|
|
@@ -51,7 +51,9 @@ export async function executeComplete(binary: string, prompt: string, options?:
|
|
|
51
51
|
cmd.push("--output-format", "json");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const
|
|
54
|
+
const spawnOpts: { stdout: "pipe"; stderr: "pipe"; cwd?: string } = { stdout: "pipe", stderr: "pipe" };
|
|
55
|
+
if (options?.workdir) spawnOpts.cwd = options.workdir;
|
|
56
|
+
const proc = _completeDeps.spawn(cmd, spawnOpts);
|
|
55
57
|
const exitCode = await proc.exited;
|
|
56
58
|
|
|
57
59
|
const stdout = await new Response(proc.stdout).text();
|
|
@@ -55,6 +55,8 @@ export interface PlanOptions {
|
|
|
55
55
|
* Used to persist the name to status.json for plan→run session continuity.
|
|
56
56
|
*/
|
|
57
57
|
onAcpSessionCreated?: (sessionName: string) => Promise<void> | void;
|
|
58
|
+
/** PID registry for tracking spawned agent processes — cleanup on crash/SIGTERM */
|
|
59
|
+
pidRegistry?: import("../execution/pid-registry").PidRegistry;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
package/src/agents/types.ts
CHANGED
|
@@ -102,6 +102,12 @@ export interface CompleteOptions {
|
|
|
102
102
|
model?: string;
|
|
103
103
|
/** Whether to skip permission prompts (maps to permissionMode in ACP) */
|
|
104
104
|
dangerouslySkipPermissions?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Working directory for the completion call.
|
|
107
|
+
* Used by ACP adapter to set --cwd on the spawned acpx session.
|
|
108
|
+
* CLI adapter uses this as the process cwd when spawning the agent binary.
|
|
109
|
+
*/
|
|
110
|
+
workdir?: string;
|
|
105
111
|
/**
|
|
106
112
|
* Timeout for the completion call in milliseconds.
|
|
107
113
|
* Adapters that support it (e.g. ACP) will enforce this as a hard deadline.
|
package/src/cli/plan.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { AgentAdapter } from "../agents/types";
|
|
|
15
15
|
import { scanCodebase } from "../analyze/scanner";
|
|
16
16
|
import type { CodebaseScan } from "../analyze/types";
|
|
17
17
|
import type { NaxConfig } from "../config";
|
|
18
|
+
import { PidRegistry } from "../execution/pid-registry";
|
|
18
19
|
import { getLogger } from "../logger";
|
|
19
20
|
import { validatePlanOutput } from "../prd/schema";
|
|
20
21
|
|
|
@@ -107,7 +108,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
107
108
|
const prompt = buildPlanningPrompt(specContent, codebaseContext);
|
|
108
109
|
const cliAdapter = _deps.getAgent(agentName);
|
|
109
110
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
110
|
-
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true });
|
|
111
|
+
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
|
|
111
112
|
// CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
|
|
112
113
|
try {
|
|
113
114
|
const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
|
|
@@ -123,6 +124,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
123
124
|
const adapter = _deps.getAgent(agentName, config);
|
|
124
125
|
if (!adapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
125
126
|
const interactionBridge = createCliInteractionBridge();
|
|
127
|
+
const pidRegistry = new PidRegistry(workdir);
|
|
126
128
|
logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
|
|
127
129
|
try {
|
|
128
130
|
await adapter.plan({
|
|
@@ -135,8 +137,11 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
135
137
|
modelTier: config?.plan?.model ?? "balanced",
|
|
136
138
|
dangerouslySkipPermissions: config?.execution?.dangerouslySkipPermissions ?? false,
|
|
137
139
|
maxInteractionTurns: config?.agent?.maxInteractionTurns,
|
|
140
|
+
featureName: options.feature,
|
|
141
|
+
pidRegistry,
|
|
138
142
|
});
|
|
139
143
|
} finally {
|
|
144
|
+
await pidRegistry.killAll().catch(() => {});
|
|
140
145
|
logger?.info("plan", "Interactive session ended");
|
|
141
146
|
}
|
|
142
147
|
// Read back from file written by agent
|