nexus-agentd 0.2.0 → 0.2.1
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/README.md +2 -1
- package/dist/acp/runtime.js +9 -6
- package/dist/drivers/pi.d.ts +1 -0
- package/dist/drivers/stdio.js +2 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -4
- package/dist/process-tree.d.ts +5 -0
- package/dist/process-tree.js +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,7 +215,8 @@ GET /v1/sessions/:id/events
|
|
|
215
215
|
`/v1/meta` 和 Session 响应包含 Gateway `instanceId`;完成的 Session 还包含与当前 Run 绑定的
|
|
216
216
|
`completion` 证明,客户端可识别进程重启和迟到/伪造的完成状态。授权与输入通过精确的
|
|
217
217
|
`requestId` 解析;过期 ID 返回 `409`,不会误答后续请求。`DELETE /v1/sessions/:id` 会取消活动任务、
|
|
218
|
-
释放 Agent
|
|
218
|
+
释放 Agent 整个进程组并移除内存 Session。Gateway 停止时会先终止 Session,再在有限宽限期后关闭残留
|
|
219
|
+
HTTP/SSE 连接,避免长连接或 Agent 孙进程阻塞服务重启。
|
|
219
220
|
|
|
220
221
|
API Key 的 Agent scope 在 Agent inventory、Session 创建和后续 Session 操作上都会检查;Session
|
|
221
222
|
还绑定创建它的 Key,其他 Key 即使拥有同一 Agent scope 也不能读取或控制该 Session。
|
package/dist/acp/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import * as acp from '@agentclientprotocol/sdk';
|
|
7
|
+
import { terminateProcessTree } from '../process-tree.js';
|
|
7
8
|
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
8
9
|
export class AcpProcessRuntime {
|
|
9
10
|
driver;
|
|
@@ -61,7 +62,7 @@ export class AcpProcessRuntime {
|
|
|
61
62
|
},
|
|
62
63
|
clientInfo: {
|
|
63
64
|
name: 'nexus-agentd',
|
|
64
|
-
version: '0.2.
|
|
65
|
+
version: '0.2.1'
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
@@ -224,15 +225,17 @@ export class AcpProcessRuntime {
|
|
|
224
225
|
}
|
|
225
226
|
this.connection?.close();
|
|
226
227
|
this.connection = undefined;
|
|
228
|
+
const child = this.process;
|
|
229
|
+
this.process = undefined;
|
|
230
|
+
if (child) {
|
|
231
|
+
await terminateProcessTree(child, {
|
|
232
|
+
ownsProcessGroup: this.driver.ownsProcessGroup
|
|
233
|
+
});
|
|
234
|
+
}
|
|
227
235
|
if (this.inputDirectory) {
|
|
228
236
|
await rm(this.inputDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
229
237
|
this.inputDirectory = undefined;
|
|
230
238
|
}
|
|
231
|
-
const child = this.process;
|
|
232
|
-
this.process = undefined;
|
|
233
|
-
if (child && child.exitCode === null && child.signalCode === null) {
|
|
234
|
-
child.kill();
|
|
235
|
-
}
|
|
236
239
|
}
|
|
237
240
|
requestPermission(params) {
|
|
238
241
|
if (this.driver.permissionPolicy === 'deny') {
|
package/dist/drivers/pi.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export declare function createPiDriver(id: string, config: AgentdDriverConfig):
|
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
permissionPolicy: import("../types.js").PermissionPolicy;
|
|
11
11
|
permissionTimeoutMs: number;
|
|
12
|
+
ownsProcessGroup?: boolean;
|
|
12
13
|
spawn(workspace: string): import("child_process").ChildProcessWithoutNullStreams;
|
|
13
14
|
};
|
package/dist/drivers/stdio.js
CHANGED
|
@@ -28,6 +28,7 @@ export function createStdioAcpDriver(id, config, defaults) {
|
|
|
28
28
|
env,
|
|
29
29
|
permissionPolicy: config.permissionPolicy || 'ask',
|
|
30
30
|
permissionTimeoutMs: config.permissionTimeoutMs || 15 * 60 * 1000,
|
|
31
|
+
ownsProcessGroup: process.platform !== 'win32',
|
|
31
32
|
async probe() {
|
|
32
33
|
try {
|
|
33
34
|
const version = await probeCommand(command, defaults.probeArgs || ['--version'], env, name);
|
|
@@ -55,6 +56,7 @@ export function createStdioAcpDriver(id, config, defaults) {
|
|
|
55
56
|
return spawn(command, args, {
|
|
56
57
|
cwd: workspace,
|
|
57
58
|
env,
|
|
59
|
+
detached: process.platform !== 'win32',
|
|
58
60
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
59
61
|
});
|
|
60
62
|
}
|
package/dist/drivers/types.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AgentDriver {
|
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
permissionPolicy: PermissionPolicy;
|
|
11
11
|
permissionTimeoutMs: number;
|
|
12
|
+
/** The spawned Agent owns a dedicated process group that can be terminated as a tree. */
|
|
13
|
+
ownsProcessGroup?: boolean;
|
|
12
14
|
probe(): Promise<AgentdAgentView>;
|
|
13
15
|
spawn(workspace: string): ChildProcessWithoutNullStreams;
|
|
14
16
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function startAgentd(configPath: string): Promise<{
|
|
|
10
10
|
controlPlane: AgentdControlPlane;
|
|
11
11
|
close(): Promise<void>;
|
|
12
12
|
}>;
|
|
13
|
+
export declare function closeServer(server: Server, graceMs?: number): Promise<void>;
|
|
13
14
|
export * from './config.js';
|
|
14
15
|
export * from './control-plane.js';
|
|
15
16
|
export * from './server.js';
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,7 @@ export async function startAgentd(configPath) {
|
|
|
24
24
|
runStore,
|
|
25
25
|
controlPlane,
|
|
26
26
|
async close() {
|
|
27
|
-
await closeServer(server);
|
|
28
|
-
await sessions.shutdown();
|
|
27
|
+
await Promise.all([closeServer(server), sessions.shutdown()]);
|
|
29
28
|
await runStore.flush();
|
|
30
29
|
}
|
|
31
30
|
};
|
|
@@ -45,9 +44,24 @@ function listen(server, port, host) {
|
|
|
45
44
|
server.listen(port, host);
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
|
-
function closeServer(server) {
|
|
47
|
+
export function closeServer(server, graceMs = 2_000) {
|
|
49
48
|
return new Promise((resolve, reject) => {
|
|
50
|
-
|
|
49
|
+
let settled = false;
|
|
50
|
+
const finish = (error) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (error)
|
|
56
|
+
reject(error);
|
|
57
|
+
else
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
server.closeAllConnections?.();
|
|
62
|
+
finish();
|
|
63
|
+
}, Math.max(0, graceMs));
|
|
64
|
+
server.close((error) => finish(error || undefined));
|
|
51
65
|
server.closeIdleConnections?.();
|
|
52
66
|
});
|
|
53
67
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const DEFAULT_GRACE_MS = 2_000;
|
|
3
|
+
const FORCE_KILL_WAIT_MS = 1_000;
|
|
4
|
+
export async function terminateProcessTree(child, options = {}) {
|
|
5
|
+
const pid = child.pid;
|
|
6
|
+
if (!pid || hasExited(child))
|
|
7
|
+
return;
|
|
8
|
+
const graceMs = Math.max(0, options.graceMs ?? DEFAULT_GRACE_MS);
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
await taskkill(pid, false);
|
|
11
|
+
if (await waitForExit(child, graceMs))
|
|
12
|
+
return;
|
|
13
|
+
await taskkill(pid, true);
|
|
14
|
+
await waitForExit(child, FORCE_KILL_WAIT_MS);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (options.ownsProcessGroup === true) {
|
|
18
|
+
signalUnixGroup(pid, 'SIGTERM');
|
|
19
|
+
if (await waitForUnixGroupExit(pid, graceMs))
|
|
20
|
+
return;
|
|
21
|
+
signalUnixGroup(pid, 'SIGKILL');
|
|
22
|
+
await waitForUnixGroupExit(pid, FORCE_KILL_WAIT_MS);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
signalUnix(child, 'SIGTERM');
|
|
26
|
+
if (await waitForExit(child, graceMs))
|
|
27
|
+
return;
|
|
28
|
+
signalUnix(child, 'SIGKILL');
|
|
29
|
+
await waitForExit(child, FORCE_KILL_WAIT_MS);
|
|
30
|
+
}
|
|
31
|
+
function signalUnix(child, signal) {
|
|
32
|
+
try {
|
|
33
|
+
child.kill(signal);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
function signalUnixGroup(pid, signal) {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(-pid, signal);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
async function waitForUnixGroupExit(pid, timeoutMs) {
|
|
44
|
+
const deadline = Date.now() + timeoutMs;
|
|
45
|
+
while (unixGroupExists(pid)) {
|
|
46
|
+
const remaining = deadline - Date.now();
|
|
47
|
+
if (remaining <= 0)
|
|
48
|
+
return false;
|
|
49
|
+
await delay(Math.min(25, remaining));
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function unixGroupExists(pid) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(-pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return error.code !== 'ESRCH';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function waitForExit(child, timeoutMs) {
|
|
63
|
+
if (hasExited(child))
|
|
64
|
+
return Promise.resolve(true);
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let settled = false;
|
|
67
|
+
const finish = (exited) => {
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
settled = true;
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
child.off('exit', onExit);
|
|
73
|
+
resolve(exited);
|
|
74
|
+
};
|
|
75
|
+
const onExit = () => finish(true);
|
|
76
|
+
const timer = setTimeout(() => finish(hasExited(child)), timeoutMs);
|
|
77
|
+
child.once('exit', onExit);
|
|
78
|
+
if (hasExited(child))
|
|
79
|
+
finish(true);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function taskkill(pid, force) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const args = ['/PID', String(pid), '/T'];
|
|
85
|
+
if (force)
|
|
86
|
+
args.push('/F');
|
|
87
|
+
const killer = spawn('taskkill.exe', args, { stdio: 'ignore', windowsHide: true });
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
killer.kill();
|
|
90
|
+
resolve();
|
|
91
|
+
}, FORCE_KILL_WAIT_MS);
|
|
92
|
+
const finish = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
killer.once('error', finish);
|
|
97
|
+
killer.once('exit', finish);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function delay(ms) {
|
|
101
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
|
+
}
|
|
103
|
+
function hasExited(child) {
|
|
104
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
105
|
+
}
|