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.
Files changed (38) hide show
  1. package/README.md +31 -0
  2. package/apps/supervisor-api/dist/index.js +2132 -417
  3. package/apps/supervisor-web/dist/assets/index-BBq6mr8o.js +22 -0
  4. package/apps/supervisor-web/dist/assets/{index-Dy8PgXgw.css → index-Bg8FdhS1.css} +1 -1
  5. package/apps/supervisor-web/dist/assets/thread-ui-C5pxOmDJ.js +3974 -0
  6. package/apps/supervisor-web/dist/index.html +3 -3
  7. package/package.json +7 -1
  8. package/packages/acp/src/agent-catalog.test.ts +25 -0
  9. package/packages/acp/src/agent-catalog.ts +3 -1
  10. package/packages/acp/src/capabilities.ts +139 -0
  11. package/packages/acp/src/capability-parity.test.ts +99 -0
  12. package/packages/acp/src/catalog-runtime.test.ts +165 -6
  13. package/packages/acp/src/catalog-runtime.ts +234 -25
  14. package/packages/acp/src/extension-registry.test.ts +198 -0
  15. package/packages/acp/src/extension-registry.ts +285 -0
  16. package/packages/acp/src/extensions.test.ts +45 -0
  17. package/packages/acp/src/extensions.ts +103 -0
  18. package/packages/acp/src/harness-contract.test.ts +81 -0
  19. package/packages/acp/src/harness-contract.ts +62 -0
  20. package/packages/acp/src/index.ts +7 -0
  21. package/packages/acp/src/item-mapper.ts +28 -2
  22. package/packages/acp/src/prompt-content.test.ts +90 -0
  23. package/packages/acp/src/prompt-content.ts +99 -0
  24. package/packages/acp/src/runtimeAdapter.test.ts +471 -5
  25. package/packages/acp/src/runtimeAdapter.ts +791 -60
  26. package/packages/acp/src/session-hydrator.test.ts +135 -0
  27. package/packages/acp/src/session-hydrator.ts +147 -0
  28. package/packages/acp/src/terminal-service.test.ts +21 -2
  29. package/packages/acp/src/terminal-service.ts +23 -3
  30. package/packages/acp/src/test/fixtures/fake-acp-agent.mjs +514 -0
  31. package/packages/acp/src/workspace-boundary.test.ts +32 -0
  32. package/packages/acp/src/workspace-boundary.ts +47 -0
  33. package/packages/agent-runtime/src/types.ts +61 -1
  34. package/packages/codex/src/appServerManager.test.ts +2 -2
  35. package/packages/db/src/repositories.ts +3 -2
  36. package/packages/shared/src/index.ts +32 -1
  37. package/apps/supervisor-web/dist/assets/index-BXypG9kl.js +0 -22
  38. package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +0 -3968
@@ -0,0 +1,285 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ import type { AgentProviderCapabilities } from '../../agent-runtime/src/types';
4
+ import {
5
+ REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
6
+ type HarnessExtensionCallEnvelope,
7
+ type HarnessExtensionDescriptor,
8
+ type HarnessExtensionErrorEnvelope,
9
+ type HarnessExtensionEventEnvelope,
10
+ createHarnessExtensionCall,
11
+ harnessExtensionMethodName,
12
+ } from './extensions';
13
+
14
+ interface HarnessExtensionTransport {
15
+ request(
16
+ method: string,
17
+ params: unknown,
18
+ signal: AbortSignal,
19
+ ): Promise<unknown>;
20
+ }
21
+
22
+ interface RegisteredExtension {
23
+ ownerId: string;
24
+ descriptor: HarnessExtensionDescriptor;
25
+ transport: HarnessExtensionTransport;
26
+ wireMethods: Record<string, string>;
27
+ capabilityPatch: AgentProviderCapabilityPatch | null;
28
+ paramMappers: Record<
29
+ string,
30
+ (envelope: HarnessExtensionCallEnvelope) => unknown
31
+ >;
32
+ }
33
+
34
+ export type AgentProviderCapabilityPatch = {
35
+ [Section in keyof AgentProviderCapabilities]?: Partial<
36
+ AgentProviderCapabilities[Section]
37
+ >;
38
+ };
39
+
40
+ interface CachedOperation {
41
+ fingerprint: string;
42
+ promise: Promise<unknown>;
43
+ }
44
+
45
+ export class HarnessExtensionInvocationError extends Error {
46
+ constructor(public readonly payload: HarnessExtensionErrorEnvelope) {
47
+ super(payload.message);
48
+ this.name = 'HarnessExtensionInvocationError';
49
+ }
50
+ }
51
+
52
+ function extensionKey(extensionId: string, version: number) {
53
+ return `${extensionId}@${version}`;
54
+ }
55
+
56
+ function operationFingerprint(input: {
57
+ extensionId: string;
58
+ extensionVersion: number;
59
+ method: string;
60
+ params: unknown;
61
+ }) {
62
+ return JSON.stringify(input);
63
+ }
64
+
65
+ export class HarnessExtensionRegistry extends EventEmitter {
66
+ private readonly extensions = new Map<string, RegisteredExtension>();
67
+ private readonly operations = new Map<string, CachedOperation>();
68
+ private readonly eventSequences = new Set<string>();
69
+
70
+ register(input: {
71
+ ownerId: string;
72
+ descriptor: HarnessExtensionDescriptor;
73
+ transport: HarnessExtensionTransport;
74
+ wireMethods?: Record<string, string>;
75
+ capabilityPatch?: AgentProviderCapabilityPatch;
76
+ paramMappers?: Record<
77
+ string,
78
+ (envelope: HarnessExtensionCallEnvelope) => unknown
79
+ >;
80
+ }) {
81
+ const key = extensionKey(input.descriptor.id, input.descriptor.version);
82
+ const existing = this.extensions.get(key);
83
+ if (existing && existing.ownerId !== input.ownerId) {
84
+ throw new Error(
85
+ `Harness extension ${key} is already owned by ${existing.ownerId}.`,
86
+ );
87
+ }
88
+ if (new Set(input.descriptor.methods).size !== input.descriptor.methods.length) {
89
+ throw new Error(`Harness extension ${key} declares duplicate methods.`);
90
+ }
91
+ if (new Set(input.descriptor.events).size !== input.descriptor.events.length) {
92
+ throw new Error(`Harness extension ${key} declares duplicate events.`);
93
+ }
94
+ this.extensions.set(key, {
95
+ ownerId: input.ownerId,
96
+ descriptor: structuredClone(input.descriptor),
97
+ transport: input.transport,
98
+ wireMethods: { ...(input.wireMethods ?? {}) },
99
+ capabilityPatch: input.capabilityPatch
100
+ ? structuredClone(input.capabilityPatch)
101
+ : null,
102
+ paramMappers: { ...(input.paramMappers ?? {}) },
103
+ });
104
+ }
105
+
106
+ unregisterOwner(ownerId: string) {
107
+ for (const [key, extension] of this.extensions) {
108
+ if (extension.ownerId === ownerId) {
109
+ this.extensions.delete(key);
110
+ }
111
+ }
112
+ }
113
+
114
+ list() {
115
+ return [...this.extensions.values()].map((extension) => ({
116
+ ownerId: extension.ownerId,
117
+ descriptor: structuredClone(extension.descriptor),
118
+ }));
119
+ }
120
+
121
+ supports(extensionId: string, version: number, method: string) {
122
+ return this.extensions
123
+ .get(extensionKey(extensionId, version))
124
+ ?.descriptor.methods.includes(method) ?? false;
125
+ }
126
+
127
+ effectiveCapabilities(base: AgentProviderCapabilities) {
128
+ const effective = structuredClone(base);
129
+ for (const extension of this.extensions.values()) {
130
+ if (!extension.capabilityPatch) continue;
131
+ for (const [section, patch] of Object.entries(extension.capabilityPatch)) {
132
+ Object.assign(
133
+ effective[section as keyof AgentProviderCapabilities],
134
+ patch,
135
+ );
136
+ }
137
+ }
138
+ return effective;
139
+ }
140
+
141
+ invoke<T = unknown>(input: {
142
+ extensionId: string;
143
+ extensionVersion: number;
144
+ method: string;
145
+ operationId: string;
146
+ idempotencyKey: string;
147
+ params: unknown;
148
+ timeoutMs?: number;
149
+ signal?: AbortSignal;
150
+ }): Promise<T> {
151
+ const key = extensionKey(input.extensionId, input.extensionVersion);
152
+ const extension = this.extensions.get(key);
153
+ if (!extension || !extension.descriptor.methods.includes(input.method)) {
154
+ return Promise.reject(this.error(input, {
155
+ code: 'extension_method_unavailable',
156
+ message: `Harness extension method is unavailable: ${key}/${input.method}`,
157
+ retryable: false,
158
+ }));
159
+ }
160
+ if (input.signal?.aborted) {
161
+ return Promise.reject(this.error(input, {
162
+ code: 'extension_cancelled',
163
+ message: 'Harness extension request was cancelled before dispatch.',
164
+ retryable: true,
165
+ }));
166
+ }
167
+ const fingerprint = operationFingerprint(input);
168
+ const cached = this.operations.get(input.idempotencyKey);
169
+ if (cached) {
170
+ if (cached.fingerprint !== fingerprint) {
171
+ return Promise.reject(this.error(input, {
172
+ code: 'idempotency_conflict',
173
+ message: 'Harness extension idempotency key was reused for another operation.',
174
+ retryable: false,
175
+ }));
176
+ }
177
+ return cached.promise as Promise<T>;
178
+ }
179
+
180
+ const envelope = createHarnessExtensionCall(input);
181
+ const controller = new AbortController();
182
+ const timeoutMs = input.timeoutMs ?? 30_000;
183
+ let timer: NodeJS.Timeout | null = null;
184
+ let abort: (() => void) | null = null;
185
+ const request = new Promise<T>((resolve, reject) => {
186
+ abort = () => {
187
+ controller.abort(input.signal?.reason);
188
+ reject(this.error(input, {
189
+ code: 'extension_cancelled',
190
+ message: 'Harness extension request was cancelled.',
191
+ retryable: true,
192
+ }));
193
+ };
194
+ input.signal?.addEventListener('abort', abort, { once: true });
195
+ timer = setTimeout(() => {
196
+ controller.abort(new Error('Harness extension request timed out.'));
197
+ reject(this.error(input, {
198
+ code: 'extension_timeout',
199
+ message: `Harness extension request timed out after ${timeoutMs}ms.`,
200
+ retryable: true,
201
+ }));
202
+ }, timeoutMs);
203
+ void Promise.resolve().then(() => extension.transport.request(
204
+ extension.wireMethods[input.method] ?? harnessExtensionMethodName(
205
+ input.extensionId,
206
+ input.extensionVersion,
207
+ input.method,
208
+ ),
209
+ extension.paramMappers[input.method]?.(envelope) ?? envelope,
210
+ controller.signal,
211
+ )).then((value) => resolve(value as T), (cause) => reject(
212
+ cause instanceof HarnessExtensionInvocationError
213
+ ? cause
214
+ : this.error(input, {
215
+ code: 'extension_request_failed',
216
+ message: cause instanceof Error ? cause.message : String(cause),
217
+ retryable: false,
218
+ }),
219
+ ));
220
+ }).finally(() => {
221
+ if (timer) clearTimeout(timer);
222
+ if (abort) input.signal?.removeEventListener('abort', abort);
223
+ });
224
+ this.operations.set(input.idempotencyKey, { fingerprint, promise: request });
225
+ request.catch(() => {
226
+ if (this.operations.get(input.idempotencyKey)?.promise === request) {
227
+ this.operations.delete(input.idempotencyKey);
228
+ }
229
+ });
230
+ while (this.operations.size > 256) {
231
+ this.operations.delete(this.operations.keys().next().value!);
232
+ }
233
+ return request;
234
+ }
235
+
236
+ handleEvent(ownerId: string, event: HarnessExtensionEventEnvelope) {
237
+ if (event.protocol !== REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL) {
238
+ throw new Error('Harness extension event protocol is unsupported.');
239
+ }
240
+ const extension = this.extensions.get(
241
+ extensionKey(event.extensionId, event.extensionVersion),
242
+ );
243
+ if (!extension || extension.ownerId !== ownerId) {
244
+ throw new Error('Harness extension event owner does not match registration.');
245
+ }
246
+ if (!extension.descriptor.events.includes(event.event)) {
247
+ throw new Error(`Harness extension event is not declared: ${event.event}`);
248
+ }
249
+ if (event.sequence !== null) {
250
+ const sequenceKey = [
251
+ ownerId,
252
+ event.extensionId,
253
+ event.extensionVersion,
254
+ event.providerSessionId,
255
+ event.event,
256
+ event.sequence,
257
+ ].join('\0');
258
+ if (this.eventSequences.has(sequenceKey)) {
259
+ return false;
260
+ }
261
+ this.eventSequences.add(sequenceKey);
262
+ }
263
+ this.emit('event', structuredClone(event));
264
+ return true;
265
+ }
266
+
267
+ private error(
268
+ input: {
269
+ extensionId: string;
270
+ extensionVersion: number;
271
+ method: string;
272
+ operationId: string;
273
+ },
274
+ error: { code: string; message: string; retryable: boolean },
275
+ ) {
276
+ return new HarnessExtensionInvocationError({
277
+ protocol: REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
278
+ extensionId: input.extensionId,
279
+ extensionVersion: input.extensionVersion,
280
+ method: input.method,
281
+ operationId: input.operationId,
282
+ ...error,
283
+ });
284
+ }
285
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
5
+ createHarnessExtensionCall,
6
+ harnessExtensionMethodName,
7
+ } from './extensions';
8
+
9
+ describe('harness extension contract', () => {
10
+ it('creates versioned, idempotent extension calls', () => {
11
+ expect(createHarnessExtensionCall({
12
+ extensionId: 'codex.session',
13
+ extensionVersion: 1,
14
+ method: 'compact',
15
+ operationId: 'operation-1',
16
+ idempotencyKey: 'thread-1:compact:operation-1',
17
+ params: { providerSessionId: 'session-1' },
18
+ })).toEqual({
19
+ protocol: REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
20
+ extensionId: 'codex.session',
21
+ extensionVersion: 1,
22
+ method: 'compact',
23
+ operationId: 'operation-1',
24
+ idempotencyKey: 'thread-1:compact:operation-1',
25
+ params: { providerSessionId: 'session-1' },
26
+ });
27
+ expect(harnessExtensionMethodName('codex.session', 1, 'compact')).toBe(
28
+ 'remoteCodex/codex.session/v1/compact',
29
+ );
30
+ });
31
+
32
+ it('rejects ambiguous names and missing idempotency fields', () => {
33
+ expect(() => harnessExtensionMethodName('Codex Session', 1, 'compact')).toThrow(
34
+ /lowercase extension identifier/,
35
+ );
36
+ expect(() => createHarnessExtensionCall({
37
+ extensionId: 'codex.session',
38
+ extensionVersion: 1,
39
+ method: 'compact',
40
+ operationId: '',
41
+ idempotencyKey: '',
42
+ params: {},
43
+ })).toThrow(/operation id is required/);
44
+ });
45
+ });
@@ -0,0 +1,103 @@
1
+ export const REMOTE_CODEX_HARNESS_EXTENSION_META_KEY =
2
+ 'remoteCodex.harnessExtensions';
3
+ export const REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL =
4
+ 'remote-codex.harness-extension/v1';
5
+ export const REMOTE_CODEX_HARNESS_EXTENSION_VERSION = 1;
6
+ export const REMOTE_CODEX_HARNESS_EXTENSION_EVENT_METHOD =
7
+ 'remoteCodex/harness-extension/event';
8
+
9
+ export type HarnessExtensionStability = 'experimental' | 'stable';
10
+
11
+ export interface HarnessExtensionDescriptor {
12
+ id: string;
13
+ version: number;
14
+ stability: HarnessExtensionStability;
15
+ methods: string[];
16
+ events: string[];
17
+ }
18
+
19
+ export interface HarnessExtensionCallEnvelope<TParams = unknown> {
20
+ protocol: typeof REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL;
21
+ extensionId: string;
22
+ extensionVersion: number;
23
+ method: string;
24
+ operationId: string;
25
+ idempotencyKey: string;
26
+ params: TParams;
27
+ }
28
+
29
+ export interface HarnessExtensionEventEnvelope<TPayload = unknown> {
30
+ protocol: typeof REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL;
31
+ extensionId: string;
32
+ extensionVersion: number;
33
+ event: string;
34
+ operationId: string | null;
35
+ providerSessionId: string;
36
+ providerTurnId: string | null;
37
+ providerItemId: string | null;
38
+ sequence: number | null;
39
+ payload: TPayload;
40
+ }
41
+
42
+ export interface HarnessExtensionErrorEnvelope {
43
+ protocol: typeof REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL;
44
+ extensionId: string;
45
+ extensionVersion: number;
46
+ method: string;
47
+ operationId: string;
48
+ code: string;
49
+ message: string;
50
+ retryable: boolean;
51
+ details?: Record<string, unknown>;
52
+ }
53
+
54
+ const extensionSegmentPattern = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
55
+
56
+ function assertExtensionSegment(value: string, label: string) {
57
+ if (!extensionSegmentPattern.test(value)) {
58
+ throw new Error(`${label} must be a lowercase extension identifier.`);
59
+ }
60
+ }
61
+
62
+ export function harnessExtensionMethodName(
63
+ extensionId: string,
64
+ version: number,
65
+ method: string,
66
+ ) {
67
+ assertExtensionSegment(extensionId, 'Extension id');
68
+ assertExtensionSegment(method, 'Extension method');
69
+ if (!Number.isInteger(version) || version < 1) {
70
+ throw new Error('Extension version must be a positive integer.');
71
+ }
72
+ return `remoteCodex/${extensionId}/v${version}/${method}`;
73
+ }
74
+
75
+ export function createHarnessExtensionCall<TParams>(input: {
76
+ extensionId: string;
77
+ extensionVersion: number;
78
+ method: string;
79
+ operationId: string;
80
+ idempotencyKey: string;
81
+ params: TParams;
82
+ }): HarnessExtensionCallEnvelope<TParams> {
83
+ harnessExtensionMethodName(
84
+ input.extensionId,
85
+ input.extensionVersion,
86
+ input.method,
87
+ );
88
+ if (!input.operationId.trim()) {
89
+ throw new Error('Extension operation id is required.');
90
+ }
91
+ if (!input.idempotencyKey.trim()) {
92
+ throw new Error('Extension idempotency key is required.');
93
+ }
94
+ return {
95
+ protocol: REMOTE_CODEX_HARNESS_EXTENSION_PROTOCOL,
96
+ extensionId: input.extensionId,
97
+ extensionVersion: input.extensionVersion,
98
+ method: input.method,
99
+ operationId: input.operationId,
100
+ idempotencyKey: input.idempotencyKey,
101
+ params: input.params,
102
+ };
103
+ }
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { AgentProviderCapabilities } from '../../agent-runtime/src/index';
4
+ import type { AcpNegotiatedCapabilitySnapshot } from './capabilities';
5
+ import { acpCapabilities } from './runtimeAdapter';
6
+ import { assertAcpHarnessContract } from './harness-contract';
7
+
8
+ function negotiated(name: string): AcpNegotiatedCapabilitySnapshot {
9
+ return {
10
+ protocolVersion: 1,
11
+ agentInfo: { name, title: null, version: 'test' },
12
+ agentCapabilities: {},
13
+ harnessExtensions: [],
14
+ legacyExtensions: { steering: null, goal: null },
15
+ };
16
+ }
17
+
18
+ function capabilities(
19
+ patch: Partial<AgentProviderCapabilities>,
20
+ ): AgentProviderCapabilities {
21
+ const value = structuredClone(acpCapabilities);
22
+ for (const section of Object.keys(patch) as Array<keyof AgentProviderCapabilities>) {
23
+ Object.assign(value[section], patch[section]);
24
+ }
25
+ return value;
26
+ }
27
+
28
+ describe('ACP harness contract kit', () => {
29
+ it('validates the Codex extension profile', () => {
30
+ expect(assertAcpHarnessContract({
31
+ negotiated: negotiated('@agentclientprotocol/codex-acp'),
32
+ effectiveCapabilities: capabilities({
33
+ sessions: { ...acpCapabilities.sessions, list: true, load: true, resume: true },
34
+ turns: { ...acpCapabilities.turns, steer: true, compact: true },
35
+ controls: { ...acpCapabilities.controls, goals: true, performanceMode: true },
36
+ }),
37
+ expectedAgentName: '@agentclientprotocol/codex-acp',
38
+ required: [
39
+ 'sessions.list',
40
+ 'sessions.load',
41
+ 'sessions.resume',
42
+ 'turns.steer',
43
+ 'turns.compact',
44
+ 'controls.goals',
45
+ 'controls.performanceMode',
46
+ ],
47
+ unsupported: ['branching.fork', 'branching.hardRollback'],
48
+ })).toMatchObject({ protocolVersion: 1 });
49
+ });
50
+
51
+ it('validates a portable fork profile without Codex-only compact', () => {
52
+ expect(assertAcpHarnessContract({
53
+ negotiated: negotiated('@agentclientprotocol/claude-agent-acp'),
54
+ effectiveCapabilities: capabilities({
55
+ sessions: { ...acpCapabilities.sessions, list: true, load: true, resume: true },
56
+ turns: { ...acpCapabilities.turns, steer: true },
57
+ branching: { ...acpCapabilities.branching, fork: true },
58
+ controls: { ...acpCapabilities.controls, goals: true },
59
+ }),
60
+ expectedAgentName: '@agentclientprotocol/claude-agent-acp',
61
+ required: [
62
+ 'sessions.list',
63
+ 'sessions.load',
64
+ 'sessions.resume',
65
+ 'turns.steer',
66
+ 'branching.fork',
67
+ 'controls.goals',
68
+ ],
69
+ unsupported: ['turns.compact', 'branching.hardRollback'],
70
+ })).toMatchObject({ protocolVersion: 1 });
71
+ });
72
+
73
+ it('fails closed when a required capability disappears', () => {
74
+ expect(() => assertAcpHarnessContract({
75
+ negotiated: negotiated('minimal-agent'),
76
+ effectiveCapabilities: capabilities({}),
77
+ expectedAgentName: 'minimal-agent',
78
+ required: ['sessions.resume'],
79
+ })).toThrow(/missing=sessions\.resume/);
80
+ });
81
+ });
@@ -0,0 +1,62 @@
1
+ import type { AgentProviderCapabilities } from '../../agent-runtime/src/index';
2
+ import type { AcpNegotiatedCapabilitySnapshot } from './capabilities';
3
+
4
+ export type AcpHarnessContractCapability =
5
+ | 'sessions.list'
6
+ | 'sessions.load'
7
+ | 'sessions.resume'
8
+ | 'sessions.close'
9
+ | 'sessions.delete'
10
+ | 'turns.steer'
11
+ | 'turns.compact'
12
+ | 'branching.fork'
13
+ | 'branching.hardRollback'
14
+ | 'controls.performanceMode'
15
+ | 'controls.goals';
16
+
17
+ function capabilityValue(
18
+ capabilities: AgentProviderCapabilities,
19
+ capability: AcpHarnessContractCapability,
20
+ ) {
21
+ const [section, key] = capability.split('.') as [
22
+ keyof AgentProviderCapabilities,
23
+ string,
24
+ ];
25
+ return capabilities[section][
26
+ key as keyof AgentProviderCapabilities[typeof section]
27
+ ] === true;
28
+ }
29
+
30
+ export function assertAcpHarnessContract(input: {
31
+ negotiated: AcpNegotiatedCapabilitySnapshot | null;
32
+ effectiveCapabilities: AgentProviderCapabilities;
33
+ expectedAgentName: string;
34
+ required: AcpHarnessContractCapability[];
35
+ unsupported?: AcpHarnessContractCapability[];
36
+ }) {
37
+ const actualAgentName = input.negotiated?.agentInfo?.name ?? null;
38
+ if (actualAgentName !== input.expectedAgentName) {
39
+ throw new Error(
40
+ `ACP harness contract expected ${input.expectedAgentName}, received ` +
41
+ `${actualAgentName ?? 'unknown'}.`,
42
+ );
43
+ }
44
+ const missing = input.required.filter(
45
+ (capability) => !capabilityValue(input.effectiveCapabilities, capability),
46
+ );
47
+ const unexpectedlySupported = (input.unsupported ?? []).filter(
48
+ (capability) => capabilityValue(input.effectiveCapabilities, capability),
49
+ );
50
+ if (missing.length > 0 || unexpectedlySupported.length > 0) {
51
+ throw new Error(
52
+ `ACP harness contract mismatch: missing=${missing.join(',') || 'none'}; ` +
53
+ `unexpected=${unexpectedlySupported.join(',') || 'none'}.`,
54
+ );
55
+ }
56
+ return {
57
+ agentName: actualAgentName,
58
+ protocolVersion: input.negotiated!.protocolVersion,
59
+ required: [...input.required],
60
+ unsupported: [...(input.unsupported ?? [])],
61
+ };
62
+ }
@@ -1,5 +1,12 @@
1
1
  export * from './agent-catalog';
2
+ export * from './capabilities';
2
3
  export * from './catalog-runtime';
4
+ export * from './extensions';
5
+ export * from './harness-contract';
6
+ export * from './extension-registry';
3
7
  export * from './item-mapper';
8
+ export * from './prompt-content';
4
9
  export * from './runtimeAdapter';
10
+ export * from './session-hydrator';
5
11
  export * from './terminal-service';
12
+ export * from './workspace-boundary';
@@ -23,6 +23,8 @@ export interface AcpMappedSessionUpdate {
23
23
  usage: { used: number; size: number; cost?: acp.Cost | null } | null;
24
24
  }
25
25
 
26
+ export type AcpMappingMode = 'hydrate' | 'live';
27
+
26
28
  interface StoredToolCall extends acp.ToolCall {
27
29
  title: string;
28
30
  }
@@ -227,12 +229,24 @@ export class AcpTurnItemMapper {
227
229
  constructor(
228
230
  readonly turnId: string,
229
231
  initialItems: AgentHistoryItem[] = [],
232
+ readonly mode: AcpMappingMode = 'live',
230
233
  ) {
231
234
  for (const item of initialItems) {
232
235
  this.upsert(item);
233
236
  }
234
237
  }
235
238
 
239
+ appendUserMessage(content: acp.ContentBlock, itemId: string) {
240
+ const current = this.items.get(itemId);
241
+ const item: AgentHistoryItem = {
242
+ ...(current ?? { id: itemId, kind: 'userMessage' as const }),
243
+ text: `${current?.text ?? ''}${acpContentBlockText(content)}`,
244
+ status: modeStatus(this.mode),
245
+ };
246
+ this.upsert(item);
247
+ return item;
248
+ }
249
+
236
250
  turn(status: AgentTurn['status'] = 'inProgress', error: string | null = null): AgentTurn {
237
251
  return {
238
252
  providerTurnId: this.turnId,
@@ -257,7 +271,8 @@ export class AcpTurnItemMapper {
257
271
  case 'agent_message_chunk': {
258
272
  result.itemUpdates.push(...this.finishThought());
259
273
  const delta = acpContentBlockText(update.content);
260
- const itemId = this.currentAgentMessageId ?? `${this.turnId}:agent:${++this.agentMessageIndex}`;
274
+ const itemId = this.currentAgentMessageId ?? messageId(update) ??
275
+ `${this.turnId}:agent:${++this.agentMessageIndex}`;
261
276
  this.currentAgentMessageId = itemId;
262
277
  const current = this.items.get(itemId);
263
278
  const item: AgentHistoryItem = {
@@ -272,7 +287,8 @@ export class AcpTurnItemMapper {
272
287
  case 'agent_thought_chunk': {
273
288
  result.itemUpdates.push(...this.finishAgentMessage());
274
289
  const delta = acpContentBlockText(update.content);
275
- const itemId = this.currentThoughtId ?? `${this.turnId}:thought:${++this.thoughtIndex}`;
290
+ const itemId = this.currentThoughtId ?? messageId(update) ??
291
+ `${this.turnId}:thought:${++this.thoughtIndex}`;
276
292
  this.currentThoughtId = itemId;
277
293
  const current = this.items.get(itemId);
278
294
  const item: AgentHistoryItem = {
@@ -471,3 +487,13 @@ export class AcpTurnItemMapper {
471
487
  return [...this.finishAgentMessage(), ...this.finishThought()];
472
488
  }
473
489
  }
490
+
491
+ function messageId(update: { messageId?: unknown }) {
492
+ return typeof update.messageId === 'string' && update.messageId.trim()
493
+ ? update.messageId.trim()
494
+ : null;
495
+ }
496
+
497
+ function modeStatus(mode: AcpMappingMode) {
498
+ return mode === 'hydrate' ? 'completed' : 'running';
499
+ }