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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AcpSessionHydrator } from './session-hydrator';
|
|
4
|
+
|
|
5
|
+
describe('AcpSessionHydrator', () => {
|
|
6
|
+
it('reconstructs stable turns from replayed user-message boundaries', () => {
|
|
7
|
+
const hydrate = () => {
|
|
8
|
+
const hydrator = new AcpSessionHydrator('session-1');
|
|
9
|
+
hydrator.apply({
|
|
10
|
+
sessionUpdate: 'user_message_chunk',
|
|
11
|
+
messageId: 'user-1',
|
|
12
|
+
content: { type: 'text', text: 'First prompt' },
|
|
13
|
+
});
|
|
14
|
+
hydrator.apply({
|
|
15
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
16
|
+
messageId: 'reasoning-1',
|
|
17
|
+
content: { type: 'text', text: 'First reasoning' },
|
|
18
|
+
});
|
|
19
|
+
hydrator.apply({
|
|
20
|
+
sessionUpdate: 'tool_call',
|
|
21
|
+
toolCallId: 'tool-1',
|
|
22
|
+
title: 'Optional check',
|
|
23
|
+
kind: 'execute',
|
|
24
|
+
status: 'failed',
|
|
25
|
+
});
|
|
26
|
+
hydrator.apply({
|
|
27
|
+
sessionUpdate: 'agent_message_chunk',
|
|
28
|
+
messageId: 'agent-1',
|
|
29
|
+
content: { type: 'text', text: 'First response' },
|
|
30
|
+
});
|
|
31
|
+
hydrator.apply({
|
|
32
|
+
sessionUpdate: 'user_message_chunk',
|
|
33
|
+
messageId: 'user-2',
|
|
34
|
+
content: { type: 'text', text: 'Second prompt' },
|
|
35
|
+
});
|
|
36
|
+
hydrator.apply({
|
|
37
|
+
sessionUpdate: 'agent_message_chunk',
|
|
38
|
+
messageId: 'agent-2',
|
|
39
|
+
content: { type: 'text', text: 'Second response' },
|
|
40
|
+
});
|
|
41
|
+
return hydrator.complete();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const first = hydrate();
|
|
45
|
+
const second = hydrate();
|
|
46
|
+
expect(first.map((turn) => turn.providerTurnId)).toEqual([
|
|
47
|
+
'acp-hydrated:user-1',
|
|
48
|
+
'acp-hydrated:user-2',
|
|
49
|
+
]);
|
|
50
|
+
expect(second.map((turn) => turn.providerTurnId)).toEqual(
|
|
51
|
+
first.map((turn) => turn.providerTurnId),
|
|
52
|
+
);
|
|
53
|
+
expect(first[0]).toMatchObject({
|
|
54
|
+
status: 'completed',
|
|
55
|
+
items: [
|
|
56
|
+
{ id: 'user-1', kind: 'userMessage', text: 'First prompt' },
|
|
57
|
+
{ id: 'reasoning-1', kind: 'reasoning', text: 'First reasoning' },
|
|
58
|
+
{ id: 'tool-1', kind: 'commandExecution', status: 'failed' },
|
|
59
|
+
{ id: 'agent-1', kind: 'agentMessage', text: 'First response' },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
expect(first[1]?.items).toMatchObject([
|
|
63
|
+
{ id: 'user-2', kind: 'userMessage' },
|
|
64
|
+
{ id: 'agent-2', kind: 'agentMessage' },
|
|
65
|
+
]);
|
|
66
|
+
const coverageHydrator = new AcpSessionHydrator('session-coverage');
|
|
67
|
+
coverageHydrator.apply({
|
|
68
|
+
sessionUpdate: 'user_message_chunk',
|
|
69
|
+
messageId: 'coverage-user',
|
|
70
|
+
content: { type: 'text', text: 'Coverage prompt' },
|
|
71
|
+
});
|
|
72
|
+
coverageHydrator.apply({
|
|
73
|
+
sessionUpdate: 'agent_message_chunk',
|
|
74
|
+
messageId: 'coverage-agent',
|
|
75
|
+
content: { type: 'text', text: 'Coverage response' },
|
|
76
|
+
});
|
|
77
|
+
expect(coverageHydrator.coverage()).toEqual({
|
|
78
|
+
source: 'providerReplay',
|
|
79
|
+
completeness: 'unknown',
|
|
80
|
+
replayedTurnCount: 1,
|
|
81
|
+
replayedItemCount: 2,
|
|
82
|
+
providerIdentifiedTurnCount: 1,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps consecutive anonymous user chunks in one turn', () => {
|
|
87
|
+
const hydrator = new AcpSessionHydrator('session-anonymous');
|
|
88
|
+
hydrator.apply({
|
|
89
|
+
sessionUpdate: 'user_message_chunk',
|
|
90
|
+
content: { type: 'text', text: 'First ' },
|
|
91
|
+
});
|
|
92
|
+
hydrator.apply({
|
|
93
|
+
sessionUpdate: 'user_message_chunk',
|
|
94
|
+
content: { type: 'text', text: 'prompt' },
|
|
95
|
+
});
|
|
96
|
+
hydrator.apply({
|
|
97
|
+
sessionUpdate: 'agent_message_chunk',
|
|
98
|
+
content: { type: 'text', text: 'Response' },
|
|
99
|
+
});
|
|
100
|
+
expect(hydrator.complete()).toMatchObject([{
|
|
101
|
+
items: [
|
|
102
|
+
{ kind: 'userMessage', text: 'First prompt' },
|
|
103
|
+
{ kind: 'agentMessage', text: 'Response' },
|
|
104
|
+
],
|
|
105
|
+
}]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('attaches leading replay updates to the first user turn', () => {
|
|
109
|
+
const hydrator = new AcpSessionHydrator('session-leading');
|
|
110
|
+
hydrator.apply({
|
|
111
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
112
|
+
messageId: 'leading-reasoning',
|
|
113
|
+
content: { type: 'text', text: 'Recovered leading context' },
|
|
114
|
+
});
|
|
115
|
+
hydrator.apply({
|
|
116
|
+
sessionUpdate: 'user_message_chunk',
|
|
117
|
+
messageId: 'user-leading',
|
|
118
|
+
content: { type: 'text', text: 'Actual prompt' },
|
|
119
|
+
});
|
|
120
|
+
hydrator.apply({
|
|
121
|
+
sessionUpdate: 'agent_message_chunk',
|
|
122
|
+
messageId: 'agent-leading',
|
|
123
|
+
content: { type: 'text', text: 'Actual response' },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(hydrator.complete()).toMatchObject([{
|
|
127
|
+
providerTurnId: 'acp-hydrated:user-leading',
|
|
128
|
+
items: [
|
|
129
|
+
{ id: 'user-leading', kind: 'userMessage' },
|
|
130
|
+
{ id: 'leading-reasoning', kind: 'reasoning' },
|
|
131
|
+
{ id: 'agent-leading', kind: 'agentMessage' },
|
|
132
|
+
],
|
|
133
|
+
}]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AgentSessionHistoryCoverage,
|
|
5
|
+
AgentTurn,
|
|
6
|
+
} from '../../agent-runtime/src/types';
|
|
7
|
+
import { AcpTurnItemMapper } from './item-mapper';
|
|
8
|
+
|
|
9
|
+
interface HydratedTurnBuilder {
|
|
10
|
+
userMessageId: string | null;
|
|
11
|
+
mapper: AcpTurnItemMapper;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
sawNonUserUpdate: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function updateMessageId(update: { messageId?: unknown }) {
|
|
17
|
+
return typeof update.messageId === 'string' && update.messageId.trim()
|
|
18
|
+
? update.messageId.trim()
|
|
19
|
+
: null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AcpSessionHydrator {
|
|
23
|
+
private readonly turns: AgentTurn[] = [];
|
|
24
|
+
private readonly leadingUpdates: acp.SessionUpdate[] = [];
|
|
25
|
+
private current: HydratedTurnBuilder | null = null;
|
|
26
|
+
private turnIndex = 0;
|
|
27
|
+
private providerIdentifiedTurnCount = 0;
|
|
28
|
+
private readonly receivedAt = Date.now();
|
|
29
|
+
|
|
30
|
+
constructor(private readonly providerSessionId: string) {}
|
|
31
|
+
|
|
32
|
+
apply(update: acp.SessionUpdate) {
|
|
33
|
+
if (update.sessionUpdate === 'user_message_chunk') {
|
|
34
|
+
this.applyUserMessage(update);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
update.sessionUpdate === 'config_option_update' ||
|
|
39
|
+
update.sessionUpdate === 'current_mode_update' ||
|
|
40
|
+
update.sessionUpdate === 'available_commands_update' ||
|
|
41
|
+
update.sessionUpdate === 'session_info_update' ||
|
|
42
|
+
update.sessionUpdate === 'usage_update'
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!this.current) {
|
|
47
|
+
this.leadingUpdates.push(update);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const current = this.current;
|
|
51
|
+
this.flushLeadingUpdates(current);
|
|
52
|
+
current.sawNonUserUpdate = true;
|
|
53
|
+
current.mapper.apply(update);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
complete() {
|
|
57
|
+
this.finishCurrent();
|
|
58
|
+
return [...this.turns];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
coverage(): AgentSessionHistoryCoverage {
|
|
62
|
+
this.finishCurrent();
|
|
63
|
+
return {
|
|
64
|
+
source: 'providerReplay',
|
|
65
|
+
// ACP v1 has no authoritative history total; absence cannot prove completeness.
|
|
66
|
+
completeness: 'unknown',
|
|
67
|
+
replayedTurnCount: this.turns.length,
|
|
68
|
+
replayedItemCount: this.turns.reduce(
|
|
69
|
+
(count, turn) => count + turn.items.length,
|
|
70
|
+
0,
|
|
71
|
+
),
|
|
72
|
+
providerIdentifiedTurnCount: this.providerIdentifiedTurnCount,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private applyUserMessage(
|
|
77
|
+
update: Extract<acp.SessionUpdate, { sessionUpdate: 'user_message_chunk' }>,
|
|
78
|
+
) {
|
|
79
|
+
const messageId = updateMessageId(update);
|
|
80
|
+
const shouldStartTurn =
|
|
81
|
+
!this.current ||
|
|
82
|
+
this.current.sawNonUserUpdate ||
|
|
83
|
+
Boolean(
|
|
84
|
+
messageId &&
|
|
85
|
+
this.current.userMessageId &&
|
|
86
|
+
messageId !== this.current.userMessageId,
|
|
87
|
+
);
|
|
88
|
+
if (shouldStartTurn) {
|
|
89
|
+
if (this.current) {
|
|
90
|
+
this.finishCurrent();
|
|
91
|
+
}
|
|
92
|
+
this.startTurn(messageId);
|
|
93
|
+
}
|
|
94
|
+
const current = this.current!;
|
|
95
|
+
if (messageId && !current.userMessageId) {
|
|
96
|
+
current.userMessageId = messageId;
|
|
97
|
+
}
|
|
98
|
+
current.mapper.appendUserMessage(
|
|
99
|
+
update.content,
|
|
100
|
+
current.userMessageId ?? `${current.mapper.turnId}:user`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private startTurn(userMessageId: string | null) {
|
|
105
|
+
const index = this.turnIndex++;
|
|
106
|
+
const stableSource = userMessageId ?? `${this.providerSessionId}:${index}`;
|
|
107
|
+
const builder: HydratedTurnBuilder = {
|
|
108
|
+
userMessageId,
|
|
109
|
+
mapper: new AcpTurnItemMapper(
|
|
110
|
+
`acp-hydrated:${stableSource}`,
|
|
111
|
+
[],
|
|
112
|
+
'hydrate',
|
|
113
|
+
),
|
|
114
|
+
startedAt: new Date(this.receivedAt + index).toISOString(),
|
|
115
|
+
sawNonUserUpdate: false,
|
|
116
|
+
};
|
|
117
|
+
if (userMessageId) {
|
|
118
|
+
this.providerIdentifiedTurnCount += 1;
|
|
119
|
+
}
|
|
120
|
+
this.current = builder;
|
|
121
|
+
return builder;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private finishCurrent() {
|
|
125
|
+
if (!this.current && this.leadingUpdates.length > 0) {
|
|
126
|
+
this.startTurn(null);
|
|
127
|
+
}
|
|
128
|
+
if (!this.current) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.flushLeadingUpdates(this.current);
|
|
132
|
+
const completed = this.current.mapper.complete('completed').turn;
|
|
133
|
+
if (completed.items.length > 0) {
|
|
134
|
+
this.turns.push({
|
|
135
|
+
...completed,
|
|
136
|
+
startedAt: this.current.startedAt,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
this.current = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private flushLeadingUpdates(current: HydratedTurnBuilder) {
|
|
143
|
+
for (const update of this.leadingUpdates.splice(0)) {
|
|
144
|
+
current.mapper.apply(update);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -4,8 +4,12 @@ import { AcpTerminalService } from './terminal-service';
|
|
|
4
4
|
|
|
5
5
|
describe('AcpTerminalService', () => {
|
|
6
6
|
it('parses agents that send command and argv as one ACP command string', async () => {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const operations: Array<{ operation: string; sessionId: string; path: string }> = [];
|
|
8
|
+
const service = new AcpTerminalService(
|
|
9
|
+
() => process.cwd(),
|
|
10
|
+
(operation) => operations.push(operation),
|
|
11
|
+
);
|
|
12
|
+
const created = await service.create({
|
|
9
13
|
sessionId: 'session-1',
|
|
10
14
|
command: `"${process.execPath}" -e "process.stdout.write('ACP_TERMINAL_OK')"`,
|
|
11
15
|
args: [],
|
|
@@ -23,9 +27,24 @@ describe('AcpTerminalService', () => {
|
|
|
23
27
|
expect(exit.exitCode).toBe(0);
|
|
24
28
|
expect(output.output).toBe('ACP_TERMINAL_OK');
|
|
25
29
|
expect(output.truncated).toBe(false);
|
|
30
|
+
expect(operations).toMatchObject([{
|
|
31
|
+
operation: 'terminal.create',
|
|
32
|
+
sessionId: 'session-1',
|
|
33
|
+
path: await import('node:fs/promises').then((fs) => fs.realpath(process.cwd())),
|
|
34
|
+
}]);
|
|
26
35
|
service.release({
|
|
27
36
|
sessionId: 'session-1',
|
|
28
37
|
terminalId: created.terminalId,
|
|
29
38
|
});
|
|
30
39
|
});
|
|
40
|
+
|
|
41
|
+
it('rejects terminal working directories outside the session workspace', async () => {
|
|
42
|
+
const service = new AcpTerminalService(() => process.cwd());
|
|
43
|
+
await expect(service.create({
|
|
44
|
+
sessionId: 'session-1',
|
|
45
|
+
command: process.execPath,
|
|
46
|
+
args: ['--version'],
|
|
47
|
+
cwd: '/tmp',
|
|
48
|
+
})).rejects.toThrow(/must stay inside the session workspace/);
|
|
49
|
+
});
|
|
31
50
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
parseCommandLine,
|
|
8
8
|
spawnProcess,
|
|
9
9
|
} from '../../process-runtime/src/index';
|
|
10
|
+
import { resolveAcpWorkspacePath } from './workspace-boundary';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_OUTPUT_BYTE_LIMIT = 1024 * 1024;
|
|
12
13
|
|
|
@@ -38,11 +39,30 @@ function retainedOutput(state: AcpTerminalState) {
|
|
|
38
39
|
export class AcpTerminalService {
|
|
39
40
|
private readonly terminals = new Map<string, AcpTerminalState>();
|
|
40
41
|
|
|
41
|
-
constructor(
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly sessionCwd: (sessionId: string) => string | null,
|
|
44
|
+
private readonly onOperation: (input: {
|
|
45
|
+
operation: 'terminal.create';
|
|
46
|
+
sessionId: string;
|
|
47
|
+
path: string;
|
|
48
|
+
}) => void = () => undefined,
|
|
49
|
+
) {}
|
|
42
50
|
|
|
43
|
-
create(params: acp.CreateTerminalRequest): acp.CreateTerminalResponse {
|
|
51
|
+
async create(params: acp.CreateTerminalRequest): Promise<acp.CreateTerminalResponse> {
|
|
44
52
|
const terminalId = randomUUID();
|
|
45
|
-
const
|
|
53
|
+
const sessionCwd = this.sessionCwd(params.sessionId);
|
|
54
|
+
if (!sessionCwd) {
|
|
55
|
+
throw new Error(`ACP session workspace not found: ${params.sessionId}`);
|
|
56
|
+
}
|
|
57
|
+
const cwd = await resolveAcpWorkspacePath(
|
|
58
|
+
sessionCwd,
|
|
59
|
+
params.cwd ?? sessionCwd,
|
|
60
|
+
);
|
|
61
|
+
this.onOperation({
|
|
62
|
+
operation: 'terminal.create',
|
|
63
|
+
sessionId: params.sessionId,
|
|
64
|
+
path: cwd,
|
|
65
|
+
});
|
|
46
66
|
const parsed = params.args && params.args.length > 0
|
|
47
67
|
? { command: params.command, args: params.args }
|
|
48
68
|
: parseCommandLine(params.command);
|