@engineeros/connector 0.8.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -11
- package/bin/engineeros-connector.mjs +168 -78
- package/package.json +2 -2
- package/src/acp-client.mjs +348 -84
- package/src/agent-harness.mjs +108 -0
- package/src/agent-registry.mjs +643 -0
- package/src/capabilities.mjs +2 -0
- package/src/config.mjs +7 -0
- package/src/runner.mjs +78 -23
- package/src/skills/change-planning/SKILL.md +12 -0
- package/src/skills/change-verification/SKILL.md +12 -0
- package/src/skills/codebase-research/SKILL.md +12 -0
- package/src/skills/goal-execution/SKILL.md +12 -0
package/src/acp-client.mjs
CHANGED
|
@@ -2,7 +2,11 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
3
|
import * as acp from "@agentclientprotocol/sdk";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const RUNTIME_IDLE_MS = 30 * 60 * 1_000;
|
|
6
|
+
const runtimes = new Map();
|
|
7
|
+
|
|
8
|
+
export function permissionOutcome(options = [], allow = false) {
|
|
9
|
+
if (!allow) return { outcome: { outcome: "cancelled" } };
|
|
6
10
|
const selected =
|
|
7
11
|
options.find((option) => option.kind === "allow_once") ??
|
|
8
12
|
options.find((option) => option.kind === "allow_always");
|
|
@@ -11,99 +15,359 @@ function permissionOutcome(options = []) {
|
|
|
11
15
|
: { outcome: { outcome: "cancelled" } };
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export function launchAcpAgent(
|
|
18
|
+
export function launchAcpAgent(
|
|
19
|
+
workspace,
|
|
20
|
+
prompt,
|
|
21
|
+
config,
|
|
22
|
+
callbacks = {},
|
|
23
|
+
options = {},
|
|
24
|
+
) {
|
|
15
25
|
if (!config.agent_command) {
|
|
16
26
|
throw new Error(
|
|
17
|
-
"No ACP coding agent is configured. Pair again with --agent-command and optional --agent-args JSON.",
|
|
27
|
+
"No ACP coding agent is configured. Pair again with --agent or --agent-command and optional --agent-args JSON.",
|
|
18
28
|
);
|
|
19
29
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
);
|
|
30
|
-
let stderr = "";
|
|
31
|
-
child.stderr.setEncoding("utf8");
|
|
32
|
-
child.stderr.on("data", (chunk) => {
|
|
33
|
-
stderr += chunk;
|
|
34
|
-
callbacks.onEvent?.({
|
|
35
|
-
type: "agent.stderr",
|
|
36
|
-
message: String(chunk).trim().slice(0, 500),
|
|
30
|
+
const persistent = options.persistent === true;
|
|
31
|
+
const runtimeKey = persistent ? acpRuntimeKey(workspace, config) : null;
|
|
32
|
+
let runtime = runtimeKey ? runtimes.get(runtimeKey) : null;
|
|
33
|
+
if (!runtime?.isRunning()) {
|
|
34
|
+
runtime = new AcpRuntime(workspace, config, () => {
|
|
35
|
+
if (runtimeKey && runtimes.get(runtimeKey) === runtime) {
|
|
36
|
+
runtimes.delete(runtimeKey);
|
|
37
|
+
}
|
|
37
38
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
if (runtimeKey) runtimes.set(runtimeKey, runtime);
|
|
40
|
+
}
|
|
41
|
+
const sessionKey = options.sessionKey || `isolated-${crypto.randomUUID()}`;
|
|
42
|
+
const completed = runtime
|
|
43
|
+
.prompt({
|
|
44
|
+
sessionKey,
|
|
45
|
+
prompt,
|
|
46
|
+
previousSessionId: options.previousSessionId,
|
|
47
|
+
sandbox: options.sandbox,
|
|
48
|
+
profile: options.profile,
|
|
49
|
+
callbacks,
|
|
50
|
+
})
|
|
51
|
+
.finally(async () => {
|
|
52
|
+
if (!persistent) await runtime.dispose();
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
child: runtime.child,
|
|
56
|
+
completed,
|
|
57
|
+
cancel: () => runtime.cancel(sessionKey),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function disposeAcpRuntimes() {
|
|
62
|
+
const active = [...runtimes.values()];
|
|
63
|
+
runtimes.clear();
|
|
64
|
+
await Promise.all(active.map((runtime) => runtime.dispose()));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function activeAcpRuntimeCount() {
|
|
68
|
+
return runtimes.size;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class AcpRuntime {
|
|
72
|
+
constructor(workspace, config, onClose) {
|
|
73
|
+
this.workspace = workspace;
|
|
74
|
+
this.config = config;
|
|
75
|
+
this.onClose = onClose;
|
|
76
|
+
this.sessions = new Map();
|
|
77
|
+
this.turns = new Map();
|
|
78
|
+
this.stderr = "";
|
|
79
|
+
this.disposed = false;
|
|
80
|
+
this.idleTimer = null;
|
|
81
|
+
this.child = spawn(
|
|
82
|
+
config.agent_command,
|
|
83
|
+
Array.isArray(config.agent_args) ? config.agent_args : [],
|
|
84
|
+
{
|
|
85
|
+
cwd: workspace,
|
|
86
|
+
env: { ...process.env, ...(config.agent_env || {}) },
|
|
87
|
+
shell:
|
|
88
|
+
process.platform === "win32" &&
|
|
89
|
+
/\.(cmd|bat)$/i.test(config.agent_command),
|
|
90
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
this.child.stderr.setEncoding("utf8");
|
|
94
|
+
this.child.stderr.on("data", (chunk) => {
|
|
95
|
+
this.stderr = `${this.stderr}${chunk}`.slice(-4_000);
|
|
96
|
+
for (const turn of this.turns.values()) {
|
|
97
|
+
turn.callbacks.onEvent?.({
|
|
98
|
+
type: "agent.stderr",
|
|
99
|
+
message: String(chunk).trim().slice(0, 500),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this.child.once("error", (error) => {
|
|
104
|
+
this.spawnError = error;
|
|
105
|
+
});
|
|
106
|
+
const stream = acp.ndJsonStream(
|
|
107
|
+
Writable.toWeb(this.child.stdin),
|
|
108
|
+
Readable.toWeb(this.child.stdout),
|
|
109
|
+
);
|
|
110
|
+
this.connection = acp
|
|
111
|
+
.client({ name: "EngineerOS" })
|
|
112
|
+
.onRequest(acp.methods.client.session.requestPermission, (ctx) =>
|
|
113
|
+
this.permissionOutcome(ctx.params),
|
|
114
|
+
)
|
|
115
|
+
.onNotification(acp.methods.client.session.update, (ctx) =>
|
|
116
|
+
this.handleUpdate(ctx.params),
|
|
117
|
+
)
|
|
118
|
+
.connect(stream);
|
|
119
|
+
this.context = this.connection.agent;
|
|
120
|
+
this.ready = this.initialize();
|
|
121
|
+
void this.ready.catch(() => {
|
|
122
|
+
void this.dispose();
|
|
123
|
+
});
|
|
124
|
+
this.child.once("exit", () => this.closed());
|
|
125
|
+
void this.connection.closed.then(
|
|
126
|
+
() => this.closed(),
|
|
127
|
+
() => this.closed(),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async initialize() {
|
|
132
|
+
try {
|
|
133
|
+
const initialized = await this.context.request(
|
|
134
|
+
acp.methods.agent.initialize,
|
|
135
|
+
{
|
|
136
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
137
|
+
clientCapabilities: {
|
|
138
|
+
session: { configOptions: { boolean: {} } },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
this.capabilities = initialized.agentCapabilities ?? {};
|
|
143
|
+
return initialized;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw this.failure("could not initialize", error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async prompt({
|
|
150
|
+
sessionKey,
|
|
151
|
+
prompt,
|
|
152
|
+
previousSessionId,
|
|
153
|
+
sandbox = "read-only",
|
|
154
|
+
profile = {},
|
|
155
|
+
callbacks,
|
|
156
|
+
}) {
|
|
157
|
+
this.clearIdleTimer();
|
|
158
|
+
const initialized = await this.ready;
|
|
159
|
+
let session = this.sessions.get(sessionKey);
|
|
160
|
+
if (!session) {
|
|
161
|
+
session = await this.openSession(
|
|
162
|
+
sessionKey,
|
|
163
|
+
previousSessionId,
|
|
164
|
+
sandbox,
|
|
165
|
+
profile,
|
|
166
|
+
);
|
|
55
167
|
callbacks.onEvent?.({
|
|
56
168
|
type: "agent.connected",
|
|
57
|
-
message: `
|
|
169
|
+
message: `Agent connected with protocol ${initialized.protocolVersion}`,
|
|
58
170
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
171
|
+
}
|
|
172
|
+
if (this.turns.has(session.sessionId)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"The agent is already answering another prompt in this session.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const turn = { callbacks, finalMessage: "", sandbox };
|
|
178
|
+
this.turns.set(session.sessionId, turn);
|
|
179
|
+
try {
|
|
180
|
+
const response = await this.context.request(
|
|
181
|
+
acp.methods.agent.session.prompt,
|
|
182
|
+
{
|
|
183
|
+
sessionId: session.sessionId,
|
|
184
|
+
prompt: [{ type: "text", text: prompt }],
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
if (response.stopReason === "error") {
|
|
188
|
+
throw new Error("The ACP agent ended the task with an error.");
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
finalMessage: turn.finalMessage,
|
|
192
|
+
model: profile.model || this.config.agent_name || "acp-agent",
|
|
193
|
+
sessionId: session.sessionId,
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw this.failure("failed", error);
|
|
197
|
+
} finally {
|
|
198
|
+
this.turns.delete(session.sessionId);
|
|
199
|
+
this.scheduleIdleDisposal();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async openSession(sessionKey, previousSessionId, sandbox, profile) {
|
|
204
|
+
let response;
|
|
205
|
+
let sessionId;
|
|
206
|
+
if (previousSessionId && this.capabilities?.loadSession === true) {
|
|
207
|
+
response = await this.context.request(acp.methods.agent.session.load, {
|
|
208
|
+
sessionId: previousSessionId,
|
|
209
|
+
cwd: this.workspace,
|
|
210
|
+
mcpServers: [],
|
|
93
211
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
212
|
+
sessionId = previousSessionId;
|
|
213
|
+
} else {
|
|
214
|
+
response = await this.context.request(acp.methods.agent.session.new, {
|
|
215
|
+
cwd: this.workspace,
|
|
216
|
+
mcpServers: [],
|
|
217
|
+
});
|
|
218
|
+
sessionId = response.sessionId;
|
|
219
|
+
}
|
|
220
|
+
await this.applyMode(sessionId, response.modes, sandbox);
|
|
221
|
+
await this.applyProfile(sessionId, response.configOptions, profile);
|
|
222
|
+
const session = { sessionId };
|
|
223
|
+
this.sessions.set(sessionKey, session);
|
|
224
|
+
return session;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async applyMode(sessionId, modes, sandbox) {
|
|
228
|
+
const modeId = this.config.agent_modes?.[sandbox];
|
|
229
|
+
if (!modeId) return;
|
|
230
|
+
if (!modes?.availableModes?.some((mode) => mode.id === modeId)) {
|
|
97
231
|
throw new Error(
|
|
98
|
-
|
|
232
|
+
`${this.config.agent_name || "This ACP agent"} does not advertise the required '${modeId}' mode for ${sandbox} prompts.`,
|
|
99
233
|
);
|
|
100
|
-
}
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
child.stdout.destroy();
|
|
105
|
-
child.stderr.destroy();
|
|
106
|
-
child.unref();
|
|
234
|
+
}
|
|
235
|
+
await this.context.request(acp.methods.agent.session.setMode, {
|
|
236
|
+
sessionId,
|
|
237
|
+
modeId,
|
|
107
238
|
});
|
|
108
|
-
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async applyProfile(sessionId, configOptions = [], profile = {}) {
|
|
242
|
+
const requested = [
|
|
243
|
+
["model", profile.model],
|
|
244
|
+
["thought_level", profile.reasoning_effort],
|
|
245
|
+
];
|
|
246
|
+
for (const [category, value] of requested) {
|
|
247
|
+
if (!value) continue;
|
|
248
|
+
const option = configOptions?.find(
|
|
249
|
+
(candidate) => candidate.category === category,
|
|
250
|
+
);
|
|
251
|
+
if (!option || option.type !== "select") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`This ACP agent does not advertise ${category.replace("_", " ")} selection.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const available = option.options.flatMap((candidate) =>
|
|
257
|
+
Array.isArray(candidate.options) ? candidate.options : [candidate],
|
|
258
|
+
);
|
|
259
|
+
const selected = available.find(
|
|
260
|
+
(candidate) =>
|
|
261
|
+
candidate.value === value ||
|
|
262
|
+
candidate.name?.toLowerCase() === String(value).toLowerCase(),
|
|
263
|
+
);
|
|
264
|
+
if (!selected) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`${option.name} does not offer '${value}'. Available values: ${available
|
|
267
|
+
.map((candidate) => candidate.value)
|
|
268
|
+
.join(", ")}.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
await this.context.request(acp.methods.agent.session.setConfigOption, {
|
|
272
|
+
sessionId,
|
|
273
|
+
configId: option.id,
|
|
274
|
+
value: selected.value,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async handleUpdate(notification) {
|
|
280
|
+
const turn = this.turns.get(notification.sessionId);
|
|
281
|
+
if (!turn) return;
|
|
282
|
+
const update = notification.update;
|
|
283
|
+
if (
|
|
284
|
+
update.sessionUpdate === "agent_message_chunk" &&
|
|
285
|
+
update.content?.type === "text"
|
|
286
|
+
) {
|
|
287
|
+
turn.finalMessage += update.content.text;
|
|
288
|
+
}
|
|
289
|
+
turn.callbacks.onEvent?.({
|
|
290
|
+
type: `acp.${update.sessionUpdate}`,
|
|
291
|
+
update,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
permissionOutcome(request) {
|
|
296
|
+
const turn = this.turns.get(request.sessionId);
|
|
297
|
+
return permissionOutcome(
|
|
298
|
+
request.options,
|
|
299
|
+
turn?.sandbox === "workspace-write",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async cancel(sessionKey) {
|
|
304
|
+
const session = this.sessions.get(sessionKey);
|
|
305
|
+
if (!session || !this.isRunning()) return;
|
|
306
|
+
await this.context.notify(acp.methods.agent.session.cancel, {
|
|
307
|
+
sessionId: session.sessionId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
isRunning() {
|
|
312
|
+
return (
|
|
313
|
+
!this.disposed &&
|
|
314
|
+
this.child.exitCode === null &&
|
|
315
|
+
!this.connection.signal.aborted
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
scheduleIdleDisposal() {
|
|
320
|
+
if (this.turns.size || this.disposed) return;
|
|
321
|
+
this.clearIdleTimer();
|
|
322
|
+
this.idleTimer = setTimeout(() => void this.dispose(), RUNTIME_IDLE_MS);
|
|
323
|
+
this.idleTimer.unref?.();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
clearIdleTimer() {
|
|
327
|
+
clearTimeout(this.idleTimer);
|
|
328
|
+
this.idleTimer = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
failure(action, error) {
|
|
332
|
+
const detail = this.stderr.trim().slice(-1_000);
|
|
333
|
+
const cause = this.spawnError || error;
|
|
334
|
+
return new Error(
|
|
335
|
+
`ACP agent ${action}: ${cause instanceof Error ? cause.message : String(cause)}${detail ? ` ${detail}` : ""}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
closed() {
|
|
340
|
+
if (this.disposed) return;
|
|
341
|
+
this.disposed = true;
|
|
342
|
+
this.clearIdleTimer();
|
|
343
|
+
this.onClose?.();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async dispose() {
|
|
347
|
+
if (this.disposed) return;
|
|
348
|
+
this.disposed = true;
|
|
349
|
+
this.clearIdleTimer();
|
|
350
|
+
this.onClose?.();
|
|
351
|
+
this.connection.close();
|
|
352
|
+
if (this.child.exitCode === null) {
|
|
353
|
+
this.child.kill("SIGTERM");
|
|
354
|
+
await Promise.race([
|
|
355
|
+
new Promise((resolve) => this.child.once("exit", resolve)),
|
|
356
|
+
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
if (this.child.exitCode === null) this.child.kill("SIGKILL");
|
|
360
|
+
this.child.stdin.destroy();
|
|
361
|
+
this.child.stdout.destroy();
|
|
362
|
+
this.child.stderr.destroy();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function acpRuntimeKey(workspace, config) {
|
|
367
|
+
return JSON.stringify([
|
|
368
|
+
workspace,
|
|
369
|
+
config.agent_command,
|
|
370
|
+
config.agent_args || [],
|
|
371
|
+
config.agent_env || {},
|
|
372
|
+
]);
|
|
109
373
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const ROLE_DEFINITIONS = Object.freeze({
|
|
4
|
+
research: Object.freeze({
|
|
5
|
+
title: "Repository Research",
|
|
6
|
+
sandboxMode: "read-only",
|
|
7
|
+
skill: "codebase-research",
|
|
8
|
+
instruction:
|
|
9
|
+
"Investigate the connected workspace deeply enough to answer from observed evidence. Trace relevant relationships, distinguish facts from inference, and do not change the workspace.",
|
|
10
|
+
}),
|
|
11
|
+
planning: Object.freeze({
|
|
12
|
+
title: "Change Planning",
|
|
13
|
+
sandboxMode: "read-only",
|
|
14
|
+
skill: "change-planning",
|
|
15
|
+
instruction:
|
|
16
|
+
"Turn the request and current workspace evidence into a decision-complete change plan. Resolve discoverable questions through inspection, state consequential assumptions, and do not implement the plan.",
|
|
17
|
+
}),
|
|
18
|
+
implementation: Object.freeze({
|
|
19
|
+
title: "Goal Implementation",
|
|
20
|
+
sandboxMode: "workspace-write",
|
|
21
|
+
skill: "goal-execution",
|
|
22
|
+
instruction:
|
|
23
|
+
"Execute the bounded Goal autonomously, keep changes inside its boundary, and verify the result. EngineerOS owns the final commit and acceptance workflow.",
|
|
24
|
+
}),
|
|
25
|
+
verification: Object.freeze({
|
|
26
|
+
title: "Independent Verification",
|
|
27
|
+
sandboxMode: "read-only",
|
|
28
|
+
skill: "change-verification",
|
|
29
|
+
instruction:
|
|
30
|
+
"Independently inspect and test the supplied revision. Report only directly observed evidence and do not repair or otherwise modify the workspace.",
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const skillCache = new Map();
|
|
35
|
+
|
|
36
|
+
export const AGENT_ROLE_IDS = Object.freeze(Object.keys(ROLE_DEFINITIONS));
|
|
37
|
+
export const AGENT_SKILL_IDS = Object.freeze(
|
|
38
|
+
AGENT_ROLE_IDS.map((roleId) => ROLE_DEFINITIONS[roleId].skill),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export function agentHarnessCapabilities() {
|
|
42
|
+
return {
|
|
43
|
+
agent_roles: [...AGENT_ROLE_IDS],
|
|
44
|
+
agent_skills: [...AGENT_SKILL_IDS],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildAgentHarnessPrompt({ agentRole, prompt, sandboxMode }) {
|
|
49
|
+
const role = ROLE_DEFINITIONS[agentRole];
|
|
50
|
+
if (!role) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`EngineerOS assignment has an unsupported agent_role. Expected one of: ${AGENT_ROLE_IDS.join(", ")}.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (role.sandboxMode !== sandboxMode) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`EngineerOS ${agentRole} role requires ${role.sandboxMode} access, but the assignment requested ${sandboxMode}.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const assignment = String(prompt || "").trim();
|
|
61
|
+
if (!assignment) {
|
|
62
|
+
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
"# EngineerOS Agent Assignment",
|
|
67
|
+
"",
|
|
68
|
+
"## Active Role",
|
|
69
|
+
"",
|
|
70
|
+
`- Role: ${role.title}`,
|
|
71
|
+
`- Access: ${role.sandboxMode}`,
|
|
72
|
+
`- Responsibility: ${role.instruction}`,
|
|
73
|
+
"- User-facing language: refer to yourself neutrally as the Agent. Do not expose internal role, harness, provider, or coding-agent terminology unless the user asks.",
|
|
74
|
+
"",
|
|
75
|
+
"## Interaction Contract",
|
|
76
|
+
"",
|
|
77
|
+
"- Infer the current project situation from the assignment and workspace evidence before responding.",
|
|
78
|
+
"- Lead with the useful answer or outcome. Do not narrate routine searches, tool calls, or internal work.",
|
|
79
|
+
"- When one next activity clearly follows and would help, offer exactly one short, concrete suggestion. Do not force a next step when none is useful.",
|
|
80
|
+
"- Ask a question only when a consequential choice cannot be resolved from available evidence.",
|
|
81
|
+
"",
|
|
82
|
+
"## Active Skill",
|
|
83
|
+
"",
|
|
84
|
+
bundledSkill(role.skill),
|
|
85
|
+
"",
|
|
86
|
+
"## Assignment",
|
|
87
|
+
"",
|
|
88
|
+
assignment,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function bundledSkill(skillName) {
|
|
93
|
+
const cached = skillCache.get(skillName);
|
|
94
|
+
if (cached) return cached;
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(
|
|
97
|
+
new URL(`./skills/${skillName}/SKILL.md`, import.meta.url),
|
|
98
|
+
"utf8",
|
|
99
|
+
).trim();
|
|
100
|
+
skillCache.set(skillName, content);
|
|
101
|
+
return content;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`EngineerOS bundled skill '${skillName}' is unavailable. Reinstall @engineeros/connector.`,
|
|
105
|
+
{ cause: error },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|