@engineeros/connector 0.9.2 → 0.10.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/package.json +1 -1
- package/src/acp-client.mjs +381 -373
- package/src/agent-harness.mjs +196 -183
- package/src/codex-app-server.mjs +139 -0
- package/src/runner.mjs +1368 -1324
package/src/agent-harness.mjs
CHANGED
|
@@ -1,183 +1,196 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
research: Object.freeze({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sandboxMode: "
|
|
14
|
-
skill: "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
const role =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const ROLE_BOUNDARIES = Object.freeze({
|
|
4
|
+
research: Object.freeze({
|
|
5
|
+
sandboxMode: "read-only",
|
|
6
|
+
skill: "codebase-research",
|
|
7
|
+
}),
|
|
8
|
+
planning: Object.freeze({
|
|
9
|
+
sandboxMode: "read-only",
|
|
10
|
+
skill: "change-planning",
|
|
11
|
+
}),
|
|
12
|
+
implementation: Object.freeze({
|
|
13
|
+
sandboxMode: "workspace-write",
|
|
14
|
+
skill: "goal-execution",
|
|
15
|
+
}),
|
|
16
|
+
verification: Object.freeze({
|
|
17
|
+
sandboxMode: "read-only",
|
|
18
|
+
skill: "change-verification",
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const skillCache = new Map();
|
|
23
|
+
|
|
24
|
+
export const AGENT_ROLE_IDS = Object.freeze(Object.keys(ROLE_BOUNDARIES));
|
|
25
|
+
export const AGENT_SKILL_IDS = Object.freeze(
|
|
26
|
+
AGENT_ROLE_IDS.map((roleId) => ROLE_BOUNDARIES[roleId].skill),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function agentHarnessCapabilities() {
|
|
30
|
+
return {
|
|
31
|
+
agent_roles: [...AGENT_ROLE_IDS],
|
|
32
|
+
agent_skills: [...AGENT_SKILL_IDS],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildAgentHarnessPrompt({
|
|
37
|
+
agentRole,
|
|
38
|
+
agentDefinition,
|
|
39
|
+
prompt,
|
|
40
|
+
sandboxMode,
|
|
41
|
+
requiredOutputHeading,
|
|
42
|
+
}) {
|
|
43
|
+
const boundary = ROLE_BOUNDARIES[agentRole];
|
|
44
|
+
if (!boundary) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`EngineerOS assignment has an unsupported agent_role. Expected one of: ${AGENT_ROLE_IDS.join(", ")}.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (boundary.sandboxMode !== sandboxMode) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`EngineerOS ${agentRole} role requires ${boundary.sandboxMode} access, but the assignment requested ${sandboxMode}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const role = validatedAgentDefinition(agentDefinition, agentRole, boundary);
|
|
55
|
+
const assignment = String(prompt || "").trim();
|
|
56
|
+
if (!assignment) {
|
|
57
|
+
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|
|
58
|
+
}
|
|
59
|
+
const outputHeading = normalizedOutputHeading(requiredOutputHeading);
|
|
60
|
+
|
|
61
|
+
const sections = [
|
|
62
|
+
"# EngineerOS Agent Assignment",
|
|
63
|
+
"",
|
|
64
|
+
"## Active Role",
|
|
65
|
+
"",
|
|
66
|
+
`- Role: ${role.title}`,
|
|
67
|
+
`- Access: ${role.access}`,
|
|
68
|
+
`- Responsibility: ${role.instruction}`,
|
|
69
|
+
`- Permitted tools: ${role.tools.length ? role.tools.join(", ") : "none"}`,
|
|
70
|
+
"- 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.",
|
|
71
|
+
];
|
|
72
|
+
if (!outputHeading) {
|
|
73
|
+
sections.push(
|
|
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
|
+
"- If the Assignment defines a response format, follow it exactly instead of adding conversational guidance.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
sections.push(
|
|
85
|
+
"",
|
|
86
|
+
"## Active Skill",
|
|
87
|
+
"",
|
|
88
|
+
bundledSkill(role.skill),
|
|
89
|
+
"",
|
|
90
|
+
"## Assignment",
|
|
91
|
+
"",
|
|
92
|
+
assignment,
|
|
93
|
+
);
|
|
94
|
+
if (outputHeading) {
|
|
95
|
+
sections.push(
|
|
96
|
+
"",
|
|
97
|
+
"## Final Response Contract",
|
|
98
|
+
"",
|
|
99
|
+
"- Return only the structured Markdown required by the Assignment.",
|
|
100
|
+
`- The first non-whitespace line must be exactly: ${outputHeading}`,
|
|
101
|
+
"- Do not add a preamble, status message, commentary, or code fence.",
|
|
102
|
+
"- Do not append a conversational summary or next activity.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return sections.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validatedAgentDefinition(definition, agentRole, boundary) {
|
|
109
|
+
if (!definition || typeof definition !== "object") {
|
|
110
|
+
throw new Error("EngineerOS assignment is missing agent_definition.");
|
|
111
|
+
}
|
|
112
|
+
if (definition.role !== agentRole || definition.access !== boundary.sandboxMode) {
|
|
113
|
+
throw new Error("EngineerOS agent_definition does not match the assigned role and access.");
|
|
114
|
+
}
|
|
115
|
+
if (definition.skill !== boundary.skill) {
|
|
116
|
+
throw new Error(`EngineerOS ${agentRole} role requires the ${boundary.skill} skill.`);
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
typeof definition.title !== "string" ||
|
|
120
|
+
!definition.title.trim() ||
|
|
121
|
+
typeof definition.instruction !== "string" ||
|
|
122
|
+
!definition.instruction.trim() ||
|
|
123
|
+
!Array.isArray(definition.tools)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error("EngineerOS agent_definition is incomplete.");
|
|
126
|
+
}
|
|
127
|
+
return definition;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeAgentStructuredOutput(
|
|
131
|
+
response,
|
|
132
|
+
requiredOutputHeading,
|
|
133
|
+
) {
|
|
134
|
+
const outputHeading = normalizedOutputHeading(requiredOutputHeading);
|
|
135
|
+
if (!outputHeading) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"EngineerOS structured output requires a heading contract.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const content = String(response || "").trim();
|
|
141
|
+
if (!content) {
|
|
142
|
+
throw new Error("Agent completed without returning a response.");
|
|
143
|
+
}
|
|
144
|
+
const lines = content.split(/\r?\n/);
|
|
145
|
+
const headingIndex = lines.findIndex(
|
|
146
|
+
(line, index) => index < 20 && line.trim() === outputHeading,
|
|
147
|
+
);
|
|
148
|
+
const preamble =
|
|
149
|
+
headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
|
|
150
|
+
if (headingIndex < 0) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Agent response is missing the required heading '${outputHeading}'.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (preamble.length > 2_000) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Agent response contains too much text before the required heading '${outputHeading}'.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const normalized = lines.slice(headingIndex).join("\n").trim();
|
|
161
|
+
if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return normalized;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function bundledSkill(skillName) {
|
|
170
|
+
const cached = skillCache.get(skillName);
|
|
171
|
+
if (cached) return cached;
|
|
172
|
+
try {
|
|
173
|
+
const content = readFileSync(
|
|
174
|
+
new URL(`./skills/${skillName}/SKILL.md`, import.meta.url),
|
|
175
|
+
"utf8",
|
|
176
|
+
).trim();
|
|
177
|
+
skillCache.set(skillName, content);
|
|
178
|
+
return content;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`EngineerOS bundled skill '${skillName}' is unavailable. Reinstall @engineeros/connector.`,
|
|
182
|
+
{ cause: error },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizedOutputHeading(value) {
|
|
188
|
+
if (value === undefined || value === null) return null;
|
|
189
|
+
const heading = String(value).trim();
|
|
190
|
+
if (!/^# [^\r\n]+$/.test(heading)) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"EngineerOS requiredOutputHeading must be one level-one Markdown heading.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return heading;
|
|
196
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
|
|
4
|
+
export function launchCodexAppServer({
|
|
5
|
+
workspace,
|
|
6
|
+
prompt,
|
|
7
|
+
sandbox,
|
|
8
|
+
profile = {},
|
|
9
|
+
previousSessionId,
|
|
10
|
+
callbacks = {},
|
|
11
|
+
command = process.env.CODEX_BIN || (process.platform === "win32" ? "codex.cmd" : "codex"),
|
|
12
|
+
spawnProcess = spawn,
|
|
13
|
+
}) {
|
|
14
|
+
const child = spawnProcess(command, ["app-server", "--stdio"], {
|
|
15
|
+
cwd: workspace,
|
|
16
|
+
env: process.env,
|
|
17
|
+
shell: process.platform === "win32",
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
});
|
|
21
|
+
const pending = new Map();
|
|
22
|
+
let requestId = 0;
|
|
23
|
+
let threadId = previousSessionId || "";
|
|
24
|
+
let turnId = "";
|
|
25
|
+
let finalMessage = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
let settled = false;
|
|
28
|
+
|
|
29
|
+
const request = (method, params) =>
|
|
30
|
+
new Promise((resolve, reject) => {
|
|
31
|
+
const id = ++requestId;
|
|
32
|
+
pending.set(id, { resolve, reject });
|
|
33
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const completed = new Promise((resolve, reject) => {
|
|
37
|
+
const finish = (error) => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
for (const waiter of pending.values()) waiter.reject(error || new Error("Codex app-server stopped."));
|
|
41
|
+
pending.clear();
|
|
42
|
+
if (error) reject(error);
|
|
43
|
+
else resolve({ finalMessage, output: stderr.slice(-20_000), sessionId: threadId });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
child.once("error", finish);
|
|
47
|
+
child.once("close", (code) => {
|
|
48
|
+
if (!settled) finish(new Error(`Codex app-server exited before completing the turn (code ${code ?? 1}). ${stderr}`));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const lines = readline.createInterface({ input: child.stdout });
|
|
52
|
+
lines.on("line", (line) => {
|
|
53
|
+
let message;
|
|
54
|
+
try {
|
|
55
|
+
message = JSON.parse(line);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (message.id !== undefined) {
|
|
60
|
+
const waiter = pending.get(message.id);
|
|
61
|
+
if (!waiter) return;
|
|
62
|
+
pending.delete(message.id);
|
|
63
|
+
if (message.error) waiter.reject(new Error(message.error.message || "Codex app-server request failed."));
|
|
64
|
+
else waiter.resolve(message.result);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const params = message.params || {};
|
|
68
|
+
if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
|
|
69
|
+
finalMessage += params.delta;
|
|
70
|
+
callbacks.onEvent?.({ type: "codex.agent_message_delta", delta: params.delta });
|
|
71
|
+
} else if (message.method === "item/started" || message.method === "item/completed") {
|
|
72
|
+
callbacks.onEvent?.({ type: `codex.${message.method}`, item: params.item });
|
|
73
|
+
} else if (message.method === "thread/tokenUsage/updated") {
|
|
74
|
+
callbacks.onEvent?.({ type: "codex.usage", update: params });
|
|
75
|
+
} else if (message.method === "turn/completed" && params.threadId === threadId) {
|
|
76
|
+
const status = params.turn?.status;
|
|
77
|
+
if (status === "failed") finish(new Error(params.turn?.error?.message || "Codex turn failed."));
|
|
78
|
+
else if (!finalMessage.trim()) finish(new Error("Codex completed without returning a response."));
|
|
79
|
+
else finish();
|
|
80
|
+
child.kill();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
child.stderr.setEncoding("utf8");
|
|
85
|
+
child.stderr.on("data", (chunk) => {
|
|
86
|
+
stderr = `${stderr}${chunk}`.slice(-20_000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
void (async () => {
|
|
90
|
+
try {
|
|
91
|
+
await request("initialize", {
|
|
92
|
+
clientInfo: { name: "engineeros-connector", title: "EngineerOS Connector", version: "0.10.1" },
|
|
93
|
+
});
|
|
94
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
95
|
+
const threadResult = previousSessionId
|
|
96
|
+
? await request("thread/resume", {
|
|
97
|
+
threadId: previousSessionId,
|
|
98
|
+
cwd: workspace,
|
|
99
|
+
sandbox,
|
|
100
|
+
approvalPolicy: "never",
|
|
101
|
+
model: profile.model || null,
|
|
102
|
+
})
|
|
103
|
+
: await request("thread/start", {
|
|
104
|
+
cwd: workspace,
|
|
105
|
+
sandbox,
|
|
106
|
+
approvalPolicy: "never",
|
|
107
|
+
model: profile.model || null,
|
|
108
|
+
ephemeral: false,
|
|
109
|
+
});
|
|
110
|
+
threadId = threadResult.thread.id;
|
|
111
|
+
callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
|
|
112
|
+
const turnResult = await request("turn/start", {
|
|
113
|
+
threadId,
|
|
114
|
+
input: [{ type: "text", text: prompt }],
|
|
115
|
+
effort: profile.reasoning_effort || null,
|
|
116
|
+
});
|
|
117
|
+
turnId = turnResult.turn.id;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
child.kill();
|
|
120
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
child,
|
|
127
|
+
completed,
|
|
128
|
+
cancel: async () => {
|
|
129
|
+
if (threadId && turnId && !settled) {
|
|
130
|
+
try {
|
|
131
|
+
await request("turn/interrupt", { threadId, turnId });
|
|
132
|
+
} catch {
|
|
133
|
+
// Process termination below is the final cancellation boundary.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
child.kill();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|