@yuandc/aica 0.1.0 → 0.1.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/dist/acp/agent.js +1 -54
- package/dist/acp/client/acp-client.js +1 -102
- package/dist/acp/client/acp-content.js +1 -13
- package/dist/acp/client/acp-events.js +1 -106
- package/dist/acp/client/acp-process.js +1 -34
- package/dist/acp/client/acp-runtime-pool.js +1 -248
- package/dist/acp/client/context-usage.js +1 -29
- package/dist/acp/client/json-rpc.js +4 -128
- package/dist/acp/provider-types.js +0 -1
- package/dist/acp/providers/codex/codex-process.js +1 -51
- package/dist/acp/providers/codex/events.js +28 -1473
- package/dist/acp/providers/codex/permissions.js +1 -49
- package/dist/acp/providers/codex/provider.js +1 -376
- package/dist/acp/providers/codex-acp/adapter.js +5 -947
- package/dist/acp/providers/codex-acp/context-maintenance.js +5 -148
- package/dist/acp/providers/codex-acp/launch.js +1 -35
- package/dist/acp/providers/codex-acp/provider.js +1 -486
- package/dist/acp/providers/mimo/provider.js +5 -448
- package/dist/acp/providers/opencode/provider.js +4 -489
- package/dist/acp/providers/registry.js +1 -23
- package/dist/acp/standard-events.js +1 -167
- package/dist/commands/start.js +1 -137
- package/dist/commands/worker-auth.js +4 -100
- package/dist/commands/worker-project.js +1 -57
- package/dist/core/aca-config.js +1 -74
- package/dist/core/aca-server-client.js +1 -57
- package/dist/core/acp-event-coalescer.js +1 -108
- package/dist/core/acp-event-upload-filter.js +1 -16
- package/dist/core/acp-orphan-cleanup.js +1 -91
- package/dist/core/affected-files.js +2 -268
- package/dist/core/auth.js +1 -36
- package/dist/core/file-transfer-worker.js +1 -169
- package/dist/core/fs.js +2 -28
- package/dist/core/heartbeat.js +3 -578
- package/dist/core/job-permission-policy.js +1 -42
- package/dist/core/job-worker.js +6 -749
- package/dist/core/logger.js +3 -42
- package/dist/core/long-poll-worker.js +1 -26
- package/dist/core/machine-filesystem-worker.js +3 -352
- package/dist/core/paths.js +1 -26
- package/dist/core/process-identity.js +1 -34
- package/dist/core/process.js +2 -33
- package/dist/core/provider-health.js +1 -54
- package/dist/core/runtime-options.js +1 -38
- package/dist/core/worktree.js +1 -95
- package/dist/worker-cli.js +1 -26
- package/dist/worker-single-cli.js +1 -16
- package/package.json +1 -1
|
@@ -1,486 +1 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { promptBlocksToAcpContent } from "../../client/acp-content.js";
|
|
3
|
-
import { eventFromAcpSessionUpdate, statusFromAcpSessionUpdate } from "../../client/acp-events.js";
|
|
4
|
-
import { defaultAcpRuntimePool } from "../../client/acp-runtime-pool.js";
|
|
5
|
-
import { resolveCodexAcpLaunch } from "./launch.js";
|
|
6
|
-
import { CodexPromptPerformanceTracker, readCodexContextMaintenancePolicy } from "./context-maintenance.js";
|
|
7
|
-
export const codexAcpCapabilities = {
|
|
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 CodexAcpProvider {
|
|
22
|
-
id = "codex-acp";
|
|
23
|
-
name = "Codex ACP";
|
|
24
|
-
capabilities = codexAcpCapabilities;
|
|
25
|
-
async createSession(input) {
|
|
26
|
-
return new CodexAcpSession(input.cwd, input.providerSessionId ?? null, input.timeoutMs);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
class CodexAcpSession {
|
|
30
|
-
cwd;
|
|
31
|
-
providerSessionId;
|
|
32
|
-
defaultTimeoutMs;
|
|
33
|
-
lease = null;
|
|
34
|
-
sessionResponse = null;
|
|
35
|
-
statusCallback = null;
|
|
36
|
-
updateCallback = null;
|
|
37
|
-
permissionCallback = null;
|
|
38
|
-
userInputCallback = null;
|
|
39
|
-
abortHandler = null;
|
|
40
|
-
statusTimeline = [];
|
|
41
|
-
updates = [];
|
|
42
|
-
content = "";
|
|
43
|
-
performanceTracker = null;
|
|
44
|
-
constructor(cwd, providerSessionId, defaultTimeoutMs = readCodexAcpPromptTimeoutMs()) {
|
|
45
|
-
this.cwd = cwd;
|
|
46
|
-
this.providerSessionId = providerSessionId;
|
|
47
|
-
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
48
|
-
}
|
|
49
|
-
async sendPrompt(input) {
|
|
50
|
-
if (!fs.existsSync(input.cwd) || !fs.statSync(input.cwd).isDirectory()) {
|
|
51
|
-
throw new Error(`Project root does not exist or is not a directory: ${input.cwd}`);
|
|
52
|
-
}
|
|
53
|
-
const startedAt = Date.now();
|
|
54
|
-
this.statusCallback = input.onStatus ?? null;
|
|
55
|
-
this.updateCallback = input.onUpdate ?? null;
|
|
56
|
-
this.permissionCallback = input.onPermissionRequest ?? null;
|
|
57
|
-
this.userInputCallback = input.onUserInputRequest ?? null;
|
|
58
|
-
this.attachAbortSignal(input.signal);
|
|
59
|
-
this.content = "";
|
|
60
|
-
this.updates.length = 0;
|
|
61
|
-
this.statusTimeline.length = 0;
|
|
62
|
-
this.performanceTracker = new CodexPromptPerformanceTracker(startedAt);
|
|
63
|
-
let client = null;
|
|
64
|
-
let started = null;
|
|
65
|
-
let sessionId = this.providerSessionId;
|
|
66
|
-
let promptResponse;
|
|
67
|
-
let slowStatusTimer = null;
|
|
68
|
-
try {
|
|
69
|
-
this.emitStatus("initializing", "Codex ACP 启动中", "codex-acp");
|
|
70
|
-
const lease = await this.acquireRuntime(input.cwd);
|
|
71
|
-
this.lease = lease;
|
|
72
|
-
client = lease.client;
|
|
73
|
-
started = lease.started;
|
|
74
|
-
await this.initialize(lease);
|
|
75
|
-
sessionId = await this.establishSession(lease, input);
|
|
76
|
-
await this.applyConfig(client, sessionId, input);
|
|
77
|
-
this.emitStatus("thinking", "思考中");
|
|
78
|
-
slowStatusTimer = this.startSlowStatusTimer();
|
|
79
|
-
promptResponse = await this.promptWithAbort(client, {
|
|
80
|
-
sessionId,
|
|
81
|
-
prompt: promptBlocksToAcpContent(input.prompt, input.promptBlocks),
|
|
82
|
-
timeoutMs: input.timeoutMs ?? this.defaultTimeoutMs,
|
|
83
|
-
signal: input.signal
|
|
84
|
-
});
|
|
85
|
-
const nextSessionId = sessionIdFromPromptResponse(promptResponse);
|
|
86
|
-
if (nextSessionId && nextSessionId !== sessionId) {
|
|
87
|
-
sessionId = nextSessionId;
|
|
88
|
-
this.providerSessionId = nextSessionId;
|
|
89
|
-
lease.setProviderSession({ providerSessionId: nextSessionId, sessionResponse: this.sessionResponse });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
if (slowStatusTimer)
|
|
94
|
-
clearInterval(slowStatusTimer);
|
|
95
|
-
if (this.abortHandler && input.signal) {
|
|
96
|
-
input.signal.removeEventListener("abort", this.abortHandler);
|
|
97
|
-
this.abortHandler = null;
|
|
98
|
-
}
|
|
99
|
-
this.releaseLease();
|
|
100
|
-
}
|
|
101
|
-
this.emitStatus("completed", "已完成");
|
|
102
|
-
return {
|
|
103
|
-
providerSessionId: sessionId,
|
|
104
|
-
cliType: "builtin",
|
|
105
|
-
agentType: "codex",
|
|
106
|
-
effectiveConfig: {
|
|
107
|
-
model: input.model ?? null,
|
|
108
|
-
mode: input.mode ?? null,
|
|
109
|
-
configOptionValues: input.configOptionValues ?? null,
|
|
110
|
-
command: started.command,
|
|
111
|
-
processPid: started.child.pid ?? null,
|
|
112
|
-
provider: "codex-acp",
|
|
113
|
-
protocol: "acp"
|
|
114
|
-
},
|
|
115
|
-
command: started.command,
|
|
116
|
-
args: started.args,
|
|
117
|
-
cwd: input.cwd,
|
|
118
|
-
exitCode: started.child.exitCode,
|
|
119
|
-
signal: started.child.signalCode,
|
|
120
|
-
durationMs: Date.now() - startedAt,
|
|
121
|
-
stdout: client.rpc.stdout,
|
|
122
|
-
stderr: client.rpc.stderr,
|
|
123
|
-
content: this.content.trim(),
|
|
124
|
-
updates: [...this.updates],
|
|
125
|
-
statusTimeline: [...this.statusTimeline],
|
|
126
|
-
performance: this.performanceTracker?.snapshot(),
|
|
127
|
-
contextUsage: contextUsageFromPromptResponse(promptResponse),
|
|
128
|
-
promptResponse
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async compactContext(input) {
|
|
132
|
-
if (!fs.existsSync(input.cwd) || !fs.statSync(input.cwd).isDirectory()) {
|
|
133
|
-
throw new Error(`Project root does not exist or is not a directory: ${input.cwd}`);
|
|
134
|
-
}
|
|
135
|
-
const startedAt = Date.now();
|
|
136
|
-
this.statusCallback = input.onStatus ?? null;
|
|
137
|
-
this.updateCallback = input.onUpdate ?? null;
|
|
138
|
-
this.attachAbortSignal(input.signal);
|
|
139
|
-
let sessionId = this.providerSessionId;
|
|
140
|
-
let response;
|
|
141
|
-
try {
|
|
142
|
-
this.emitStatus("initializing", "Codex ACP 启动中", "codex-acp");
|
|
143
|
-
const lease = await this.acquireRuntime(input.cwd);
|
|
144
|
-
this.lease = lease;
|
|
145
|
-
await this.initialize(lease);
|
|
146
|
-
sessionId = await this.establishSession(lease, input);
|
|
147
|
-
await this.applyConfig(lease.client, sessionId, input);
|
|
148
|
-
response = await lease.client.compactSession({
|
|
149
|
-
sessionId,
|
|
150
|
-
allowRollover: input.allowRollover === true
|
|
151
|
-
}, input.timeoutMs ?? 10 * 60 * 1000);
|
|
152
|
-
const nextSessionId = sessionIdFromPromptResponse(response);
|
|
153
|
-
if (nextSessionId && nextSessionId !== sessionId) {
|
|
154
|
-
sessionId = nextSessionId;
|
|
155
|
-
this.providerSessionId = nextSessionId;
|
|
156
|
-
lease.setProviderSession({ providerSessionId: nextSessionId, sessionResponse: this.sessionResponse });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
if (this.lease && shouldDisposeRuntimeAfterCompactError(error, input.signal)) {
|
|
161
|
-
const lease = this.lease;
|
|
162
|
-
this.lease = null;
|
|
163
|
-
await lease.dispose().catch(() => void 0);
|
|
164
|
-
}
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
if (this.abortHandler && input.signal) {
|
|
169
|
-
input.signal.removeEventListener("abort", this.abortHandler);
|
|
170
|
-
this.abortHandler = null;
|
|
171
|
-
}
|
|
172
|
-
this.releaseLease();
|
|
173
|
-
}
|
|
174
|
-
const record = objectOrNull(response) ?? {};
|
|
175
|
-
return {
|
|
176
|
-
providerSessionId: sessionId || this.providerSessionId || "",
|
|
177
|
-
contextUsage: contextUsageFromRecord(record.contextUsage),
|
|
178
|
-
maintenance: objectOrNull(record.maintenance) ?? {},
|
|
179
|
-
durationMs: Date.now() - startedAt
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
async cancel() {
|
|
183
|
-
if (this.providerSessionId)
|
|
184
|
-
this.lease?.client.cancel(this.providerSessionId);
|
|
185
|
-
}
|
|
186
|
-
async close() {
|
|
187
|
-
this.statusCallback = null;
|
|
188
|
-
this.updateCallback = null;
|
|
189
|
-
this.permissionCallback = null;
|
|
190
|
-
this.userInputCallback = null;
|
|
191
|
-
if (this.abortHandler)
|
|
192
|
-
this.abortHandler = null;
|
|
193
|
-
this.releaseLease();
|
|
194
|
-
}
|
|
195
|
-
async acquireRuntime(cwd) {
|
|
196
|
-
const launch = resolveCodexAcpLaunch();
|
|
197
|
-
return defaultAcpRuntimePool.acquire({
|
|
198
|
-
providerId: "codex-acp",
|
|
199
|
-
cwd,
|
|
200
|
-
providerSessionId: this.providerSessionId,
|
|
201
|
-
command: launch.command,
|
|
202
|
-
args: launch.args,
|
|
203
|
-
onSessionUpdate: (params) => this.handleSessionUpdate(params),
|
|
204
|
-
onClientRequest: async (method, params) => {
|
|
205
|
-
if (method === "session/request_permission")
|
|
206
|
-
return this.requestPermission(params);
|
|
207
|
-
if (method === "elicitation/create" || method === "session/request_user_input")
|
|
208
|
-
return this.requestUserInput(params);
|
|
209
|
-
throw new Error(`Unsupported Codex ACP client request: ${method}`);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
async initialize(lease) {
|
|
214
|
-
if (lease.initialized)
|
|
215
|
-
return;
|
|
216
|
-
await lease.client.initialize(60_000);
|
|
217
|
-
lease.markInitialized();
|
|
218
|
-
}
|
|
219
|
-
async establishSession(lease, input) {
|
|
220
|
-
const client = lease.client;
|
|
221
|
-
const initialConfig = initialCodexConfig(input);
|
|
222
|
-
if (lease.providerSessionId && lease.sessionEstablished) {
|
|
223
|
-
this.providerSessionId = lease.providerSessionId;
|
|
224
|
-
this.sessionResponse = lease.sessionResponse;
|
|
225
|
-
return lease.providerSessionId;
|
|
226
|
-
}
|
|
227
|
-
if (this.providerSessionId) {
|
|
228
|
-
try {
|
|
229
|
-
this.emitStatus("resuming", "Codex ACP 会话恢复中", "session/load");
|
|
230
|
-
const loaded = await client.loadSession({ sessionId: this.providerSessionId, cwd: input.cwd, config: initialConfig }, 60_000);
|
|
231
|
-
this.sessionResponse = objectOrNull(loaded);
|
|
232
|
-
const sessionId = sessionIdFromResponse(loaded) || this.providerSessionId;
|
|
233
|
-
this.providerSessionId = sessionId;
|
|
234
|
-
lease.setProviderSession({ providerSessionId: sessionId, sessionResponse: this.sessionResponse });
|
|
235
|
-
return sessionId;
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
this.providerSessionId = null;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
this.emitStatus("acp", "Codex ACP 会话创建中", "session/new");
|
|
242
|
-
const created = await client.newSession({ cwd: input.cwd, config: initialConfig }, 120_000);
|
|
243
|
-
this.sessionResponse = objectOrNull(created);
|
|
244
|
-
const sessionId = sessionIdFromResponse(created);
|
|
245
|
-
if (!sessionId)
|
|
246
|
-
throw new Error("Codex ACP session/new did not return sessionId");
|
|
247
|
-
this.providerSessionId = sessionId;
|
|
248
|
-
lease.setProviderSession({ providerSessionId: sessionId, sessionResponse: this.sessionResponse });
|
|
249
|
-
return sessionId;
|
|
250
|
-
}
|
|
251
|
-
async applyConfig(client, sessionId, input) {
|
|
252
|
-
if (input.model) {
|
|
253
|
-
await client.setSessionConfigOption({ sessionId, configId: "model", value: input.model }, 60_000).catch(() => void 0);
|
|
254
|
-
}
|
|
255
|
-
const approvalPolicy = approvalPolicyFromMode(input.mode);
|
|
256
|
-
if (approvalPolicy) {
|
|
257
|
-
await client.setSessionConfigOption({ sessionId, configId: "approvalPolicy", value: approvalPolicy }, 60_000).catch(() => void 0);
|
|
258
|
-
}
|
|
259
|
-
const mode = modeToCodexSandbox(input.mode);
|
|
260
|
-
if (mode) {
|
|
261
|
-
await client.setSessionConfigOption({ sessionId, configId: "sandbox", value: mode }, 60_000).catch(() => void 0);
|
|
262
|
-
}
|
|
263
|
-
for (const [configId, value] of Object.entries(input.configOptionValues ?? {})) {
|
|
264
|
-
if (["model", "mode", "sandbox", "approvalPolicy"].includes(configId))
|
|
265
|
-
continue;
|
|
266
|
-
await client.setSessionConfigOption({ sessionId, configId, value }, 60_000).catch(() => void 0);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
handleSessionUpdate(params) {
|
|
270
|
-
this.performanceTracker?.recordEvent();
|
|
271
|
-
this.updates.push({ method: "session/update", params });
|
|
272
|
-
const event = eventFromAcpSessionUpdate(params);
|
|
273
|
-
if (event) {
|
|
274
|
-
if (event.type === "agent_message_chunk")
|
|
275
|
-
this.content += event.text ?? "";
|
|
276
|
-
this.updateCallback?.(event);
|
|
277
|
-
}
|
|
278
|
-
const status = statusFromAcpSessionUpdate(params);
|
|
279
|
-
if (status)
|
|
280
|
-
this.emitStatus(status.phase, status.label, status.detail, status.updateType);
|
|
281
|
-
}
|
|
282
|
-
startSlowStatusTimer() {
|
|
283
|
-
const thresholdMs = readCodexContextMaintenancePolicy().slowEventThresholdMs;
|
|
284
|
-
const timer = setInterval(() => {
|
|
285
|
-
const tracker = this.performanceTracker;
|
|
286
|
-
const now = Date.now();
|
|
287
|
-
if (!tracker?.shouldReportSlow(now, thresholdMs))
|
|
288
|
-
return;
|
|
289
|
-
const snapshot = tracker.snapshot(now);
|
|
290
|
-
const minutes = Math.max(1, Math.round(snapshot.silentForMs / 60_000));
|
|
291
|
-
this.emitStatus("thinking", "模型长时间处理中", `已 ${minutes} 分钟没有收到新的 ACP 事件`, "aca.performance.slow");
|
|
292
|
-
}, Math.min(30_000, Math.max(1_000, Math.floor(thresholdMs / 4))));
|
|
293
|
-
timer.unref();
|
|
294
|
-
return timer;
|
|
295
|
-
}
|
|
296
|
-
attachAbortSignal(signal) {
|
|
297
|
-
if (!signal)
|
|
298
|
-
return;
|
|
299
|
-
this.abortHandler = () => {
|
|
300
|
-
void this.cancel();
|
|
301
|
-
};
|
|
302
|
-
if (signal.aborted)
|
|
303
|
-
throw new Error("Codex ACP prompt cancelled");
|
|
304
|
-
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
305
|
-
}
|
|
306
|
-
async promptWithAbort(client, input) {
|
|
307
|
-
const promptPromise = client.prompt({ sessionId: input.sessionId, prompt: input.prompt }, input.timeoutMs);
|
|
308
|
-
if (!input.signal)
|
|
309
|
-
return promptPromise;
|
|
310
|
-
if (input.signal.aborted) {
|
|
311
|
-
await this.lease?.dispose().catch(() => void 0);
|
|
312
|
-
throw new Error("Codex ACP prompt cancelled");
|
|
313
|
-
}
|
|
314
|
-
let abortHandler = null;
|
|
315
|
-
const abortPromise = new Promise((_, reject) => {
|
|
316
|
-
abortHandler = () => {
|
|
317
|
-
client.cancel(input.sessionId);
|
|
318
|
-
void this.lease?.dispose().catch(() => void 0);
|
|
319
|
-
reject(new Error("Codex ACP prompt cancelled"));
|
|
320
|
-
};
|
|
321
|
-
input.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
322
|
-
});
|
|
323
|
-
try {
|
|
324
|
-
return await Promise.race([promptPromise, abortPromise]);
|
|
325
|
-
}
|
|
326
|
-
finally {
|
|
327
|
-
if (abortHandler)
|
|
328
|
-
input.signal.removeEventListener("abort", abortHandler);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async requestPermission(params) {
|
|
332
|
-
if (!this.permissionCallback)
|
|
333
|
-
return { outcome: { outcome: "cancelled" } };
|
|
334
|
-
const record = params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
335
|
-
const request = record.request && typeof record.request === "object" && !Array.isArray(record.request) ? record.request : record;
|
|
336
|
-
const rawOptions = Array.isArray(request.options) ? request.options : [];
|
|
337
|
-
const options = rawOptions.map((option, index) => {
|
|
338
|
-
const item = option && typeof option === "object" && !Array.isArray(option) ? option : {};
|
|
339
|
-
return {
|
|
340
|
-
optionId: String(item.optionId ?? item.id ?? index),
|
|
341
|
-
kind: String(item.kind ?? "choice"),
|
|
342
|
-
...(typeof item.label === "string" ? { label: item.label } : {})
|
|
343
|
-
};
|
|
344
|
-
});
|
|
345
|
-
this.emitStatus("requestPermission", "等待授权", permissionRequestDetail(request), "session/request_permission");
|
|
346
|
-
return this.permissionCallback({
|
|
347
|
-
requestId: String(request.requestId ?? request.id ?? `codex-acp-permission-${Date.now()}`),
|
|
348
|
-
params,
|
|
349
|
-
options
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
async requestUserInput(params) {
|
|
353
|
-
if (!this.userInputCallback)
|
|
354
|
-
return { outcome: { outcome: "cancelled" } };
|
|
355
|
-
const record = params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
356
|
-
const request = record.request && typeof record.request === "object" && !Array.isArray(record.request) ? record.request : record;
|
|
357
|
-
this.emitStatus("requestPermission", "等待输入", userInputRequestDetail(request), "elicitation/create");
|
|
358
|
-
return this.userInputCallback({
|
|
359
|
-
requestId: String(request.requestId ?? request.id ?? `codex-acp-input-${Date.now()}`),
|
|
360
|
-
prompt: String(request.prompt ?? request.message ?? request.question ?? ""),
|
|
361
|
-
params,
|
|
362
|
-
...(typeof request.defaultValue === "string" ? { defaultValue: request.defaultValue } : {})
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
emitStatus(phase, label, detail, updateType) {
|
|
366
|
-
const status = {
|
|
367
|
-
phase,
|
|
368
|
-
label,
|
|
369
|
-
...(detail ? { detail } : {}),
|
|
370
|
-
...(updateType ? { updateType } : {}),
|
|
371
|
-
atMs: Date.now()
|
|
372
|
-
};
|
|
373
|
-
const last = this.statusTimeline[this.statusTimeline.length - 1];
|
|
374
|
-
if (last && last.phase === status.phase && last.label === status.label && last.detail === status.detail && last.updateType === status.updateType)
|
|
375
|
-
return;
|
|
376
|
-
this.statusTimeline.push(status);
|
|
377
|
-
this.statusCallback?.(status);
|
|
378
|
-
}
|
|
379
|
-
releaseLease() {
|
|
380
|
-
this.lease?.release();
|
|
381
|
-
this.lease = null;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function shouldDisposeRuntimeAfterCompactError(error, signal) {
|
|
385
|
-
if (signal?.aborted)
|
|
386
|
-
return true;
|
|
387
|
-
const message = error instanceof Error ? error.message : String(error || "");
|
|
388
|
-
return /compact.*timed out|request timed out.*compact|adapter closed|aborted|cancelled/i.test(message);
|
|
389
|
-
}
|
|
390
|
-
function sessionIdFromResponse(value) {
|
|
391
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
392
|
-
return null;
|
|
393
|
-
const sessionId = value.sessionId;
|
|
394
|
-
return typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
395
|
-
}
|
|
396
|
-
function objectOrNull(value) {
|
|
397
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
398
|
-
}
|
|
399
|
-
function modeToCodexSandbox(mode) {
|
|
400
|
-
const normalized = String(mode || "").trim();
|
|
401
|
-
if (!normalized)
|
|
402
|
-
return null;
|
|
403
|
-
if (normalized === "read-only")
|
|
404
|
-
return "read-only";
|
|
405
|
-
if (["agent-full-access", "full-access", "bypassPermissions", "danger-full-access"].includes(normalized))
|
|
406
|
-
return "danger-full-access";
|
|
407
|
-
return "workspace-write";
|
|
408
|
-
}
|
|
409
|
-
function approvalPolicyFromMode(mode) {
|
|
410
|
-
const normalized = String(mode || "").trim();
|
|
411
|
-
if (!normalized)
|
|
412
|
-
return null;
|
|
413
|
-
if (["agent-full-access", "full-access", "bypassPermissions", "danger-full-access"].includes(normalized))
|
|
414
|
-
return "never";
|
|
415
|
-
if (normalized === "acceptEdits" || normalized === "accept-edits")
|
|
416
|
-
return "on-failure";
|
|
417
|
-
return "on-request";
|
|
418
|
-
}
|
|
419
|
-
function initialCodexConfig(input) {
|
|
420
|
-
return {
|
|
421
|
-
...(input.model ? { model: input.model } : {}),
|
|
422
|
-
...(approvalPolicyFromMode(input.mode) ? { approvalPolicy: approvalPolicyFromMode(input.mode) } : {}),
|
|
423
|
-
...(modeToCodexSandbox(input.mode) ? { sandbox: modeToCodexSandbox(input.mode) } : {}),
|
|
424
|
-
...(input.configOptionValues ?? {})
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
function permissionRequestDetail(request) {
|
|
428
|
-
const params = request.params && typeof request.params === "object" && !Array.isArray(request.params) ? request.params : request;
|
|
429
|
-
for (const key of ["command", "cwd", "reason", "grantRoot", "method"]) {
|
|
430
|
-
const value = params[key];
|
|
431
|
-
if (typeof value === "string" && value.trim())
|
|
432
|
-
return textPreview(value);
|
|
433
|
-
}
|
|
434
|
-
return undefined;
|
|
435
|
-
}
|
|
436
|
-
function userInputRequestDetail(request) {
|
|
437
|
-
for (const key of ["prompt", "message", "question", "title"]) {
|
|
438
|
-
const value = request[key];
|
|
439
|
-
if (typeof value === "string" && value.trim())
|
|
440
|
-
return textPreview(value);
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
function textPreview(value) {
|
|
445
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
446
|
-
return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
|
|
447
|
-
}
|
|
448
|
-
function readCodexAcpPromptTimeoutMs() {
|
|
449
|
-
const parsed = Number.parseInt(process.env.ACA_CODEX_ACP_PROMPT_TIMEOUT_MS ?? process.env.ACA_ACP_PROMPT_TIMEOUT_MS ?? "", 10);
|
|
450
|
-
return Number.isInteger(parsed) && parsed >= 30_000 ? parsed : 6 * 60 * 60 * 1000;
|
|
451
|
-
}
|
|
452
|
-
function sessionIdFromPromptResponse(value) {
|
|
453
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
454
|
-
return null;
|
|
455
|
-
const record = value;
|
|
456
|
-
return typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId : null;
|
|
457
|
-
}
|
|
458
|
-
function contextUsageFromPromptResponse(value) {
|
|
459
|
-
const record = objectOrNull(value);
|
|
460
|
-
const meta = objectOrNull(record?._meta);
|
|
461
|
-
return contextUsageFromRecord(meta?.acaContextUsage);
|
|
462
|
-
}
|
|
463
|
-
function contextUsageFromRecord(value) {
|
|
464
|
-
const record = objectOrNull(value);
|
|
465
|
-
if (!record)
|
|
466
|
-
return null;
|
|
467
|
-
const contextWindow = finiteNumber(record.contextWindow);
|
|
468
|
-
const usedTokens = finiteNumber(record.usedTokens);
|
|
469
|
-
const inputTokens = finiteNumber(record.inputTokens);
|
|
470
|
-
const rawState = String(record.state || "unknown");
|
|
471
|
-
const state = ["active", "compacted", "new_thread"].includes(rawState)
|
|
472
|
-
? rawState
|
|
473
|
-
: "unknown";
|
|
474
|
-
return {
|
|
475
|
-
usedTokens,
|
|
476
|
-
inputTokens,
|
|
477
|
-
contextWindow,
|
|
478
|
-
ratio: contextWindow > 0 ? Math.max(0, finiteNumber(record.ratio) || usedTokens / contextWindow) : 0,
|
|
479
|
-
observedAtMs: finiteNumber(record.observedAtMs) || Date.now(),
|
|
480
|
-
state,
|
|
481
|
-
maintenanceAction: typeof record.maintenanceAction === "string" ? record.maintenanceAction : null
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
function finiteNumber(value) {
|
|
485
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
486
|
-
}
|
|
1
|
+
import p from"node:fs";import{promptBlocksToAcpContent as y}from"../../client/acp-content.js";import{eventFromAcpSessionUpdate as b,statusFromAcpSessionUpdate as C}from"../../client/acp-events.js";import{defaultAcpRuntimePool as I}from"../../client/acp-runtime-pool.js";import{resolveCodexAcpLaunch as v}from"./launch.js";import{CodexPromptPerformanceTracker as A,readCodexContextMaintenancePolicy as x}from"./context-maintenance.js";const P={sessionResume:!0,imageInput:!0,fileAttachment:!0,filesystem:!0,terminal:!0,permissionRequest:!0,configOptions:!0,usage:!0,contextUsage:!0,contextCompaction:!0,plan:!0,diff:!0};class L{id="codex-acp";name="Codex ACP";capabilities=P;async createSession(e){return new k(e.cwd,e.providerSessionId??null,e.timeoutMs)}}class k{cwd;providerSessionId;defaultTimeoutMs;lease=null;sessionResponse=null;statusCallback=null;updateCallback=null;permissionCallback=null;userInputCallback=null;abortHandler=null;statusTimeline=[];updates=[];content="";performanceTracker=null;constructor(e,t,s=_()){this.cwd=e,this.providerSessionId=t,this.defaultTimeoutMs=s}async sendPrompt(e){if(!p.existsSync(e.cwd)||!p.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const t=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.permissionCallback=e.onPermissionRequest??null,this.userInputCallback=e.onUserInputRequest??null,this.attachAbortSignal(e.signal),this.content="",this.updates.length=0,this.statusTimeline.length=0,this.performanceTracker=new A(t);let s=null,n=null,r=this.providerSessionId,i,a=null;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const c=await this.acquireRuntime(e.cwd);this.lease=c,s=c.client,n=c.started,await this.initialize(c),r=await this.establishSession(c,e),await this.applyConfig(s,r,e),this.emitStatus("thinking","思考中"),a=this.startSlowStatusTimer(),i=await this.promptWithAbort(s,{sessionId:r,prompt:y(e.prompt,e.promptBlocks),timeoutMs:e.timeoutMs??this.defaultTimeoutMs,signal:e.signal});const d=S(i);d&&d!==r&&(r=d,this.providerSessionId=d,c.setProviderSession({providerSessionId:d,sessionResponse:this.sessionResponse}))}finally{a&&clearInterval(a),this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}return this.emitStatus("completed","已完成"),{providerSessionId:r,cliType:"builtin",agentType:"codex",effectiveConfig:{model:e.model??null,mode:e.mode??null,configOptionValues:e.configOptionValues??null,command:n.command,processPid:n.child.pid??null,provider:"codex-acp",protocol:"acp"},command:n.command,args:n.args,cwd:e.cwd,exitCode:n.child.exitCode,signal:n.child.signalCode,durationMs:Date.now()-t,stdout:s.rpc.stdout,stderr:s.rpc.stderr,content:this.content.trim(),updates:[...this.updates],statusTimeline:[...this.statusTimeline],performance:this.performanceTracker?.snapshot(),contextUsage:E(i),promptResponse:i}}async compactContext(e){if(!p.existsSync(e.cwd)||!p.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const t=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.attachAbortSignal(e.signal);let s=this.providerSessionId,n;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const i=await this.acquireRuntime(e.cwd);this.lease=i,await this.initialize(i),s=await this.establishSession(i,e),await this.applyConfig(i.client,s,e),n=await i.client.compactSession({sessionId:s,allowRollover:e.allowRollover===!0},e.timeoutMs??600*1e3);const a=S(n);a&&a!==s&&(s=a,this.providerSessionId=a,i.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}))}catch(i){if(this.lease&&T(i,e.signal)){const a=this.lease;this.lease=null,await a.dispose().catch(()=>{})}throw i}finally{this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}const r=l(n)??{};return{providerSessionId:s||this.providerSessionId||"",contextUsage:w(r.contextUsage),maintenance:l(r.maintenance)??{},durationMs:Date.now()-t}}async cancel(){this.providerSessionId&&this.lease?.client.cancel(this.providerSessionId)}async close(){this.statusCallback=null,this.updateCallback=null,this.permissionCallback=null,this.userInputCallback=null,this.abortHandler&&(this.abortHandler=null),this.releaseLease()}async acquireRuntime(e){const t=v();return I.acquire({providerId:"codex-acp",cwd:e,providerSessionId:this.providerSessionId,command:t.command,args:t.args,onSessionUpdate:s=>this.handleSessionUpdate(s),onClientRequest:async(s,n)=>{if(s==="session/request_permission")return this.requestPermission(n);if(s==="elicitation/create"||s==="session/request_user_input")return this.requestUserInput(n);throw new Error(`Unsupported Codex ACP client request: ${s}`)}})}async initialize(e){e.initialized||(await e.client.initialize(6e4),e.markInitialized())}async establishSession(e,t){const s=e.client,n=R(t);if(e.providerSessionId&&e.sessionEstablished)return this.providerSessionId=e.providerSessionId,this.sessionResponse=e.sessionResponse,e.providerSessionId;if(this.providerSessionId)try{this.emitStatus("resuming","Codex ACP 会话恢复中","session/load");const a=await s.loadSession({sessionId:this.providerSessionId,cwd:t.cwd,config:n},6e4);this.sessionResponse=l(a);const c=f(a)||this.providerSessionId;return this.providerSessionId=c,e.setProviderSession({providerSessionId:c,sessionResponse:this.sessionResponse}),c}catch{this.providerSessionId=null}this.emitStatus("acp","Codex ACP 会话创建中","session/new");const r=await s.newSession({cwd:t.cwd,config:n},12e4);this.sessionResponse=l(r);const i=f(r);if(!i)throw new Error("Codex ACP session/new did not return sessionId");return this.providerSessionId=i,e.setProviderSession({providerSessionId:i,sessionResponse:this.sessionResponse}),i}async applyConfig(e,t,s){s.model&&await e.setSessionConfigOption({sessionId:t,configId:"model",value:s.model},6e4).catch(()=>{});const n=h(s.mode);n&&await e.setSessionConfigOption({sessionId:t,configId:"approvalPolicy",value:n},6e4).catch(()=>{});const r=m(s.mode);r&&await e.setSessionConfigOption({sessionId:t,configId:"sandbox",value:r},6e4).catch(()=>{});for(const[i,a]of Object.entries(s.configOptionValues??{}))["model","mode","sandbox","approvalPolicy"].includes(i)||await e.setSessionConfigOption({sessionId:t,configId:i,value:a},6e4).catch(()=>{})}handleSessionUpdate(e){this.performanceTracker?.recordEvent(),this.updates.push({method:"session/update",params:e});const t=b(e);t&&(t.type==="agent_message_chunk"&&(this.content+=t.text??""),this.updateCallback?.(t));const s=C(e);s&&this.emitStatus(s.phase,s.label,s.detail,s.updateType)}startSlowStatusTimer(){const e=x().slowEventThresholdMs,t=setInterval(()=>{const s=this.performanceTracker,n=Date.now();if(!s?.shouldReportSlow(n,e))return;const r=s.snapshot(n),i=Math.max(1,Math.round(r.silentForMs/6e4));this.emitStatus("thinking","模型长时间处理中",`已 ${i} 分钟没有收到新的 ACP 事件`,"aca.performance.slow")},Math.min(3e4,Math.max(1e3,Math.floor(e/4))));return t.unref(),t}attachAbortSignal(e){if(e){if(this.abortHandler=()=>{this.cancel()},e.aborted)throw new Error("Codex ACP prompt cancelled");e.addEventListener("abort",this.abortHandler,{once:!0})}}async promptWithAbort(e,t){const s=e.prompt({sessionId:t.sessionId,prompt:t.prompt},t.timeoutMs);if(!t.signal)return s;if(t.signal.aborted)throw await this.lease?.dispose().catch(()=>{}),new Error("Codex ACP prompt cancelled");let n=null;const r=new Promise((i,a)=>{n=()=>{e.cancel(t.sessionId),this.lease?.dispose().catch(()=>{}),a(new Error("Codex ACP prompt cancelled"))},t.signal?.addEventListener("abort",n,{once:!0})});try{return await Promise.race([s,r])}finally{n&&t.signal.removeEventListener("abort",n)}}async requestPermission(e){if(!this.permissionCallback)return{outcome:{outcome:"cancelled"}};const t=e&&typeof e=="object"&&!Array.isArray(e)?e:{},s=t.request&&typeof t.request=="object"&&!Array.isArray(t.request)?t.request:t,r=(Array.isArray(s.options)?s.options:[]).map((i,a)=>{const c=i&&typeof i=="object"&&!Array.isArray(i)?i:{};return{optionId:String(c.optionId??c.id??a),kind:String(c.kind??"choice"),...typeof c.label=="string"?{label:c.label}:{}}});return this.emitStatus("requestPermission","等待授权",q(s),"session/request_permission"),this.permissionCallback({requestId:String(s.requestId??s.id??`codex-acp-permission-${Date.now()}`),params:e,options:r})}async requestUserInput(e){if(!this.userInputCallback)return{outcome:{outcome:"cancelled"}};const t=e&&typeof e=="object"&&!Array.isArray(e)?e:{},s=t.request&&typeof t.request=="object"&&!Array.isArray(t.request)?t.request:t;return this.emitStatus("requestPermission","等待输入",M(s),"elicitation/create"),this.userInputCallback({requestId:String(s.requestId??s.id??`codex-acp-input-${Date.now()}`),prompt:String(s.prompt??s.message??s.question??""),params:e,...typeof s.defaultValue=="string"?{defaultValue:s.defaultValue}:{}})}emitStatus(e,t,s,n){const r={phase:e,label:t,...s?{detail:s}:{},...n?{updateType:n}:{},atMs:Date.now()},i=this.statusTimeline[this.statusTimeline.length-1];i&&i.phase===r.phase&&i.label===r.label&&i.detail===r.detail&&i.updateType===r.updateType||(this.statusTimeline.push(r),this.statusCallback?.(r))}releaseLease(){this.lease?.release(),this.lease=null}}function T(o,e){if(e?.aborted)return!0;const t=o instanceof Error?o.message:String(o||"");return/compact.*timed out|request timed out.*compact|adapter closed|aborted|cancelled/i.test(t)}function f(o){if(!o||typeof o!="object"||Array.isArray(o))return null;const e=o.sessionId;return typeof e=="string"&&e.trim()?e.trim():null}function l(o){return o&&typeof o=="object"&&!Array.isArray(o)?o:null}function m(o){const e=String(o||"").trim();return e?e==="read-only"?"read-only":["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"danger-full-access":"workspace-write":null}function h(o){const e=String(o||"").trim();return e?["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"never":e==="acceptEdits"||e==="accept-edits"?"on-failure":"on-request":null}function R(o){return{...o.model?{model:o.model}:{},...h(o.mode)?{approvalPolicy:h(o.mode)}:{},...m(o.mode)?{sandbox:m(o.mode)}:{},...o.configOptionValues??{}}}function q(o){const e=o.params&&typeof o.params=="object"&&!Array.isArray(o.params)?o.params:o;for(const t of["command","cwd","reason","grantRoot","method"]){const s=e[t];if(typeof s=="string"&&s.trim())return g(s)}}function M(o){for(const e of["prompt","message","question","title"]){const t=o[e];if(typeof t=="string"&&t.trim())return g(t)}}function g(o){const e=o.replace(/\s+/g," ").trim();return e.length>120?`${e.slice(0,117)}...`:e}function _(){const o=Number.parseInt(process.env.ACA_CODEX_ACP_PROMPT_TIMEOUT_MS??process.env.ACA_ACP_PROMPT_TIMEOUT_MS??"",10);return Number.isInteger(o)&&o>=3e4?o:360*60*1e3}function S(o){if(!o||typeof o!="object"||Array.isArray(o))return null;const e=o;return typeof e.sessionId=="string"&&e.sessionId.trim()?e.sessionId:null}function E(o){const e=l(o),t=l(e?._meta);return w(t?.acaContextUsage)}function w(o){const e=l(o);if(!e)return null;const t=u(e.contextWindow),s=u(e.usedTokens),n=u(e.inputTokens),r=String(e.state||"unknown"),i=["active","compacted","new_thread"].includes(r)?r:"unknown";return{usedTokens:s,inputTokens:n,contextWindow:t,ratio:t>0?Math.max(0,u(e.ratio)||s/t):0,observedAtMs:u(e.observedAtMs)||Date.now(),state:i,maintenanceAction:typeof e.maintenanceAction=="string"?e.maintenanceAction:null}}function u(o){return typeof o=="number"&&Number.isFinite(o)&&o>=0?o:0}export{L as CodexAcpProvider,P as codexAcpCapabilities};
|