@peakflo/agent-harness 0.0.0-stage → 0.2.1-rc.3
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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/adapters/acp-adapter.d.ts +177 -0
- package/dist/adapters/acp-adapter.js +1732 -0
- package/dist/adapters/claude-code-adapter.d.ts +171 -0
- package/dist/adapters/claude-code-adapter.js +1918 -0
- package/dist/adapters/codex-adapter.d.ts +67 -0
- package/dist/adapters/codex-adapter.js +681 -0
- package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
- package/dist/adapters/codex-app-server-adapter.js +1309 -0
- package/dist/adapters/coding-agent-adapter.d.ts +289 -0
- package/dist/adapters/coding-agent-adapter.js +31 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +35 -0
- package/dist/adapters/model-gateway.d.ts +84 -0
- package/dist/adapters/model-gateway.js +270 -0
- package/dist/adapters/opencode-adapter.d.ts +403 -0
- package/dist/adapters/opencode-adapter.js +2710 -0
- package/dist/adapters/opencode-config.d.ts +41 -0
- package/dist/adapters/opencode-config.js +147 -0
- package/dist/adapters/opencode-server-policy.d.ts +82 -0
- package/dist/adapters/opencode-server-policy.js +148 -0
- package/dist/adapters/pi-adapter.d.ts +168 -0
- package/dist/adapters/pi-adapter.js +1468 -0
- package/dist/capabilities.d.ts +58 -0
- package/dist/capabilities.js +161 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +231 -0
- package/dist/contract.d.ts +273 -0
- package/dist/contract.js +357 -0
- package/dist/control-plane.d.ts +178 -0
- package/dist/control-plane.js +431 -0
- package/dist/credential-store.d.ts +110 -0
- package/dist/credential-store.js +181 -0
- package/dist/enrol/browser-enrol.d.ts +85 -0
- package/dist/enrol/browser-enrol.js +387 -0
- package/dist/github-command-guards.d.ts +20 -0
- package/dist/github-command-guards.js +330 -0
- package/dist/github-proxy-certificates.d.ts +25 -0
- package/dist/github-proxy-certificates.js +145 -0
- package/dist/github-proxy.d.ts +37 -0
- package/dist/github-proxy.js +476 -0
- package/dist/github-workspace.d.ts +55 -0
- package/dist/github-workspace.js +537 -0
- package/dist/harness.d.ts +333 -0
- package/dist/harness.js +1414 -0
- package/dist/health.d.ts +45 -0
- package/dist/health.js +116 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +123 -0
- package/dist/model-probe.d.ts +96 -0
- package/dist/model-probe.js +195 -0
- package/dist/options.d.ts +15 -0
- package/dist/options.js +212 -0
- package/dist/ports-default.d.ts +212 -0
- package/dist/ports-default.js +333 -0
- package/dist/ports.d.ts +436 -0
- package/dist/ports.js +27 -0
- package/dist/runtime/attachments.d.ts +92 -0
- package/dist/runtime/attachments.js +377 -0
- package/dist/runtime/child-stream-guards.d.ts +45 -0
- package/dist/runtime/child-stream-guards.js +111 -0
- package/dist/runtime/conversation.d.ts +87 -0
- package/dist/runtime/conversation.js +156 -0
- package/dist/runtime/lease-renewal.d.ts +169 -0
- package/dist/runtime/lease-renewal.js +433 -0
- package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
- package/dist/runtime/mcp-auth-proxy.js +300 -0
- package/dist/runtime/permission-mode.d.ts +116 -0
- package/dist/runtime/permission-mode.js +146 -0
- package/dist/runtime/projection.d.ts +71 -0
- package/dist/runtime/projection.js +211 -0
- package/dist/runtime/session-runner.d.ts +520 -0
- package/dist/runtime/session-runner.js +2122 -0
- package/dist/shared/reasoning-effort.d.ts +11 -0
- package/dist/shared/reasoning-effort.js +20 -0
- package/dist/store/conversation-store.d.ts +100 -0
- package/dist/store/conversation-store.js +305 -0
- package/dist/store/sqlite-store.d.ts +120 -0
- package/dist/store/sqlite-store.js +346 -0
- package/dist/types.d.ts +952 -0
- package/dist/types.js +55 -0
- package/dist/version.d.ts +32 -0
- package/dist/version.js +77 -0
- package/package.json +53 -4
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex App Server adapter.
|
|
4
|
+
*
|
|
5
|
+
* Experimental replacement path for Codex ACP. This talks to `codex app-server`
|
|
6
|
+
* over JSON-RPC stdio and maps the app-server thread/turn/item protocol onto
|
|
7
|
+
* 20x's CodingAgentAdapter contract.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.CodexAppServerAdapter = void 0;
|
|
44
|
+
/**
|
|
45
|
+
* Extracted verbatim from 20x `src/main/adapters/codex-app-server-adapter.ts`.
|
|
46
|
+
* CHANGED: imports only — `../child-stream-guards` ->
|
|
47
|
+
* `../runtime/child-stream-guards`. Source formatting (no semicolons)
|
|
48
|
+
* preserved.
|
|
49
|
+
*/
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const fs_1 = require("fs");
|
|
52
|
+
const child_stream_guards_1 = require("../runtime/child-stream-guards");
|
|
53
|
+
const os_1 = require("os");
|
|
54
|
+
const path_1 = require("path");
|
|
55
|
+
const util_1 = require("util");
|
|
56
|
+
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
57
|
+
const DEFAULT_CODEX_APP_SERVER_MODEL = 'gpt-5.5';
|
|
58
|
+
const MAX_IPC_TOOL_INPUT_CHARS = 20_000;
|
|
59
|
+
const MAX_IPC_TOOL_OUTPUT_CHARS = 100_000;
|
|
60
|
+
const MIN_THREAD_LEVEL_ASSISTANT_DEDUPE_CHARS = 40;
|
|
61
|
+
function isObject(value) {
|
|
62
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function asString(value) {
|
|
65
|
+
return typeof value === 'string' ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
function extractThreadId(value) {
|
|
68
|
+
if (!isObject(value))
|
|
69
|
+
return null;
|
|
70
|
+
const direct = asString(value.threadId) || asString(value.thread_id) || asString(value.id);
|
|
71
|
+
if (direct)
|
|
72
|
+
return direct;
|
|
73
|
+
if (isObject(value.thread)) {
|
|
74
|
+
return asString(value.thread.id) || asString(value.thread.threadId) || asString(value.thread.thread_id) || null;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Deterministic 32-bit string hash (djb2). Used to build stable dedup keys for
|
|
80
|
+
* thread items that carry no id — never use Date.now()/random here, or the same
|
|
81
|
+
* item would produce a new key on every reconcile pass and be re-emitted.
|
|
82
|
+
*/
|
|
83
|
+
function hashString(value) {
|
|
84
|
+
let hash = 5381;
|
|
85
|
+
for (let i = 0; i < value.length; i++) {
|
|
86
|
+
hash = ((hash << 5) + hash + value.charCodeAt(i)) | 0;
|
|
87
|
+
}
|
|
88
|
+
return (hash >>> 0).toString(36);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Derives a STABLE identity for a Codex thread item. Many item types
|
|
92
|
+
* (function_call_output, custom_tool_call_output, tool_search_output, and
|
|
93
|
+
* user/developer input messages) have no top-level `id` — only a `call_id`, or
|
|
94
|
+
* nothing at all. This must return the same value across reconcile passes for the
|
|
95
|
+
* same logical item, otherwise the transcript repeats older messages after every
|
|
96
|
+
* idle. Returns null when no id-like field exists (caller falls back to a
|
|
97
|
+
* content-based key).
|
|
98
|
+
*/
|
|
99
|
+
function deriveItemIdentity(item) {
|
|
100
|
+
if (!item)
|
|
101
|
+
return null;
|
|
102
|
+
const direct = asString(item.id) || asString(item.itemId) || asString(item.item_id);
|
|
103
|
+
if (direct)
|
|
104
|
+
return direct;
|
|
105
|
+
const callId = asString(item.call_id) || asString(item.callId);
|
|
106
|
+
if (callId) {
|
|
107
|
+
const type = asString(item.type) || 'item';
|
|
108
|
+
return `${type}:${callId}`;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function extractItemId(params) {
|
|
113
|
+
const item = isObject(params.item) ? params.item : undefined;
|
|
114
|
+
const identity = asString(params.itemId) || deriveItemIdentity(item);
|
|
115
|
+
if (identity)
|
|
116
|
+
return identity;
|
|
117
|
+
// Last resort: a deterministic key derived from the item's content so the same
|
|
118
|
+
// item maps to the same part id across reconciles (was `item-${Date.now()}`,
|
|
119
|
+
// which minted a new id every pass and duplicated the message on each idle).
|
|
120
|
+
const fingerprint = item ? `${asString(item.type) || 'item'}:${extractText(item)}` : 'item';
|
|
121
|
+
return `item-${hashString(fingerprint)}`;
|
|
122
|
+
}
|
|
123
|
+
function extractText(value) {
|
|
124
|
+
if (!value)
|
|
125
|
+
return '';
|
|
126
|
+
if (typeof value === 'string')
|
|
127
|
+
return value;
|
|
128
|
+
if (Array.isArray(value))
|
|
129
|
+
return value.map(extractText).filter(Boolean).join('\n');
|
|
130
|
+
if (!isObject(value))
|
|
131
|
+
return '';
|
|
132
|
+
for (const key of ['text', 'content', 'delta', 'message', 'output']) {
|
|
133
|
+
const text = extractText(value[key]);
|
|
134
|
+
if (text)
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
function extractRole(item) {
|
|
140
|
+
return (asString(item.role) || asString(item.author) || asString(item.sender) || '').toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
function extractToolName(item) {
|
|
143
|
+
const server = asString(item.server);
|
|
144
|
+
const tool = asString(item.tool);
|
|
145
|
+
if ((item.type === 'mcpToolCall' || server || tool) && (server || tool)) {
|
|
146
|
+
return [server, tool].filter(Boolean).join('.');
|
|
147
|
+
}
|
|
148
|
+
return (asString(item.toolName) ||
|
|
149
|
+
asString(item.tool_name) ||
|
|
150
|
+
asString(item.name) ||
|
|
151
|
+
asString(item.type) ||
|
|
152
|
+
'tool');
|
|
153
|
+
}
|
|
154
|
+
function extractToolTitle(item, toolName) {
|
|
155
|
+
if (item.type === 'commandExecution') {
|
|
156
|
+
const actionCommand = Array.isArray(item.commandActions)
|
|
157
|
+
? item.commandActions
|
|
158
|
+
.filter(isObject)
|
|
159
|
+
.map((action) => asString(action.command))
|
|
160
|
+
.find(Boolean)
|
|
161
|
+
: undefined;
|
|
162
|
+
const command = actionCommand || asString(item.command);
|
|
163
|
+
if (command)
|
|
164
|
+
return command;
|
|
165
|
+
}
|
|
166
|
+
if (item.type === 'fileChange') {
|
|
167
|
+
const paths = Array.isArray(item.changes)
|
|
168
|
+
? item.changes
|
|
169
|
+
.filter(isObject)
|
|
170
|
+
.map((change) => asString(change.path))
|
|
171
|
+
.filter((path) => !!path)
|
|
172
|
+
: [];
|
|
173
|
+
const directPath = asString(item.path);
|
|
174
|
+
if (paths.length === 1)
|
|
175
|
+
return (0, path_1.basename)(paths[0]);
|
|
176
|
+
if (paths.length > 1)
|
|
177
|
+
return `${(0, path_1.basename)(paths[0])} +${paths.length - 1}`;
|
|
178
|
+
if (directPath)
|
|
179
|
+
return (0, path_1.basename)(directPath);
|
|
180
|
+
}
|
|
181
|
+
return toolName;
|
|
182
|
+
}
|
|
183
|
+
function truncateForIpc(value, maxChars) {
|
|
184
|
+
if (value.length <= maxChars)
|
|
185
|
+
return value;
|
|
186
|
+
return `${value.slice(0, maxChars)}\n... (truncated for display)`;
|
|
187
|
+
}
|
|
188
|
+
function stringifyForIpc(value, maxChars) {
|
|
189
|
+
if (value == null)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (typeof value === 'string')
|
|
192
|
+
return truncateForIpc(value, maxChars);
|
|
193
|
+
try {
|
|
194
|
+
const seen = new WeakSet();
|
|
195
|
+
return truncateForIpc(JSON.stringify(value, (_key, nested) => {
|
|
196
|
+
if (typeof nested === 'bigint')
|
|
197
|
+
return nested.toString();
|
|
198
|
+
if (typeof nested === 'function' || typeof nested === 'symbol')
|
|
199
|
+
return undefined;
|
|
200
|
+
if (nested && typeof nested === 'object') {
|
|
201
|
+
if (seen.has(nested))
|
|
202
|
+
return '[Circular]';
|
|
203
|
+
seen.add(nested);
|
|
204
|
+
}
|
|
205
|
+
return nested;
|
|
206
|
+
}, 2), maxChars);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return truncateForIpc(String(value), maxChars);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function normalizeCodexMcpServerName(name) {
|
|
213
|
+
const normalized = name.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
214
|
+
return normalized || 'mcp_server';
|
|
215
|
+
}
|
|
216
|
+
function uniquePaths(paths) {
|
|
217
|
+
return Array.from(new Set(paths.filter(Boolean).map((path) => (0, path_1.resolve)(path))));
|
|
218
|
+
}
|
|
219
|
+
function isSubpath(parent, child) {
|
|
220
|
+
const rel = (0, path_1.relative)((0, path_1.resolve)(parent), (0, path_1.resolve)(child));
|
|
221
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel));
|
|
222
|
+
}
|
|
223
|
+
function summarizeApproval(params, fallback) {
|
|
224
|
+
const command = asString(params.command);
|
|
225
|
+
const reason = asString(params.reason);
|
|
226
|
+
const itemId = asString(params.itemId) || asString(params.callId);
|
|
227
|
+
return [command || fallback, reason, itemId ? `id: ${itemId}` : ''].filter(Boolean).join('\n');
|
|
228
|
+
}
|
|
229
|
+
function normalizeDecisionName(decision) {
|
|
230
|
+
if (typeof decision === 'string')
|
|
231
|
+
return decision;
|
|
232
|
+
if (isObject(decision)) {
|
|
233
|
+
return Object.keys(decision)[0] || 'accept';
|
|
234
|
+
}
|
|
235
|
+
return 'accept';
|
|
236
|
+
}
|
|
237
|
+
function decisionLabel(decision) {
|
|
238
|
+
switch (decision) {
|
|
239
|
+
case 'accept':
|
|
240
|
+
case 'approved':
|
|
241
|
+
return 'Allow';
|
|
242
|
+
case 'acceptForSession':
|
|
243
|
+
case 'approved_for_session':
|
|
244
|
+
return 'Allow for Session';
|
|
245
|
+
case 'decline':
|
|
246
|
+
case 'denied':
|
|
247
|
+
return 'Deny';
|
|
248
|
+
case 'cancel':
|
|
249
|
+
case 'abort':
|
|
250
|
+
return 'Deny and Stop';
|
|
251
|
+
default:
|
|
252
|
+
return decision;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
class CodexAppServerAdapter {
|
|
256
|
+
sessions = new Map();
|
|
257
|
+
codexExecutablePath = null;
|
|
258
|
+
onDataAvailable;
|
|
259
|
+
async initialize() {
|
|
260
|
+
const health = await this.checkHealth();
|
|
261
|
+
if (!health.available) {
|
|
262
|
+
throw new Error(health.reason || 'Codex app-server not available');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async createSession(config) {
|
|
266
|
+
const session = await this.startAppServerProcess(config, config.taskId);
|
|
267
|
+
this.sessions.set(config.taskId, session);
|
|
268
|
+
await this.initializeAppServer(session);
|
|
269
|
+
const result = await this.sendRpcRequest(session, 'thread/start', {
|
|
270
|
+
cwd: config.workspaceDir,
|
|
271
|
+
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
272
|
+
approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
|
|
273
|
+
approvalsReviewer: 'user',
|
|
274
|
+
sandbox: this.resolveSandboxMode(config),
|
|
275
|
+
developerInstructions: config.systemPrompt || null,
|
|
276
|
+
runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
|
|
277
|
+
config: this.buildConfigOverrides(config)
|
|
278
|
+
});
|
|
279
|
+
const threadId = extractThreadId(result);
|
|
280
|
+
if (!threadId) {
|
|
281
|
+
throw new Error('Codex app-server did not return a thread id');
|
|
282
|
+
}
|
|
283
|
+
session.threadId = threadId;
|
|
284
|
+
this.sessions.delete(config.taskId);
|
|
285
|
+
this.sessions.set(threadId, session);
|
|
286
|
+
void this.logMcpServerInventory(session, threadId, 'thread/start');
|
|
287
|
+
return threadId;
|
|
288
|
+
}
|
|
289
|
+
async resumeSession(sessionId, config) {
|
|
290
|
+
const session = await this.startAppServerProcess(config, sessionId);
|
|
291
|
+
session.threadId = sessionId;
|
|
292
|
+
this.sessions.set(sessionId, session);
|
|
293
|
+
await this.initializeAppServer(session);
|
|
294
|
+
await this.sendRpcRequest(session, 'thread/resume', {
|
|
295
|
+
threadId: sessionId,
|
|
296
|
+
cwd: config.workspaceDir,
|
|
297
|
+
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
298
|
+
approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
|
|
299
|
+
approvalsReviewer: 'user',
|
|
300
|
+
sandbox: this.resolveSandboxMode(config),
|
|
301
|
+
runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
|
|
302
|
+
initialTurnsPage: { limit: 50 },
|
|
303
|
+
config: this.buildConfigOverrides(config)
|
|
304
|
+
});
|
|
305
|
+
void this.logMcpServerInventory(session, sessionId, 'thread/resume');
|
|
306
|
+
try {
|
|
307
|
+
await this.bufferAllThreadItems(session, sessionId);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.warn('[CodexAppServerAdapter] Failed to list thread items after resume:', error);
|
|
311
|
+
}
|
|
312
|
+
const messages = await this.getAllMessages(sessionId, config);
|
|
313
|
+
session.messageBuffer = [];
|
|
314
|
+
return messages;
|
|
315
|
+
}
|
|
316
|
+
async sendPrompt(sessionId, parts, config) {
|
|
317
|
+
const session = this.requireSession(sessionId);
|
|
318
|
+
if (!session.threadId) {
|
|
319
|
+
throw new Error(`Codex app-server session has no thread id: ${sessionId}`);
|
|
320
|
+
}
|
|
321
|
+
const promptText = parts
|
|
322
|
+
.filter((part) => part.type === coding_agent_adapter_1.MessagePartType.TEXT && part.text)
|
|
323
|
+
.map((part) => part.text)
|
|
324
|
+
.join('\n');
|
|
325
|
+
if (!promptText) {
|
|
326
|
+
throw new Error('No text content in message parts');
|
|
327
|
+
}
|
|
328
|
+
session.messageBuffer = [];
|
|
329
|
+
const userItem = {
|
|
330
|
+
method: 'item/completed',
|
|
331
|
+
params: {
|
|
332
|
+
threadId: session.threadId,
|
|
333
|
+
turnId: session.activeTurnId || `local-${Date.now()}`,
|
|
334
|
+
item: {
|
|
335
|
+
id: `user-${Date.now()}`,
|
|
336
|
+
type: 'user_message',
|
|
337
|
+
text: promptText
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
this.addEvent(session, userItem);
|
|
342
|
+
session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
|
|
343
|
+
session.lastError = null;
|
|
344
|
+
const result = await this.sendRpcRequest(session, 'turn/start', {
|
|
345
|
+
threadId: session.threadId,
|
|
346
|
+
input: [{ type: 'text', text: promptText }],
|
|
347
|
+
cwd: config.workspaceDir,
|
|
348
|
+
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
349
|
+
effort: config.reasoningEffort && config.reasoningEffort !== 'max' ? config.reasoningEffort : null,
|
|
350
|
+
approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
|
|
351
|
+
approvalsReviewer: 'user',
|
|
352
|
+
sandbox: this.resolveSandboxMode(config),
|
|
353
|
+
sandboxPolicy: this.buildSandboxPolicy(config),
|
|
354
|
+
runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
|
|
355
|
+
config: this.buildConfigOverrides(config)
|
|
356
|
+
});
|
|
357
|
+
if (isObject(result)) {
|
|
358
|
+
session.activeTurnId = asString(result.turnId) || asString(result.turn_id) || session.activeTurnId;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async getStatus(sessionId, _config) {
|
|
362
|
+
const session = this.sessions.get(sessionId);
|
|
363
|
+
if (!session) {
|
|
364
|
+
return { type: coding_agent_adapter_1.SessionStatusType.ERROR, message: 'Session not found' };
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
type: session.status,
|
|
368
|
+
message: session.status === coding_agent_adapter_1.SessionStatusType.ERROR ? (session.lastError || 'Process error') : undefined
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async pollMessages(sessionId, seenMessageIds, seenPartIds, partContentLengths, _config) {
|
|
372
|
+
const session = this.sessions.get(sessionId);
|
|
373
|
+
if (!session)
|
|
374
|
+
return [];
|
|
375
|
+
const parts = session.messageBuffer.flatMap((event) => this.convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session));
|
|
376
|
+
session.messageBuffer = [];
|
|
377
|
+
return parts;
|
|
378
|
+
}
|
|
379
|
+
async getAllMessages(sessionId, _config) {
|
|
380
|
+
const session = this.requireSession(sessionId);
|
|
381
|
+
const seenMessageIds = new Set();
|
|
382
|
+
const seenPartIds = new Set();
|
|
383
|
+
const partContentLengths = new Map();
|
|
384
|
+
const partsByIdAndRole = new Map();
|
|
385
|
+
const assistantTextKeysByTurn = session.assistantTextKeysByTurn;
|
|
386
|
+
session.assistantTextKeysByTurn = new Map();
|
|
387
|
+
try {
|
|
388
|
+
for (const event of session.permanentMessages) {
|
|
389
|
+
const parts = this.convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session);
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
const key = `${part.id || `part-${partsByIdAndRole.size}`}-${part.role || coding_agent_adapter_1.MessageRole.ASSISTANT}`;
|
|
392
|
+
if (!partsByIdAndRole.has(key) || part.update) {
|
|
393
|
+
partsByIdAndRole.set(key, part);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
session.assistantTextKeysByTurn = assistantTextKeysByTurn;
|
|
400
|
+
}
|
|
401
|
+
const messages = [];
|
|
402
|
+
let currentMessage = null;
|
|
403
|
+
let previousPart = null;
|
|
404
|
+
let messageCounter = 0;
|
|
405
|
+
for (const part of partsByIdAndRole.values()) {
|
|
406
|
+
const role = part.role === coding_agent_adapter_1.MessageRole.USER
|
|
407
|
+
? coding_agent_adapter_1.MessageRole.USER
|
|
408
|
+
: part.role === coding_agent_adapter_1.MessageRole.SYSTEM
|
|
409
|
+
? coding_agent_adapter_1.MessageRole.SYSTEM
|
|
410
|
+
: coding_agent_adapter_1.MessageRole.ASSISTANT;
|
|
411
|
+
const startsNewMessage = !currentMessage
|
|
412
|
+
|| currentMessage.role !== role
|
|
413
|
+
|| !previousPart
|
|
414
|
+
|| part.id !== previousPart.id;
|
|
415
|
+
if (startsNewMessage) {
|
|
416
|
+
if (currentMessage)
|
|
417
|
+
messages.push(currentMessage);
|
|
418
|
+
currentMessage = {
|
|
419
|
+
id: `msg-${messageCounter++}`,
|
|
420
|
+
role,
|
|
421
|
+
parts: []
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
currentMessage.parts.push(part);
|
|
425
|
+
previousPart = part;
|
|
426
|
+
}
|
|
427
|
+
if (currentMessage)
|
|
428
|
+
messages.push(currentMessage);
|
|
429
|
+
return messages;
|
|
430
|
+
}
|
|
431
|
+
async abortPrompt(sessionId, _config) {
|
|
432
|
+
const session = this.requireSession(sessionId);
|
|
433
|
+
if (!session.threadId || !session.activeTurnId)
|
|
434
|
+
return;
|
|
435
|
+
await this.sendRpcRequest(session, 'turn/interrupt', {
|
|
436
|
+
threadId: session.threadId,
|
|
437
|
+
turnId: session.activeTurnId
|
|
438
|
+
});
|
|
439
|
+
session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
|
|
440
|
+
}
|
|
441
|
+
async destroySession(sessionId, _config) {
|
|
442
|
+
const session = this.sessions.get(sessionId);
|
|
443
|
+
if (!session)
|
|
444
|
+
return;
|
|
445
|
+
session.process.kill('SIGTERM');
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
if (!session.process.killed) {
|
|
448
|
+
session.process.kill('SIGKILL');
|
|
449
|
+
}
|
|
450
|
+
}, 1000);
|
|
451
|
+
session.messageBuffer.length = 0;
|
|
452
|
+
session.permanentMessages.length = 0;
|
|
453
|
+
session.pendingRequests.clear();
|
|
454
|
+
session.streamedTextByItemId.clear();
|
|
455
|
+
session.assistantTextKeysByTurn.clear();
|
|
456
|
+
session.runningTools.clear();
|
|
457
|
+
this.sessions.delete(sessionId);
|
|
458
|
+
}
|
|
459
|
+
async registerMcpServer(_serverName, _mcpConfig, _workspaceDir) {
|
|
460
|
+
// App Server reads MCP configuration from Codex config. Per-session MCP
|
|
461
|
+
// injection will be added after the thread config shape is verified in app.
|
|
462
|
+
console.log('[CodexAppServerAdapter] MCP server registration deferred to Codex app-server config');
|
|
463
|
+
}
|
|
464
|
+
async checkHealth() {
|
|
465
|
+
try {
|
|
466
|
+
const executable = await this.findCodexExecutable();
|
|
467
|
+
await this.execFileAsync(executable, ['app-server', '--help'], { timeout: 5000 });
|
|
468
|
+
return { available: true };
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
return {
|
|
472
|
+
available: false,
|
|
473
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
getPendingApproval(sessionId) {
|
|
478
|
+
return this.sessions.get(sessionId)?.pendingApproval || null;
|
|
479
|
+
}
|
|
480
|
+
async respondToApproval(sessionId, approved, optionId) {
|
|
481
|
+
const session = this.requireSession(sessionId);
|
|
482
|
+
const approval = session.pendingApproval;
|
|
483
|
+
if (!approval)
|
|
484
|
+
return;
|
|
485
|
+
const selected = optionId || approval.options.find((option) => approved
|
|
486
|
+
? ['acceptForSession', 'accept', 'approved_for_session', 'approved'].includes(option.optionId)
|
|
487
|
+
: ['cancel', 'abort', 'decline', 'denied'].includes(option.optionId))?.optionId || (approved ? 'accept' : 'cancel');
|
|
488
|
+
const response = this.buildApprovalResponse(approval.responseKind, selected, approved);
|
|
489
|
+
this.sendRpcResponse(session, approval.requestId, response);
|
|
490
|
+
session.pendingApproval = null;
|
|
491
|
+
if (!approved) {
|
|
492
|
+
session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async getRunningTools(sessionId, _config) {
|
|
496
|
+
return Array.from(this.sessions.get(sessionId)?.runningTools.values() || []);
|
|
497
|
+
}
|
|
498
|
+
async startAppServerProcess(config, sessionId) {
|
|
499
|
+
const executable = await this.findCodexExecutable();
|
|
500
|
+
const authEnv = this.buildEnvironment(config);
|
|
501
|
+
const env = authEnv.env;
|
|
502
|
+
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(executable);
|
|
503
|
+
const child = (0, child_process_1.spawn)(executable, ['app-server', '--stdio'], {
|
|
504
|
+
cwd: config.workspaceDir,
|
|
505
|
+
env,
|
|
506
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
507
|
+
...(needsShell ? { shell: true } : {})
|
|
508
|
+
});
|
|
509
|
+
// Every pipe needs an error listener before the first write. The app server
|
|
510
|
+
// can exit at any moment, and an unhandled EPIPE on its stdin crashes the
|
|
511
|
+
// main process.
|
|
512
|
+
(0, child_stream_guards_1.guardChildStreams)(child, 'CodexAppServerAdapter');
|
|
513
|
+
const session = {
|
|
514
|
+
sessionId,
|
|
515
|
+
threadId: null,
|
|
516
|
+
activeTurnId: null,
|
|
517
|
+
process: child,
|
|
518
|
+
stdoutBuffer: '',
|
|
519
|
+
status: coding_agent_adapter_1.SessionStatusType.IDLE,
|
|
520
|
+
messageBuffer: [],
|
|
521
|
+
permanentMessages: [],
|
|
522
|
+
bufferedThreadItemIds: new Set(),
|
|
523
|
+
pendingCompletionRefreshes: 0,
|
|
524
|
+
sawThreadStatusNotification: false,
|
|
525
|
+
pendingThreadIdle: false,
|
|
526
|
+
pendingRequests: new Map(),
|
|
527
|
+
pendingApproval: null,
|
|
528
|
+
nextRequestId: 1,
|
|
529
|
+
lastError: null,
|
|
530
|
+
config,
|
|
531
|
+
streamedTextByItemId: new Map(),
|
|
532
|
+
assistantTextKeysByTurn: new Map(),
|
|
533
|
+
runningTools: new Map(),
|
|
534
|
+
codexUseApiKey: authEnv.usesApiKey,
|
|
535
|
+
codexAuthSummary: authEnv.summary
|
|
536
|
+
};
|
|
537
|
+
this.setupStdoutParser(child, session);
|
|
538
|
+
child.stderr?.on('data', (chunk) => {
|
|
539
|
+
console.log('[CodexAppServerAdapter] stderr:', chunk.toString());
|
|
540
|
+
});
|
|
541
|
+
child.on('exit', (code, signal) => {
|
|
542
|
+
console.log(`[CodexAppServerAdapter] process exited: code=${code}, signal=${signal}`);
|
|
543
|
+
if (code !== 0 && code !== null) {
|
|
544
|
+
session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
|
|
545
|
+
session.lastError = `Codex app-server exited with code ${code}`;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
return session;
|
|
549
|
+
}
|
|
550
|
+
async initializeAppServer(session) {
|
|
551
|
+
await this.sendRpcRequest(session, 'initialize', {
|
|
552
|
+
clientInfo: {
|
|
553
|
+
name: '20x',
|
|
554
|
+
title: '20x',
|
|
555
|
+
version: '0.0.1'
|
|
556
|
+
},
|
|
557
|
+
capabilities: {
|
|
558
|
+
experimentalApi: true,
|
|
559
|
+
mcpServerOpenaiFormElicitation: true
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
this.sendRpcNotification(session, 'initialized', {});
|
|
563
|
+
}
|
|
564
|
+
async findCodexExecutable() {
|
|
565
|
+
if (this.codexExecutablePath)
|
|
566
|
+
return this.codexExecutablePath;
|
|
567
|
+
const isWin = process.platform === 'win32';
|
|
568
|
+
const finder = isWin ? 'where' : 'which';
|
|
569
|
+
const binary = isWin ? 'codex.cmd' : 'codex';
|
|
570
|
+
const { stdout } = await this.execFileAsync(finder, [binary]);
|
|
571
|
+
this.codexExecutablePath = stdout.trim().split(/\r?\n/)[0];
|
|
572
|
+
return this.codexExecutablePath;
|
|
573
|
+
}
|
|
574
|
+
async execFileAsync(command, args, options) {
|
|
575
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
576
|
+
const execFileAsync = (0, util_1.promisify)(execFile);
|
|
577
|
+
return await execFileAsync(command, args, options);
|
|
578
|
+
}
|
|
579
|
+
buildEnvironment(config) {
|
|
580
|
+
const env = { ...process.env };
|
|
581
|
+
for (const [key, value] of Object.entries(config.secretEnvVars || {})) {
|
|
582
|
+
env[key] = value;
|
|
583
|
+
}
|
|
584
|
+
const explicitApiKey = config.apiKeys?.openai;
|
|
585
|
+
const useApiKey = config.authMethod === 'api_key' ? true
|
|
586
|
+
: config.authMethod === 'subscription' ? false
|
|
587
|
+
: !!explicitApiKey;
|
|
588
|
+
if (useApiKey) {
|
|
589
|
+
const key = explicitApiKey || env.OPENAI_API_KEY || env.CODEX_API_KEY;
|
|
590
|
+
if (key) {
|
|
591
|
+
env.OPENAI_API_KEY = key;
|
|
592
|
+
env.CODEX_API_KEY = key;
|
|
593
|
+
}
|
|
594
|
+
env.NO_BROWSER = '1';
|
|
595
|
+
env.CODEX_HOME = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'codex-app-server-session-'));
|
|
596
|
+
return {
|
|
597
|
+
env,
|
|
598
|
+
usesApiKey: true,
|
|
599
|
+
summary: `API key (authMethod=${config.authMethod ?? 'legacy'}, isolated CODEX_HOME)`
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (!useApiKey) {
|
|
603
|
+
delete env.OPENAI_API_KEY;
|
|
604
|
+
delete env.CODEX_API_KEY;
|
|
605
|
+
if (!env.CODEX_HOME) {
|
|
606
|
+
env.CODEX_HOME = (0, path_1.join)((0, os_1.homedir)(), '.codex');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
env,
|
|
611
|
+
usesApiKey: false,
|
|
612
|
+
summary: `subscription (authMethod=${config.authMethod ?? 'legacy'}, CODEX_HOME=${env.CODEX_HOME ?? 'default'})`
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
buildConfigOverrides(config) {
|
|
616
|
+
const overrides = {};
|
|
617
|
+
if (config.reasoningEffort && config.reasoningEffort !== 'max') {
|
|
618
|
+
overrides.model_reasoning_effort = config.reasoningEffort;
|
|
619
|
+
}
|
|
620
|
+
if (this.resolveSandboxMode(config) === 'workspace-write') {
|
|
621
|
+
overrides.sandbox_workspace_write = {
|
|
622
|
+
network_access: true,
|
|
623
|
+
writable_roots: this.buildRuntimeWorkspaceRoots(config.workspaceDir)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
627
|
+
overrides.mcp_servers = this.convertMcpServers(config.mcpServers);
|
|
628
|
+
}
|
|
629
|
+
return overrides;
|
|
630
|
+
}
|
|
631
|
+
buildRuntimeWorkspaceRoots(workspaceDir) {
|
|
632
|
+
return uniquePaths([workspaceDir, ...this.resolveExternalGitRoots(workspaceDir)]);
|
|
633
|
+
}
|
|
634
|
+
resolveExternalGitRoots(workspaceDir) {
|
|
635
|
+
const workspaceRoot = (0, path_1.resolve)(workspaceDir);
|
|
636
|
+
const dotGitPath = (0, path_1.join)(workspaceRoot, '.git');
|
|
637
|
+
if (!(0, fs_1.existsSync)(dotGitPath))
|
|
638
|
+
return [];
|
|
639
|
+
try {
|
|
640
|
+
const dotGitContent = (0, fs_1.readFileSync)(dotGitPath, 'utf8').trim();
|
|
641
|
+
if (!dotGitContent.startsWith('gitdir:'))
|
|
642
|
+
return [];
|
|
643
|
+
const rawGitDir = dotGitContent.slice('gitdir:'.length).trim();
|
|
644
|
+
if (!rawGitDir)
|
|
645
|
+
return [];
|
|
646
|
+
const gitDir = (0, path_1.isAbsolute)(rawGitDir)
|
|
647
|
+
? (0, path_1.resolve)(rawGitDir)
|
|
648
|
+
: (0, path_1.resolve)(workspaceRoot, rawGitDir);
|
|
649
|
+
const commonDirPath = (0, path_1.join)(gitDir, 'commondir');
|
|
650
|
+
const rawCommonDir = (0, fs_1.existsSync)(commonDirPath)
|
|
651
|
+
? (0, fs_1.readFileSync)(commonDirPath, 'utf8').trim()
|
|
652
|
+
: '';
|
|
653
|
+
const commonDir = rawCommonDir
|
|
654
|
+
? ((0, path_1.isAbsolute)(rawCommonDir) ? (0, path_1.resolve)(rawCommonDir) : (0, path_1.resolve)(gitDir, rawCommonDir))
|
|
655
|
+
: gitDir;
|
|
656
|
+
return [commonDir, gitDir].filter((path) => !isSubpath(workspaceRoot, path));
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
console.warn('[CodexAppServerAdapter] Failed to resolve external git metadata roots:', error);
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
convertMcpServers(servers) {
|
|
664
|
+
const result = {};
|
|
665
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
666
|
+
const codexName = normalizeCodexMcpServerName(name);
|
|
667
|
+
if (server.type === 'stdio') {
|
|
668
|
+
result[codexName] = {
|
|
669
|
+
command: server.command,
|
|
670
|
+
args: server.args || [],
|
|
671
|
+
env: server.env || {}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
const remoteConfig = { url: server.url };
|
|
676
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
677
|
+
remoteConfig.http_headers = server.headers;
|
|
678
|
+
}
|
|
679
|
+
result[codexName] = remoteConfig;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
async logMcpServerInventory(session, threadId, context) {
|
|
685
|
+
try {
|
|
686
|
+
const result = await this.sendRpcRequest(session, 'mcpServerStatus/list', {
|
|
687
|
+
threadId,
|
|
688
|
+
detail: 'toolsAndAuthOnly',
|
|
689
|
+
limit: 100
|
|
690
|
+
});
|
|
691
|
+
if (!isObject(result) || !Array.isArray(result.data))
|
|
692
|
+
return;
|
|
693
|
+
const servers = result.data
|
|
694
|
+
.filter(isObject)
|
|
695
|
+
.map((server) => {
|
|
696
|
+
const tools = server.tools;
|
|
697
|
+
const toolNames = Array.isArray(tools)
|
|
698
|
+
? tools.map((tool) => isObject(tool) ? asString(tool.name) : undefined).filter(Boolean)
|
|
699
|
+
: isObject(tools)
|
|
700
|
+
? Object.keys(tools)
|
|
701
|
+
: [];
|
|
702
|
+
return {
|
|
703
|
+
name: asString(server.name) || 'unknown',
|
|
704
|
+
authStatus: asString(server.authStatus) || null,
|
|
705
|
+
toolCount: toolNames.length,
|
|
706
|
+
toolNames: toolNames.slice(0, 50)
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
console.log('[CodexAppServerAdapter] MCP inventory', { context, threadId, servers });
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
console.warn('[CodexAppServerAdapter] Failed to list MCP inventory:', error);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
setupStdoutParser(process, session) {
|
|
716
|
+
process.stdout?.on('data', (chunk) => {
|
|
717
|
+
session.stdoutBuffer += chunk.toString();
|
|
718
|
+
let newlineIndex;
|
|
719
|
+
while ((newlineIndex = session.stdoutBuffer.indexOf('\n')) !== -1) {
|
|
720
|
+
const line = session.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
721
|
+
session.stdoutBuffer = session.stdoutBuffer.slice(newlineIndex + 1);
|
|
722
|
+
if (!line)
|
|
723
|
+
continue;
|
|
724
|
+
try {
|
|
725
|
+
this.handleRpcMessage(session, JSON.parse(line));
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
console.error('[CodexAppServerAdapter] Failed to parse JSON-RPC:', line, error);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
handleRpcMessage(session, message) {
|
|
734
|
+
if ('id' in message && 'method' in message) {
|
|
735
|
+
this.handleServerRequest(session, message);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if ('id' in message) {
|
|
739
|
+
const pending = session.pendingRequests.get(message.id);
|
|
740
|
+
if (!pending)
|
|
741
|
+
return;
|
|
742
|
+
session.pendingRequests.delete(message.id);
|
|
743
|
+
const response = message;
|
|
744
|
+
if (response.error) {
|
|
745
|
+
pending.reject(new Error(response.error.message));
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
pending.resolve(response.result);
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if ('method' in message) {
|
|
753
|
+
this.handleNotification(session, message);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
handleServerRequest(session, request) {
|
|
757
|
+
const params = isObject(request.params) ? request.params : {};
|
|
758
|
+
if (request.method.includes('Approval') || request.method.includes('requestApproval')) {
|
|
759
|
+
this.handleApprovalRequest(session, request, params);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (request.method === 'mcpServer/elicitation/request' || request.method === 'item/tool/requestUserInput') {
|
|
763
|
+
this.handleApprovalRequest(session, request, params);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
this.sendRpcResponse(session, request.id, {});
|
|
767
|
+
}
|
|
768
|
+
handleNotification(session, notification) {
|
|
769
|
+
const params = isObject(notification.params) ? notification.params : {};
|
|
770
|
+
if (notification.method === 'thread/status/changed') {
|
|
771
|
+
const status = asString(params.status);
|
|
772
|
+
session.status = status === 'running' || status === 'busy' ? coding_agent_adapter_1.SessionStatusType.BUSY : coding_agent_adapter_1.SessionStatusType.IDLE;
|
|
773
|
+
}
|
|
774
|
+
if (notification.method === 'turn/started') {
|
|
775
|
+
session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
|
|
776
|
+
session.activeTurnId = asString(params.turnId) || session.activeTurnId;
|
|
777
|
+
session.pendingThreadIdle = false;
|
|
778
|
+
}
|
|
779
|
+
if (notification.method === 'turn/completed') {
|
|
780
|
+
session.activeTurnId = null;
|
|
781
|
+
if (session.threadId) {
|
|
782
|
+
session.pendingCompletionRefreshes += 1;
|
|
783
|
+
void this.reconcileCompletedTurn(session);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (notification.method === 'thread/status/changed') {
|
|
790
|
+
session.sawThreadStatusNotification = true;
|
|
791
|
+
const status = isObject(params.status) ? asString(params.status.type) : '';
|
|
792
|
+
if (status === 'active') {
|
|
793
|
+
session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
|
|
794
|
+
session.pendingThreadIdle = false;
|
|
795
|
+
}
|
|
796
|
+
else if (status === 'idle') {
|
|
797
|
+
session.activeTurnId = null;
|
|
798
|
+
session.pendingThreadIdle = true;
|
|
799
|
+
this.markIdleIfSettled(session);
|
|
800
|
+
}
|
|
801
|
+
else if (status === 'systemError') {
|
|
802
|
+
session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
|
|
803
|
+
session.lastError = 'Codex app-server thread entered system error state';
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (notification.method === 'error') {
|
|
807
|
+
session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
|
|
808
|
+
session.lastError = extractText(params) || 'Codex app-server error';
|
|
809
|
+
}
|
|
810
|
+
if (notification.method === 'serverRequest/resolved') {
|
|
811
|
+
const requestId = params.requestId;
|
|
812
|
+
if (session.pendingApproval && String(session.pendingApproval.requestId) === String(requestId)) {
|
|
813
|
+
session.pendingApproval = null;
|
|
814
|
+
if (session.status === coding_agent_adapter_1.SessionStatusType.WAITING_APPROVAL) {
|
|
815
|
+
session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
this.addEvent(session, notification);
|
|
820
|
+
}
|
|
821
|
+
handleApprovalRequest(session, request, params) {
|
|
822
|
+
if (session.config.permissionMode === 'allow') {
|
|
823
|
+
const responseKind = this.getApprovalResponseKind(request.method);
|
|
824
|
+
const selected = responseKind === 'execCommand' ? 'approved' : 'accept';
|
|
825
|
+
this.sendRpcResponse(session, request.id, this.buildApprovalResponse(responseKind, selected, true));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const toolCallId = asString(params.approvalId) || asString(params.itemId) || asString(params.callId) || String(request.id);
|
|
829
|
+
const responseKind = this.getApprovalResponseKind(request.method);
|
|
830
|
+
const rawDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
|
|
831
|
+
const approvalOptions = rawDecisions.length > 0
|
|
832
|
+
? rawDecisions.map((decision) => {
|
|
833
|
+
const optionId = normalizeDecisionName(decision);
|
|
834
|
+
return {
|
|
835
|
+
optionId,
|
|
836
|
+
name: decisionLabel(optionId),
|
|
837
|
+
kind: optionId.includes('accept') || optionId === 'approved' ? 'allow' : 'reject'
|
|
838
|
+
};
|
|
839
|
+
})
|
|
840
|
+
: [
|
|
841
|
+
{ optionId: 'accept', name: 'Allow', kind: 'allow' },
|
|
842
|
+
{ optionId: 'cancel', name: 'Deny', kind: 'reject' }
|
|
843
|
+
];
|
|
844
|
+
session.pendingApproval = {
|
|
845
|
+
requestId: request.id,
|
|
846
|
+
toolCallId,
|
|
847
|
+
question: summarizeApproval(params, request.method),
|
|
848
|
+
options: approvalOptions,
|
|
849
|
+
responseKind
|
|
850
|
+
};
|
|
851
|
+
session.status = coding_agent_adapter_1.SessionStatusType.WAITING_APPROVAL;
|
|
852
|
+
this.onDataAvailable?.(session.threadId || session.sessionId);
|
|
853
|
+
}
|
|
854
|
+
buildApprovalResponse(responseKind, selected, approved) {
|
|
855
|
+
switch (responseKind) {
|
|
856
|
+
case 'execCommand':
|
|
857
|
+
return { decision: approved ? (selected === 'approved_for_session' ? 'approved_for_session' : 'approved') : (selected === 'denied' ? 'denied' : 'abort') };
|
|
858
|
+
case 'commandExecution':
|
|
859
|
+
return { decision: selected };
|
|
860
|
+
case 'fileChange':
|
|
861
|
+
case 'permissions':
|
|
862
|
+
return { decision: selected };
|
|
863
|
+
case 'elicitation':
|
|
864
|
+
return approved
|
|
865
|
+
? { action: 'accept', content: {} }
|
|
866
|
+
: { action: 'decline' };
|
|
867
|
+
case 'userInput':
|
|
868
|
+
return approved
|
|
869
|
+
? { response: selected }
|
|
870
|
+
: { response: null };
|
|
871
|
+
default:
|
|
872
|
+
return { decision: approved ? selected : 'cancel' };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
getApprovalResponseKind(method) {
|
|
876
|
+
if (method === 'execCommandApproval')
|
|
877
|
+
return 'execCommand';
|
|
878
|
+
if (method.includes('commandExecution'))
|
|
879
|
+
return 'commandExecution';
|
|
880
|
+
if (method.includes('fileChange'))
|
|
881
|
+
return 'fileChange';
|
|
882
|
+
if (method.includes('permissions'))
|
|
883
|
+
return 'permissions';
|
|
884
|
+
if (method === 'mcpServer/elicitation/request')
|
|
885
|
+
return 'elicitation';
|
|
886
|
+
if (method === 'item/tool/requestUserInput')
|
|
887
|
+
return 'userInput';
|
|
888
|
+
return 'generic';
|
|
889
|
+
}
|
|
890
|
+
resolveSandboxMode(config) {
|
|
891
|
+
switch (config.sandboxMode) {
|
|
892
|
+
case 'read-only':
|
|
893
|
+
case 'workspace-write':
|
|
894
|
+
case 'danger-full-access':
|
|
895
|
+
return config.sandboxMode;
|
|
896
|
+
default:
|
|
897
|
+
return 'danger-full-access';
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
buildSandboxPolicy(config) {
|
|
901
|
+
switch (this.resolveSandboxMode(config)) {
|
|
902
|
+
case 'read-only':
|
|
903
|
+
return { type: 'readOnly', networkAccess: true };
|
|
904
|
+
case 'danger-full-access':
|
|
905
|
+
return { type: 'dangerFullAccess' };
|
|
906
|
+
case 'workspace-write':
|
|
907
|
+
default:
|
|
908
|
+
return {
|
|
909
|
+
type: 'workspaceWrite',
|
|
910
|
+
networkAccess: true,
|
|
911
|
+
writableRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
addEvent(session, event) {
|
|
916
|
+
session.messageBuffer.push(event);
|
|
917
|
+
session.permanentMessages.push(event);
|
|
918
|
+
if (session.permanentMessages.length > 1000) {
|
|
919
|
+
session.permanentMessages.splice(0, 250);
|
|
920
|
+
}
|
|
921
|
+
this.onDataAvailable?.(session.threadId || session.sessionId);
|
|
922
|
+
}
|
|
923
|
+
bufferThreadItems(session, result) {
|
|
924
|
+
const items = isObject(result) && Array.isArray(result.data) ? result.data : [];
|
|
925
|
+
for (const item of items) {
|
|
926
|
+
this.bufferReconciledThreadItem(session, item, isObject(item) ? asString(item.turnId) : undefined);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Buffers a single thread item from a reconcile pass, skipping it if it has
|
|
931
|
+
* already been buffered. reconcileCompletedTurn() re-lists the entire thread on
|
|
932
|
+
* every turn/completed, so this MUST be idempotent for every item — including
|
|
933
|
+
* the many Codex item types that carry no top-level `id` (function_call_output,
|
|
934
|
+
* custom_tool_call_output, tool_search_output, user/developer messages). We key
|
|
935
|
+
* off a stable identity (id/itemId/call_id) or, failing that, a deterministic
|
|
936
|
+
* content hash, and forward that key as `itemId` so the derived part id stays
|
|
937
|
+
* stable across passes. Without this, id-less items were re-emitted with a fresh
|
|
938
|
+
* id on every idle and the transcript repeated older messages.
|
|
939
|
+
*/
|
|
940
|
+
bufferReconciledThreadItem(session, item, turnId) {
|
|
941
|
+
let stableItemId;
|
|
942
|
+
if (isObject(item)) {
|
|
943
|
+
stableItemId = this.computeThreadItemKey(item, turnId);
|
|
944
|
+
if (session.bufferedThreadItemIds.has(stableItemId))
|
|
945
|
+
return;
|
|
946
|
+
session.bufferedThreadItemIds.add(stableItemId);
|
|
947
|
+
}
|
|
948
|
+
this.addEvent(session, {
|
|
949
|
+
method: 'item/completed',
|
|
950
|
+
params: {
|
|
951
|
+
threadId: session.threadId,
|
|
952
|
+
turnId,
|
|
953
|
+
item,
|
|
954
|
+
...(stableItemId ? { itemId: stableItemId } : {})
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
computeThreadItemKey(item, turnId) {
|
|
959
|
+
const identity = deriveItemIdentity(item);
|
|
960
|
+
if (identity)
|
|
961
|
+
return identity;
|
|
962
|
+
// No id-like field (e.g. user/developer input messages). Build a deterministic
|
|
963
|
+
// key from turn + type + role + content so the same item maps to the same key
|
|
964
|
+
// across reconcile passes instead of duplicating.
|
|
965
|
+
const type = asString(item.type) || 'item';
|
|
966
|
+
const role = extractRole(item);
|
|
967
|
+
return `synthetic:${turnId || 'noturn'}:${type}:${role}:${hashString(extractText(item))}`;
|
|
968
|
+
}
|
|
969
|
+
async bufferAllThreadItems(session, threadId) {
|
|
970
|
+
try {
|
|
971
|
+
let cursor = null;
|
|
972
|
+
for (let page = 0; page < 20; page++) {
|
|
973
|
+
const result = await this.sendRpcRequest(session, 'thread/items/list', {
|
|
974
|
+
threadId,
|
|
975
|
+
cursor,
|
|
976
|
+
limit: 200,
|
|
977
|
+
sortDirection: 'asc'
|
|
978
|
+
});
|
|
979
|
+
this.bufferThreadItems(session, result);
|
|
980
|
+
cursor = isObject(result) ? (asString(result.nextCursor) || null) : null;
|
|
981
|
+
if (!cursor)
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
console.warn('[CodexAppServerAdapter] Stopped thread/items pagination after 20 pages');
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
988
|
+
if (!message.includes('not supported'))
|
|
989
|
+
throw error;
|
|
990
|
+
await this.bufferAllThreadTurns(session, threadId);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async bufferAllThreadTurns(session, threadId) {
|
|
994
|
+
let cursor = null;
|
|
995
|
+
for (let page = 0; page < 20; page++) {
|
|
996
|
+
let result;
|
|
997
|
+
try {
|
|
998
|
+
result = await this.sendRpcRequest(session, 'thread/turns/list', {
|
|
999
|
+
threadId,
|
|
1000
|
+
cursor,
|
|
1001
|
+
limit: 100,
|
|
1002
|
+
sortDirection: 'asc',
|
|
1003
|
+
itemsView: 'full'
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1008
|
+
if (message.includes('not materialized yet') || message.includes('before first user message')) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
const turns = isObject(result) && Array.isArray(result.data) ? result.data : [];
|
|
1014
|
+
for (const turn of turns) {
|
|
1015
|
+
if (!isObject(turn) || !Array.isArray(turn.items))
|
|
1016
|
+
continue;
|
|
1017
|
+
for (const item of turn.items) {
|
|
1018
|
+
this.bufferReconciledThreadItem(session, item, asString(turn.id));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
cursor = isObject(result) ? (asString(result.nextCursor) || null) : null;
|
|
1022
|
+
if (!cursor)
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
console.warn('[CodexAppServerAdapter] Stopped thread/turns pagination after 20 pages');
|
|
1026
|
+
}
|
|
1027
|
+
async reconcileCompletedTurn(session) {
|
|
1028
|
+
try {
|
|
1029
|
+
if (!session.threadId)
|
|
1030
|
+
return;
|
|
1031
|
+
await this.bufferAllThreadItems(session, session.threadId);
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
console.warn('[CodexAppServerAdapter] Failed to reconcile completed turn items:', error);
|
|
1035
|
+
}
|
|
1036
|
+
finally {
|
|
1037
|
+
session.pendingCompletionRefreshes = Math.max(0, session.pendingCompletionRefreshes - 1);
|
|
1038
|
+
this.markIdleIfSettled(session);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
markIdleIfSettled(session) {
|
|
1042
|
+
if (session.status === coding_agent_adapter_1.SessionStatusType.ERROR)
|
|
1043
|
+
return;
|
|
1044
|
+
if (session.pendingCompletionRefreshes > 0)
|
|
1045
|
+
return;
|
|
1046
|
+
if (session.activeTurnId)
|
|
1047
|
+
return;
|
|
1048
|
+
if (session.sawThreadStatusNotification && !session.pendingThreadIdle)
|
|
1049
|
+
return;
|
|
1050
|
+
session.pendingThreadIdle = false;
|
|
1051
|
+
session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
|
|
1052
|
+
this.onDataAvailable?.(session.threadId || session.sessionId);
|
|
1053
|
+
}
|
|
1054
|
+
convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session) {
|
|
1055
|
+
if (!isObject(event) || !asString(event.method))
|
|
1056
|
+
return [];
|
|
1057
|
+
const method = event.method;
|
|
1058
|
+
const params = isObject(event.params) ? event.params : {};
|
|
1059
|
+
const item = isObject(params.item) ? params.item : undefined;
|
|
1060
|
+
if (method === 'item/agentMessage/delta') {
|
|
1061
|
+
const itemId = asString(params.itemId) || `agent-${Date.now()}`;
|
|
1062
|
+
const partId = `agent-${itemId}`;
|
|
1063
|
+
const delta = asString(params.delta) || '';
|
|
1064
|
+
const previous = session.streamedTextByItemId.get(itemId) || '';
|
|
1065
|
+
const next = previous + delta;
|
|
1066
|
+
const update = seenPartIds.has(partId);
|
|
1067
|
+
seenPartIds.add(partId);
|
|
1068
|
+
session.streamedTextByItemId.set(itemId, next);
|
|
1069
|
+
this.markAssistantTextForTurn(session, params, {}, next);
|
|
1070
|
+
partContentLengths.set(partId, String(next.length));
|
|
1071
|
+
return [{
|
|
1072
|
+
id: partId,
|
|
1073
|
+
type: coding_agent_adapter_1.MessagePartType.TEXT,
|
|
1074
|
+
text: next,
|
|
1075
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT,
|
|
1076
|
+
update
|
|
1077
|
+
}];
|
|
1078
|
+
}
|
|
1079
|
+
if (method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') {
|
|
1080
|
+
const itemId = asString(params.itemId) || `reasoning-${Date.now()}`;
|
|
1081
|
+
const partId = `reasoning-${itemId}`;
|
|
1082
|
+
const text = asString(params.delta) || '';
|
|
1083
|
+
if (!text && seenPartIds.has(partId))
|
|
1084
|
+
return [];
|
|
1085
|
+
seenPartIds.add(partId);
|
|
1086
|
+
partContentLengths.set(partId, String(text.length));
|
|
1087
|
+
return [{
|
|
1088
|
+
id: partId,
|
|
1089
|
+
type: coding_agent_adapter_1.MessagePartType.REASONING,
|
|
1090
|
+
text,
|
|
1091
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT
|
|
1092
|
+
}];
|
|
1093
|
+
}
|
|
1094
|
+
if (method === 'item/commandExecution/outputDelta' ||
|
|
1095
|
+
method === 'command/exec/outputDelta' ||
|
|
1096
|
+
method === 'process/outputDelta') {
|
|
1097
|
+
return this.convertOutputDeltaEvent('command', params, seenPartIds, partContentLengths, session);
|
|
1098
|
+
}
|
|
1099
|
+
if (method === 'item/fileChange/outputDelta' ||
|
|
1100
|
+
method === 'item/fileChange/patchUpdated') {
|
|
1101
|
+
return this.convertOutputDeltaEvent('file_change', params, seenPartIds, partContentLengths, session);
|
|
1102
|
+
}
|
|
1103
|
+
if (method === 'item/plan/delta' ||
|
|
1104
|
+
method === 'turn/plan/updated') {
|
|
1105
|
+
return this.convertOutputDeltaEvent('plan', params, seenPartIds, partContentLengths, session);
|
|
1106
|
+
}
|
|
1107
|
+
if (method === 'item/mcpToolCall/progress') {
|
|
1108
|
+
return this.convertOutputDeltaEvent('mcp_tool', params, seenPartIds, partContentLengths, session);
|
|
1109
|
+
}
|
|
1110
|
+
if ((method === 'item/started' || method === 'item/completed') && item) {
|
|
1111
|
+
return this.convertThreadItem(method, item, params, seenMessageIds, seenPartIds, partContentLengths, session);
|
|
1112
|
+
}
|
|
1113
|
+
if (method === 'error') {
|
|
1114
|
+
const partId = `error-${Date.now()}`;
|
|
1115
|
+
return [{
|
|
1116
|
+
id: partId,
|
|
1117
|
+
type: coding_agent_adapter_1.MessagePartType.ERROR,
|
|
1118
|
+
text: extractText(params) || 'Codex app-server error',
|
|
1119
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT
|
|
1120
|
+
}];
|
|
1121
|
+
}
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
convertOutputDeltaEvent(toolName, params, seenPartIds, partContentLengths, session) {
|
|
1125
|
+
const itemId = asString(params.itemId) || asString(params.processId) || asString(params.turnId) || `${toolName}-${Date.now()}`;
|
|
1126
|
+
const partId = `tool-${itemId}`;
|
|
1127
|
+
const previous = session.streamedTextByItemId.get(partId) || '';
|
|
1128
|
+
const next = truncateForIpc(previous + extractText(params), MAX_IPC_TOOL_OUTPUT_CHARS);
|
|
1129
|
+
const update = seenPartIds.has(partId);
|
|
1130
|
+
seenPartIds.add(partId);
|
|
1131
|
+
session.streamedTextByItemId.set(partId, next);
|
|
1132
|
+
partContentLengths.set(partId, `${next.length}:${toolName}`);
|
|
1133
|
+
return [{
|
|
1134
|
+
id: partId,
|
|
1135
|
+
type: coding_agent_adapter_1.MessagePartType.TOOL,
|
|
1136
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT,
|
|
1137
|
+
tool: {
|
|
1138
|
+
name: toolName,
|
|
1139
|
+
status: 'running',
|
|
1140
|
+
title: toolName,
|
|
1141
|
+
input: stringifyForIpc(params, MAX_IPC_TOOL_INPUT_CHARS),
|
|
1142
|
+
output: next
|
|
1143
|
+
},
|
|
1144
|
+
update
|
|
1145
|
+
}];
|
|
1146
|
+
}
|
|
1147
|
+
convertThreadItem(method, item, params, _seenMessageIds, seenPartIds, partContentLengths, session) {
|
|
1148
|
+
const itemId = extractItemId(params);
|
|
1149
|
+
const type = asString(item.type) || '';
|
|
1150
|
+
const role = extractRole(item);
|
|
1151
|
+
const text = extractText(item);
|
|
1152
|
+
if (type.includes('reasoning')) {
|
|
1153
|
+
const partId = `reasoning-${itemId}`;
|
|
1154
|
+
if (seenPartIds.has(partId))
|
|
1155
|
+
return [];
|
|
1156
|
+
seenPartIds.add(partId);
|
|
1157
|
+
partContentLengths.set(partId, String(text.length));
|
|
1158
|
+
return [{
|
|
1159
|
+
id: partId,
|
|
1160
|
+
type: coding_agent_adapter_1.MessagePartType.REASONING,
|
|
1161
|
+
text,
|
|
1162
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT
|
|
1163
|
+
}];
|
|
1164
|
+
}
|
|
1165
|
+
if (role === 'user' || type.includes('user') || type === 'user_message') {
|
|
1166
|
+
const partId = `user-${itemId}`;
|
|
1167
|
+
if (seenPartIds.has(partId))
|
|
1168
|
+
return [];
|
|
1169
|
+
seenPartIds.add(partId);
|
|
1170
|
+
partContentLengths.set(partId, String(text.length));
|
|
1171
|
+
return [{
|
|
1172
|
+
id: partId,
|
|
1173
|
+
type: coding_agent_adapter_1.MessagePartType.TEXT,
|
|
1174
|
+
text,
|
|
1175
|
+
role: coding_agent_adapter_1.MessageRole.USER
|
|
1176
|
+
}];
|
|
1177
|
+
}
|
|
1178
|
+
if (role === 'assistant' || type.includes('agent') || type.includes('assistant') || (!type && text)) {
|
|
1179
|
+
const partId = `agent-${itemId}`;
|
|
1180
|
+
if (seenPartIds.has(partId) && method !== 'item/completed')
|
|
1181
|
+
return [];
|
|
1182
|
+
const finalText = text || session.streamedTextByItemId.get(itemId) || '';
|
|
1183
|
+
const alreadySeen = seenPartIds.has(partId);
|
|
1184
|
+
if (!alreadySeen && method === 'item/completed' && this.hasSeenAssistantTextForTurn(session, params, item, finalText)) {
|
|
1185
|
+
return [];
|
|
1186
|
+
}
|
|
1187
|
+
seenPartIds.add(partId);
|
|
1188
|
+
if (method === 'item/completed') {
|
|
1189
|
+
this.markAssistantTextForTurn(session, params, item, finalText);
|
|
1190
|
+
}
|
|
1191
|
+
partContentLengths.set(partId, String(finalText.length));
|
|
1192
|
+
return [{
|
|
1193
|
+
id: partId,
|
|
1194
|
+
type: coding_agent_adapter_1.MessagePartType.TEXT,
|
|
1195
|
+
text: finalText,
|
|
1196
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT,
|
|
1197
|
+
update: method === 'item/completed' && session.streamedTextByItemId.has(itemId)
|
|
1198
|
+
}];
|
|
1199
|
+
}
|
|
1200
|
+
const toolName = extractToolName(item);
|
|
1201
|
+
const toolTitle = extractToolTitle(item, toolName);
|
|
1202
|
+
const partId = `tool-${itemId}`;
|
|
1203
|
+
const isCompleted = method === 'item/completed';
|
|
1204
|
+
if (!isCompleted && seenPartIds.has(partId))
|
|
1205
|
+
return [];
|
|
1206
|
+
seenPartIds.add(partId);
|
|
1207
|
+
const part = {
|
|
1208
|
+
id: partId,
|
|
1209
|
+
type: coding_agent_adapter_1.MessagePartType.TOOL,
|
|
1210
|
+
role: coding_agent_adapter_1.MessageRole.ASSISTANT,
|
|
1211
|
+
tool: {
|
|
1212
|
+
name: toolName,
|
|
1213
|
+
status: isCompleted ? 'completed' : 'running',
|
|
1214
|
+
title: toolTitle,
|
|
1215
|
+
input: stringifyForIpc(item, MAX_IPC_TOOL_INPUT_CHARS),
|
|
1216
|
+
output: isCompleted ? stringifyForIpc(extractText(item) || item, MAX_IPC_TOOL_OUTPUT_CHARS) : undefined
|
|
1217
|
+
},
|
|
1218
|
+
update: isCompleted
|
|
1219
|
+
};
|
|
1220
|
+
if (isCompleted) {
|
|
1221
|
+
session.runningTools.delete(partId);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
session.runningTools.set(partId, {
|
|
1225
|
+
partId,
|
|
1226
|
+
toolName,
|
|
1227
|
+
startTime: typeof params.startedAtMs === 'number' ? params.startedAtMs : Date.now(),
|
|
1228
|
+
input: item
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
partContentLengths.set(partId, `${part.tool?.status}:${toolName}`);
|
|
1232
|
+
return [part];
|
|
1233
|
+
}
|
|
1234
|
+
hasSeenAssistantTextForTurn(session, params, item, text) {
|
|
1235
|
+
const key = this.buildAssistantTextKey(text);
|
|
1236
|
+
if (!key)
|
|
1237
|
+
return false;
|
|
1238
|
+
for (const scopeKey of this.extractAssistantTextScopeKeys(params, item, key)) {
|
|
1239
|
+
if (session.assistantTextKeysByTurn.get(scopeKey)?.has(key))
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
markAssistantTextForTurn(session, params, item, text) {
|
|
1245
|
+
const key = this.buildAssistantTextKey(text);
|
|
1246
|
+
if (!key)
|
|
1247
|
+
return;
|
|
1248
|
+
for (const scopeKey of this.extractAssistantTextScopeKeys(params, item, key)) {
|
|
1249
|
+
const seenForScope = session.assistantTextKeysByTurn.get(scopeKey) ?? new Set();
|
|
1250
|
+
seenForScope.add(key);
|
|
1251
|
+
session.assistantTextKeysByTurn.set(scopeKey, seenForScope);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
extractTurnKey(params, item) {
|
|
1255
|
+
const metadata = isObject(item.internal_chat_message_metadata_passthrough)
|
|
1256
|
+
? item.internal_chat_message_metadata_passthrough
|
|
1257
|
+
: {};
|
|
1258
|
+
return (asString(params.turnId) ||
|
|
1259
|
+
asString(params.turn_id) ||
|
|
1260
|
+
asString(item.turnId) ||
|
|
1261
|
+
asString(item.turn_id) ||
|
|
1262
|
+
asString(metadata.turn_id) ||
|
|
1263
|
+
asString(metadata.turnId) ||
|
|
1264
|
+
asString(params.threadId) ||
|
|
1265
|
+
'unknown-turn');
|
|
1266
|
+
}
|
|
1267
|
+
extractAssistantTextScopeKeys(params, item, textKey) {
|
|
1268
|
+
const keys = new Set([this.extractTurnKey(params, item)]);
|
|
1269
|
+
const threadKey = asString(params.threadId) || asString(item.threadId) || asString(item.thread_id);
|
|
1270
|
+
if (threadKey && textKey.length >= MIN_THREAD_LEVEL_ASSISTANT_DEDUPE_CHARS) {
|
|
1271
|
+
keys.add(`thread:${threadKey}`);
|
|
1272
|
+
}
|
|
1273
|
+
return Array.from(keys);
|
|
1274
|
+
}
|
|
1275
|
+
buildAssistantTextKey(text) {
|
|
1276
|
+
return text.trim().replace(/\s+/g, ' ');
|
|
1277
|
+
}
|
|
1278
|
+
sendRpcRequest(session, method, params) {
|
|
1279
|
+
return new Promise((resolve, reject) => {
|
|
1280
|
+
const id = session.nextRequestId++;
|
|
1281
|
+
session.pendingRequests.set(id, { resolve, reject });
|
|
1282
|
+
this.writeJson(session, { jsonrpc: '2.0', id, method, params });
|
|
1283
|
+
setTimeout(() => {
|
|
1284
|
+
if (!session.pendingRequests.has(id))
|
|
1285
|
+
return;
|
|
1286
|
+
session.pendingRequests.delete(id);
|
|
1287
|
+
reject(new Error(`Codex app-server RPC timed out: ${method}`));
|
|
1288
|
+
}, 30000);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
sendRpcResponse(session, id, result) {
|
|
1292
|
+
this.writeJson(session, { jsonrpc: '2.0', id, result });
|
|
1293
|
+
}
|
|
1294
|
+
sendRpcNotification(session, method, params) {
|
|
1295
|
+
this.writeJson(session, { jsonrpc: '2.0', method, params });
|
|
1296
|
+
}
|
|
1297
|
+
writeJson(session, payload) {
|
|
1298
|
+
// A dead app server must not take the application with it.
|
|
1299
|
+
(0, child_stream_guards_1.writeToChildStdin)(session.process, `${JSON.stringify(payload)}\n`, 'CodexAppServerAdapter');
|
|
1300
|
+
}
|
|
1301
|
+
requireSession(sessionId) {
|
|
1302
|
+
const session = this.sessions.get(sessionId);
|
|
1303
|
+
if (!session) {
|
|
1304
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1305
|
+
}
|
|
1306
|
+
return session;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
exports.CodexAppServerAdapter = CodexAppServerAdapter;
|