@engineeros/connector 0.16.0 → 0.17.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/README.md +4 -13
- package/bin/engineeros-connector.mjs +132 -1085
- package/package.json +24 -41
- package/src/acp-client.mjs +0 -468
- package/src/agent-harness.mjs +0 -264
- package/src/agent-registry.mjs +0 -643
- package/src/assessment-spool.mjs +0 -315
- package/src/capabilities.mjs +0 -24
- package/src/cli-args.mjs +0 -18
- package/src/codex-app-server.mjs +0 -250
- package/src/config.mjs +0 -109
- package/src/connection.mjs +0 -80
- package/src/mcp-server.mjs +0 -256
- package/src/runner.mjs +0 -2124
- package/src/skills/change-planning/SKILL.md +0 -12
- package/src/skills/change-verification/SKILL.md +0 -12
- package/src/skills/codebase-research/SKILL.md +0 -12
- package/src/skills/goal-execution/SKILL.md +0 -12
package/package.json
CHANGED
|
@@ -1,41 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@engineeros/connector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/vinpuli/engineerosv2.git",
|
|
29
|
-
"directory": "packages/engineeros-connector"
|
|
30
|
-
},
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/vinpuli/engineerosv2/issues"
|
|
33
|
-
},
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@agentclientprotocol/sdk": "1.3.0",
|
|
39
|
-
"zod": "3.25.76"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@engineeros/connector",
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"bin/engineeros-connector.mjs",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"engineeros-connector": "bin/engineeros-connector.mjs"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@agentclientprotocol/sdk": "1.3.0",
|
|
22
|
+
"zod": "3.25.76"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/acp-client.mjs
DELETED
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { Readable, Writable } from "node:stream";
|
|
3
|
-
import * as acp from "@agentclientprotocol/sdk";
|
|
4
|
-
|
|
5
|
-
const RUNTIME_IDLE_MS = 30 * 60 * 1_000;
|
|
6
|
-
const runtimes = new Map();
|
|
7
|
-
|
|
8
|
-
export function permissionOutcome(options = [], allow = false) {
|
|
9
|
-
if (!allow) return { outcome: { outcome: "cancelled" } };
|
|
10
|
-
const selected =
|
|
11
|
-
options.find((option) => option.kind === "allow_once") ??
|
|
12
|
-
options.find((option) => option.kind === "allow_always");
|
|
13
|
-
return selected
|
|
14
|
-
? { outcome: { outcome: "selected", optionId: selected.optionId } }
|
|
15
|
-
: { outcome: { outcome: "cancelled" } };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function launchAcpAgent(
|
|
19
|
-
workspace,
|
|
20
|
-
prompt,
|
|
21
|
-
config,
|
|
22
|
-
callbacks = {},
|
|
23
|
-
options = {},
|
|
24
|
-
) {
|
|
25
|
-
if (!config.agent_command) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"No ACP coding agent is configured. Pair again with --agent or --agent-command and optional --agent-args JSON.",
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
const persistent = options.persistent === true;
|
|
31
|
-
const runtimeKey = persistent ? acpRuntimeKey(workspace, config) : null;
|
|
32
|
-
let runtime = runtimeKey ? runtimes.get(runtimeKey) : null;
|
|
33
|
-
if (!runtime?.isRunning()) {
|
|
34
|
-
runtime = new AcpRuntime(workspace, config, () => {
|
|
35
|
-
if (runtimeKey && runtimes.get(runtimeKey) === runtime) {
|
|
36
|
-
runtimes.delete(runtimeKey);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
if (runtimeKey) runtimes.set(runtimeKey, runtime);
|
|
40
|
-
}
|
|
41
|
-
const sessionKey = options.sessionKey || `isolated-${crypto.randomUUID()}`;
|
|
42
|
-
const completed = runtime
|
|
43
|
-
.prompt({
|
|
44
|
-
sessionKey,
|
|
45
|
-
prompt,
|
|
46
|
-
previousSessionId: options.previousSessionId,
|
|
47
|
-
sandbox: options.sandbox,
|
|
48
|
-
profile: options.profile,
|
|
49
|
-
callbacks,
|
|
50
|
-
})
|
|
51
|
-
.finally(async () => {
|
|
52
|
-
if (!persistent) await runtime.dispose();
|
|
53
|
-
});
|
|
54
|
-
runtime.child.engineerOsCancel = () => runtime.dispose();
|
|
55
|
-
return {
|
|
56
|
-
child: runtime.child,
|
|
57
|
-
completed,
|
|
58
|
-
cancel: () => runtime.cancel(sessionKey),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function disposeAcpRuntimes() {
|
|
63
|
-
const active = [...runtimes.values()];
|
|
64
|
-
runtimes.clear();
|
|
65
|
-
await Promise.all(active.map((runtime) => runtime.dispose()));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function inspectAcpExecutionProfiles(workspace, config) {
|
|
69
|
-
const runtime = new AcpRuntime(workspace, config, () => {});
|
|
70
|
-
try {
|
|
71
|
-
await runtime.ready;
|
|
72
|
-
const response = await runtime.context.request(acp.methods.agent.session.new, {
|
|
73
|
-
cwd: workspace,
|
|
74
|
-
mcpServers: [],
|
|
75
|
-
});
|
|
76
|
-
return executionProfilesFromConfigOptions(response.configOptions);
|
|
77
|
-
} finally {
|
|
78
|
-
await runtime.dispose();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function executionProfilesFromConfigOptions(configOptions = []) {
|
|
83
|
-
const selectOption = (category) =>
|
|
84
|
-
configOptions.find((candidate) => candidate.category === category && candidate.type === "select");
|
|
85
|
-
const selectValues = (option) =>
|
|
86
|
-
(option?.options || []).flatMap((candidate) =>
|
|
87
|
-
Array.isArray(candidate.options) ? candidate.options : [candidate],
|
|
88
|
-
);
|
|
89
|
-
const modelOption = selectOption("model");
|
|
90
|
-
const effortOption = selectOption("thought_level");
|
|
91
|
-
const models = selectValues(modelOption);
|
|
92
|
-
const efforts = selectValues(effortOption);
|
|
93
|
-
return {
|
|
94
|
-
model_selection: models.length > 0,
|
|
95
|
-
model_profiles: models.map((model) => ({
|
|
96
|
-
id: model.value,
|
|
97
|
-
name: model.name || model.value,
|
|
98
|
-
description: model.description || "",
|
|
99
|
-
is_default: model.value === modelOption.currentValue,
|
|
100
|
-
default_reasoning_effort: effortOption?.currentValue || null,
|
|
101
|
-
reasoning_efforts: efforts.map((effort) => effort.value),
|
|
102
|
-
})),
|
|
103
|
-
reasoning_efforts: efforts.map((effort) => effort.value),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function activeAcpRuntimeCount() {
|
|
108
|
-
return runtimes.size;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
class AcpRuntime {
|
|
112
|
-
constructor(workspace, config, onClose) {
|
|
113
|
-
this.workspace = workspace;
|
|
114
|
-
this.config = config;
|
|
115
|
-
this.onClose = onClose;
|
|
116
|
-
this.sessions = new Map();
|
|
117
|
-
this.turns = new Map();
|
|
118
|
-
this.stderr = "";
|
|
119
|
-
this.disposed = false;
|
|
120
|
-
this.disposePromise = null;
|
|
121
|
-
this.idleTimer = null;
|
|
122
|
-
this.child = spawn(
|
|
123
|
-
config.agent_command,
|
|
124
|
-
Array.isArray(config.agent_args) ? config.agent_args : [],
|
|
125
|
-
{
|
|
126
|
-
cwd: workspace,
|
|
127
|
-
env: { ...process.env, ...(config.agent_env || {}) },
|
|
128
|
-
windowsHide: true,
|
|
129
|
-
shell:
|
|
130
|
-
process.platform === "win32" &&
|
|
131
|
-
/\.(cmd|bat)$/i.test(config.agent_command),
|
|
132
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
this.child.stderr.setEncoding("utf8");
|
|
136
|
-
this.child.stderr.on("data", (chunk) => {
|
|
137
|
-
this.stderr = `${this.stderr}${chunk}`.slice(-4_000);
|
|
138
|
-
for (const turn of this.turns.values()) {
|
|
139
|
-
turn.callbacks.onEvent?.({
|
|
140
|
-
type: "agent.stderr",
|
|
141
|
-
message: String(chunk).trim().slice(0, 500),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
this.child.once("error", (error) => {
|
|
146
|
-
this.spawnError = error;
|
|
147
|
-
});
|
|
148
|
-
const stream = acp.ndJsonStream(
|
|
149
|
-
Writable.toWeb(this.child.stdin),
|
|
150
|
-
Readable.toWeb(this.child.stdout),
|
|
151
|
-
);
|
|
152
|
-
this.connection = acp
|
|
153
|
-
.client({ name: "EngineerOS" })
|
|
154
|
-
.onRequest(acp.methods.client.session.requestPermission, (ctx) =>
|
|
155
|
-
this.permissionOutcome(ctx.params),
|
|
156
|
-
)
|
|
157
|
-
.onNotification(acp.methods.client.session.update, (ctx) =>
|
|
158
|
-
this.handleUpdate(ctx.params),
|
|
159
|
-
)
|
|
160
|
-
.connect(stream);
|
|
161
|
-
this.context = this.connection.agent;
|
|
162
|
-
this.ready = this.initialize();
|
|
163
|
-
void this.ready.catch(() => {
|
|
164
|
-
void this.dispose();
|
|
165
|
-
});
|
|
166
|
-
this.child.stdout.once("end", () => this.closed());
|
|
167
|
-
this.child.stdout.once("close", () => this.closed());
|
|
168
|
-
this.child.once("exit", () => this.closed());
|
|
169
|
-
void this.connection.closed.then(
|
|
170
|
-
() => this.closed(),
|
|
171
|
-
() => this.closed(),
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async initialize() {
|
|
176
|
-
try {
|
|
177
|
-
const initialized = await this.context.request(
|
|
178
|
-
acp.methods.agent.initialize,
|
|
179
|
-
{
|
|
180
|
-
protocolVersion: acp.PROTOCOL_VERSION,
|
|
181
|
-
clientCapabilities: {
|
|
182
|
-
session: { configOptions: { boolean: {} } },
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
this.capabilities = initialized.agentCapabilities ?? {};
|
|
187
|
-
return initialized;
|
|
188
|
-
} catch (error) {
|
|
189
|
-
throw this.failure("could not initialize", error);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async prompt({
|
|
194
|
-
sessionKey,
|
|
195
|
-
prompt,
|
|
196
|
-
previousSessionId,
|
|
197
|
-
sandbox = "read-only",
|
|
198
|
-
profile = {},
|
|
199
|
-
callbacks,
|
|
200
|
-
}) {
|
|
201
|
-
this.clearIdleTimer();
|
|
202
|
-
const initialized = await this.ready;
|
|
203
|
-
let session = this.sessions.get(sessionKey);
|
|
204
|
-
if (!session) {
|
|
205
|
-
session = await this.openSession(
|
|
206
|
-
sessionKey,
|
|
207
|
-
previousSessionId,
|
|
208
|
-
sandbox,
|
|
209
|
-
profile,
|
|
210
|
-
);
|
|
211
|
-
callbacks.onEvent?.({
|
|
212
|
-
type: "agent.connected",
|
|
213
|
-
message: `Agent connected with protocol ${initialized.protocolVersion}`,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
await callbacks.onSession?.(session.sessionId);
|
|
217
|
-
if (this.turns.has(session.sessionId)) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
"The agent is already answering another prompt in this session.",
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
const turn = { callbacks, finalMessage: "", sandbox };
|
|
223
|
-
this.turns.set(session.sessionId, turn);
|
|
224
|
-
try {
|
|
225
|
-
const response = await this.context.request(
|
|
226
|
-
acp.methods.agent.session.prompt,
|
|
227
|
-
{
|
|
228
|
-
sessionId: session.sessionId,
|
|
229
|
-
prompt: [{ type: "text", text: prompt }],
|
|
230
|
-
},
|
|
231
|
-
);
|
|
232
|
-
if (response.stopReason === "error") {
|
|
233
|
-
throw new Error("The ACP agent ended the task with an error.");
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
finalMessage: turn.finalMessage,
|
|
237
|
-
model: profile.model || this.config.agent_name || "acp-agent",
|
|
238
|
-
sessionId: session.sessionId,
|
|
239
|
-
usage: normalizeTokenUsage(response.usage),
|
|
240
|
-
};
|
|
241
|
-
} catch (error) {
|
|
242
|
-
throw this.failure("failed", error);
|
|
243
|
-
} finally {
|
|
244
|
-
this.turns.delete(session.sessionId);
|
|
245
|
-
this.scheduleIdleDisposal();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async openSession(sessionKey, previousSessionId, sandbox, profile) {
|
|
250
|
-
let response;
|
|
251
|
-
let sessionId;
|
|
252
|
-
if (previousSessionId && this.capabilities?.loadSession === true) {
|
|
253
|
-
response = await this.context.request(acp.methods.agent.session.load, {
|
|
254
|
-
sessionId: previousSessionId,
|
|
255
|
-
cwd: this.workspace,
|
|
256
|
-
mcpServers: [],
|
|
257
|
-
});
|
|
258
|
-
sessionId = previousSessionId;
|
|
259
|
-
} else {
|
|
260
|
-
response = await this.context.request(acp.methods.agent.session.new, {
|
|
261
|
-
cwd: this.workspace,
|
|
262
|
-
mcpServers: [],
|
|
263
|
-
});
|
|
264
|
-
sessionId = response.sessionId;
|
|
265
|
-
}
|
|
266
|
-
await this.applyMode(sessionId, response.modes, sandbox);
|
|
267
|
-
await this.applyProfile(sessionId, response.configOptions, profile);
|
|
268
|
-
const session = { sessionId };
|
|
269
|
-
this.sessions.set(sessionKey, session);
|
|
270
|
-
return session;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async applyMode(sessionId, modes, sandbox) {
|
|
274
|
-
const modeId = this.config.agent_modes?.[sandbox];
|
|
275
|
-
if (!modeId) return;
|
|
276
|
-
if (!modes?.availableModes?.some((mode) => mode.id === modeId)) {
|
|
277
|
-
throw new Error(
|
|
278
|
-
`${this.config.agent_name || "This ACP agent"} does not advertise the required '${modeId}' mode for ${sandbox} prompts.`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
await this.context.request(acp.methods.agent.session.setMode, {
|
|
282
|
-
sessionId,
|
|
283
|
-
modeId,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async applyProfile(sessionId, configOptions = [], profile = {}) {
|
|
288
|
-
const requested = [
|
|
289
|
-
["model", profile.model],
|
|
290
|
-
["thought_level", profile.reasoning_effort],
|
|
291
|
-
];
|
|
292
|
-
for (const [category, value] of requested) {
|
|
293
|
-
if (!value) continue;
|
|
294
|
-
const option = configOptions?.find(
|
|
295
|
-
(candidate) => candidate.category === category,
|
|
296
|
-
);
|
|
297
|
-
if (!option || option.type !== "select") {
|
|
298
|
-
throw new Error(
|
|
299
|
-
`This ACP agent does not advertise ${category.replace("_", " ")} selection.`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
const available = option.options.flatMap((candidate) =>
|
|
303
|
-
Array.isArray(candidate.options) ? candidate.options : [candidate],
|
|
304
|
-
);
|
|
305
|
-
const selected = available.find(
|
|
306
|
-
(candidate) =>
|
|
307
|
-
candidate.value === value ||
|
|
308
|
-
candidate.name?.toLowerCase() === String(value).toLowerCase(),
|
|
309
|
-
);
|
|
310
|
-
if (!selected) {
|
|
311
|
-
throw new Error(
|
|
312
|
-
`${option.name} does not offer '${value}'. Available values: ${available
|
|
313
|
-
.map((candidate) => candidate.value)
|
|
314
|
-
.join(", ")}.`,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
await this.context.request(acp.methods.agent.session.setConfigOption, {
|
|
318
|
-
sessionId,
|
|
319
|
-
configId: option.id,
|
|
320
|
-
value: selected.value,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async handleUpdate(notification) {
|
|
326
|
-
const turn = this.turns.get(notification.sessionId);
|
|
327
|
-
if (!turn) return;
|
|
328
|
-
const update = notification.update;
|
|
329
|
-
if (
|
|
330
|
-
update.sessionUpdate === "agent_message_chunk" &&
|
|
331
|
-
update.content?.type === "text"
|
|
332
|
-
) {
|
|
333
|
-
turn.finalMessage += update.content.text;
|
|
334
|
-
}
|
|
335
|
-
turn.callbacks.onEvent?.({
|
|
336
|
-
type: `acp.${update.sessionUpdate}`,
|
|
337
|
-
update,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
permissionOutcome(request) {
|
|
342
|
-
const turn = this.turns.get(request.sessionId);
|
|
343
|
-
const outcome = permissionOutcome(
|
|
344
|
-
request.options,
|
|
345
|
-
turn?.sandbox === "workspace-write",
|
|
346
|
-
);
|
|
347
|
-
turn?.callbacks.onEvent?.({
|
|
348
|
-
type: "acp.permission",
|
|
349
|
-
update: {
|
|
350
|
-
title: "Agent requested workspace permission",
|
|
351
|
-
status: turn?.sandbox === "workspace-write" ? "approved" : "denied",
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
return outcome;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async cancel(sessionKey) {
|
|
358
|
-
const session = this.sessions.get(sessionKey);
|
|
359
|
-
if (!session || !this.isRunning()) return;
|
|
360
|
-
await this.context.notify(acp.methods.agent.session.cancel, {
|
|
361
|
-
sessionId: session.sessionId,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
isRunning() {
|
|
366
|
-
return (
|
|
367
|
-
!this.disposed &&
|
|
368
|
-
this.childIsRunning() &&
|
|
369
|
-
!this.connection.signal.aborted
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
childIsRunning() {
|
|
374
|
-
return this.child.exitCode === null && this.child.signalCode === null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
scheduleIdleDisposal() {
|
|
378
|
-
if (this.turns.size || this.disposed) return;
|
|
379
|
-
this.clearIdleTimer();
|
|
380
|
-
this.idleTimer = setTimeout(() => void this.dispose(), RUNTIME_IDLE_MS);
|
|
381
|
-
this.idleTimer.unref?.();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
clearIdleTimer() {
|
|
385
|
-
clearTimeout(this.idleTimer);
|
|
386
|
-
this.idleTimer = null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
failure(action, error) {
|
|
390
|
-
const detail = this.stderr.trim().slice(-1_000);
|
|
391
|
-
const cause = this.spawnError || error;
|
|
392
|
-
return new Error(
|
|
393
|
-
`ACP agent ${action}: ${cause instanceof Error ? cause.message : String(cause)}${detail ? ` ${detail}` : ""}`,
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
closed() {
|
|
398
|
-
void this.dispose();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async dispose() {
|
|
402
|
-
if (this.disposePromise) return this.disposePromise;
|
|
403
|
-
this.disposed = true;
|
|
404
|
-
this.clearIdleTimer();
|
|
405
|
-
this.onClose?.();
|
|
406
|
-
this.disposePromise = this.releaseResources();
|
|
407
|
-
return this.disposePromise;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async releaseResources() {
|
|
411
|
-
try {
|
|
412
|
-
this.connection.close();
|
|
413
|
-
} catch {
|
|
414
|
-
// The ACP transport may already be closed.
|
|
415
|
-
}
|
|
416
|
-
if (this.childIsRunning()) {
|
|
417
|
-
this.child.kill("SIGTERM");
|
|
418
|
-
await Promise.race([
|
|
419
|
-
new Promise((resolve) => this.child.once("exit", resolve)),
|
|
420
|
-
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
421
|
-
]);
|
|
422
|
-
}
|
|
423
|
-
if (this.childIsRunning()) {
|
|
424
|
-
this.child.kill("SIGKILL");
|
|
425
|
-
await Promise.race([
|
|
426
|
-
new Promise((resolve) => this.child.once("exit", resolve)),
|
|
427
|
-
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
428
|
-
]);
|
|
429
|
-
}
|
|
430
|
-
this.child.stdin.destroy();
|
|
431
|
-
this.child.stdout.destroy();
|
|
432
|
-
this.child.stderr.destroy();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function normalizeTokenUsage(value) {
|
|
437
|
-
if (!value || typeof value !== "object") return null;
|
|
438
|
-
const number = (...keys) => {
|
|
439
|
-
for (const key of keys) {
|
|
440
|
-
const candidate = value[key];
|
|
441
|
-
if (Number.isFinite(candidate) && candidate >= 0) return Math.trunc(candidate);
|
|
442
|
-
}
|
|
443
|
-
return 0;
|
|
444
|
-
};
|
|
445
|
-
const usage = {
|
|
446
|
-
input_tokens: number("inputTokens", "input_tokens"),
|
|
447
|
-
output_tokens: number("outputTokens", "output_tokens"),
|
|
448
|
-
cache_read_tokens: number("cachedReadTokens", "cachedInputTokens", "cache_read_input_tokens"),
|
|
449
|
-
cache_write_tokens: number(
|
|
450
|
-
"cachedWriteTokens",
|
|
451
|
-
"cacheWriteInputTokens",
|
|
452
|
-
"cache_creation_input_tokens",
|
|
453
|
-
),
|
|
454
|
-
reasoning_tokens: number("reasoningTokens", "reasoningOutputTokens", "reasoning_tokens"),
|
|
455
|
-
};
|
|
456
|
-
usage.total_tokens = number("totalTokens", "total_tokens") ||
|
|
457
|
-
Object.values(usage).reduce((total, tokens) => total + tokens, 0);
|
|
458
|
-
return usage.total_tokens > 0 ? usage : null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function acpRuntimeKey(workspace, config) {
|
|
462
|
-
return JSON.stringify([
|
|
463
|
-
workspace,
|
|
464
|
-
config.agent_command,
|
|
465
|
-
config.agent_args || [],
|
|
466
|
-
config.agent_env || {},
|
|
467
|
-
]);
|
|
468
|
-
}
|