@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.
@@ -1,183 +1,196 @@
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({
49
- agentRole,
50
- prompt,
51
- sandboxMode,
52
- requiredOutputHeading,
53
- }) {
54
- const role = ROLE_DEFINITIONS[agentRole];
55
- if (!role) {
56
- throw new Error(
57
- `EngineerOS assignment has an unsupported agent_role. Expected one of: ${AGENT_ROLE_IDS.join(", ")}.`,
58
- );
59
- }
60
- if (role.sandboxMode !== sandboxMode) {
61
- throw new Error(
62
- `EngineerOS ${agentRole} role requires ${role.sandboxMode} access, but the assignment requested ${sandboxMode}.`,
63
- );
64
- }
65
- const assignment = String(prompt || "").trim();
66
- if (!assignment) {
67
- throw new Error("EngineerOS assignment is missing prompt_markdown.");
68
- }
69
- const outputHeading = normalizedOutputHeading(requiredOutputHeading);
70
-
71
- const sections = [
72
- "# EngineerOS Agent Assignment",
73
- "",
74
- "## Active Role",
75
- "",
76
- `- Role: ${role.title}`,
77
- `- Access: ${role.sandboxMode}`,
78
- `- Responsibility: ${role.instruction}`,
79
- "- 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.",
80
- ];
81
- if (!outputHeading) {
82
- sections.push(
83
- "",
84
- "## Interaction Contract",
85
- "",
86
- "- Infer the current project situation from the assignment and workspace evidence before responding.",
87
- "- Lead with the useful answer or outcome. Do not narrate routine searches, tool calls, or internal work.",
88
- "- 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.",
89
- "- Ask a question only when a consequential choice cannot be resolved from available evidence.",
90
- "- If the Assignment defines a response format, follow it exactly instead of adding conversational guidance.",
91
- );
92
- }
93
- sections.push(
94
- "",
95
- "## Active Skill",
96
- "",
97
- bundledSkill(role.skill),
98
- "",
99
- "## Assignment",
100
- "",
101
- assignment,
102
- );
103
- if (outputHeading) {
104
- sections.push(
105
- "",
106
- "## Final Response Contract",
107
- "",
108
- "- Return only the structured Markdown required by the Assignment.",
109
- `- The first non-whitespace line must be exactly: ${outputHeading}`,
110
- "- Do not add a preamble, status message, commentary, or code fence.",
111
- "- Do not append a conversational summary or next activity.",
112
- );
113
- }
114
- return sections.join("\n");
115
- }
116
-
117
- export function normalizeAgentStructuredOutput(
118
- response,
119
- requiredOutputHeading,
120
- ) {
121
- const outputHeading = normalizedOutputHeading(requiredOutputHeading);
122
- if (!outputHeading) {
123
- throw new Error(
124
- "EngineerOS structured output requires a heading contract.",
125
- );
126
- }
127
- const content = String(response || "").trim();
128
- if (!content) {
129
- throw new Error("Agent completed without returning a response.");
130
- }
131
- const lines = content.split(/\r?\n/);
132
- const headingIndex = lines.findIndex(
133
- (line, index) => index < 20 && line.trim() === outputHeading,
134
- );
135
- const preamble =
136
- headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
137
- if (headingIndex < 0) {
138
- throw new Error(
139
- `Agent response is missing the required heading '${outputHeading}'.`,
140
- );
141
- }
142
- if (preamble.length > 2_000) {
143
- throw new Error(
144
- `Agent response contains too much text before the required heading '${outputHeading}'.`,
145
- );
146
- }
147
- const normalized = lines.slice(headingIndex).join("\n").trim();
148
- if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
149
- throw new Error(
150
- `Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
151
- );
152
- }
153
- return normalized;
154
- }
155
-
156
- function bundledSkill(skillName) {
157
- const cached = skillCache.get(skillName);
158
- if (cached) return cached;
159
- try {
160
- const content = readFileSync(
161
- new URL(`./skills/${skillName}/SKILL.md`, import.meta.url),
162
- "utf8",
163
- ).trim();
164
- skillCache.set(skillName, content);
165
- return content;
166
- } catch (error) {
167
- throw new Error(
168
- `EngineerOS bundled skill '${skillName}' is unavailable. Reinstall @engineeros/connector.`,
169
- { cause: error },
170
- );
171
- }
172
- }
173
-
174
- function normalizedOutputHeading(value) {
175
- if (value === undefined || value === null) return null;
176
- const heading = String(value).trim();
177
- if (!/^# [^\r\n]+$/.test(heading)) {
178
- throw new Error(
179
- "EngineerOS requiredOutputHeading must be one level-one Markdown heading.",
180
- );
181
- }
182
- return heading;
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
+ }