@engineeros/connector 0.13.4 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/engineeros-connector.mjs +907 -852
- package/package.json +41 -41
- package/src/acp-client.mjs +87 -68
- package/src/agent-harness.mjs +37 -37
- package/src/codex-app-server.mjs +40 -40
- package/src/connection.mjs +74 -74
- package/src/runner.mjs +349 -291
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@engineeros/connector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
|
-
"private": false,
|
|
6
|
-
"type": "module",
|
|
7
|
-
"license": "UNLICENSED",
|
|
8
|
-
"files": [
|
|
9
|
-
"bin",
|
|
10
|
-
"src",
|
|
11
|
-
"README.md"
|
|
12
|
-
],
|
|
13
|
-
"bin": {
|
|
14
|
-
"engineeros-connector": "bin/engineeros-connector.mjs"
|
|
15
|
-
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"start": "node ./bin/engineeros-connector.mjs",
|
|
18
|
-
"prepublishOnly": "node ./scripts/release.mjs check",
|
|
19
|
-
"release:patch": "node ./scripts/release.mjs patch",
|
|
20
|
-
"test": "node --test --test-concurrency=1",
|
|
21
|
-
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./scripts/release.mjs && node --check ./src/acp-client.mjs && node --check ./src/agent-harness.mjs && node --check ./src/agent-registry.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/codex-app-server.mjs && node --check ./src/config.mjs && node --check ./src/connection.mjs && node --check ./src/mcp-server.mjs && node --check ./src/runner.mjs"
|
|
22
|
-
},
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=22"
|
|
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.14.2",
|
|
4
|
+
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"engineeros-connector": "bin/engineeros-connector.mjs"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node ./bin/engineeros-connector.mjs",
|
|
18
|
+
"prepublishOnly": "node ./scripts/release.mjs check",
|
|
19
|
+
"release:patch": "node ./scripts/release.mjs patch",
|
|
20
|
+
"test": "node --test --test-concurrency=1",
|
|
21
|
+
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./scripts/release.mjs && node --check ./src/acp-client.mjs && node --check ./src/agent-harness.mjs && node --check ./src/agent-registry.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/codex-app-server.mjs && node --check ./src/config.mjs && node --check ./src/connection.mjs && node --check ./src/mcp-server.mjs && node --check ./src/runner.mjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
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
|
+
}
|
package/src/acp-client.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import * as acp from "@agentclientprotocol/sdk";
|
|
|
5
5
|
const RUNTIME_IDLE_MS = 30 * 60 * 1_000;
|
|
6
6
|
const runtimes = new Map();
|
|
7
7
|
|
|
8
|
-
export function permissionOutcome(options = [], allow = false) {
|
|
8
|
+
export function permissionOutcome(options = [], allow = false) {
|
|
9
9
|
if (!allow) return { outcome: { outcome: "cancelled" } };
|
|
10
10
|
const selected =
|
|
11
11
|
options.find((option) => option.kind === "allow_once") ??
|
|
@@ -13,9 +13,9 @@ export function permissionOutcome(options = [], allow = false) {
|
|
|
13
13
|
return selected
|
|
14
14
|
? { outcome: { outcome: "selected", optionId: selected.optionId } }
|
|
15
15
|
: { outcome: { outcome: "cancelled" } };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function launchAcpAgent(
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function launchAcpAgent(
|
|
19
19
|
workspace,
|
|
20
20
|
prompt,
|
|
21
21
|
config,
|
|
@@ -39,7 +39,7 @@ export function launchAcpAgent(
|
|
|
39
39
|
if (runtimeKey) runtimes.set(runtimeKey, runtime);
|
|
40
40
|
}
|
|
41
41
|
const sessionKey = options.sessionKey || `isolated-${crypto.randomUUID()}`;
|
|
42
|
-
const completed = runtime
|
|
42
|
+
const completed = runtime
|
|
43
43
|
.prompt({
|
|
44
44
|
sessionKey,
|
|
45
45
|
prompt,
|
|
@@ -49,10 +49,10 @@ export function launchAcpAgent(
|
|
|
49
49
|
callbacks,
|
|
50
50
|
})
|
|
51
51
|
.finally(async () => {
|
|
52
|
-
if (!persistent) await runtime.dispose();
|
|
53
|
-
});
|
|
54
|
-
runtime.child.engineerOsCancel = () => runtime.dispose();
|
|
55
|
-
return {
|
|
52
|
+
if (!persistent) await runtime.dispose();
|
|
53
|
+
});
|
|
54
|
+
runtime.child.engineerOsCancel = () => runtime.dispose();
|
|
55
|
+
return {
|
|
56
56
|
child: runtime.child,
|
|
57
57
|
completed,
|
|
58
58
|
cancel: () => runtime.cancel(sessionKey),
|
|
@@ -115,22 +115,23 @@ class AcpRuntime {
|
|
|
115
115
|
this.onClose = onClose;
|
|
116
116
|
this.sessions = new Map();
|
|
117
117
|
this.turns = new Map();
|
|
118
|
-
this.stderr = "";
|
|
119
|
-
this.disposed = false;
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
);
|
|
134
135
|
this.child.stderr.setEncoding("utf8");
|
|
135
136
|
this.child.stderr.on("data", (chunk) => {
|
|
136
137
|
this.stderr = `${this.stderr}${chunk}`.slice(-4_000);
|
|
@@ -158,11 +159,13 @@ class AcpRuntime {
|
|
|
158
159
|
)
|
|
159
160
|
.connect(stream);
|
|
160
161
|
this.context = this.connection.agent;
|
|
161
|
-
this.ready = this.initialize();
|
|
162
|
-
void this.ready.catch(() => {
|
|
163
|
-
void this.dispose();
|
|
164
|
-
});
|
|
165
|
-
this.child.once("
|
|
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());
|
|
166
169
|
void this.connection.closed.then(
|
|
167
170
|
() => this.closed(),
|
|
168
171
|
() => this.closed(),
|
|
@@ -358,13 +361,17 @@ class AcpRuntime {
|
|
|
358
361
|
});
|
|
359
362
|
}
|
|
360
363
|
|
|
361
|
-
isRunning() {
|
|
362
|
-
return (
|
|
363
|
-
!this.disposed &&
|
|
364
|
-
this.
|
|
365
|
-
!this.connection.signal.aborted
|
|
366
|
-
);
|
|
367
|
-
}
|
|
364
|
+
isRunning() {
|
|
365
|
+
return (
|
|
366
|
+
!this.disposed &&
|
|
367
|
+
this.childIsRunning() &&
|
|
368
|
+
!this.connection.signal.aborted
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
childIsRunning() {
|
|
373
|
+
return this.child.exitCode === null && this.child.signalCode === null;
|
|
374
|
+
}
|
|
368
375
|
|
|
369
376
|
scheduleIdleDisposal() {
|
|
370
377
|
if (this.turns.size || this.disposed) return;
|
|
@@ -386,32 +393,44 @@ class AcpRuntime {
|
|
|
386
393
|
);
|
|
387
394
|
}
|
|
388
395
|
|
|
389
|
-
closed() {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
this.
|
|
399
|
-
this.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
396
|
+
closed() {
|
|
397
|
+
void this.dispose();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async dispose() {
|
|
401
|
+
if (this.disposePromise) return this.disposePromise;
|
|
402
|
+
this.disposed = true;
|
|
403
|
+
this.clearIdleTimer();
|
|
404
|
+
this.onClose?.();
|
|
405
|
+
this.disposePromise = this.releaseResources();
|
|
406
|
+
return this.disposePromise;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async releaseResources() {
|
|
410
|
+
try {
|
|
411
|
+
this.connection.close();
|
|
412
|
+
} catch {
|
|
413
|
+
// The ACP transport may already be closed.
|
|
414
|
+
}
|
|
415
|
+
if (this.childIsRunning()) {
|
|
416
|
+
this.child.kill("SIGTERM");
|
|
417
|
+
await Promise.race([
|
|
418
|
+
new Promise((resolve) => this.child.once("exit", resolve)),
|
|
419
|
+
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
420
|
+
]);
|
|
421
|
+
}
|
|
422
|
+
if (this.childIsRunning()) {
|
|
423
|
+
this.child.kill("SIGKILL");
|
|
424
|
+
await Promise.race([
|
|
425
|
+
new Promise((resolve) => this.child.once("exit", resolve)),
|
|
426
|
+
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
429
|
+
this.child.stdin.destroy();
|
|
430
|
+
this.child.stdout.destroy();
|
|
431
|
+
this.child.stderr.destroy();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
415
434
|
|
|
416
435
|
export function normalizeTokenUsage(value) {
|
|
417
436
|
if (!value || typeof value !== "object") return null;
|
|
@@ -426,11 +445,11 @@ export function normalizeTokenUsage(value) {
|
|
|
426
445
|
input_tokens: number("inputTokens", "input_tokens"),
|
|
427
446
|
output_tokens: number("outputTokens", "output_tokens"),
|
|
428
447
|
cache_read_tokens: number("cachedReadTokens", "cachedInputTokens", "cache_read_input_tokens"),
|
|
429
|
-
cache_write_tokens: number(
|
|
430
|
-
"cachedWriteTokens",
|
|
431
|
-
"cacheWriteInputTokens",
|
|
432
|
-
"cache_creation_input_tokens",
|
|
433
|
-
),
|
|
448
|
+
cache_write_tokens: number(
|
|
449
|
+
"cachedWriteTokens",
|
|
450
|
+
"cacheWriteInputTokens",
|
|
451
|
+
"cache_creation_input_tokens",
|
|
452
|
+
),
|
|
434
453
|
reasoning_tokens: number("reasoningTokens", "reasoningOutputTokens", "reasoning_tokens"),
|
|
435
454
|
};
|
|
436
455
|
usage.total_tokens = number("totalTokens", "total_tokens") ||
|
package/src/agent-harness.mjs
CHANGED
|
@@ -141,15 +141,15 @@ export function normalizeAgentStructuredOutput(
|
|
|
141
141
|
if (!content) {
|
|
142
142
|
throw new Error("Agent completed without returning a response.");
|
|
143
143
|
}
|
|
144
|
-
const firstSection = {
|
|
145
|
-
"# Workspace Assessment": "## Executive Summary",
|
|
146
|
-
"# Workspace Assessment Delta": "## Updated Executive Summary",
|
|
147
|
-
"# Assessment Stage: Architecture": "## Architecture Summary",
|
|
148
|
-
"# Assessment Stage: Capability Catalog": "## Capability Catalog",
|
|
149
|
-
"# Assessment Stage: Capabilities": "## Observed Capabilities",
|
|
150
|
-
"# Assessment Stage: Quality": "## Quality Summary",
|
|
151
|
-
"# Assessment Stage: Synthesis": "## Executive Summary",
|
|
152
|
-
}[outputHeading];
|
|
144
|
+
const firstSection = {
|
|
145
|
+
"# Workspace Assessment": "## Executive Summary",
|
|
146
|
+
"# Workspace Assessment Delta": "## Updated Executive Summary",
|
|
147
|
+
"# Assessment Stage: Architecture": "## Architecture Summary",
|
|
148
|
+
"# Assessment Stage: Capability Catalog": "## Capability Catalog",
|
|
149
|
+
"# Assessment Stage: Capabilities": "## Observed Capabilities",
|
|
150
|
+
"# Assessment Stage: Quality": "## Quality Summary",
|
|
151
|
+
"# Assessment Stage: Synthesis": "## Executive Summary",
|
|
152
|
+
}[outputHeading];
|
|
153
153
|
if (
|
|
154
154
|
firstSection &&
|
|
155
155
|
(content.startsWith(firstSection) ||
|
|
@@ -157,34 +157,34 @@ export function normalizeAgentStructuredOutput(
|
|
|
157
157
|
) {
|
|
158
158
|
return `${outputHeading}\n\n${content}`;
|
|
159
159
|
}
|
|
160
|
-
const lines = content.split(/\r?\n/);
|
|
161
|
-
const headingIndex = lines.findIndex(
|
|
162
|
-
(line) => line.trim() === outputHeading,
|
|
163
|
-
);
|
|
164
|
-
const firstSectionIndex = firstSection
|
|
165
|
-
? lines.findIndex((line) => line.trim() === firstSection)
|
|
166
|
-
: -1;
|
|
167
|
-
if (
|
|
168
|
-
headingIndex < 0 &&
|
|
169
|
-
outputHeading.startsWith("# Assessment Stage:") &&
|
|
170
|
-
firstSectionIndex >= 0
|
|
171
|
-
) {
|
|
172
|
-
const preamble = lines.slice(0, firstSectionIndex).join("\n");
|
|
173
|
-
if (preamble.length > 2_000) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Agent response contains too much text before the required section '${firstSection}'.`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
const normalized = lines.slice(firstSectionIndex).join("\n").trim();
|
|
179
|
-
if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
return `${outputHeading}\n\n${normalized}`;
|
|
185
|
-
}
|
|
186
|
-
const preamble =
|
|
187
|
-
headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
|
|
160
|
+
const lines = content.split(/\r?\n/);
|
|
161
|
+
const headingIndex = lines.findIndex(
|
|
162
|
+
(line) => line.trim() === outputHeading,
|
|
163
|
+
);
|
|
164
|
+
const firstSectionIndex = firstSection
|
|
165
|
+
? lines.findIndex((line) => line.trim() === firstSection)
|
|
166
|
+
: -1;
|
|
167
|
+
if (
|
|
168
|
+
headingIndex < 0 &&
|
|
169
|
+
outputHeading.startsWith("# Assessment Stage:") &&
|
|
170
|
+
firstSectionIndex >= 0
|
|
171
|
+
) {
|
|
172
|
+
const preamble = lines.slice(0, firstSectionIndex).join("\n");
|
|
173
|
+
if (preamble.length > 2_000) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Agent response contains too much text before the required section '${firstSection}'.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const normalized = lines.slice(firstSectionIndex).join("\n").trim();
|
|
179
|
+
if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return `${outputHeading}\n\n${normalized}`;
|
|
185
|
+
}
|
|
186
|
+
const preamble =
|
|
187
|
+
headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
|
|
188
188
|
if (headingIndex < 0) {
|
|
189
189
|
throw new Error(
|
|
190
190
|
`Agent response is missing the required heading '${outputHeading}'.`,
|
package/src/codex-app-server.mjs
CHANGED
|
@@ -103,10 +103,10 @@ export function launchCodexAppServer({
|
|
|
103
103
|
let threadId = previousSessionId || "";
|
|
104
104
|
let turnId = "";
|
|
105
105
|
let finalMessage = "";
|
|
106
|
-
let stderr = "";
|
|
107
|
-
let usage = null;
|
|
108
|
-
let settled = false;
|
|
109
|
-
let completionTimer = null;
|
|
106
|
+
let stderr = "";
|
|
107
|
+
let usage = null;
|
|
108
|
+
let settled = false;
|
|
109
|
+
let completionTimer = null;
|
|
110
110
|
|
|
111
111
|
const request = (method, params) =>
|
|
112
112
|
new Promise((resolve, reject) => {
|
|
@@ -116,10 +116,10 @@ export function launchCodexAppServer({
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
const completed = new Promise((resolve, reject) => {
|
|
119
|
-
const finish = (error) => {
|
|
120
|
-
if (settled) return;
|
|
121
|
-
settled = true;
|
|
122
|
-
if (completionTimer) clearTimeout(completionTimer);
|
|
119
|
+
const finish = (error) => {
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
123
123
|
for (const waiter of pending.values()) waiter.reject(error || new Error("Codex app-server stopped."));
|
|
124
124
|
pending.clear();
|
|
125
125
|
if (error) reject(error);
|
|
@@ -157,38 +157,38 @@ export function launchCodexAppServer({
|
|
|
157
157
|
if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
|
|
158
158
|
finalMessage += params.delta;
|
|
159
159
|
callbacks.onEvent?.({ type: "codex.agent_message_delta", delta: params.delta });
|
|
160
|
-
} else if (message.method === "item/started" || message.method === "item/completed") {
|
|
161
|
-
callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
|
|
162
|
-
} else if (message.method === "thread/tokenUsage/updated") {
|
|
163
|
-
const tokenUsage = params.tokenUsage || params.usage || params;
|
|
164
|
-
usage = normalizeTokenUsage(
|
|
165
|
-
previousSessionId
|
|
166
|
-
? tokenUsage.last || params.last || tokenUsage
|
|
167
|
-
: tokenUsage.total || params.total || tokenUsage.last || params.last || tokenUsage,
|
|
168
|
-
);
|
|
169
|
-
callbacks.onEvent?.({ type: "codex.usage", update: params });
|
|
170
|
-
if (usage && completionTimer) {
|
|
171
|
-
finish();
|
|
172
|
-
child.kill();
|
|
173
|
-
}
|
|
174
|
-
} else if (message.method === "turn/completed" && params.threadId === threadId) {
|
|
175
|
-
const status = params.turn?.status;
|
|
176
|
-
if (status === "failed") {
|
|
177
|
-
finish(new Error(params.turn?.error?.message || "Codex turn failed."));
|
|
178
|
-
child.kill();
|
|
179
|
-
} else if (!finalMessage.trim()) {
|
|
180
|
-
finish(new Error("Codex completed without returning a response."));
|
|
181
|
-
child.kill();
|
|
182
|
-
} else if (usage) {
|
|
183
|
-
finish();
|
|
184
|
-
child.kill();
|
|
185
|
-
} else if (!completionTimer) {
|
|
186
|
-
completionTimer = setTimeout(() => {
|
|
187
|
-
finish();
|
|
188
|
-
child.kill();
|
|
189
|
-
}, 250);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
160
|
+
} else if (message.method === "item/started" || message.method === "item/completed") {
|
|
161
|
+
callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
|
|
162
|
+
} else if (message.method === "thread/tokenUsage/updated") {
|
|
163
|
+
const tokenUsage = params.tokenUsage || params.usage || params;
|
|
164
|
+
usage = normalizeTokenUsage(
|
|
165
|
+
previousSessionId
|
|
166
|
+
? tokenUsage.last || params.last || tokenUsage
|
|
167
|
+
: tokenUsage.total || params.total || tokenUsage.last || params.last || tokenUsage,
|
|
168
|
+
);
|
|
169
|
+
callbacks.onEvent?.({ type: "codex.usage", update: params });
|
|
170
|
+
if (usage && completionTimer) {
|
|
171
|
+
finish();
|
|
172
|
+
child.kill();
|
|
173
|
+
}
|
|
174
|
+
} else if (message.method === "turn/completed" && params.threadId === threadId) {
|
|
175
|
+
const status = params.turn?.status;
|
|
176
|
+
if (status === "failed") {
|
|
177
|
+
finish(new Error(params.turn?.error?.message || "Codex turn failed."));
|
|
178
|
+
child.kill();
|
|
179
|
+
} else if (!finalMessage.trim()) {
|
|
180
|
+
finish(new Error("Codex completed without returning a response."));
|
|
181
|
+
child.kill();
|
|
182
|
+
} else if (usage) {
|
|
183
|
+
finish();
|
|
184
|
+
child.kill();
|
|
185
|
+
} else if (!completionTimer) {
|
|
186
|
+
completionTimer = setTimeout(() => {
|
|
187
|
+
finish();
|
|
188
|
+
child.kill();
|
|
189
|
+
}, 250);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
child.stderr.setEncoding("utf8");
|
package/src/connection.mjs
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
const PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH = 2_000;
|
|
2
|
-
const TRUNCATED_FAILURE_SUFFIX = "\n… [truncated by EngineerOS connector]";
|
|
3
|
-
|
|
4
|
-
export function describeWebSocketError(event) {
|
|
5
|
-
return (
|
|
6
|
-
event?.error?.message ||
|
|
7
|
-
event?.message ||
|
|
8
|
-
"The WebSocket connection failed without providing an error detail."
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function protocolFailureMessage(error) {
|
|
13
|
-
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
14
|
-
const message =
|
|
15
|
-
raw.trim() || "The connector failed without providing an error detail.";
|
|
16
|
-
const characters = [...message];
|
|
17
|
-
if (characters.length <= PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH) return message;
|
|
18
|
-
const suffix = [...TRUNCATED_FAILURE_SUFFIX];
|
|
19
|
-
return `${characters
|
|
20
|
-
.slice(0, PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH - suffix.length)
|
|
21
|
-
.join("")}${TRUNCATED_FAILURE_SUFFIX}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function describeRejectedResponse(response, subject) {
|
|
25
|
-
const responseBody = await response.text();
|
|
26
|
-
const detail = responseDetail(responseBody);
|
|
27
|
-
return protocolFailureMessage(
|
|
28
|
-
`EngineerOS rejected ${subject} (${response.status})${detail ? `: ${detail}` : "."}`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function startConnectionWatchdog(
|
|
33
|
-
socket,
|
|
34
|
-
url,
|
|
35
|
-
{ timeoutMs = 15_000, onTimeout = console.error } = {},
|
|
36
|
-
) {
|
|
37
|
-
const timer = setTimeout(() => {
|
|
38
|
-
onTimeout(
|
|
39
|
-
`EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
|
|
40
|
-
);
|
|
41
|
-
try {
|
|
42
|
-
socket.close();
|
|
43
|
-
} catch {
|
|
44
|
-
// Reconnect scheduling is owned by the caller.
|
|
45
|
-
}
|
|
46
|
-
}, timeoutMs);
|
|
47
|
-
|
|
48
|
-
return () => clearTimeout(timer);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function responseDetail(responseBody) {
|
|
52
|
-
const body = String(responseBody || "").trim();
|
|
53
|
-
if (!body) return "";
|
|
54
|
-
try {
|
|
55
|
-
const parsed = JSON.parse(body);
|
|
56
|
-
if (typeof parsed?.detail === "string") return parsed.detail;
|
|
57
|
-
if (Array.isArray(parsed?.detail)) {
|
|
58
|
-
return parsed.detail.map(validationIssue).filter(Boolean).join("; ");
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Plain-text server errors are already useful to the operator.
|
|
62
|
-
}
|
|
63
|
-
return body;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function validationIssue(issue) {
|
|
67
|
-
if (typeof issue === "string") return issue;
|
|
68
|
-
if (!issue || typeof issue !== "object") return "";
|
|
69
|
-
const location = Array.isArray(issue.loc) ? issue.loc.join(".") : "";
|
|
70
|
-
const message = typeof issue.msg === "string" ? issue.msg : "";
|
|
71
|
-
const type = typeof issue.type === "string" ? ` (${issue.type})` : "";
|
|
72
|
-
if (!location && !message) return "";
|
|
73
|
-
return `${location ? `${location}: ` : ""}${message}${type}`;
|
|
74
|
-
}
|
|
1
|
+
const PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH = 2_000;
|
|
2
|
+
const TRUNCATED_FAILURE_SUFFIX = "\n… [truncated by EngineerOS connector]";
|
|
3
|
+
|
|
4
|
+
export function describeWebSocketError(event) {
|
|
5
|
+
return (
|
|
6
|
+
event?.error?.message ||
|
|
7
|
+
event?.message ||
|
|
8
|
+
"The WebSocket connection failed without providing an error detail."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function protocolFailureMessage(error) {
|
|
13
|
+
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
14
|
+
const message =
|
|
15
|
+
raw.trim() || "The connector failed without providing an error detail.";
|
|
16
|
+
const characters = [...message];
|
|
17
|
+
if (characters.length <= PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH) return message;
|
|
18
|
+
const suffix = [...TRUNCATED_FAILURE_SUFFIX];
|
|
19
|
+
return `${characters
|
|
20
|
+
.slice(0, PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH - suffix.length)
|
|
21
|
+
.join("")}${TRUNCATED_FAILURE_SUFFIX}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function describeRejectedResponse(response, subject) {
|
|
25
|
+
const responseBody = await response.text();
|
|
26
|
+
const detail = responseDetail(responseBody);
|
|
27
|
+
return protocolFailureMessage(
|
|
28
|
+
`EngineerOS rejected ${subject} (${response.status})${detail ? `: ${detail}` : "."}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function startConnectionWatchdog(
|
|
33
|
+
socket,
|
|
34
|
+
url,
|
|
35
|
+
{ timeoutMs = 15_000, onTimeout = console.error } = {},
|
|
36
|
+
) {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
onTimeout(
|
|
39
|
+
`EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
|
|
40
|
+
);
|
|
41
|
+
try {
|
|
42
|
+
socket.close();
|
|
43
|
+
} catch {
|
|
44
|
+
// Reconnect scheduling is owned by the caller.
|
|
45
|
+
}
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
|
|
48
|
+
return () => clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function responseDetail(responseBody) {
|
|
52
|
+
const body = String(responseBody || "").trim();
|
|
53
|
+
if (!body) return "";
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(body);
|
|
56
|
+
if (typeof parsed?.detail === "string") return parsed.detail;
|
|
57
|
+
if (Array.isArray(parsed?.detail)) {
|
|
58
|
+
return parsed.detail.map(validationIssue).filter(Boolean).join("; ");
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Plain-text server errors are already useful to the operator.
|
|
62
|
+
}
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validationIssue(issue) {
|
|
67
|
+
if (typeof issue === "string") return issue;
|
|
68
|
+
if (!issue || typeof issue !== "object") return "";
|
|
69
|
+
const location = Array.isArray(issue.loc) ? issue.loc.join(".") : "";
|
|
70
|
+
const message = typeof issue.msg === "string" ? issue.msg : "";
|
|
71
|
+
const type = typeof issue.type === "string" ? ` (${issue.type})` : "";
|
|
72
|
+
if (!location && !message) return "";
|
|
73
|
+
return `${location ? `${location}: ` : ""}${message}${type}`;
|
|
74
|
+
}
|