@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,489 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { defaultAcpRuntimePool } from "../../client/acp-runtime-pool.js";
|
|
6
|
-
export const openCodeCapabilities = {
|
|
7
|
-
sessionResume: true,
|
|
8
|
-
imageInput: true,
|
|
9
|
-
fileAttachment: true,
|
|
10
|
-
filesystem: true,
|
|
11
|
-
terminal: true,
|
|
12
|
-
permissionRequest: true,
|
|
13
|
-
configOptions: true,
|
|
14
|
-
usage: true,
|
|
15
|
-
contextUsage: true,
|
|
16
|
-
contextCompaction: false,
|
|
17
|
-
plan: true,
|
|
18
|
-
diff: true
|
|
19
|
-
};
|
|
20
|
-
export class OpenCodeProvider {
|
|
21
|
-
id = "opencode";
|
|
22
|
-
name = "OpenCode";
|
|
23
|
-
capabilities = openCodeCapabilities;
|
|
24
|
-
async createSession(input) {
|
|
25
|
-
return new OpenCodeSession(input.cwd, input.providerSessionId ?? null, input.timeoutMs);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
class OpenCodeSession {
|
|
29
|
-
cwd;
|
|
30
|
-
providerSessionId;
|
|
31
|
-
defaultTimeoutMs;
|
|
32
|
-
lease = null;
|
|
33
|
-
sessionResponse = null;
|
|
34
|
-
statusCallback = null;
|
|
35
|
-
updateCallback = null;
|
|
36
|
-
permissionCallback = null;
|
|
37
|
-
userInputCallback = null;
|
|
38
|
-
canExecuteTools = true;
|
|
39
|
-
abortHandler = null;
|
|
40
|
-
statusTimeline = [];
|
|
41
|
-
updates = [];
|
|
42
|
-
content = "";
|
|
43
|
-
latestContextUsage = null;
|
|
44
|
-
contextWindow = 0;
|
|
45
|
-
resumedExistingSession = false;
|
|
46
|
-
constructor(cwd, providerSessionId, defaultTimeoutMs = readOpenCodePromptTimeoutMs()) {
|
|
47
|
-
this.cwd = cwd;
|
|
48
|
-
this.providerSessionId = providerSessionId;
|
|
49
|
-
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
50
|
-
}
|
|
51
|
-
async sendPrompt(input) {
|
|
52
|
-
if (!fs.existsSync(input.cwd) || !fs.statSync(input.cwd).isDirectory()) {
|
|
53
|
-
throw new Error(`Project root does not exist or is not a directory: ${input.cwd}`);
|
|
54
|
-
}
|
|
55
|
-
const startedAt = Date.now();
|
|
56
|
-
this.statusCallback = input.onStatus ?? null;
|
|
57
|
-
this.updateCallback = input.onUpdate ?? null;
|
|
58
|
-
this.permissionCallback = input.onPermissionRequest ?? null;
|
|
59
|
-
this.userInputCallback = input.onUserInputRequest ?? null;
|
|
60
|
-
this.canExecuteTools = input.canExecuteTools !== false;
|
|
61
|
-
this.attachAbortSignal(input.signal);
|
|
62
|
-
this.content = "";
|
|
63
|
-
this.updates.length = 0;
|
|
64
|
-
this.statusTimeline.length = 0;
|
|
65
|
-
this.latestContextUsage = null;
|
|
66
|
-
this.contextWindow = positiveNumber(input.contextWindow);
|
|
67
|
-
let client = null;
|
|
68
|
-
let started = null;
|
|
69
|
-
let sessionId = this.providerSessionId;
|
|
70
|
-
let promptResponse;
|
|
71
|
-
try {
|
|
72
|
-
this.emitStatus("initializing", "OpenCode ACP 启动中", "opencode acp");
|
|
73
|
-
const lease = await this.acquireRuntime(input.cwd);
|
|
74
|
-
this.lease = lease;
|
|
75
|
-
client = lease.client;
|
|
76
|
-
started = lease.started;
|
|
77
|
-
await this.initialize(lease);
|
|
78
|
-
sessionId = await this.establishSession(lease, input);
|
|
79
|
-
await this.applyConfig(client, sessionId, input);
|
|
80
|
-
this.emitStatus("thinking", "思考中");
|
|
81
|
-
const prompt = promptBlocksToOpenCodeAcpContent(input.prompt, input.promptBlocks);
|
|
82
|
-
try {
|
|
83
|
-
promptResponse = await this.promptWithAbort(client, {
|
|
84
|
-
sessionId,
|
|
85
|
-
prompt,
|
|
86
|
-
timeoutMs: input.timeoutMs ?? this.defaultTimeoutMs,
|
|
87
|
-
signal: input.signal
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
if (!this.resumedExistingSession || !isInvalidParamsError(error))
|
|
92
|
-
throw error;
|
|
93
|
-
// OpenCode can successfully load a persisted session whose prompt endpoint
|
|
94
|
-
// is no longer compatible after an upstream/runtime change. Reusing it would
|
|
95
|
-
// make every queued Chat Room task fail identically. Retry once from a fresh
|
|
96
|
-
// ACP session; a second failure is surfaced unchanged to the caller.
|
|
97
|
-
this.appendStderr("OpenCode resumed session rejected session/prompt with Invalid params; creating a fresh session and retrying once.\n");
|
|
98
|
-
await this.lease?.dispose().catch(() => void 0);
|
|
99
|
-
this.lease = null;
|
|
100
|
-
this.providerSessionId = null;
|
|
101
|
-
this.sessionResponse = null;
|
|
102
|
-
this.resumedExistingSession = false;
|
|
103
|
-
this.emitStatus("resuming", "OpenCode 会话已失效,正在新建", "session/prompt invalid params");
|
|
104
|
-
const freshLease = await this.acquireRuntime(input.cwd);
|
|
105
|
-
this.lease = freshLease;
|
|
106
|
-
client = freshLease.client;
|
|
107
|
-
started = freshLease.started;
|
|
108
|
-
await this.initialize(freshLease);
|
|
109
|
-
sessionId = await this.establishSession(freshLease, input);
|
|
110
|
-
await this.applyConfig(client, sessionId, input);
|
|
111
|
-
this.emitStatus("thinking", "思考中");
|
|
112
|
-
promptResponse = await this.promptWithAbort(client, {
|
|
113
|
-
sessionId,
|
|
114
|
-
prompt,
|
|
115
|
-
timeoutMs: input.timeoutMs ?? this.defaultTimeoutMs,
|
|
116
|
-
signal: input.signal
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
finally {
|
|
121
|
-
if (this.abortHandler && input.signal) {
|
|
122
|
-
input.signal.removeEventListener("abort", this.abortHandler);
|
|
123
|
-
this.abortHandler = null;
|
|
124
|
-
}
|
|
125
|
-
this.releaseLease();
|
|
126
|
-
}
|
|
127
|
-
this.emitStatus("completed", "已完成");
|
|
128
|
-
return {
|
|
129
|
-
providerSessionId: sessionId,
|
|
130
|
-
cliType: "builtin",
|
|
131
|
-
agentType: "opencode",
|
|
132
|
-
effectiveConfig: {
|
|
133
|
-
model: openCodeModelValue(input.model) || null,
|
|
134
|
-
mode: input.mode || null,
|
|
135
|
-
configOptionValues: input.configOptionValues ?? null,
|
|
136
|
-
command: started.command,
|
|
137
|
-
processPid: started.child.pid ?? null,
|
|
138
|
-
provider: "opencode",
|
|
139
|
-
protocol: "acp"
|
|
140
|
-
},
|
|
141
|
-
command: started.command,
|
|
142
|
-
args: started.args,
|
|
143
|
-
cwd: input.cwd,
|
|
144
|
-
exitCode: started.child.exitCode,
|
|
145
|
-
signal: started.child.signalCode,
|
|
146
|
-
durationMs: Date.now() - startedAt,
|
|
147
|
-
stdout: client.rpc.stdout,
|
|
148
|
-
stderr: client.rpc.stderr,
|
|
149
|
-
content: this.content.trim(),
|
|
150
|
-
updates: [...this.updates],
|
|
151
|
-
statusTimeline: [...this.statusTimeline],
|
|
152
|
-
contextUsage: this.latestContextUsage,
|
|
153
|
-
promptResponse
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
async cancel() {
|
|
157
|
-
if (this.providerSessionId)
|
|
158
|
-
this.lease?.client.cancel(this.providerSessionId);
|
|
159
|
-
}
|
|
160
|
-
async close() {
|
|
161
|
-
this.statusCallback = null;
|
|
162
|
-
this.updateCallback = null;
|
|
163
|
-
this.permissionCallback = null;
|
|
164
|
-
this.userInputCallback = null;
|
|
165
|
-
this.canExecuteTools = true;
|
|
166
|
-
if (this.abortHandler)
|
|
167
|
-
this.abortHandler = null;
|
|
168
|
-
this.releaseLease();
|
|
169
|
-
}
|
|
170
|
-
async acquireRuntime(cwd) {
|
|
171
|
-
const command = process.env.ACA_OPENCODE_COMMAND || "opencode";
|
|
172
|
-
return defaultAcpRuntimePool.acquire({
|
|
173
|
-
providerId: "opencode",
|
|
174
|
-
cwd,
|
|
175
|
-
providerSessionId: this.providerSessionId,
|
|
176
|
-
command,
|
|
177
|
-
args: ["acp", "--cwd", cwd],
|
|
178
|
-
onSessionUpdate: (params) => this.handleSessionUpdate(params),
|
|
179
|
-
onClientRequest: (method, params) => this.handleClientRequest(method, params)
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
async initialize(lease) {
|
|
183
|
-
if (lease.initialized)
|
|
184
|
-
return;
|
|
185
|
-
await lease.client.initialize(60_000);
|
|
186
|
-
lease.markInitialized();
|
|
187
|
-
}
|
|
188
|
-
async establishSession(lease, input) {
|
|
189
|
-
const client = lease.client;
|
|
190
|
-
const initialConfig = initialOpenCodeConfig(input);
|
|
191
|
-
this.resumedExistingSession = false;
|
|
192
|
-
if (lease.providerSessionId && lease.sessionEstablished) {
|
|
193
|
-
if (openCodeSessionModelMatches(lease.sessionResponse, input.model)) {
|
|
194
|
-
this.providerSessionId = lease.providerSessionId;
|
|
195
|
-
this.sessionResponse = lease.sessionResponse;
|
|
196
|
-
this.resumedExistingSession = true;
|
|
197
|
-
return lease.providerSessionId;
|
|
198
|
-
}
|
|
199
|
-
this.providerSessionId = null;
|
|
200
|
-
}
|
|
201
|
-
else if (this.providerSessionId) {
|
|
202
|
-
try {
|
|
203
|
-
this.emitStatus("resuming", "OpenCode 会话恢复中", "session/load");
|
|
204
|
-
const loaded = await client.loadSession({ sessionId: this.providerSessionId, cwd: input.cwd }, 60_000);
|
|
205
|
-
this.sessionResponse = objectOrNull(loaded);
|
|
206
|
-
if (openCodeSessionModelMatches(this.sessionResponse, input.model)) {
|
|
207
|
-
const sessionId = sessionIdFromResponse(loaded) || this.providerSessionId;
|
|
208
|
-
this.providerSessionId = sessionId;
|
|
209
|
-
lease.setProviderSession({ providerSessionId: sessionId, sessionResponse: this.sessionResponse });
|
|
210
|
-
this.resumedExistingSession = true;
|
|
211
|
-
return sessionId;
|
|
212
|
-
}
|
|
213
|
-
this.appendStderr(`OpenCode model changed from ${openCodeCurrentModelId(this.sessionResponse) ?? "unknown"} to ${openCodeModelValue(input.model) || "default"}, creating new session\n`);
|
|
214
|
-
this.providerSessionId = null;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
this.providerSessionId = null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
this.emitStatus("acp", "OpenCode 会话创建中", "session/new");
|
|
221
|
-
const created = await client.newSession({ cwd: input.cwd, config: initialConfig }, 120_000);
|
|
222
|
-
this.sessionResponse = objectOrNull(created);
|
|
223
|
-
const sessionId = sessionIdFromResponse(created);
|
|
224
|
-
if (!sessionId)
|
|
225
|
-
throw new Error("OpenCode ACP session/new did not return sessionId");
|
|
226
|
-
this.providerSessionId = sessionId;
|
|
227
|
-
lease.setProviderSession({ providerSessionId: sessionId, sessionResponse: this.sessionResponse });
|
|
228
|
-
return sessionId;
|
|
229
|
-
}
|
|
230
|
-
async applyConfig(client, sessionId, input) {
|
|
231
|
-
const mode = openCodeModeValue(input.mode);
|
|
232
|
-
if (mode) {
|
|
233
|
-
await client.setSessionMode({ sessionId, modeId: mode }, 60_000).catch((error) => {
|
|
234
|
-
this.appendStderr(`OpenCode mode config failed: ${errorMessage(error)}\n`);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
handleSessionUpdate(params) {
|
|
239
|
-
this.updates.push({ method: "session/update", params });
|
|
240
|
-
this.latestContextUsage = contextUsageFromAcpSessionUpdate(params, this.contextWindow) ?? this.latestContextUsage;
|
|
241
|
-
const event = eventFromAcpSessionUpdate(params);
|
|
242
|
-
if (event) {
|
|
243
|
-
if (event.type === "agent_message_chunk")
|
|
244
|
-
this.content += event.text ?? "";
|
|
245
|
-
this.updateCallback?.(event);
|
|
246
|
-
}
|
|
247
|
-
const status = statusFromAcpSessionUpdate(params);
|
|
248
|
-
if (status)
|
|
249
|
-
this.emitStatus(status.phase, status.label, status.detail, status.updateType);
|
|
250
|
-
}
|
|
251
|
-
async handleClientRequest(method, params) {
|
|
252
|
-
if (!this.canExecuteTools && (method === "fs/read_text_file" || method === "fs/write_text_file")) {
|
|
253
|
-
throw new Error(`Chat Room role policy disabled tool execution: ${method}`);
|
|
254
|
-
}
|
|
255
|
-
switch (method) {
|
|
256
|
-
case "fs/read_text_file":
|
|
257
|
-
return this.readTextFile(params);
|
|
258
|
-
case "fs/write_text_file":
|
|
259
|
-
return this.writeTextFile(params);
|
|
260
|
-
case "session/request_permission":
|
|
261
|
-
return this.requestPermission(params);
|
|
262
|
-
case "elicitation/create":
|
|
263
|
-
case "session/request_user_input":
|
|
264
|
-
return this.requestUserInput(params);
|
|
265
|
-
default:
|
|
266
|
-
throw new Error(`Unsupported OpenCode ACP client request: ${method}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
readTextFile(params) {
|
|
270
|
-
const filePath = resolveScopedPath(this.cwd, stringParam(params, "path") || stringParam(params, "filePath"));
|
|
271
|
-
return { content: fs.readFileSync(filePath, "utf8") };
|
|
272
|
-
}
|
|
273
|
-
writeTextFile(params) {
|
|
274
|
-
const filePath = resolveScopedPath(this.cwd, stringParam(params, "path") || stringParam(params, "filePath"));
|
|
275
|
-
const content = stringParam(params, "content") ?? stringParam(params, "text") ?? "";
|
|
276
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
277
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
278
|
-
return {};
|
|
279
|
-
}
|
|
280
|
-
attachAbortSignal(signal) {
|
|
281
|
-
if (!signal)
|
|
282
|
-
return;
|
|
283
|
-
this.abortHandler = () => {
|
|
284
|
-
void this.cancel();
|
|
285
|
-
};
|
|
286
|
-
if (signal.aborted)
|
|
287
|
-
throw new Error("OpenCode prompt cancelled");
|
|
288
|
-
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
289
|
-
}
|
|
290
|
-
async promptWithAbort(client, input) {
|
|
291
|
-
const promptPromise = client.prompt({ sessionId: input.sessionId, prompt: input.prompt }, input.timeoutMs);
|
|
292
|
-
if (!input.signal)
|
|
293
|
-
return promptPromise;
|
|
294
|
-
if (input.signal.aborted) {
|
|
295
|
-
await this.lease?.dispose().catch(() => void 0);
|
|
296
|
-
throw new Error("OpenCode prompt cancelled");
|
|
297
|
-
}
|
|
298
|
-
let abortHandler = null;
|
|
299
|
-
const abortPromise = new Promise((_, reject) => {
|
|
300
|
-
abortHandler = () => {
|
|
301
|
-
client.cancel(input.sessionId);
|
|
302
|
-
void this.lease?.dispose().catch(() => void 0);
|
|
303
|
-
reject(new Error("OpenCode prompt cancelled"));
|
|
304
|
-
};
|
|
305
|
-
input.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
306
|
-
});
|
|
307
|
-
try {
|
|
308
|
-
return await Promise.race([promptPromise, abortPromise]);
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
if (abortHandler)
|
|
312
|
-
input.signal.removeEventListener("abort", abortHandler);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
async requestPermission(params) {
|
|
316
|
-
if (!this.permissionCallback)
|
|
317
|
-
return { outcome: { outcome: "cancelled" } };
|
|
318
|
-
const record = objectOrNull(params) ?? {};
|
|
319
|
-
const request = objectOrNull(record.request) ?? record;
|
|
320
|
-
const rawOptions = Array.isArray(request.options) ? request.options : [];
|
|
321
|
-
const options = rawOptions.map((option, index) => {
|
|
322
|
-
const item = objectOrNull(option) ?? {};
|
|
323
|
-
return {
|
|
324
|
-
optionId: String(item.optionId ?? item.id ?? index),
|
|
325
|
-
kind: String(item.kind ?? item.outcome ?? "choice"),
|
|
326
|
-
...(typeof item.label === "string" ? { label: item.label } : {})
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
|
-
this.emitStatus("requestPermission", "等待授权", permissionRequestDetail(request), "session/request_permission");
|
|
330
|
-
return this.permissionCallback({
|
|
331
|
-
requestId: String(request.requestId ?? request.id ?? `opencode-permission-${Date.now()}`),
|
|
332
|
-
params,
|
|
333
|
-
options
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
async requestUserInput(params) {
|
|
337
|
-
if (!this.userInputCallback)
|
|
338
|
-
return { outcome: { outcome: "cancelled" } };
|
|
339
|
-
const record = objectOrNull(params) ?? {};
|
|
340
|
-
const request = objectOrNull(record.request) ?? record;
|
|
341
|
-
this.emitStatus("requestPermission", "等待输入", userInputRequestDetail(request), "elicitation/create");
|
|
342
|
-
return this.userInputCallback({
|
|
343
|
-
requestId: String(request.requestId ?? request.id ?? `opencode-input-${Date.now()}`),
|
|
344
|
-
prompt: String(request.prompt ?? request.message ?? request.question ?? ""),
|
|
345
|
-
params,
|
|
346
|
-
...(typeof request.defaultValue === "string" ? { defaultValue: request.defaultValue } : {})
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
emitStatus(phase, label, detail, updateType) {
|
|
350
|
-
const status = {
|
|
351
|
-
phase,
|
|
352
|
-
label,
|
|
353
|
-
...(detail ? { detail } : {}),
|
|
354
|
-
...(updateType ? { updateType } : {}),
|
|
355
|
-
atMs: Date.now()
|
|
356
|
-
};
|
|
357
|
-
const last = this.statusTimeline[this.statusTimeline.length - 1];
|
|
358
|
-
if (last && last.phase === status.phase && last.label === status.label && last.detail === status.detail && last.updateType === status.updateType)
|
|
359
|
-
return;
|
|
360
|
-
this.statusTimeline.push(status);
|
|
361
|
-
this.statusCallback?.(status);
|
|
362
|
-
}
|
|
363
|
-
appendStderr(value) {
|
|
364
|
-
const client = this.lease?.client;
|
|
365
|
-
if (client)
|
|
366
|
-
client.rpc.stderr += value;
|
|
367
|
-
}
|
|
368
|
-
releaseLease() {
|
|
369
|
-
this.lease?.release();
|
|
370
|
-
this.lease = null;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function sessionIdFromResponse(value) {
|
|
374
|
-
const record = objectOrNull(value);
|
|
375
|
-
const sessionId = record?.sessionId;
|
|
376
|
-
return typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
377
|
-
}
|
|
378
|
-
function objectOrNull(value) {
|
|
379
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
380
|
-
}
|
|
381
|
-
function positiveNumber(value) {
|
|
382
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
383
|
-
}
|
|
384
|
-
function initialOpenCodeConfig(input) {
|
|
385
|
-
const model = openCodeModelValue(input.model);
|
|
386
|
-
const mode = openCodeModeValue(input.mode);
|
|
387
|
-
return {
|
|
388
|
-
...openCodeExtraConfig(input.configOptionValues),
|
|
389
|
-
...(model ? { model } : {}),
|
|
390
|
-
...(mode ? { mode } : {})
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
function openCodeExtraConfig(values) {
|
|
394
|
-
const result = {};
|
|
395
|
-
for (const [key, value] of Object.entries(values ?? {})) {
|
|
396
|
-
if (["model", "mode", "reasoning_effort", "effort", "variant"].includes(key))
|
|
397
|
-
continue;
|
|
398
|
-
result[key] = value;
|
|
399
|
-
}
|
|
400
|
-
return result;
|
|
401
|
-
}
|
|
402
|
-
function openCodeSessionModelMatches(response, requestedModel) {
|
|
403
|
-
const model = openCodeModelValue(requestedModel);
|
|
404
|
-
if (!model)
|
|
405
|
-
return true;
|
|
406
|
-
const currentModel = openCodeCurrentModelId(response);
|
|
407
|
-
return !currentModel || currentModel === model;
|
|
408
|
-
}
|
|
409
|
-
function openCodeCurrentModelId(response) {
|
|
410
|
-
const models = objectOrNull(response?.models);
|
|
411
|
-
const value = models?.currentModelId;
|
|
412
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
413
|
-
}
|
|
414
|
-
function openCodeModeValue(mode) {
|
|
415
|
-
const normalized = String(mode || "").trim();
|
|
416
|
-
if (!normalized)
|
|
417
|
-
return null;
|
|
418
|
-
if (["agent-full-access", "full-access", "bypassPermissions", "danger-full-access"].includes(normalized))
|
|
419
|
-
return "build";
|
|
420
|
-
if (normalized === "read-only")
|
|
421
|
-
return "plan";
|
|
422
|
-
return normalized;
|
|
423
|
-
}
|
|
424
|
-
function stringParam(params, key) {
|
|
425
|
-
const record = objectOrNull(params);
|
|
426
|
-
const value = record?.[key];
|
|
427
|
-
return typeof value === "string" && value.trim() ? value : null;
|
|
428
|
-
}
|
|
429
|
-
function resolveScopedPath(root, filePath) {
|
|
430
|
-
if (!filePath)
|
|
431
|
-
throw new Error("ACP file request did not include a path");
|
|
432
|
-
const resolved = path.resolve(root, filePath);
|
|
433
|
-
const rootResolved = path.resolve(root);
|
|
434
|
-
if (resolved !== rootResolved && !resolved.startsWith(`${rootResolved}${path.sep}`)) {
|
|
435
|
-
throw new Error(`ACP file request is outside project root: ${filePath}`);
|
|
436
|
-
}
|
|
437
|
-
return resolved;
|
|
438
|
-
}
|
|
439
|
-
function permissionRequestDetail(request) {
|
|
440
|
-
const params = objectOrNull(request.params) ?? request;
|
|
441
|
-
for (const key of ["command", "cwd", "reason", "grantRoot", "method"]) {
|
|
442
|
-
const value = params[key];
|
|
443
|
-
if (typeof value === "string" && value.trim())
|
|
444
|
-
return textPreview(value);
|
|
445
|
-
}
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
448
|
-
function userInputRequestDetail(request) {
|
|
449
|
-
for (const key of ["prompt", "message", "question", "title"]) {
|
|
450
|
-
const value = request[key];
|
|
451
|
-
if (typeof value === "string" && value.trim())
|
|
452
|
-
return textPreview(value);
|
|
453
|
-
}
|
|
454
|
-
return undefined;
|
|
455
|
-
}
|
|
456
|
-
function textPreview(value) {
|
|
457
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
458
|
-
return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
|
|
459
|
-
}
|
|
460
|
-
function errorMessage(error) {
|
|
461
|
-
return error instanceof Error ? error.message : String(error);
|
|
462
|
-
}
|
|
463
|
-
function isInvalidParamsError(error) {
|
|
464
|
-
return /\binvalid params\b/i.test(errorMessage(error));
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* OpenCode ACP 对图片仅接受标准 base64 data,不兼容本地 uri 参数。
|
|
468
|
-
* 本地附件路径仍会通过 Job Worker 拼入文本提示,供角色使用工具读取。
|
|
469
|
-
*/
|
|
470
|
-
function promptBlocksToOpenCodeAcpContent(prompt, blocks) {
|
|
471
|
-
const inputBlocks = blocks?.length ? blocks : [{ type: "text", text: prompt }];
|
|
472
|
-
return inputBlocks.map((block) => {
|
|
473
|
-
if (block.type === "image") {
|
|
474
|
-
return { type: "image", data: block.data, mimeType: block.mimeType };
|
|
475
|
-
}
|
|
476
|
-
return { type: "text", text: block.text };
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
function readOpenCodeDefaultModel() {
|
|
480
|
-
return process.env.ACA_OPENCODE_DEFAULT_MODEL || "";
|
|
481
|
-
}
|
|
482
|
-
function openCodeModelValue(model) {
|
|
483
|
-
const normalized = String(model || readOpenCodeDefaultModel()).trim();
|
|
484
|
-
return normalized === "default" || normalized === "opencode-default" ? "" : normalized;
|
|
485
|
-
}
|
|
486
|
-
function readOpenCodePromptTimeoutMs() {
|
|
487
|
-
const parsed = Number.parseInt(process.env.ACA_OPENCODE_PROMPT_TIMEOUT_MS ?? process.env.ACA_ACP_PROMPT_TIMEOUT_MS ?? "", 10);
|
|
488
|
-
return Number.isInteger(parsed) && parsed >= 30_000 ? parsed : 6 * 60 * 60 * 1000;
|
|
489
|
-
}
|
|
1
|
+
import p from"node:fs";import h from"node:path";import{eventFromAcpSessionUpdate as v,statusFromAcpSessionUpdate as x}from"../../client/acp-events.js";import{contextUsageFromAcpSessionUpdate as O}from"../../client/context-usage.js";import{defaultAcpRuntimePool as E}from"../../client/acp-runtime-pool.js";const P={sessionResume:!0,imageInput:!0,fileAttachment:!0,filesystem:!0,terminal:!0,permissionRequest:!0,configOptions:!0,usage:!0,contextUsage:!0,contextCompaction:!1,plan:!0,diff:!0};class V{id="opencode";name="OpenCode";capabilities=P;async createSession(e){return new T(e.cwd,e.providerSessionId??null,e.timeoutMs)}}class T{cwd;providerSessionId;defaultTimeoutMs;lease=null;sessionResponse=null;statusCallback=null;updateCallback=null;permissionCallback=null;userInputCallback=null;canExecuteTools=!0;abortHandler=null;statusTimeline=[];updates=[];content="";latestContextUsage=null;contextWindow=0;resumedExistingSession=!1;constructor(e,t,s=F()){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.canExecuteTools=e.canExecuteTools!==!1,this.attachAbortSignal(e.signal),this.content="",this.updates.length=0,this.statusTimeline.length=0,this.latestContextUsage=null,this.contextWindow=_(e.contextWindow);let s=null,n=null,o=this.providerSessionId,r;try{this.emitStatus("initializing","OpenCode ACP 启动中","opencode acp");const l=await this.acquireRuntime(e.cwd);this.lease=l,s=l.client,n=l.started,await this.initialize(l),o=await this.establishSession(l,e),await this.applyConfig(s,o,e),this.emitStatus("thinking","思考中");const a=U(e.prompt,e.promptBlocks);try{r=await this.promptWithAbort(s,{sessionId:o,prompt:a,timeoutMs:e.timeoutMs??this.defaultTimeoutMs,signal:e.signal})}catch(f){if(!this.resumedExistingSession||!k(f))throw f;this.appendStderr(`OpenCode resumed session rejected session/prompt with Invalid params; creating a fresh session and retrying once.
|
|
2
|
+
`),await this.lease?.dispose().catch(()=>{}),this.lease=null,this.providerSessionId=null,this.sessionResponse=null,this.resumedExistingSession=!1,this.emitStatus("resuming","OpenCode 会话已失效,正在新建","session/prompt invalid params");const u=await this.acquireRuntime(e.cwd);this.lease=u,s=u.client,n=u.started,await this.initialize(u),o=await this.establishSession(u,e),await this.applyConfig(s,o,e),this.emitStatus("thinking","思考中"),r=await this.promptWithAbort(s,{sessionId:o,prompt:a,timeoutMs:e.timeoutMs??this.defaultTimeoutMs,signal:e.signal})}}finally{this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}return this.emitStatus("completed","已完成"),{providerSessionId:o,cliType:"builtin",agentType:"opencode",effectiveConfig:{model:m(e.model)||null,mode:e.mode||null,configOptionValues:e.configOptionValues??null,command:n.command,processPid:n.child.pid??null,provider:"opencode",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],contextUsage:this.latestContextUsage,promptResponse:r}}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.canExecuteTools=!0,this.abortHandler&&(this.abortHandler=null),this.releaseLease()}async acquireRuntime(e){const t=process.env.ACA_OPENCODE_COMMAND||"opencode";return E.acquire({providerId:"opencode",cwd:e,providerSessionId:this.providerSessionId,command:t,args:["acp","--cwd",e],onSessionUpdate:s=>this.handleSessionUpdate(s),onClientRequest:(s,n)=>this.handleClientRequest(s,n)})}async initialize(e){e.initialized||(await e.client.initialize(6e4),e.markInitialized())}async establishSession(e,t){const s=e.client,n=q(t);if(this.resumedExistingSession=!1,e.providerSessionId&&e.sessionEstablished){if(S(e.sessionResponse,t.model))return this.providerSessionId=e.providerSessionId,this.sessionResponse=e.sessionResponse,this.resumedExistingSession=!0,e.providerSessionId;this.providerSessionId=null}else if(this.providerSessionId)try{this.emitStatus("resuming","OpenCode 会话恢复中","session/load");const l=await s.loadSession({sessionId:this.providerSessionId,cwd:t.cwd},6e4);if(this.sessionResponse=d(l),S(this.sessionResponse,t.model)){const a=g(l)||this.providerSessionId;return this.providerSessionId=a,e.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}),this.resumedExistingSession=!0,a}this.appendStderr(`OpenCode model changed from ${C(this.sessionResponse)??"unknown"} to ${m(t.model)||"default"}, creating new session
|
|
3
|
+
`),this.providerSessionId=null}catch{this.providerSessionId=null}this.emitStatus("acp","OpenCode 会话创建中","session/new");const o=await s.newSession({cwd:t.cwd,config:n},12e4);this.sessionResponse=d(o);const r=g(o);if(!r)throw new Error("OpenCode ACP session/new did not return sessionId");return this.providerSessionId=r,e.setProviderSession({providerSessionId:r,sessionResponse:this.sessionResponse}),r}async applyConfig(e,t,s){const n=w(s.mode);n&&await e.setSessionMode({sessionId:t,modeId:n},6e4).catch(o=>{this.appendStderr(`OpenCode mode config failed: ${y(o)}
|
|
4
|
+
`)})}handleSessionUpdate(e){this.updates.push({method:"session/update",params:e}),this.latestContextUsage=O(e,this.contextWindow)??this.latestContextUsage;const t=v(e);t&&(t.type==="agent_message_chunk"&&(this.content+=t.text??""),this.updateCallback?.(t));const s=x(e);s&&this.emitStatus(s.phase,s.label,s.detail,s.updateType)}async handleClientRequest(e,t){if(!this.canExecuteTools&&(e==="fs/read_text_file"||e==="fs/write_text_file"))throw new Error(`Chat Room role policy disabled tool execution: ${e}`);switch(e){case"fs/read_text_file":return this.readTextFile(t);case"fs/write_text_file":return this.writeTextFile(t);case"session/request_permission":return this.requestPermission(t);case"elicitation/create":case"session/request_user_input":return this.requestUserInput(t);default:throw new Error(`Unsupported OpenCode ACP client request: ${e}`)}}readTextFile(e){const t=I(this.cwd,c(e,"path")||c(e,"filePath"));return{content:p.readFileSync(t,"utf8")}}writeTextFile(e){const t=I(this.cwd,c(e,"path")||c(e,"filePath")),s=c(e,"content")??c(e,"text")??"";return p.mkdirSync(h.dirname(t),{recursive:!0}),p.writeFileSync(t,s,"utf8"),{}}attachAbortSignal(e){if(e){if(this.abortHandler=()=>{this.cancel()},e.aborted)throw new Error("OpenCode 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("OpenCode prompt cancelled");let n=null;const o=new Promise((r,l)=>{n=()=>{e.cancel(t.sessionId),this.lease?.dispose().catch(()=>{}),l(new Error("OpenCode prompt cancelled"))},t.signal?.addEventListener("abort",n,{once:!0})});try{return await Promise.race([s,o])}finally{n&&t.signal.removeEventListener("abort",n)}}async requestPermission(e){if(!this.permissionCallback)return{outcome:{outcome:"cancelled"}};const t=d(e)??{},s=d(t.request)??t,o=(Array.isArray(s.options)?s.options:[]).map((r,l)=>{const a=d(r)??{};return{optionId:String(a.optionId??a.id??l),kind:String(a.kind??a.outcome??"choice"),...typeof a.label=="string"?{label:a.label}:{}}});return this.emitStatus("requestPermission","等待授权",A(s),"session/request_permission"),this.permissionCallback({requestId:String(s.requestId??s.id??`opencode-permission-${Date.now()}`),params:e,options:o})}async requestUserInput(e){if(!this.userInputCallback)return{outcome:{outcome:"cancelled"}};const t=d(e)??{},s=d(t.request)??t;return this.emitStatus("requestPermission","等待输入",M(s),"elicitation/create"),this.userInputCallback({requestId:String(s.requestId??s.id??`opencode-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 o={phase:e,label:t,...s?{detail:s}:{},...n?{updateType:n}:{},atMs:Date.now()},r=this.statusTimeline[this.statusTimeline.length-1];r&&r.phase===o.phase&&r.label===o.label&&r.detail===o.detail&&r.updateType===o.updateType||(this.statusTimeline.push(o),this.statusCallback?.(o))}appendStderr(e){const t=this.lease?.client;t&&(t.rpc.stderr+=e)}releaseLease(){this.lease?.release(),this.lease=null}}function g(i){const t=d(i)?.sessionId;return typeof t=="string"&&t.trim()?t.trim():null}function d(i){return i&&typeof i=="object"&&!Array.isArray(i)?i:null}function _(i){return typeof i=="number"&&Number.isFinite(i)&&i>0?i:0}function q(i){const e=m(i.model),t=w(i.mode);return{...R(i.configOptionValues),...e?{model:e}:{},...t?{mode:t}:{}}}function R(i){const e={};for(const[t,s]of Object.entries(i??{}))["model","mode","reasoning_effort","effort","variant"].includes(t)||(e[t]=s);return e}function S(i,e){const t=m(e);if(!t)return!0;const s=C(i);return!s||s===t}function C(i){const t=d(i?.models)?.currentModelId;return typeof t=="string"&&t.trim()?t.trim():null}function w(i){const e=String(i||"").trim();return e?["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"build":e==="read-only"?"plan":e:null}function c(i,e){const s=d(i)?.[e];return typeof s=="string"&&s.trim()?s:null}function I(i,e){if(!e)throw new Error("ACP file request did not include a path");const t=h.resolve(i,e),s=h.resolve(i);if(t!==s&&!t.startsWith(`${s}${h.sep}`))throw new Error(`ACP file request is outside project root: ${e}`);return t}function A(i){const e=d(i.params)??i;for(const t of["command","cwd","reason","grantRoot","method"]){const s=e[t];if(typeof s=="string"&&s.trim())return b(s)}}function M(i){for(const e of["prompt","message","question","title"]){const t=i[e];if(typeof t=="string"&&t.trim())return b(t)}}function b(i){const e=i.replace(/\s+/g," ").trim();return e.length>120?`${e.slice(0,117)}...`:e}function y(i){return i instanceof Error?i.message:String(i)}function k(i){return/\binvalid params\b/i.test(y(i))}function U(i,e){return(e?.length?e:[{type:"text",text:i}]).map(s=>s.type==="image"?{type:"image",data:s.data,mimeType:s.mimeType}:{type:"text",text:s.text})}function D(){return process.env.ACA_OPENCODE_DEFAULT_MODEL||""}function m(i){const e=String(i||D()).trim();return e==="default"||e==="opencode-default"?"":e}function F(){const i=Number.parseInt(process.env.ACA_OPENCODE_PROMPT_TIMEOUT_MS??process.env.ACA_ACP_PROMPT_TIMEOUT_MS??"",10);return Number.isInteger(i)&&i>=3e4?i:360*60*1e3}export{V as OpenCodeProvider,P as openCodeCapabilities};
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CodexAcpProvider } from "./codex-acp/provider.js";
|
|
3
|
-
import { MimoCodeProvider } from "./mimo/provider.js";
|
|
4
|
-
import { OpenCodeProvider } from "./opencode/provider.js";
|
|
5
|
-
export function createAgentProvider(input) {
|
|
6
|
-
const agentType = normalizeProviderKey(input.agentType || "codex");
|
|
7
|
-
const cliType = normalizeProviderKey(input.cliType || "builtin");
|
|
8
|
-
if (cliType !== "builtin") {
|
|
9
|
-
throw new Error(`Unsupported ACP cliType: ${input.cliType ?? ""}`);
|
|
10
|
-
}
|
|
11
|
-
if (agentType === "codex" || agentType === "codexacp" || agentType === "codexstandard")
|
|
12
|
-
return new CodexAcpProvider();
|
|
13
|
-
if (agentType === "nativecodex" || agentType === "codexnative")
|
|
14
|
-
return new NativeCodexProvider();
|
|
15
|
-
if (agentType === "mimo" || agentType === "mimocode" || agentType === "mimocodeprovider")
|
|
16
|
-
return new MimoCodeProvider();
|
|
17
|
-
if (agentType === "opencode" || agentType === "opencodeprovider")
|
|
18
|
-
return new OpenCodeProvider();
|
|
19
|
-
throw new Error(`Unsupported ACP agentType: ${input.agentType ?? ""}`);
|
|
20
|
-
}
|
|
21
|
-
function normalizeProviderKey(value) {
|
|
22
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
23
|
-
}
|
|
1
|
+
import{NativeCodexProvider as i}from"./codex/provider.js";import{CodexAcpProvider as n}from"./codex-acp/provider.js";import{MimoCodeProvider as t}from"./mimo/provider.js";import{OpenCodeProvider as d}from"./opencode/provider.js";function v(o){const e=r(o.agentType||"codex");if(r(o.cliType||"builtin")!=="builtin")throw new Error(`Unsupported ACP cliType: ${o.cliType??""}`);if(e==="codex"||e==="codexacp"||e==="codexstandard")return new n;if(e==="nativecodex"||e==="codexnative")return new i;if(e==="mimo"||e==="mimocode"||e==="mimocodeprovider")return new t;if(e==="opencode"||e==="opencodeprovider")return new d;throw new Error(`Unsupported ACP agentType: ${o.agentType??""}`)}function r(o){return o.toLowerCase().replace(/[^a-z0-9]+/g,"")}export{v as createAgentProvider};
|