remote-codex 0.11.49 → 0.11.51
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 +38 -5
- package/apps/supervisor-api/dist/index.js +2137 -422
- package/apps/supervisor-web/dist/assets/index-7LiZQ3aJ.js +22 -0
- package/apps/supervisor-web/dist/assets/{index-Dy8PgXgw.css → index-Bg8FdhS1.css} +1 -1
- package/apps/supervisor-web/dist/assets/{thread-ui-gcslNXur.js → thread-ui-BbtcUwps.js} +25 -19
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +4 -2
- package/package.json +7 -1
- package/packages/acp/src/agent-catalog.test.ts +25 -0
- package/packages/acp/src/agent-catalog.ts +3 -1
- package/packages/acp/src/capabilities.ts +139 -0
- package/packages/acp/src/capability-parity.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.test.ts +200 -6
- package/packages/acp/src/catalog-runtime.ts +239 -31
- package/packages/acp/src/extension-registry.test.ts +198 -0
- package/packages/acp/src/extension-registry.ts +285 -0
- package/packages/acp/src/extensions.test.ts +45 -0
- package/packages/acp/src/extensions.ts +103 -0
- package/packages/acp/src/harness-contract.test.ts +81 -0
- package/packages/acp/src/harness-contract.ts +62 -0
- package/packages/acp/src/index.ts +7 -0
- package/packages/acp/src/item-mapper.ts +28 -2
- package/packages/acp/src/prompt-content.test.ts +90 -0
- package/packages/acp/src/prompt-content.ts +99 -0
- package/packages/acp/src/runtimeAdapter.test.ts +458 -5
- package/packages/acp/src/runtimeAdapter.ts +791 -60
- package/packages/acp/src/session-hydrator.test.ts +135 -0
- package/packages/acp/src/session-hydrator.ts +147 -0
- package/packages/acp/src/terminal-service.test.ts +21 -2
- package/packages/acp/src/terminal-service.ts +23 -3
- package/packages/acp/src/test/fixtures/fake-acp-agent.mjs +514 -0
- package/packages/acp/src/workspace-boundary.test.ts +32 -0
- package/packages/acp/src/workspace-boundary.ts +47 -0
- package/packages/agent-runtime/src/types.ts +61 -1
- package/packages/db/src/repositories.ts +3 -2
- package/packages/shared/src/index.ts +32 -1
- package/scripts/service-manager.mjs +13 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +0 -22
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type InterruptAgentTurnInput,
|
|
19
19
|
type ReadAgentSessionOptions,
|
|
20
20
|
type ResumeAgentSessionInput,
|
|
21
|
+
type SendAgentInputInput,
|
|
22
|
+
type SetAgentGoalInput,
|
|
21
23
|
type StartAgentSessionInput,
|
|
22
24
|
type StartAgentSessionResult,
|
|
23
25
|
type StartAgentTurnInput,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
acpAgentMetadata,
|
|
29
31
|
type AcpAgentCatalogEntry,
|
|
30
32
|
} from './agent-catalog';
|
|
33
|
+
import { snapshotAcpAgentCapabilities } from './capabilities';
|
|
31
34
|
import { AcpRuntimeAdapter } from './runtimeAdapter';
|
|
32
35
|
import { loadCodexAcpEnvironment } from './codex-environment';
|
|
33
36
|
|
|
@@ -90,7 +93,12 @@ const catalogCapabilities: AgentProviderCapabilities = {
|
|
|
90
93
|
|
|
91
94
|
const catalogManagementSchema: AgentRuntimeManagementSchema = {
|
|
92
95
|
hostConfigFiles: [],
|
|
93
|
-
toolboxItems: [
|
|
96
|
+
toolboxItems: [
|
|
97
|
+
{ action: 'fast', command: '/fast', label: 'Fast mode' },
|
|
98
|
+
{ action: 'compact', command: '/compact', label: 'Compact context' },
|
|
99
|
+
{ action: 'goal', command: '/goal', label: 'Goal' },
|
|
100
|
+
{ action: 'fork', command: '/fork', label: 'Fork' },
|
|
101
|
+
],
|
|
94
102
|
hookCommandTemplates: [],
|
|
95
103
|
providerConfigFormat: 'none',
|
|
96
104
|
mcpConfigFormat: 'none',
|
|
@@ -117,6 +125,7 @@ function decodeScopedId(value: string | number) {
|
|
|
117
125
|
function scopedSession(agentId: string, session: AgentSessionDetail): AgentSessionDetail {
|
|
118
126
|
return {
|
|
119
127
|
...session,
|
|
128
|
+
agentId,
|
|
120
129
|
providerSessionId: encodeSessionId(agentId, session.providerSessionId),
|
|
121
130
|
};
|
|
122
131
|
}
|
|
@@ -124,6 +133,7 @@ function scopedSession(agentId: string, session: AgentSessionDetail): AgentSessi
|
|
|
124
133
|
function scopedSummary(agentId: string, session: AgentSessionSummary): AgentSessionSummary {
|
|
125
134
|
return {
|
|
126
135
|
...session,
|
|
136
|
+
agentId,
|
|
127
137
|
providerSessionId: encodeSessionId(agentId, session.providerSessionId),
|
|
128
138
|
};
|
|
129
139
|
}
|
|
@@ -142,8 +152,8 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
142
152
|
readonly managementSchema = catalogManagementSchema;
|
|
143
153
|
readonly installation: AgentBackendInstallationDto = {
|
|
144
154
|
packageName: null,
|
|
145
|
-
installed:
|
|
146
|
-
installedVersion:
|
|
155
|
+
installed: true,
|
|
156
|
+
installedVersion: 'Built in · 0 ACP agents ready',
|
|
147
157
|
latestVersion: null,
|
|
148
158
|
installCommand: null,
|
|
149
159
|
updateCommand: null,
|
|
@@ -154,6 +164,11 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
154
164
|
private readonly catalog: AcpAgentCatalog;
|
|
155
165
|
private readonly agents = new Map<string, AcpRuntimeAdapter>();
|
|
156
166
|
private readonly modelCache = new Map<string, { at: number; models: AgentModel[] }>();
|
|
167
|
+
private readonly operationalMetrics = {
|
|
168
|
+
sessionStartFailures: 0,
|
|
169
|
+
resumeFailures: 0,
|
|
170
|
+
capabilityProbeFailures: 0,
|
|
171
|
+
};
|
|
157
172
|
private status: AgentRuntimeStatus = {
|
|
158
173
|
state: 'stopped',
|
|
159
174
|
transport: 'stdio',
|
|
@@ -172,7 +187,10 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
getStatus() {
|
|
175
|
-
return {
|
|
190
|
+
return {
|
|
191
|
+
...this.status,
|
|
192
|
+
operationalMetrics: { ...this.operationalMetrics },
|
|
193
|
+
};
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
async start() {
|
|
@@ -189,6 +207,7 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
189
207
|
async stop() {
|
|
190
208
|
await Promise.allSettled([...this.agents.values()].map((agent) => agent.stop()));
|
|
191
209
|
this.agents.clear();
|
|
210
|
+
this.recomputeAgentCapabilities();
|
|
192
211
|
this.status = {
|
|
193
212
|
...this.status,
|
|
194
213
|
state: 'stopped',
|
|
@@ -219,21 +238,65 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
219
238
|
}
|
|
220
239
|
|
|
221
240
|
async listModelsForAgent(agentId: string, cwd: string) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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;
|
|
241
|
+
try {
|
|
242
|
+
const cacheKey = `${agentId}\0${cwd}`;
|
|
243
|
+
const cached = this.modelCache.get(cacheKey);
|
|
244
|
+
if (cached && Date.now() - cached.at < 30_000) {
|
|
245
|
+
return cached.models;
|
|
233
246
|
}
|
|
247
|
+
const agent = await this.agentFor(agentId);
|
|
248
|
+
let models = await agent.inspectModelOptions(cwd);
|
|
249
|
+
if (agent.capabilities.controls.performanceMode) {
|
|
250
|
+
models = models.map((model) => ({
|
|
251
|
+
...model,
|
|
252
|
+
supportsPerformanceMode: true,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
if (models.length === 1 && models[0]?.model === 'default') {
|
|
256
|
+
const commandModels = await this.catalog.listCommandModels(agentId);
|
|
257
|
+
if (commandModels.length > 0) {
|
|
258
|
+
models = commandModels;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.modelCache.set(cacheKey, { at: Date.now(), models });
|
|
262
|
+
return models;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.operationalMetrics.capabilityProbeFailures += 1;
|
|
265
|
+
throw error;
|
|
234
266
|
}
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async getAgentCapabilitySnapshot(agentId: string) {
|
|
270
|
+
const entry = (await this.refreshCatalog()).find((candidate) => candidate.id === agentId);
|
|
271
|
+
if (!entry) {
|
|
272
|
+
throw new AgentRuntimeError(`Unknown ACP agent: ${agentId}`, 'acp', 'request_failed');
|
|
273
|
+
}
|
|
274
|
+
if (entry.availability !== 'ready') {
|
|
275
|
+
return snapshotAcpAgentCapabilities({
|
|
276
|
+
agentId,
|
|
277
|
+
availability: entry.availability,
|
|
278
|
+
effectiveCapabilities: this.capabilities,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const agent = await this.agentFor(agentId);
|
|
282
|
+
return snapshotAcpAgentCapabilities({
|
|
283
|
+
agentId,
|
|
284
|
+
availability: entry.availability,
|
|
285
|
+
negotiated: agent.getProtocolSnapshot(),
|
|
286
|
+
effectiveCapabilities: agent.capabilities,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
getScopedCapabilities(input: {
|
|
291
|
+
agentId?: string | null;
|
|
292
|
+
providerSessionId?: string | null;
|
|
293
|
+
}) {
|
|
294
|
+
const sessionOwner = input.providerSessionId
|
|
295
|
+
? decodeScopedId(input.providerSessionId)
|
|
296
|
+
: null;
|
|
297
|
+
const agentId = sessionOwner?.agentId ?? input.agentId ?? null;
|
|
298
|
+
const child = agentId ? this.agents.get(agentId) : null;
|
|
299
|
+
return structuredClone(child?.capabilities ?? catalogCapabilities);
|
|
237
300
|
}
|
|
238
301
|
|
|
239
302
|
async installModel(modelId: string) {
|
|
@@ -250,6 +313,27 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
250
313
|
return sessions.flat();
|
|
251
314
|
}
|
|
252
315
|
|
|
316
|
+
async listImportSessions(agentId?: string | null) {
|
|
317
|
+
const entries = agentId
|
|
318
|
+
? (await this.refreshCatalog()).filter((entry) => entry.id === agentId)
|
|
319
|
+
: [];
|
|
320
|
+
const sessions = await Promise.all(
|
|
321
|
+
entries.map(async (entry) => {
|
|
322
|
+
if (entry.availability !== 'ready') {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const agent = await this.agentFor(entry.id);
|
|
327
|
+
return (await agent.listSessions()).map((session) =>
|
|
328
|
+
scopedSummary(entry.id, session));
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
return sessions.flat();
|
|
335
|
+
}
|
|
336
|
+
|
|
253
337
|
async listLoadedSessions() {
|
|
254
338
|
const sessions = await Promise.all(
|
|
255
339
|
[...this.agents.entries()].map(async ([agentId, agent]) =>
|
|
@@ -279,14 +363,30 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
279
363
|
if (!agentId) {
|
|
280
364
|
throw new AgentRuntimeError('Select an ACP agent before creating the thread.', 'acp');
|
|
281
365
|
}
|
|
282
|
-
|
|
283
|
-
|
|
366
|
+
let agent: AcpRuntimeAdapter;
|
|
367
|
+
let response: StartAgentSessionResult;
|
|
368
|
+
try {
|
|
369
|
+
agent = await this.agentFor(agentId);
|
|
370
|
+
response = await agent.startSession(delegatedSessionInput(input));
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.operationalMetrics.sessionStartFailures += 1;
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
const probedDefaultModel = this.modelCache
|
|
376
|
+
.get(`${agentId}\0${input.cwd}`)
|
|
377
|
+
?.models.find((model) => model.isDefault)?.model ?? null;
|
|
378
|
+
const resolvedModel = response.model && response.model !== 'default'
|
|
379
|
+
? response.model
|
|
380
|
+
: input.model !== 'default'
|
|
381
|
+
? input.model
|
|
382
|
+
: probedDefaultModel ?? input.model;
|
|
383
|
+
this.refreshAgentSessionCapabilities(agentId, agent);
|
|
284
384
|
const session = scopedSession(agentId, response.session);
|
|
285
385
|
return {
|
|
286
386
|
...response,
|
|
287
387
|
agentId,
|
|
288
388
|
providerSessionId: session.providerSessionId,
|
|
289
|
-
model:
|
|
389
|
+
model: resolvedModel,
|
|
290
390
|
reasoningEffort: response.reasoningEffort ?? input.reasoningEffort ?? null,
|
|
291
391
|
session,
|
|
292
392
|
};
|
|
@@ -294,11 +394,19 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
294
394
|
|
|
295
395
|
async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
296
396
|
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
397
|
+
let agent: AcpRuntimeAdapter;
|
|
398
|
+
let response: StartAgentSessionResult;
|
|
399
|
+
try {
|
|
400
|
+
agent = await this.agentFor(owner.agentId);
|
|
401
|
+
response = await agent.resumeSession({
|
|
402
|
+
...input,
|
|
403
|
+
providerSessionId: owner.rawId,
|
|
404
|
+
});
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.operationalMetrics.resumeFailures += 1;
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
this.refreshAgentSessionCapabilities(owner.agentId, agent);
|
|
302
410
|
const session = scopedSession(owner.agentId, response.session);
|
|
303
411
|
return {
|
|
304
412
|
...response,
|
|
@@ -327,6 +435,55 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
327
435
|
});
|
|
328
436
|
}
|
|
329
437
|
|
|
438
|
+
async sendInput(input: SendAgentInputInput) {
|
|
439
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
440
|
+
const agent = await this.agentFor(owner.agentId);
|
|
441
|
+
return agent.sendInput({
|
|
442
|
+
...input,
|
|
443
|
+
providerSessionId: owner.rawId,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async compactSession(providerSessionId: string) {
|
|
448
|
+
const owner = this.requireSessionOwner(providerSessionId);
|
|
449
|
+
const agent = await this.agentFor(owner.agentId);
|
|
450
|
+
return agent.compactSession(owner.rawId);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async forkSession(input: { providerSessionId: string; atTurnId?: string | null }) {
|
|
454
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
455
|
+
const agent = await this.agentFor(owner.agentId);
|
|
456
|
+
return scopedSession(owner.agentId, await agent.forkSession({
|
|
457
|
+
...input,
|
|
458
|
+
providerSessionId: owner.rawId,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async getGoal(providerSessionId: string) {
|
|
463
|
+
const owner = this.requireSessionOwner(providerSessionId);
|
|
464
|
+
const agent = await this.agentFor(owner.agentId);
|
|
465
|
+
return agent.getGoal(owner.rawId);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async setGoal(input: SetAgentGoalInput) {
|
|
469
|
+
const owner = this.requireSessionOwner(input.providerSessionId);
|
|
470
|
+
const agent = await this.agentFor(owner.agentId);
|
|
471
|
+
const goal = await agent.setGoal({
|
|
472
|
+
...input,
|
|
473
|
+
providerSessionId: owner.rawId,
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
...goal,
|
|
477
|
+
providerSessionId: encodeSessionId(owner.agentId, goal.providerSessionId),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async clearGoal(providerSessionId: string) {
|
|
482
|
+
const owner = this.requireSessionOwner(providerSessionId);
|
|
483
|
+
const agent = await this.agentFor(owner.agentId);
|
|
484
|
+
return agent.clearGoal(owner.rawId);
|
|
485
|
+
}
|
|
486
|
+
|
|
330
487
|
mapProviderRequest(
|
|
331
488
|
request: AgentProviderRequest,
|
|
332
489
|
options: { approvalMode: 'yolo' | 'guarded' },
|
|
@@ -336,10 +493,17 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
336
493
|
if (!owner || !agent) {
|
|
337
494
|
return null;
|
|
338
495
|
}
|
|
496
|
+
const scopedProviderSessionId = request.params && typeof request.params === 'object'
|
|
497
|
+
? String((request.params as Record<string, unknown>).sessionId ?? '')
|
|
498
|
+
: '';
|
|
499
|
+
const sessionOwner = decodeScopedId(scopedProviderSessionId);
|
|
500
|
+
if (!sessionOwner || sessionOwner.agentId !== owner.agentId || !sessionOwner.rawId) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
339
503
|
const params = request.params && typeof request.params === 'object'
|
|
340
504
|
? {
|
|
341
505
|
...(request.params as Record<string, unknown>),
|
|
342
|
-
sessionId:
|
|
506
|
+
sessionId: sessionOwner.rawId,
|
|
343
507
|
}
|
|
344
508
|
: request.params;
|
|
345
509
|
const mapping = agent.mapProviderRequest({
|
|
@@ -393,10 +557,9 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
393
557
|
const entries = await this.catalog.list({ force });
|
|
394
558
|
const ready = entries.filter((entry) => entry.availability === 'ready');
|
|
395
559
|
const baseInstalled = entries.filter((entry) => entry.availability !== 'base_missing');
|
|
396
|
-
this.installation.installed =
|
|
397
|
-
this.installation.installedVersion =
|
|
398
|
-
|
|
399
|
-
: null;
|
|
560
|
+
this.installation.installed = true;
|
|
561
|
+
this.installation.installedVersion =
|
|
562
|
+
`Built in · ${ready.length} ACP agent${ready.length === 1 ? '' : 's'} ready`;
|
|
400
563
|
this.installation.lastError = ready.length > 0
|
|
401
564
|
? null
|
|
402
565
|
: baseInstalled.length > 0
|
|
@@ -416,6 +579,7 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
416
579
|
const existing = this.agents.get(agentId);
|
|
417
580
|
if (existing) {
|
|
418
581
|
await existing.start();
|
|
582
|
+
this.mergeAgentCapabilities(existing);
|
|
419
583
|
return existing;
|
|
420
584
|
}
|
|
421
585
|
|
|
@@ -436,6 +600,7 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
436
600
|
this.agents.set(agentId, agent);
|
|
437
601
|
try {
|
|
438
602
|
await agent.start();
|
|
603
|
+
this.mergeAgentCapabilities(agent);
|
|
439
604
|
return agent;
|
|
440
605
|
} catch (error) {
|
|
441
606
|
this.agents.delete(agentId);
|
|
@@ -443,6 +608,40 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
443
608
|
}
|
|
444
609
|
}
|
|
445
610
|
|
|
611
|
+
private mergeAgentCapabilities(agent: AcpRuntimeAdapter) {
|
|
612
|
+
void agent;
|
|
613
|
+
this.recomputeAgentCapabilities();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private refreshAgentSessionCapabilities(
|
|
617
|
+
agentId: string,
|
|
618
|
+
agent: AcpRuntimeAdapter,
|
|
619
|
+
) {
|
|
620
|
+
for (const key of this.modelCache.keys()) {
|
|
621
|
+
if (key.startsWith(`${agentId}\0`)) {
|
|
622
|
+
this.modelCache.delete(key);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
this.mergeAgentCapabilities(agent);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private recomputeAgentCapabilities() {
|
|
629
|
+
const next = structuredClone(catalogCapabilities);
|
|
630
|
+
for (const child of this.agents.values()) {
|
|
631
|
+
next.turns.steer ||= child.capabilities.turns.steer;
|
|
632
|
+
next.turns.compact ||= child.capabilities.turns.compact;
|
|
633
|
+
next.branching.fork ||= child.capabilities.branching.fork;
|
|
634
|
+
next.controls.goals ||= child.capabilities.controls.goals;
|
|
635
|
+
next.controls.performanceMode ||= child.capabilities.controls.performanceMode;
|
|
636
|
+
next.management.mcpStatus ||= child.capabilities.management.mcpStatus;
|
|
637
|
+
next.management.skills ||= child.capabilities.management.skills;
|
|
638
|
+
next.management.hooks ||= child.capabilities.management.hooks;
|
|
639
|
+
}
|
|
640
|
+
for (const section of Object.keys(next) as Array<keyof AgentProviderCapabilities>) {
|
|
641
|
+
Object.assign(this.capabilities[section], next[section]);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
446
645
|
private async createAgent(entry: AcpAgentCatalogEntry) {
|
|
447
646
|
const env = entry.id === 'codex'
|
|
448
647
|
? await loadCodexAcpEnvironment(
|
|
@@ -462,9 +661,18 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
|
|
|
462
661
|
: {}),
|
|
463
662
|
});
|
|
464
663
|
agent.on('event', (event: AgentRuntimeEvent) => {
|
|
664
|
+
const providerSessionId = encodeSessionId(entry.id, event.providerSessionId);
|
|
465
665
|
this.emit('event', {
|
|
466
666
|
...event,
|
|
467
|
-
providerSessionId
|
|
667
|
+
providerSessionId,
|
|
668
|
+
...(event.type === 'goal.updated'
|
|
669
|
+
? {
|
|
670
|
+
goal: {
|
|
671
|
+
...event.goal,
|
|
672
|
+
providerSessionId,
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
: {}),
|
|
468
676
|
} satisfies AgentRuntimeEvent);
|
|
469
677
|
});
|
|
470
678
|
agent.on('provider-request', (request: AgentProviderRequest) => {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
|
|
5
|
+
type HarnessExtensionEventEnvelope,
|
|
6
|
+
} from './extensions';
|
|
7
|
+
import {
|
|
8
|
+
HarnessExtensionInvocationError,
|
|
9
|
+
HarnessExtensionRegistry,
|
|
10
|
+
} from './extension-registry';
|
|
11
|
+
|
|
12
|
+
const descriptor = {
|
|
13
|
+
id: 'fixture.session',
|
|
14
|
+
version: 1,
|
|
15
|
+
stability: 'experimental' as const,
|
|
16
|
+
methods: ['compact'],
|
|
17
|
+
events: ['checkpoint'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('HarnessExtensionRegistry', () => {
|
|
21
|
+
it('enforces one owner and deduplicates idempotent calls', async () => {
|
|
22
|
+
const registry = new HarnessExtensionRegistry();
|
|
23
|
+
const request = vi.fn(async () => ({ compacted: true }));
|
|
24
|
+
registry.register({
|
|
25
|
+
ownerId: 'owner-1',
|
|
26
|
+
descriptor,
|
|
27
|
+
transport: { request },
|
|
28
|
+
capabilityPatch: {
|
|
29
|
+
turns: { compact: true },
|
|
30
|
+
controls: { goals: true },
|
|
31
|
+
},
|
|
32
|
+
paramMappers: {
|
|
33
|
+
compact: (envelope) => ({
|
|
34
|
+
sessionId: (envelope.params as { providerSessionId: string }).providerSessionId,
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(() => registry.register({
|
|
39
|
+
ownerId: 'owner-2',
|
|
40
|
+
descriptor,
|
|
41
|
+
transport: { request },
|
|
42
|
+
})).toThrow(/already owned/);
|
|
43
|
+
const input = {
|
|
44
|
+
extensionId: 'fixture.session',
|
|
45
|
+
extensionVersion: 1,
|
|
46
|
+
method: 'compact',
|
|
47
|
+
operationId: 'operation-1',
|
|
48
|
+
idempotencyKey: 'thread-1:compact:1',
|
|
49
|
+
params: { providerSessionId: 'session-1' },
|
|
50
|
+
};
|
|
51
|
+
const [first, second] = await Promise.all([
|
|
52
|
+
registry.invoke(input),
|
|
53
|
+
registry.invoke(input),
|
|
54
|
+
]);
|
|
55
|
+
expect(first).toEqual({ compacted: true });
|
|
56
|
+
expect(second).toEqual(first);
|
|
57
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(request).toHaveBeenCalledWith(
|
|
59
|
+
'remoteCodex/fixture.session/v1/compact',
|
|
60
|
+
{ sessionId: 'session-1' },
|
|
61
|
+
expect.any(AbortSignal),
|
|
62
|
+
);
|
|
63
|
+
expect(registry.effectiveCapabilities({
|
|
64
|
+
sessions: { list: true, read: true, resume: true, importLocal: false },
|
|
65
|
+
turns: { start: true, streamInput: false, steer: false, interrupt: true, compact: false },
|
|
66
|
+
branching: { fork: false, hardRollback: false, resumeAt: false, rewindFiles: false },
|
|
67
|
+
controls: {
|
|
68
|
+
planMode: true,
|
|
69
|
+
permissionRequests: true,
|
|
70
|
+
sandboxMode: true,
|
|
71
|
+
performanceMode: false,
|
|
72
|
+
goals: false,
|
|
73
|
+
},
|
|
74
|
+
management: {
|
|
75
|
+
models: false,
|
|
76
|
+
mcpStatus: false,
|
|
77
|
+
skills: false,
|
|
78
|
+
hooks: false,
|
|
79
|
+
hookTrust: false,
|
|
80
|
+
hostConfigFiles: false,
|
|
81
|
+
providerSettings: false,
|
|
82
|
+
},
|
|
83
|
+
usage: { contextWindow: true, tokenUsage: true, costUsd: false },
|
|
84
|
+
})).toMatchObject({
|
|
85
|
+
turns: { compact: true },
|
|
86
|
+
controls: { goals: true },
|
|
87
|
+
});
|
|
88
|
+
await expect(registry.invoke({
|
|
89
|
+
...input,
|
|
90
|
+
params: { providerSessionId: 'session-2' },
|
|
91
|
+
})).rejects.toMatchObject({
|
|
92
|
+
payload: { code: 'idempotency_conflict', retryable: false },
|
|
93
|
+
});
|
|
94
|
+
await expect(registry.invoke({
|
|
95
|
+
...input,
|
|
96
|
+
extensionVersion: 2,
|
|
97
|
+
idempotencyKey: 'thread-1:compact:v2',
|
|
98
|
+
})).rejects.toMatchObject({
|
|
99
|
+
payload: { code: 'extension_method_unavailable', retryable: false },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('times out, aborts transport, and permits an explicit retry', async () => {
|
|
104
|
+
const registry = new HarnessExtensionRegistry();
|
|
105
|
+
let calls = 0;
|
|
106
|
+
registry.register({
|
|
107
|
+
ownerId: 'owner-1',
|
|
108
|
+
descriptor,
|
|
109
|
+
transport: {
|
|
110
|
+
request: async (_method, _params, signal) => {
|
|
111
|
+
calls += 1;
|
|
112
|
+
await new Promise((_resolve, reject) =>
|
|
113
|
+
signal.addEventListener('abort', () => reject(signal.reason), { once: true }));
|
|
114
|
+
return {};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const input = {
|
|
119
|
+
extensionId: 'fixture.session',
|
|
120
|
+
extensionVersion: 1,
|
|
121
|
+
method: 'compact',
|
|
122
|
+
operationId: 'operation-timeout',
|
|
123
|
+
idempotencyKey: 'thread-1:compact:timeout',
|
|
124
|
+
params: {},
|
|
125
|
+
timeoutMs: 5,
|
|
126
|
+
};
|
|
127
|
+
await expect(registry.invoke(input)).rejects.toBeInstanceOf(
|
|
128
|
+
HarnessExtensionInvocationError,
|
|
129
|
+
);
|
|
130
|
+
await expect(registry.invoke(input)).rejects.toMatchObject({
|
|
131
|
+
payload: { code: 'extension_timeout', retryable: true },
|
|
132
|
+
});
|
|
133
|
+
expect(calls).toBe(2);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('cancels promptly even when transport ignores abort and does not dispatch pre-aborted calls', async () => {
|
|
137
|
+
const registry = new HarnessExtensionRegistry();
|
|
138
|
+
const request = vi.fn(async () => new Promise<never>(() => {}));
|
|
139
|
+
registry.register({
|
|
140
|
+
ownerId: 'owner-1',
|
|
141
|
+
descriptor,
|
|
142
|
+
transport: { request },
|
|
143
|
+
});
|
|
144
|
+
const preAborted = new AbortController();
|
|
145
|
+
preAborted.abort(new Error('already cancelled'));
|
|
146
|
+
const input = {
|
|
147
|
+
extensionId: 'fixture.session',
|
|
148
|
+
extensionVersion: 1,
|
|
149
|
+
method: 'compact',
|
|
150
|
+
operationId: 'operation-cancel',
|
|
151
|
+
idempotencyKey: 'thread-1:compact:cancel',
|
|
152
|
+
params: {},
|
|
153
|
+
timeoutMs: 10_000,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await expect(registry.invoke({
|
|
157
|
+
...input,
|
|
158
|
+
signal: preAborted.signal,
|
|
159
|
+
})).rejects.toMatchObject({
|
|
160
|
+
payload: { code: 'extension_cancelled', retryable: true },
|
|
161
|
+
});
|
|
162
|
+
expect(request).not.toHaveBeenCalled();
|
|
163
|
+
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const pending = registry.invoke({ ...input, signal: controller.signal });
|
|
166
|
+
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
167
|
+
controller.abort(new Error('cancel now'));
|
|
168
|
+
await expect(pending).rejects.toMatchObject({
|
|
169
|
+
payload: { code: 'extension_cancelled', retryable: true },
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('validates and deduplicates declared extension events', () => {
|
|
174
|
+
const registry = new HarnessExtensionRegistry();
|
|
175
|
+
registry.register({
|
|
176
|
+
ownerId: 'owner-1',
|
|
177
|
+
descriptor,
|
|
178
|
+
transport: { request: async () => ({}) },
|
|
179
|
+
});
|
|
180
|
+
const events: HarnessExtensionEventEnvelope[] = [];
|
|
181
|
+
registry.on('event', (event) => events.push(event));
|
|
182
|
+
const event: HarnessExtensionEventEnvelope = {
|
|
183
|
+
protocol: REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
|
|
184
|
+
extensionId: 'fixture.session',
|
|
185
|
+
extensionVersion: 1,
|
|
186
|
+
event: 'checkpoint',
|
|
187
|
+
operationId: 'operation-1',
|
|
188
|
+
providerSessionId: 'session-1',
|
|
189
|
+
providerTurnId: 'turn-1',
|
|
190
|
+
providerItemId: 'checkpoint-1',
|
|
191
|
+
sequence: 1,
|
|
192
|
+
payload: { status: 'completed' },
|
|
193
|
+
};
|
|
194
|
+
expect(registry.handleEvent('owner-1', event)).toBe(true);
|
|
195
|
+
expect(registry.handleEvent('owner-1', event)).toBe(false);
|
|
196
|
+
expect(events).toHaveLength(1);
|
|
197
|
+
});
|
|
198
|
+
});
|