remote-codex 0.11.50 → 0.11.52
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 +31 -0
- package/apps/supervisor-api/dist/index.js +2132 -417
- package/apps/supervisor-web/dist/assets/index-BBq6mr8o.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-C5pxOmDJ.js +3974 -0
- package/apps/supervisor-web/dist/index.html +3 -3
- 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 +165 -6
- package/packages/acp/src/catalog-runtime.ts +234 -25
- 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 +471 -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/codex/src/appServerManager.test.ts +2 -2
- package/packages/db/src/repositories.ts +3 -2
- package/packages/shared/src/index.ts +32 -1
- package/apps/supervisor-web/dist/assets/index-BXypG9kl.js +0 -22
- package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +0 -3968
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
AgentRuntimeError,
|
|
12
12
|
type AgentActionRequestResponseInput,
|
|
13
13
|
type AgentHistoryItem,
|
|
14
|
+
type AgentGoal,
|
|
14
15
|
type AgentModel,
|
|
15
16
|
type AgentPendingProviderRequest,
|
|
16
17
|
type AgentProviderCapabilities,
|
|
@@ -21,10 +22,13 @@ import {
|
|
|
21
22
|
type AgentRuntimeManagementSchema,
|
|
22
23
|
type AgentRuntimeStatus,
|
|
23
24
|
type AgentSessionDetail,
|
|
25
|
+
type AgentSessionHistoryCoverage,
|
|
24
26
|
type AgentSessionSummary,
|
|
25
27
|
type AgentTurn,
|
|
26
28
|
type InterruptAgentTurnInput,
|
|
27
29
|
type ResumeAgentSessionInput,
|
|
30
|
+
type SendAgentInputInput,
|
|
31
|
+
type SetAgentGoalInput,
|
|
28
32
|
type StartAgentSessionInput,
|
|
29
33
|
type StartAgentSessionResult,
|
|
30
34
|
type StartAgentTurnInput,
|
|
@@ -38,7 +42,17 @@ import {
|
|
|
38
42
|
AcpTurnItemMapper,
|
|
39
43
|
type AcpMappedItemUpdate,
|
|
40
44
|
} from './item-mapper';
|
|
45
|
+
import { snapshotAcpInitializeResponse } from './capabilities';
|
|
46
|
+
import { buildAcpPromptContent } from './prompt-content';
|
|
47
|
+
import { HarnessExtensionRegistry } from './extension-registry';
|
|
48
|
+
import {
|
|
49
|
+
REMOTE_CODEX_HARNESS_EXTENSION_EVENT_METHOD,
|
|
50
|
+
type HarnessExtensionCallEnvelope,
|
|
51
|
+
type HarnessExtensionEventEnvelope,
|
|
52
|
+
} from './extensions';
|
|
53
|
+
import { AcpSessionHydrator } from './session-hydrator';
|
|
41
54
|
import { AcpTerminalService } from './terminal-service';
|
|
55
|
+
import { resolveAcpWorkspacePath } from './workspace-boundary';
|
|
42
56
|
|
|
43
57
|
interface AcpRuntimeOptions {
|
|
44
58
|
command: string;
|
|
@@ -66,6 +80,8 @@ interface AcpSessionState {
|
|
|
66
80
|
modes: acp.SessionModeState | null;
|
|
67
81
|
configOptions: acp.SessionConfigOption[];
|
|
68
82
|
availableCommands: acp.AvailableCommand[];
|
|
83
|
+
hydrationCoverage: AgentSessionHistoryCoverage | null;
|
|
84
|
+
goal: AgentGoal | null;
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
interface PendingPermission {
|
|
@@ -74,12 +90,15 @@ interface PendingPermission {
|
|
|
74
90
|
timer: NodeJS.Timeout;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
const acpCapabilities: AgentProviderCapabilities = {
|
|
93
|
+
export const acpCapabilities: AgentProviderCapabilities = {
|
|
78
94
|
sessions: {
|
|
79
|
-
list:
|
|
95
|
+
list: false,
|
|
80
96
|
read: true,
|
|
81
|
-
resume:
|
|
97
|
+
resume: false,
|
|
82
98
|
importLocal: false,
|
|
99
|
+
load: false,
|
|
100
|
+
close: false,
|
|
101
|
+
delete: false,
|
|
83
102
|
},
|
|
84
103
|
turns: {
|
|
85
104
|
start: true,
|
|
@@ -135,6 +154,23 @@ function cloneCapabilities(): AgentProviderCapabilities {
|
|
|
135
154
|
return structuredClone(acpCapabilities);
|
|
136
155
|
}
|
|
137
156
|
|
|
157
|
+
export function applyNegotiatedAcpCapabilities(
|
|
158
|
+
target: AgentProviderCapabilities,
|
|
159
|
+
capabilities: acp.AgentCapabilities | null | undefined,
|
|
160
|
+
) {
|
|
161
|
+
target.sessions.list = Boolean(capabilities?.sessionCapabilities?.list);
|
|
162
|
+
target.sessions.load = capabilities?.loadSession === true;
|
|
163
|
+
target.sessions.resume = Boolean(
|
|
164
|
+
capabilities?.loadSession || capabilities?.sessionCapabilities?.resume,
|
|
165
|
+
);
|
|
166
|
+
target.sessions.close = Boolean(capabilities?.sessionCapabilities?.close);
|
|
167
|
+
target.sessions.delete = Boolean(capabilities?.sessionCapabilities?.delete);
|
|
168
|
+
// Imported ACP sessions need Supervisor-owned stable history before import is safe.
|
|
169
|
+
target.sessions.importLocal = false;
|
|
170
|
+
target.branching.fork = Boolean(capabilities?.sessionCapabilities?.fork);
|
|
171
|
+
return target;
|
|
172
|
+
}
|
|
173
|
+
|
|
138
174
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
139
175
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
140
176
|
}
|
|
@@ -249,8 +285,14 @@ function sessionDetail(state: AcpSessionState): AgentSessionDetail {
|
|
|
249
285
|
createdAt: state.createdAt,
|
|
250
286
|
updatedAt: state.updatedAt,
|
|
251
287
|
status: state.status,
|
|
252
|
-
turns: state.turns
|
|
288
|
+
turns: state.turns.map((turn) => ({
|
|
289
|
+
...turn,
|
|
290
|
+
items: turn.items.map((item) => ({ ...item })),
|
|
291
|
+
})),
|
|
253
292
|
totalTurnCount: state.turns.length,
|
|
293
|
+
...(state.hydrationCoverage
|
|
294
|
+
? { historyCoverage: { ...state.hydrationCoverage } }
|
|
295
|
+
: {}),
|
|
254
296
|
};
|
|
255
297
|
}
|
|
256
298
|
|
|
@@ -288,6 +330,8 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
288
330
|
private readonly sessions = new Map<string, AcpSessionState>();
|
|
289
331
|
private readonly knownSessions = new Map<string, AgentSessionSummary>();
|
|
290
332
|
private readonly pendingPermissions = new Map<number, PendingPermission>();
|
|
333
|
+
private readonly hydrators = new Map<string, AcpSessionHydrator>();
|
|
334
|
+
private readonly extensionRegistry = new HarnessExtensionRegistry();
|
|
291
335
|
private readonly terminalService: AcpTerminalService;
|
|
292
336
|
private child: ChildProcess | null = null;
|
|
293
337
|
private connection: acp.ClientConnection | null = null;
|
|
@@ -306,8 +350,25 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
306
350
|
|
|
307
351
|
constructor(private readonly options: AcpRuntimeOptions) {
|
|
308
352
|
super();
|
|
353
|
+
this.extensionRegistry.on(
|
|
354
|
+
'event',
|
|
355
|
+
(event: HarnessExtensionEventEnvelope) => this.emitRuntimeEvent({
|
|
356
|
+
type: 'harness.extension',
|
|
357
|
+
provider: 'acp',
|
|
358
|
+
providerSessionId: event.providerSessionId,
|
|
359
|
+
providerTurnId: event.providerTurnId,
|
|
360
|
+
providerItemId: event.providerItemId,
|
|
361
|
+
extensionId: event.extensionId,
|
|
362
|
+
extensionVersion: event.extensionVersion,
|
|
363
|
+
event: event.event,
|
|
364
|
+
operationId: event.operationId,
|
|
365
|
+
sequence: event.sequence,
|
|
366
|
+
payload: event.payload,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
309
369
|
this.terminalService = new AcpTerminalService(
|
|
310
370
|
(sessionId) => this.sessions.get(sessionId)?.cwd ?? null,
|
|
371
|
+
(operation) => this.emit('fs-operation', operation),
|
|
311
372
|
);
|
|
312
373
|
}
|
|
313
374
|
|
|
@@ -315,6 +376,10 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
315
376
|
return { ...this.status };
|
|
316
377
|
}
|
|
317
378
|
|
|
379
|
+
getProtocolSnapshot() {
|
|
380
|
+
return snapshotAcpInitializeResponse(this.initializeResponse);
|
|
381
|
+
}
|
|
382
|
+
|
|
318
383
|
async start() {
|
|
319
384
|
if (this.status.state === 'ready') {
|
|
320
385
|
return;
|
|
@@ -374,6 +439,16 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
374
439
|
this.requestPermission(request.params))
|
|
375
440
|
.onNotification(acp.methods.client.session.update, (notification) =>
|
|
376
441
|
this.handleSessionUpdate(notification.params))
|
|
442
|
+
.onNotification<HarnessExtensionEventEnvelope>(
|
|
443
|
+
REMOTE_CODEX_HARNESS_EXTENSION_EVENT_METHOD,
|
|
444
|
+
(params) => params as HarnessExtensionEventEnvelope,
|
|
445
|
+
(notification) => {
|
|
446
|
+
this.extensionRegistry.handleEvent(
|
|
447
|
+
'acp-agent',
|
|
448
|
+
notification.params,
|
|
449
|
+
);
|
|
450
|
+
},
|
|
451
|
+
)
|
|
377
452
|
.onRequest(acp.methods.client.fs.readTextFile, (request) =>
|
|
378
453
|
this.readTextFile(request.params))
|
|
379
454
|
.onRequest(acp.methods.client.fs.writeTextFile, (request) =>
|
|
@@ -409,7 +484,9 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
409
484
|
}) as Promise<acp.InitializeResponse>;
|
|
410
485
|
const initializeResponse = await this.withStartupTimeout(initialize);
|
|
411
486
|
this.initializeResponse = initializeResponse;
|
|
487
|
+
this.resetCapabilities();
|
|
412
488
|
this.applyAgentCapabilities(initializeResponse.agentCapabilities);
|
|
489
|
+
this.registerNegotiatedExtensions(initializeResponse);
|
|
413
490
|
this.status = {
|
|
414
491
|
...this.status,
|
|
415
492
|
state: 'ready',
|
|
@@ -444,11 +521,11 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
444
521
|
async stop() {
|
|
445
522
|
this.stopping = true;
|
|
446
523
|
this.terminalService.stop();
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
524
|
+
this.settleActiveTurns('interrupted', 'ACP runtime stopped.');
|
|
525
|
+
this.cancelPendingPermissions();
|
|
526
|
+
this.hydrators.clear();
|
|
527
|
+
await this.closeLoadedSessionsBeforeStop();
|
|
528
|
+
this.extensionRegistry.unregisterOwner('acp-agent');
|
|
452
529
|
this.connection?.close();
|
|
453
530
|
this.child?.kill('SIGTERM');
|
|
454
531
|
this.connection = null;
|
|
@@ -464,6 +541,26 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
464
541
|
this.emit('status', this.getStatus());
|
|
465
542
|
}
|
|
466
543
|
|
|
544
|
+
private async closeLoadedSessionsBeforeStop() {
|
|
545
|
+
if (
|
|
546
|
+
!this.context ||
|
|
547
|
+
!this.initializeResponse?.agentCapabilities?.sessionCapabilities?.close
|
|
548
|
+
) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const sessionIds = [...this.sessions.keys()];
|
|
552
|
+
const results = await Promise.allSettled(sessionIds.map((sessionId) =>
|
|
553
|
+
this.context!.request(acp.methods.agent.session.close, { sessionId })));
|
|
554
|
+
results.forEach((result, index) => {
|
|
555
|
+
if (result.status === 'rejected') {
|
|
556
|
+
this.emit(
|
|
557
|
+
'warning',
|
|
558
|
+
`Unable to close ACP session ${sessionIds[index] ?? 'unknown'} before stop: ${errorMessage(result.reason)}`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
467
564
|
async listModels(): Promise<AgentModel[]> {
|
|
468
565
|
return [{
|
|
469
566
|
id: 'default',
|
|
@@ -478,6 +575,11 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
478
575
|
}
|
|
479
576
|
|
|
480
577
|
async inspectModelOptions(cwd: string): Promise<AgentModel[]> {
|
|
578
|
+
const sessionCapabilities =
|
|
579
|
+
this.initializeResponse?.agentCapabilities?.sessionCapabilities;
|
|
580
|
+
if (!sessionCapabilities?.delete) {
|
|
581
|
+
return this.listModels();
|
|
582
|
+
}
|
|
481
583
|
const context = await this.requireContext();
|
|
482
584
|
const response = await context.request(acp.methods.agent.session.new, {
|
|
483
585
|
cwd: path.resolve(cwd),
|
|
@@ -485,6 +587,10 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
485
587
|
_meta: { yoloMode: false, remoteCodexCapabilityProbe: true },
|
|
486
588
|
});
|
|
487
589
|
let configOptions = response.configOptions ?? [];
|
|
590
|
+
this.updateCapabilitiesFromConfigOptions(configOptions);
|
|
591
|
+
const supportsPerformanceMode = configOptions.some(
|
|
592
|
+
(option) => option.id === 'fast-mode' || option.category === 'model_config',
|
|
593
|
+
);
|
|
488
594
|
try {
|
|
489
595
|
const modelOption = configOptionByCategory(configOptions, 'model');
|
|
490
596
|
if (!modelOption || modelOption.type !== 'select') {
|
|
@@ -496,6 +602,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
496
602
|
description: 'Use the model configured by the ACP agent.',
|
|
497
603
|
isDefault: true,
|
|
498
604
|
hidden: false,
|
|
605
|
+
supportsPerformanceMode,
|
|
499
606
|
supportedReasoningEfforts: reasoning.efforts,
|
|
500
607
|
defaultReasoningEffort: reasoning.defaultEffort,
|
|
501
608
|
selectionKind: 'model',
|
|
@@ -524,6 +631,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
524
631
|
description: option.description ?? '',
|
|
525
632
|
isDefault: option.value === modelOption.currentValue,
|
|
526
633
|
hidden: false,
|
|
634
|
+
supportsPerformanceMode,
|
|
527
635
|
supportedReasoningEfforts: reasoning.efforts,
|
|
528
636
|
defaultReasoningEffort: reasoning.defaultEffort,
|
|
529
637
|
selectionKind: 'model',
|
|
@@ -531,11 +639,9 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
531
639
|
}
|
|
532
640
|
return models;
|
|
533
641
|
} finally {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}).catch(() => undefined);
|
|
538
|
-
}
|
|
642
|
+
await context.request(acp.methods.agent.session.delete, {
|
|
643
|
+
sessionId: response.sessionId,
|
|
644
|
+
}).catch(() => undefined);
|
|
539
645
|
}
|
|
540
646
|
}
|
|
541
647
|
|
|
@@ -573,19 +679,198 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
573
679
|
return [...this.sessions.keys()];
|
|
574
680
|
}
|
|
575
681
|
|
|
682
|
+
async closeSession(providerSessionId: string) {
|
|
683
|
+
if (!this.initializeResponse?.agentCapabilities?.sessionCapabilities?.close) {
|
|
684
|
+
throw new AgentRuntimeError(
|
|
685
|
+
'ACP agent does not support session/close.',
|
|
686
|
+
'acp',
|
|
687
|
+
'request_failed',
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
await (await this.requireContext()).request(acp.methods.agent.session.close, {
|
|
691
|
+
sessionId: providerSessionId,
|
|
692
|
+
});
|
|
693
|
+
this.sessions.delete(providerSessionId);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async deleteSession(providerSessionId: string) {
|
|
697
|
+
if (!this.initializeResponse?.agentCapabilities?.sessionCapabilities?.delete) {
|
|
698
|
+
throw new AgentRuntimeError(
|
|
699
|
+
'ACP agent does not support session/delete.',
|
|
700
|
+
'acp',
|
|
701
|
+
'request_failed',
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
await (await this.requireContext()).request(acp.methods.agent.session.delete, {
|
|
705
|
+
sessionId: providerSessionId,
|
|
706
|
+
});
|
|
707
|
+
this.sessions.delete(providerSessionId);
|
|
708
|
+
this.knownSessions.delete(providerSessionId);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async sendInput(input: SendAgentInputInput): Promise<AgentTurn | null> {
|
|
712
|
+
const state = this.sessions.get(input.providerSessionId);
|
|
713
|
+
if (!state?.activeMapper || state.activeMapper.turnId !== input.providerTurnId) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
if (!this.extensionRegistry.supports('acp.steering', 1, 'steer')) {
|
|
717
|
+
throw new AgentRuntimeError(
|
|
718
|
+
'The selected ACP agent does not support running-turn steering.',
|
|
719
|
+
'acp',
|
|
720
|
+
'request_failed',
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const prompt = await buildAcpPromptContent({
|
|
724
|
+
prompt: input.prompt,
|
|
725
|
+
workspacePath: input.workspacePath ?? state.cwd,
|
|
726
|
+
promptCapabilities: this.initializeResponse?.agentCapabilities?.promptCapabilities,
|
|
727
|
+
});
|
|
728
|
+
const operationId = randomUUID();
|
|
729
|
+
await this.extensionRegistry.invoke({
|
|
730
|
+
extensionId: 'acp.steering',
|
|
731
|
+
extensionVersion: 1,
|
|
732
|
+
method: 'steer',
|
|
733
|
+
operationId,
|
|
734
|
+
idempotencyKey: `${input.providerSessionId}:${input.providerTurnId}:steer:${operationId}`,
|
|
735
|
+
params: {
|
|
736
|
+
providerSessionId: input.providerSessionId,
|
|
737
|
+
prompt,
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
return state.activeMapper.turn();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async compactSession(providerSessionId: string) {
|
|
744
|
+
const operationId = randomUUID();
|
|
745
|
+
await this.extensionRegistry.invoke({
|
|
746
|
+
extensionId: 'codex.control',
|
|
747
|
+
extensionVersion: 1,
|
|
748
|
+
method: 'compact',
|
|
749
|
+
operationId,
|
|
750
|
+
idempotencyKey: `${providerSessionId}:compact:${operationId}`,
|
|
751
|
+
params: { providerSessionId },
|
|
752
|
+
timeoutMs: 180_000,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async forkSession(input: { providerSessionId: string; atTurnId?: string | null }) {
|
|
757
|
+
if (!this.initializeResponse?.agentCapabilities?.sessionCapabilities?.fork) {
|
|
758
|
+
throw new AgentRuntimeError(
|
|
759
|
+
'The selected ACP agent does not support session/fork.',
|
|
760
|
+
'acp',
|
|
761
|
+
'request_failed',
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
const source = this.sessions.get(input.providerSessionId) ??
|
|
765
|
+
await this.restoreSession(input.providerSessionId);
|
|
766
|
+
const response = await (await this.requireContext()).request(
|
|
767
|
+
acp.methods.agent.session.fork,
|
|
768
|
+
{
|
|
769
|
+
sessionId: source.providerSessionId,
|
|
770
|
+
cwd: source.cwd,
|
|
771
|
+
mcpServers: [],
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
const now = new Date().toISOString();
|
|
775
|
+
this.knownSessions.set(response.sessionId, {
|
|
776
|
+
provider: 'acp',
|
|
777
|
+
providerSessionId: response.sessionId,
|
|
778
|
+
cwd: source.cwd,
|
|
779
|
+
title: source.title,
|
|
780
|
+
preview: source.turns.at(-1)?.items
|
|
781
|
+
.filter((item) => item.kind === 'agentMessage')
|
|
782
|
+
.map((item) => item.text)
|
|
783
|
+
.join('\n') || null,
|
|
784
|
+
createdAt: now,
|
|
785
|
+
updatedAt: now,
|
|
786
|
+
status: 'not_loaded',
|
|
787
|
+
});
|
|
788
|
+
const forked = await this.restoreSession(response.sessionId);
|
|
789
|
+
if (forked.turns.length === 0 && source.turns.length > 0) {
|
|
790
|
+
forked.turns = structuredClone(source.turns);
|
|
791
|
+
forked.hydrationCoverage = null;
|
|
792
|
+
}
|
|
793
|
+
forked.modes = response.modes ?? forked.modes;
|
|
794
|
+
forked.configOptions = response.configOptions ?? forked.configOptions;
|
|
795
|
+
this.syncStateFromConfigOptions(forked);
|
|
796
|
+
return sessionDetail(forked);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async getGoal(providerSessionId: string) {
|
|
800
|
+
return this.sessions.get(providerSessionId)?.goal ?? null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async setGoal(input: SetAgentGoalInput) {
|
|
804
|
+
const state = this.sessions.get(input.providerSessionId);
|
|
805
|
+
if (!state) {
|
|
806
|
+
throw new AgentRuntimeError('ACP session is not loaded.', 'acp', 'request_failed');
|
|
807
|
+
}
|
|
808
|
+
if (input.tokenBudget !== undefined && input.tokenBudget !== null) {
|
|
809
|
+
throw new AgentRuntimeError(
|
|
810
|
+
'Codex ACP goal control does not expose token budgets.',
|
|
811
|
+
'acp',
|
|
812
|
+
'request_failed',
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const action: { action: 'set'; objective: string } |
|
|
816
|
+
{ action: 'pause' | 'resume' | 'clear' } | null = input.objective?.trim()
|
|
817
|
+
? { action: 'set', objective: input.objective.trim() }
|
|
818
|
+
: input.status === 'paused'
|
|
819
|
+
? { action: 'pause' }
|
|
820
|
+
: input.status === 'active'
|
|
821
|
+
? { action: 'resume' }
|
|
822
|
+
: null;
|
|
823
|
+
if (!action) {
|
|
824
|
+
throw new AgentRuntimeError(
|
|
825
|
+
'Codex ACP goal update requires an objective, pause, or resume action.',
|
|
826
|
+
'acp',
|
|
827
|
+
'request_failed',
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
await this.invokeAcpGoal(input.providerSessionId, action);
|
|
831
|
+
if (!state.goal) {
|
|
832
|
+
throw new AgentRuntimeError(
|
|
833
|
+
'Codex ACP completed goal control without publishing a goal snapshot.',
|
|
834
|
+
'acp',
|
|
835
|
+
'invalid_response',
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return state.goal;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async clearGoal(providerSessionId: string) {
|
|
842
|
+
const state = this.sessions.get(providerSessionId);
|
|
843
|
+
if (!state) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const existed = Boolean(state.goal);
|
|
847
|
+
await this.invokeAcpGoal(providerSessionId, { action: 'clear' });
|
|
848
|
+
return existed;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
listHarnessExtensions() {
|
|
852
|
+
return this.extensionRegistry.list();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
invokeHarnessExtension<T = unknown>(input: {
|
|
856
|
+
extensionId: string;
|
|
857
|
+
extensionVersion: number;
|
|
858
|
+
method: string;
|
|
859
|
+
operationId: string;
|
|
860
|
+
idempotencyKey: string;
|
|
861
|
+
params: unknown;
|
|
862
|
+
timeoutMs?: number;
|
|
863
|
+
signal?: AbortSignal;
|
|
864
|
+
}) {
|
|
865
|
+
return this.extensionRegistry.invoke<T>(input);
|
|
866
|
+
}
|
|
867
|
+
|
|
576
868
|
async readSession(
|
|
577
869
|
providerSessionId: string,
|
|
578
870
|
): Promise<AgentSessionDetail> {
|
|
579
|
-
const state = this.sessions.get(providerSessionId)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
throw new AgentRuntimeError(
|
|
584
|
-
'ACP session history is owned by the Remote Codex supervisor and is not materialized in this runtime process.',
|
|
585
|
-
'acp',
|
|
586
|
-
'request_failed',
|
|
587
|
-
{ historyUnavailable: true, providerSessionId },
|
|
588
|
-
);
|
|
871
|
+
const state = this.sessions.get(providerSessionId) ??
|
|
872
|
+
await this.restoreSession(providerSessionId);
|
|
873
|
+
return sessionDetail(state);
|
|
589
874
|
}
|
|
590
875
|
|
|
591
876
|
async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
@@ -604,8 +889,8 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
604
889
|
title: null,
|
|
605
890
|
createdAt: now,
|
|
606
891
|
updatedAt: now,
|
|
607
|
-
model:
|
|
608
|
-
reasoningEffort:
|
|
892
|
+
model: null,
|
|
893
|
+
reasoningEffort: null,
|
|
609
894
|
sandboxMode: input.sandboxMode ?? null,
|
|
610
895
|
status: 'idle',
|
|
611
896
|
turns: [],
|
|
@@ -613,10 +898,20 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
613
898
|
modes: response.modes ?? null,
|
|
614
899
|
configOptions: response.configOptions ?? [],
|
|
615
900
|
availableCommands: [],
|
|
901
|
+
hydrationCoverage: null,
|
|
902
|
+
goal: null,
|
|
616
903
|
};
|
|
617
904
|
this.sessions.set(state.providerSessionId, state);
|
|
905
|
+
this.syncStateFromConfigOptions(state);
|
|
618
906
|
this.knownSessions.set(state.providerSessionId, sessionDetail(state));
|
|
619
|
-
await this.applySessionSettings(
|
|
907
|
+
await this.applySessionSettings(
|
|
908
|
+
state,
|
|
909
|
+
input.model,
|
|
910
|
+
input.reasoningEffort,
|
|
911
|
+
input.sandboxMode,
|
|
912
|
+
undefined,
|
|
913
|
+
input.performanceMode,
|
|
914
|
+
);
|
|
620
915
|
const session = sessionDetail(state);
|
|
621
916
|
return {
|
|
622
917
|
provider: 'acp',
|
|
@@ -633,7 +928,14 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
633
928
|
async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
634
929
|
const existing = this.sessions.get(input.providerSessionId);
|
|
635
930
|
const state = existing ?? await this.restoreSession(input.providerSessionId);
|
|
636
|
-
await this.applySessionSettings(
|
|
931
|
+
await this.applySessionSettings(
|
|
932
|
+
state,
|
|
933
|
+
input.model,
|
|
934
|
+
undefined,
|
|
935
|
+
input.sandboxMode,
|
|
936
|
+
undefined,
|
|
937
|
+
input.performanceMode,
|
|
938
|
+
);
|
|
637
939
|
state.status = 'idle';
|
|
638
940
|
state.updatedAt = new Date().toISOString();
|
|
639
941
|
const session = sessionDetail(state);
|
|
@@ -665,6 +967,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
665
967
|
input.reasoningEffort,
|
|
666
968
|
input.sandboxMode,
|
|
667
969
|
input.collaborationMode,
|
|
970
|
+
input.performanceMode,
|
|
668
971
|
);
|
|
669
972
|
|
|
670
973
|
const turnId = input.displayTurnId ?? randomUUID();
|
|
@@ -690,13 +993,19 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
690
993
|
turn: startedTurn,
|
|
691
994
|
});
|
|
692
995
|
|
|
693
|
-
const prompt =
|
|
694
|
-
|
|
695
|
-
: input.
|
|
996
|
+
const prompt = await buildAcpPromptContent({
|
|
997
|
+
prompt: input.prompt,
|
|
998
|
+
workspacePath: input.workspacePath ?? state.cwd,
|
|
999
|
+
promptCapabilities: this.initializeResponse?.agentCapabilities?.promptCapabilities,
|
|
1000
|
+
...(input.content ? { content: input.content } : {}),
|
|
1001
|
+
});
|
|
1002
|
+
if (input.developerInstructions?.trim()) {
|
|
1003
|
+
prompt.unshift({ type: 'text', text: `${input.developerInstructions.trim()}\n\n` });
|
|
1004
|
+
}
|
|
696
1005
|
const context = await this.requireContext();
|
|
697
1006
|
void context.request(acp.methods.agent.session.prompt, {
|
|
698
1007
|
sessionId: state.providerSessionId,
|
|
699
|
-
prompt
|
|
1008
|
+
prompt,
|
|
700
1009
|
}).then(
|
|
701
1010
|
(response) => this.completePrompt(state, mapper, response),
|
|
702
1011
|
(error) => this.failPrompt(state, mapper, error),
|
|
@@ -826,18 +1135,29 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
826
1135
|
modes: null,
|
|
827
1136
|
configOptions: [],
|
|
828
1137
|
availableCommands: [],
|
|
1138
|
+
hydrationCoverage: null,
|
|
1139
|
+
goal: null,
|
|
829
1140
|
};
|
|
830
1141
|
this.sessions.set(providerSessionId, state);
|
|
831
1142
|
try {
|
|
832
1143
|
const capabilities = this.initializeResponse?.agentCapabilities;
|
|
833
1144
|
if (capabilities?.loadSession) {
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1145
|
+
const hydrator = new AcpSessionHydrator(providerSessionId);
|
|
1146
|
+
this.hydrators.set(providerSessionId, hydrator);
|
|
1147
|
+
try {
|
|
1148
|
+
const response = await context.request(acp.methods.agent.session.load, {
|
|
1149
|
+
sessionId: providerSessionId,
|
|
1150
|
+
cwd: summary.cwd,
|
|
1151
|
+
mcpServers: [],
|
|
1152
|
+
});
|
|
1153
|
+
state.modes = response.modes ?? null;
|
|
1154
|
+
state.configOptions = response.configOptions ?? [];
|
|
1155
|
+
state.turns = hydrator.complete();
|
|
1156
|
+
state.hydrationCoverage = hydrator.coverage();
|
|
1157
|
+
this.syncStateFromConfigOptions(state);
|
|
1158
|
+
} finally {
|
|
1159
|
+
this.hydrators.delete(providerSessionId);
|
|
1160
|
+
}
|
|
841
1161
|
} else if (capabilities?.sessionCapabilities?.resume) {
|
|
842
1162
|
const response = await context.request(acp.methods.agent.session.resume, {
|
|
843
1163
|
sessionId: providerSessionId,
|
|
@@ -846,6 +1166,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
846
1166
|
});
|
|
847
1167
|
state.modes = response.modes ?? null;
|
|
848
1168
|
state.configOptions = response.configOptions ?? [];
|
|
1169
|
+
this.syncStateFromConfigOptions(state);
|
|
849
1170
|
} else {
|
|
850
1171
|
throw new Error('ACP agent does not support session/load or session/resume.');
|
|
851
1172
|
}
|
|
@@ -869,18 +1190,25 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
869
1190
|
reasoningEffort?: string | null,
|
|
870
1191
|
sandboxMode?: string | null,
|
|
871
1192
|
collaborationMode?: 'default' | 'plan' | null,
|
|
1193
|
+
performanceMode?: 'standard' | 'fast' | null,
|
|
872
1194
|
) {
|
|
873
1195
|
if (model && model !== 'default') {
|
|
874
|
-
await this.setConfigOption(state, 'model', model);
|
|
875
|
-
state.model = model;
|
|
1196
|
+
state.model = await this.setConfigOption(state, 'model', model);
|
|
876
1197
|
}
|
|
877
1198
|
if (reasoningEffort) {
|
|
878
|
-
await this.setConfigOption(
|
|
879
|
-
|
|
1199
|
+
const applied = await this.setConfigOption(
|
|
1200
|
+
state,
|
|
1201
|
+
'thought_level',
|
|
1202
|
+
reasoningEffort,
|
|
1203
|
+
);
|
|
1204
|
+
state.reasoningEffort = normalizeAcpEffort(applied);
|
|
880
1205
|
}
|
|
881
1206
|
if (sandboxMode !== undefined) {
|
|
882
1207
|
state.sandboxMode = sandboxMode;
|
|
883
1208
|
}
|
|
1209
|
+
if (performanceMode) {
|
|
1210
|
+
await this.setFastMode(state, performanceMode === 'fast');
|
|
1211
|
+
}
|
|
884
1212
|
|
|
885
1213
|
const modes = state.modes?.availableModes ?? [];
|
|
886
1214
|
if (modes.length === 0) {
|
|
@@ -920,16 +1248,27 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
920
1248
|
sessionId: state.providerSessionId,
|
|
921
1249
|
modelId: value,
|
|
922
1250
|
});
|
|
923
|
-
return;
|
|
1251
|
+
return value;
|
|
924
1252
|
}
|
|
925
1253
|
if (!option || option.type !== 'select') {
|
|
926
|
-
|
|
1254
|
+
throw new AgentRuntimeError(
|
|
1255
|
+
`ACP agent does not expose a ${category} config option.`,
|
|
1256
|
+
'acp',
|
|
1257
|
+
'request_failed',
|
|
1258
|
+
);
|
|
927
1259
|
}
|
|
928
1260
|
const selected = allSelectOptions(option).find((candidate) =>
|
|
929
1261
|
candidate.value === value || candidate.name.toLowerCase() === value.toLowerCase(),
|
|
930
1262
|
);
|
|
931
|
-
if (!selected
|
|
932
|
-
|
|
1263
|
+
if (!selected) {
|
|
1264
|
+
throw new AgentRuntimeError(
|
|
1265
|
+
`ACP agent rejected unknown ${category} option: ${value}`,
|
|
1266
|
+
'acp',
|
|
1267
|
+
'request_failed',
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (selected.value === option.currentValue) {
|
|
1271
|
+
return selected.value;
|
|
933
1272
|
}
|
|
934
1273
|
const context = await this.requireContext();
|
|
935
1274
|
const response = await context.request(acp.methods.agent.session.setConfigOption, {
|
|
@@ -938,6 +1277,73 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
938
1277
|
value: selected.value,
|
|
939
1278
|
});
|
|
940
1279
|
state.configOptions = response.configOptions;
|
|
1280
|
+
return selected.value;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private syncStateFromConfigOptions(state: AcpSessionState) {
|
|
1284
|
+
this.updateCapabilitiesFromConfigOptions(state.configOptions);
|
|
1285
|
+
const modelOption = configOptionByCategory(state.configOptions, 'model');
|
|
1286
|
+
if (modelOption?.type === 'select') {
|
|
1287
|
+
state.model = modelOption.currentValue;
|
|
1288
|
+
}
|
|
1289
|
+
const thoughtOption = configOptionByCategory(
|
|
1290
|
+
state.configOptions,
|
|
1291
|
+
'thought_level',
|
|
1292
|
+
);
|
|
1293
|
+
if (thoughtOption?.type === 'select') {
|
|
1294
|
+
state.reasoningEffort = normalizeAcpEffort(thoughtOption.currentValue);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
private updateCapabilitiesFromConfigOptions(
|
|
1299
|
+
configOptions: acp.SessionConfigOption[],
|
|
1300
|
+
) {
|
|
1301
|
+
this.capabilities.management.models ||= Boolean(
|
|
1302
|
+
configOptionByCategory(configOptions, 'model'),
|
|
1303
|
+
);
|
|
1304
|
+
this.capabilities.controls.performanceMode ||= configOptions.some(
|
|
1305
|
+
(option) => option.id === 'fast-mode' || option.category === 'model_config',
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private async setFastMode(state: AcpSessionState, enabled: boolean) {
|
|
1310
|
+
const option = state.configOptions.find(
|
|
1311
|
+
(candidate) =>
|
|
1312
|
+
candidate.id === 'fast-mode' || candidate.category === 'model_config',
|
|
1313
|
+
);
|
|
1314
|
+
if (!option) {
|
|
1315
|
+
throw new AgentRuntimeError(
|
|
1316
|
+
'The selected ACP agent does not expose fast mode.',
|
|
1317
|
+
'acp',
|
|
1318
|
+
'request_failed',
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const value = option.type === 'boolean'
|
|
1322
|
+
? enabled
|
|
1323
|
+
: allSelectOptions(option).find((candidate) =>
|
|
1324
|
+
enabled
|
|
1325
|
+
? ['on', 'true', 'fast'].includes(candidate.value.toLowerCase())
|
|
1326
|
+
: ['off', 'false', 'standard'].includes(candidate.value.toLowerCase()))?.value;
|
|
1327
|
+
if (value === undefined || value === option.currentValue) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const response = await (await this.requireContext()).request(
|
|
1331
|
+
acp.methods.agent.session.setConfigOption,
|
|
1332
|
+
option.type === 'boolean'
|
|
1333
|
+
? {
|
|
1334
|
+
sessionId: state.providerSessionId,
|
|
1335
|
+
configId: option.id,
|
|
1336
|
+
type: 'boolean',
|
|
1337
|
+
value: value as boolean,
|
|
1338
|
+
}
|
|
1339
|
+
: {
|
|
1340
|
+
sessionId: state.providerSessionId,
|
|
1341
|
+
configId: option.id,
|
|
1342
|
+
value: value as string,
|
|
1343
|
+
},
|
|
1344
|
+
);
|
|
1345
|
+
state.configOptions = response.configOptions;
|
|
1346
|
+
this.updateCapabilitiesFromConfigOptions(state.configOptions);
|
|
941
1347
|
}
|
|
942
1348
|
|
|
943
1349
|
private handleSessionUpdate(notification: acp.SessionNotification) {
|
|
@@ -947,8 +1353,10 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
947
1353
|
}
|
|
948
1354
|
state.updatedAt = new Date().toISOString();
|
|
949
1355
|
const update = notification.update;
|
|
1356
|
+
const hydrator = this.hydrators.get(notification.sessionId);
|
|
950
1357
|
if (update.sessionUpdate === 'config_option_update') {
|
|
951
1358
|
state.configOptions = update.configOptions;
|
|
1359
|
+
this.syncStateFromConfigOptions(state);
|
|
952
1360
|
} else if (update.sessionUpdate === 'current_mode_update' && state.modes) {
|
|
953
1361
|
state.modes = { ...state.modes, currentModeId: update.currentModeId };
|
|
954
1362
|
} else if (update.sessionUpdate === 'available_commands_update') {
|
|
@@ -956,7 +1364,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
956
1364
|
} else if (update.sessionUpdate === 'session_info_update') {
|
|
957
1365
|
if (update.title !== undefined) {
|
|
958
1366
|
state.title = update.title;
|
|
959
|
-
if (update.title) {
|
|
1367
|
+
if (update.title && !hydrator) {
|
|
960
1368
|
this.emitRuntimeEvent({
|
|
961
1369
|
type: 'session.title.updated',
|
|
962
1370
|
provider: 'acp',
|
|
@@ -965,6 +1373,33 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
965
1373
|
});
|
|
966
1374
|
}
|
|
967
1375
|
}
|
|
1376
|
+
const goal = this.goalFromSessionInfoUpdate(
|
|
1377
|
+
state.providerSessionId,
|
|
1378
|
+
update,
|
|
1379
|
+
);
|
|
1380
|
+
if (goal !== undefined) {
|
|
1381
|
+
state.goal = goal;
|
|
1382
|
+
if (!hydrator) {
|
|
1383
|
+
this.emitRuntimeEvent(goal
|
|
1384
|
+
? {
|
|
1385
|
+
type: 'goal.updated',
|
|
1386
|
+
provider: 'acp',
|
|
1387
|
+
providerSessionId: state.providerSessionId,
|
|
1388
|
+
providerTurnId: state.activeMapper?.turnId ?? null,
|
|
1389
|
+
goal,
|
|
1390
|
+
}
|
|
1391
|
+
: {
|
|
1392
|
+
type: 'goal.cleared',
|
|
1393
|
+
provider: 'acp',
|
|
1394
|
+
providerSessionId: state.providerSessionId,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (hydrator) {
|
|
1401
|
+
hydrator.apply(update);
|
|
1402
|
+
return;
|
|
968
1403
|
}
|
|
969
1404
|
|
|
970
1405
|
const mapper = state.activeMapper;
|
|
@@ -1118,10 +1553,17 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1118
1553
|
}
|
|
1119
1554
|
|
|
1120
1555
|
private async readTextFile(params: acp.ReadTextFileRequest): Promise<acp.ReadTextFileResponse> {
|
|
1121
|
-
|
|
1122
|
-
|
|
1556
|
+
const session = this.sessions.get(params.sessionId);
|
|
1557
|
+
if (!session) {
|
|
1558
|
+
throw new Error(`ACP session workspace not found: ${params.sessionId}`);
|
|
1123
1559
|
}
|
|
1124
|
-
const
|
|
1560
|
+
const filePath = await resolveAcpWorkspacePath(session.cwd, params.path);
|
|
1561
|
+
this.emit('fs-operation', {
|
|
1562
|
+
operation: 'fs.readTextFile',
|
|
1563
|
+
sessionId: params.sessionId,
|
|
1564
|
+
path: filePath,
|
|
1565
|
+
});
|
|
1566
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1125
1567
|
if (params.line === undefined && params.limit === undefined) {
|
|
1126
1568
|
return { content };
|
|
1127
1569
|
}
|
|
@@ -1132,11 +1574,18 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1132
1574
|
}
|
|
1133
1575
|
|
|
1134
1576
|
private async writeTextFile(params: acp.WriteTextFileRequest) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1577
|
+
const session = this.sessions.get(params.sessionId);
|
|
1578
|
+
if (!session) {
|
|
1579
|
+
throw new Error(`ACP session workspace not found: ${params.sessionId}`);
|
|
1137
1580
|
}
|
|
1138
|
-
await
|
|
1139
|
-
|
|
1581
|
+
const filePath = await resolveAcpWorkspacePath(session.cwd, params.path);
|
|
1582
|
+
this.emit('fs-operation', {
|
|
1583
|
+
operation: 'fs.writeTextFile',
|
|
1584
|
+
sessionId: params.sessionId,
|
|
1585
|
+
path: filePath,
|
|
1586
|
+
});
|
|
1587
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1588
|
+
await fs.writeFile(filePath, params.content, 'utf8');
|
|
1140
1589
|
return {};
|
|
1141
1590
|
}
|
|
1142
1591
|
|
|
@@ -1149,11 +1598,251 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1149
1598
|
}
|
|
1150
1599
|
|
|
1151
1600
|
private applyAgentCapabilities(capabilities: acp.AgentCapabilities | null | undefined) {
|
|
1152
|
-
this.capabilities
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1601
|
+
applyNegotiatedAcpCapabilities(this.capabilities, capabilities);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private resetCapabilities() {
|
|
1605
|
+
const baseline = cloneCapabilities();
|
|
1606
|
+
for (const section of Object.keys(baseline) as Array<keyof AgentProviderCapabilities>) {
|
|
1607
|
+
Object.assign(this.capabilities[section], baseline[section]);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
private goalFromSessionInfoUpdate(
|
|
1612
|
+
providerSessionId: string,
|
|
1613
|
+
update: Extract<acp.SessionUpdate, { sessionUpdate: 'session_info_update' }>,
|
|
1614
|
+
): AgentGoal | null | undefined {
|
|
1615
|
+
const meta = isRecord(update._meta) ? update._meta : null;
|
|
1616
|
+
if (!meta || !Object.hasOwn(meta, 'goal')) {
|
|
1617
|
+
return undefined;
|
|
1618
|
+
}
|
|
1619
|
+
if (meta.goal === null) {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
if (!isRecord(meta.goal)) {
|
|
1623
|
+
return undefined;
|
|
1624
|
+
}
|
|
1625
|
+
const objective = typeof meta.goal.objective === 'string'
|
|
1626
|
+
? meta.goal.objective.trim()
|
|
1627
|
+
: '';
|
|
1628
|
+
if (!objective) {
|
|
1629
|
+
return undefined;
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
providerSessionId,
|
|
1633
|
+
objective,
|
|
1634
|
+
status: typeof meta.goal.status === 'string' ? meta.goal.status : 'active',
|
|
1635
|
+
tokenBudget: typeof meta.goal.tokenBudget === 'number'
|
|
1636
|
+
? meta.goal.tokenBudget
|
|
1637
|
+
: null,
|
|
1638
|
+
tokensUsed: typeof meta.goal.tokensUsed === 'number' ? meta.goal.tokensUsed : 0,
|
|
1639
|
+
timeUsedSeconds: typeof meta.goal.timeUsedSeconds === 'number'
|
|
1640
|
+
? meta.goal.timeUsedSeconds
|
|
1641
|
+
: 0,
|
|
1642
|
+
createdAt: typeof meta.goal.createdAt === 'number'
|
|
1643
|
+
? meta.goal.createdAt
|
|
1644
|
+
: Date.now(),
|
|
1645
|
+
updatedAt: typeof meta.goal.updatedAt === 'number'
|
|
1646
|
+
? meta.goal.updatedAt
|
|
1647
|
+
: Date.now(),
|
|
1648
|
+
rawGoal: structuredClone(meta.goal),
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private async invokeAcpGoal(
|
|
1653
|
+
providerSessionId: string,
|
|
1654
|
+
action: { action: 'set'; objective: string } | { action: 'pause' | 'resume' | 'clear' },
|
|
1655
|
+
) {
|
|
1656
|
+
if (!this.extensionRegistry.supports('acp.goal', 1, action.action)) {
|
|
1657
|
+
throw new AgentRuntimeError(
|
|
1658
|
+
'The selected ACP agent does not expose goal control.',
|
|
1659
|
+
'acp',
|
|
1660
|
+
'request_failed',
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
const operationId = randomUUID();
|
|
1664
|
+
await this.extensionRegistry.invoke({
|
|
1665
|
+
extensionId: 'acp.goal',
|
|
1666
|
+
extensionVersion: 1,
|
|
1667
|
+
method: action.action,
|
|
1668
|
+
operationId,
|
|
1669
|
+
idempotencyKey: `${providerSessionId}:goal:${operationId}`,
|
|
1670
|
+
params: { providerSessionId, ...action },
|
|
1671
|
+
timeoutMs: 180_000,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
private async runControlPrompt(
|
|
1676
|
+
providerSessionId: string,
|
|
1677
|
+
prompt: string,
|
|
1678
|
+
signal: AbortSignal,
|
|
1679
|
+
) {
|
|
1680
|
+
const state = this.sessions.get(providerSessionId);
|
|
1681
|
+
if (!state || state.activeMapper) {
|
|
1682
|
+
throw new AgentRuntimeError(
|
|
1683
|
+
'ACP control prompt requires an idle loaded session.',
|
|
1684
|
+
'acp',
|
|
1685
|
+
'request_failed',
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
let startedTurnId: string | null = null;
|
|
1689
|
+
let cleanupCompletion!: () => void;
|
|
1690
|
+
const completed = new Promise<Extract<AgentRuntimeEvent, { type: 'turn.completed' }>>(
|
|
1691
|
+
(resolve, reject) => {
|
|
1692
|
+
const cleanup = () => {
|
|
1693
|
+
this.off('event', onEvent);
|
|
1694
|
+
signal.removeEventListener('abort', onAbort);
|
|
1695
|
+
};
|
|
1696
|
+
const onEvent = (event: AgentRuntimeEvent) => {
|
|
1697
|
+
if (
|
|
1698
|
+
event.type === 'turn.completed' &&
|
|
1699
|
+
event.providerSessionId === providerSessionId &&
|
|
1700
|
+
(!startedTurnId || event.turn.providerTurnId === startedTurnId)
|
|
1701
|
+
) {
|
|
1702
|
+
cleanup();
|
|
1703
|
+
resolve(event);
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
const onAbort = () => {
|
|
1707
|
+
cleanup();
|
|
1708
|
+
reject(signal.reason ?? new Error('ACP control prompt cancelled.'));
|
|
1709
|
+
};
|
|
1710
|
+
cleanupCompletion = cleanup;
|
|
1711
|
+
this.on('event', onEvent);
|
|
1712
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1713
|
+
},
|
|
1714
|
+
);
|
|
1715
|
+
try {
|
|
1716
|
+
const turn = await this.startTurn({
|
|
1717
|
+
providerSessionId,
|
|
1718
|
+
prompt,
|
|
1719
|
+
hidden: true,
|
|
1720
|
+
});
|
|
1721
|
+
startedTurnId = turn.providerTurnId;
|
|
1722
|
+
const event = await completed;
|
|
1723
|
+
if (event.turn.status === 'failed') {
|
|
1724
|
+
throw new Error(event.turn.error?.message ?? 'ACP control prompt failed.');
|
|
1725
|
+
}
|
|
1726
|
+
return { providerTurnId: event.turn.providerTurnId, status: event.turn.status };
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
cleanupCompletion();
|
|
1729
|
+
throw error;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
private registerNegotiatedExtensions(response: acp.InitializeResponse) {
|
|
1734
|
+
const snapshot = snapshotAcpInitializeResponse(response);
|
|
1735
|
+
if (!snapshot || !this.context) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const wireTransport = {
|
|
1739
|
+
request: (method: string, params: unknown, signal: AbortSignal) =>
|
|
1740
|
+
this.context!.request<unknown, typeof params>(method, params, {
|
|
1741
|
+
cancellationSignal: signal,
|
|
1742
|
+
}),
|
|
1743
|
+
};
|
|
1744
|
+
for (const descriptor of snapshot.harnessExtensions) {
|
|
1745
|
+
this.extensionRegistry.register({
|
|
1746
|
+
ownerId: 'acp-agent',
|
|
1747
|
+
descriptor,
|
|
1748
|
+
transport: wireTransport,
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
if (snapshot.legacyExtensions.steering?.supported) {
|
|
1752
|
+
this.extensionRegistry.register({
|
|
1753
|
+
ownerId: 'acp-agent',
|
|
1754
|
+
descriptor: {
|
|
1755
|
+
id: 'acp.steering',
|
|
1756
|
+
version: 1,
|
|
1757
|
+
stability: 'experimental',
|
|
1758
|
+
methods: ['steer'],
|
|
1759
|
+
events: [],
|
|
1760
|
+
},
|
|
1761
|
+
transport: wireTransport,
|
|
1762
|
+
wireMethods: { steer: '_session/steering' },
|
|
1763
|
+
paramMappers: {
|
|
1764
|
+
steer: (envelope) => {
|
|
1765
|
+
const params = isRecord(envelope.params) ? envelope.params : {};
|
|
1766
|
+
return {
|
|
1767
|
+
sessionId: params.providerSessionId,
|
|
1768
|
+
prompt: params.prompt,
|
|
1769
|
+
};
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1772
|
+
capabilityPatch: { turns: { steer: true } },
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
const goal = snapshot.legacyExtensions.goal;
|
|
1776
|
+
const goalVersion = typeof goal?.version === 'number'
|
|
1777
|
+
? goal.version
|
|
1778
|
+
: goal?.version === '1'
|
|
1779
|
+
? 1
|
|
1780
|
+
: null;
|
|
1781
|
+
if (
|
|
1782
|
+
goal?.controlMethod &&
|
|
1783
|
+
goalVersion === 1 &&
|
|
1784
|
+
goal.actions.includes('set') &&
|
|
1785
|
+
goal.actions.includes('clear')
|
|
1786
|
+
) {
|
|
1787
|
+
const actions = goal.actions.filter((action) =>
|
|
1788
|
+
['set', 'pause', 'resume', 'clear'].includes(action));
|
|
1789
|
+
this.extensionRegistry.register({
|
|
1790
|
+
ownerId: 'acp-agent',
|
|
1791
|
+
descriptor: {
|
|
1792
|
+
id: 'acp.goal',
|
|
1793
|
+
version: goalVersion,
|
|
1794
|
+
stability: 'experimental',
|
|
1795
|
+
methods: actions,
|
|
1796
|
+
events: [],
|
|
1797
|
+
},
|
|
1798
|
+
transport: wireTransport,
|
|
1799
|
+
wireMethods: Object.fromEntries(actions.map((action) => [action, goal.controlMethod!])),
|
|
1800
|
+
paramMappers: Object.fromEntries(actions.map((action) => [
|
|
1801
|
+
action,
|
|
1802
|
+
(envelope: HarnessExtensionCallEnvelope) => {
|
|
1803
|
+
const params = isRecord(envelope.params) ? envelope.params : {};
|
|
1804
|
+
return {
|
|
1805
|
+
sessionId: params.providerSessionId,
|
|
1806
|
+
action,
|
|
1807
|
+
...(action === 'set' ? { objective: params.objective } : {}),
|
|
1808
|
+
};
|
|
1809
|
+
},
|
|
1810
|
+
])),
|
|
1811
|
+
capabilityPatch: { controls: { goals: true } },
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
if (snapshot.agentInfo?.name === '@agentclientprotocol/codex-acp') {
|
|
1815
|
+
this.extensionRegistry.register({
|
|
1816
|
+
ownerId: 'acp-agent',
|
|
1817
|
+
descriptor: {
|
|
1818
|
+
id: 'codex.control',
|
|
1819
|
+
version: 1,
|
|
1820
|
+
stability: 'experimental',
|
|
1821
|
+
methods: ['compact'],
|
|
1822
|
+
events: [],
|
|
1823
|
+
},
|
|
1824
|
+
transport: {
|
|
1825
|
+
request: async (_method, params, signal) => {
|
|
1826
|
+
const providerSessionId = isRecord(params)
|
|
1827
|
+
? String(params.providerSessionId ?? '')
|
|
1828
|
+
: '';
|
|
1829
|
+
return this.runControlPrompt(providerSessionId, '/compact', signal);
|
|
1830
|
+
},
|
|
1831
|
+
},
|
|
1832
|
+
paramMappers: {
|
|
1833
|
+
compact: (envelope) => ({
|
|
1834
|
+
providerSessionId: isRecord(envelope.params)
|
|
1835
|
+
? envelope.params.providerSessionId
|
|
1836
|
+
: null,
|
|
1837
|
+
}),
|
|
1838
|
+
},
|
|
1839
|
+
capabilityPatch: { turns: { compact: true } },
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
const effective = this.extensionRegistry.effectiveCapabilities(this.capabilities);
|
|
1843
|
+
for (const section of Object.keys(effective) as Array<keyof AgentProviderCapabilities>) {
|
|
1844
|
+
Object.assign(this.capabilities[section], effective[section]);
|
|
1845
|
+
}
|
|
1157
1846
|
}
|
|
1158
1847
|
|
|
1159
1848
|
private withStartupTimeout<T>(promise: Promise<T>) {
|
|
@@ -1178,6 +1867,8 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1178
1867
|
|
|
1179
1868
|
private markFailed(error: unknown) {
|
|
1180
1869
|
const message = errorMessage(error);
|
|
1870
|
+
this.settleActiveTurns('failed', message);
|
|
1871
|
+
this.cancelPendingPermissions();
|
|
1181
1872
|
this.status = {
|
|
1182
1873
|
...this.status,
|
|
1183
1874
|
state: 'failed',
|
|
@@ -1187,6 +1878,46 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1187
1878
|
this.emit('status', this.getStatus());
|
|
1188
1879
|
}
|
|
1189
1880
|
|
|
1881
|
+
private cancelPendingPermissions() {
|
|
1882
|
+
for (const [id, permission] of this.pendingPermissions) {
|
|
1883
|
+
clearTimeout(permission.timer);
|
|
1884
|
+
permission.resolve(cancelledPermission());
|
|
1885
|
+
this.pendingPermissions.delete(id);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
private settleActiveTurns(
|
|
1890
|
+
status: 'failed' | 'interrupted',
|
|
1891
|
+
error: string,
|
|
1892
|
+
) {
|
|
1893
|
+
for (const state of this.sessions.values()) {
|
|
1894
|
+
const mapper = state.activeMapper;
|
|
1895
|
+
if (!mapper) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const completed = mapper.complete(status, error);
|
|
1899
|
+
for (const itemUpdate of completed.updates) {
|
|
1900
|
+
this.emitItemUpdate(state, mapper.turnId, itemUpdate);
|
|
1901
|
+
}
|
|
1902
|
+
const turn = {
|
|
1903
|
+
...completed.turn,
|
|
1904
|
+
startedAt: state.turns.find(
|
|
1905
|
+
(candidate) => candidate.providerTurnId === mapper.turnId,
|
|
1906
|
+
)?.startedAt ?? null,
|
|
1907
|
+
};
|
|
1908
|
+
this.replaceTurn(state, turn);
|
|
1909
|
+
state.activeMapper = null;
|
|
1910
|
+
state.status = status;
|
|
1911
|
+
state.updatedAt = new Date().toISOString();
|
|
1912
|
+
this.emitRuntimeEvent({
|
|
1913
|
+
type: 'turn.completed',
|
|
1914
|
+
provider: 'acp',
|
|
1915
|
+
providerSessionId: state.providerSessionId,
|
|
1916
|
+
turn,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1190
1921
|
private emitRuntimeEvent(event: AgentRuntimeEvent) {
|
|
1191
1922
|
this.emit('event', event);
|
|
1192
1923
|
}
|