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