remote-codex 0.1.6 → 0.1.7
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/apps/supervisor-api/dist/index.js +6786 -5630
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-DvEvXPo5.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
- package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
- package/apps/supervisor-web/dist/assets/{xterm-CWQ1ih_R.js → xterm-DisVWgDR.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +5 -1
- package/packages/agent-runtime/src/index.ts +2 -0
- package/packages/agent-runtime/src/registry.ts +44 -0
- package/packages/agent-runtime/src/types.ts +531 -0
- package/packages/codex/src/appServerManager.test.ts +328 -0
- package/packages/codex/src/appServerManager.ts +656 -0
- package/packages/codex/src/historyItems.ts +1185 -0
- package/packages/codex/src/hookHistory.ts +224 -0
- package/packages/codex/src/index.ts +6 -0
- package/packages/codex/src/jsonrpc.test.ts +58 -0
- package/packages/codex/src/jsonrpc.ts +198 -0
- package/packages/codex/src/requestMapper.test.ts +127 -0
- package/packages/codex/src/requestMapper.ts +511 -0
- package/packages/codex/src/runtimeAdapter.ts +692 -0
- package/packages/codex/src/types.ts +403 -0
- package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
- package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
- package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
- package/packages/db/src/client.ts +53 -0
- package/packages/db/src/index.ts +5 -0
- package/packages/db/src/migrate.test.ts +36 -0
- package/packages/db/src/migrate.ts +84 -0
- package/packages/db/src/repositories.ts +893 -0
- package/packages/db/src/schema.ts +177 -0
- package/packages/db/src/seed.ts +51 -0
- package/packages/shared/src/index.ts +878 -0
- package/apps/supervisor-web/dist/assets/index-CQu6sRq7.css +0 -32
- package/apps/supervisor-web/dist/assets/index-MELw9ga_.js +0 -377
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ThreadHistoryItemDto } from '../../shared/src/index';
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function decodeXmlEntities(value: string) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, "'")
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/&/g, '&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function textFromUnknown(value: unknown): string | null {
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
return value.trim() ? value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
const parts: string[] = value
|
|
23
|
+
.map((entry) => textFromUnknown(entry))
|
|
24
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
25
|
+
return parts.length > 0 ? parts.join(' ') : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function codexHookEventLabel(value: string) {
|
|
32
|
+
switch (value) {
|
|
33
|
+
case 'preToolUse':
|
|
34
|
+
return 'PreToolUse';
|
|
35
|
+
case 'permissionRequest':
|
|
36
|
+
return 'PermissionRequest';
|
|
37
|
+
case 'postToolUse':
|
|
38
|
+
return 'PostToolUse';
|
|
39
|
+
case 'preCompact':
|
|
40
|
+
return 'PreCompact';
|
|
41
|
+
case 'postCompact':
|
|
42
|
+
return 'PostCompact';
|
|
43
|
+
case 'sessionStart':
|
|
44
|
+
return 'SessionStart';
|
|
45
|
+
case 'userPromptSubmit':
|
|
46
|
+
return 'UserPromptSubmit';
|
|
47
|
+
case 'stop':
|
|
48
|
+
return 'Stop';
|
|
49
|
+
default:
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function codexHookStatusLabel(value: string) {
|
|
55
|
+
switch (value) {
|
|
56
|
+
case 'running':
|
|
57
|
+
return 'Running';
|
|
58
|
+
case 'completed':
|
|
59
|
+
return 'Completed';
|
|
60
|
+
case 'failed':
|
|
61
|
+
return 'Failed';
|
|
62
|
+
case 'blocked':
|
|
63
|
+
return 'Blocked';
|
|
64
|
+
case 'stopped':
|
|
65
|
+
return 'Stopped';
|
|
66
|
+
default:
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hookRunOutputEntryText(entry: unknown) {
|
|
72
|
+
if (!isRecord(entry)) {
|
|
73
|
+
return textFromUnknown(entry);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
textFromUnknown(entry.text) ??
|
|
78
|
+
textFromUnknown(entry.message) ??
|
|
79
|
+
textFromUnknown(entry.systemMessage) ??
|
|
80
|
+
textFromUnknown(entry.stopReason) ??
|
|
81
|
+
textFromUnknown(entry.reason) ??
|
|
82
|
+
textFromUnknown(entry.output) ??
|
|
83
|
+
textFromUnknown(entry.stdout) ??
|
|
84
|
+
textFromUnknown(entry.stderr)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeHookRunOutputEntries(run: {
|
|
89
|
+
entries?: Array<{ kind?: string; text?: string }>;
|
|
90
|
+
outputEntries?: Array<{ kind?: string; text?: string }>;
|
|
91
|
+
output_entries?: Array<{ kind?: string; text?: string }>;
|
|
92
|
+
stdout?: unknown;
|
|
93
|
+
stderr?: unknown;
|
|
94
|
+
output?: unknown;
|
|
95
|
+
text?: unknown;
|
|
96
|
+
systemMessage?: unknown;
|
|
97
|
+
stopReason?: unknown;
|
|
98
|
+
reason?: unknown;
|
|
99
|
+
}): Array<{ kind: string; text: string }> {
|
|
100
|
+
const rawEntries = Array.isArray(run.entries)
|
|
101
|
+
? run.entries
|
|
102
|
+
: Array.isArray(run.outputEntries)
|
|
103
|
+
? run.outputEntries
|
|
104
|
+
: Array.isArray(run.output_entries)
|
|
105
|
+
? run.output_entries
|
|
106
|
+
: [];
|
|
107
|
+
const entries = rawEntries
|
|
108
|
+
.map((entry) => {
|
|
109
|
+
const text = hookRunOutputEntryText(entry)?.trim();
|
|
110
|
+
if (!text) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
kind: typeof entry.kind === 'string' && entry.kind.trim() ? entry.kind : 'context',
|
|
116
|
+
text,
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.filter((entry): entry is { kind: string; text: string } => Boolean(entry));
|
|
120
|
+
const seenTexts = new Set(entries.map((entry) => entry.text));
|
|
121
|
+
|
|
122
|
+
for (const [kind, value] of [
|
|
123
|
+
['context', run.output],
|
|
124
|
+
['context', run.text],
|
|
125
|
+
['warning', run.systemMessage],
|
|
126
|
+
['warning', run.stopReason],
|
|
127
|
+
['warning', run.reason],
|
|
128
|
+
['context', run.stdout],
|
|
129
|
+
['warning', run.stderr],
|
|
130
|
+
] as const) {
|
|
131
|
+
const text = textFromUnknown(value)?.trim();
|
|
132
|
+
if (text && !seenTexts.has(text)) {
|
|
133
|
+
entries.push({ kind, text });
|
|
134
|
+
seenTexts.add(text);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return entries;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseCodexHookPromptText(text: string) {
|
|
142
|
+
const match = text
|
|
143
|
+
.trim()
|
|
144
|
+
.match(/^<hook_prompt(?:\s+hook_run_id="([^"]+)")?>([\s\S]*)<\/hook_prompt>$/);
|
|
145
|
+
if (!match) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hookRunId = match[1] ? decodeXmlEntities(match[1]) : null;
|
|
150
|
+
const output = decodeXmlEntities(match[2] ?? '').trim();
|
|
151
|
+
const eventName = hookRunId?.split(':')[0] ?? 'hook';
|
|
152
|
+
const eventLabel = codexHookEventLabel(eventName);
|
|
153
|
+
const sourcePath = hookRunId?.split(':').slice(2).join(':') || null;
|
|
154
|
+
const outputEntries = output ? [{ kind: 'warning', text: output }] : [];
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
hookRunId,
|
|
158
|
+
output,
|
|
159
|
+
outputEntries,
|
|
160
|
+
eventName,
|
|
161
|
+
eventLabel,
|
|
162
|
+
sourcePath,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function codexHookRunToHistoryItem(run: {
|
|
167
|
+
id: string;
|
|
168
|
+
eventName: string;
|
|
169
|
+
handlerType: string;
|
|
170
|
+
executionMode: string;
|
|
171
|
+
scope: string;
|
|
172
|
+
sourcePath: string;
|
|
173
|
+
source: string;
|
|
174
|
+
status: string;
|
|
175
|
+
statusMessage: string | null;
|
|
176
|
+
durationMs: number | null;
|
|
177
|
+
entries?: Array<{ kind: string; text: string }>;
|
|
178
|
+
outputEntries?: Array<{ kind?: string; text?: string }>;
|
|
179
|
+
output_entries?: Array<{ kind?: string; text?: string }>;
|
|
180
|
+
stdout?: unknown;
|
|
181
|
+
stderr?: unknown;
|
|
182
|
+
output?: unknown;
|
|
183
|
+
text?: unknown;
|
|
184
|
+
systemMessage?: unknown;
|
|
185
|
+
stopReason?: unknown;
|
|
186
|
+
reason?: unknown;
|
|
187
|
+
}): ThreadHistoryItemDto {
|
|
188
|
+
const eventLabel = codexHookEventLabel(run.eventName);
|
|
189
|
+
const outputEntries = normalizeHookRunOutputEntries(run);
|
|
190
|
+
const entryPreview = outputEntries
|
|
191
|
+
.map((entry) => entry.text.trim())
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join('\n')
|
|
194
|
+
.trim();
|
|
195
|
+
const firstEntryLine = entryPreview.split('\n').find(Boolean) ?? null;
|
|
196
|
+
const detailLines = [
|
|
197
|
+
`Event: ${eventLabel}`,
|
|
198
|
+
`Status: ${codexHookStatusLabel(run.status)}`,
|
|
199
|
+
`Handler: ${run.handlerType}`,
|
|
200
|
+
`Scope: ${run.scope}`,
|
|
201
|
+
`Source: ${run.source}`,
|
|
202
|
+
`Path: ${run.sourcePath}`,
|
|
203
|
+
run.durationMs !== null ? `Duration: ${run.durationMs} ms` : null,
|
|
204
|
+
run.statusMessage ? `Message: ${run.statusMessage}` : null,
|
|
205
|
+
entryPreview ? `\n${entryPreview}` : null,
|
|
206
|
+
].filter((line): line is string => Boolean(line));
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
id: `hook:${run.id}`,
|
|
210
|
+
kind: 'hook',
|
|
211
|
+
text: `${eventLabel} hook`,
|
|
212
|
+
previewText: run.statusMessage ?? firstEntryLine ?? `${eventLabel} hook`,
|
|
213
|
+
detailText: detailLines.join('\n'),
|
|
214
|
+
status: codexHookStatusLabel(run.status),
|
|
215
|
+
hookEventName: run.eventName,
|
|
216
|
+
hookEventLabel: eventLabel,
|
|
217
|
+
hookHandlerType: run.handlerType,
|
|
218
|
+
hookScope: run.scope,
|
|
219
|
+
hookSource: run.source,
|
|
220
|
+
hookSourcePath: run.sourcePath,
|
|
221
|
+
hookStatusMessage: run.statusMessage,
|
|
222
|
+
hookOutputEntries: outputEntries,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
|
|
6
|
+
|
|
7
|
+
describe('JsonRpcClient', () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('resolves requests from line-delimited responses', async () => {
|
|
13
|
+
const input = new PassThrough();
|
|
14
|
+
const output = new PassThrough();
|
|
15
|
+
const client = new JsonRpcClient(input, output);
|
|
16
|
+
|
|
17
|
+
const promise = client.request<{ ok: boolean }>('ping', { hello: 'world' }, 1000);
|
|
18
|
+
const outbound = output.read()?.toString() ?? '';
|
|
19
|
+
const parsed = JSON.parse(outbound.trim());
|
|
20
|
+
|
|
21
|
+
expect(parsed.method).toBe('ping');
|
|
22
|
+
|
|
23
|
+
input.write(`${JSON.stringify({ id: parsed.id, result: { ok: true } })}\n`);
|
|
24
|
+
|
|
25
|
+
await expect(promise).resolves.toEqual({ ok: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('emits notifications', async () => {
|
|
29
|
+
const input = new PassThrough();
|
|
30
|
+
const output = new PassThrough();
|
|
31
|
+
const client = new JsonRpcClient(input, output);
|
|
32
|
+
const onNotification = vi.fn();
|
|
33
|
+
|
|
34
|
+
client.on('notification', onNotification);
|
|
35
|
+
input.write(`${JSON.stringify({ method: 'turn/started', params: { threadId: 't1' } })}\n`);
|
|
36
|
+
|
|
37
|
+
expect(onNotification).toHaveBeenCalledWith({
|
|
38
|
+
method: 'turn/started',
|
|
39
|
+
params: { threadId: 't1' }
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('times out pending requests', async () => {
|
|
44
|
+
vi.useFakeTimers();
|
|
45
|
+
|
|
46
|
+
const input = new PassThrough();
|
|
47
|
+
const output = new PassThrough();
|
|
48
|
+
const client = new JsonRpcClient(input, output);
|
|
49
|
+
const promise = client.request('slow', undefined, 50);
|
|
50
|
+
const assertion = expect(promise).rejects.toMatchObject({
|
|
51
|
+
code: 'request_timeout'
|
|
52
|
+
} satisfies Partial<JsonRpcClientError>);
|
|
53
|
+
|
|
54
|
+
await vi.advanceTimersByTimeAsync(60);
|
|
55
|
+
|
|
56
|
+
await assertion;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
JsonRpcFailure,
|
|
7
|
+
JsonRpcId,
|
|
8
|
+
JsonRpcNotification,
|
|
9
|
+
JsonRpcRequest,
|
|
10
|
+
JsonRpcSuccess
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
export class JsonRpcClientError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
message: string,
|
|
16
|
+
public readonly code: string,
|
|
17
|
+
public readonly details?: Record<string, unknown>
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PendingRequest {
|
|
24
|
+
resolve: (value: unknown) => void;
|
|
25
|
+
reject: (reason?: unknown) => void;
|
|
26
|
+
timer: NodeJS.Timeout;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class JsonRpcClient extends EventEmitter {
|
|
30
|
+
private readonly reader: readline.Interface;
|
|
31
|
+
private readonly pending = new Map<JsonRpcId, PendingRequest>();
|
|
32
|
+
private nextRequestId = 1;
|
|
33
|
+
private closed = false;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly input: Readable,
|
|
37
|
+
private readonly output: Writable
|
|
38
|
+
) {
|
|
39
|
+
super();
|
|
40
|
+
|
|
41
|
+
this.reader = readline.createInterface({
|
|
42
|
+
input: this.input,
|
|
43
|
+
crlfDelay: Infinity
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.reader.on('line', (line) => {
|
|
47
|
+
if (!line.trim()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.handleMessage(line);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.reader.on('close', () => {
|
|
55
|
+
this.close();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async request<TResult = unknown, TParams = unknown>(
|
|
60
|
+
method: string,
|
|
61
|
+
params?: TParams,
|
|
62
|
+
timeoutMs = 20_000
|
|
63
|
+
): Promise<TResult> {
|
|
64
|
+
if (this.closed) {
|
|
65
|
+
throw new JsonRpcClientError('JSON-RPC client is closed.', 'client_closed');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const id = this.nextRequestId++;
|
|
69
|
+
const payload: JsonRpcRequest<TParams> = {
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
id,
|
|
72
|
+
method
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (params !== undefined) {
|
|
76
|
+
payload.params = params;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.pending.delete(id);
|
|
82
|
+
reject(
|
|
83
|
+
new JsonRpcClientError(`JSON-RPC request timed out for ${method}.`, 'request_timeout', {
|
|
84
|
+
method,
|
|
85
|
+
timeoutMs
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}, timeoutMs);
|
|
89
|
+
|
|
90
|
+
this.pending.set(id, {
|
|
91
|
+
resolve: (value) => resolve(value as TResult),
|
|
92
|
+
reject,
|
|
93
|
+
timer
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.output.write(`${JSON.stringify(payload)}\n`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
close() {
|
|
101
|
+
if (this.closed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.closed = true;
|
|
106
|
+
this.reader.close();
|
|
107
|
+
|
|
108
|
+
for (const [id, request] of this.pending.entries()) {
|
|
109
|
+
clearTimeout(request.timer);
|
|
110
|
+
request.reject(
|
|
111
|
+
new JsonRpcClientError('JSON-RPC client closed before response was received.', 'client_closed', {
|
|
112
|
+
id
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.pending.clear();
|
|
118
|
+
this.emit('closed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private handleMessage(raw: string) {
|
|
122
|
+
let parsed: Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.emit(
|
|
128
|
+
'warning',
|
|
129
|
+
new JsonRpcClientError('Failed to parse JSON-RPC payload.', 'invalid_json', {
|
|
130
|
+
raw,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error)
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof parsed.method === 'string' && !('id' in parsed)) {
|
|
138
|
+
this.emit('notification', parsed as unknown as JsonRpcNotification);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof parsed.method === 'string' && typeof parsed.id === 'number') {
|
|
143
|
+
this.emit('request', parsed as unknown as JsonRpcRequest);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof parsed.id !== 'number') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const request = this.pending.get(parsed.id);
|
|
152
|
+
if (!request) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
clearTimeout(request.timer);
|
|
157
|
+
this.pending.delete(parsed.id);
|
|
158
|
+
|
|
159
|
+
if ('error' in parsed && parsed.error && typeof parsed.error === 'object') {
|
|
160
|
+
const error = parsed.error as JsonRpcFailure['error'];
|
|
161
|
+
request.reject(
|
|
162
|
+
new JsonRpcClientError(error.message, 'remote_error', {
|
|
163
|
+
code: error.code,
|
|
164
|
+
data: error.data
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
request.resolve((parsed as unknown as JsonRpcSuccess).result);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
respond(id: JsonRpcId, result: unknown) {
|
|
174
|
+
if (this.closed) {
|
|
175
|
+
throw new JsonRpcClientError('JSON-RPC client is closed.', 'client_closed');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.output.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
respondError(id: JsonRpcId, message: string, code = -32000, data?: unknown) {
|
|
182
|
+
if (this.closed) {
|
|
183
|
+
throw new JsonRpcClientError('JSON-RPC client is closed.', 'client_closed');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.output.write(
|
|
187
|
+
`${JSON.stringify({
|
|
188
|
+
jsonrpc: '2.0',
|
|
189
|
+
id,
|
|
190
|
+
error: {
|
|
191
|
+
code,
|
|
192
|
+
message,
|
|
193
|
+
data
|
|
194
|
+
}
|
|
195
|
+
})}\n`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { AgentProviderRequest } from '../../agent-runtime/src/index';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildCodexProviderRequestResponse,
|
|
7
|
+
mapCodexProviderRequest,
|
|
8
|
+
} from './requestMapper';
|
|
9
|
+
|
|
10
|
+
describe('Codex provider request mapping', () => {
|
|
11
|
+
it('auto-approves command approval requests in yolo mode', () => {
|
|
12
|
+
const request: AgentProviderRequest = {
|
|
13
|
+
provider: 'codex',
|
|
14
|
+
id: 7,
|
|
15
|
+
method: 'item/commandExecution/requestApproval',
|
|
16
|
+
params: {
|
|
17
|
+
threadId: 'thread-1',
|
|
18
|
+
turnId: 'turn-1',
|
|
19
|
+
command: 'pnpm test',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(mapCodexProviderRequest(request, 'yolo')).toEqual({
|
|
24
|
+
providerRequestId: 7,
|
|
25
|
+
providerSessionId: 'thread-1',
|
|
26
|
+
autoApprovedResult: { decision: 'accept' },
|
|
27
|
+
pendingRequest: null,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('maps guarded command approval requests to pending action requests', () => {
|
|
32
|
+
const request: AgentProviderRequest = {
|
|
33
|
+
provider: 'codex',
|
|
34
|
+
id: 8,
|
|
35
|
+
method: 'item/commandExecution/requestApproval',
|
|
36
|
+
params: {
|
|
37
|
+
threadId: 'thread-2',
|
|
38
|
+
turnId: 'turn-2',
|
|
39
|
+
itemId: 'item-1',
|
|
40
|
+
command: ['pnpm', 'test'],
|
|
41
|
+
cwd: '/repo',
|
|
42
|
+
reason: 'Needs to verify changes.',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mapped = mapCodexProviderRequest(request, 'guarded');
|
|
47
|
+
|
|
48
|
+
expect(mapped?.providerSessionId).toBe('thread-2');
|
|
49
|
+
expect(mapped?.autoApprovedResult).toBeNull();
|
|
50
|
+
expect(mapped?.pendingRequest?.providerRequestId).toBe(8);
|
|
51
|
+
expect(mapped?.pendingRequest?.responseKind).toBe('commandExecutionApproval');
|
|
52
|
+
expect(mapped?.pendingRequest?.request).toMatchObject({
|
|
53
|
+
id: '8',
|
|
54
|
+
kind: 'requestUserInput',
|
|
55
|
+
title: 'Command approval required',
|
|
56
|
+
turnId: 'turn-2',
|
|
57
|
+
itemId: 'item-1',
|
|
58
|
+
});
|
|
59
|
+
expect(mapped?.pendingRequest?.request.description).toContain('Needs to verify changes.');
|
|
60
|
+
expect(mapped?.pendingRequest?.request.description).toContain('Command: pnpm test');
|
|
61
|
+
expect(mapped?.pendingRequest?.request.description).toContain('CWD: /repo');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('maps generic user-input requests and auto-approves recommended allow answers in yolo mode', () => {
|
|
65
|
+
const request: AgentProviderRequest = {
|
|
66
|
+
provider: 'codex',
|
|
67
|
+
id: 9,
|
|
68
|
+
method: 'requestUserInput',
|
|
69
|
+
params: {
|
|
70
|
+
threadId: 'thread-3',
|
|
71
|
+
turnId: 'turn-3',
|
|
72
|
+
questions: [
|
|
73
|
+
{
|
|
74
|
+
id: 'approval',
|
|
75
|
+
header: 'Tool approval',
|
|
76
|
+
question: 'Allow tool use?',
|
|
77
|
+
isOther: false,
|
|
78
|
+
isSecret: false,
|
|
79
|
+
options: [
|
|
80
|
+
{ label: 'Allow (Recommended)', description: 'Continue.' },
|
|
81
|
+
{ label: 'Deny', description: 'Stop.' },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(mapCodexProviderRequest(request, 'yolo')).toEqual({
|
|
89
|
+
providerRequestId: 9,
|
|
90
|
+
providerSessionId: 'thread-3',
|
|
91
|
+
autoApprovedResult: {
|
|
92
|
+
answers: {
|
|
93
|
+
approval: {
|
|
94
|
+
answers: ['Allow (Recommended)'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
pendingRequest: null,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('builds interactive approval responses from pending request answers', () => {
|
|
103
|
+
const pending = mapCodexProviderRequest(
|
|
104
|
+
{
|
|
105
|
+
provider: 'codex',
|
|
106
|
+
id: 10,
|
|
107
|
+
method: 'item/fileChange/requestApproval',
|
|
108
|
+
params: {
|
|
109
|
+
threadId: 'thread-4',
|
|
110
|
+
turnId: 'turn-4',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
'guarded',
|
|
114
|
+
)?.pendingRequest;
|
|
115
|
+
|
|
116
|
+
expect(pending).toBeTruthy();
|
|
117
|
+
expect(
|
|
118
|
+
buildCodexProviderRequestResponse(pending!, {
|
|
119
|
+
answers: {
|
|
120
|
+
approval: {
|
|
121
|
+
answers: ['Deny'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
).toEqual({ decision: 'decline' });
|
|
126
|
+
});
|
|
127
|
+
});
|