remote-codex 0.11.44 → 0.11.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -5
- package/apps/relay-server/dist/index.js +18 -2
- package/apps/supervisor-api/dist/index.js +10034 -2782
- package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
- package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
- package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
- package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
- package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
- package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
- package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
- package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
- package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
- package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
- package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
- package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
- package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/apps/supervisor-web/dist/index.html +4 -4
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows-device-manager.zh.md +143 -0
- package/docs/windows-device-setup.zh.md +263 -0
- package/docs/windows-one-click-installer-research.zh.md +481 -0
- package/docs/windows.md +87 -0
- package/package.json +20 -4
- package/packages/acp/src/agent-catalog.test.ts +57 -0
- package/packages/acp/src/agent-catalog.ts +361 -0
- package/packages/acp/src/catalog-runtime.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.ts +507 -0
- package/packages/acp/src/codex-environment.test.ts +58 -0
- package/packages/acp/src/codex-environment.ts +33 -0
- package/packages/acp/src/index.ts +5 -0
- package/packages/acp/src/item-mapper.test.ts +137 -0
- package/packages/acp/src/item-mapper.ts +473 -0
- package/packages/acp/src/runtimeAdapter.test.ts +79 -0
- package/packages/acp/src/runtimeAdapter.ts +1193 -0
- package/packages/acp/src/terminal-service.test.ts +31 -0
- package/packages/acp/src/terminal-service.ts +137 -0
- package/packages/agent-runtime/src/runtime-errors.ts +1 -0
- package/packages/agent-runtime/src/types.ts +8 -0
- package/packages/claude/src/runtimeAdapter.test.ts +2 -2
- package/packages/claude/src/runtimeAdapter.ts +10 -19
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
- package/packages/db/src/repositories.ts +3 -0
- package/packages/db/src/schema.ts +1 -0
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/agent-providers.ts +10 -1
- package/packages/shared/src/index.ts +36 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/build-device-manager.ps1 +64 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
- package/apps/supervisor-web/dist/assets/index-CO8cU7a0.js +0 -21
- package/apps/supervisor-web/dist/assets/thread-ui-Dmrigdek.js +0 -3714
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AcpTurnItemMapper,
|
|
5
|
+
acpToolCallToHistoryItem,
|
|
6
|
+
} from './item-mapper';
|
|
7
|
+
|
|
8
|
+
describe('AcpTurnItemMapper', () => {
|
|
9
|
+
it('maps streamed ACP updates into ordered Remote Codex items', () => {
|
|
10
|
+
const mapper = new AcpTurnItemMapper('turn-1', [{
|
|
11
|
+
id: 'turn-1:user',
|
|
12
|
+
kind: 'userMessage',
|
|
13
|
+
text: 'Inspect the project',
|
|
14
|
+
}]);
|
|
15
|
+
|
|
16
|
+
const message = mapper.apply({
|
|
17
|
+
sessionUpdate: 'agent_message_chunk',
|
|
18
|
+
content: { type: 'text', text: 'I will inspect it.' },
|
|
19
|
+
});
|
|
20
|
+
expect(message.outputDeltas).toEqual([{
|
|
21
|
+
itemId: 'turn-1:agent:1',
|
|
22
|
+
delta: 'I will inspect it.',
|
|
23
|
+
}]);
|
|
24
|
+
|
|
25
|
+
mapper.apply({
|
|
26
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
27
|
+
content: { type: 'text', text: 'Need the source tree.' },
|
|
28
|
+
});
|
|
29
|
+
mapper.apply({
|
|
30
|
+
sessionUpdate: 'tool_call',
|
|
31
|
+
toolCallId: 'read-1',
|
|
32
|
+
title: 'Read package.json',
|
|
33
|
+
kind: 'read',
|
|
34
|
+
status: 'in_progress',
|
|
35
|
+
locations: [{ path: '/workspace/package.json' }],
|
|
36
|
+
rawInput: { path: '/workspace/package.json' },
|
|
37
|
+
});
|
|
38
|
+
mapper.apply({
|
|
39
|
+
sessionUpdate: 'tool_call_update',
|
|
40
|
+
toolCallId: 'read-1',
|
|
41
|
+
status: 'completed',
|
|
42
|
+
rawOutput: { content: '{}' },
|
|
43
|
+
});
|
|
44
|
+
const plan = mapper.apply({
|
|
45
|
+
sessionUpdate: 'plan',
|
|
46
|
+
entries: [
|
|
47
|
+
{ content: 'Inspect files', priority: 'high', status: 'completed' },
|
|
48
|
+
{ content: 'Apply fix', priority: 'high', status: 'in_progress' },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
expect(plan.planUpdate?.plan).toEqual([
|
|
52
|
+
{ step: 'Inspect files', status: 'completed' },
|
|
53
|
+
{ step: 'Apply fix', status: 'in_progress' },
|
|
54
|
+
]);
|
|
55
|
+
mapper.apply({
|
|
56
|
+
sessionUpdate: 'compaction_update',
|
|
57
|
+
compactionId: 'compact-1',
|
|
58
|
+
status: 'completed',
|
|
59
|
+
summary: [{ type: 'text', text: 'Retained summary' }],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { turn } = mapper.complete('completed');
|
|
63
|
+
expect(turn.items.map((item) => item.kind)).toEqual([
|
|
64
|
+
'userMessage',
|
|
65
|
+
'agentMessage',
|
|
66
|
+
'reasoning',
|
|
67
|
+
'fileRead',
|
|
68
|
+
'plan',
|
|
69
|
+
'contextCompaction',
|
|
70
|
+
]);
|
|
71
|
+
expect(turn.items.find((item) => item.id === 'read-1')).toMatchObject({
|
|
72
|
+
text: '/workspace/package.json',
|
|
73
|
+
status: 'completed',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
['execute', 'Run tests', 'commandExecution'],
|
|
79
|
+
['edit', 'Edit source', 'fileChange'],
|
|
80
|
+
['read', 'Read source', 'fileRead'],
|
|
81
|
+
['fetch', 'Fetch docs', 'webSearch'],
|
|
82
|
+
['other', 'Launch subagent', 'agentToolCall'],
|
|
83
|
+
['other', 'Load skill', 'skillToolCall'],
|
|
84
|
+
['other', 'Custom action', 'toolCall'],
|
|
85
|
+
] as const)('maps %s %s tools to %s', (kind, title, expected) => {
|
|
86
|
+
const item = acpToolCallToHistoryItem({
|
|
87
|
+
toolCallId: `${kind}:${title}`,
|
|
88
|
+
title,
|
|
89
|
+
kind,
|
|
90
|
+
status: 'completed',
|
|
91
|
+
});
|
|
92
|
+
expect(item.kind).toBe(expected);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('preserves file diff details and stats', () => {
|
|
96
|
+
const item = acpToolCallToHistoryItem({
|
|
97
|
+
toolCallId: 'edit-1',
|
|
98
|
+
title: 'Edit source',
|
|
99
|
+
kind: 'edit',
|
|
100
|
+
status: 'completed',
|
|
101
|
+
content: [{
|
|
102
|
+
type: 'diff',
|
|
103
|
+
path: '/workspace/a.ts',
|
|
104
|
+
oldText: 'one\ntwo',
|
|
105
|
+
newText: 'one\ntwo\nthree',
|
|
106
|
+
}],
|
|
107
|
+
});
|
|
108
|
+
expect(item).toMatchObject({
|
|
109
|
+
kind: 'fileChange',
|
|
110
|
+
changedFiles: 1,
|
|
111
|
+
addedLines: 1,
|
|
112
|
+
removedLines: 0,
|
|
113
|
+
});
|
|
114
|
+
expect(item.detailText).toContain('/workspace/a.ts');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('clears experimental plans when the agent removes them', () => {
|
|
118
|
+
const mapper = new AcpTurnItemMapper('turn-plan');
|
|
119
|
+
mapper.apply({
|
|
120
|
+
sessionUpdate: 'plan_update',
|
|
121
|
+
plan: {
|
|
122
|
+
type: 'markdown',
|
|
123
|
+
planId: 'plan-1',
|
|
124
|
+
content: '# Plan',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const removed = mapper.apply({
|
|
128
|
+
sessionUpdate: 'plan_removed',
|
|
129
|
+
planId: 'plan-1',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(removed.planUpdate).toEqual({ explanation: null, plan: [] });
|
|
133
|
+
expect(removed.itemUpdates).toMatchObject([
|
|
134
|
+
{ item: { id: 'turn-plan:plan:plan-1', status: 'cancelled' }, completed: true },
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AgentHistoryItem,
|
|
5
|
+
AgentTurn,
|
|
6
|
+
} from '../../agent-runtime/src/index';
|
|
7
|
+
|
|
8
|
+
export interface AcpMappedItemUpdate {
|
|
9
|
+
item: AgentHistoryItem;
|
|
10
|
+
completed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AcpMappedPlanUpdate {
|
|
14
|
+
explanation: string | null;
|
|
15
|
+
plan: Array<{ step: string; status: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AcpMappedSessionUpdate {
|
|
19
|
+
itemUpdates: AcpMappedItemUpdate[];
|
|
20
|
+
outputDeltas: Array<{ itemId: string; delta: string }>;
|
|
21
|
+
planUpdate: AcpMappedPlanUpdate | null;
|
|
22
|
+
title: string | null | undefined;
|
|
23
|
+
usage: { used: number; size: number; cost?: acp.Cost | null } | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StoredToolCall extends acp.ToolCall {
|
|
27
|
+
title: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compactJson(value: unknown) {
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(value, null, 2);
|
|
36
|
+
} catch {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function recordValue(value: unknown, keys: string[]) {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const record = value as Record<string, unknown>;
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const candidate = record[key];
|
|
48
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
49
|
+
return candidate.trim();
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(candidate) && candidate.every((part) => typeof part === 'string')) {
|
|
52
|
+
return candidate.join(' ');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function acpContentBlockText(content: acp.ContentBlock): string {
|
|
59
|
+
switch (content.type) {
|
|
60
|
+
case 'text':
|
|
61
|
+
return content.text;
|
|
62
|
+
case 'image':
|
|
63
|
+
return content.uri
|
|
64
|
+
? `[Image: ${content.uri}]`
|
|
65
|
+
: `[Image: ${content.mimeType}]`;
|
|
66
|
+
case 'audio':
|
|
67
|
+
return `[Audio: ${content.mimeType}]`;
|
|
68
|
+
case 'resource_link':
|
|
69
|
+
return `[${content.title ?? content.name}](${content.uri})`;
|
|
70
|
+
case 'resource':
|
|
71
|
+
return 'text' in content.resource
|
|
72
|
+
? content.resource.text
|
|
73
|
+
: `[Resource: ${content.resource.uri}]`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolContentText(content: acp.ToolCallContent) {
|
|
78
|
+
switch (content.type) {
|
|
79
|
+
case 'content':
|
|
80
|
+
return acpContentBlockText(content.content);
|
|
81
|
+
case 'diff':
|
|
82
|
+
return [
|
|
83
|
+
`File: ${content.path}`,
|
|
84
|
+
'',
|
|
85
|
+
'Before:',
|
|
86
|
+
content.oldText ?? '(new file)',
|
|
87
|
+
'',
|
|
88
|
+
'After:',
|
|
89
|
+
content.newText,
|
|
90
|
+
].join('\n');
|
|
91
|
+
case 'terminal':
|
|
92
|
+
return `Terminal: ${content.terminalId}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizedToolName(tool: StoredToolCall) {
|
|
97
|
+
return `${tool.name ?? ''} ${tool.title}`.trim().toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toolItemKind(tool: StoredToolCall): AgentHistoryItem['kind'] {
|
|
101
|
+
const name = normalizedToolName(tool);
|
|
102
|
+
if (name.includes('subagent') || /\b(agent|task)\b/.test(name)) {
|
|
103
|
+
return 'agentToolCall';
|
|
104
|
+
}
|
|
105
|
+
if (/\bskill\b/.test(name)) {
|
|
106
|
+
return 'skillToolCall';
|
|
107
|
+
}
|
|
108
|
+
if (tool.kind === 'execute') {
|
|
109
|
+
return 'commandExecution';
|
|
110
|
+
}
|
|
111
|
+
if (tool.kind === 'edit' || tool.kind === 'delete' || tool.kind === 'move') {
|
|
112
|
+
return 'fileChange';
|
|
113
|
+
}
|
|
114
|
+
if (tool.kind === 'read') {
|
|
115
|
+
return 'fileRead';
|
|
116
|
+
}
|
|
117
|
+
if (tool.kind === 'fetch' || name.includes('web') || name.includes('http')) {
|
|
118
|
+
return 'webSearch';
|
|
119
|
+
}
|
|
120
|
+
if (tool.kind === 'think') {
|
|
121
|
+
return 'reasoning';
|
|
122
|
+
}
|
|
123
|
+
return 'toolCall';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function mappedToolStatus(status: acp.ToolCallStatus | null | undefined) {
|
|
127
|
+
switch (status) {
|
|
128
|
+
case 'completed':
|
|
129
|
+
return 'completed';
|
|
130
|
+
case 'failed':
|
|
131
|
+
return 'failed';
|
|
132
|
+
case 'pending':
|
|
133
|
+
case 'in_progress':
|
|
134
|
+
default:
|
|
135
|
+
return 'running';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function lineCount(value: string | null | undefined) {
|
|
140
|
+
return value ? value.replace(/\r\n/g, '\n').split('\n').length : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function acpToolCallToHistoryItem(tool: StoredToolCall): AgentHistoryItem {
|
|
144
|
+
const kind = toolItemKind(tool);
|
|
145
|
+
const locationText = tool.locations?.map((location) => location.path).join(', ') ?? '';
|
|
146
|
+
const command = recordValue(tool.rawInput, ['command', 'cmd', 'argv']);
|
|
147
|
+
const summary = command || locationText || tool.title || tool.name || 'Tool call';
|
|
148
|
+
const detailParts = [
|
|
149
|
+
`Tool: ${tool.name ?? tool.title}`,
|
|
150
|
+
tool.kind ? `Kind: ${tool.kind}` : '',
|
|
151
|
+
tool.status ? `Status: ${tool.status}` : '',
|
|
152
|
+
tool.locations?.length
|
|
153
|
+
? `Locations:\n${tool.locations.map((location) => `- ${location.path}${location.line ? `:${location.line}` : ''}`).join('\n')}`
|
|
154
|
+
: '',
|
|
155
|
+
tool.rawInput !== undefined ? `Input:\n${compactJson(tool.rawInput)}` : '',
|
|
156
|
+
tool.content?.length
|
|
157
|
+
? `Content:\n${tool.content.map(toolContentText).join('\n\n')}`
|
|
158
|
+
: '',
|
|
159
|
+
tool.rawOutput !== undefined ? `Output:\n${compactJson(tool.rawOutput)}` : '',
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
const diffs = tool.content?.filter(
|
|
162
|
+
(content): content is Extract<acp.ToolCallContent, { type: 'diff' }> =>
|
|
163
|
+
content.type === 'diff',
|
|
164
|
+
) ?? [];
|
|
165
|
+
const addedLines = diffs.reduce(
|
|
166
|
+
(total, diff) => total + Math.max(0, lineCount(diff.newText) - lineCount(diff.oldText)),
|
|
167
|
+
0,
|
|
168
|
+
);
|
|
169
|
+
const removedLines = diffs.reduce(
|
|
170
|
+
(total, diff) => total + Math.max(0, lineCount(diff.oldText) - lineCount(diff.newText)),
|
|
171
|
+
0,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: tool.toolCallId,
|
|
176
|
+
kind,
|
|
177
|
+
text: summary,
|
|
178
|
+
previewText: summary,
|
|
179
|
+
detailText: detailParts.join('\n\n'),
|
|
180
|
+
status: mappedToolStatus(tool.status),
|
|
181
|
+
...(kind === 'fileChange'
|
|
182
|
+
? {
|
|
183
|
+
changedFiles: new Set([
|
|
184
|
+
...diffs.map((diff) => diff.path),
|
|
185
|
+
...(tool.locations?.map((location) => location.path) ?? []),
|
|
186
|
+
]).size || null,
|
|
187
|
+
addedLines: diffs.length > 0 ? addedLines : null,
|
|
188
|
+
removedLines: diffs.length > 0 ? removedLines : null,
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mergeDefined<T extends object>(current: T, patch: object): T {
|
|
195
|
+
const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
|
|
196
|
+
return Object.assign({}, current, Object.fromEntries(entries));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function planText(entries: acp.PlanEntry[]) {
|
|
200
|
+
return entries
|
|
201
|
+
.map((entry) => {
|
|
202
|
+
const marker = entry.status === 'completed' ? 'x' : ' ';
|
|
203
|
+
return `- [${marker}] ${entry.content}`;
|
|
204
|
+
})
|
|
205
|
+
.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function mappedPlan(entries: acp.PlanEntry[]): AcpMappedPlanUpdate {
|
|
209
|
+
return {
|
|
210
|
+
explanation: null,
|
|
211
|
+
plan: entries.map((entry) => ({
|
|
212
|
+
step: entry.content,
|
|
213
|
+
status: entry.status === 'in_progress' ? 'in_progress' : entry.status,
|
|
214
|
+
})),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class AcpTurnItemMapper {
|
|
219
|
+
private readonly items = new Map<string, AgentHistoryItem>();
|
|
220
|
+
private readonly order: string[] = [];
|
|
221
|
+
private readonly tools = new Map<string, StoredToolCall>();
|
|
222
|
+
private agentMessageIndex = 0;
|
|
223
|
+
private thoughtIndex = 0;
|
|
224
|
+
private currentAgentMessageId: string | null = null;
|
|
225
|
+
private currentThoughtId: string | null = null;
|
|
226
|
+
|
|
227
|
+
constructor(
|
|
228
|
+
readonly turnId: string,
|
|
229
|
+
initialItems: AgentHistoryItem[] = [],
|
|
230
|
+
) {
|
|
231
|
+
for (const item of initialItems) {
|
|
232
|
+
this.upsert(item);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
turn(status: AgentTurn['status'] = 'inProgress', error: string | null = null): AgentTurn {
|
|
237
|
+
return {
|
|
238
|
+
providerTurnId: this.turnId,
|
|
239
|
+
status,
|
|
240
|
+
error: error ? { message: error } : null,
|
|
241
|
+
items: this.order.map((id) => this.items.get(id)!).filter(Boolean),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
apply(update: acp.SessionUpdate): AcpMappedSessionUpdate {
|
|
246
|
+
const result: AcpMappedSessionUpdate = {
|
|
247
|
+
itemUpdates: [],
|
|
248
|
+
outputDeltas: [],
|
|
249
|
+
planUpdate: null,
|
|
250
|
+
title: undefined,
|
|
251
|
+
usage: null,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
switch (update.sessionUpdate) {
|
|
255
|
+
case 'user_message_chunk':
|
|
256
|
+
return result;
|
|
257
|
+
case 'agent_message_chunk': {
|
|
258
|
+
result.itemUpdates.push(...this.finishThought());
|
|
259
|
+
const delta = acpContentBlockText(update.content);
|
|
260
|
+
const itemId = this.currentAgentMessageId ?? `${this.turnId}:agent:${++this.agentMessageIndex}`;
|
|
261
|
+
this.currentAgentMessageId = itemId;
|
|
262
|
+
const current = this.items.get(itemId);
|
|
263
|
+
const item: AgentHistoryItem = {
|
|
264
|
+
...(current ?? { id: itemId, kind: 'agentMessage' as const }),
|
|
265
|
+
text: `${current?.text ?? ''}${delta}`,
|
|
266
|
+
status: 'running',
|
|
267
|
+
};
|
|
268
|
+
this.upsert(item);
|
|
269
|
+
result.outputDeltas.push({ itemId, delta });
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
case 'agent_thought_chunk': {
|
|
273
|
+
result.itemUpdates.push(...this.finishAgentMessage());
|
|
274
|
+
const delta = acpContentBlockText(update.content);
|
|
275
|
+
const itemId = this.currentThoughtId ?? `${this.turnId}:thought:${++this.thoughtIndex}`;
|
|
276
|
+
this.currentThoughtId = itemId;
|
|
277
|
+
const current = this.items.get(itemId);
|
|
278
|
+
const item: AgentHistoryItem = {
|
|
279
|
+
...(current ?? { id: itemId, kind: 'reasoning' as const }),
|
|
280
|
+
text: `${current?.text ?? ''}${delta}`,
|
|
281
|
+
status: 'running',
|
|
282
|
+
};
|
|
283
|
+
this.upsert(item);
|
|
284
|
+
result.itemUpdates.push({ item, completed: false });
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
case 'tool_call': {
|
|
288
|
+
result.itemUpdates.push(...this.finishOpenText());
|
|
289
|
+
const tool: StoredToolCall = {
|
|
290
|
+
...update,
|
|
291
|
+
title: update.title || update.name || 'Tool call',
|
|
292
|
+
};
|
|
293
|
+
this.tools.set(tool.toolCallId, tool);
|
|
294
|
+
const item = acpToolCallToHistoryItem(tool);
|
|
295
|
+
this.upsert(item);
|
|
296
|
+
result.itemUpdates.push({ item, completed: tool.status === 'completed' || tool.status === 'failed' });
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
case 'tool_call_update': {
|
|
300
|
+
result.itemUpdates.push(...this.finishOpenText());
|
|
301
|
+
const previous = this.tools.get(update.toolCallId) ?? {
|
|
302
|
+
toolCallId: update.toolCallId,
|
|
303
|
+
title: update.title ?? update.name ?? 'Tool call',
|
|
304
|
+
};
|
|
305
|
+
const tool = mergeDefined(previous, update) as StoredToolCall;
|
|
306
|
+
tool.title ||= tool.name || 'Tool call';
|
|
307
|
+
this.tools.set(tool.toolCallId, tool);
|
|
308
|
+
const item = acpToolCallToHistoryItem(tool);
|
|
309
|
+
this.upsert(item);
|
|
310
|
+
result.itemUpdates.push({ item, completed: tool.status === 'completed' || tool.status === 'failed' });
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
case 'plan': {
|
|
314
|
+
result.itemUpdates.push(...this.finishOpenText());
|
|
315
|
+
const item: AgentHistoryItem = {
|
|
316
|
+
id: `${this.turnId}:plan`,
|
|
317
|
+
kind: 'plan',
|
|
318
|
+
text: planText(update.entries),
|
|
319
|
+
previewText: 'Plan',
|
|
320
|
+
status: update.entries.every((entry) => entry.status === 'completed')
|
|
321
|
+
? 'completed'
|
|
322
|
+
: 'running',
|
|
323
|
+
};
|
|
324
|
+
this.upsert(item);
|
|
325
|
+
result.itemUpdates.push({ item, completed: item.status === 'completed' });
|
|
326
|
+
result.planUpdate = mappedPlan(update.entries);
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
case 'plan_update': {
|
|
330
|
+
result.itemUpdates.push(...this.finishOpenText());
|
|
331
|
+
if (update.plan.type === 'items') {
|
|
332
|
+
const item: AgentHistoryItem = {
|
|
333
|
+
id: `${this.turnId}:plan:${update.plan.planId}`,
|
|
334
|
+
kind: 'plan',
|
|
335
|
+
text: planText(update.plan.entries),
|
|
336
|
+
previewText: 'Plan',
|
|
337
|
+
status: update.plan.entries.every((entry) => entry.status === 'completed')
|
|
338
|
+
? 'completed'
|
|
339
|
+
: 'running',
|
|
340
|
+
};
|
|
341
|
+
this.upsert(item);
|
|
342
|
+
result.itemUpdates.push({ item, completed: item.status === 'completed' });
|
|
343
|
+
result.planUpdate = mappedPlan(update.plan.entries);
|
|
344
|
+
} else if (update.plan.type === 'markdown') {
|
|
345
|
+
const item: AgentHistoryItem = {
|
|
346
|
+
id: `${this.turnId}:plan:${update.plan.planId}`,
|
|
347
|
+
kind: 'plan',
|
|
348
|
+
text: update.plan.content,
|
|
349
|
+
previewText: 'Plan',
|
|
350
|
+
status: 'running',
|
|
351
|
+
};
|
|
352
|
+
this.upsert(item);
|
|
353
|
+
result.itemUpdates.push({ item, completed: false });
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
case 'plan_removed': {
|
|
358
|
+
const itemId = `${this.turnId}:plan:${update.planId}`;
|
|
359
|
+
const current = this.items.get(itemId);
|
|
360
|
+
if (current) {
|
|
361
|
+
const item = { ...current, status: 'cancelled' };
|
|
362
|
+
this.upsert(item);
|
|
363
|
+
result.itemUpdates.push({ item, completed: true });
|
|
364
|
+
}
|
|
365
|
+
result.planUpdate = { explanation: null, plan: [] };
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
case 'session_info_update':
|
|
369
|
+
result.title = update.title;
|
|
370
|
+
return result;
|
|
371
|
+
case 'usage_update':
|
|
372
|
+
result.usage = {
|
|
373
|
+
used: update.used,
|
|
374
|
+
size: update.size,
|
|
375
|
+
...(update.cost !== undefined ? { cost: update.cost } : {}),
|
|
376
|
+
};
|
|
377
|
+
return result;
|
|
378
|
+
case 'compaction_update': {
|
|
379
|
+
result.itemUpdates.push(...this.finishOpenText());
|
|
380
|
+
const summary = update.summary?.map(acpContentBlockText).join('\n\n') ?? '';
|
|
381
|
+
const item: AgentHistoryItem = {
|
|
382
|
+
id: `${this.turnId}:compaction:${update.compactionId}`,
|
|
383
|
+
kind: 'contextCompaction',
|
|
384
|
+
text: summary || update.error || 'Context compaction',
|
|
385
|
+
previewText: 'Context compaction',
|
|
386
|
+
status: update.status,
|
|
387
|
+
};
|
|
388
|
+
this.upsert(item);
|
|
389
|
+
result.itemUpdates.push({
|
|
390
|
+
item,
|
|
391
|
+
completed: ['completed', 'failed', 'cancelled'].includes(update.status),
|
|
392
|
+
});
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
case 'compaction_summary_chunk': {
|
|
396
|
+
const itemId = `${this.turnId}:compaction:${update.compactionId}`;
|
|
397
|
+
const current = this.items.get(itemId);
|
|
398
|
+
const item: AgentHistoryItem = {
|
|
399
|
+
...(current ?? {
|
|
400
|
+
id: itemId,
|
|
401
|
+
kind: 'contextCompaction' as const,
|
|
402
|
+
previewText: 'Context compaction',
|
|
403
|
+
}),
|
|
404
|
+
text: `${current?.text ?? ''}${acpContentBlockText(update.content)}`,
|
|
405
|
+
status: 'in_progress',
|
|
406
|
+
};
|
|
407
|
+
this.upsert(item);
|
|
408
|
+
result.itemUpdates.push({ item, completed: false });
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
case 'available_commands_update':
|
|
412
|
+
case 'current_mode_update':
|
|
413
|
+
case 'config_option_update':
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
complete(status: AgentTurn['status'], error: string | null = null) {
|
|
419
|
+
const updates = this.finishOpenText();
|
|
420
|
+
for (const id of this.order) {
|
|
421
|
+
const current = this.items.get(id)!;
|
|
422
|
+
const nextStatus = current.status === 'failed'
|
|
423
|
+
? 'failed'
|
|
424
|
+
: status === 'failed'
|
|
425
|
+
? 'failed'
|
|
426
|
+
: status === 'interrupted'
|
|
427
|
+
? 'interrupted'
|
|
428
|
+
: 'completed';
|
|
429
|
+
const item = current.status === nextStatus ? current : { ...current, status: nextStatus };
|
|
430
|
+
this.items.set(id, item);
|
|
431
|
+
if (!updates.some((entry) => entry.item.id === id)) {
|
|
432
|
+
updates.push({ item, completed: true });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
updates,
|
|
437
|
+
turn: this.turn(status, error),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private upsert(item: AgentHistoryItem) {
|
|
442
|
+
if (!this.items.has(item.id)) {
|
|
443
|
+
this.order.push(item.id);
|
|
444
|
+
}
|
|
445
|
+
this.items.set(item.id, item);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private finishAgentMessage(): AcpMappedItemUpdate[] {
|
|
449
|
+
if (!this.currentAgentMessageId) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
const item = this.items.get(this.currentAgentMessageId)!;
|
|
453
|
+
this.currentAgentMessageId = null;
|
|
454
|
+
const completed = { ...item, status: 'completed' };
|
|
455
|
+
this.items.set(completed.id, completed);
|
|
456
|
+
return [{ item: completed, completed: true }];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private finishThought(): AcpMappedItemUpdate[] {
|
|
460
|
+
if (!this.currentThoughtId) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
const item = this.items.get(this.currentThoughtId)!;
|
|
464
|
+
this.currentThoughtId = null;
|
|
465
|
+
const completed = { ...item, status: 'completed' };
|
|
466
|
+
this.items.set(completed.id, completed);
|
|
467
|
+
return [{ item: completed, completed: true }];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private finishOpenText() {
|
|
471
|
+
return [...this.finishAgentMessage(), ...this.finishThought()];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AgentProviderRequest,
|
|
7
|
+
AgentRuntimeEvent,
|
|
8
|
+
} from '../../agent-runtime/src/index';
|
|
9
|
+
import { AcpRuntimeAdapter } from './runtimeAdapter';
|
|
10
|
+
|
|
11
|
+
const adapters: AcpRuntimeAdapter[] = [];
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await Promise.all(adapters.splice(0).map((adapter) => adapter.stop()));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('AcpRuntimeAdapter', () => {
|
|
18
|
+
it('runs a stdio ACP turn and resolves protocol permissions', async () => {
|
|
19
|
+
const fixture = path.resolve(
|
|
20
|
+
'node_modules/@agentclientprotocol/sdk/dist/examples/agent.js',
|
|
21
|
+
);
|
|
22
|
+
const adapter = new AcpRuntimeAdapter({
|
|
23
|
+
command: `"${process.execPath}" "${fixture}"`,
|
|
24
|
+
startupTimeoutMs: 5_000,
|
|
25
|
+
clientInfo: {
|
|
26
|
+
name: 'remote-codex-acp-test',
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
adapters.push(adapter);
|
|
31
|
+
const events: AgentRuntimeEvent[] = [];
|
|
32
|
+
adapter.on('event', (event) => events.push(event as AgentRuntimeEvent));
|
|
33
|
+
adapter.on('provider-request', (request) => {
|
|
34
|
+
const mapping = adapter.mapProviderRequest(
|
|
35
|
+
request as AgentProviderRequest,
|
|
36
|
+
{ approvalMode: 'yolo' },
|
|
37
|
+
);
|
|
38
|
+
if (mapping?.autoApprovedResult) {
|
|
39
|
+
adapter.respondToProviderRequest(mapping.providerRequestId, mapping.autoApprovedResult);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await adapter.start();
|
|
44
|
+
expect(adapter.getStatus().state).toBe('ready');
|
|
45
|
+
const started = await adapter.startSession({
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
model: 'default',
|
|
48
|
+
approvalMode: 'yolo',
|
|
49
|
+
sandboxMode: 'workspace-write',
|
|
50
|
+
});
|
|
51
|
+
const completed = new Promise<Extract<AgentRuntimeEvent, { type: 'turn.completed' }>>(
|
|
52
|
+
(resolve, reject) => {
|
|
53
|
+
const timer = setTimeout(() => reject(new Error('ACP turn did not complete.')), 10_000);
|
|
54
|
+
adapter.on('event', (event: AgentRuntimeEvent) => {
|
|
55
|
+
if (event.type === 'turn.completed') {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve(event);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
await adapter.startTurn({
|
|
63
|
+
providerSessionId: started.providerSessionId,
|
|
64
|
+
prompt: 'Inspect this project.',
|
|
65
|
+
workspacePath: process.cwd(),
|
|
66
|
+
});
|
|
67
|
+
const event = await completed;
|
|
68
|
+
|
|
69
|
+
expect(event.turn.status).toBe('completed');
|
|
70
|
+
expect(event.turn.items.map((item) => item.kind)).toEqual(expect.arrayContaining([
|
|
71
|
+
'userMessage',
|
|
72
|
+
'agentMessage',
|
|
73
|
+
'fileRead',
|
|
74
|
+
'fileChange',
|
|
75
|
+
]));
|
|
76
|
+
expect(events.some((candidate) => candidate.type === 'output.delta')).toBe(true);
|
|
77
|
+
expect(await adapter.listLoadedSessions()).toContain(started.providerSessionId);
|
|
78
|
+
}, 15_000);
|
|
79
|
+
});
|