@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
|
@@ -1,913 +1,968 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
assessmentResultUrl,
|
|
6
|
-
connectorResumeCredentials,
|
|
7
|
-
loadConfig,
|
|
8
|
-
mergePairedConfig,
|
|
9
|
-
resultUrl,
|
|
10
|
-
saveConfig,
|
|
11
|
-
socketUrl,
|
|
12
|
-
workspaceUrl,
|
|
13
|
-
} from "../src/config.mjs";
|
|
14
|
-
import {
|
|
15
|
-
assessmentInactivityFailure,
|
|
16
|
-
assessmentProgressMessage,
|
|
17
|
-
assessmentStreamDelta,
|
|
18
|
-
applyAcceptedChange,
|
|
19
|
-
enqueueWorkspaceAssessment,
|
|
20
|
-
executeAssignment,
|
|
21
|
-
executeConnectedPrompt,
|
|
22
|
-
executeWorkspaceAssessment,
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
assessmentResultUrl,
|
|
6
|
+
connectorResumeCredentials,
|
|
7
|
+
loadConfig,
|
|
8
|
+
mergePairedConfig,
|
|
9
|
+
resultUrl,
|
|
10
|
+
saveConfig,
|
|
11
|
+
socketUrl,
|
|
12
|
+
workspaceUrl,
|
|
13
|
+
} from "../src/config.mjs";
|
|
14
|
+
import {
|
|
15
|
+
assessmentInactivityFailure,
|
|
16
|
+
assessmentProgressMessage,
|
|
17
|
+
assessmentStreamDelta,
|
|
18
|
+
applyAcceptedChange,
|
|
19
|
+
enqueueWorkspaceAssessment,
|
|
20
|
+
executeAssignment,
|
|
21
|
+
executeConnectedPrompt,
|
|
22
|
+
executeWorkspaceAssessment,
|
|
23
23
|
inspectCodingAgent,
|
|
24
24
|
promptStreamEvent,
|
|
25
25
|
requeueInterruptedAssessment,
|
|
26
|
+
shouldReportAssessmentProgress,
|
|
26
27
|
stopProcess,
|
|
28
|
+
submitAssessmentResultWithRetry,
|
|
27
29
|
takeWorkspaceAssessmentWave,
|
|
30
|
+
workspaceAssessmentWorkerLimit,
|
|
28
31
|
workspaceSnapshot,
|
|
29
|
-
} from "../src/runner.mjs";
|
|
30
|
-
import { disposeAcpRuntimes } from "../src/acp-client.mjs";
|
|
31
|
-
import {
|
|
32
|
-
installRegisteredAgent,
|
|
33
|
-
inspectRegisteredAgent,
|
|
34
|
-
registeredAgentConfig,
|
|
35
|
-
registeredAgents,
|
|
36
|
-
} from "../src/agent-registry.mjs";
|
|
37
|
-
import {
|
|
38
|
-
describeRejectedResponse,
|
|
39
|
-
describeWebSocketError,
|
|
40
|
-
protocolFailureMessage,
|
|
41
|
-
startConnectionWatchdog,
|
|
42
|
-
} from "../src/connection.mjs";
|
|
43
|
-
import { advertisedCapabilities } from "../src/capabilities.mjs";
|
|
44
|
-
import { parseConnectorArgs } from "../src/cli-args.mjs";
|
|
45
|
-
import { runMcpServer } from "../src/mcp-server.mjs";
|
|
46
|
-
import packageJson from "../package.json" with { type: "json" };
|
|
47
|
-
|
|
48
|
-
const { command, positional, flags } = parseConnectorArgs(
|
|
49
|
-
process.argv.slice(2),
|
|
32
|
+
} from "../src/runner.mjs";
|
|
33
|
+
import { disposeAcpRuntimes } from "../src/acp-client.mjs";
|
|
34
|
+
import {
|
|
35
|
+
installRegisteredAgent,
|
|
36
|
+
inspectRegisteredAgent,
|
|
37
|
+
registeredAgentConfig,
|
|
38
|
+
registeredAgents,
|
|
39
|
+
} from "../src/agent-registry.mjs";
|
|
40
|
+
import {
|
|
41
|
+
describeRejectedResponse,
|
|
42
|
+
describeWebSocketError,
|
|
43
|
+
protocolFailureMessage,
|
|
44
|
+
startConnectionWatchdog,
|
|
45
|
+
} from "../src/connection.mjs";
|
|
46
|
+
import { advertisedCapabilities } from "../src/capabilities.mjs";
|
|
47
|
+
import { parseConnectorArgs } from "../src/cli-args.mjs";
|
|
48
|
+
import { runMcpServer } from "../src/mcp-server.mjs";
|
|
49
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
50
|
+
|
|
51
|
+
const { command, positional, flags } = parseConnectorArgs(
|
|
52
|
+
process.argv.slice(2),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (command === "agents") {
|
|
56
|
+
let agents;
|
|
57
|
+
try {
|
|
58
|
+
agents = await registeredAgents();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
61
|
+
}
|
|
62
|
+
const statuses = await Promise.all(
|
|
63
|
+
agents.map(async (agent) => {
|
|
64
|
+
try {
|
|
65
|
+
await inspectRegisteredAgent(agent.id);
|
|
66
|
+
return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
for (const status of statuses) {
|
|
74
|
+
console.log(status);
|
|
75
|
+
}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (command === "agent") {
|
|
80
|
+
const agentId = positional[0];
|
|
81
|
+
const action = positional[1] || "check";
|
|
82
|
+
if (!agentId || !["check", "install"].includes(action)) {
|
|
83
|
+
fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const installed =
|
|
87
|
+
action === "install"
|
|
88
|
+
? await installRegisteredAgent(agentId)
|
|
89
|
+
: await inspectRegisteredAgent(agentId);
|
|
90
|
+
console.log(
|
|
91
|
+
`${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
95
|
+
}
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command === "mcp") {
|
|
100
|
+
const config = await loadConfig(flags.workspace || process.cwd());
|
|
101
|
+
if (!config) fail("This workspace is not paired with EngineerOS.");
|
|
102
|
+
const runId = flags["run-id"] || positional[0];
|
|
103
|
+
if (!runId)
|
|
104
|
+
fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
|
|
105
|
+
await runMcpServer({
|
|
106
|
+
config,
|
|
107
|
+
runId,
|
|
108
|
+
version: packageJson.version,
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout,
|
|
111
|
+
});
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (command === "status") {
|
|
116
|
+
const config = await loadConfig(flags.workspace || process.cwd());
|
|
117
|
+
console.log(
|
|
118
|
+
config
|
|
119
|
+
? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
|
|
120
|
+
: "Not paired",
|
|
121
|
+
);
|
|
122
|
+
process.exit(config ? 0 : 1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let config;
|
|
126
|
+
let firstMessage;
|
|
127
|
+
let existingConfig;
|
|
128
|
+
if (command === "pair") {
|
|
129
|
+
const pairingCode = positional[0];
|
|
130
|
+
if (!pairingCode)
|
|
131
|
+
fail(
|
|
132
|
+
"Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
|
|
133
|
+
);
|
|
134
|
+
const url = flags.url;
|
|
135
|
+
if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
|
|
136
|
+
if (flags.agent && flags["agent-command"]) {
|
|
137
|
+
fail("Use either --agent or --agent-command, not both.");
|
|
138
|
+
}
|
|
139
|
+
let agentConfig = {};
|
|
140
|
+
try {
|
|
141
|
+
if (flags.agent) {
|
|
142
|
+
await inspectRegisteredAgent(flags.agent);
|
|
143
|
+
agentConfig = await registeredAgentConfig(flags.agent);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
147
|
+
}
|
|
148
|
+
config = {
|
|
149
|
+
server_url: socketUrl(url),
|
|
150
|
+
workspace: path.resolve(flags.workspace || process.cwd()),
|
|
151
|
+
onboard: flags.onboard === true,
|
|
152
|
+
onboarding_pending: flags.onboard === true,
|
|
153
|
+
agent_protocol: flags["agent-command"] ? "acp" : "codex",
|
|
154
|
+
agent_command: flags["agent-command"] || null,
|
|
155
|
+
agent_args: parseAgentArgs(flags["agent-args"]),
|
|
156
|
+
agent_name: flags["agent-name"] || null,
|
|
157
|
+
...agentConfig,
|
|
158
|
+
skip_git_repo_check: flags["skip-git-repo-check"] === true,
|
|
159
|
+
name:
|
|
160
|
+
flags.name ||
|
|
161
|
+
`${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
|
|
162
|
+
};
|
|
163
|
+
existingConfig = await loadConfig(config.workspace);
|
|
164
|
+
firstMessage = {
|
|
165
|
+
type: "pair",
|
|
166
|
+
pairing_code: pairingCode,
|
|
167
|
+
name: config.name,
|
|
168
|
+
capabilities: {},
|
|
169
|
+
...connectorResumeCredentials(existingConfig, config.server_url),
|
|
170
|
+
};
|
|
171
|
+
} else if (command === "start") {
|
|
172
|
+
config = await loadConfig(flags.workspace || process.cwd());
|
|
173
|
+
if (!config)
|
|
174
|
+
fail(
|
|
175
|
+
"This connector is not paired. Create a pairing command in EngineerOS first.",
|
|
176
|
+
);
|
|
177
|
+
firstMessage = {
|
|
178
|
+
type: "authenticate",
|
|
179
|
+
connector_id: config.connector_id,
|
|
180
|
+
token: config.token,
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
fail(
|
|
184
|
+
"Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let codingAgent;
|
|
189
|
+
try {
|
|
190
|
+
codingAgent = await inspectCodingAgent(config, config.workspace);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
193
|
+
}
|
|
194
|
+
console.log(
|
|
195
|
+
`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
|
|
196
|
+
);
|
|
197
|
+
const capabilities = advertisedCapabilities(config, codingAgent);
|
|
198
|
+
firstMessage.capabilities = capabilities;
|
|
199
|
+
|
|
200
|
+
let stopped = false;
|
|
201
|
+
let active = null;
|
|
202
|
+
const activeAssessments = new Map();
|
|
203
|
+
const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
|
|
204
|
+
codingAgent.protocol,
|
|
50
205
|
);
|
|
51
|
-
|
|
52
|
-
if (command === "agents") {
|
|
53
|
-
let agents;
|
|
54
|
-
try {
|
|
55
|
-
agents = await registeredAgents();
|
|
56
|
-
} catch (error) {
|
|
57
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
58
|
-
}
|
|
59
|
-
const statuses = await Promise.all(
|
|
60
|
-
agents.map(async (agent) => {
|
|
61
|
-
try {
|
|
62
|
-
await inspectRegisteredAgent(agent.id);
|
|
63
|
-
return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
66
|
-
return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
|
|
67
|
-
}
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
for (const status of statuses) {
|
|
71
|
-
console.log(status);
|
|
72
|
-
}
|
|
73
|
-
process.exit(0);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (command === "agent") {
|
|
77
|
-
const agentId = positional[0];
|
|
78
|
-
const action = positional[1] || "check";
|
|
79
|
-
if (!agentId || !["check", "install"].includes(action)) {
|
|
80
|
-
fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
const installed =
|
|
84
|
-
action === "install"
|
|
85
|
-
? await installRegisteredAgent(agentId)
|
|
86
|
-
: await inspectRegisteredAgent(agentId);
|
|
87
|
-
console.log(
|
|
88
|
-
`${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
|
|
89
|
-
);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
92
|
-
}
|
|
93
|
-
process.exit(0);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (command === "mcp") {
|
|
97
|
-
const config = await loadConfig(flags.workspace || process.cwd());
|
|
98
|
-
if (!config) fail("This workspace is not paired with EngineerOS.");
|
|
99
|
-
const runId = flags["run-id"] || positional[0];
|
|
100
|
-
if (!runId)
|
|
101
|
-
fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
|
|
102
|
-
await runMcpServer({
|
|
103
|
-
config,
|
|
104
|
-
runId,
|
|
105
|
-
version: packageJson.version,
|
|
106
|
-
input: process.stdin,
|
|
107
|
-
output: process.stdout,
|
|
108
|
-
});
|
|
109
|
-
process.exit(0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (command === "status") {
|
|
113
|
-
const config = await loadConfig(flags.workspace || process.cwd());
|
|
206
|
+
if (ASSESSMENT_WORKER_LIMIT === 1) {
|
|
114
207
|
console.log(
|
|
115
|
-
|
|
116
|
-
? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
|
|
117
|
-
: "Not paired",
|
|
118
|
-
);
|
|
119
|
-
process.exit(config ? 0 : 1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let config;
|
|
123
|
-
let firstMessage;
|
|
124
|
-
let existingConfig;
|
|
125
|
-
if (command === "pair") {
|
|
126
|
-
const pairingCode = positional[0];
|
|
127
|
-
if (!pairingCode)
|
|
128
|
-
fail(
|
|
129
|
-
"Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
|
|
130
|
-
);
|
|
131
|
-
const url = flags.url;
|
|
132
|
-
if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
|
|
133
|
-
if (flags.agent && flags["agent-command"]) {
|
|
134
|
-
fail("Use either --agent or --agent-command, not both.");
|
|
135
|
-
}
|
|
136
|
-
let agentConfig = {};
|
|
137
|
-
try {
|
|
138
|
-
if (flags.agent) {
|
|
139
|
-
await inspectRegisteredAgent(flags.agent);
|
|
140
|
-
agentConfig = await registeredAgentConfig(flags.agent);
|
|
141
|
-
}
|
|
142
|
-
} catch (error) {
|
|
143
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
144
|
-
}
|
|
145
|
-
config = {
|
|
146
|
-
server_url: socketUrl(url),
|
|
147
|
-
workspace: path.resolve(flags.workspace || process.cwd()),
|
|
148
|
-
onboard: flags.onboard === true,
|
|
149
|
-
onboarding_pending: flags.onboard === true,
|
|
150
|
-
agent_protocol: flags["agent-command"] ? "acp" : "codex",
|
|
151
|
-
agent_command: flags["agent-command"] || null,
|
|
152
|
-
agent_args: parseAgentArgs(flags["agent-args"]),
|
|
153
|
-
agent_name: flags["agent-name"] || null,
|
|
154
|
-
...agentConfig,
|
|
155
|
-
skip_git_repo_check: flags["skip-git-repo-check"] === true,
|
|
156
|
-
name:
|
|
157
|
-
flags.name ||
|
|
158
|
-
`${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
|
|
159
|
-
};
|
|
160
|
-
existingConfig = await loadConfig(config.workspace);
|
|
161
|
-
firstMessage = {
|
|
162
|
-
type: "pair",
|
|
163
|
-
pairing_code: pairingCode,
|
|
164
|
-
name: config.name,
|
|
165
|
-
capabilities: {},
|
|
166
|
-
...connectorResumeCredentials(existingConfig, config.server_url),
|
|
167
|
-
};
|
|
168
|
-
} else if (command === "start") {
|
|
169
|
-
config = await loadConfig(flags.workspace || process.cwd());
|
|
170
|
-
if (!config)
|
|
171
|
-
fail(
|
|
172
|
-
"This connector is not paired. Create a pairing command in EngineerOS first.",
|
|
173
|
-
);
|
|
174
|
-
firstMessage = {
|
|
175
|
-
type: "authenticate",
|
|
176
|
-
connector_id: config.connector_id,
|
|
177
|
-
token: config.token,
|
|
178
|
-
};
|
|
179
|
-
} else {
|
|
180
|
-
fail(
|
|
181
|
-
"Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
|
|
208
|
+
"ACP assessments run one stage at a time to keep the local agent and EngineerOS responsive.",
|
|
182
209
|
);
|
|
183
210
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
if (message.type === "run.error") {
|
|
332
|
-
console.error(`EngineerOS: ${message.message}`);
|
|
333
|
-
if (!message.run_id) snapshotInFlight = false;
|
|
334
|
-
if (active?.runId === message.run_id && !active.child) {
|
|
335
|
-
active = null;
|
|
336
|
-
pump();
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (message.type === "connection.error") {
|
|
340
|
-
connectionRejected = true;
|
|
341
|
-
console.error(`EngineerOS: ${message.message}`);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
socket.addEventListener("close", (event) => {
|
|
345
|
-
clearConnectionWatchdog?.();
|
|
346
|
-
clearInterval(pingTimer);
|
|
211
|
+
const available = [];
|
|
212
|
+
const assessments = [];
|
|
213
|
+
const activePrompts = new Map();
|
|
214
|
+
let socket;
|
|
215
|
+
let pingTimer;
|
|
216
|
+
let reconnectTimer;
|
|
217
|
+
let clearConnectionWatchdog;
|
|
218
|
+
let reconnectDelay = 1_000;
|
|
219
|
+
let connectionRejected = false;
|
|
220
|
+
let lastConnectionError;
|
|
221
|
+
let snapshotInFlight = false;
|
|
222
|
+
|
|
223
|
+
process.on("SIGINT", async () => {
|
|
224
|
+
stopped = true;
|
|
225
|
+
clearConnectionWatchdog?.();
|
|
226
|
+
clearTimeout(reconnectTimer);
|
|
227
|
+
clearInterval(pingTimer);
|
|
228
|
+
await Promise.all([
|
|
229
|
+
stopProcess(active?.child),
|
|
230
|
+
...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
|
|
231
|
+
]);
|
|
232
|
+
await Promise.all([...activePrompts.values()].map(cancelPrompt));
|
|
233
|
+
await disposeAcpRuntimes();
|
|
234
|
+
socket?.close();
|
|
235
|
+
process.exit(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await connect();
|
|
239
|
+
|
|
240
|
+
async function connect() {
|
|
241
|
+
console.log(`Connecting ${config.name} to ${config.server_url}`);
|
|
242
|
+
connectionRejected = false;
|
|
243
|
+
lastConnectionError = undefined;
|
|
244
|
+
socket = new WebSocket(config.server_url);
|
|
245
|
+
clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
|
|
246
|
+
onTimeout: (message) => {
|
|
247
|
+
lastConnectionError = message;
|
|
248
|
+
console.error(message);
|
|
249
|
+
scheduleReconnect();
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
socket.addEventListener("open", () =>
|
|
253
|
+
socket.send(JSON.stringify(firstMessage)),
|
|
254
|
+
);
|
|
255
|
+
socket.addEventListener("message", async (event) => {
|
|
256
|
+
const message = JSON.parse(String(event.data));
|
|
257
|
+
if (message.type === "paired") {
|
|
258
|
+
clearConnectionWatchdog?.();
|
|
259
|
+
const reusedConnector =
|
|
260
|
+
existingConfig?.connector_id === message.connector.id;
|
|
261
|
+
config = mergePairedConfig(
|
|
262
|
+
config,
|
|
263
|
+
existingConfig,
|
|
264
|
+
message.connector.id,
|
|
265
|
+
message.token,
|
|
266
|
+
);
|
|
267
|
+
await saveConfig(config);
|
|
268
|
+
firstMessage = {
|
|
269
|
+
type: "authenticate",
|
|
270
|
+
connector_id: config.connector_id,
|
|
271
|
+
token: config.token,
|
|
272
|
+
capabilities,
|
|
273
|
+
};
|
|
274
|
+
console.log(
|
|
275
|
+
reusedConnector
|
|
276
|
+
? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
|
|
277
|
+
: `Paired. Connector ${config.connector_id} is online.`,
|
|
278
|
+
);
|
|
279
|
+
reconnectDelay = 1_000;
|
|
280
|
+
startPings();
|
|
281
|
+
if (config.onboarding_pending) void submitWorkspaceSnapshot();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (message.type === "authenticated") {
|
|
285
|
+
clearConnectionWatchdog?.();
|
|
286
|
+
console.log("Connected and waiting for EngineerOS runs.");
|
|
287
|
+
reconnectDelay = 1_000;
|
|
288
|
+
startPings();
|
|
289
|
+
if (config.onboarding_pending) void submitWorkspaceSnapshot();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (message.type === "workspace.refresh") {
|
|
293
|
+
void submitWorkspaceSnapshot();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (message.type === "workspace.assessment") {
|
|
297
|
+
const assessmentKey = `${message.assessment_id}:${message.stage}`;
|
|
298
|
+
const disposition = enqueueWorkspaceAssessment(
|
|
299
|
+
assessments,
|
|
300
|
+
activeAssessments.get(assessmentKey),
|
|
301
|
+
message,
|
|
302
|
+
);
|
|
303
|
+
if (disposition === "deferred") {
|
|
304
|
+
console.log(
|
|
305
|
+
`Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
pump();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (message.type === "prompt.execute") {
|
|
312
|
+
if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (message.type === "prompt.cancel") {
|
|
316
|
+
const promptState = activePrompts.get(message.prompt_id);
|
|
317
|
+
if (promptState) await cancelPrompt(promptState);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (message.type === "run.available") {
|
|
321
|
+
if (!available.includes(message.run_id)) available.push(message.run_id);
|
|
322
|
+
pump();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (message.type === "run.assignment") {
|
|
326
|
+
await execute(message);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (message.type === "run.cancelled" && active?.runId === message.run_id) {
|
|
330
|
+
console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
|
|
331
|
+
active.cancelled = true;
|
|
332
|
+
await stopProcess(active.child);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (message.type === "connector.revoked") {
|
|
336
|
+
stopped = true;
|
|
337
|
+
console.error("This connector was revoked in EngineerOS.");
|
|
338
|
+
socket.close();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (message.type === "run.error") {
|
|
342
|
+
console.error(`EngineerOS: ${message.message}`);
|
|
343
|
+
if (!message.run_id) snapshotInFlight = false;
|
|
344
|
+
if (active?.runId === message.run_id && !active.child) {
|
|
345
|
+
active = null;
|
|
346
|
+
pump();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (message.type === "connection.error") {
|
|
350
|
+
connectionRejected = true;
|
|
351
|
+
console.error(`EngineerOS: ${message.message}`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
socket.addEventListener("close", (event) => {
|
|
355
|
+
clearConnectionWatchdog?.();
|
|
356
|
+
clearInterval(pingTimer);
|
|
347
357
|
for (const promptState of activePrompts.values())
|
|
348
358
|
void cancelPrompt(promptState);
|
|
349
359
|
for (const assessmentState of activeAssessments.values()) {
|
|
350
|
-
assessmentState.transportInterrupted = true;
|
|
351
360
|
if (event.code === 4001) void stopProcess(assessmentState.child);
|
|
352
361
|
}
|
|
353
|
-
if (connectionRejected) {
|
|
354
|
-
stopped = true;
|
|
355
|
-
console.error(
|
|
356
|
-
"Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
|
|
357
|
-
);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (stopped) return;
|
|
361
|
-
scheduleReconnect();
|
|
362
|
-
});
|
|
363
|
-
socket.addEventListener("error", (event) => {
|
|
364
|
-
lastConnectionError = describeWebSocketError(event);
|
|
365
|
-
console.error(
|
|
366
|
-
`WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
|
|
367
|
-
);
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function scheduleReconnect() {
|
|
372
|
-
if (stopped || connectionRejected || reconnectTimer) return;
|
|
373
|
-
const detail = lastConnectionError
|
|
374
|
-
? ` Last error: ${lastConnectionError}`
|
|
375
|
-
: "";
|
|
376
|
-
console.error(
|
|
377
|
-
`Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
|
|
378
|
-
);
|
|
379
|
-
reconnectTimer = setTimeout(() => {
|
|
380
|
-
reconnectTimer = undefined;
|
|
381
|
-
void connect();
|
|
382
|
-
}, reconnectDelay);
|
|
383
|
-
reconnectDelay = Math.min(30_000, reconnectDelay * 2);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function submitWorkspaceSnapshot() {
|
|
387
|
-
if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
|
|
388
|
-
snapshotInFlight = true;
|
|
389
|
-
console.log("Inspecting the workspace without executing its code.");
|
|
390
|
-
try {
|
|
391
|
-
const snapshot = await workspaceSnapshot(config.workspace);
|
|
392
|
-
const response = await fetch(
|
|
393
|
-
workspaceUrl(config.server_url, config.connector_id),
|
|
394
|
-
{
|
|
395
|
-
method: "POST",
|
|
396
|
-
headers: {
|
|
397
|
-
"Content-Type": "application/json",
|
|
398
|
-
Authorization: `Bearer ${config.token}`,
|
|
399
|
-
},
|
|
400
|
-
body: JSON.stringify(snapshot),
|
|
401
|
-
},
|
|
402
|
-
);
|
|
403
|
-
if (!response.ok) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
await describeRejectedResponse(response, "the workspace"),
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const result = await response.json();
|
|
409
|
-
config = { ...config, onboarding_pending: false };
|
|
410
|
-
await saveConfig(config);
|
|
411
|
-
console.log(
|
|
412
|
-
`Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
|
|
413
|
-
);
|
|
414
|
-
console.log(
|
|
415
|
-
`Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized
|
|
416
|
-
);
|
|
417
|
-
console.log(
|
|
418
|
-
`Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
|
|
419
|
-
);
|
|
420
|
-
if (snapshot.omitted_evidence_file_count > 0) {
|
|
421
|
-
console.log(
|
|
422
|
-
`${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
const executionProfiles =
|
|
426
|
-
result.connector?.capabilities?.execution_profiles;
|
|
427
|
-
const canSelectAssessmentModel =
|
|
428
|
-
executionProfiles?.model_selection === true &&
|
|
429
|
-
Array.isArray(executionProfiles.models) &&
|
|
430
|
-
executionProfiles.models.length > 0;
|
|
431
|
-
console.log(
|
|
432
|
-
canSelectAssessmentModel
|
|
433
|
-
? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
|
|
434
|
-
: `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
|
|
435
|
-
);
|
|
436
|
-
snapshotInFlight = false;
|
|
437
|
-
} catch (error) {
|
|
438
|
-
snapshotInFlight = false;
|
|
439
|
-
console.error(
|
|
440
|
-
`Workspace assessment failed: ${protocolFailureMessage(error)}`,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function startPings() {
|
|
446
|
-
clearInterval(pingTimer);
|
|
447
|
-
pingTimer = setInterval(() => {
|
|
448
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
449
|
-
socket.send(
|
|
450
|
-
JSON.stringify({
|
|
451
|
-
type: "ping",
|
|
452
|
-
active_run_id: active?.kind === "goal" ? active.runId : null,
|
|
453
|
-
}),
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
}, 10_000);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function sendPromptEvent(promptId, event) {
|
|
460
|
-
if (!event || socket.readyState !== WebSocket.OPEN) return;
|
|
461
|
-
const payload = {
|
|
462
|
-
type: "prompt.event",
|
|
463
|
-
prompt_id: promptId,
|
|
464
|
-
kind: event.kind,
|
|
465
|
-
};
|
|
466
|
-
if (event.message) payload.message = String(event.message).slice(0, 500);
|
|
467
|
-
if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
|
|
468
|
-
if (event.status) payload.status = String(event.status).slice(0, 100);
|
|
469
|
-
socket.send(JSON.stringify(payload));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function pump() {
|
|
473
|
-
if (socket.readyState !== WebSocket.OPEN || active) return;
|
|
474
|
-
const wave = takeWorkspaceAssessmentWave(
|
|
475
|
-
assessments,
|
|
476
|
-
activeAssessments.size,
|
|
477
|
-
ASSESSMENT_WORKER_LIMIT,
|
|
478
|
-
);
|
|
479
|
-
for (const assessment of wave) {
|
|
480
|
-
const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
|
|
481
|
-
const assessmentState = {
|
|
482
|
-
kind: "assessment",
|
|
483
|
-
key: assessmentKey,
|
|
484
|
-
runId: assessment.assessment_id,
|
|
362
|
+
if (connectionRejected) {
|
|
363
|
+
stopped = true;
|
|
364
|
+
console.error(
|
|
365
|
+
"Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (stopped) return;
|
|
370
|
+
scheduleReconnect();
|
|
371
|
+
});
|
|
372
|
+
socket.addEventListener("error", (event) => {
|
|
373
|
+
lastConnectionError = describeWebSocketError(event);
|
|
374
|
+
console.error(
|
|
375
|
+
`WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function scheduleReconnect() {
|
|
381
|
+
if (stopped || connectionRejected || reconnectTimer) return;
|
|
382
|
+
const detail = lastConnectionError
|
|
383
|
+
? ` Last error: ${lastConnectionError}`
|
|
384
|
+
: "";
|
|
385
|
+
console.error(
|
|
386
|
+
`Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
|
|
387
|
+
);
|
|
388
|
+
reconnectTimer = setTimeout(() => {
|
|
389
|
+
reconnectTimer = undefined;
|
|
390
|
+
void connect();
|
|
391
|
+
}, reconnectDelay);
|
|
392
|
+
reconnectDelay = Math.min(30_000, reconnectDelay * 2);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function submitWorkspaceSnapshot() {
|
|
396
|
+
if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
|
|
397
|
+
snapshotInFlight = true;
|
|
398
|
+
console.log("Inspecting the workspace without executing its code.");
|
|
399
|
+
try {
|
|
400
|
+
const snapshot = await workspaceSnapshot(config.workspace);
|
|
401
|
+
const response = await fetch(
|
|
402
|
+
workspaceUrl(config.server_url, config.connector_id),
|
|
403
|
+
{
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/json",
|
|
407
|
+
Authorization: `Bearer ${config.token}`,
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify(snapshot),
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
await describeRejectedResponse(response, "the workspace"),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const result = await response.json();
|
|
418
|
+
config = { ...config, onboarding_pending: false };
|
|
419
|
+
await saveConfig(config);
|
|
420
|
+
console.log(
|
|
421
|
+
`Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
|
|
422
|
+
);
|
|
423
|
+
console.log(
|
|
424
|
+
`Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
|
|
425
|
+
);
|
|
426
|
+
console.log(
|
|
427
|
+
`Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
|
|
428
|
+
);
|
|
429
|
+
if (snapshot.omitted_evidence_file_count > 0) {
|
|
430
|
+
console.log(
|
|
431
|
+
`${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const executionProfiles =
|
|
435
|
+
result.connector?.capabilities?.execution_profiles;
|
|
436
|
+
const canSelectAssessmentModel =
|
|
437
|
+
executionProfiles?.model_selection === true &&
|
|
438
|
+
Array.isArray(executionProfiles.models) &&
|
|
439
|
+
executionProfiles.models.length > 0;
|
|
440
|
+
console.log(
|
|
441
|
+
canSelectAssessmentModel
|
|
442
|
+
? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
|
|
443
|
+
: `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
|
|
444
|
+
);
|
|
445
|
+
snapshotInFlight = false;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
snapshotInFlight = false;
|
|
448
|
+
console.error(
|
|
449
|
+
`Workspace assessment failed: ${protocolFailureMessage(error)}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function startPings() {
|
|
455
|
+
clearInterval(pingTimer);
|
|
456
|
+
pingTimer = setInterval(() => {
|
|
457
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
458
|
+
socket.send(
|
|
459
|
+
JSON.stringify({
|
|
460
|
+
type: "ping",
|
|
461
|
+
active_run_id: active?.kind === "goal" ? active.runId : null,
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}, 10_000);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function sendPromptEvent(promptId, event) {
|
|
469
|
+
if (!event || socket.readyState !== WebSocket.OPEN) return;
|
|
470
|
+
const payload = {
|
|
471
|
+
type: "prompt.event",
|
|
472
|
+
prompt_id: promptId,
|
|
473
|
+
kind: event.kind,
|
|
474
|
+
};
|
|
475
|
+
if (event.message) payload.message = String(event.message).slice(0, 500);
|
|
476
|
+
if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
|
|
477
|
+
if (event.status) payload.status = String(event.status).slice(0, 100);
|
|
478
|
+
socket.send(JSON.stringify(payload));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pump() {
|
|
482
|
+
if (socket.readyState !== WebSocket.OPEN || active) return;
|
|
483
|
+
const wave = takeWorkspaceAssessmentWave(
|
|
484
|
+
assessments,
|
|
485
|
+
activeAssessments.size,
|
|
486
|
+
ASSESSMENT_WORKER_LIMIT,
|
|
487
|
+
);
|
|
488
|
+
for (const assessment of wave) {
|
|
489
|
+
const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
|
|
490
|
+
const assessmentState = {
|
|
491
|
+
kind: "assessment",
|
|
492
|
+
key: assessmentKey,
|
|
493
|
+
runId: assessment.assessment_id,
|
|
485
494
|
stage: assessment.stage,
|
|
486
495
|
child: null,
|
|
487
496
|
resumeAssignment: null,
|
|
488
|
-
transportInterrupted: false,
|
|
489
497
|
};
|
|
490
|
-
activeAssessments.set(assessmentKey, assessmentState);
|
|
491
|
-
void executeAssessment(assessment, assessmentState);
|
|
492
|
-
}
|
|
493
|
-
if (activeAssessments.size > 0 || assessments.length > 0) return;
|
|
494
|
-
const runId = available.shift();
|
|
495
|
-
if (!runId) return;
|
|
496
|
-
active = { kind: "goal", runId, child: null, cancelled: false };
|
|
497
|
-
socket.send(
|
|
498
|
-
JSON.stringify({
|
|
499
|
-
type: "run.claim",
|
|
500
|
-
run_id: runId,
|
|
501
|
-
runner_name: codingAgent.name,
|
|
502
|
-
metadata: {
|
|
503
|
-
hostname: os.hostname(),
|
|
504
|
-
workspace_name: path.basename(config.workspace),
|
|
505
|
-
},
|
|
506
|
-
}),
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function executePrompt(assignment) {
|
|
511
|
-
const promptId = assignment.prompt_id;
|
|
512
|
-
const promptState = {
|
|
513
|
-
runId: promptId,
|
|
514
|
-
child: null,
|
|
515
|
-
controller: null,
|
|
516
|
-
cancelled: false,
|
|
517
|
-
};
|
|
518
|
-
activePrompts.set(promptId, promptState);
|
|
519
|
-
let lastEvent;
|
|
520
|
-
const reportEvent = (event) => {
|
|
521
|
-
const serialized = event ? JSON.stringify(event) : null;
|
|
522
|
-
if (
|
|
523
|
-
!serialized ||
|
|
524
|
-
serialized === lastEvent ||
|
|
525
|
-
socket.readyState !== WebSocket.OPEN ||
|
|
526
|
-
activePrompts.get(promptId) !== promptState
|
|
527
|
-
) {
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
lastEvent = serialized;
|
|
531
|
-
sendPromptEvent(promptId, event);
|
|
532
|
-
};
|
|
533
|
-
sendPromptEvent(promptId, {
|
|
534
|
-
kind: "status",
|
|
535
|
-
message: "Agent started this request",
|
|
536
|
-
});
|
|
537
|
-
console.log(
|
|
538
|
-
`Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
|
|
539
|
-
);
|
|
540
|
-
try {
|
|
541
|
-
const result = await executeConnectedPrompt(assignment, config, {
|
|
542
|
-
onProcess: (child) => {
|
|
543
|
-
if (activePrompts.get(promptId) === promptState)
|
|
544
|
-
promptState.child = child;
|
|
545
|
-
},
|
|
546
|
-
onController: (controller) => {
|
|
547
|
-
if (activePrompts.get(promptId) === promptState) {
|
|
548
|
-
promptState.controller = controller;
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
onEvent: (event) => reportEvent(promptStreamEvent(event)),
|
|
552
|
-
});
|
|
553
|
-
config = {
|
|
554
|
-
...config,
|
|
555
|
-
sessions: {
|
|
556
|
-
...(config.sessions || {}),
|
|
557
|
-
[result.sessionKey]: result.sessionId,
|
|
558
|
-
},
|
|
559
|
-
};
|
|
560
|
-
await saveConfig(config);
|
|
561
|
-
if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
|
|
562
|
-
socket.send(
|
|
563
|
-
JSON.stringify({
|
|
564
|
-
type: "prompt.completed",
|
|
565
|
-
prompt_id: promptId,
|
|
566
|
-
content: result.content,
|
|
567
|
-
model: result.model,
|
|
568
|
-
session_id: result.sessionId,
|
|
569
|
-
usage: result.usage,
|
|
570
|
-
}),
|
|
571
|
-
);
|
|
572
|
-
} catch (error) {
|
|
573
|
-
const message = protocolFailureMessage(error);
|
|
574
|
-
if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
575
|
-
socket.send(
|
|
576
|
-
JSON.stringify({
|
|
577
|
-
type: "prompt.failed",
|
|
578
|
-
prompt_id: promptId,
|
|
579
|
-
message,
|
|
580
|
-
}),
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
if (!promptState.cancelled) {
|
|
584
|
-
console.error(message);
|
|
585
|
-
}
|
|
586
|
-
} finally {
|
|
587
|
-
if (activePrompts.get(promptId) === promptState)
|
|
588
|
-
activePrompts.delete(promptId);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async function cancelPrompt(promptState) {
|
|
593
|
-
promptState.cancelled = true;
|
|
594
|
-
if (typeof promptState.controller?.cancel === "function") {
|
|
595
|
-
await promptState.controller.cancel();
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
await stopProcess(promptState.child);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function executeAssessment(assignment, assessmentState) {
|
|
602
|
-
const assessmentId = assignment.assessment_id;
|
|
603
|
-
const stage = assignment.stage;
|
|
604
|
-
console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
|
|
605
|
-
const stageStartedAt = Date.now();
|
|
606
|
-
let lastAgentActivityAt = stageStartedAt;
|
|
607
|
-
let lastAgentStatus = "Starting the connected agent";
|
|
498
|
+
activeAssessments.set(assessmentKey, assessmentState);
|
|
499
|
+
void executeAssessment(assessment, assessmentState);
|
|
500
|
+
}
|
|
501
|
+
if (activeAssessments.size > 0 || assessments.length > 0) return;
|
|
502
|
+
const runId = available.shift();
|
|
503
|
+
if (!runId) return;
|
|
504
|
+
active = { kind: "goal", runId, child: null, cancelled: false };
|
|
505
|
+
socket.send(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
type: "run.claim",
|
|
508
|
+
run_id: runId,
|
|
509
|
+
runner_name: codingAgent.name,
|
|
510
|
+
metadata: {
|
|
511
|
+
hostname: os.hostname(),
|
|
512
|
+
workspace_name: path.basename(config.workspace),
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function executePrompt(assignment) {
|
|
519
|
+
const promptId = assignment.prompt_id;
|
|
520
|
+
const promptState = {
|
|
521
|
+
runId: promptId,
|
|
522
|
+
child: null,
|
|
523
|
+
controller: null,
|
|
524
|
+
cancelled: false,
|
|
525
|
+
};
|
|
526
|
+
activePrompts.set(promptId, promptState);
|
|
527
|
+
let lastEvent;
|
|
528
|
+
const reportEvent = (event) => {
|
|
529
|
+
const serialized = event ? JSON.stringify(event) : null;
|
|
530
|
+
if (
|
|
531
|
+
!serialized ||
|
|
532
|
+
serialized === lastEvent ||
|
|
533
|
+
socket.readyState !== WebSocket.OPEN ||
|
|
534
|
+
activePrompts.get(promptId) !== promptState
|
|
535
|
+
) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
lastEvent = serialized;
|
|
539
|
+
sendPromptEvent(promptId, event);
|
|
540
|
+
};
|
|
541
|
+
sendPromptEvent(promptId, {
|
|
542
|
+
kind: "status",
|
|
543
|
+
message: "Agent started this request",
|
|
544
|
+
});
|
|
545
|
+
console.log(
|
|
546
|
+
`Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
|
|
547
|
+
);
|
|
548
|
+
try {
|
|
549
|
+
const result = await executeConnectedPrompt(assignment, config, {
|
|
550
|
+
onProcess: (child) => {
|
|
551
|
+
if (activePrompts.get(promptId) === promptState)
|
|
552
|
+
promptState.child = child;
|
|
553
|
+
},
|
|
554
|
+
onController: (controller) => {
|
|
555
|
+
if (activePrompts.get(promptId) === promptState) {
|
|
556
|
+
promptState.controller = controller;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
onEvent: (event) => reportEvent(promptStreamEvent(event)),
|
|
560
|
+
});
|
|
561
|
+
config = {
|
|
562
|
+
...config,
|
|
563
|
+
sessions: {
|
|
564
|
+
...(config.sessions || {}),
|
|
565
|
+
[result.sessionKey]: result.sessionId,
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
await saveConfig(config);
|
|
569
|
+
if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
|
|
570
|
+
socket.send(
|
|
571
|
+
JSON.stringify({
|
|
572
|
+
type: "prompt.completed",
|
|
573
|
+
prompt_id: promptId,
|
|
574
|
+
content: result.content,
|
|
575
|
+
model: result.model,
|
|
576
|
+
session_id: result.sessionId,
|
|
577
|
+
usage: result.usage,
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const message = protocolFailureMessage(error);
|
|
582
|
+
if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
583
|
+
socket.send(
|
|
584
|
+
JSON.stringify({
|
|
585
|
+
type: "prompt.failed",
|
|
586
|
+
prompt_id: promptId,
|
|
587
|
+
message,
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (!promptState.cancelled) {
|
|
592
|
+
console.error(message);
|
|
593
|
+
}
|
|
594
|
+
} finally {
|
|
595
|
+
if (activePrompts.get(promptId) === promptState)
|
|
596
|
+
activePrompts.delete(promptId);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function cancelPrompt(promptState) {
|
|
601
|
+
promptState.cancelled = true;
|
|
602
|
+
if (typeof promptState.controller?.cancel === "function") {
|
|
603
|
+
await promptState.controller.cancel();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
await stopProcess(promptState.child);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function executeAssessment(assignment, assessmentState) {
|
|
610
|
+
const assessmentId = assignment.assessment_id;
|
|
611
|
+
const stage = assignment.stage;
|
|
612
|
+
console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
|
|
613
|
+
const stageStartedAt = Date.now();
|
|
614
|
+
let lastAgentActivityAt = stageStartedAt;
|
|
615
|
+
let lastAgentStatus = "Starting the connected agent";
|
|
608
616
|
let agentEventCount = 0;
|
|
609
617
|
let agentProcessStarted = false;
|
|
610
|
-
let
|
|
611
|
-
let
|
|
612
|
-
let
|
|
613
|
-
let
|
|
614
|
-
let
|
|
615
|
-
let
|
|
616
|
-
let
|
|
618
|
+
let agentCompleted = false;
|
|
619
|
+
let statusTicks = 0;
|
|
620
|
+
let progress = 5;
|
|
621
|
+
let outputBuffer = "";
|
|
622
|
+
let outputLength = 0;
|
|
623
|
+
let outputTimer;
|
|
624
|
+
let accepted = false;
|
|
625
|
+
let failureReported = false;
|
|
617
626
|
let inactivityFailure = null;
|
|
618
627
|
let lastReportedMilestone = null;
|
|
628
|
+
let lastProgressMessage = null;
|
|
629
|
+
let lastProgressPercent = -1;
|
|
630
|
+
let lastProgressSentAt = 0;
|
|
619
631
|
const creditedMilestones = new Set();
|
|
620
632
|
const isActiveAssessment = () =>
|
|
621
633
|
activeAssessments.get(assessmentState.key) === assessmentState;
|
|
622
|
-
const
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
634
|
+
const sendAssessmentMessage = (payload) => {
|
|
635
|
+
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
636
|
+
try {
|
|
637
|
+
socket.send(JSON.stringify(payload));
|
|
638
|
+
return true;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
lastConnectionError = protocolFailureMessage(error);
|
|
641
|
+
try {
|
|
642
|
+
socket.close();
|
|
643
|
+
} catch {
|
|
644
|
+
// The reconnect timer below owns recovery.
|
|
630
645
|
}
|
|
631
|
-
|
|
632
|
-
|
|
646
|
+
scheduleReconnect();
|
|
647
|
+
return false;
|
|
633
648
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
649
|
+
};
|
|
650
|
+
const reportProgress = (message, { milestone = true } = {}) => {
|
|
651
|
+
if (!isActiveAssessment()) return;
|
|
652
|
+
if (milestone && lastReportedMilestone === message) return;
|
|
653
|
+
if (milestone) {
|
|
654
|
+
lastReportedMilestone = message;
|
|
655
|
+
if (!creditedMilestones.has(message)) {
|
|
656
|
+
creditedMilestones.add(message);
|
|
657
|
+
progress = Math.min(90, progress + 10);
|
|
658
|
+
}
|
|
659
|
+
lastAgentStatus = message;
|
|
660
|
+
console.log(`[assessment:${stage}] ${message}.`);
|
|
661
|
+
}
|
|
662
|
+
const boundedMessage = String(message).slice(0, 500);
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
if (
|
|
665
|
+
!shouldReportAssessmentProgress(
|
|
666
|
+
{
|
|
667
|
+
message: boundedMessage,
|
|
668
|
+
progressPercent: progress,
|
|
669
|
+
lastMessage: lastProgressMessage,
|
|
670
|
+
lastProgressPercent,
|
|
671
|
+
lastSentAt: lastProgressSentAt,
|
|
672
|
+
},
|
|
673
|
+
{ now },
|
|
674
|
+
)
|
|
675
|
+
) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (
|
|
679
|
+
sendAssessmentMessage({
|
|
637
680
|
type: "workspace.assessment.progress",
|
|
638
681
|
assessment_id: assessmentId,
|
|
639
682
|
stage,
|
|
640
683
|
progress_percent: progress,
|
|
641
|
-
message:
|
|
642
|
-
})
|
|
643
|
-
);
|
|
644
|
-
};
|
|
645
|
-
const flushOutput = () => {
|
|
646
|
-
if (outputTimer) clearTimeout(outputTimer);
|
|
647
|
-
outputTimer = undefined;
|
|
648
|
-
const output = outputBuffer;
|
|
649
|
-
outputBuffer = "";
|
|
650
|
-
if (
|
|
651
|
-
!output ||
|
|
652
|
-
socket.readyState !== WebSocket.OPEN ||
|
|
653
|
-
!isActiveAssessment()
|
|
684
|
+
message: boundedMessage,
|
|
685
|
+
})
|
|
654
686
|
) {
|
|
655
|
-
|
|
687
|
+
lastProgressMessage = boundedMessage;
|
|
688
|
+
lastProgressPercent = progress;
|
|
689
|
+
lastProgressSentAt = now;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
const flushOutput = () => {
|
|
693
|
+
if (outputTimer) clearTimeout(outputTimer);
|
|
694
|
+
outputTimer = undefined;
|
|
695
|
+
const output = outputBuffer;
|
|
696
|
+
outputBuffer = "";
|
|
697
|
+
if (
|
|
698
|
+
!output ||
|
|
699
|
+
socket.readyState !== WebSocket.OPEN ||
|
|
700
|
+
!isActiveAssessment()
|
|
701
|
+
) {
|
|
702
|
+
return;
|
|
656
703
|
}
|
|
657
704
|
for (let offset = 0; offset < output.length; offset += 50_000) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
outputBuffer
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
flushOutput
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
const inactiveMs = now - lastAgentActivityAt;
|
|
684
|
-
const nextInactivityFailure = agentProcessStarted
|
|
705
|
+
sendAssessmentMessage({
|
|
706
|
+
type: "workspace.assessment.output",
|
|
707
|
+
assessment_id: assessmentId,
|
|
708
|
+
stage,
|
|
709
|
+
delta: output.slice(offset, offset + 50_000),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const reportOutput = (delta) => {
|
|
714
|
+
const remaining = 120_000 - outputLength;
|
|
715
|
+
if (!delta || remaining <= 0) return;
|
|
716
|
+
const bounded = String(delta).slice(0, remaining);
|
|
717
|
+
outputBuffer += bounded;
|
|
718
|
+
outputLength += bounded.length;
|
|
719
|
+
if (outputBuffer.length >= 50_000) {
|
|
720
|
+
flushOutput();
|
|
721
|
+
} else if (!outputTimer) {
|
|
722
|
+
outputTimer = setTimeout(flushOutput, 500);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
reportProgress(`Starting ${stage} assessment stage`);
|
|
726
|
+
const heartbeat = setInterval(() => {
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
const inactiveMs = now - lastAgentActivityAt;
|
|
729
|
+
const nextInactivityFailure = agentProcessStarted && !agentCompleted
|
|
685
730
|
? assessmentInactivityFailure(stage, inactiveMs)
|
|
686
731
|
: null;
|
|
687
|
-
if (!inactivityFailure && nextInactivityFailure) {
|
|
688
|
-
inactivityFailure = nextInactivityFailure;
|
|
689
|
-
console.error(`[assessment:${stage}] ${inactivityFailure}`);
|
|
690
|
-
void stopProcess(assessmentState.child);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
if (inactivityFailure) return;
|
|
694
|
-
reportProgress("Assessment in progress", { milestone: false });
|
|
695
|
-
statusTicks += 1;
|
|
696
|
-
if (statusTicks % 2 === 0) {
|
|
697
|
-
console.log(
|
|
698
|
-
`[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
|
|
699
|
-
`${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
|
|
700
|
-
`(${agentEventCount.toLocaleString()} events).`,
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
}, 15_000);
|
|
704
|
-
try {
|
|
732
|
+
if (!inactivityFailure && nextInactivityFailure) {
|
|
733
|
+
inactivityFailure = nextInactivityFailure;
|
|
734
|
+
console.error(`[assessment:${stage}] ${inactivityFailure}`);
|
|
735
|
+
void stopProcess(assessmentState.child);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (inactivityFailure) return;
|
|
739
|
+
reportProgress("Assessment in progress", { milestone: false });
|
|
740
|
+
statusTicks += 1;
|
|
741
|
+
if (statusTicks % 2 === 0) {
|
|
742
|
+
console.log(
|
|
743
|
+
`[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
|
|
744
|
+
`${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
|
|
745
|
+
`(${agentEventCount.toLocaleString()} events).`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}, 15_000);
|
|
749
|
+
try {
|
|
705
750
|
if (assignment.assessment_mode === "incremental") {
|
|
706
751
|
progress = 15;
|
|
707
|
-
reportProgress("Calculating changed files and affected
|
|
708
|
-
}
|
|
752
|
+
reportProgress("Calculating changed files and affected behavior");
|
|
753
|
+
}
|
|
709
754
|
let result = await executeWorkspaceAssessment(assignment, config, {
|
|
710
|
-
onProcess: (child) => {
|
|
711
|
-
if (isActiveAssessment()) {
|
|
712
|
-
assessmentState.child = child;
|
|
713
|
-
agentProcessStarted = true;
|
|
714
|
-
lastAgentActivityAt = Date.now();
|
|
715
|
-
reportProgress("Connected agent process started");
|
|
716
|
-
}
|
|
717
|
-
},
|
|
718
|
-
onEvent: (event) => {
|
|
719
|
-
lastAgentActivityAt = Date.now();
|
|
720
|
-
agentEventCount += 1;
|
|
721
|
-
reportOutput(assessmentStreamDelta(event));
|
|
722
|
-
const message = assessmentProgressMessage(event);
|
|
755
|
+
onProcess: (child) => {
|
|
756
|
+
if (isActiveAssessment()) {
|
|
757
|
+
assessmentState.child = child;
|
|
758
|
+
agentProcessStarted = true;
|
|
759
|
+
lastAgentActivityAt = Date.now();
|
|
760
|
+
reportProgress("Connected agent process started");
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
onEvent: (event) => {
|
|
764
|
+
lastAgentActivityAt = Date.now();
|
|
765
|
+
agentEventCount += 1;
|
|
766
|
+
reportOutput(assessmentStreamDelta(event));
|
|
767
|
+
const message = assessmentProgressMessage(event);
|
|
723
768
|
if (message) reportProgress(message);
|
|
724
769
|
},
|
|
725
770
|
});
|
|
771
|
+
agentCompleted = true;
|
|
726
772
|
flushOutput();
|
|
727
773
|
const submitResult = (payload) =>
|
|
728
774
|
fetch(
|
|
729
|
-
assessmentResultUrl(
|
|
730
|
-
config.server_url,
|
|
731
|
-
config.connector_id,
|
|
732
|
-
assessmentId,
|
|
733
|
-
),
|
|
734
|
-
{
|
|
735
|
-
method: "POST",
|
|
775
|
+
assessmentResultUrl(
|
|
776
|
+
config.server_url,
|
|
777
|
+
config.connector_id,
|
|
778
|
+
assessmentId,
|
|
779
|
+
),
|
|
780
|
+
{
|
|
781
|
+
method: "POST",
|
|
736
782
|
headers: {
|
|
737
783
|
"Content-Type": "application/json",
|
|
738
784
|
Authorization: `Bearer ${config.token}`,
|
|
739
785
|
},
|
|
740
786
|
body: JSON.stringify(payload),
|
|
787
|
+
signal: AbortSignal.timeout(30_000),
|
|
741
788
|
},
|
|
742
789
|
);
|
|
743
|
-
|
|
790
|
+
const deliverResult = (payload) =>
|
|
791
|
+
submitAssessmentResultWithRetry(payload, submitResult, {
|
|
792
|
+
isActive: () => isActiveAssessment() && !stopped,
|
|
793
|
+
onRetry: (error, delayMs) => {
|
|
794
|
+
reportProgress("Completed result is waiting for EngineerOS", {
|
|
795
|
+
milestone: false,
|
|
796
|
+
});
|
|
797
|
+
console.warn(
|
|
798
|
+
`[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
|
|
799
|
+
`Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
|
|
800
|
+
);
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
let response = await deliverResult(result);
|
|
744
804
|
if (response.status === 422 && result.correctAfterRejection) {
|
|
745
805
|
const rejection = await describeRejectedResponse(
|
|
746
806
|
response,
|
|
747
807
|
"the assessment",
|
|
748
808
|
);
|
|
809
|
+
agentCompleted = false;
|
|
749
810
|
result = await result.correctAfterRejection(rejection);
|
|
811
|
+
agentCompleted = true;
|
|
750
812
|
flushOutput();
|
|
751
|
-
response = await
|
|
752
|
-
}
|
|
753
|
-
if (!response.ok) {
|
|
754
|
-
throw new Error(
|
|
755
|
-
await describeRejectedResponse(response, "the assessment"),
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
accepted = true;
|
|
759
|
-
console.log(`Workspace assessment stage ${stage} was accepted by EngineerOS.`);
|
|
760
|
-
} catch (error) {
|
|
761
|
-
flushOutput();
|
|
762
|
-
const message = inactivityFailure || protocolFailureMessage(error);
|
|
763
|
-
if (
|
|
764
|
-
!assessmentState.transportInterrupted &&
|
|
765
|
-
isActiveAssessment() &&
|
|
766
|
-
socket.readyState === WebSocket.OPEN
|
|
767
|
-
) {
|
|
768
|
-
socket.send(
|
|
769
|
-
JSON.stringify({
|
|
770
|
-
type: "workspace.assessment.failed",
|
|
771
|
-
assessment_id: assessmentId,
|
|
772
|
-
stage,
|
|
773
|
-
message,
|
|
774
|
-
}),
|
|
775
|
-
);
|
|
776
|
-
failureReported = true;
|
|
777
|
-
}
|
|
778
|
-
console.error(message);
|
|
779
|
-
} finally {
|
|
780
|
-
if (outputTimer) clearTimeout(outputTimer);
|
|
781
|
-
clearInterval(heartbeat);
|
|
813
|
+
response = await deliverResult(result);
|
|
814
|
+
}
|
|
815
|
+
if (!response.ok) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
await describeRejectedResponse(response, "the assessment"),
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
accepted = true;
|
|
821
|
+
console.log(`Workspace assessment stage ${stage} was accepted by EngineerOS.`);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
flushOutput();
|
|
824
|
+
const message = inactivityFailure || protocolFailureMessage(error);
|
|
782
825
|
if (isActiveAssessment()) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
},
|
|
852
|
-
);
|
|
853
|
-
if (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
active
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
826
|
+
failureReported = sendAssessmentMessage({
|
|
827
|
+
type: "workspace.assessment.failed",
|
|
828
|
+
assessment_id: assessmentId,
|
|
829
|
+
stage,
|
|
830
|
+
message,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
console.error(message);
|
|
834
|
+
} finally {
|
|
835
|
+
if (outputTimer) clearTimeout(outputTimer);
|
|
836
|
+
clearInterval(heartbeat);
|
|
837
|
+
if (isActiveAssessment()) {
|
|
838
|
+
activeAssessments.delete(assessmentState.key);
|
|
839
|
+
const replayQueued = requeueInterruptedAssessment(
|
|
840
|
+
assessments,
|
|
841
|
+
assessmentState,
|
|
842
|
+
{ accepted, failureReported },
|
|
843
|
+
);
|
|
844
|
+
if (replayQueued) {
|
|
845
|
+
console.log(`Restarting interrupted assessment stage ${stage}.`);
|
|
846
|
+
}
|
|
847
|
+
pump();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function formatAssessmentDuration(milliseconds) {
|
|
853
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
854
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
855
|
+
const seconds = totalSeconds % 60;
|
|
856
|
+
return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function execute(assignment) {
|
|
860
|
+
const runId = assignment.run_id;
|
|
861
|
+
console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
|
|
862
|
+
let progress = 10;
|
|
863
|
+
const heartbeat = setInterval(() => {
|
|
864
|
+
if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
|
|
865
|
+
progress = Math.min(90, progress + 5);
|
|
866
|
+
socket.send(
|
|
867
|
+
JSON.stringify({
|
|
868
|
+
type: "run.progress",
|
|
869
|
+
run_id: runId,
|
|
870
|
+
progress_percent: progress,
|
|
871
|
+
message: "Agent is working",
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}, 15_000);
|
|
876
|
+
try {
|
|
877
|
+
const result = await executeAssignment(assignment, config, {
|
|
878
|
+
onProcess: (child) => {
|
|
879
|
+
if (active?.runId === runId) active.child = child;
|
|
880
|
+
},
|
|
881
|
+
onEvent: (event) => {
|
|
882
|
+
const message =
|
|
883
|
+
event.message || event.item?.text || event.type || "Agent is working";
|
|
884
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
885
|
+
socket.send(
|
|
886
|
+
JSON.stringify({
|
|
887
|
+
type: "run.progress",
|
|
888
|
+
run_id: runId,
|
|
889
|
+
progress_percent: progress,
|
|
890
|
+
message: String(message).slice(0, 500),
|
|
891
|
+
}),
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
if (active?.cancelled) return;
|
|
897
|
+
const response = await fetch(
|
|
898
|
+
resultUrl(config.server_url, config.connector_id, runId),
|
|
899
|
+
{
|
|
900
|
+
method: "POST",
|
|
901
|
+
headers: {
|
|
902
|
+
"Content-Type": "application/json",
|
|
903
|
+
Authorization: `Bearer ${config.token}`,
|
|
904
|
+
},
|
|
905
|
+
body: JSON.stringify(result),
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
if (!response.ok) {
|
|
909
|
+
throw new Error(await describeRejectedResponse(response, "the result"));
|
|
910
|
+
}
|
|
911
|
+
const integration = await applyAcceptedChange(
|
|
912
|
+
config.workspace,
|
|
913
|
+
assignment.base_revision,
|
|
914
|
+
result.head_revision,
|
|
915
|
+
);
|
|
916
|
+
if (integration.applied) {
|
|
917
|
+
console.log(
|
|
918
|
+
`Run accepted and applied to the connected repository at ${integration.revision}.`,
|
|
919
|
+
);
|
|
920
|
+
} else {
|
|
921
|
+
console.warn(
|
|
922
|
+
`Run accepted but not applied locally. ${integration.reason}`,
|
|
923
|
+
);
|
|
924
|
+
console.warn(
|
|
925
|
+
`The verified run workspace remains at ${result.run_workspace}.`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
} catch (error) {
|
|
929
|
+
const message = protocolFailureMessage(error);
|
|
930
|
+
if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
931
|
+
socket.send(
|
|
932
|
+
JSON.stringify({
|
|
933
|
+
type: "run.failed",
|
|
934
|
+
run_id: runId,
|
|
935
|
+
message,
|
|
936
|
+
}),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (!active?.cancelled) console.error(message);
|
|
940
|
+
} finally {
|
|
941
|
+
clearInterval(heartbeat);
|
|
942
|
+
active = null;
|
|
943
|
+
pump();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function parseAgentArgs(value) {
|
|
948
|
+
if (!value) return [];
|
|
949
|
+
try {
|
|
950
|
+
const parsed = JSON.parse(value);
|
|
951
|
+
if (
|
|
952
|
+
!Array.isArray(parsed) ||
|
|
953
|
+
parsed.some((item) => typeof item !== "string")
|
|
954
|
+
) {
|
|
955
|
+
throw new Error();
|
|
956
|
+
}
|
|
957
|
+
return parsed;
|
|
958
|
+
} catch {
|
|
959
|
+
fail(
|
|
960
|
+
'--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function fail(message) {
|
|
966
|
+
console.error(message);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|