@yuandc/aica 0.1.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.
Files changed (49) hide show
  1. package/README.md +9 -0
  2. package/dist/acp/agent.js +54 -0
  3. package/dist/acp/client/acp-client.js +102 -0
  4. package/dist/acp/client/acp-content.js +13 -0
  5. package/dist/acp/client/acp-events.js +106 -0
  6. package/dist/acp/client/acp-process.js +34 -0
  7. package/dist/acp/client/acp-runtime-pool.js +248 -0
  8. package/dist/acp/client/context-usage.js +29 -0
  9. package/dist/acp/client/json-rpc.js +128 -0
  10. package/dist/acp/provider-types.js +1 -0
  11. package/dist/acp/providers/codex/codex-process.js +51 -0
  12. package/dist/acp/providers/codex/events.js +1473 -0
  13. package/dist/acp/providers/codex/permissions.js +49 -0
  14. package/dist/acp/providers/codex/provider.js +376 -0
  15. package/dist/acp/providers/codex-acp/adapter.js +947 -0
  16. package/dist/acp/providers/codex-acp/context-maintenance.js +148 -0
  17. package/dist/acp/providers/codex-acp/launch.js +35 -0
  18. package/dist/acp/providers/codex-acp/provider.js +486 -0
  19. package/dist/acp/providers/mimo/provider.js +448 -0
  20. package/dist/acp/providers/opencode/provider.js +489 -0
  21. package/dist/acp/providers/registry.js +23 -0
  22. package/dist/acp/standard-events.js +167 -0
  23. package/dist/commands/start.js +137 -0
  24. package/dist/commands/worker-auth.js +100 -0
  25. package/dist/commands/worker-project.js +57 -0
  26. package/dist/core/aca-config.js +74 -0
  27. package/dist/core/aca-server-client.js +57 -0
  28. package/dist/core/acp-event-coalescer.js +108 -0
  29. package/dist/core/acp-event-upload-filter.js +16 -0
  30. package/dist/core/acp-orphan-cleanup.js +91 -0
  31. package/dist/core/affected-files.js +268 -0
  32. package/dist/core/auth.js +36 -0
  33. package/dist/core/file-transfer-worker.js +169 -0
  34. package/dist/core/fs.js +28 -0
  35. package/dist/core/heartbeat.js +578 -0
  36. package/dist/core/job-permission-policy.js +42 -0
  37. package/dist/core/job-worker.js +749 -0
  38. package/dist/core/logger.js +42 -0
  39. package/dist/core/long-poll-worker.js +26 -0
  40. package/dist/core/machine-filesystem-worker.js +352 -0
  41. package/dist/core/paths.js +26 -0
  42. package/dist/core/process-identity.js +34 -0
  43. package/dist/core/process.js +33 -0
  44. package/dist/core/provider-health.js +54 -0
  45. package/dist/core/runtime-options.js +38 -0
  46. package/dist/core/worktree.js +95 -0
  47. package/dist/worker-cli.js +27 -0
  48. package/dist/worker-single-cli.js +17 -0
  49. package/package.json +35 -0
@@ -0,0 +1,49 @@
1
+ export async function resolveCodexApprovalRequest(method, params, callback) {
2
+ const options = approvalOptionsForMethod(method);
3
+ if (callback) {
4
+ const response = await callback({
5
+ requestId: `perm-${Date.now()}-${Math.random().toString(16).slice(2)}`,
6
+ params,
7
+ options
8
+ });
9
+ return codexApprovalResponse(method, response.outcome.outcome === "selected");
10
+ }
11
+ return codexApprovalResponse(method, false);
12
+ }
13
+ export function isCodexApprovalRequest(method) {
14
+ return method === "item/commandExecution/requestApproval"
15
+ || method === "item/fileChange/requestApproval"
16
+ || method === "item/permissions/requestApproval"
17
+ || method === "execCommandApproval"
18
+ || method === "applyPatchApproval";
19
+ }
20
+ function approvalOptionsForMethod(method) {
21
+ if (method === "item/permissions/requestApproval") {
22
+ return [
23
+ { optionId: "allow", kind: "allow", label: "允许" },
24
+ { optionId: "deny", kind: "reject", label: "拒绝" }
25
+ ];
26
+ }
27
+ return [
28
+ { optionId: "accept", kind: "allow", label: "允许" },
29
+ { optionId: "decline", kind: "reject", label: "拒绝" }
30
+ ];
31
+ }
32
+ function codexApprovalResponse(method, allowed) {
33
+ switch (method) {
34
+ case "item/commandExecution/requestApproval":
35
+ return { decision: allowed ? "accept" : "decline" };
36
+ case "item/fileChange/requestApproval":
37
+ return { decision: allowed ? "accept" : "decline" };
38
+ case "item/permissions/requestApproval":
39
+ return allowed
40
+ ? { permissions: {}, scope: "turn" }
41
+ : { permissions: {}, scope: "turn", strictAutoReview: true };
42
+ case "execCommandApproval":
43
+ return { decision: allowed ? "approved" : "denied" };
44
+ case "applyPatchApproval":
45
+ return { decision: allowed ? "approved" : "denied" };
46
+ default:
47
+ return {};
48
+ }
49
+ }
@@ -0,0 +1,376 @@
1
+ import fs from "node:fs";
2
+ import { JsonLineRpcClient } from "../../client/json-rpc.js";
3
+ import { startCodexAppServer } from "./codex-process.js";
4
+ import { extractAssistantTextFromTurn, mapCodexNotification } from "./events.js";
5
+ import { isCodexApprovalRequest, resolveCodexApprovalRequest } from "./permissions.js";
6
+ const CANCEL_INTERRUPT_TIMEOUT_MS = 1_500;
7
+ export const nativeCodexCapabilities = {
8
+ sessionResume: true,
9
+ imageInput: true,
10
+ fileAttachment: true,
11
+ filesystem: true,
12
+ terminal: true,
13
+ permissionRequest: true,
14
+ configOptions: true,
15
+ usage: true,
16
+ contextUsage: true,
17
+ contextCompaction: true,
18
+ plan: true,
19
+ diff: true
20
+ };
21
+ export class NativeCodexProvider {
22
+ id = "native-codex";
23
+ name = "Native Codex";
24
+ capabilities = nativeCodexCapabilities;
25
+ async createSession(input) {
26
+ return new NativeCodexSession(input.cwd, input.providerSessionId ?? null, input.timeoutMs);
27
+ }
28
+ }
29
+ class NativeCodexSession {
30
+ cwd;
31
+ providerSessionId;
32
+ defaultTimeoutMs;
33
+ started;
34
+ rpc;
35
+ initialized = false;
36
+ content = "";
37
+ promptActive = false;
38
+ updates = [];
39
+ statusTimeline = [];
40
+ statusCallback = null;
41
+ updateCallback = null;
42
+ permissionCallback = null;
43
+ abortHandler = null;
44
+ completedTurns = new Map();
45
+ pendingTurnCompletions = new Map();
46
+ agentMessagePhases = new Map();
47
+ constructor(cwd, providerSessionId, defaultTimeoutMs = readAcpPromptTimeoutMs()) {
48
+ this.cwd = cwd;
49
+ this.providerSessionId = providerSessionId;
50
+ this.defaultTimeoutMs = defaultTimeoutMs;
51
+ this.started = startCodexAppServer(cwd);
52
+ this.rpc = new JsonLineRpcClient(this.started.child, {
53
+ onNotification: (method, params) => this.handleNotification(method, params),
54
+ onRequest: (method, params) => this.handleReverseRequest(method, params)
55
+ });
56
+ }
57
+ async sendPrompt(input) {
58
+ const startedAt = Date.now();
59
+ const effectiveConfig = codexAccessConfigFromMode(input.mode);
60
+ this.statusCallback = input.onStatus ?? null;
61
+ this.updateCallback = input.onUpdate ?? null;
62
+ this.permissionCallback = input.onPermissionRequest ?? null;
63
+ this.attachAbortSignal(input.signal);
64
+ this.emitStatus("initializing", "Codex 初始化中", "initialize");
65
+ await this.initialize();
66
+ const threadId = await this.establishThread(input);
67
+ this.emitStatus("thinking", "思考中");
68
+ this.content = "";
69
+ this.updates.length = 0;
70
+ this.agentMessagePhases.clear();
71
+ this.promptActive = true;
72
+ let promptResponse;
73
+ let turn;
74
+ try {
75
+ promptResponse = await this.rpc.request("turn/start", {
76
+ threadId,
77
+ clientUserMessageId: `aca-${Date.now()}-${Math.random().toString(16).slice(2)}`,
78
+ input: promptBlocksToCodexInput(input.prompt, input.promptBlocks),
79
+ cwd: input.cwd,
80
+ ...(input.model ? { model: input.model } : {}),
81
+ ...turnConfigFromOptions(input.configOptionValues)
82
+ }, input.timeoutMs ?? this.defaultTimeoutMs);
83
+ const turnId = String(promptResponse?.turn?.id ?? "");
84
+ turn = turnId
85
+ ? await this.waitForTurnCompleted(turnId, input.timeoutMs ?? this.defaultTimeoutMs)
86
+ : promptResponse?.turn;
87
+ }
88
+ finally {
89
+ this.promptActive = false;
90
+ }
91
+ const finalContent = this.content.trim() || extractAssistantTextFromTurn(turn);
92
+ this.emitStatus("completed", "已完成");
93
+ return {
94
+ providerSessionId: threadId,
95
+ cliType: "builtin",
96
+ agentType: "codex",
97
+ effectiveConfig,
98
+ command: this.started.command,
99
+ args: this.started.args,
100
+ cwd: this.cwd,
101
+ exitCode: this.rpc.exitCode,
102
+ signal: this.rpc.signal,
103
+ durationMs: Date.now() - startedAt,
104
+ stdout: this.rpc.stdout,
105
+ stderr: this.rpc.stderr,
106
+ content: finalContent,
107
+ updates: this.updates,
108
+ statusTimeline: this.statusTimeline,
109
+ promptResponse
110
+ };
111
+ }
112
+ async cancel() {
113
+ try {
114
+ if (this.providerSessionId && this.started.child.exitCode === null && this.started.child.signalCode === null) {
115
+ await this.rpc.request("turn/interrupt", { threadId: this.providerSessionId }, CANCEL_INTERRUPT_TIMEOUT_MS).catch(() => void 0);
116
+ }
117
+ }
118
+ finally {
119
+ await this.close();
120
+ }
121
+ }
122
+ async close() {
123
+ this.statusCallback = null;
124
+ this.updateCallback = null;
125
+ this.permissionCallback = null;
126
+ for (const pending of this.pendingTurnCompletions.values()) {
127
+ clearTimeout(pending.timer);
128
+ pending.reject(new Error("Codex app-server session closed"));
129
+ }
130
+ this.pendingTurnCompletions.clear();
131
+ if (this.abortHandler)
132
+ this.abortHandler = null;
133
+ await this.closeCodexProcess();
134
+ }
135
+ async closeCodexProcess() {
136
+ if (this.started.child.exitCode !== null || this.started.child.signalCode !== null)
137
+ return;
138
+ this.started.child.stdin.end();
139
+ if (await this.waitForProcessClose(2_000))
140
+ return;
141
+ this.started.child.kill("SIGTERM");
142
+ if (await this.waitForProcessClose(1_000))
143
+ return;
144
+ this.started.child.kill("SIGKILL");
145
+ await this.waitForProcessClose(1_000);
146
+ }
147
+ waitForProcessClose(timeoutMs) {
148
+ if (this.started.child.exitCode !== null || this.started.child.signalCode !== null)
149
+ return Promise.resolve(true);
150
+ return new Promise((resolve) => {
151
+ const timer = setTimeout(() => resolve(false), timeoutMs);
152
+ timer.unref();
153
+ this.started.child.once("close", () => {
154
+ clearTimeout(timer);
155
+ resolve(true);
156
+ });
157
+ });
158
+ }
159
+ async initialize() {
160
+ if (this.initialized)
161
+ return;
162
+ await this.rpc.request("initialize", {
163
+ clientInfo: { name: "aca", version: "0.1.0" },
164
+ capabilities: {
165
+ experimentalApi: true,
166
+ requestAttestation: false
167
+ }
168
+ });
169
+ this.initialized = true;
170
+ }
171
+ async establishThread(input) {
172
+ const accessConfig = codexAccessConfigFromMode(input.mode);
173
+ if (this.providerSessionId) {
174
+ try {
175
+ this.emitStatus("resuming", "Codex 会话恢复中", "thread/resume");
176
+ const resumed = await this.rpc.request("thread/resume", {
177
+ threadId: this.providerSessionId,
178
+ cwd: input.cwd,
179
+ ...(input.model ? { model: input.model } : {}),
180
+ approvalsReviewer: "user",
181
+ ...accessConfig
182
+ }, 60_000);
183
+ this.providerSessionId = String(resumed?.thread?.id ?? this.providerSessionId);
184
+ return this.providerSessionId;
185
+ }
186
+ catch {
187
+ this.providerSessionId = null;
188
+ }
189
+ }
190
+ this.emitStatus("acp", "Codex 启动中", "thread/start");
191
+ const created = await this.rpc.request("thread/start", {
192
+ cwd: input.cwd,
193
+ ...(input.model ? { model: input.model } : {}),
194
+ approvalsReviewer: "user",
195
+ ...accessConfig,
196
+ ephemeral: false,
197
+ threadSource: "aca"
198
+ }, 60_000);
199
+ const threadId = created?.thread?.id;
200
+ if (!threadId)
201
+ throw new Error("Codex app-server thread/start did not return thread id");
202
+ this.providerSessionId = threadId;
203
+ return threadId;
204
+ }
205
+ attachAbortSignal(signal) {
206
+ if (!signal)
207
+ return;
208
+ this.abortHandler = () => {
209
+ void this.cancel();
210
+ };
211
+ if (signal.aborted)
212
+ throw new Error("ACP prompt cancelled");
213
+ signal.addEventListener("abort", this.abortHandler, { once: true });
214
+ }
215
+ handleNotification(method, params) {
216
+ if (!this.promptActive && !shouldHandleIdleCodexNotification(method))
217
+ return;
218
+ this.rpc.refreshPendingRequestTimeout("turn/start");
219
+ this.updates.push({ method, params });
220
+ this.captureAgentMessagePhase(method, params);
221
+ this.appendFinalAnswerDelta(method, params);
222
+ if (method === "turn/completed") {
223
+ this.resolveTurnCompleted(params);
224
+ }
225
+ mapCodexNotification(method, params, {
226
+ pushUpdate: (event) => this.updateCallback?.(event),
227
+ pushStatus: (phase, label, detail, updateType) => this.emitStatus(phase, label, detail, updateType)
228
+ }, { agentMessagePhases: this.agentMessagePhases });
229
+ }
230
+ captureAgentMessagePhase(method, params) {
231
+ if (method !== "item/started" && method !== "item/completed")
232
+ return;
233
+ const item = params?.item;
234
+ if (item?.type !== "agentMessage" || !item.id)
235
+ return;
236
+ this.agentMessagePhases.set(item.id, item.phase ?? "");
237
+ }
238
+ appendFinalAnswerDelta(method, params) {
239
+ if (method !== "item/agentMessage/delta")
240
+ return;
241
+ const deltaParams = params;
242
+ if (!deltaParams.itemId || typeof deltaParams.delta !== "string")
243
+ return;
244
+ const phase = this.agentMessagePhases.get(deltaParams.itemId);
245
+ if (phase === "final_answer" || !phase) {
246
+ this.content += deltaParams.delta;
247
+ }
248
+ }
249
+ waitForTurnCompleted(turnId, timeoutMs) {
250
+ const completed = this.completedTurns.get(turnId);
251
+ if (completed)
252
+ return Promise.resolve(completed);
253
+ return new Promise((resolve, reject) => {
254
+ const timer = setTimeout(() => {
255
+ this.pendingTurnCompletions.delete(turnId);
256
+ reject(new Error(`Codex app-server request timed out while waiting for turn/completed`));
257
+ }, timeoutMs);
258
+ timer.unref();
259
+ this.pendingTurnCompletions.set(turnId, { resolve, reject, timer });
260
+ });
261
+ }
262
+ resolveTurnCompleted(params) {
263
+ const turn = params?.turn;
264
+ const turnId = turn?.id;
265
+ if (!turnId)
266
+ return;
267
+ this.completedTurns.set(turnId, turn);
268
+ const pending = this.pendingTurnCompletions.get(turnId);
269
+ if (!pending)
270
+ return;
271
+ this.pendingTurnCompletions.delete(turnId);
272
+ clearTimeout(pending.timer);
273
+ if (turn.status === "failed") {
274
+ pending.reject(new Error(`Codex turn failed: ${JSON.stringify(turn.error ?? {})}`));
275
+ return;
276
+ }
277
+ pending.resolve(turn);
278
+ }
279
+ async handleReverseRequest(method, params) {
280
+ if (isCodexApprovalRequest(method)) {
281
+ this.emitStatus("requestPermission", "等待授权", summarizeRequest(params), method);
282
+ return resolveCodexApprovalRequest(method, params, this.permissionCallback);
283
+ }
284
+ return {};
285
+ }
286
+ emitStatus(phase, label, detail, updateType) {
287
+ const status = {
288
+ phase,
289
+ label,
290
+ ...(detail ? { detail } : {}),
291
+ ...(updateType ? { updateType } : {}),
292
+ atMs: Date.now()
293
+ };
294
+ const last = this.statusTimeline[this.statusTimeline.length - 1];
295
+ if (last && last.phase === status.phase && last.label === status.label && last.detail === status.detail && last.updateType === status.updateType)
296
+ return;
297
+ this.statusTimeline.push(status);
298
+ this.statusCallback?.(status);
299
+ }
300
+ }
301
+ function promptBlocksToCodexInput(prompt, blocks) {
302
+ const inputBlocks = blocks?.length ? blocks : [{ type: "text", text: prompt }];
303
+ return inputBlocks.map((block) => {
304
+ if (block.type === "image") {
305
+ if (block.uri && fs.existsSync(block.uri))
306
+ return { type: "localImage", path: block.uri };
307
+ return { type: "image", url: `data:${block.mimeType};base64,${block.data}` };
308
+ }
309
+ return { type: "text", text: block.text, text_elements: [] };
310
+ });
311
+ }
312
+ function codexAccessConfigFromMode(mode) {
313
+ const normalized = String(mode || "").trim();
314
+ if (normalized === "read-only") {
315
+ return {
316
+ approvalPolicy: "on-request",
317
+ sandbox: "read-only"
318
+ };
319
+ }
320
+ if (normalized === "acceptEdits" || normalized === "accept-edits") {
321
+ return {
322
+ approvalPolicy: "on-failure",
323
+ sandbox: "workspace-write"
324
+ };
325
+ }
326
+ if (["agent-full-access", "full-access", "bypassPermissions", "danger-full-access"].includes(normalized)) {
327
+ return {
328
+ approvalPolicy: "never",
329
+ sandbox: "danger-full-access"
330
+ };
331
+ }
332
+ return {
333
+ approvalPolicy: "on-request",
334
+ sandbox: "workspace-write"
335
+ };
336
+ }
337
+ function turnConfigFromOptions(values) {
338
+ if (!values)
339
+ return {};
340
+ const result = {};
341
+ const effort = values.reasoningEffort ?? values["model_reasoning_effort"];
342
+ if (typeof effort === "string" && effort)
343
+ result.effort = effort;
344
+ return result;
345
+ }
346
+ function summarizeRequest(params) {
347
+ if (!params || typeof params !== "object" || Array.isArray(params))
348
+ return undefined;
349
+ const record = params;
350
+ for (const key of ["command", "cwd", "reason", "grantRoot"]) {
351
+ const value = record[key];
352
+ if (typeof value === "string" && value.trim())
353
+ return textPreview(value);
354
+ }
355
+ return undefined;
356
+ }
357
+ function textPreview(value) {
358
+ const normalized = value.replace(/\s+/g, " ").trim();
359
+ return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
360
+ }
361
+ function shouldHandleIdleCodexNotification(method) {
362
+ if (method.startsWith("thread/"))
363
+ return true;
364
+ return [
365
+ "account/rateLimits/updated",
366
+ "account/updated",
367
+ "configWarning",
368
+ "warning",
369
+ "model/rerouted",
370
+ "thread/compacted"
371
+ ].includes(method);
372
+ }
373
+ function readAcpPromptTimeoutMs() {
374
+ const parsed = Number.parseInt(process.env.ACA_ACP_PROMPT_TIMEOUT_MS ?? "", 10);
375
+ return Number.isInteger(parsed) && parsed >= 30_000 ? parsed : 6 * 60 * 60 * 1000;
376
+ }