replicas-engine 0.1.1043 → 0.1.1045

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.
@@ -2,28 +2,47 @@
2
2
  import { createRequire as __createRequire } from 'node:module';
3
3
  const require = __createRequire(import.meta.url);
4
4
  import {
5
- recoverCompletedTurn
6
- } from "./chunk-P73LGPQO.js";
5
+ THREAD_TOKEN_USAGE_UPDATED_METHOD,
6
+ fetchOpencodeModelVariants,
7
+ getOpenCodeGoModel,
8
+ getOpencodeProvider,
9
+ initializeMuse,
10
+ museTurnStartParams,
11
+ opencodeConfig,
12
+ opencodeThinkingVariant,
13
+ recoverCompletedTurn,
14
+ spawnMuseMsp,
15
+ startMuseSession
16
+ } from "./chunk-MRMHCG4U.js";
7
17
  import {
8
18
  AppServerProcess,
9
19
  buildCodexAgentEnv
10
- } from "./chunk-S5UZOWCH.js";
11
- import "./chunk-YKJ47TTY.js";
20
+ } from "./chunk-6EZXQ2C7.js";
21
+ import "./chunk-VSDRLCZY.js";
12
22
  import {
13
23
  AGENT,
14
- headlessAgentRequestSchema
15
- } from "./chunk-6D6XBIE7.js";
24
+ OPENCODE_GO_PROVIDER,
25
+ codexReasoningEffortForThinkingLevel,
26
+ extractJsonObject,
27
+ headlessAgentRequestSchema,
28
+ headlessAgentUsagePath,
29
+ isRecord,
30
+ withTimeout
31
+ } from "./chunk-PZEZIF5C.js";
16
32
  import "./chunk-VEQXQN22.js";
17
33
 
18
34
  // src/headless-agent.ts
19
35
  import { readFile, writeFile } from "fs/promises";
20
36
  import { query } from "@anthropic-ai/claude-agent-sdk";
37
+ import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk/v2";
38
+ import { Agent as CursorAgent } from "@cursor/sdk";
21
39
  async function runClaude(request) {
22
40
  const response = query({
23
41
  prompt: request.prompt,
24
42
  options: {
25
43
  cwd: request.workingDirectory,
26
44
  model: request.model,
45
+ ...request.thinkingLevel ? { effort: request.thinkingLevel } : {},
27
46
  tools: [],
28
47
  permissionMode: "dontAsk",
29
48
  settingSources: [],
@@ -32,21 +51,31 @@ async function runClaude(request) {
32
51
  env: { ...process.env, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" }
33
52
  }
34
53
  });
35
- let result = null;
36
- try {
54
+ const lastResult = async () => {
55
+ let last = null;
37
56
  for await (const message of response) {
38
- if (message.type === "result") result = message;
57
+ if (message.type === "result") last = message;
39
58
  }
59
+ return last;
60
+ };
61
+ let result;
62
+ try {
63
+ result = await withTimeout(lastResult(), request.timeoutSeconds * 1e3, "Claude run timed out");
40
64
  } finally {
41
65
  response.close();
42
66
  }
43
67
  if (!result) throw new Error("Claude Agent SDK returned no result");
44
68
  if (result.subtype !== "success") throw new Error(result.errors.join("\n") || result.subtype);
45
- return result.structured_output ?? JSON.parse(result.result);
69
+ const { input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens } = result.usage;
70
+ return {
71
+ output: result.structured_output ?? JSON.parse(result.result),
72
+ usage: { inputTokens: input_tokens + cache_creation_input_tokens + cache_read_input_tokens, outputTokens: output_tokens }
73
+ };
46
74
  }
47
75
  function runCodexTurn(client, params, timeoutMs) {
48
76
  return new Promise((resolve, reject) => {
49
77
  const completedItems = [];
78
+ let usage = null;
50
79
  const messageDeltas = /* @__PURE__ */ new Map();
51
80
  const cleanup = () => {
52
81
  clearTimeout(timeout);
@@ -54,6 +83,12 @@ function runCodexTurn(client, params, timeoutMs) {
54
83
  client.off("dispose", onDispose);
55
84
  };
56
85
  const onNotification = (notification) => {
86
+ if (notification.method === THREAD_TOKEN_USAGE_UPDATED_METHOD) {
87
+ if (notification.params.threadId !== params.threadId) return;
88
+ const { inputTokens, outputTokens } = notification.params.tokenUsage.total;
89
+ usage = { inputTokens, outputTokens };
90
+ return;
91
+ }
57
92
  if (notification.method === "item/completed") {
58
93
  if (notification.params.threadId !== params.threadId || notification.params.item.type !== "agentMessage") return;
59
94
  completedItems.push(notification.params.item);
@@ -71,7 +106,7 @@ function runCodexTurn(client, params, timeoutMs) {
71
106
  const turn = recoverCompletedTurn(notification.params.turn, completedItems, messageDeltas);
72
107
  const message = turn.items.findLast((item) => item.type === "agentMessage")?.text ?? null;
73
108
  cleanup();
74
- resolve({ turn, message });
109
+ resolve({ turn, message, usage });
75
110
  };
76
111
  const onDispose = (error) => {
77
112
  cleanup();
@@ -122,20 +157,112 @@ async function runCodex(request) {
122
157
  sandbox: "read-only",
123
158
  ephemeral: true
124
159
  });
125
- const { turn, message } = await runCodexTurn(client, {
160
+ const { turn, message, usage } = await runCodexTurn(client, {
126
161
  threadId: thread.thread.id,
127
162
  input: [{ type: "text", text: request.prompt, text_elements: [] }],
128
163
  outputSchema: request.outputSchema,
164
+ ...request.thinkingLevel ? { effort: codexReasoningEffortForThinkingLevel(request.thinkingLevel) } : {},
129
165
  approvalPolicy: "never",
130
166
  approvalsReviewer: "user"
131
167
  }, request.timeoutSeconds * 1e3);
132
168
  if (turn.status === "failed") throw new Error(turn.error?.message ?? "Codex ASP turn failed");
133
169
  if (!message) throw new Error("Codex ASP returned no final message");
134
- return JSON.parse(message);
170
+ return { output: JSON.parse(message), usage };
135
171
  } finally {
136
172
  await appServer.stop();
137
173
  }
138
174
  }
175
+ async function runOpencode(request) {
176
+ const provider = await getOpencodeProvider();
177
+ const model = provider === OPENCODE_GO_PROVIDER ? getOpenCodeGoModel(request.model) : request.model;
178
+ const server = await createOpencodeServer({ port: 0, timeout: 3e4, config: opencodeConfig(provider, model) });
179
+ try {
180
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
181
+ const client = createOpencodeClient({
182
+ baseUrl: server.url,
183
+ ...password ? { headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` } } : {}
184
+ });
185
+ const variant = opencodeThinkingVariant((await fetchOpencodeModelVariants(client, request.workingDirectory, provider)).get(model), request.thinkingLevel);
186
+ const session = await client.session.create({ directory: request.workingDirectory, model: { providerID: provider, id: model } }, { throwOnError: true });
187
+ const result = await client.session.prompt({
188
+ sessionID: session.data.id,
189
+ directory: request.workingDirectory,
190
+ model: { providerID: provider, modelID: model },
191
+ ...variant ? { variant } : {},
192
+ format: { type: "json_schema", schema: request.outputSchema, retryCount: 2 },
193
+ parts: [{ type: "text", text: request.prompt }]
194
+ }, { throwOnError: true, signal: AbortSignal.timeout(request.timeoutSeconds * 1e3) });
195
+ const { info } = result.data;
196
+ if (info.error) throw new Error(`${info.error.name}: ${JSON.stringify(info.error.data)}`);
197
+ if (info.structured === void 0) throw new Error("Opencode returned no structured output");
198
+ return {
199
+ output: info.structured,
200
+ usage: { inputTokens: info.tokens.input + info.tokens.cache.read + info.tokens.cache.write, outputTokens: info.tokens.output + info.tokens.reasoning }
201
+ };
202
+ } finally {
203
+ server.close();
204
+ }
205
+ }
206
+ var jsonReplyPrompt = (request) => `${request.prompt}
207
+
208
+ Reply with only JSON, no prose or code fences, matching this JSON Schema:
209
+ ${JSON.stringify(request.outputSchema)}`;
210
+ function parseJsonReply(reply) {
211
+ const output = extractJsonObject(reply);
212
+ if (output === null) throw new Error(`Reply was not JSON: ${reply.slice(0, 200)}`);
213
+ return output;
214
+ }
215
+ async function runCursor(request) {
216
+ const apiKey = process.env.CURSOR_API_KEY;
217
+ if (!apiKey) throw new Error("Cursor API key is not configured");
218
+ const agent = await CursorAgent.create({ apiKey, model: { id: request.model }, local: { cwd: request.workingDirectory, settingSources: [] } });
219
+ const run = await agent.send(jsonReplyPrompt(request));
220
+ const result = await withTimeout(run.wait(), request.timeoutSeconds * 1e3, "Cursor run timed out");
221
+ if (result.status !== "finished" || !result.result) throw new Error(result.error?.message ?? `Cursor run ${result.status}`);
222
+ return {
223
+ output: parseJsonReply(result.result),
224
+ usage: result.usage ? { inputTokens: result.usage.inputTokens + result.usage.cacheReadTokens + result.usage.cacheWriteTokens, outputTokens: result.usage.outputTokens } : null
225
+ };
226
+ }
227
+ async function runMuse(request) {
228
+ const { child, client } = spawnMuseMsp(request.workingDirectory);
229
+ let stderr = "";
230
+ child.stderr.on("data", (chunk) => {
231
+ stderr = `${stderr}${chunk}`.slice(-2e3);
232
+ });
233
+ child.on("exit", (code, signal) => client.dispose(new Error(`Muse exited with ${code ?? signal}: ${stderr.trim()}`)));
234
+ client.on("serverRequest", (serverRequest) => client.reject(serverRequest.id, -32601, "Headless agents do not accept server requests"));
235
+ try {
236
+ await initializeMuse(client, "replicas_headless_agent");
237
+ const sessionId = await startMuseSession(client, { approvalMode: "denyUnmatched", modelId: request.model, workspaceRoot: request.workingDirectory });
238
+ let message = null;
239
+ let usage = null;
240
+ const completed = new Promise((resolve, reject) => {
241
+ client.on("dispose", reject);
242
+ client.on("notification", (frame) => {
243
+ if (!isRecord(frame) || !isRecord(frame.params) || frame.params.sessionId !== sessionId) return;
244
+ const { method, params } = frame;
245
+ if (method === "item/completed" && isRecord(params.item) && params.item.kind === "agentMessage" && typeof params.item.text === "string") {
246
+ message = params.item.text;
247
+ } else if (method === "session/tokenUsage" && isRecord(params.cumulative) && typeof params.cumulative.promptTokens === "number" && typeof params.cumulative.outputTokens === "number") {
248
+ usage = { inputTokens: params.cumulative.promptTokens, outputTokens: params.cumulative.outputTokens };
249
+ } else if (method === "turn/completed") {
250
+ if (isRecord(params.error)) reject(new Error(typeof params.error.message === "string" ? params.error.message : "Muse turn failed"));
251
+ else resolve();
252
+ }
253
+ });
254
+ });
255
+ await Promise.all([
256
+ client.request("turn/start", museTurnStartParams(sessionId, [{ type: "text", text: jsonReplyPrompt(request) }], request.thinkingLevel)),
257
+ withTimeout(completed, request.timeoutSeconds * 1e3, "Muse turn timed out")
258
+ ]);
259
+ if (!message) throw new Error("Muse returned no final message");
260
+ return { output: parseJsonReply(message), usage };
261
+ } finally {
262
+ client.dispose();
263
+ child.kill("SIGTERM");
264
+ }
265
+ }
139
266
  async function main() {
140
267
  const [requestPath, outputPath] = process.argv.slice(2);
141
268
  if (!requestPath || !outputPath) throw new Error("Usage: replicas-headless-agent <request.json> <output.json>");
@@ -148,13 +275,26 @@ async function main() {
148
275
  case AGENT.CODEX:
149
276
  result = await runCodex(request);
150
277
  break;
151
- default:
152
- throw new Error(`Unsupported headless agent: ${request.agent}`);
278
+ case AGENT.OPENCODE:
279
+ result = await runOpencode(request);
280
+ break;
281
+ case AGENT.CURSOR:
282
+ result = await runCursor(request);
283
+ break;
284
+ case AGENT.MUSE:
285
+ result = await runMuse(request);
286
+ break;
287
+ case AGENT.PI:
288
+ throw new Error("Send Pi credentials as an Opencode request, which reads the same stored secrets");
289
+ default: {
290
+ const unsupported = request.agent;
291
+ throw new Error(`Unsupported headless agent: ${String(unsupported)}`);
292
+ }
153
293
  }
154
- await writeFile(outputPath, JSON.stringify(result), { mode: 384 });
294
+ await writeFile(outputPath, JSON.stringify(result.output), { mode: 384 });
295
+ if (result.usage) await writeFile(headlessAgentUsagePath(outputPath), JSON.stringify(result.usage), { mode: 384 });
155
296
  }
156
- main().catch((error) => {
157
- process.exitCode = 1;
297
+ main().then(() => process.exit(0), (error) => {
158
298
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
159
- `);
299
+ `, () => process.exit(1));
160
300
  });