remote-codex 0.11.44 → 0.11.49
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/README.md +84 -5
- package/apps/relay-server/dist/index.js +18 -2
- package/apps/supervisor-api/dist/index.js +10034 -2782
- package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
- package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
- package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
- package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
- package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
- package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
- package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
- package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
- package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
- package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
- package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
- package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
- package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/apps/supervisor-web/dist/index.html +4 -4
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows-device-manager.zh.md +143 -0
- package/docs/windows-device-setup.zh.md +263 -0
- package/docs/windows-one-click-installer-research.zh.md +481 -0
- package/docs/windows.md +87 -0
- package/package.json +20 -4
- package/packages/acp/src/agent-catalog.test.ts +57 -0
- package/packages/acp/src/agent-catalog.ts +361 -0
- package/packages/acp/src/catalog-runtime.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.ts +507 -0
- package/packages/acp/src/codex-environment.test.ts +58 -0
- package/packages/acp/src/codex-environment.ts +33 -0
- package/packages/acp/src/index.ts +5 -0
- package/packages/acp/src/item-mapper.test.ts +137 -0
- package/packages/acp/src/item-mapper.ts +473 -0
- package/packages/acp/src/runtimeAdapter.test.ts +79 -0
- package/packages/acp/src/runtimeAdapter.ts +1193 -0
- package/packages/acp/src/terminal-service.test.ts +31 -0
- package/packages/acp/src/terminal-service.ts +137 -0
- package/packages/agent-runtime/src/runtime-errors.ts +1 -0
- package/packages/agent-runtime/src/types.ts +8 -0
- package/packages/claude/src/runtimeAdapter.test.ts +2 -2
- package/packages/claude/src/runtimeAdapter.ts +10 -19
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
- package/packages/db/src/repositories.ts +3 -0
- package/packages/db/src/schema.ts +1 -0
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/agent-providers.ts +10 -1
- package/packages/shared/src/index.ts +36 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/build-device-manager.ps1 +64 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
- package/apps/supervisor-web/dist/assets/index-CO8cU7a0.js +0 -21
- package/apps/supervisor-web/dist/assets/thread-ui-Dmrigdek.js +0 -3714
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AgentRuntimeError,
|
|
5
|
+
type AgentActionRequestResponseInput,
|
|
6
|
+
type AgentModel,
|
|
7
|
+
type AgentPendingProviderRequest,
|
|
8
|
+
type AgentProviderCapabilities,
|
|
9
|
+
type AgentProviderRequest,
|
|
10
|
+
type AgentProviderRequestMapping,
|
|
11
|
+
type AgentRuntime,
|
|
12
|
+
type AgentRuntimeEvent,
|
|
13
|
+
type AgentRuntimeManagementSchema,
|
|
14
|
+
type AgentRuntimeStatus,
|
|
15
|
+
type AgentSessionDetail,
|
|
16
|
+
type AgentSessionSummary,
|
|
17
|
+
type AgentTurn,
|
|
18
|
+
type InterruptAgentTurnInput,
|
|
19
|
+
type ReadAgentSessionOptions,
|
|
20
|
+
type ResumeAgentSessionInput,
|
|
21
|
+
type StartAgentSessionInput,
|
|
22
|
+
type StartAgentSessionResult,
|
|
23
|
+
type StartAgentTurnInput,
|
|
24
|
+
} from '../../agent-runtime/src/index';
|
|
25
|
+
import type { AgentBackendInstallationDto } from '../../shared/src/index';
|
|
26
|
+
import {
|
|
27
|
+
AcpAgentCatalog,
|
|
28
|
+
acpAgentMetadata,
|
|
29
|
+
type AcpAgentCatalogEntry,
|
|
30
|
+
} from './agent-catalog';
|
|
31
|
+
import { AcpRuntimeAdapter } from './runtimeAdapter';
|
|
32
|
+
import { loadCodexAcpEnvironment } from './codex-environment';
|
|
33
|
+
|
|
34
|
+
interface AcpCatalogRuntimeOptions {
|
|
35
|
+
customCommand?: string | null;
|
|
36
|
+
codexHome?: string | null;
|
|
37
|
+
startupTimeoutMs?: number;
|
|
38
|
+
clientInfo?: {
|
|
39
|
+
name: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
version?: string;
|
|
42
|
+
};
|
|
43
|
+
catalog?: AcpAgentCatalog;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SESSION_DELIMITER = '::';
|
|
47
|
+
|
|
48
|
+
const catalogCapabilities: AgentProviderCapabilities = {
|
|
49
|
+
sessions: {
|
|
50
|
+
list: true,
|
|
51
|
+
read: true,
|
|
52
|
+
resume: true,
|
|
53
|
+
importLocal: false,
|
|
54
|
+
},
|
|
55
|
+
turns: {
|
|
56
|
+
start: true,
|
|
57
|
+
streamInput: false,
|
|
58
|
+
steer: false,
|
|
59
|
+
interrupt: true,
|
|
60
|
+
compact: false,
|
|
61
|
+
},
|
|
62
|
+
branching: {
|
|
63
|
+
fork: false,
|
|
64
|
+
hardRollback: false,
|
|
65
|
+
resumeAt: false,
|
|
66
|
+
rewindFiles: false,
|
|
67
|
+
},
|
|
68
|
+
controls: {
|
|
69
|
+
planMode: true,
|
|
70
|
+
permissionRequests: true,
|
|
71
|
+
sandboxMode: true,
|
|
72
|
+
performanceMode: false,
|
|
73
|
+
goals: false,
|
|
74
|
+
},
|
|
75
|
+
management: {
|
|
76
|
+
models: false,
|
|
77
|
+
mcpStatus: false,
|
|
78
|
+
skills: false,
|
|
79
|
+
hooks: false,
|
|
80
|
+
hookTrust: false,
|
|
81
|
+
hostConfigFiles: false,
|
|
82
|
+
providerSettings: false,
|
|
83
|
+
},
|
|
84
|
+
usage: {
|
|
85
|
+
contextWindow: true,
|
|
86
|
+
tokenUsage: true,
|
|
87
|
+
costUsd: false,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const catalogManagementSchema: AgentRuntimeManagementSchema = {
|
|
92
|
+
hostConfigFiles: [],
|
|
93
|
+
toolboxItems: [],
|
|
94
|
+
hookCommandTemplates: [],
|
|
95
|
+
providerConfigFormat: 'none',
|
|
96
|
+
mcpConfigFormat: 'none',
|
|
97
|
+
configArchives: false,
|
|
98
|
+
buildRestart: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function encodeSessionId(agentId: string, providerSessionId: string) {
|
|
102
|
+
return `${agentId}${SESSION_DELIMITER}${providerSessionId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodeScopedId(value: string | number) {
|
|
106
|
+
const normalized = String(value);
|
|
107
|
+
const delimiterIndex = normalized.indexOf(SESSION_DELIMITER);
|
|
108
|
+
if (delimiterIndex <= 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
agentId: normalized.slice(0, delimiterIndex),
|
|
113
|
+
rawId: normalized.slice(delimiterIndex + SESSION_DELIMITER.length),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scopedSession(agentId: string, session: AgentSessionDetail): AgentSessionDetail {
|
|
118
|
+
return {
|
|
119
|
+
...session,
|
|
120
|
+
providerSessionId: encodeSessionId(agentId, session.providerSessionId),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scopedSummary(agentId: string, session: AgentSessionSummary): AgentSessionSummary {
|
|
125
|
+
return {
|
|
126
|
+
...session,
|
|
127
|
+
providerSessionId: encodeSessionId(agentId, session.providerSessionId),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function delegatedSessionInput(input: StartAgentSessionInput): StartAgentSessionInput {
|
|
132
|
+
const delegated = { ...input };
|
|
133
|
+
delete delegated.agentId;
|
|
134
|
+
return delegated;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
138
|
+
readonly provider = 'acp' as const;
|
|
139
|
+
readonly displayName = 'ACP Agent';
|
|
140
|
+
readonly description = 'Choose from installed ACP-native agents and ACP adapters.';
|
|
141
|
+
readonly capabilities = structuredClone(catalogCapabilities);
|
|
142
|
+
readonly managementSchema = catalogManagementSchema;
|
|
143
|
+
readonly installation: AgentBackendInstallationDto = {
|
|
144
|
+
packageName: null,
|
|
145
|
+
installed: false,
|
|
146
|
+
installedVersion: null,
|
|
147
|
+
latestVersion: null,
|
|
148
|
+
installCommand: null,
|
|
149
|
+
updateCommand: null,
|
|
150
|
+
busy: false,
|
|
151
|
+
lastError: null,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
private readonly catalog: AcpAgentCatalog;
|
|
155
|
+
private readonly agents = new Map<string, AcpRuntimeAdapter>();
|
|
156
|
+
private readonly modelCache = new Map<string, { at: number; models: AgentModel[] }>();
|
|
157
|
+
private status: AgentRuntimeStatus = {
|
|
158
|
+
state: 'stopped',
|
|
159
|
+
transport: 'stdio',
|
|
160
|
+
lastStartedAt: null,
|
|
161
|
+
lastError: null,
|
|
162
|
+
restartCount: 0,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
constructor(private readonly options: AcpCatalogRuntimeOptions = {}) {
|
|
166
|
+
super();
|
|
167
|
+
this.catalog = options.catalog ?? new AcpAgentCatalog(
|
|
168
|
+
options.customCommand !== undefined
|
|
169
|
+
? { customCommand: options.customCommand }
|
|
170
|
+
: {},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getStatus() {
|
|
175
|
+
return { ...this.status };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async start() {
|
|
179
|
+
this.status = {
|
|
180
|
+
...this.status,
|
|
181
|
+
state: 'starting',
|
|
182
|
+
lastError: null,
|
|
183
|
+
restartCount: this.status.lastStartedAt ? this.status.restartCount + 1 : 0,
|
|
184
|
+
};
|
|
185
|
+
this.emit('status', this.getStatus());
|
|
186
|
+
await this.refreshCatalog(true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async stop() {
|
|
190
|
+
await Promise.allSettled([...this.agents.values()].map((agent) => agent.stop()));
|
|
191
|
+
this.agents.clear();
|
|
192
|
+
this.status = {
|
|
193
|
+
...this.status,
|
|
194
|
+
state: 'stopped',
|
|
195
|
+
lastError: null,
|
|
196
|
+
};
|
|
197
|
+
this.emit('status', this.getStatus());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async listModels(): Promise<AgentModel[]> {
|
|
201
|
+
return this.listAgentOptions();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async listAgentOptions(): Promise<AgentModel[]> {
|
|
205
|
+
const entries = await this.refreshCatalog();
|
|
206
|
+
const defaultAgent = entries.find((entry) => entry.availability === 'ready')?.id ?? null;
|
|
207
|
+
return entries.map((entry) => ({
|
|
208
|
+
id: entry.id,
|
|
209
|
+
model: entry.id,
|
|
210
|
+
displayName: entry.displayName,
|
|
211
|
+
description: entry.description,
|
|
212
|
+
isDefault: entry.id === defaultAgent,
|
|
213
|
+
hidden: false,
|
|
214
|
+
supportedReasoningEfforts: [],
|
|
215
|
+
defaultReasoningEffort: null,
|
|
216
|
+
selectionKind: 'agent',
|
|
217
|
+
acpAgent: acpAgentMetadata(entry),
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async listModelsForAgent(agentId: string, cwd: string) {
|
|
222
|
+
const cacheKey = `${agentId}\0${cwd}`;
|
|
223
|
+
const cached = this.modelCache.get(cacheKey);
|
|
224
|
+
if (cached && Date.now() - cached.at < 30_000) {
|
|
225
|
+
return cached.models;
|
|
226
|
+
}
|
|
227
|
+
const agent = await this.agentFor(agentId);
|
|
228
|
+
let models = await agent.inspectModelOptions(cwd);
|
|
229
|
+
if (models.length === 1 && models[0]?.model === 'default') {
|
|
230
|
+
const commandModels = await this.catalog.listCommandModels(agentId);
|
|
231
|
+
if (commandModels.length > 0) {
|
|
232
|
+
models = commandModels;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.modelCache.set(cacheKey, { at: Date.now(), models });
|
|
236
|
+
return models;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async installModel(modelId: string) {
|
|
240
|
+
await this.catalog.installAdapter(modelId);
|
|
241
|
+
await this.refreshCatalog(true);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async listSessions() {
|
|
245
|
+
const sessions = await Promise.all(
|
|
246
|
+
[...this.agents.entries()].map(async ([agentId, agent]) =>
|
|
247
|
+
(await agent.listSessions()).map((session) => scopedSummary(agentId, session)),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
return sessions.flat();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async listLoadedSessions() {
|
|
254
|
+
const sessions = await Promise.all(
|
|
255
|
+
[...this.agents.entries()].map(async ([agentId, agent]) =>
|
|
256
|
+
(await agent.listLoadedSessions()).map((sessionId) =>
|
|
257
|
+
encodeSessionId(agentId, sessionId),
|
|
258
|
+
),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
return sessions.flat();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async readSession(
|
|
265
|
+
providerSessionId: string,
|
|
266
|
+
options?: ReadAgentSessionOptions,
|
|
267
|
+
): Promise<AgentSessionDetail> {
|
|
268
|
+
void options;
|
|
269
|
+
const owner = this.requireSessionOwner(providerSessionId);
|
|
270
|
+
const agent = await this.agentFor(owner.agentId);
|
|
271
|
+
return scopedSession(
|
|
272
|
+
owner.agentId,
|
|
273
|
+
await agent.readSession(owner.rawId),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
278
|
+
const agentId = input.agentId;
|
|
279
|
+
if (!agentId) {
|
|
280
|
+
throw new AgentRuntimeError('Select an ACP agent before creating the thread.', 'acp');
|
|
281
|
+
}
|
|
282
|
+
const agent = await this.agentFor(agentId);
|
|
283
|
+
const response = await agent.startSession(delegatedSessionInput(input));
|
|
284
|
+
const session = scopedSession(agentId, response.session);
|
|
285
|
+
return {
|
|
286
|
+
...response,
|
|
287
|
+
agentId,
|
|
288
|
+
providerSessionId: session.providerSessionId,
|
|
289
|
+
model: response.model ?? input.model,
|
|
290
|
+
reasoningEffort: response.reasoningEffort ?? input.reasoningEffort ?? null,
|
|
291
|
+
session,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
296
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
297
|
+
const agent = await this.agentFor(owner.agentId);
|
|
298
|
+
const response = await agent.resumeSession({
|
|
299
|
+
...input,
|
|
300
|
+
providerSessionId: owner.rawId,
|
|
301
|
+
});
|
|
302
|
+
const session = scopedSession(owner.agentId, response.session);
|
|
303
|
+
return {
|
|
304
|
+
...response,
|
|
305
|
+
agentId: owner.agentId,
|
|
306
|
+
providerSessionId: session.providerSessionId,
|
|
307
|
+
model: response.model ?? input.model ?? null,
|
|
308
|
+
session,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async startTurn(input: StartAgentTurnInput): Promise<AgentTurn> {
|
|
313
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
314
|
+
const agent = await this.agentFor(owner.agentId);
|
|
315
|
+
return agent.startTurn({
|
|
316
|
+
...input,
|
|
317
|
+
providerSessionId: owner.rawId,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async interruptTurn(input: InterruptAgentTurnInput): Promise<AgentTurn | null> {
|
|
322
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
323
|
+
const agent = await this.agentFor(owner.agentId);
|
|
324
|
+
return agent.interruptTurn({
|
|
325
|
+
...input,
|
|
326
|
+
providerSessionId: owner.rawId,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
mapProviderRequest(
|
|
331
|
+
request: AgentProviderRequest,
|
|
332
|
+
options: { approvalMode: 'yolo' | 'guarded' },
|
|
333
|
+
): AgentProviderRequestMapping | null {
|
|
334
|
+
const owner = decodeScopedId(request.id);
|
|
335
|
+
const agent = owner ? this.agents.get(owner.agentId) : null;
|
|
336
|
+
if (!owner || !agent) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const params = request.params && typeof request.params === 'object'
|
|
340
|
+
? {
|
|
341
|
+
...(request.params as Record<string, unknown>),
|
|
342
|
+
sessionId: owner.rawId,
|
|
343
|
+
}
|
|
344
|
+
: request.params;
|
|
345
|
+
const mapping = agent.mapProviderRequest({
|
|
346
|
+
...request,
|
|
347
|
+
id: owner.rawId,
|
|
348
|
+
params,
|
|
349
|
+
}, options);
|
|
350
|
+
if (!mapping) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const providerRequestId = encodeSessionId(owner.agentId, String(mapping.providerRequestId));
|
|
354
|
+
return {
|
|
355
|
+
...mapping,
|
|
356
|
+
providerRequestId,
|
|
357
|
+
providerSessionId: encodeSessionId(owner.agentId, mapping.providerSessionId),
|
|
358
|
+
pendingRequest: mapping.pendingRequest
|
|
359
|
+
? {
|
|
360
|
+
...mapping.pendingRequest,
|
|
361
|
+
providerRequestId,
|
|
362
|
+
request: {
|
|
363
|
+
...mapping.pendingRequest.request,
|
|
364
|
+
id: `acp-permission:${providerRequestId}`,
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
: null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
buildProviderRequestResponse(
|
|
372
|
+
pending: AgentPendingProviderRequest,
|
|
373
|
+
input: AgentActionRequestResponseInput,
|
|
374
|
+
) {
|
|
375
|
+
const owner = decodeScopedId(pending.providerRequestId);
|
|
376
|
+
const agent = owner ? this.agents.get(owner.agentId) : null;
|
|
377
|
+
if (!owner || !agent) {
|
|
378
|
+
throw new Error('ACP permission request owner is no longer available.');
|
|
379
|
+
}
|
|
380
|
+
return agent.buildProviderRequestResponse({
|
|
381
|
+
...pending,
|
|
382
|
+
providerRequestId: owner.rawId,
|
|
383
|
+
}, input);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
respondToProviderRequest(id: string | number, result: unknown) {
|
|
387
|
+
const owner = decodeScopedId(id);
|
|
388
|
+
const agent = owner ? this.agents.get(owner.agentId) : null;
|
|
389
|
+
agent?.respondToProviderRequest(owner!.rawId, result);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async refreshCatalog(force = false) {
|
|
393
|
+
const entries = await this.catalog.list({ force });
|
|
394
|
+
const ready = entries.filter((entry) => entry.availability === 'ready');
|
|
395
|
+
const baseInstalled = entries.filter((entry) => entry.availability !== 'base_missing');
|
|
396
|
+
this.installation.installed = baseInstalled.length > 0;
|
|
397
|
+
this.installation.installedVersion = ready.length > 0
|
|
398
|
+
? `${ready.length} ACP agent${ready.length === 1 ? '' : 's'} ready`
|
|
399
|
+
: null;
|
|
400
|
+
this.installation.lastError = ready.length > 0
|
|
401
|
+
? null
|
|
402
|
+
: baseInstalled.length > 0
|
|
403
|
+
? 'ACP adapters or native ACP servers are not ready.'
|
|
404
|
+
: 'No supported base agent was detected.';
|
|
405
|
+
this.status = {
|
|
406
|
+
...this.status,
|
|
407
|
+
state: ready.length > 0 ? 'ready' : 'degraded',
|
|
408
|
+
lastStartedAt: this.status.lastStartedAt ?? new Date().toISOString(),
|
|
409
|
+
lastError: this.installation.lastError,
|
|
410
|
+
};
|
|
411
|
+
this.emit('status', this.getStatus());
|
|
412
|
+
return entries;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async agentFor(agentId: string) {
|
|
416
|
+
const existing = this.agents.get(agentId);
|
|
417
|
+
if (existing) {
|
|
418
|
+
await existing.start();
|
|
419
|
+
return existing;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const entry = (await this.catalog.list()).find((candidate) => candidate.id === agentId);
|
|
423
|
+
if (!entry) {
|
|
424
|
+
throw new AgentRuntimeError(`Unknown ACP agent: ${agentId}`, 'acp', 'request_failed');
|
|
425
|
+
}
|
|
426
|
+
if (entry.availability !== 'ready') {
|
|
427
|
+
throw new AgentRuntimeError(entry.statusMessage, 'acp', 'provider_unavailable', {
|
|
428
|
+
agentId,
|
|
429
|
+
availability: entry.availability,
|
|
430
|
+
baseProbeCommand: entry.baseProbeCommand,
|
|
431
|
+
serverProbeCommand: entry.serverProbeCommand,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const agent = await this.createAgent(entry);
|
|
436
|
+
this.agents.set(agentId, agent);
|
|
437
|
+
try {
|
|
438
|
+
await agent.start();
|
|
439
|
+
return agent;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.agents.delete(agentId);
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private async createAgent(entry: AcpAgentCatalogEntry) {
|
|
447
|
+
const env = entry.id === 'codex'
|
|
448
|
+
? await loadCodexAcpEnvironment(
|
|
449
|
+
this.options.codexHome,
|
|
450
|
+
process.env,
|
|
451
|
+
entry.baseCommand,
|
|
452
|
+
)
|
|
453
|
+
: undefined;
|
|
454
|
+
const agent = new AcpRuntimeAdapter({
|
|
455
|
+
command: entry.serverCommand,
|
|
456
|
+
...(env ? { env } : {}),
|
|
457
|
+
...(this.options.startupTimeoutMs !== undefined
|
|
458
|
+
? { startupTimeoutMs: this.options.startupTimeoutMs }
|
|
459
|
+
: {}),
|
|
460
|
+
...(this.options.clientInfo !== undefined
|
|
461
|
+
? { clientInfo: this.options.clientInfo }
|
|
462
|
+
: {}),
|
|
463
|
+
});
|
|
464
|
+
agent.on('event', (event: AgentRuntimeEvent) => {
|
|
465
|
+
this.emit('event', {
|
|
466
|
+
...event,
|
|
467
|
+
providerSessionId: encodeSessionId(entry.id, event.providerSessionId),
|
|
468
|
+
} satisfies AgentRuntimeEvent);
|
|
469
|
+
});
|
|
470
|
+
agent.on('provider-request', (request: AgentProviderRequest) => {
|
|
471
|
+
const params = request.params && typeof request.params === 'object'
|
|
472
|
+
? {
|
|
473
|
+
...(request.params as Record<string, unknown>),
|
|
474
|
+
sessionId: encodeSessionId(
|
|
475
|
+
entry.id,
|
|
476
|
+
String((request.params as Record<string, unknown>).sessionId ?? ''),
|
|
477
|
+
),
|
|
478
|
+
}
|
|
479
|
+
: request.params;
|
|
480
|
+
this.emit('provider-request', {
|
|
481
|
+
...request,
|
|
482
|
+
id: encodeSessionId(entry.id, String(request.id)),
|
|
483
|
+
params,
|
|
484
|
+
} satisfies AgentProviderRequest);
|
|
485
|
+
});
|
|
486
|
+
agent.on('stderr', (message) => this.emit('stderr', `[${entry.id}] ${String(message)}`));
|
|
487
|
+
agent.on('warning', (message) => this.emit('warning', `[${entry.id}] ${String(message)}`));
|
|
488
|
+
return agent;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private requireSessionOwner(providerSessionId: string) {
|
|
492
|
+
const owner = decodeScopedId(providerSessionId);
|
|
493
|
+
if (!owner || !this.catalog.definition(owner.agentId)) {
|
|
494
|
+
throw new AgentRuntimeError(
|
|
495
|
+
`ACP session is missing its agent scope: ${providerSessionId}`,
|
|
496
|
+
'acp',
|
|
497
|
+
'invalid_response',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return owner;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const acpSessionId = {
|
|
505
|
+
encode: encodeSessionId,
|
|
506
|
+
decode: decodeScopedId,
|
|
507
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { loadCodexAcpEnvironment } from './codex-environment';
|
|
8
|
+
|
|
9
|
+
describe('loadCodexAcpEnvironment', () => {
|
|
10
|
+
it('passes the parsed user config to codex-acp without hard-coding context limits', async () => {
|
|
11
|
+
const codexHome = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-acp-home-'));
|
|
12
|
+
try {
|
|
13
|
+
await fs.writeFile(
|
|
14
|
+
path.join(codexHome, 'config.toml'),
|
|
15
|
+
[
|
|
16
|
+
'model = "gpt-5.6-sol"',
|
|
17
|
+
'model_context_window = 1000000',
|
|
18
|
+
'model_auto_compact_token_limit = 900000',
|
|
19
|
+
'',
|
|
20
|
+
'[features]',
|
|
21
|
+
'goals = true',
|
|
22
|
+
].join('\n'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const env = await loadCodexAcpEnvironment(codexHome, {});
|
|
26
|
+
expect(env?.CODEX_PATH).toBe('codex');
|
|
27
|
+
expect(env?.CODEX_HOME).toBe(codexHome);
|
|
28
|
+
expect(JSON.parse(env?.CODEX_CONFIG ?? '{}')).toEqual({
|
|
29
|
+
model: 'gpt-5.6-sol',
|
|
30
|
+
model_context_window: 1_000_000,
|
|
31
|
+
model_auto_compact_token_limit: 900_000,
|
|
32
|
+
features: { goals: true },
|
|
33
|
+
});
|
|
34
|
+
} finally {
|
|
35
|
+
await fs.rm(codexHome, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('preserves an explicitly supplied CODEX_CONFIG', async () => {
|
|
40
|
+
const env = await loadCodexAcpEnvironment('/tmp/codex-home', {
|
|
41
|
+
CODEX_CONFIG: '{"model_context_window":516000}',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(env).toEqual({
|
|
45
|
+
CODEX_PATH: 'codex',
|
|
46
|
+
CODEX_HOME: '/tmp/codex-home',
|
|
47
|
+
CODEX_CONFIG: '{"model_context_window":516000}',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('prefers an explicitly configured Codex executable', async () => {
|
|
52
|
+
const env = await loadCodexAcpEnvironment(null, {
|
|
53
|
+
CODEX_PATH: '/opt/codex/bin/codex',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(env).toEqual({ CODEX_PATH: '/opt/codex/bin/codex' });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { parse } from 'smol-toml';
|
|
5
|
+
|
|
6
|
+
export async function loadCodexAcpEnvironment(
|
|
7
|
+
codexHome: string | null | undefined,
|
|
8
|
+
inheritedEnv: NodeJS.ProcessEnv = process.env,
|
|
9
|
+
codexPath: string | null | undefined = 'codex',
|
|
10
|
+
): Promise<NodeJS.ProcessEnv | undefined> {
|
|
11
|
+
const normalizedHome = codexHome?.trim();
|
|
12
|
+
const normalizedPath = inheritedEnv.CODEX_PATH?.trim() || codexPath?.trim();
|
|
13
|
+
const env: NodeJS.ProcessEnv = {};
|
|
14
|
+
if (normalizedPath) {
|
|
15
|
+
env.CODEX_PATH = normalizedPath;
|
|
16
|
+
}
|
|
17
|
+
if (!normalizedHome) {
|
|
18
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
19
|
+
}
|
|
20
|
+
env.CODEX_HOME = normalizedHome;
|
|
21
|
+
if (inheritedEnv.CODEX_CONFIG) {
|
|
22
|
+
env.CODEX_CONFIG = inheritedEnv.CODEX_CONFIG;
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const source = await fs.readFile(path.join(normalizedHome, 'config.toml'), 'utf8');
|
|
28
|
+
env.CODEX_CONFIG = JSON.stringify(parse(source));
|
|
29
|
+
} catch {
|
|
30
|
+
// Codex can still start with its own defaults when config is absent or invalid.
|
|
31
|
+
}
|
|
32
|
+
return env;
|
|
33
|
+
}
|