@makaio/adapter-codex-app-server 1.0.0-dev-1779051654000
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 +311 -0
- package/descriptor.json +27 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/index.d.mts +5565 -0
- package/dist/index.mjs +3787 -0
- package/package.json +68 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3787 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
|
|
2
|
+
import { AIAdapter, AIAgent, AIAgentConnector, BaseConnectorTurn, UserMessageQueue, createAdapterNamespace, extractMcpCallTarget, formatContextBlocksAsText, formatMessageHistoryAsTranscript, isMcpCallTool, resolveConformanceTestPreset, resolveDisabledNativeTools, resolveTestConfig, serializeBlockToText, serializeTurnContext } from "@makaio/framework/adapters";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AgentSubjects, ClientSubjects, ToolSubjects } from "@makaio/framework/contracts";
|
|
5
|
+
import { MakaioBus } from "@makaio/framework/bus";
|
|
6
|
+
import { buildClientSessionBase, emitBestEffort } from "@makaio/framework/clients";
|
|
7
|
+
import { filterToolsWithSchema, loadToolsFromRegistry } from "@makaio/framework/adapters/stream-session";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { createAdapterConfigFactory, resolveSessionEnvironment } from "@makaio/framework/adapters/config";
|
|
11
|
+
|
|
12
|
+
//#region src/namespaces/schemas/thread-lifecycle.ts
|
|
13
|
+
/**
|
|
14
|
+
* Schema for thread.started event
|
|
15
|
+
* Emitted when a new thread is successfully started.
|
|
16
|
+
*
|
|
17
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
18
|
+
* The filteredBus.on() filters events by agentId.
|
|
19
|
+
*/
|
|
20
|
+
const ThreadStartedSchema = z.object({
|
|
21
|
+
agentId: z.string(),
|
|
22
|
+
threadId: z.string(),
|
|
23
|
+
timestamp: z.number()
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Schema for thread.completed event
|
|
27
|
+
* Emitted when a thread is archived or completed.
|
|
28
|
+
*
|
|
29
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
30
|
+
*/
|
|
31
|
+
const ThreadCompletedSchema = z.object({
|
|
32
|
+
agentId: z.string(),
|
|
33
|
+
threadId: z.string(),
|
|
34
|
+
timestamp: z.number()
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/namespaces/schemas/turn-lifecycle.ts
|
|
39
|
+
/**
|
|
40
|
+
* Turn state for Codex App-Server connector.
|
|
41
|
+
*
|
|
42
|
+
* State machine follows the app-server protocol:
|
|
43
|
+
* - idle → active (turn/start request sent)
|
|
44
|
+
* - active → processing_started (turn/started notification received)
|
|
45
|
+
* - processing_started → turn_started (first item/started notification)
|
|
46
|
+
* - turn_started → step_started (command/file item starts)
|
|
47
|
+
* - step_started → step_finished (item completes)
|
|
48
|
+
* - step_finished → turn_finished (turn/completed received)
|
|
49
|
+
* - turn_finished → idle (cleanup, ready for next turn)
|
|
50
|
+
*
|
|
51
|
+
* Immediate mode: active → interrupted → idle (cancelled, new turn with merged content)
|
|
52
|
+
*/
|
|
53
|
+
const CodexAppServerTurnStateSchema = z.enum([
|
|
54
|
+
"idle",
|
|
55
|
+
"active",
|
|
56
|
+
"processing_started",
|
|
57
|
+
"turn_started",
|
|
58
|
+
"step_started",
|
|
59
|
+
"step_finished",
|
|
60
|
+
"turn_finished",
|
|
61
|
+
"interrupted"
|
|
62
|
+
]);
|
|
63
|
+
/**
|
|
64
|
+
* Turn state change event
|
|
65
|
+
* Emitted whenever turn state transitions
|
|
66
|
+
*/
|
|
67
|
+
const TurnStateChangedSchema = z.object({
|
|
68
|
+
adapterId: z.string(),
|
|
69
|
+
agentId: z.string(),
|
|
70
|
+
oldState: CodexAppServerTurnStateSchema,
|
|
71
|
+
newState: CodexAppServerTurnStateSchema,
|
|
72
|
+
timestamp: z.number()
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Schema for turn.started event
|
|
76
|
+
* Emitted when a turn begins processing (turn_started state).
|
|
77
|
+
*
|
|
78
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
79
|
+
*/
|
|
80
|
+
const TurnStartedSchema = z.object({
|
|
81
|
+
agentId: z.string(),
|
|
82
|
+
threadId: z.string(),
|
|
83
|
+
turnId: z.string(),
|
|
84
|
+
timestamp: z.number()
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Schema for turn.completed event
|
|
88
|
+
* Emitted when a turn completes (turn_finished state).
|
|
89
|
+
*
|
|
90
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
91
|
+
*/
|
|
92
|
+
const TurnCompletedSchema = z.object({
|
|
93
|
+
agentId: z.string(),
|
|
94
|
+
threadId: z.string(),
|
|
95
|
+
turnId: z.string(),
|
|
96
|
+
timestamp: z.number()
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Schema for turn.step_started event
|
|
100
|
+
* Emitted when a step/item begins execution.
|
|
101
|
+
*
|
|
102
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
103
|
+
*/
|
|
104
|
+
const TurnStepStartedSchema = z.object({
|
|
105
|
+
agentId: z.string(),
|
|
106
|
+
threadId: z.string(),
|
|
107
|
+
turnId: z.string(),
|
|
108
|
+
itemId: z.string(),
|
|
109
|
+
timestamp: z.number()
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* Schema for turn.step_finished event
|
|
113
|
+
* Emitted when a step/item completes.
|
|
114
|
+
*
|
|
115
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
116
|
+
*/
|
|
117
|
+
const TurnStepFinishedSchema = z.object({
|
|
118
|
+
agentId: z.string(),
|
|
119
|
+
threadId: z.string(),
|
|
120
|
+
turnId: z.string(),
|
|
121
|
+
itemId: z.string(),
|
|
122
|
+
timestamp: z.number()
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/namespaces/schemas/item-lifecycle.ts
|
|
127
|
+
/**
|
|
128
|
+
* Schema for item.started event
|
|
129
|
+
* Emitted when an item begins execution
|
|
130
|
+
*/
|
|
131
|
+
const ItemStartedSchema = z.object({
|
|
132
|
+
threadId: z.string(),
|
|
133
|
+
turnId: z.string(),
|
|
134
|
+
itemId: z.string(),
|
|
135
|
+
itemType: z.string(),
|
|
136
|
+
timestamp: z.number()
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* Schema for item.completed event
|
|
140
|
+
* Emitted when an item completes
|
|
141
|
+
*/
|
|
142
|
+
const ItemCompletedSchema = z.object({
|
|
143
|
+
threadId: z.string(),
|
|
144
|
+
turnId: z.string(),
|
|
145
|
+
itemId: z.string(),
|
|
146
|
+
itemType: z.string(),
|
|
147
|
+
timestamp: z.number()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/namespaces/schemas/agent-message.ts
|
|
152
|
+
/**
|
|
153
|
+
* Schema for agent_message.delta event
|
|
154
|
+
* Emitted for incremental text updates from the agent
|
|
155
|
+
*/
|
|
156
|
+
const AgentMessageDeltaSchema = z.object({
|
|
157
|
+
threadId: z.string(),
|
|
158
|
+
turnId: z.string(),
|
|
159
|
+
delta: z.string(),
|
|
160
|
+
timestamp: z.number()
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Schema for agent_message event
|
|
164
|
+
* Emitted when a complete agent message is ready
|
|
165
|
+
*/
|
|
166
|
+
const AgentMessageSchema = z.object({
|
|
167
|
+
threadId: z.string(),
|
|
168
|
+
turnId: z.string(),
|
|
169
|
+
message: z.string(),
|
|
170
|
+
timestamp: z.number()
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/namespaces/schemas/shared.ts
|
|
175
|
+
/**
|
|
176
|
+
* Enrichment fields auto-injected by requestToolApproval.
|
|
177
|
+
* These are added by the connector's base class, not passed by the caller.
|
|
178
|
+
*
|
|
179
|
+
* Shared across command-execution and file-change approval schemas.
|
|
180
|
+
*/
|
|
181
|
+
const EnrichmentFieldsSchema = z.object({
|
|
182
|
+
agentId: z.string().optional(),
|
|
183
|
+
adapterId: z.string().optional(),
|
|
184
|
+
adapterName: z.string().optional(),
|
|
185
|
+
adapterSessionId: z.string().optional()
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* Shared response schema for approval RPCs (exec and file-change).
|
|
189
|
+
*
|
|
190
|
+
* Both command execution and file change approval return
|
|
191
|
+
* the same accept/decline decision format.
|
|
192
|
+
*/
|
|
193
|
+
const ApprovalResponseSchema = z.object({
|
|
194
|
+
decision: z.enum(["accept", "decline"]),
|
|
195
|
+
message: z.string().optional()
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/namespaces/schemas/command-execution.ts
|
|
200
|
+
/**
|
|
201
|
+
* Schema for exec_command.begin event
|
|
202
|
+
* Emitted when a command execution starts
|
|
203
|
+
*/
|
|
204
|
+
const ExecCommandBeginSchema = z.object({
|
|
205
|
+
threadId: z.string(),
|
|
206
|
+
turnId: z.string(),
|
|
207
|
+
callId: z.string(),
|
|
208
|
+
command: z.array(z.string()),
|
|
209
|
+
cwd: z.string(),
|
|
210
|
+
timestamp: z.number()
|
|
211
|
+
});
|
|
212
|
+
/**
|
|
213
|
+
* Schema for exec_approval_request RPC
|
|
214
|
+
* Request/response pair for command approval routing via scoped bus.
|
|
215
|
+
* Connector calls requestToolApproval → registerToolApprovalHandler routes to global bus → returns response.
|
|
216
|
+
*
|
|
217
|
+
* Note: Enrichment fields (agentId, adapterId, etc.) are auto-injected by requestToolApproval.
|
|
218
|
+
*/
|
|
219
|
+
const ExecApprovalRequestSchema = {
|
|
220
|
+
request: z.object({
|
|
221
|
+
threadId: z.string(),
|
|
222
|
+
turnId: z.string(),
|
|
223
|
+
callId: z.string(),
|
|
224
|
+
command: z.array(z.string()),
|
|
225
|
+
cwd: z.string(),
|
|
226
|
+
reason: z.string().nullable(),
|
|
227
|
+
timestamp: z.number()
|
|
228
|
+
}).merge(EnrichmentFieldsSchema),
|
|
229
|
+
response: ApprovalResponseSchema
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Schema for exec_command.output.delta event
|
|
233
|
+
* Emitted for incremental output from a running command
|
|
234
|
+
*/
|
|
235
|
+
const ExecCommandOutputDeltaSchema = z.object({
|
|
236
|
+
threadId: z.string(),
|
|
237
|
+
turnId: z.string(),
|
|
238
|
+
callId: z.string(),
|
|
239
|
+
stream: z.enum(["stdout", "stderr"]),
|
|
240
|
+
chunk: z.string(),
|
|
241
|
+
timestamp: z.number()
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* Schema for exec_command.end event
|
|
245
|
+
* Emitted when a command execution completes
|
|
246
|
+
*/
|
|
247
|
+
const ExecCommandEndSchema = z.object({
|
|
248
|
+
threadId: z.string(),
|
|
249
|
+
turnId: z.string(),
|
|
250
|
+
callId: z.string(),
|
|
251
|
+
exitCode: z.number(),
|
|
252
|
+
timestamp: z.number()
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/namespaces/schemas/file-change.ts
|
|
257
|
+
/**
|
|
258
|
+
* Schema for file_change_approval_request RPC
|
|
259
|
+
* Request/response pair for file change approval routing via scoped bus.
|
|
260
|
+
* Connector calls requestToolApproval → registerToolApprovalHandler routes to global bus → returns response.
|
|
261
|
+
*
|
|
262
|
+
* Note: Enrichment fields (agentId, adapterId, etc.) are auto-injected by requestToolApproval.
|
|
263
|
+
*/
|
|
264
|
+
const FileChangeApprovalRequestSchema = {
|
|
265
|
+
request: z.object({
|
|
266
|
+
threadId: z.string(),
|
|
267
|
+
turnId: z.string(),
|
|
268
|
+
itemId: z.string(),
|
|
269
|
+
reason: z.string().nullable(),
|
|
270
|
+
grantRoot: z.string().nullable(),
|
|
271
|
+
timestamp: z.number()
|
|
272
|
+
}).merge(EnrichmentFieldsSchema),
|
|
273
|
+
response: ApprovalResponseSchema
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Schema for file_change.output.delta event
|
|
277
|
+
* Emitted for incremental file change updates
|
|
278
|
+
*/
|
|
279
|
+
const FileChangeOutputDeltaSchema = z.object({
|
|
280
|
+
threadId: z.string(),
|
|
281
|
+
turnId: z.string(),
|
|
282
|
+
itemId: z.string(),
|
|
283
|
+
delta: z.string(),
|
|
284
|
+
timestamp: z.number()
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/namespaces/schemas/reasoning.ts
|
|
289
|
+
/**
|
|
290
|
+
* Schema for reasoning.delta event
|
|
291
|
+
* Emitted for incremental reasoning content
|
|
292
|
+
*/
|
|
293
|
+
const ReasoningDeltaSchema = z.object({
|
|
294
|
+
threadId: z.string(),
|
|
295
|
+
turnId: z.string(),
|
|
296
|
+
delta: z.string(),
|
|
297
|
+
timestamp: z.number()
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* Schema for reasoning event
|
|
301
|
+
* Emitted when complete reasoning is available
|
|
302
|
+
*/
|
|
303
|
+
const ReasoningSchema = z.object({
|
|
304
|
+
threadId: z.string(),
|
|
305
|
+
turnId: z.string(),
|
|
306
|
+
reasoning: z.string(),
|
|
307
|
+
timestamp: z.number()
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/namespaces/schemas/token-usage.ts
|
|
312
|
+
/**
|
|
313
|
+
* Schema for token_usage event
|
|
314
|
+
* Emitted when token usage is updated.
|
|
315
|
+
*
|
|
316
|
+
* Note: agentId is required for bus filtering to work correctly.
|
|
317
|
+
*/
|
|
318
|
+
const TokenUsageSchema = z.object({
|
|
319
|
+
agentId: z.string(),
|
|
320
|
+
threadId: z.string(),
|
|
321
|
+
turnId: z.string().optional(),
|
|
322
|
+
promptTokens: z.number(),
|
|
323
|
+
inputCachedTokens: z.number().default(0),
|
|
324
|
+
completionTokens: z.number(),
|
|
325
|
+
reasoningTokens: z.number().default(0),
|
|
326
|
+
totalTokens: z.number(),
|
|
327
|
+
modelContextWindow: z.number().optional(),
|
|
328
|
+
timestamp: z.number()
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/namespaces/schemas/dynamic-tool-call.ts
|
|
333
|
+
/**
|
|
334
|
+
* Schema for dynamic_tool_call_begin event.
|
|
335
|
+
*
|
|
336
|
+
* Emitted when an `item/started` notification arrives for a `dynamicToolCall` item.
|
|
337
|
+
* The `name` and `args` fields are populated from the cached `item/tool/call` server
|
|
338
|
+
* request that was handled before (or concurrent with) the `item/started` notification.
|
|
339
|
+
*/
|
|
340
|
+
const DynamicToolCallBeginSchema = z.object({
|
|
341
|
+
threadId: z.string(),
|
|
342
|
+
turnId: z.string(),
|
|
343
|
+
/** Item ID — same as `toolCallId` used in `item/tool/call` */
|
|
344
|
+
itemId: z.string(),
|
|
345
|
+
/** Tool name as declared in `dynamicTools` at `thread/start` */
|
|
346
|
+
name: z.string(),
|
|
347
|
+
/** Tool input arguments */
|
|
348
|
+
args: z.record(z.string(), z.unknown()),
|
|
349
|
+
timestamp: z.number()
|
|
350
|
+
});
|
|
351
|
+
/**
|
|
352
|
+
* Schema for dynamic_tool_call_end event.
|
|
353
|
+
*
|
|
354
|
+
* Emitted when an `item/completed` notification arrives for a `dynamicToolCall` item.
|
|
355
|
+
* The `output` and `success` fields are populated from the cached tool execution result.
|
|
356
|
+
*/
|
|
357
|
+
const DynamicToolCallEndSchema = z.object({
|
|
358
|
+
threadId: z.string(),
|
|
359
|
+
turnId: z.string(),
|
|
360
|
+
/** Item ID — same as `toolCallId` used in `item/tool/call` */
|
|
361
|
+
itemId: z.string(),
|
|
362
|
+
/** Tool name as declared in `dynamicTools` at `thread/start` */
|
|
363
|
+
name: z.string(),
|
|
364
|
+
/** Serialised tool output */
|
|
365
|
+
output: z.string(),
|
|
366
|
+
/** Whether execution succeeded (false if the result contained an `error` key) */
|
|
367
|
+
success: z.boolean(),
|
|
368
|
+
timestamp: z.number()
|
|
369
|
+
});
|
|
370
|
+
/**
|
|
371
|
+
* Schema for dynamic_tool_call_approval_request RPC.
|
|
372
|
+
*
|
|
373
|
+
* Request/response pair for dynamic tool call approval routing via scoped bus.
|
|
374
|
+
* Connector calls requestToolApproval → approval handler routes to global bus → returns response.
|
|
375
|
+
*
|
|
376
|
+
* Note: Enrichment fields (agentId, adapterId, etc.) are auto-injected by requestToolApproval.
|
|
377
|
+
*/
|
|
378
|
+
const DynamicToolCallApprovalRequestSchema = {
|
|
379
|
+
request: z.object({
|
|
380
|
+
threadId: z.string(),
|
|
381
|
+
turnId: z.string(),
|
|
382
|
+
/** Item ID — same as `toolCallId` used in `item/tool/call` */
|
|
383
|
+
itemId: z.string(),
|
|
384
|
+
/** Tool name as declared in `dynamicTools` at `thread/start` */
|
|
385
|
+
name: z.string(),
|
|
386
|
+
/** Tool input arguments */
|
|
387
|
+
args: z.record(z.string(), z.unknown()),
|
|
388
|
+
timestamp: z.number()
|
|
389
|
+
}).merge(EnrichmentFieldsSchema),
|
|
390
|
+
response: ApprovalResponseSchema
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/namespaces/index.ts
|
|
395
|
+
/**
|
|
396
|
+
* Codex App-Server Adapter Namespace
|
|
397
|
+
*
|
|
398
|
+
* Defines internal bus events for the codex app-server adapter.
|
|
399
|
+
* These events are emitted by the adapter and consumed by the core system.
|
|
400
|
+
*/
|
|
401
|
+
const namespace = "adapter:codex-app-server";
|
|
402
|
+
/**
|
|
403
|
+
* Codex App-Server Adapter schemas.
|
|
404
|
+
* Extracted as const to enable FilterPayload type computation via typeof.
|
|
405
|
+
*/
|
|
406
|
+
const codexAppServerSchemas = {
|
|
407
|
+
thread_started: ThreadStartedSchema,
|
|
408
|
+
thread_completed: ThreadCompletedSchema,
|
|
409
|
+
turn_state_changed: TurnStateChangedSchema,
|
|
410
|
+
turn_started: TurnStartedSchema,
|
|
411
|
+
turn_completed: TurnCompletedSchema,
|
|
412
|
+
turn_step_started: TurnStepStartedSchema,
|
|
413
|
+
turn_step_finished: TurnStepFinishedSchema,
|
|
414
|
+
item_started: ItemStartedSchema,
|
|
415
|
+
item_completed: ItemCompletedSchema,
|
|
416
|
+
agent_message_delta: AgentMessageDeltaSchema,
|
|
417
|
+
agent_message: AgentMessageSchema,
|
|
418
|
+
exec_command_begin: ExecCommandBeginSchema,
|
|
419
|
+
exec_approval_request: ExecApprovalRequestSchema,
|
|
420
|
+
exec_command_output_delta: ExecCommandOutputDeltaSchema,
|
|
421
|
+
exec_command_end: ExecCommandEndSchema,
|
|
422
|
+
file_change_approval_request: FileChangeApprovalRequestSchema,
|
|
423
|
+
file_change_output_delta: FileChangeOutputDeltaSchema,
|
|
424
|
+
reasoning_delta: ReasoningDeltaSchema,
|
|
425
|
+
reasoning: ReasoningSchema,
|
|
426
|
+
token_usage: TokenUsageSchema,
|
|
427
|
+
dynamic_tool_call_begin: DynamicToolCallBeginSchema,
|
|
428
|
+
dynamic_tool_call_end: DynamicToolCallEndSchema,
|
|
429
|
+
dynamic_tool_call_approval_request: DynamicToolCallApprovalRequestSchema
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Codex App-Server Adapter Namespace
|
|
433
|
+
*
|
|
434
|
+
* Defines the event subjects and schemas for internal bus communication
|
|
435
|
+
* between the codex app-server adapter and the core system.
|
|
436
|
+
*/
|
|
437
|
+
const CodexAppServerNamespace = createAdapterNamespace(namespace, codexAppServerSchemas);
|
|
438
|
+
/**
|
|
439
|
+
* Typed subject literals for Codex App-Server adapter.
|
|
440
|
+
* Use these constants to subscribe to specific message types with full type safety.
|
|
441
|
+
*/
|
|
442
|
+
const CodexAppServerSubjects = CodexAppServerNamespace.subjects;
|
|
443
|
+
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/tool-handling.ts
|
|
446
|
+
const TOOL_APPROVAL_DEBUG_ENABLED = process.env.MAKAIO_DEBUG_TOOL_APPROVAL === "1" || process.env.DEBUG?.includes("codex-tool-approval") === true;
|
|
447
|
+
/**
|
|
448
|
+
* Debug logger for tool-approval bridge flow.
|
|
449
|
+
* @param message - Message to log
|
|
450
|
+
* @param context - Optional structured context
|
|
451
|
+
*/
|
|
452
|
+
function logToolApprovalDebug(message, context) {
|
|
453
|
+
if (!TOOL_APPROVAL_DEBUG_ENABLED) return;
|
|
454
|
+
console.debug("[registerToolApprovalHandler]", message, context ?? {});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Build the shared approval envelope fields from adapter context.
|
|
458
|
+
* @param context - Adapter context
|
|
459
|
+
* @returns Shared approval envelope fields
|
|
460
|
+
*/
|
|
461
|
+
function createApprovalEnvelope(context) {
|
|
462
|
+
return {
|
|
463
|
+
adapterId: context.adapterId,
|
|
464
|
+
adapterName: context.adapterName,
|
|
465
|
+
agentId: context.agentId,
|
|
466
|
+
adapterSessionId: context.adapterSessionId,
|
|
467
|
+
sessionId: context.sessionId
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Transform command execution approval request → AgentToolApproveRequest.
|
|
472
|
+
*
|
|
473
|
+
* Converts the app-server's command execution approval params into the global
|
|
474
|
+
* tool approval request format. The params are included as args for context.
|
|
475
|
+
* @param params - Raw app-server approval request parameters
|
|
476
|
+
* @param context - Adapter context for request enrichment
|
|
477
|
+
* @returns Global tool approval request
|
|
478
|
+
*/
|
|
479
|
+
function toGlobalToolApproval(params, context) {
|
|
480
|
+
return {
|
|
481
|
+
...createApprovalEnvelope(context),
|
|
482
|
+
toolCallId: params.itemId,
|
|
483
|
+
toolName: "bash",
|
|
484
|
+
reasoning: params.reason ?? void 0,
|
|
485
|
+
args: {
|
|
486
|
+
threadId: params.threadId,
|
|
487
|
+
turnId: params.turnId,
|
|
488
|
+
itemId: params.itemId,
|
|
489
|
+
reason: params.reason,
|
|
490
|
+
proposedExecpolicyAmendment: params.proposedExecpolicyAmendment
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Transform file change approval request → AgentToolApproveRequest.
|
|
496
|
+
*
|
|
497
|
+
* Converts the app-server's file change approval params into the global
|
|
498
|
+
* tool approval request format. The params are included as args for context.
|
|
499
|
+
* @param params - Raw app-server approval request parameters
|
|
500
|
+
* @param context - Adapter context for request enrichment
|
|
501
|
+
* @returns Global tool approval request
|
|
502
|
+
*/
|
|
503
|
+
function toGlobalFileApproval(params, context) {
|
|
504
|
+
return {
|
|
505
|
+
...createApprovalEnvelope(context),
|
|
506
|
+
toolCallId: params.itemId,
|
|
507
|
+
toolName: "patch",
|
|
508
|
+
reasoning: params.reason ?? void 0,
|
|
509
|
+
args: {
|
|
510
|
+
threadId: params.threadId,
|
|
511
|
+
turnId: params.turnId,
|
|
512
|
+
itemId: params.itemId,
|
|
513
|
+
reason: params.reason,
|
|
514
|
+
grantRoot: params.grantRoot
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Transform AgentToolApproveResponse → app-server decision format.
|
|
520
|
+
*
|
|
521
|
+
* Converts the global approval response into the simple accept/decline format
|
|
522
|
+
* expected by the codex app-server protocol.
|
|
523
|
+
* @param response - Global tool approval response
|
|
524
|
+
* @returns App-server approval decision with optional message
|
|
525
|
+
*/
|
|
526
|
+
function fromGlobalToolApproval(response) {
|
|
527
|
+
if (response.action === "allow") return { decision: "accept" };
|
|
528
|
+
return {
|
|
529
|
+
decision: "decline",
|
|
530
|
+
message: response.message || void 0
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Register tool approval handler that bridges connector's approval requests
|
|
535
|
+
* to the global AgentSubjects.toolApprove bus.
|
|
536
|
+
*
|
|
537
|
+
* Wires:
|
|
538
|
+
* - CodexAppServerSubjects.exec_approval_request → AgentSubjects.toolApprove
|
|
539
|
+
* - CodexAppServerSubjects.file_change_approval_request → AgentSubjects.toolApprove
|
|
540
|
+
*
|
|
541
|
+
* Used by both createTestConfig (test harness) and agent.ts (production).
|
|
542
|
+
* @param connector - Connector for Codex App-Server adapter (needs `on` and `getTimeoutMs` methods)
|
|
543
|
+
* @param context - Adapter context (can be lazy callback)
|
|
544
|
+
* @returns Unsubscribe function that removes both handlers
|
|
545
|
+
*/
|
|
546
|
+
function registerToolApprovalHandler(connector, context) {
|
|
547
|
+
const timeout = connector.getTimeoutMs("toolApproval");
|
|
548
|
+
/**
|
|
549
|
+
* Resolve adapter context lazily (callback avoids race condition with adapterSessionId)
|
|
550
|
+
* @returns Resolved ToolApprovalContext
|
|
551
|
+
*/
|
|
552
|
+
const resolveContext = async () => {
|
|
553
|
+
const providedContext = typeof context === "function" ? await context() : context;
|
|
554
|
+
if (providedContext == null || typeof providedContext !== "object" || typeof providedContext.sessionId !== "string") throw new Error("Tool approval requires a sessionId — ensure the agent was created within a session");
|
|
555
|
+
const sessionId = providedContext.sessionId.trim();
|
|
556
|
+
if (sessionId === "") throw new Error("Tool approval requires a sessionId — ensure the agent was created within a session");
|
|
557
|
+
return {
|
|
558
|
+
adapterId: providedContext?.adapterId ?? connector.adapterId,
|
|
559
|
+
adapterName: providedContext?.adapterName ?? connector.getAdapterName(),
|
|
560
|
+
agentId: providedContext?.agentId ?? connector.getAgentId(),
|
|
561
|
+
adapterSessionId: providedContext?.adapterSessionId ?? await connector.getAdapterSessionId(),
|
|
562
|
+
sessionId
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Bridge a connector approval request to the global approval bus.
|
|
567
|
+
* @param ctx - Connector request context
|
|
568
|
+
* @param transform - Payload-to-global-request transformer
|
|
569
|
+
* @param logTag - Optional debug label
|
|
570
|
+
*/
|
|
571
|
+
const handleApprovalRequest = async (ctx, transform, logTag) => {
|
|
572
|
+
if (logTag) logToolApprovalDebug(`${logTag} invoked`, { timestamp: Date.now() });
|
|
573
|
+
let globalResponse;
|
|
574
|
+
try {
|
|
575
|
+
const resolvedContext = await resolveContext();
|
|
576
|
+
if (logTag) logToolApprovalDebug("resolved context", {
|
|
577
|
+
agentId: resolvedContext.agentId,
|
|
578
|
+
timestamp: Date.now()
|
|
579
|
+
});
|
|
580
|
+
const request = transform(ctx.payload, resolvedContext);
|
|
581
|
+
if (logTag) logToolApprovalDebug("requesting global approval", {
|
|
582
|
+
agentId: request.agentId,
|
|
583
|
+
timestamp: Date.now()
|
|
584
|
+
});
|
|
585
|
+
globalResponse = await MakaioBus.request(AgentSubjects.toolApprove, request, { timeout });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error("[registerToolApprovalHandler] Tool approval request failed:", error);
|
|
588
|
+
const errorDetails = error instanceof Error ? `: ${error.message}` : "";
|
|
589
|
+
ctx.setResult({
|
|
590
|
+
decision: "decline",
|
|
591
|
+
message: `Tool approval request failed${errorDetails}`
|
|
592
|
+
});
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (logTag) logToolApprovalDebug("received global approval response", {
|
|
596
|
+
action: globalResponse.action,
|
|
597
|
+
timestamp: Date.now()
|
|
598
|
+
});
|
|
599
|
+
if (globalResponse.action === "deny" && globalResponse.shouldAbort) throw new Error(`Tool use denied by approval handler: ${globalResponse.message ?? "access denied"}`);
|
|
600
|
+
ctx.setResult(fromGlobalToolApproval(globalResponse));
|
|
601
|
+
};
|
|
602
|
+
const unsubExec = connector.on(CodexAppServerSubjects.exec_approval_request, async (ctx) => {
|
|
603
|
+
await handleApprovalRequest(ctx, toGlobalToolApprovalFromInternal, "exec_approval_request");
|
|
604
|
+
});
|
|
605
|
+
const unsubFile = connector.on(CodexAppServerSubjects.file_change_approval_request, async (ctx) => {
|
|
606
|
+
await handleApprovalRequest(ctx, toGlobalFileApprovalFromInternal, "file_change_approval_request");
|
|
607
|
+
});
|
|
608
|
+
return () => {
|
|
609
|
+
unsubExec();
|
|
610
|
+
unsubFile();
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Transform internal scoped bus exec approval payload → AgentToolApproveRequest.
|
|
615
|
+
*
|
|
616
|
+
* Similar to toGlobalToolApproval but works with the internal scoped bus payload
|
|
617
|
+
* which has a slightly different shape (callId vs itemId).
|
|
618
|
+
* @param payload - Internal scoped bus payload with threadId, turnId, callId, command, cwd
|
|
619
|
+
* @param context - Adapter context for request enrichment
|
|
620
|
+
* @returns Global tool approval request
|
|
621
|
+
*/
|
|
622
|
+
function toGlobalToolApprovalFromInternal(payload, context) {
|
|
623
|
+
return {
|
|
624
|
+
...createApprovalEnvelope(context),
|
|
625
|
+
toolCallId: payload.callId,
|
|
626
|
+
toolName: "bash",
|
|
627
|
+
reasoning: payload.reason ?? void 0,
|
|
628
|
+
args: {
|
|
629
|
+
threadId: payload.threadId,
|
|
630
|
+
turnId: payload.turnId,
|
|
631
|
+
callId: payload.callId,
|
|
632
|
+
command: payload.command,
|
|
633
|
+
cwd: payload.cwd,
|
|
634
|
+
reason: payload.reason
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Transform internal scoped bus file approval payload → AgentToolApproveRequest.
|
|
640
|
+
*
|
|
641
|
+
* Similar to toGlobalFileApproval but works with the internal scoped bus payload
|
|
642
|
+
* which has a slightly different shape (itemId vs callId).
|
|
643
|
+
* @param payload - Internal scoped bus payload with threadId, turnId, itemId, reason, grantRoot
|
|
644
|
+
* @param context - Adapter context for request enrichment
|
|
645
|
+
* @returns Global tool approval request
|
|
646
|
+
*/
|
|
647
|
+
function toGlobalFileApprovalFromInternal(payload, context) {
|
|
648
|
+
return {
|
|
649
|
+
...createApprovalEnvelope(context),
|
|
650
|
+
toolCallId: payload.itemId,
|
|
651
|
+
toolName: "patch",
|
|
652
|
+
reasoning: payload.reason ?? void 0,
|
|
653
|
+
args: {
|
|
654
|
+
threadId: payload.threadId,
|
|
655
|
+
turnId: payload.turnId,
|
|
656
|
+
itemId: payload.itemId,
|
|
657
|
+
reason: payload.reason,
|
|
658
|
+
grantRoot: payload.grantRoot
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/agent.ts
|
|
665
|
+
/**
|
|
666
|
+
* Codex App-Server Agent - Event routing layer.
|
|
667
|
+
*
|
|
668
|
+
* Wires connector events to global agent.* subjects.
|
|
669
|
+
* Auto-enriches payloads with AgentContext via emitGlobal().
|
|
670
|
+
*
|
|
671
|
+
* Responsibilities:
|
|
672
|
+
* 1. Wire connector's scoped bus events to global agent.* subjects
|
|
673
|
+
* 2. Auto-enrich payloads with AgentContext
|
|
674
|
+
* 3. Track usage metrics
|
|
675
|
+
* 4. Route tool approval to global bus
|
|
676
|
+
* 5. Emit normalized client.session.* observed-semantics events
|
|
677
|
+
* 6. Emit client.runtime.observe (best-effort) when a thread starts with a confirmed adapter session ID
|
|
678
|
+
*
|
|
679
|
+
* Event Flow:
|
|
680
|
+
* - CodexAppServerConnector emits to adapter:codex-app-server:* subjects
|
|
681
|
+
* - CodexAppServerAgent routes to semantic subjects via wireConnectorEvents()
|
|
682
|
+
* - CodexAppServerAgent subscribes to semantic subjects and emits to global bus (agent.*)
|
|
683
|
+
* - Downstream consumers subscribe to normalized agent.* subjects
|
|
684
|
+
* @packageDocumentation
|
|
685
|
+
*/
|
|
686
|
+
/**
|
|
687
|
+
* Maximum number of characters retained in `toolOutputCache` per tool call.
|
|
688
|
+
*
|
|
689
|
+
* Streaming consumers already receive every chunk via
|
|
690
|
+
* `AgentSubjects.tool.output`, so the cache is only needed to populate the
|
|
691
|
+
* final `tool_output` content block in `emitToolStepFinished`. Capping it
|
|
692
|
+
* prevents unbounded memory growth for long-running commands (builds, test
|
|
693
|
+
* suites, etc.) whose output can otherwise reach tens of megabytes.
|
|
694
|
+
*/
|
|
695
|
+
const MAX_TOOL_OUTPUT_CACHE_CHARS = 256 * 1024;
|
|
696
|
+
/**
|
|
697
|
+
* Keep the newest tool output in the bounded per-tool cache.
|
|
698
|
+
*
|
|
699
|
+
* Streaming consumers receive every chunk via `AgentSubjects.tool.output`; the
|
|
700
|
+
* cache is only the final content-block snapshot, so retaining the tail keeps
|
|
701
|
+
* the diagnostic context where command failures usually appear.
|
|
702
|
+
* @param output - Full accumulated output candidate
|
|
703
|
+
* @returns Output capped to the configured character count
|
|
704
|
+
*/
|
|
705
|
+
function capToolOutputCache(output) {
|
|
706
|
+
return output.length > MAX_TOOL_OUTPUT_CACHE_CHARS ? output.slice(-MAX_TOOL_OUTPUT_CACHE_CHARS) : output;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Codex App-Server Agent - Middle layer between AIAdapter and CodexAppServerConnector.
|
|
710
|
+
*
|
|
711
|
+
* Responsibilities:
|
|
712
|
+
* 1. Wire connector's scoped bus events to global agent.* subjects
|
|
713
|
+
* 2. Auto-enrich payloads with AgentContext via emitGlobal()
|
|
714
|
+
* 3. Emit normalized client.session.* observed-semantics events (best-effort)
|
|
715
|
+
* 4. Emit client.runtime.observe (best-effort) when a thread starts with a confirmed adapter session ID
|
|
716
|
+
*
|
|
717
|
+
* Event Flow:
|
|
718
|
+
* - CodexAppServerConnector emits to adapter:codex-app-server:* subjects
|
|
719
|
+
* - CodexAppServerAgent routes to semantic subjects via wireConnectorEvents()
|
|
720
|
+
* - CodexAppServerAgent subscribes to semantic subjects and emits to global bus (agent.*)
|
|
721
|
+
* - Downstream consumers subscribe to normalized agent.* subjects
|
|
722
|
+
*/
|
|
723
|
+
var CodexAppServerAgent = class extends AIAgent {
|
|
724
|
+
/** Cache of tool call arguments indexed by callId for content blocks */
|
|
725
|
+
toolCallCache = /* @__PURE__ */ new Map();
|
|
726
|
+
/** Accumulated tool output indexed by callId during streaming */
|
|
727
|
+
toolOutputCache = /* @__PURE__ */ new Map();
|
|
728
|
+
/** Track toolCallId to blockIndex for correlation between step.started and step.finished */
|
|
729
|
+
toolBlockIndexMap = /* @__PURE__ */ new Map();
|
|
730
|
+
/**
|
|
731
|
+
* Guard: client.session.* global bus observations are wired once per agent
|
|
732
|
+
* lifetime (not per connector swap). Connector-level wiring uses
|
|
733
|
+
* addConnectorWiringCleanup(); these stable global subscriptions use
|
|
734
|
+
* addBusHandlerCleanup() and must not accumulate across swaps.
|
|
735
|
+
*/
|
|
736
|
+
clientSessionObservationsWired = false;
|
|
737
|
+
wireEvents(connector) {
|
|
738
|
+
this.wireConnectorEvents(connector);
|
|
739
|
+
this.wireToolApprovalRpc(connector);
|
|
740
|
+
this.wireClientSessionTurnObservations();
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Wire connector's bus events to global agent.* subjects.
|
|
744
|
+
*
|
|
745
|
+
* Maps CodexAppServerSubjects.* to AgentSubjects.*
|
|
746
|
+
*
|
|
747
|
+
* Note: Turn lifecycle events (turn.started, turn.completed) are NOT wired here
|
|
748
|
+
* because the base AIAgent's message-lifecycle-tracker already handles emitting
|
|
749
|
+
* these global events when messages are acknowledged and turns complete.
|
|
750
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
751
|
+
*/
|
|
752
|
+
wireConnectorEvents(connector) {
|
|
753
|
+
this.wireThreadLifecycleEvents(connector);
|
|
754
|
+
this.wireMessageEvents(connector);
|
|
755
|
+
this.wireToolEvents(connector);
|
|
756
|
+
this.wireUsageTracking(connector);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Build the shared base payload for all `client.session.*` observed-semantics
|
|
760
|
+
* events by forwarding this agent's current identity fields to the shared
|
|
761
|
+
* {@link buildClientSessionBase} helper.
|
|
762
|
+
*
|
|
763
|
+
* Captures the adapter session ID best-effort: if the connector has not yet
|
|
764
|
+
* confirmed a session ID (e.g., very early in initialization), the field is
|
|
765
|
+
* omitted rather than blocking emission.
|
|
766
|
+
* @returns Base payload with clientId, source, observedAt, and optional session IDs
|
|
767
|
+
*/
|
|
768
|
+
getClientSessionBase() {
|
|
769
|
+
return buildClientSessionBase({
|
|
770
|
+
clientId: this.config.clientId ?? "codex",
|
|
771
|
+
sessionId: this.sessionId,
|
|
772
|
+
adapterSessionId: this.connector?.adapterSessionId
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Wire stable global bus subscriptions for client.session.turn.* events.
|
|
777
|
+
*
|
|
778
|
+
* Uses addBusHandlerCleanup() so these subscriptions survive connector swaps.
|
|
779
|
+
* Guarded by clientSessionObservationsWired so they are registered exactly once
|
|
780
|
+
* per agent lifetime regardless of how many connector swaps occur.
|
|
781
|
+
*
|
|
782
|
+
* Subscribes to:
|
|
783
|
+
* - AgentSubjects.turn.started → client.session.turn.started
|
|
784
|
+
* - AgentSubjects.turn.completed → client.session.turn.completed
|
|
785
|
+
* - AgentSubjects.user_message.sent → client.session.userPrompt.submitted
|
|
786
|
+
*/
|
|
787
|
+
wireClientSessionTurnObservations() {
|
|
788
|
+
if (this.clientSessionObservationsWired) return;
|
|
789
|
+
this.clientSessionObservationsWired = true;
|
|
790
|
+
const filteredBus = this.globalBus.withFilter({ agentId: this.agentId });
|
|
791
|
+
this.addBusHandlerCleanup(filteredBus.on(AgentSubjects.turn.started, () => {
|
|
792
|
+
emitBestEffort(async () => {
|
|
793
|
+
await this.globalBus.emit(ClientSubjects.session.turn.started, this.getClientSessionBase());
|
|
794
|
+
});
|
|
795
|
+
}));
|
|
796
|
+
this.addBusHandlerCleanup(filteredBus.on(AgentSubjects.turn.completed, () => {
|
|
797
|
+
emitBestEffort(async () => {
|
|
798
|
+
await this.globalBus.emit(ClientSubjects.session.turn.completed, this.getClientSessionBase());
|
|
799
|
+
});
|
|
800
|
+
}));
|
|
801
|
+
this.addBusHandlerCleanup(filteredBus.on(AgentSubjects.user_message.sent, (ctx) => {
|
|
802
|
+
const prompt = ctx.payload.content.message;
|
|
803
|
+
emitBestEffort(async () => {
|
|
804
|
+
await this.globalBus.emit(ClientSubjects.session.userPrompt.submitted, {
|
|
805
|
+
...this.getClientSessionBase(),
|
|
806
|
+
...prompt !== void 0 && { prompt }
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Wire thread lifecycle events.
|
|
813
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
814
|
+
*/
|
|
815
|
+
wireThreadLifecycleEvents(connector) {
|
|
816
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.thread_started, async () => {
|
|
817
|
+
this.toolCallCache.clear();
|
|
818
|
+
this.toolOutputCache.clear();
|
|
819
|
+
this.toolBlockIndexMap.clear();
|
|
820
|
+
await this.emitStart();
|
|
821
|
+
emitBestEffort(async () => {
|
|
822
|
+
await this.globalBus.emit(ClientSubjects.session.started, this.getClientSessionBase());
|
|
823
|
+
});
|
|
824
|
+
const adapterSessionId = this.connector?.adapterSessionId;
|
|
825
|
+
if (adapterSessionId) this.globalBus.requestOptional(ClientSubjects.runtime.observe, {
|
|
826
|
+
clientId: this.config.clientId ?? "codex",
|
|
827
|
+
source: {
|
|
828
|
+
layer: "adapter",
|
|
829
|
+
producer: "codex-app-server"
|
|
830
|
+
},
|
|
831
|
+
observedAt: Date.now(),
|
|
832
|
+
adapterSessionId,
|
|
833
|
+
sessionId: this.sessionId
|
|
834
|
+
}).catch(() => {});
|
|
835
|
+
});
|
|
836
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.thread_completed, async () => {
|
|
837
|
+
const messageId = crypto.randomUUID();
|
|
838
|
+
return this.emitCompletion({ messageId });
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Wire assistant message events.
|
|
843
|
+
*
|
|
844
|
+
* Note: Turn lifecycle events (agent.turn.started, agent.turn.completed) are emitted by
|
|
845
|
+
* MessageLifecycleTracker in ai-adapters-core based on message handle states,
|
|
846
|
+
* not by adapter-specific events.
|
|
847
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
848
|
+
*/
|
|
849
|
+
wireMessageEvents(connector) {
|
|
850
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.agent_message_delta, async (ctx) => {
|
|
851
|
+
await this.emitGlobal(AgentSubjects.message_delta, { text: ctx.payload.delta });
|
|
852
|
+
});
|
|
853
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.agent_message, async (ctx) => {
|
|
854
|
+
await this.emitGlobal(AgentSubjects.message, { content: ctx.payload.message });
|
|
855
|
+
await this.emitStepStarted("text");
|
|
856
|
+
await this.emitStepFinished("text", {
|
|
857
|
+
type: "text",
|
|
858
|
+
content: ctx.payload.message
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.reasoning_delta, async (ctx) => {
|
|
862
|
+
await this.emitGlobal(AgentSubjects.reasoning_delta, { content: ctx.payload.delta });
|
|
863
|
+
});
|
|
864
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.reasoning, async (ctx) => {
|
|
865
|
+
await this.emitGlobal(AgentSubjects.reasoning, { content: ctx.payload.reasoning });
|
|
866
|
+
await this.emitStepStarted("reasoning");
|
|
867
|
+
await this.emitStepFinished("reasoning", {
|
|
868
|
+
type: "reasoning",
|
|
869
|
+
content: ctx.payload.reasoning
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Wire tool-related events.
|
|
875
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
876
|
+
*/
|
|
877
|
+
wireToolEvents(connector) {
|
|
878
|
+
this.wireCommandExecutionEvents(connector);
|
|
879
|
+
this.wireFileChangeEvents(connector);
|
|
880
|
+
this.wireDynamicToolCallEvents(connector);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Wire command execution events (bash tool).
|
|
884
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
885
|
+
*/
|
|
886
|
+
wireCommandExecutionEvents(connector) {
|
|
887
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.exec_approval_request, async (ctx) => {
|
|
888
|
+
const payload = ctx.payload;
|
|
889
|
+
this.toolCallCache.set(payload.callId, {
|
|
890
|
+
command: payload.command,
|
|
891
|
+
cwd: payload.cwd,
|
|
892
|
+
toolCallId: payload.callId,
|
|
893
|
+
name: "bash",
|
|
894
|
+
args: {
|
|
895
|
+
command: payload.command,
|
|
896
|
+
cwd: payload.cwd
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
await this.emitGlobal(AgentSubjects.tool.use, {
|
|
900
|
+
toolName: "bash",
|
|
901
|
+
args: {
|
|
902
|
+
command: payload.command,
|
|
903
|
+
cwd: payload.cwd
|
|
904
|
+
},
|
|
905
|
+
toolCallId: payload.callId
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.exec_command_begin, async (ctx) => {
|
|
909
|
+
await this.handleExecCommandBegin(ctx.payload);
|
|
910
|
+
});
|
|
911
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.exec_command_output_delta, async (ctx) => {
|
|
912
|
+
const payload = ctx.payload;
|
|
913
|
+
const appended = (this.toolOutputCache.get(payload.callId) ?? "") + payload.chunk;
|
|
914
|
+
this.toolOutputCache.set(payload.callId, capToolOutputCache(appended));
|
|
915
|
+
await this.emitGlobal(AgentSubjects.tool.output, {
|
|
916
|
+
output: payload.chunk,
|
|
917
|
+
toolCallId: payload.callId
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.exec_command_end, async (ctx) => {
|
|
921
|
+
const payload = ctx.payload;
|
|
922
|
+
await this.emitGlobal(AgentSubjects.tool.completed, {
|
|
923
|
+
toolName: "bash",
|
|
924
|
+
args: this.toolCallCache.get(payload.callId)?.args,
|
|
925
|
+
result: { exitCode: payload.exitCode },
|
|
926
|
+
success: payload.exitCode === 0,
|
|
927
|
+
toolCallId: payload.callId
|
|
928
|
+
});
|
|
929
|
+
await this.emitToolStepFinished(payload.callId, this.toolOutputCache.get(payload.callId) ?? `Command completed with exit code ${payload.exitCode}`, payload.exitCode !== 0);
|
|
930
|
+
emitBestEffort(async () => {
|
|
931
|
+
await this.globalBus.emit(ClientSubjects.session.tool.post, {
|
|
932
|
+
...this.getClientSessionBase(),
|
|
933
|
+
toolName: "bash",
|
|
934
|
+
toolCallId: payload.callId,
|
|
935
|
+
success: payload.exitCode === 0
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Handle exec_command_begin: build step.started content, emit step.started,
|
|
942
|
+
* tool.started, and client.session.tool.pre.
|
|
943
|
+
* @param payload - The exec_command_begin event payload
|
|
944
|
+
*/
|
|
945
|
+
async handleExecCommandBegin(payload) {
|
|
946
|
+
const cachedTool = this.toolCallCache.get(payload.callId);
|
|
947
|
+
const toolCallContent = cachedTool ? {
|
|
948
|
+
type: "tool_call",
|
|
949
|
+
toolCallId: cachedTool.toolCallId,
|
|
950
|
+
name: cachedTool.name,
|
|
951
|
+
args: cachedTool.args
|
|
952
|
+
} : {
|
|
953
|
+
type: "tool_call",
|
|
954
|
+
toolCallId: payload.callId,
|
|
955
|
+
name: "bash",
|
|
956
|
+
args: {
|
|
957
|
+
command: payload.command,
|
|
958
|
+
cwd: payload.cwd
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const blockIndex = this.getBlockIndex();
|
|
962
|
+
this.toolBlockIndexMap.set(payload.callId, blockIndex);
|
|
963
|
+
await this.emitStepStarted("tool_use", {
|
|
964
|
+
type: "tool_use",
|
|
965
|
+
toolName: "bash",
|
|
966
|
+
toolCallId: payload.callId
|
|
967
|
+
}, toolCallContent);
|
|
968
|
+
this.incrementBlockIndex();
|
|
969
|
+
await this.emitGlobal(AgentSubjects.tool.started, {
|
|
970
|
+
toolName: "bash",
|
|
971
|
+
toolCallId: payload.callId
|
|
972
|
+
});
|
|
973
|
+
emitBestEffort(async () => {
|
|
974
|
+
await this.globalBus.emit(ClientSubjects.session.tool.pre, {
|
|
975
|
+
...this.getClientSessionBase(),
|
|
976
|
+
toolName: "bash",
|
|
977
|
+
toolCallId: payload.callId
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Wire file change events (patch tool).
|
|
983
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
984
|
+
*/
|
|
985
|
+
wireFileChangeEvents(connector) {
|
|
986
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.file_change_approval_request, async (ctx) => {
|
|
987
|
+
const { itemId, reason, grantRoot } = ctx.payload;
|
|
988
|
+
const args = {
|
|
989
|
+
reason,
|
|
990
|
+
grantRoot
|
|
991
|
+
};
|
|
992
|
+
this.toolCallCache.set(itemId, {
|
|
993
|
+
command: [],
|
|
994
|
+
cwd: "",
|
|
995
|
+
toolCallId: itemId,
|
|
996
|
+
name: "patch",
|
|
997
|
+
args
|
|
998
|
+
});
|
|
999
|
+
await this.emitGlobal(AgentSubjects.tool.use, {
|
|
1000
|
+
toolName: "patch",
|
|
1001
|
+
args,
|
|
1002
|
+
toolCallId: itemId
|
|
1003
|
+
});
|
|
1004
|
+
const blockIndex = this.getBlockIndex();
|
|
1005
|
+
this.toolBlockIndexMap.set(itemId, blockIndex);
|
|
1006
|
+
await this.emitStepStarted("tool_use", {
|
|
1007
|
+
type: "tool_use",
|
|
1008
|
+
toolName: "patch",
|
|
1009
|
+
toolCallId: itemId
|
|
1010
|
+
}, {
|
|
1011
|
+
type: "tool_call",
|
|
1012
|
+
toolCallId: itemId,
|
|
1013
|
+
name: "patch",
|
|
1014
|
+
args
|
|
1015
|
+
});
|
|
1016
|
+
this.incrementBlockIndex();
|
|
1017
|
+
emitBestEffort(async () => {
|
|
1018
|
+
await this.globalBus.emit(ClientSubjects.session.tool.pre, {
|
|
1019
|
+
...this.getClientSessionBase(),
|
|
1020
|
+
toolName: "patch",
|
|
1021
|
+
toolCallId: itemId
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.file_change_output_delta, async (ctx) => {
|
|
1026
|
+
const { itemId, delta } = ctx.payload;
|
|
1027
|
+
const appended = (this.toolOutputCache.get(itemId) ?? "") + delta;
|
|
1028
|
+
this.toolOutputCache.set(itemId, capToolOutputCache(appended));
|
|
1029
|
+
await this.emitGlobal(AgentSubjects.tool.output, {
|
|
1030
|
+
output: delta,
|
|
1031
|
+
toolCallId: itemId
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.item_completed, async (ctx) => {
|
|
1035
|
+
const cached = this.toolCallCache.get(ctx.payload.itemId);
|
|
1036
|
+
if (cached?.name === "patch") {
|
|
1037
|
+
const output = this.toolOutputCache.get(ctx.payload.itemId) ?? "File change applied";
|
|
1038
|
+
await this.emitGlobal(AgentSubjects.tool.completed, {
|
|
1039
|
+
toolName: "patch",
|
|
1040
|
+
args: cached.args,
|
|
1041
|
+
result: { output },
|
|
1042
|
+
success: true,
|
|
1043
|
+
toolCallId: ctx.payload.itemId
|
|
1044
|
+
});
|
|
1045
|
+
await this.emitToolStepFinished(ctx.payload.itemId, output, false);
|
|
1046
|
+
emitBestEffort(async () => {
|
|
1047
|
+
await this.globalBus.emit(ClientSubjects.session.tool.post, {
|
|
1048
|
+
...this.getClientSessionBase(),
|
|
1049
|
+
toolName: "patch",
|
|
1050
|
+
toolCallId: ctx.payload.itemId,
|
|
1051
|
+
success: true
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Wire dynamic tool call events (experimental API).
|
|
1059
|
+
*
|
|
1060
|
+
* Handles registry tools declared in `dynamicTools` at `thread/start`.
|
|
1061
|
+
* The execution happens in the `item/tool/call` server request handler; these
|
|
1062
|
+
* events carry the cached result into the agent's step lifecycle.
|
|
1063
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
1064
|
+
*/
|
|
1065
|
+
wireDynamicToolCallEvents(connector) {
|
|
1066
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.dynamic_tool_call_begin, async (ctx) => {
|
|
1067
|
+
const { itemId, name, args } = ctx.payload;
|
|
1068
|
+
this.toolCallCache.set(itemId, {
|
|
1069
|
+
command: [],
|
|
1070
|
+
cwd: "",
|
|
1071
|
+
toolCallId: itemId,
|
|
1072
|
+
name,
|
|
1073
|
+
args
|
|
1074
|
+
});
|
|
1075
|
+
const blockIndex = this.getBlockIndex();
|
|
1076
|
+
this.toolBlockIndexMap.set(itemId, blockIndex);
|
|
1077
|
+
await this.emitGlobal(AgentSubjects.tool.use, {
|
|
1078
|
+
toolName: name,
|
|
1079
|
+
args,
|
|
1080
|
+
toolCallId: itemId
|
|
1081
|
+
});
|
|
1082
|
+
await this.emitGlobal(AgentSubjects.tool.started, {
|
|
1083
|
+
toolName: name,
|
|
1084
|
+
toolCallId: itemId
|
|
1085
|
+
});
|
|
1086
|
+
await this.emitStepStarted("tool_use", {
|
|
1087
|
+
type: "tool_use",
|
|
1088
|
+
toolName: name,
|
|
1089
|
+
toolCallId: itemId
|
|
1090
|
+
}, {
|
|
1091
|
+
type: "tool_call",
|
|
1092
|
+
toolCallId: itemId,
|
|
1093
|
+
name,
|
|
1094
|
+
args
|
|
1095
|
+
});
|
|
1096
|
+
this.incrementBlockIndex();
|
|
1097
|
+
emitBestEffort(async () => {
|
|
1098
|
+
await this.globalBus.emit(ClientSubjects.session.tool.pre, {
|
|
1099
|
+
...this.getClientSessionBase(),
|
|
1100
|
+
toolName: name,
|
|
1101
|
+
toolCallId: itemId
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.dynamic_tool_call_end, async (ctx) => {
|
|
1106
|
+
const { itemId, name, output, success } = ctx.payload;
|
|
1107
|
+
const cached = this.toolCallCache.get(itemId);
|
|
1108
|
+
await this.emitGlobal(AgentSubjects.tool.output, {
|
|
1109
|
+
output,
|
|
1110
|
+
toolCallId: itemId
|
|
1111
|
+
});
|
|
1112
|
+
await this.emitGlobal(AgentSubjects.tool.completed, {
|
|
1113
|
+
toolName: name,
|
|
1114
|
+
args: cached?.args,
|
|
1115
|
+
result: { output },
|
|
1116
|
+
success,
|
|
1117
|
+
toolCallId: itemId
|
|
1118
|
+
});
|
|
1119
|
+
await this.emitToolStepFinished(itemId, output, !success);
|
|
1120
|
+
emitBestEffort(async () => {
|
|
1121
|
+
await this.globalBus.emit(ClientSubjects.session.tool.post, {
|
|
1122
|
+
...this.getClientSessionBase(),
|
|
1123
|
+
toolName: name,
|
|
1124
|
+
toolCallId: itemId,
|
|
1125
|
+
success
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Wire usage tracking events.
|
|
1132
|
+
* The base AIAgent.trackUsage() handles dual emission to both
|
|
1133
|
+
* AgentSubjects.usage and adapter session usage subjects.
|
|
1134
|
+
* @param connector - The CodexAppServerConnector to wire events from
|
|
1135
|
+
*/
|
|
1136
|
+
wireUsageTracking(connector) {
|
|
1137
|
+
this.subscribeConnector(connector, CodexAppServerSubjects.token_usage, async (ctx) => {
|
|
1138
|
+
const payload = ctx.payload;
|
|
1139
|
+
const normalized = {
|
|
1140
|
+
provider: "openai",
|
|
1141
|
+
inputTokens: payload.promptTokens,
|
|
1142
|
+
inputCachedTokens: payload.inputCachedTokens,
|
|
1143
|
+
outputTokens: payload.completionTokens,
|
|
1144
|
+
reasoningTokens: payload.reasoningTokens,
|
|
1145
|
+
totalTokens: payload.totalTokens,
|
|
1146
|
+
costUnits: payload.totalTokens,
|
|
1147
|
+
costUnitType: "tokens",
|
|
1148
|
+
contextWindow: payload.modelContextWindow
|
|
1149
|
+
};
|
|
1150
|
+
await this.trackUsage(normalized);
|
|
1151
|
+
if (payload.modelContextWindow) await this.emitContextWindowUpdate({
|
|
1152
|
+
currentTokens: payload.totalTokens,
|
|
1153
|
+
maxTokens: payload.modelContextWindow,
|
|
1154
|
+
cachedTokens: payload.inputCachedTokens
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Wire tool approval RPC from connector's scoped bus to global AgentSubjects.toolApprove.
|
|
1160
|
+
*
|
|
1161
|
+
* Uses centralized tool-handling helper for consistent approval flow.
|
|
1162
|
+
* Note: adapterSessionId may be undefined at wire time — the lazy callback resolves it
|
|
1163
|
+
* at request time via connector.getAdapterSessionId() to avoid the race condition.
|
|
1164
|
+
* sessionId is always set for agents running within a session; asserted here as the
|
|
1165
|
+
* Zod schema on AgentSubjects.toolApprove enforces it at runtime.
|
|
1166
|
+
* @param connector - The CodexAppServerConnector to wire RPC from
|
|
1167
|
+
*/
|
|
1168
|
+
wireToolApprovalRpc(connector) {
|
|
1169
|
+
this.addConnectorWiringCleanup(registerToolApprovalHandler(connector, async () => {
|
|
1170
|
+
if (this.sessionId == null) throw new Error("Agent sessionId is required for tool approval");
|
|
1171
|
+
return {
|
|
1172
|
+
adapterId: this.adapterId,
|
|
1173
|
+
adapterName: this.adapterName,
|
|
1174
|
+
agentId: this.agentId,
|
|
1175
|
+
adapterSessionId: await this.getAdapterSessionId(),
|
|
1176
|
+
sessionId: this.sessionId
|
|
1177
|
+
};
|
|
1178
|
+
}));
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Emit tool step.finished and cleanup cached data.
|
|
1182
|
+
* Shared helper for bash and patch tool completion.
|
|
1183
|
+
* @param toolCallId - Tool call identifier
|
|
1184
|
+
* @param output - Tool output content
|
|
1185
|
+
* @param isError - Whether the tool execution failed
|
|
1186
|
+
*/
|
|
1187
|
+
async emitToolStepFinished(toolCallId, output, isError) {
|
|
1188
|
+
const blockIndex = this.toolBlockIndexMap.get(toolCallId);
|
|
1189
|
+
if (blockIndex === void 0) console.warn(`[CodexAdapter] toolCallId ${toolCallId} not found in toolBlockIndexMap - possible state mismatch`);
|
|
1190
|
+
const resolvedBlockIndex = blockIndex ?? -1;
|
|
1191
|
+
this.toolBlockIndexMap.delete(toolCallId);
|
|
1192
|
+
const content = {
|
|
1193
|
+
type: "tool_output",
|
|
1194
|
+
toolCallId,
|
|
1195
|
+
output,
|
|
1196
|
+
isError
|
|
1197
|
+
};
|
|
1198
|
+
await this.emitGlobal(AgentSubjects.step.finished, {
|
|
1199
|
+
stepType: "tool_use",
|
|
1200
|
+
blockIndex: resolvedBlockIndex,
|
|
1201
|
+
content
|
|
1202
|
+
});
|
|
1203
|
+
this.toolCallCache.delete(toolCallId);
|
|
1204
|
+
this.toolOutputCache.delete(toolCallId);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region src/dynamic-tool-handling.ts
|
|
1210
|
+
/**
|
|
1211
|
+
* Dynamic tool integration for Codex App Server.
|
|
1212
|
+
*
|
|
1213
|
+
* Converts Makaio registry tools to Codex's dynamicTools format for
|
|
1214
|
+
* `thread/start`, and handles `item/tool/call` server requests by routing
|
|
1215
|
+
* execution through the bus.
|
|
1216
|
+
* @packageDocumentation
|
|
1217
|
+
*/
|
|
1218
|
+
/**
|
|
1219
|
+
* Type guard for `item/tool/call` server requests.
|
|
1220
|
+
*
|
|
1221
|
+
* Used to narrow the generic server-request union (which does not yet include
|
|
1222
|
+
* `item/tool/call` in the generated types) to the experimental shape.
|
|
1223
|
+
* @param request - The raw server request object
|
|
1224
|
+
* @returns `true` if the request is an `item/tool/call` request
|
|
1225
|
+
*/
|
|
1226
|
+
function isDynamicToolCallRequest(request) {
|
|
1227
|
+
return request.method === "item/tool/call";
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Convert registry `ToolListItem[]` to Codex dynamic tool format.
|
|
1231
|
+
*
|
|
1232
|
+
* Filters out tools without an `inputSchema` because the codex protocol
|
|
1233
|
+
* requires a JSON Schema for each dynamic tool declaration.
|
|
1234
|
+
* @param tools - Tools from `loadToolsFromRegistry`
|
|
1235
|
+
* @returns Codex-compatible dynamic tool declarations
|
|
1236
|
+
*/
|
|
1237
|
+
function toCodexDynamicToolFormat(tools) {
|
|
1238
|
+
return filterToolsWithSchema(tools).map((tool) => ({
|
|
1239
|
+
name: tool.name,
|
|
1240
|
+
description: tool.description,
|
|
1241
|
+
inputSchema: tool.inputSchema
|
|
1242
|
+
}));
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Fetch registry tools and convert to Codex dynamic tool format.
|
|
1246
|
+
*
|
|
1247
|
+
* `loadToolsFromRegistry` catches fetch failures internally and returns `[]`,
|
|
1248
|
+
* so this function never throws. Callers always receive a (possibly empty) array.
|
|
1249
|
+
* @param adapterId - Adapter instance ID for policy filtering
|
|
1250
|
+
* @param adapterName - Adapter type name for policy filtering
|
|
1251
|
+
* @returns Codex-formatted dynamic tool declarations
|
|
1252
|
+
*/
|
|
1253
|
+
async function fetchToolsForCodex(adapterId, adapterName) {
|
|
1254
|
+
return toCodexDynamicToolFormat(await loadToolsFromRegistry(adapterId, adapterName));
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Determine whether a serialised tool output string represents a success.
|
|
1258
|
+
*
|
|
1259
|
+
* Returns `false` when the output is valid JSON that contains a top-level `error`
|
|
1260
|
+
* key (the shape emitted by {@link handleDynamicToolCall} on bus failure), and
|
|
1261
|
+
* `true` in all other cases (including non-JSON output, which is treated as success).
|
|
1262
|
+
*
|
|
1263
|
+
* Known limitation: a tool that legitimately returns `{ "error": "..." }` as data
|
|
1264
|
+
* will be misclassified as failed. This is acceptable because (a) the Codex dynamic
|
|
1265
|
+
* tool protocol has no out-of-band success channel, (b) tools should not use
|
|
1266
|
+
* "error" as a top-level key for successful results, and (c) the misclassification
|
|
1267
|
+
* only affects lifecycle telemetry, not the tool output returned to the agent.
|
|
1268
|
+
*
|
|
1269
|
+
* **Error format contract:** {@link handleDynamicToolCall} serialises bus errors as
|
|
1270
|
+
* `JSON.stringify({ error: message })` (a single top-level `error` string key).
|
|
1271
|
+
* This function is coupled to that exact shape — any change to the error format in
|
|
1272
|
+
* `handleDynamicToolCall` must be reflected here.
|
|
1273
|
+
* @param outputText - Serialised tool output from a `DynamicToolCallResponse`
|
|
1274
|
+
* @returns `true` if the output represents a successful execution
|
|
1275
|
+
*/
|
|
1276
|
+
function isDynamicToolCallSuccess(outputText) {
|
|
1277
|
+
try {
|
|
1278
|
+
return !("error" in JSON.parse(outputText));
|
|
1279
|
+
} catch {
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Handle an `item/tool/call` server request from codex.
|
|
1285
|
+
*
|
|
1286
|
+
* Routes execution through `ToolSubjects.execute` on the global bus and
|
|
1287
|
+
* converts the result to the content-item format codex expects in the response.
|
|
1288
|
+
* Both success and failure paths return a valid response — bus errors are
|
|
1289
|
+
* serialised as error content rather than rejecting the promise, so codex
|
|
1290
|
+
* always receives a well-formed reply.
|
|
1291
|
+
* @param params - Tool call parameters from the server request
|
|
1292
|
+
* @param context - Execution context for bus routing and attribution
|
|
1293
|
+
* @returns Content items to send back to codex
|
|
1294
|
+
*/
|
|
1295
|
+
async function handleDynamicToolCall(params, context) {
|
|
1296
|
+
try {
|
|
1297
|
+
const result = await MakaioBus.request(ToolSubjects.execute, {
|
|
1298
|
+
toolName: params.name,
|
|
1299
|
+
input: params.arguments,
|
|
1300
|
+
adapterId: context.adapterId,
|
|
1301
|
+
adapterName: context.adapterName,
|
|
1302
|
+
contextOverrides: {
|
|
1303
|
+
sessionId: context.sessionId,
|
|
1304
|
+
agentId: context.agentId,
|
|
1305
|
+
toolCallId: params.itemId
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
const text = result.success ? typeof result.data === "string" ? result.data : JSON.stringify(result.data ?? null) : JSON.stringify({
|
|
1309
|
+
error: result.error.message,
|
|
1310
|
+
code: result.error.code
|
|
1311
|
+
});
|
|
1312
|
+
if (result.success && context.toolLedger && isMcpCallTool(params.name)) {
|
|
1313
|
+
const targetTool = extractMcpCallTarget(params.arguments);
|
|
1314
|
+
if (targetTool !== void 0 && context.currentTurnNumber > 0) context.toolLedger.recordCall(targetTool, context.currentTurnNumber);
|
|
1315
|
+
}
|
|
1316
|
+
return { content: [{
|
|
1317
|
+
type: "text",
|
|
1318
|
+
text
|
|
1319
|
+
}] };
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1322
|
+
return { content: [{
|
|
1323
|
+
type: "text",
|
|
1324
|
+
text: JSON.stringify({ error: message })
|
|
1325
|
+
}] };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region src/connector/approval-handlers.ts
|
|
1331
|
+
/**
|
|
1332
|
+
* Handle command approval request.
|
|
1333
|
+
*
|
|
1334
|
+
* Returns an immediate `decline` when the `bash` native tool is disabled by the
|
|
1335
|
+
* active harness, bypassing the normal approval flow.
|
|
1336
|
+
* Otherwise routes the JSON-RPC server request to the scoped bus via requestToolApproval.
|
|
1337
|
+
* @param request - Server request for command approval
|
|
1338
|
+
* @param ctx - Approval context
|
|
1339
|
+
* @returns Approval response with decision and optional message
|
|
1340
|
+
*/
|
|
1341
|
+
async function handleCommandApprovalRequest(request, ctx) {
|
|
1342
|
+
if (ctx.getDisabledNativeTools().has("bash")) return {
|
|
1343
|
+
decision: "decline",
|
|
1344
|
+
message: "bash tool is disabled by the active harness"
|
|
1345
|
+
};
|
|
1346
|
+
const params = request.params;
|
|
1347
|
+
try {
|
|
1348
|
+
const commandInfo = ctx.commandExecutionByItemId.get(params.itemId) ?? await ctx.waitForCommandInfo(params.itemId);
|
|
1349
|
+
return await ctx.requestToolApproval(CodexAppServerSubjects.exec_approval_request, {
|
|
1350
|
+
threadId: params.threadId,
|
|
1351
|
+
turnId: params.turnId,
|
|
1352
|
+
callId: params.itemId,
|
|
1353
|
+
command: commandInfo ? [commandInfo.command] : ["<unknown>"],
|
|
1354
|
+
cwd: commandInfo?.cwd ?? ctx.cwd ?? "",
|
|
1355
|
+
timestamp: Date.now(),
|
|
1356
|
+
reason: params.reason ?? null
|
|
1357
|
+
});
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
ctx.handleError(/* @__PURE__ */ new Error(`Tool approval request failed, make sure that there's a handler registered: ${String(error)}`), false);
|
|
1360
|
+
return {
|
|
1361
|
+
decision: "decline",
|
|
1362
|
+
message: String(error)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Handle file change approval request.
|
|
1368
|
+
*
|
|
1369
|
+
* Returns an immediate `decline` when the `patch` native tool is disabled by the
|
|
1370
|
+
* active harness, bypassing the normal approval flow.
|
|
1371
|
+
* Otherwise routes the JSON-RPC server request to the scoped bus via requestToolApproval.
|
|
1372
|
+
* @param request - Server request for file change approval
|
|
1373
|
+
* @param ctx - Approval context
|
|
1374
|
+
* @returns Approval response with decision and optional message
|
|
1375
|
+
*/
|
|
1376
|
+
async function handleFileChangeApprovalRequest(request, ctx) {
|
|
1377
|
+
if (ctx.getDisabledNativeTools().has("patch")) return {
|
|
1378
|
+
decision: "decline",
|
|
1379
|
+
message: "patch tool is disabled by the active harness"
|
|
1380
|
+
};
|
|
1381
|
+
const params = request.params;
|
|
1382
|
+
try {
|
|
1383
|
+
return await ctx.requestToolApproval(CodexAppServerSubjects.file_change_approval_request, {
|
|
1384
|
+
threadId: params.threadId,
|
|
1385
|
+
turnId: params.turnId,
|
|
1386
|
+
itemId: params.itemId,
|
|
1387
|
+
reason: params.reason ?? null,
|
|
1388
|
+
grantRoot: params.grantRoot ?? null,
|
|
1389
|
+
timestamp: Date.now()
|
|
1390
|
+
});
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
ctx.handleError(/* @__PURE__ */ new Error(`Tool approval request failed, make sure that there's a handler registered: ${String(error)}`), false);
|
|
1393
|
+
return {
|
|
1394
|
+
decision: "decline",
|
|
1395
|
+
message: String(error)
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Emit a synthetic begin+end lifecycle pair for a denied or errored dynamic tool call.
|
|
1401
|
+
*
|
|
1402
|
+
* Denied tool calls never produce `item/started`/`item/completed` notifications from
|
|
1403
|
+
* codex, so the agent's `dynamic_tool_call_begin`/`end` handlers never fire. Emitting
|
|
1404
|
+
* the pair here ensures downstream consumers (e.g. AgentSubjects.tool.use) observe
|
|
1405
|
+
* every tool call regardless of its approval outcome.
|
|
1406
|
+
* @param params - Tool call parameters from the server request
|
|
1407
|
+
* @param output - Serialised error output to surface as the tool result
|
|
1408
|
+
* @param emit - Scoped bus emit function
|
|
1409
|
+
*/
|
|
1410
|
+
async function emitDeniedDynamicToolCallLifecycle(params, output, emit) {
|
|
1411
|
+
const now = Date.now();
|
|
1412
|
+
await emit(CodexAppServerSubjects.dynamic_tool_call_begin, {
|
|
1413
|
+
threadId: params.threadId,
|
|
1414
|
+
turnId: params.turnId,
|
|
1415
|
+
itemId: params.itemId,
|
|
1416
|
+
name: params.name,
|
|
1417
|
+
args: params.arguments,
|
|
1418
|
+
timestamp: now
|
|
1419
|
+
});
|
|
1420
|
+
await emit(CodexAppServerSubjects.dynamic_tool_call_end, {
|
|
1421
|
+
threadId: params.threadId,
|
|
1422
|
+
turnId: params.turnId,
|
|
1423
|
+
itemId: params.itemId,
|
|
1424
|
+
name: params.name,
|
|
1425
|
+
output,
|
|
1426
|
+
success: false,
|
|
1427
|
+
timestamp: now
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Handle an `item/tool/call` server request (experimental API).
|
|
1432
|
+
*
|
|
1433
|
+
* Requests tool approval via the scoped bus before executing. If denied, returns error
|
|
1434
|
+
* content without executing. If approved, executes via bus and caches result under
|
|
1435
|
+
* `itemId` for lifecycle handler correlation.
|
|
1436
|
+
*
|
|
1437
|
+
* Denied calls emit a synthetic `dynamic_tool_call_begin`/`end` pair so that
|
|
1438
|
+
* downstream lifecycle consumers (e.g. `AgentSubjects.tool.use`) observe every
|
|
1439
|
+
* tool invocation regardless of approval outcome.
|
|
1440
|
+
* @param params - Parsed `item/tool/call` request parameters
|
|
1441
|
+
* @param ctx - Dynamic tool approval and execution context
|
|
1442
|
+
* @returns Content items to send back to codex
|
|
1443
|
+
*/
|
|
1444
|
+
async function handleDynamicToolCallApprovalRequest(params, ctx) {
|
|
1445
|
+
let approval;
|
|
1446
|
+
try {
|
|
1447
|
+
approval = await ctx.requestToolApproval(CodexAppServerSubjects.dynamic_tool_call_approval_request, {
|
|
1448
|
+
threadId: params.threadId,
|
|
1449
|
+
turnId: params.turnId,
|
|
1450
|
+
itemId: params.itemId,
|
|
1451
|
+
name: params.name,
|
|
1452
|
+
args: params.arguments,
|
|
1453
|
+
timestamp: Date.now()
|
|
1454
|
+
});
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
const errorText = JSON.stringify({ error: String(error) });
|
|
1457
|
+
ctx.dynamicToolCallByItemId.set(params.itemId, {
|
|
1458
|
+
name: params.name,
|
|
1459
|
+
args: params.arguments,
|
|
1460
|
+
output: errorText,
|
|
1461
|
+
success: false
|
|
1462
|
+
});
|
|
1463
|
+
try {
|
|
1464
|
+
await emitDeniedDynamicToolCallLifecycle(params, errorText, ctx.emit);
|
|
1465
|
+
} catch {} finally {
|
|
1466
|
+
ctx.dynamicToolCallByItemId.delete(params.itemId);
|
|
1467
|
+
}
|
|
1468
|
+
return { content: [{
|
|
1469
|
+
type: "text",
|
|
1470
|
+
text: errorText
|
|
1471
|
+
}] };
|
|
1472
|
+
}
|
|
1473
|
+
if (approval?.decision !== "accept") {
|
|
1474
|
+
const message = approval?.decision === "decline" ? approval.message ?? "Tool call denied by approval handler" : "Invalid approval response from approval handler";
|
|
1475
|
+
const errorText = JSON.stringify({ error: message });
|
|
1476
|
+
ctx.dynamicToolCallByItemId.set(params.itemId, {
|
|
1477
|
+
name: params.name,
|
|
1478
|
+
args: params.arguments,
|
|
1479
|
+
output: errorText,
|
|
1480
|
+
success: false
|
|
1481
|
+
});
|
|
1482
|
+
try {
|
|
1483
|
+
await emitDeniedDynamicToolCallLifecycle(params, errorText, ctx.emit);
|
|
1484
|
+
} catch {} finally {
|
|
1485
|
+
ctx.dynamicToolCallByItemId.delete(params.itemId);
|
|
1486
|
+
}
|
|
1487
|
+
return { content: [{
|
|
1488
|
+
type: "text",
|
|
1489
|
+
text: errorText
|
|
1490
|
+
}] };
|
|
1491
|
+
}
|
|
1492
|
+
const response = await handleDynamicToolCall(params, {
|
|
1493
|
+
sessionId: ctx.sessionId,
|
|
1494
|
+
agentId: ctx.agentId,
|
|
1495
|
+
adapterId: ctx.adapterId,
|
|
1496
|
+
adapterName: ctx.adapterName,
|
|
1497
|
+
toolLedger: ctx.toolLedger,
|
|
1498
|
+
currentTurnNumber: ctx.currentTurnNumber
|
|
1499
|
+
});
|
|
1500
|
+
const outputText = response.content[0]?.text ?? "";
|
|
1501
|
+
const success = response.content.length > 0 && isDynamicToolCallSuccess(outputText);
|
|
1502
|
+
ctx.dynamicToolCallByItemId.set(params.itemId, {
|
|
1503
|
+
name: params.name,
|
|
1504
|
+
args: params.arguments,
|
|
1505
|
+
output: outputText,
|
|
1506
|
+
success
|
|
1507
|
+
});
|
|
1508
|
+
return response;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
//#endregion
|
|
1512
|
+
//#region src/connector/delta-handlers.ts
|
|
1513
|
+
/**
|
|
1514
|
+
* Delta event handlers for Codex App-Server notifications.
|
|
1515
|
+
*
|
|
1516
|
+
* These handle streaming content updates (agent messages, command output, reasoning, file changes).
|
|
1517
|
+
* @packageDocumentation
|
|
1518
|
+
*/
|
|
1519
|
+
/**
|
|
1520
|
+
* Create a type guard validator for a Zod schema.
|
|
1521
|
+
* Logs validation errors when parsing fails.
|
|
1522
|
+
* @param schema - The Zod schema to validate against
|
|
1523
|
+
* @param name - Human-readable name for error logging
|
|
1524
|
+
* @returns A type guard function that validates and logs errors
|
|
1525
|
+
*/
|
|
1526
|
+
function createValidator(schema, name) {
|
|
1527
|
+
return (params) => {
|
|
1528
|
+
const result = schema.safeParse(params);
|
|
1529
|
+
if (!result.success) console.warn(`[delta-handlers] Invalid ${name} notification:`, result.error.format());
|
|
1530
|
+
return result.success;
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const isValidAgentMessageDeltaNotification = createValidator(z.union([z.object({
|
|
1534
|
+
threadId: z.string(),
|
|
1535
|
+
turnId: z.string(),
|
|
1536
|
+
delta: z.string()
|
|
1537
|
+
}), z.object({ delta: z.string() })]), "agent message delta");
|
|
1538
|
+
const isValidCommandOutputDeltaNotification = createValidator(z.object({
|
|
1539
|
+
threadId: z.string(),
|
|
1540
|
+
turnId: z.string(),
|
|
1541
|
+
itemId: z.string(),
|
|
1542
|
+
delta: z.string()
|
|
1543
|
+
}), "command output delta");
|
|
1544
|
+
const isValidReasoningDeltaNotification = createValidator(z.union([z.object({
|
|
1545
|
+
threadId: z.string(),
|
|
1546
|
+
turnId: z.string(),
|
|
1547
|
+
delta: z.string()
|
|
1548
|
+
}), z.object({ delta: z.string() })]), "reasoning delta");
|
|
1549
|
+
const isValidFileChangeDeltaNotification = createValidator(z.object({
|
|
1550
|
+
threadId: z.string(),
|
|
1551
|
+
turnId: z.string(),
|
|
1552
|
+
itemId: z.string(),
|
|
1553
|
+
delta: z.string()
|
|
1554
|
+
}), "file change delta");
|
|
1555
|
+
/**
|
|
1556
|
+
* Handle agent message delta notification.
|
|
1557
|
+
* @param emit - Bus emit function
|
|
1558
|
+
* @param params - Delta notification params
|
|
1559
|
+
* @param onAccumulate - Callback to accumulate content
|
|
1560
|
+
*/
|
|
1561
|
+
async function handleAgentMessageDelta(emit, params, onAccumulate) {
|
|
1562
|
+
if (!isValidAgentMessageDeltaNotification(params)) {
|
|
1563
|
+
console.warn("[delta-handlers] Skipping invalid agent message delta notification");
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const delta = params.delta;
|
|
1567
|
+
onAccumulate(delta);
|
|
1568
|
+
if ("threadId" in params) await emit(CodexAppServerSubjects.agent_message_delta, {
|
|
1569
|
+
threadId: params.threadId,
|
|
1570
|
+
turnId: params.turnId,
|
|
1571
|
+
delta: params.delta,
|
|
1572
|
+
timestamp: Date.now()
|
|
1573
|
+
});
|
|
1574
|
+
else console.warn("[CodexAppServerConnector] Agent message delta received without threadId, accumulating but not emitting");
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Handle command output delta notification.
|
|
1578
|
+
* @param emit - Bus emit function
|
|
1579
|
+
* @param params - Delta notification params
|
|
1580
|
+
*/
|
|
1581
|
+
async function handleCommandOutputDelta(emit, params) {
|
|
1582
|
+
if (!isValidCommandOutputDeltaNotification(params)) {
|
|
1583
|
+
console.warn("[delta-handlers] Skipping invalid command output delta notification");
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
await emit(CodexAppServerSubjects.exec_command_output_delta, {
|
|
1587
|
+
threadId: params.threadId,
|
|
1588
|
+
turnId: params.turnId,
|
|
1589
|
+
callId: params.itemId,
|
|
1590
|
+
stream: "stdout",
|
|
1591
|
+
chunk: params.delta,
|
|
1592
|
+
timestamp: Date.now()
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Handle reasoning delta notification.
|
|
1597
|
+
* @param emit - Bus emit function
|
|
1598
|
+
* @param params - Delta notification params
|
|
1599
|
+
*/
|
|
1600
|
+
async function handleReasoningDelta(emit, params) {
|
|
1601
|
+
if (!isValidReasoningDeltaNotification(params)) {
|
|
1602
|
+
console.warn("[delta-handlers] Skipping invalid reasoning delta notification");
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if ("threadId" in params) await emit(CodexAppServerSubjects.reasoning_delta, {
|
|
1606
|
+
threadId: params.threadId,
|
|
1607
|
+
turnId: params.turnId,
|
|
1608
|
+
delta: params.delta,
|
|
1609
|
+
timestamp: Date.now()
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Handle file change delta notification.
|
|
1614
|
+
* @param emit - Bus emit function
|
|
1615
|
+
* @param params - Delta notification params
|
|
1616
|
+
*/
|
|
1617
|
+
async function handleFileChangeDelta(emit, params) {
|
|
1618
|
+
if (!isValidFileChangeDeltaNotification(params)) {
|
|
1619
|
+
console.warn("[delta-handlers] Skipping invalid file change delta notification");
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
await emit(CodexAppServerSubjects.file_change_output_delta, {
|
|
1623
|
+
threadId: params.threadId,
|
|
1624
|
+
turnId: params.turnId,
|
|
1625
|
+
itemId: params.itemId,
|
|
1626
|
+
delta: params.delta,
|
|
1627
|
+
timestamp: Date.now()
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
//#endregion
|
|
1632
|
+
//#region src/connector/lifecycle-handlers.ts
|
|
1633
|
+
/**
|
|
1634
|
+
* Extract thread ID from thread/started notification.
|
|
1635
|
+
* @param notification - Thread started notification
|
|
1636
|
+
* @returns Thread ID string
|
|
1637
|
+
*/
|
|
1638
|
+
function extractThreadId(notification) {
|
|
1639
|
+
return notification.thread.id;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Handle turn/started notification.
|
|
1643
|
+
* Acknowledges the message when the turn starts (server echoed it).
|
|
1644
|
+
* @param notification - Turn started notification
|
|
1645
|
+
* @param turn - Current turn instance
|
|
1646
|
+
* @param updateProcessingState - State update callback
|
|
1647
|
+
*/
|
|
1648
|
+
async function handleTurnStarted(notification, turn, updateProcessingState) {
|
|
1649
|
+
if (!turn) {
|
|
1650
|
+
console.warn("[CodexAppServerConnector] Received turn/started but no current turn");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
turn.markAcknowledged();
|
|
1654
|
+
await turn.handleTurnStarted(notification.turn.id);
|
|
1655
|
+
await updateProcessingState("processing_started");
|
|
1656
|
+
await updateProcessingState("turn_started");
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Handle item/started notification.
|
|
1660
|
+
* @param notification - Item started notification
|
|
1661
|
+
* @param turn - Current turn instance
|
|
1662
|
+
* @param emit - Bus emit function
|
|
1663
|
+
* @param commandCache - Cache for command execution metadata
|
|
1664
|
+
* @param dynamicToolCallCache - Cache for dynamic tool call metadata and results
|
|
1665
|
+
* @param updateProcessingState - State update callback
|
|
1666
|
+
* @param onCommandInfoReady - Optional callback invoked after a commandExecution entry
|
|
1667
|
+
* is written to `commandCache`. Used to unblock approval requests that arrived before
|
|
1668
|
+
* this `item/started` notification.
|
|
1669
|
+
*/
|
|
1670
|
+
async function handleItemStarted(notification, turn, emit, commandCache, dynamicToolCallCache, updateProcessingState, onCommandInfoReady) {
|
|
1671
|
+
if (!turn) return;
|
|
1672
|
+
const item = notification.item;
|
|
1673
|
+
const itemId = "id" in item ? item.id : "";
|
|
1674
|
+
const itemType = item.type;
|
|
1675
|
+
await turn.handleItemStarted(itemId, itemType);
|
|
1676
|
+
await updateProcessingState("step_started");
|
|
1677
|
+
if (item.type === "commandExecution") {
|
|
1678
|
+
const info = {
|
|
1679
|
+
command: item.command,
|
|
1680
|
+
cwd: item.cwd
|
|
1681
|
+
};
|
|
1682
|
+
commandCache.set(item.id, info);
|
|
1683
|
+
onCommandInfoReady?.(item.id, info);
|
|
1684
|
+
await emit(CodexAppServerSubjects.exec_command_begin, {
|
|
1685
|
+
threadId: notification.threadId,
|
|
1686
|
+
turnId: notification.turnId,
|
|
1687
|
+
callId: item.id,
|
|
1688
|
+
command: [item.command],
|
|
1689
|
+
cwd: item.cwd,
|
|
1690
|
+
timestamp: Date.now()
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
if (itemType === "dynamicToolCall") {
|
|
1694
|
+
const cached = dynamicToolCallCache.get(itemId);
|
|
1695
|
+
if (cached) await emit(CodexAppServerSubjects.dynamic_tool_call_begin, {
|
|
1696
|
+
threadId: notification.threadId,
|
|
1697
|
+
turnId: notification.turnId,
|
|
1698
|
+
itemId,
|
|
1699
|
+
name: cached.name,
|
|
1700
|
+
args: cached.args,
|
|
1701
|
+
timestamp: Date.now()
|
|
1702
|
+
});
|
|
1703
|
+
else console.warn(`[lifecycle-handlers] dynamicToolCall item ${itemId} started with no cached tool call data`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Handle item/completed notification.
|
|
1708
|
+
* @param notification - Item completed notification
|
|
1709
|
+
* @param turn - Current turn instance
|
|
1710
|
+
* @param emit - Bus emit function
|
|
1711
|
+
* @param commandCache - Cache for command execution metadata
|
|
1712
|
+
* @param dynamicToolCallCache - Cache for dynamic tool call metadata and results
|
|
1713
|
+
* @param updateProcessingState - State update callback
|
|
1714
|
+
*/
|
|
1715
|
+
async function handleItemCompleted(notification, turn, emit, commandCache, dynamicToolCallCache, updateProcessingState) {
|
|
1716
|
+
if (!turn) return;
|
|
1717
|
+
const item = notification.item;
|
|
1718
|
+
const itemId = "id" in item ? item.id : "";
|
|
1719
|
+
const itemType = item.type;
|
|
1720
|
+
await turn.handleItemCompleted(itemId);
|
|
1721
|
+
await updateProcessingState("step_finished");
|
|
1722
|
+
if (item.type === "commandExecution") {
|
|
1723
|
+
await emit(CodexAppServerSubjects.exec_command_end, {
|
|
1724
|
+
threadId: notification.threadId,
|
|
1725
|
+
turnId: notification.turnId,
|
|
1726
|
+
callId: item.id,
|
|
1727
|
+
exitCode: item.exitCode ?? 0,
|
|
1728
|
+
timestamp: Date.now()
|
|
1729
|
+
});
|
|
1730
|
+
commandCache.delete(item.id);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (itemType === "dynamicToolCall") {
|
|
1734
|
+
const cached = dynamicToolCallCache.get(itemId);
|
|
1735
|
+
if (cached) {
|
|
1736
|
+
await emit(CodexAppServerSubjects.dynamic_tool_call_end, {
|
|
1737
|
+
threadId: notification.threadId,
|
|
1738
|
+
turnId: notification.turnId,
|
|
1739
|
+
itemId,
|
|
1740
|
+
name: cached.name,
|
|
1741
|
+
output: cached.output,
|
|
1742
|
+
success: cached.success,
|
|
1743
|
+
timestamp: Date.now()
|
|
1744
|
+
});
|
|
1745
|
+
dynamicToolCallCache.delete(itemId);
|
|
1746
|
+
} else console.warn(`[lifecycle-handlers] dynamicToolCall item ${itemId} completed with no cached tool call data`);
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
await emit(CodexAppServerSubjects.item_completed, {
|
|
1750
|
+
threadId: notification.threadId,
|
|
1751
|
+
turnId: notification.turnId,
|
|
1752
|
+
itemId,
|
|
1753
|
+
itemType,
|
|
1754
|
+
timestamp: Date.now()
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Handle token usage updated notification.
|
|
1759
|
+
* @param notification - Token usage notification
|
|
1760
|
+
* @param thread - Current thread instance
|
|
1761
|
+
*/
|
|
1762
|
+
async function handleTokenUsageUpdated(notification, thread) {
|
|
1763
|
+
if (!thread) return;
|
|
1764
|
+
await thread.handleTokenUsageUpdated(notification.tokenUsage.last.inputTokens, notification.tokenUsage.last.cachedInputTokens, notification.tokenUsage.last.outputTokens, notification.tokenUsage.last.reasoningOutputTokens, notification.tokenUsage.last.totalTokens, notification.tokenUsage.modelContextWindow ?? void 0);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/connector/client-handlers.ts
|
|
1769
|
+
/**
|
|
1770
|
+
* Register JSON-RPC notification handlers for the connector client instance.
|
|
1771
|
+
* @param options - Callbacks and state accessors used to bridge notifications into connector state
|
|
1772
|
+
*/
|
|
1773
|
+
function registerNotificationHandlers(options) {
|
|
1774
|
+
const { client, enqueueNotification, onThreadStarted, consumeTurnNumber, getCurrentTurn, emit, commandExecutionByItemId, dynamicToolCallByItemId, updateProcessingState, appendAgentMessageDelta, onTurnCompleted, getThread, handleAsyncError, onCommandInfoReady } = options;
|
|
1775
|
+
client.onNotification("thread/started", (_method, params) => {
|
|
1776
|
+
enqueueNotification(() => onThreadStarted(params));
|
|
1777
|
+
});
|
|
1778
|
+
client.onNotification("turn/started", (_method, params) => {
|
|
1779
|
+
consumeTurnNumber();
|
|
1780
|
+
enqueueNotification(() => handleTurnStarted(params, getCurrentTurn(), updateProcessingState));
|
|
1781
|
+
});
|
|
1782
|
+
client.onNotification("item/started", (_method, params) => {
|
|
1783
|
+
enqueueNotification(() => handleItemStarted(params, getCurrentTurn(), emit, commandExecutionByItemId, dynamicToolCallByItemId, updateProcessingState, onCommandInfoReady));
|
|
1784
|
+
});
|
|
1785
|
+
client.onNotification("item/completed", (_method, params) => {
|
|
1786
|
+
enqueueNotification(() => handleItemCompleted(params, getCurrentTurn(), emit, commandExecutionByItemId, dynamicToolCallByItemId, updateProcessingState));
|
|
1787
|
+
});
|
|
1788
|
+
client.onNotification("item/agentMessage/delta", (_method, params) => {
|
|
1789
|
+
handleAgentMessageDelta(emit, params, appendAgentMessageDelta).catch(handleAsyncError);
|
|
1790
|
+
});
|
|
1791
|
+
client.onNotification("item/commandExecution/outputDelta", (_method, params) => {
|
|
1792
|
+
handleCommandOutputDelta(emit, params).catch(handleAsyncError);
|
|
1793
|
+
});
|
|
1794
|
+
client.onNotification("item/reasoning/textDelta", (_method, params) => {
|
|
1795
|
+
handleReasoningDelta(emit, params).catch(handleAsyncError);
|
|
1796
|
+
});
|
|
1797
|
+
client.onNotification("item/fileChange/outputDelta", (_method, params) => {
|
|
1798
|
+
handleFileChangeDelta(emit, params).catch(handleAsyncError);
|
|
1799
|
+
});
|
|
1800
|
+
client.onNotification("turn/completed", (_method, params) => {
|
|
1801
|
+
enqueueNotification(() => onTurnCompleted(params));
|
|
1802
|
+
});
|
|
1803
|
+
client.onNotification("thread/tokenUsage/updated", (_method, params) => {
|
|
1804
|
+
Promise.resolve(handleTokenUsageUpdated(params, getThread())).catch(handleAsyncError);
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Register the JSON-RPC server-request handler for approval and dynamic tool call flows.
|
|
1809
|
+
* @param options - Callbacks and state accessors needed to answer server requests
|
|
1810
|
+
*/
|
|
1811
|
+
function registerServerRequestHandler(options) {
|
|
1812
|
+
const ctx = {
|
|
1813
|
+
agentId: options.agentId,
|
|
1814
|
+
cwd: options.cwd,
|
|
1815
|
+
commandExecutionByItemId: options.commandExecutionByItemId,
|
|
1816
|
+
requestToolApproval: options.requestToolApproval,
|
|
1817
|
+
handleError: options.handleError,
|
|
1818
|
+
getDisabledNativeTools: options.getDisabledNativeTools,
|
|
1819
|
+
waitForCommandInfo: options.waitForCommandInfo
|
|
1820
|
+
};
|
|
1821
|
+
options.client.onServerRequest(async (request) => {
|
|
1822
|
+
const asMethodRequest = request;
|
|
1823
|
+
if (isDynamicToolCallRequest(asMethodRequest)) return options.handleDynamicToolCallRequest(asMethodRequest.params);
|
|
1824
|
+
if (request.method === "item/commandExecution/requestApproval") return handleCommandApprovalRequest(request, ctx);
|
|
1825
|
+
if (request.method === "item/fileChange/requestApproval") return handleFileChangeApprovalRequest(request, ctx);
|
|
1826
|
+
throw new Error(`Unknown server request: ${request.method}`);
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region ../../../packages/subprocess/src/json-rpc-client.ts
|
|
1832
|
+
/** Default request timeout for JSON-RPC calls. */
|
|
1833
|
+
const DEFAULT_JSON_RPC_REQUEST_TIMEOUT_MS = 6e4;
|
|
1834
|
+
/**
|
|
1835
|
+
* Create a JSON-RPC 2.0 client on top of a JSONL transport.
|
|
1836
|
+
*
|
|
1837
|
+
* Message dispatch rules (JSON-RPC 2.0):
|
|
1838
|
+
* - `id` + `result` → correlated response (resolves pending promise)
|
|
1839
|
+
* - `id` + `error` → correlated error response (rejects pending promise)
|
|
1840
|
+
* - `id` + `method` → server-initiated request (call handlers, send response)
|
|
1841
|
+
* - `method`, no `id` → notification (call method-specific handler)
|
|
1842
|
+
* @param transport - JSONL transport to send/receive messages on.
|
|
1843
|
+
* @returns JSON-RPC 2.0 client interface.
|
|
1844
|
+
*/
|
|
1845
|
+
function createJsonRpcClient$1(transport) {
|
|
1846
|
+
let nextId = 1;
|
|
1847
|
+
let closed = false;
|
|
1848
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
1849
|
+
const notificationHandlers = /* @__PURE__ */ new Map();
|
|
1850
|
+
const serverRequestHandlers = /* @__PURE__ */ new Set();
|
|
1851
|
+
/**
|
|
1852
|
+
* Generate a unique, monotonically increasing request ID.
|
|
1853
|
+
* @returns Next available request ID.
|
|
1854
|
+
*/
|
|
1855
|
+
function generateRequestId() {
|
|
1856
|
+
return nextId++;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Reject all pending requests with the given error and clear the map.
|
|
1860
|
+
* @param error - Error to reject all pending requests with.
|
|
1861
|
+
*/
|
|
1862
|
+
function rejectAllPending(error) {
|
|
1863
|
+
for (const [, pending] of pendingRequests) {
|
|
1864
|
+
if (pending.timeoutId !== null) clearTimeout(pending.timeoutId);
|
|
1865
|
+
pending.reject(error);
|
|
1866
|
+
}
|
|
1867
|
+
pendingRequests.clear();
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Resolve or reject a pending request exactly once and clear its timer.
|
|
1871
|
+
* @param id - Request ID to settle.
|
|
1872
|
+
* @param settle - Callback receiving the pending request callbacks.
|
|
1873
|
+
*/
|
|
1874
|
+
function settlePending(id, settle) {
|
|
1875
|
+
const pending = pendingRequests.get(id);
|
|
1876
|
+
if (!pending) return;
|
|
1877
|
+
if (pending.timeoutId !== null) clearTimeout(pending.timeoutId);
|
|
1878
|
+
pendingRequests.delete(id);
|
|
1879
|
+
settle(pending);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Dispatch an incoming message according to JSON-RPC 2.0 semantics.
|
|
1883
|
+
* @param message - Raw parsed message from the transport.
|
|
1884
|
+
*/
|
|
1885
|
+
function handleMessage(message) {
|
|
1886
|
+
if (typeof message !== "object" || message === null) return;
|
|
1887
|
+
const msg = message;
|
|
1888
|
+
const id = "id" in msg ? msg["id"] : void 0;
|
|
1889
|
+
if (id !== void 0 && "result" in msg && !("method" in msg)) {
|
|
1890
|
+
settlePending(id, (pending) => {
|
|
1891
|
+
pending.resolve(msg["result"]);
|
|
1892
|
+
});
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (id !== void 0 && "error" in msg && !("method" in msg)) {
|
|
1896
|
+
settlePending(id, (pending) => {
|
|
1897
|
+
const rpcError = msg["error"];
|
|
1898
|
+
pending.reject(/* @__PURE__ */ new Error(`JSON-RPC error ${rpcError.code}: ${rpcError.message}`));
|
|
1899
|
+
});
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (id !== void 0 && "method" in msg) {
|
|
1903
|
+
handleServerRequest(message, id, msg["method"]);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if ("method" in msg && !("id" in msg)) {
|
|
1907
|
+
const method = msg["method"];
|
|
1908
|
+
const handlers = notificationHandlers.get(method);
|
|
1909
|
+
if (handlers) for (const handler of handlers) try {
|
|
1910
|
+
handler(method, msg["params"]);
|
|
1911
|
+
} catch {}
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Dispatch a server-initiated request to registered handlers.
|
|
1917
|
+
* @param message - Raw JSON-RPC request message.
|
|
1918
|
+
* @param requestId - JSON-RPC request ID to echo in the response.
|
|
1919
|
+
* @param method - JSON-RPC method name.
|
|
1920
|
+
*/
|
|
1921
|
+
async function handleServerRequest(message, requestId, method) {
|
|
1922
|
+
const handlers = [...serverRequestHandlers];
|
|
1923
|
+
if (handlers.length === 0) {
|
|
1924
|
+
transport.send({
|
|
1925
|
+
jsonrpc: "2.0",
|
|
1926
|
+
id: requestId,
|
|
1927
|
+
error: {
|
|
1928
|
+
code: -32601,
|
|
1929
|
+
message: `No handler registered for server request: ${method}`
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const results = await Promise.allSettled(handlers.map((handler) => Promise.resolve().then(() => handler(message))));
|
|
1935
|
+
if (closed) return;
|
|
1936
|
+
const success = results.find((result) => result.status === "fulfilled");
|
|
1937
|
+
if (success) {
|
|
1938
|
+
transport.send({
|
|
1939
|
+
jsonrpc: "2.0",
|
|
1940
|
+
id: requestId,
|
|
1941
|
+
result: success.value
|
|
1942
|
+
});
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const reason = results[0]?.reason;
|
|
1946
|
+
const messageText = reason instanceof Error ? reason.message : String(reason ?? "handler failed");
|
|
1947
|
+
transport.send({
|
|
1948
|
+
jsonrpc: "2.0",
|
|
1949
|
+
id: requestId,
|
|
1950
|
+
error: {
|
|
1951
|
+
code: -32603,
|
|
1952
|
+
message: messageText
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
const unsubMessage = transport.onMessage(handleMessage);
|
|
1957
|
+
const unsubError = transport.onError((error) => {
|
|
1958
|
+
rejectAllPending(error);
|
|
1959
|
+
});
|
|
1960
|
+
return {
|
|
1961
|
+
request(method, params, timeoutMs = DEFAULT_JSON_RPC_REQUEST_TIMEOUT_MS) {
|
|
1962
|
+
if (closed) return Promise.reject(/* @__PURE__ */ new Error("JSON-RPC client is closed"));
|
|
1963
|
+
return new Promise((resolve, reject) => {
|
|
1964
|
+
const id = generateRequestId();
|
|
1965
|
+
const requestMessage = {
|
|
1966
|
+
jsonrpc: "2.0",
|
|
1967
|
+
id,
|
|
1968
|
+
method,
|
|
1969
|
+
params
|
|
1970
|
+
};
|
|
1971
|
+
const timeoutId = timeoutMs === 0 ? null : setTimeout(() => {
|
|
1972
|
+
settlePending(id, (pending) => {
|
|
1973
|
+
pending.reject(/* @__PURE__ */ new Error(`JSON-RPC request timed out after ${timeoutMs}ms: ${method}`));
|
|
1974
|
+
});
|
|
1975
|
+
}, timeoutMs);
|
|
1976
|
+
pendingRequests.set(id, {
|
|
1977
|
+
resolve,
|
|
1978
|
+
reject,
|
|
1979
|
+
timeoutId
|
|
1980
|
+
});
|
|
1981
|
+
try {
|
|
1982
|
+
transport.send(requestMessage);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
settlePending(id, (pending) => {
|
|
1985
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
},
|
|
1990
|
+
notification(method, params) {
|
|
1991
|
+
if (closed) return;
|
|
1992
|
+
const msg = {
|
|
1993
|
+
jsonrpc: "2.0",
|
|
1994
|
+
method,
|
|
1995
|
+
params
|
|
1996
|
+
};
|
|
1997
|
+
transport.send(msg);
|
|
1998
|
+
},
|
|
1999
|
+
onNotification(method, handler) {
|
|
2000
|
+
let handlers = notificationHandlers.get(method);
|
|
2001
|
+
if (!handlers) {
|
|
2002
|
+
handlers = /* @__PURE__ */ new Set();
|
|
2003
|
+
notificationHandlers.set(method, handlers);
|
|
2004
|
+
}
|
|
2005
|
+
handlers.add(handler);
|
|
2006
|
+
return () => {
|
|
2007
|
+
handlers.delete(handler);
|
|
2008
|
+
if (handlers.size === 0) notificationHandlers.delete(method);
|
|
2009
|
+
};
|
|
2010
|
+
},
|
|
2011
|
+
onServerRequest(handler) {
|
|
2012
|
+
serverRequestHandlers.add(handler);
|
|
2013
|
+
return () => {
|
|
2014
|
+
serverRequestHandlers.delete(handler);
|
|
2015
|
+
};
|
|
2016
|
+
},
|
|
2017
|
+
close() {
|
|
2018
|
+
if (closed) return;
|
|
2019
|
+
closed = true;
|
|
2020
|
+
unsubMessage();
|
|
2021
|
+
unsubError();
|
|
2022
|
+
rejectAllPending(/* @__PURE__ */ new Error("JSON-RPC client closed"));
|
|
2023
|
+
notificationHandlers.clear();
|
|
2024
|
+
serverRequestHandlers.clear();
|
|
2025
|
+
transport.close();
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
//#endregion
|
|
2031
|
+
//#region src/utils/jsonRpcClient.ts
|
|
2032
|
+
/**
|
|
2033
|
+
* Creates a JSON-RPC 2.0 client wrapping a stdio transport
|
|
2034
|
+
* @param transport - Stdio transport for sending/receiving messages
|
|
2035
|
+
* @returns JSON-RPC client interface
|
|
2036
|
+
* @example
|
|
2037
|
+
* ```ts
|
|
2038
|
+
* const transport = createStdioTransport(cwd, env);
|
|
2039
|
+
* const client = createJsonRpcClient(transport);
|
|
2040
|
+
*
|
|
2041
|
+
* // Send request
|
|
2042
|
+
* const result = await client.request('thread/start', { ... });
|
|
2043
|
+
*
|
|
2044
|
+
* // Send notification
|
|
2045
|
+
* client.notification('initialized', {});
|
|
2046
|
+
*
|
|
2047
|
+
* // Handle notifications
|
|
2048
|
+
* client.onNotification('turn/started', (method, params) => {
|
|
2049
|
+
* console.log('Turn started:', params);
|
|
2050
|
+
* });
|
|
2051
|
+
*
|
|
2052
|
+
* // Handle server requests (approvals)
|
|
2053
|
+
* client.onServerRequest(async (request) => {
|
|
2054
|
+
* return { decision: 'accept' };
|
|
2055
|
+
* });
|
|
2056
|
+
* ```
|
|
2057
|
+
*/
|
|
2058
|
+
function createJsonRpcClient(transport) {
|
|
2059
|
+
const generic = createJsonRpcClient$1({
|
|
2060
|
+
send: (msg) => transport.send(msg),
|
|
2061
|
+
close: () => transport.close(),
|
|
2062
|
+
onMessage: (listener) => {
|
|
2063
|
+
transport.onMessage((msg) => listener(msg));
|
|
2064
|
+
return () => {};
|
|
2065
|
+
},
|
|
2066
|
+
onError: (listener) => {
|
|
2067
|
+
transport.onError((err) => listener(err));
|
|
2068
|
+
return () => {};
|
|
2069
|
+
},
|
|
2070
|
+
get process() {}
|
|
2071
|
+
});
|
|
2072
|
+
let serverRequestUnsubscribe;
|
|
2073
|
+
return {
|
|
2074
|
+
/**
|
|
2075
|
+
* Send a JSON-RPC request and wait for response
|
|
2076
|
+
* @param method - Method name to call
|
|
2077
|
+
* @param params - Method parameters
|
|
2078
|
+
* @returns Promise resolving to response result
|
|
2079
|
+
*/
|
|
2080
|
+
request(method, params) {
|
|
2081
|
+
return generic.request(method, params);
|
|
2082
|
+
},
|
|
2083
|
+
/**
|
|
2084
|
+
* Send a JSON-RPC notification (no response expected)
|
|
2085
|
+
* @param method - Notification method name
|
|
2086
|
+
* @param params - Notification parameters
|
|
2087
|
+
*/
|
|
2088
|
+
notification(method, params) {
|
|
2089
|
+
generic.notification(method, params);
|
|
2090
|
+
},
|
|
2091
|
+
/**
|
|
2092
|
+
* Register a handler for specific notification type
|
|
2093
|
+
* @param method - Notification method name to handle
|
|
2094
|
+
* @param handler - Function to handle notifications
|
|
2095
|
+
*/
|
|
2096
|
+
onNotification(method, handler) {
|
|
2097
|
+
generic.onNotification(method, handler);
|
|
2098
|
+
},
|
|
2099
|
+
/**
|
|
2100
|
+
* Register a handler for server requests (approvals)
|
|
2101
|
+
* @param handler - Function to handle server requests
|
|
2102
|
+
*/
|
|
2103
|
+
onServerRequest(handler) {
|
|
2104
|
+
serverRequestUnsubscribe?.();
|
|
2105
|
+
serverRequestUnsubscribe = generic.onServerRequest((req) => handler(req));
|
|
2106
|
+
},
|
|
2107
|
+
/**
|
|
2108
|
+
* Close the client and cleanup resources
|
|
2109
|
+
*/
|
|
2110
|
+
close() {
|
|
2111
|
+
serverRequestUnsubscribe?.();
|
|
2112
|
+
serverRequestUnsubscribe = void 0;
|
|
2113
|
+
generic.close();
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
//#endregion
|
|
2119
|
+
//#region src/utils/createStdioTransport.ts
|
|
2120
|
+
/**
|
|
2121
|
+
* Resolve the command used to start the Codex app server.
|
|
2122
|
+
* @param binaryPath - Optional resolved managed binary path.
|
|
2123
|
+
* @returns Absolute managed binary path, or the PATH-resolved `codex` command.
|
|
2124
|
+
*/
|
|
2125
|
+
function resolveSpawnCommand(binaryPath) {
|
|
2126
|
+
if (binaryPath === void 0) return "codex";
|
|
2127
|
+
if (binaryPath.trim() === "" || !path.isAbsolute(binaryPath)) throw new Error("binaryPath must be a non-empty absolute path when provided");
|
|
2128
|
+
return binaryPath;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Creates a stdio transport for communicating with codex app-server subprocess
|
|
2132
|
+
* @param cwd - Working directory for the subprocess
|
|
2133
|
+
* @param env - Environment variables to pass to the subprocess (undefined values are filtered out)
|
|
2134
|
+
* @param binaryPath - Absolute path to the codex binary; when omitted, `'codex'` is resolved from PATH
|
|
2135
|
+
* @returns Transport interface for sending/receiving messages
|
|
2136
|
+
* @throws Error if subprocess fails to spawn
|
|
2137
|
+
* @example
|
|
2138
|
+
* ```ts
|
|
2139
|
+
* const transport = createStdioTransport('/path/to/project', { PATH: process.env.PATH });
|
|
2140
|
+
* transport.onMessage((message) => console.log('Received:', message));
|
|
2141
|
+
* transport.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
2142
|
+
* ```
|
|
2143
|
+
*/
|
|
2144
|
+
function createStdioTransport(cwd, env, binaryPath) {
|
|
2145
|
+
const subprocess = spawn(resolveSpawnCommand(binaryPath), ["app-server"], {
|
|
2146
|
+
cwd,
|
|
2147
|
+
env: {
|
|
2148
|
+
...Object.fromEntries(Object.entries(env).filter(([, value]) => value !== void 0)),
|
|
2149
|
+
RUST_LOG: "debug"
|
|
2150
|
+
},
|
|
2151
|
+
stdio: [
|
|
2152
|
+
"pipe",
|
|
2153
|
+
"pipe",
|
|
2154
|
+
"pipe"
|
|
2155
|
+
]
|
|
2156
|
+
});
|
|
2157
|
+
let messageCallback = null;
|
|
2158
|
+
let errorCallback = null;
|
|
2159
|
+
let buffer = "";
|
|
2160
|
+
/**
|
|
2161
|
+
* Parse JSONL from stdout and dispatch messages
|
|
2162
|
+
*/
|
|
2163
|
+
subprocess.stdout.on("data", (chunk) => {
|
|
2164
|
+
buffer += chunk.toString("utf-8");
|
|
2165
|
+
const lines = buffer.split("\n");
|
|
2166
|
+
buffer = lines.pop() || "";
|
|
2167
|
+
for (const line of lines) if (line.trim()) try {
|
|
2168
|
+
const message = JSON.parse(line);
|
|
2169
|
+
messageCallback?.(message);
|
|
2170
|
+
} catch (err) {
|
|
2171
|
+
const error = err instanceof Error ? err : /* @__PURE__ */ new Error(`Failed to parse JSONL: ${line}`);
|
|
2172
|
+
errorCallback?.(error);
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
subprocess.stderr.on("data", (chunk) => {
|
|
2176
|
+
console.warn("[codex app-server]", chunk.toString("utf-8"));
|
|
2177
|
+
});
|
|
2178
|
+
/**
|
|
2179
|
+
* Handle subprocess errors (e.g., codex not found)
|
|
2180
|
+
*/
|
|
2181
|
+
subprocess.on("error", (error) => {
|
|
2182
|
+
errorCallback?.(error);
|
|
2183
|
+
});
|
|
2184
|
+
/**
|
|
2185
|
+
* Handle subprocess exit
|
|
2186
|
+
*/
|
|
2187
|
+
subprocess.on("exit", (code) => {
|
|
2188
|
+
if (code !== 0) {
|
|
2189
|
+
const error = /* @__PURE__ */ new Error(`codex app-server exited with code ${code ?? "unknown"}`);
|
|
2190
|
+
errorCallback?.(error);
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
return {
|
|
2194
|
+
/**
|
|
2195
|
+
* Send a JSON-RPC message to the subprocess via stdin
|
|
2196
|
+
* @param message - JSON-RPC message to send (request, response, or notification)
|
|
2197
|
+
*/
|
|
2198
|
+
send(message) {
|
|
2199
|
+
subprocess.stdin.write(JSON.stringify(message) + "\n");
|
|
2200
|
+
},
|
|
2201
|
+
/**
|
|
2202
|
+
* Close the subprocess and cleanup resources
|
|
2203
|
+
*/
|
|
2204
|
+
close() {
|
|
2205
|
+
subprocess.kill();
|
|
2206
|
+
},
|
|
2207
|
+
/**
|
|
2208
|
+
* Register a callback for incoming messages
|
|
2209
|
+
* @param callback - Function to handle incoming messages
|
|
2210
|
+
*/
|
|
2211
|
+
onMessage(callback) {
|
|
2212
|
+
messageCallback = callback;
|
|
2213
|
+
},
|
|
2214
|
+
/**
|
|
2215
|
+
* Register a callback for errors
|
|
2216
|
+
* @param callback - Function to handle errors
|
|
2217
|
+
*/
|
|
2218
|
+
onError(callback) {
|
|
2219
|
+
errorCallback = callback;
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
//#endregion
|
|
2225
|
+
//#region src/connector/types.ts
|
|
2226
|
+
/**
|
|
2227
|
+
* Client info for JSON-RPC initialize handshake.
|
|
2228
|
+
*/
|
|
2229
|
+
const CLIENT_INFO = {
|
|
2230
|
+
name: "makaio-codex-app-server",
|
|
2231
|
+
title: "Makaio Codex App-Server",
|
|
2232
|
+
version: "0.1.0"
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
//#endregion
|
|
2236
|
+
//#region src/connector/connection-manager.ts
|
|
2237
|
+
/**
|
|
2238
|
+
* Connection lifecycle management for the Codex App-Server connector.
|
|
2239
|
+
*
|
|
2240
|
+
* Handles JSON-RPC client creation, subprocess spawning, credential resolution,
|
|
2241
|
+
* the ACP `initialize` handshake, and error-path teardown. The connector holds all
|
|
2242
|
+
* mutable state; this module mutates it only through the typed accessors in
|
|
2243
|
+
* {@link ConnectionManagerContext}.
|
|
2244
|
+
* @packageDocumentation
|
|
2245
|
+
*/
|
|
2246
|
+
/**
|
|
2247
|
+
* Create and attach the JSON-RPC client to the connector.
|
|
2248
|
+
*
|
|
2249
|
+
* If the context already has a client (from a test injection or a prior call),
|
|
2250
|
+
* only registers handlers and returns immediately. Otherwise, resolves credentials
|
|
2251
|
+
* and the managed binary path, spawns the subprocess via stdio transport, and
|
|
2252
|
+
* registers the error callback.
|
|
2253
|
+
* @param ctx - Connection manager context
|
|
2254
|
+
*/
|
|
2255
|
+
async function createClient(ctx) {
|
|
2256
|
+
if (ctx.getJsonRpcClient()) {
|
|
2257
|
+
ctx.registerClientHandlers();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const injected = ctx.getInjectedJsonRpcClient();
|
|
2261
|
+
if (injected) {
|
|
2262
|
+
ctx.setJsonRpcClient(injected);
|
|
2263
|
+
ctx.registerClientHandlers();
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
const { resolvedBinary, spawnEnv } = await resolveSessionEnvironment({
|
|
2267
|
+
bus: ctx.bus,
|
|
2268
|
+
providerContext: ctx.providerContext,
|
|
2269
|
+
clientId: ctx.clientId ?? "codex",
|
|
2270
|
+
baseEnv: ctx.env
|
|
2271
|
+
});
|
|
2272
|
+
const transport = ctx.getInjectedTransport() ?? createStdioTransport(ctx.cwd, spawnEnv, resolvedBinary?.binaryPath ?? void 0);
|
|
2273
|
+
if (!ctx.getInjectedTransport()) ctx.setOwnedTransport(transport);
|
|
2274
|
+
ctx.setJsonRpcClient(createJsonRpcClient(transport));
|
|
2275
|
+
transport.onError((error) => ctx.handleError(error, true));
|
|
2276
|
+
ctx.registerClientHandlers();
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Tear down the active JSON-RPC client and owned transport after a connection failure.
|
|
2280
|
+
*
|
|
2281
|
+
* Resets connection and handler-registration flags so the next `initializeConnection`
|
|
2282
|
+
* call starts fresh. If an injected test client is present it is restored (not closed)
|
|
2283
|
+
* so the test can continue using the same mock instance.
|
|
2284
|
+
* @param ctx - Connection manager context
|
|
2285
|
+
*/
|
|
2286
|
+
function resetClient(ctx) {
|
|
2287
|
+
ctx.setIsConnected(false);
|
|
2288
|
+
ctx.setClientHandlersRegistered(false);
|
|
2289
|
+
ctx.setDisabledNativeTools(/* @__PURE__ */ new Set());
|
|
2290
|
+
const transport = ctx.getOwnedTransport();
|
|
2291
|
+
ctx.setOwnedTransport(void 0);
|
|
2292
|
+
const injected = ctx.getInjectedJsonRpcClient();
|
|
2293
|
+
if (!injected) {
|
|
2294
|
+
ctx.getJsonRpcClient()?.close();
|
|
2295
|
+
if (!ctx.getJsonRpcClient() && transport) transport.close();
|
|
2296
|
+
ctx.setJsonRpcClient(void 0);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
ctx.setJsonRpcClient(injected);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Perform the full ACP `initialize` handshake in a single flight.
|
|
2303
|
+
*
|
|
2304
|
+
* Spawns the subprocess (or reuses an injected client), resolves disabled native
|
|
2305
|
+
* tools via the global harness bus, sends `initialize`, and fires `initialized`.
|
|
2306
|
+
* On any error the client is torn down via {@link resetClient} before re-throwing
|
|
2307
|
+
* so the next call starts from a clean state.
|
|
2308
|
+
* @param ctx - Connection manager context
|
|
2309
|
+
*/
|
|
2310
|
+
async function performConnectionInit(ctx) {
|
|
2311
|
+
try {
|
|
2312
|
+
await createClient(ctx);
|
|
2313
|
+
ctx.setDisabledNativeTools(new Set(await resolveDisabledNativeTools(MakaioBus, ctx.adapterName, ctx.harnessId, ctx.clientId)));
|
|
2314
|
+
const initParams = {
|
|
2315
|
+
clientInfo: CLIENT_INFO,
|
|
2316
|
+
capabilities: { experimentalApi: true }
|
|
2317
|
+
};
|
|
2318
|
+
const client = ctx.getJsonRpcClient();
|
|
2319
|
+
if (!client) throw new Error("JSON-RPC client not initialized");
|
|
2320
|
+
await client.request("initialize", initParams);
|
|
2321
|
+
client.notification("initialized", {});
|
|
2322
|
+
ctx.setIsConnected(true);
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
resetClient(ctx);
|
|
2325
|
+
throw error;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Ensure the connector is connected, using single-flight deduplication.
|
|
2330
|
+
*
|
|
2331
|
+
* Concurrent callers share the same in-flight promise so the subprocess is spawned
|
|
2332
|
+
* exactly once. Resolves immediately when already connected.
|
|
2333
|
+
* @param ctx - Connection manager context
|
|
2334
|
+
* @param inflight - Mutable holder for the in-flight promise; updated by this function
|
|
2335
|
+
* @returns Promise that resolves when the connection is established
|
|
2336
|
+
*/
|
|
2337
|
+
function initializeConnection(ctx, inflight) {
|
|
2338
|
+
if (ctx.getIsConnected()) return Promise.resolve();
|
|
2339
|
+
inflight.promise ??= performConnectionInit(ctx).finally(() => {
|
|
2340
|
+
inflight.promise = void 0;
|
|
2341
|
+
});
|
|
2342
|
+
return inflight.promise;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
//#endregion
|
|
2346
|
+
//#region src/thread.ts
|
|
2347
|
+
/**
|
|
2348
|
+
* Thread lifecycle management for Codex App-Server.
|
|
2349
|
+
*
|
|
2350
|
+
* Tracks thread state and emits namespace bus events:
|
|
2351
|
+
* - thread_started when thread/started notification received
|
|
2352
|
+
* - thread_completed when thread is archived or completed
|
|
2353
|
+
* - token_usage when thread/tokenUsage/updated received
|
|
2354
|
+
*
|
|
2355
|
+
* Thread is created by connector when thread/start response is received,
|
|
2356
|
+
* and updated throughout the conversation via notifications.
|
|
2357
|
+
*/
|
|
2358
|
+
var CodexAppServerThread = class {
|
|
2359
|
+
bus;
|
|
2360
|
+
adapterId;
|
|
2361
|
+
agentId;
|
|
2362
|
+
_threadId;
|
|
2363
|
+
_state = "active";
|
|
2364
|
+
_config = {};
|
|
2365
|
+
_tokenUsage;
|
|
2366
|
+
constructor(config) {
|
|
2367
|
+
this.bus = config.bus;
|
|
2368
|
+
this.adapterId = config.adapterId;
|
|
2369
|
+
this.agentId = config.agentId;
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Get the thread ID.
|
|
2373
|
+
* @returns Thread ID or undefined if not yet started
|
|
2374
|
+
*/
|
|
2375
|
+
get threadId() {
|
|
2376
|
+
return this._threadId;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get the current thread state.
|
|
2380
|
+
* @returns Current thread state
|
|
2381
|
+
*/
|
|
2382
|
+
get state() {
|
|
2383
|
+
return this._state;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Get the thread configuration.
|
|
2387
|
+
* @returns Thread configuration overrides
|
|
2388
|
+
*/
|
|
2389
|
+
get config() {
|
|
2390
|
+
return { ...this._config };
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Get the current token usage.
|
|
2394
|
+
* @returns Token usage or undefined if not yet received
|
|
2395
|
+
*/
|
|
2396
|
+
get tokenUsage() {
|
|
2397
|
+
return this._tokenUsage;
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Check if thread is active.
|
|
2401
|
+
* @returns True if thread is active
|
|
2402
|
+
*/
|
|
2403
|
+
isActive() {
|
|
2404
|
+
return this._state === "active";
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Check if thread is completed.
|
|
2408
|
+
* @returns True if thread is completed
|
|
2409
|
+
*/
|
|
2410
|
+
isCompleted() {
|
|
2411
|
+
return this._state === "completed";
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Handle thread/started notification.
|
|
2415
|
+
* Sets threadId and transitions to active state.
|
|
2416
|
+
* @param threadId - Thread ID from thread/started notification
|
|
2417
|
+
*/
|
|
2418
|
+
async handleThreadStarted(threadId) {
|
|
2419
|
+
this._threadId = threadId;
|
|
2420
|
+
this._state = "active";
|
|
2421
|
+
await this.bus.emit(CodexAppServerSubjects.thread_started, {
|
|
2422
|
+
agentId: this.agentId,
|
|
2423
|
+
threadId: this._threadId,
|
|
2424
|
+
timestamp: Date.now()
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Handle thread completion or archival.
|
|
2429
|
+
* Transitions state and emits completion event.
|
|
2430
|
+
* @param newState - New thread state (completed or archived)
|
|
2431
|
+
*/
|
|
2432
|
+
async handleThreadCompleted(newState = "completed") {
|
|
2433
|
+
if (!this._threadId) throw new Error("Cannot complete thread: threadId not set");
|
|
2434
|
+
this._state = newState;
|
|
2435
|
+
await this.bus.emit(CodexAppServerSubjects.thread_completed, {
|
|
2436
|
+
agentId: this.agentId,
|
|
2437
|
+
threadId: this._threadId,
|
|
2438
|
+
timestamp: Date.now()
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Handle thread/tokenUsage/updated notification.
|
|
2443
|
+
* Updates token usage tracking.
|
|
2444
|
+
* @param promptTokens - Prompt/input tokens used
|
|
2445
|
+
* @param inputCachedTokens - Cached input tokens reused for this update
|
|
2446
|
+
* @param completionTokens - Completion/output tokens used
|
|
2447
|
+
* @param reasoningTokens - Reasoning output tokens used (subset of completionTokens)
|
|
2448
|
+
* @param totalTokens - Full protocol-reported total for this update
|
|
2449
|
+
* @param modelContextWindow - Optional model context window size
|
|
2450
|
+
*/
|
|
2451
|
+
async handleTokenUsageUpdated(promptTokens, inputCachedTokens, completionTokens, reasoningTokens, totalTokens, modelContextWindow) {
|
|
2452
|
+
this._tokenUsage = {
|
|
2453
|
+
promptTokens,
|
|
2454
|
+
inputCachedTokens,
|
|
2455
|
+
completionTokens,
|
|
2456
|
+
reasoningTokens,
|
|
2457
|
+
totalTokens,
|
|
2458
|
+
modelContextWindow
|
|
2459
|
+
};
|
|
2460
|
+
await this.bus.emit(CodexAppServerSubjects.token_usage, {
|
|
2461
|
+
agentId: this.agentId,
|
|
2462
|
+
threadId: this._threadId ?? "",
|
|
2463
|
+
promptTokens,
|
|
2464
|
+
inputCachedTokens,
|
|
2465
|
+
completionTokens,
|
|
2466
|
+
reasoningTokens,
|
|
2467
|
+
totalTokens,
|
|
2468
|
+
modelContextWindow,
|
|
2469
|
+
timestamp: Date.now()
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Update thread configuration.
|
|
2474
|
+
* Used for per-turn overrides.
|
|
2475
|
+
* @param config - Configuration updates
|
|
2476
|
+
*/
|
|
2477
|
+
updateConfig(config) {
|
|
2478
|
+
this._config = {
|
|
2479
|
+
...this._config,
|
|
2480
|
+
...config
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2485
|
+
//#endregion
|
|
2486
|
+
//#region src/turn.ts
|
|
2487
|
+
/**
|
|
2488
|
+
* Codex App-Server Turn State Machine
|
|
2489
|
+
*
|
|
2490
|
+
* Manages turn lifecycle for the codex app-server protocol.
|
|
2491
|
+
* A turn is a single user input → agent response cycle within a thread.
|
|
2492
|
+
*
|
|
2493
|
+
* State machine follows the app-server protocol:
|
|
2494
|
+
* - idle → active (turn/start request sent)
|
|
2495
|
+
* - active → processing_started (turn/started notification received)
|
|
2496
|
+
* - processing_started → turn_started (first item/started notification)
|
|
2497
|
+
* - turn_started → step_started (command/file item starts)
|
|
2498
|
+
* - step_started → step_finished (item completes)
|
|
2499
|
+
* - step_finished → turn_finished (turn/completed received)
|
|
2500
|
+
* - turn_finished → idle (cleanup, ready for next turn)
|
|
2501
|
+
*
|
|
2502
|
+
* Immediate mode: active → interrupted → idle (cancelled, new turn with merged content)
|
|
2503
|
+
*/
|
|
2504
|
+
/**
|
|
2505
|
+
* Turn state machine for Codex App-Server.
|
|
2506
|
+
*
|
|
2507
|
+
* Tracks state transitions based on app-server notifications:
|
|
2508
|
+
* - turn/started → processing_started state
|
|
2509
|
+
* - item/started → step_started state
|
|
2510
|
+
* - item/completed → step_finished state
|
|
2511
|
+
* - turn/completed → turn_finished state
|
|
2512
|
+
*
|
|
2513
|
+
* Similar to codex-mcp but adapted for app-server protocol:
|
|
2514
|
+
* - Uses turnId instead of taskId
|
|
2515
|
+
* - Item-based events instead of MCP event types
|
|
2516
|
+
* - Supports interrupt via turn/interrupt method
|
|
2517
|
+
*/
|
|
2518
|
+
var CodexAppServerTurn = class extends BaseConnectorTurn {
|
|
2519
|
+
connectorBus;
|
|
2520
|
+
agentId;
|
|
2521
|
+
threadId;
|
|
2522
|
+
activeMessageHandle;
|
|
2523
|
+
turnId;
|
|
2524
|
+
currentItemId;
|
|
2525
|
+
currentItemType;
|
|
2526
|
+
interrupted = false;
|
|
2527
|
+
constructor(bus, adapterId, adapterName, agentId, threadId, messageHandle) {
|
|
2528
|
+
super(bus, adapterId, adapterName, "idle");
|
|
2529
|
+
this.connectorBus = bus;
|
|
2530
|
+
this.agentId = agentId;
|
|
2531
|
+
this.threadId = threadId;
|
|
2532
|
+
this.activeMessageHandle = messageHandle;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Get the turn ID.
|
|
2536
|
+
* @returns Turn ID or undefined if not yet received from turn/started
|
|
2537
|
+
*/
|
|
2538
|
+
getTurnId() {
|
|
2539
|
+
return this.turnId;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Set the turn ID from turn/started notification.
|
|
2543
|
+
* @param turnId - Turn ID from the app-server
|
|
2544
|
+
*/
|
|
2545
|
+
setTurnId(turnId) {
|
|
2546
|
+
this.turnId = turnId;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Get the current item ID being processed.
|
|
2550
|
+
* @returns Current item ID or undefined
|
|
2551
|
+
*/
|
|
2552
|
+
getCurrentItemId() {
|
|
2553
|
+
return this.currentItemId;
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Emit state change using typed namespace subjects.
|
|
2557
|
+
* @param oldState - The previous turn state
|
|
2558
|
+
* @param newState - The new turn state
|
|
2559
|
+
*/
|
|
2560
|
+
async emitStateChange(oldState, newState) {
|
|
2561
|
+
const payload = {
|
|
2562
|
+
adapterId: this.adapterId,
|
|
2563
|
+
agentId: this.agentId,
|
|
2564
|
+
oldState,
|
|
2565
|
+
newState,
|
|
2566
|
+
timestamp: Date.now()
|
|
2567
|
+
};
|
|
2568
|
+
await this.connectorBus.emit(CodexAppServerSubjects.turn_state_changed, payload);
|
|
2569
|
+
switch (newState) {
|
|
2570
|
+
case "turn_started":
|
|
2571
|
+
await this.connectorBus.emit(CodexAppServerSubjects.turn_started, {
|
|
2572
|
+
agentId: this.agentId,
|
|
2573
|
+
threadId: this.threadId,
|
|
2574
|
+
turnId: this.turnId ?? "",
|
|
2575
|
+
timestamp: Date.now()
|
|
2576
|
+
});
|
|
2577
|
+
break;
|
|
2578
|
+
case "step_started":
|
|
2579
|
+
await this.connectorBus.emit(CodexAppServerSubjects.turn_step_started, {
|
|
2580
|
+
agentId: this.agentId,
|
|
2581
|
+
threadId: this.threadId,
|
|
2582
|
+
turnId: this.turnId ?? "",
|
|
2583
|
+
itemId: this.currentItemId ?? "",
|
|
2584
|
+
timestamp: Date.now()
|
|
2585
|
+
});
|
|
2586
|
+
break;
|
|
2587
|
+
case "step_finished":
|
|
2588
|
+
await this.connectorBus.emit(CodexAppServerSubjects.turn_step_finished, {
|
|
2589
|
+
agentId: this.agentId,
|
|
2590
|
+
threadId: this.threadId,
|
|
2591
|
+
turnId: this.turnId ?? "",
|
|
2592
|
+
itemId: this.currentItemId ?? "",
|
|
2593
|
+
timestamp: Date.now()
|
|
2594
|
+
});
|
|
2595
|
+
break;
|
|
2596
|
+
case "turn_finished":
|
|
2597
|
+
await this.connectorBus.emit(CodexAppServerSubjects.turn_completed, {
|
|
2598
|
+
agentId: this.agentId,
|
|
2599
|
+
threadId: this.threadId,
|
|
2600
|
+
turnId: this.turnId ?? "",
|
|
2601
|
+
timestamp: Date.now()
|
|
2602
|
+
});
|
|
2603
|
+
break;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Start the turn - transition to active state.
|
|
2608
|
+
* Called when turn/start request is sent.
|
|
2609
|
+
*/
|
|
2610
|
+
async start() {
|
|
2611
|
+
await this.transitionTo("active");
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Handle turn/started notification.
|
|
2615
|
+
* Transitions from active to processing_started.
|
|
2616
|
+
* @param turnId - Turn ID from the notification
|
|
2617
|
+
*/
|
|
2618
|
+
async handleTurnStarted(turnId) {
|
|
2619
|
+
this.turnId = turnId;
|
|
2620
|
+
await this.transitionTo("processing_started");
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Handle item/started notification.
|
|
2624
|
+
* Transitions from processing_started to turn_started (first item)
|
|
2625
|
+
* or from step_finished to step_started (subsequent items).
|
|
2626
|
+
* @param itemId - Item ID from the notification
|
|
2627
|
+
* @param itemType - Type of item (agentMessage, commandExecution, etc.)
|
|
2628
|
+
*/
|
|
2629
|
+
async handleItemStarted(itemId, itemType) {
|
|
2630
|
+
this.currentItemId = itemId;
|
|
2631
|
+
this.currentItemType = itemType;
|
|
2632
|
+
if (this.state === "processing_started") {
|
|
2633
|
+
await this.transitionTo("turn_started");
|
|
2634
|
+
await this.transitionTo("step_started");
|
|
2635
|
+
} else if (this.state === "step_finished") await this.transitionTo("step_started");
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Handle item/completed notification.
|
|
2639
|
+
* Transitions from step_started to step_finished.
|
|
2640
|
+
* @param itemId - Item ID from the notification
|
|
2641
|
+
*/
|
|
2642
|
+
async handleItemCompleted(itemId) {
|
|
2643
|
+
if (this.currentItemId !== itemId) return;
|
|
2644
|
+
if (this.state === "step_started") await this.transitionTo("step_finished");
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Handle turn/completed notification.
|
|
2648
|
+
* Transitions from step_finished to turn_finished.
|
|
2649
|
+
*/
|
|
2650
|
+
async handleTurnCompleted() {
|
|
2651
|
+
if (this.state === "step_finished" || this.state === "turn_started") await this.transitionTo("turn_finished");
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Mark turn as finished.
|
|
2655
|
+
* Called by connector when turn completes.
|
|
2656
|
+
*/
|
|
2657
|
+
async markTurnFinished() {
|
|
2658
|
+
await this.transitionTo("turn_finished");
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Interrupt (pause) the turn.
|
|
2662
|
+
* Used for immediate mode - sends turn/interrupt and restarts with merged content.
|
|
2663
|
+
* @returns Pause result indicating whether turn had already ended
|
|
2664
|
+
*/
|
|
2665
|
+
async pause() {
|
|
2666
|
+
if (this.state === "turn_finished") return {
|
|
2667
|
+
stateBeforePause: this.state,
|
|
2668
|
+
turnEnded: true
|
|
2669
|
+
};
|
|
2670
|
+
const stateBeforePause = this.state;
|
|
2671
|
+
this.interrupted = true;
|
|
2672
|
+
return {
|
|
2673
|
+
stateBeforePause,
|
|
2674
|
+
turnEnded: false
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Resume is a no-op for app-server - caller creates new turn instead.
|
|
2679
|
+
* App-server doesn't support true resume - use interrupt + restart with merged content.
|
|
2680
|
+
* @param _message - Optional message (unused - app-server doesn't support resume)
|
|
2681
|
+
* @throws Error always - app-server requires creating a new turn with merged content
|
|
2682
|
+
*/
|
|
2683
|
+
async resume(_message) {
|
|
2684
|
+
throw new Error("Codex app-server does not support resume - create new turn with merged content");
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Check if turn was interrupted.
|
|
2688
|
+
* @returns True if turn was interrupted
|
|
2689
|
+
*/
|
|
2690
|
+
isPaused() {
|
|
2691
|
+
return this.interrupted;
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Check if turn is completed.
|
|
2695
|
+
* @returns True if turn has finished
|
|
2696
|
+
*/
|
|
2697
|
+
isCompleted() {
|
|
2698
|
+
return this.state === "turn_finished";
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Check if turn can accept immediate message.
|
|
2702
|
+
* True if turn is active (not finished, not interrupted).
|
|
2703
|
+
* @returns True if turn can accept an immediate message
|
|
2704
|
+
*/
|
|
2705
|
+
canAcceptImmediate() {
|
|
2706
|
+
return this.state !== "turn_finished" && !this.interrupted;
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
|
|
2710
|
+
//#endregion
|
|
2711
|
+
//#region src/utils/attachmentHelpers.ts
|
|
2712
|
+
/**
|
|
2713
|
+
* Helpers for converting message blocks to Codex `UserInput` items.
|
|
2714
|
+
*
|
|
2715
|
+
* Kept separate from the connector to respect its max-lines budget and to
|
|
2716
|
+
* allow independent testing of the conversion logic.
|
|
2717
|
+
*/
|
|
2718
|
+
/**
|
|
2719
|
+
* Decode a base64-encoded string to UTF-8 text.
|
|
2720
|
+
*
|
|
2721
|
+
* Uses Web-standard `TextDecoder`/`atob` so this works in both browser and
|
|
2722
|
+
* Node.js (16+) environments. Returns an empty string on malformed input
|
|
2723
|
+
* rather than propagating the `DOMException` thrown by `atob`.
|
|
2724
|
+
* @param data - Base64-encoded string.
|
|
2725
|
+
* @returns Decoded UTF-8 string, or `''` if `data` is not valid base64.
|
|
2726
|
+
*/
|
|
2727
|
+
function decodeBase64Text(data) {
|
|
2728
|
+
try {
|
|
2729
|
+
return new TextDecoder().decode(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)));
|
|
2730
|
+
} catch {
|
|
2731
|
+
return "";
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Return true when a MIME type represents plain-text or text-like content
|
|
2736
|
+
* that Codex can consume as a text input rather than a binary blob.
|
|
2737
|
+
* @param mimeType - MIME type string to test, or `undefined`.
|
|
2738
|
+
* @returns `true` when the MIME type is considered text-based.
|
|
2739
|
+
*/
|
|
2740
|
+
function isTextMimeType(mimeType) {
|
|
2741
|
+
if (!mimeType) return false;
|
|
2742
|
+
if (mimeType.startsWith("text/")) return true;
|
|
2743
|
+
return [
|
|
2744
|
+
"application/json",
|
|
2745
|
+
"application/xml",
|
|
2746
|
+
"application/sql",
|
|
2747
|
+
"application/graphql",
|
|
2748
|
+
"application/javascript",
|
|
2749
|
+
"application/typescript",
|
|
2750
|
+
"application/x-yaml",
|
|
2751
|
+
"application/toml"
|
|
2752
|
+
].includes(mimeType);
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Convert a single {@link MessageBlock} to a Codex {@link UserInput}, or
|
|
2756
|
+
* `null` when the block type has no Codex representation (e.g. tool_call).
|
|
2757
|
+
*
|
|
2758
|
+
* Dispatch rules:
|
|
2759
|
+
* - `text` → `{ type: 'text' }`
|
|
2760
|
+
* - `image` with URL source → `{ type: 'image' }`
|
|
2761
|
+
* - `image` with base64 source → `null` (unsupported; Codex only accepts local paths via attachment blocks)
|
|
2762
|
+
* - `attachment` with image MIME → `{ type: 'localImage' }` (uses `filePath`)
|
|
2763
|
+
* - `attachment` with text-like MIME → decoded `{ type: 'text' }`
|
|
2764
|
+
* - `attachment` otherwise → `{ type: 'text' }` placeholder via `serializeBlockToText`
|
|
2765
|
+
* - all other block types → `null` (skipped)
|
|
2766
|
+
* @param block - The normalized message block to convert.
|
|
2767
|
+
* @returns A Codex `UserInput` or `null` if the block should be skipped.
|
|
2768
|
+
*/
|
|
2769
|
+
function convertBlockToUserInput(block) {
|
|
2770
|
+
if (block.type === "text") return {
|
|
2771
|
+
type: "text",
|
|
2772
|
+
text: block.content
|
|
2773
|
+
};
|
|
2774
|
+
if (block.type === "image" && block.source.type === "url") return {
|
|
2775
|
+
type: "image",
|
|
2776
|
+
url: block.source.url
|
|
2777
|
+
};
|
|
2778
|
+
if (block.type === "image" && block.source.type === "base64") return null;
|
|
2779
|
+
if (block.type === "attachment") {
|
|
2780
|
+
const mimeType = block.source.type === "base64" ? block.source.mimeType : void 0;
|
|
2781
|
+
if (mimeType?.startsWith("image/")) return {
|
|
2782
|
+
type: "localImage",
|
|
2783
|
+
path: block.filePath
|
|
2784
|
+
};
|
|
2785
|
+
if (isTextMimeType(mimeType) && block.source.type === "base64") return {
|
|
2786
|
+
type: "text",
|
|
2787
|
+
text: decodeBase64Text(block.source.data)
|
|
2788
|
+
};
|
|
2789
|
+
return {
|
|
2790
|
+
type: "text",
|
|
2791
|
+
text: serializeBlockToText(block)
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
//#endregion
|
|
2798
|
+
//#region src/utils/formatMessageHistory.ts
|
|
2799
|
+
/**
|
|
2800
|
+
* Format message history as a prompt prefix for codex app-server.
|
|
2801
|
+
*
|
|
2802
|
+
* Since the codex app-server protocol's turn/start only accepts user input,
|
|
2803
|
+
* we serialize the message history into a human-readable format that
|
|
2804
|
+
* the AI can understand as conversation context.
|
|
2805
|
+
*/
|
|
2806
|
+
/**
|
|
2807
|
+
* Format curated message history as a text prefix.
|
|
2808
|
+
*
|
|
2809
|
+
* Serializes the history into a format the AI can understand as prior context.
|
|
2810
|
+
* Uses role labels to distinguish different message types.
|
|
2811
|
+
* @param history - Curated messages from sessionContext.messageHistory
|
|
2812
|
+
* @returns Formatted string to prepend to the prompt, or empty string if no history
|
|
2813
|
+
*/
|
|
2814
|
+
function formatMessageHistory(history) {
|
|
2815
|
+
if (!history || history.length === 0) return "";
|
|
2816
|
+
return `${formatMessageHistoryAsTranscript(history)}\n\n`;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
//#endregion
|
|
2820
|
+
//#region src/utils/buildUserInputs.ts
|
|
2821
|
+
/**
|
|
2822
|
+
* User input assembly utilities for Codex App-Server turn/start.
|
|
2823
|
+
*
|
|
2824
|
+
* Builds the `input` array expected by `turn/start` from a normalized
|
|
2825
|
+
* `MessageHandle`, inserting history, merged context, turn context, and
|
|
2826
|
+
* content blocks in the correct order.
|
|
2827
|
+
*/
|
|
2828
|
+
/**
|
|
2829
|
+
* Assemble the `input` array for a `turn/start` request from a message handle.
|
|
2830
|
+
*
|
|
2831
|
+
* Ordering: outermost → innermost is
|
|
2832
|
+
* `message_history` → `merged_context` → `turn_context` → user text and blocks.
|
|
2833
|
+
* @param messageHandle - The message handle carrying user input and context
|
|
2834
|
+
* @param mergedContent - Optional array of prior message text merged into this turn
|
|
2835
|
+
* @returns Ordered array of `UserInput` items for `turn/start`
|
|
2836
|
+
*/
|
|
2837
|
+
function buildUserInputs(messageHandle, mergedContent) {
|
|
2838
|
+
const messageContent = messageHandle.message;
|
|
2839
|
+
const userInputs = [];
|
|
2840
|
+
const historyPrefix = formatMessageHistory(messageHandle.messageHistory);
|
|
2841
|
+
if (historyPrefix) userInputs.push({
|
|
2842
|
+
type: "text",
|
|
2843
|
+
text: `[CONVERSATION HISTORY]\n${historyPrefix}[END CONVERSATION HISTORY]\n\n`
|
|
2844
|
+
});
|
|
2845
|
+
if (mergedContent && mergedContent.length > 0) {
|
|
2846
|
+
const mergedText = `[MERGED CONTEXT FROM PREVIOUS MESSAGES]\n${mergedContent.join("\n")}\n[END MERGED CONTEXT]\n\n`;
|
|
2847
|
+
userInputs.push({
|
|
2848
|
+
type: "text",
|
|
2849
|
+
text: mergedText
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
const contextText = formatContextBlocksAsText(serializeTurnContext(messageHandle.turnContext));
|
|
2853
|
+
if (contextText) userInputs.push({
|
|
2854
|
+
type: "text",
|
|
2855
|
+
text: contextText
|
|
2856
|
+
});
|
|
2857
|
+
for (const block of messageContent.blocks) {
|
|
2858
|
+
const input = convertBlockToUserInput(block);
|
|
2859
|
+
if (input) userInputs.push(input);
|
|
2860
|
+
}
|
|
2861
|
+
if (userInputs.length === 0 && messageContent.message) userInputs.push({
|
|
2862
|
+
type: "text",
|
|
2863
|
+
text: messageContent.message
|
|
2864
|
+
});
|
|
2865
|
+
return userInputs;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
//#endregion
|
|
2869
|
+
//#region src/connector/turn-flow-handlers.ts
|
|
2870
|
+
/**
|
|
2871
|
+
* Maps canonical {@link AIReasoningLevel} values to Codex protocol {@link CodexReasoningEffort} strings.
|
|
2872
|
+
* @param level - Canonical reasoning level (must not be `'none'`)
|
|
2873
|
+
* @returns Codex-native effort string
|
|
2874
|
+
*/
|
|
2875
|
+
function mapToCodexEffort(level) {
|
|
2876
|
+
return level === "extra-high" ? "xhigh" : level;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Launch the ACP `thread/start` request, await the corresponding `thread/started`
|
|
2880
|
+
* notification, and populate the connector's `adapterSessionId`.
|
|
2881
|
+
*
|
|
2882
|
+
* A deferred promise is created before the request so the notification handler
|
|
2883
|
+
* can resolve it even if it fires before `await threadStartedPromise` is reached.
|
|
2884
|
+
* On error the deferred is cleared to prevent `getAdapterSessionId()` from hanging.
|
|
2885
|
+
* @param ctx - Turn flow context
|
|
2886
|
+
*/
|
|
2887
|
+
async function startThread(ctx) {
|
|
2888
|
+
let resolve;
|
|
2889
|
+
const threadStartedPromise = new Promise((res) => {
|
|
2890
|
+
resolve = res;
|
|
2891
|
+
});
|
|
2892
|
+
ctx.setThreadStartedDeferred({
|
|
2893
|
+
promise: threadStartedPromise,
|
|
2894
|
+
resolve
|
|
2895
|
+
});
|
|
2896
|
+
try {
|
|
2897
|
+
const dynamicTools = await fetchToolsForCodex(ctx.adapterId, ctx.adapterName);
|
|
2898
|
+
const threadStartParams = {
|
|
2899
|
+
model: ctx.getModel() ?? null,
|
|
2900
|
+
modelProvider: null,
|
|
2901
|
+
cwd: ctx.cwd ?? null,
|
|
2902
|
+
approvalPolicy: ctx.getApprovalPolicy() ?? null,
|
|
2903
|
+
sandbox: ctx.getSandboxMode() ?? null,
|
|
2904
|
+
config: null,
|
|
2905
|
+
baseInstructions: ctx.resolveSystemPrompt(),
|
|
2906
|
+
developerInstructions: null,
|
|
2907
|
+
experimentalRawEvents: false,
|
|
2908
|
+
dynamicTools: dynamicTools.length > 0 ? dynamicTools : void 0
|
|
2909
|
+
};
|
|
2910
|
+
await ctx.getJsonRpcClient().request("thread/start", threadStartParams);
|
|
2911
|
+
const threadId = await threadStartedPromise;
|
|
2912
|
+
ctx.setAdapterSessionId(threadId);
|
|
2913
|
+
ctx.setThreadStartedDeferred(void 0);
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
ctx.setThreadStartedDeferred(void 0);
|
|
2916
|
+
throw error;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Send a single `turn/start` request and wire the active turn.
|
|
2921
|
+
*
|
|
2922
|
+
* Creates a new {@link CodexAppServerTurn}, assigns it as the active turn, resets
|
|
2923
|
+
* the message accumulator, and fires the ACP `turn/start` request with the
|
|
2924
|
+
* current model and reasoning effort.
|
|
2925
|
+
* @param ctx - Turn flow context
|
|
2926
|
+
* @param messageHandle - Handle for the message being dispatched
|
|
2927
|
+
* @param mergedContent - Optional content lines merged from superseded handles
|
|
2928
|
+
*/
|
|
2929
|
+
async function startTurn(ctx, messageHandle, mergedContent) {
|
|
2930
|
+
const thread = ctx.getThread();
|
|
2931
|
+
if (!thread?.threadId) throw new Error("Cannot start turn: thread not started");
|
|
2932
|
+
ctx.setCurrentTurn(new CodexAppServerTurn(ctx.bus, ctx.adapterId, ctx.adapterName, ctx.agentId, thread.threadId, messageHandle));
|
|
2933
|
+
ctx.setPendingMessageHandle(messageHandle);
|
|
2934
|
+
ctx.setAgentMessageContent("");
|
|
2935
|
+
await ctx.getCurrentTurn().start();
|
|
2936
|
+
const userInputs = buildUserInputs(messageHandle, mergedContent);
|
|
2937
|
+
const effort = ctx.getReasoningEffort();
|
|
2938
|
+
await ctx.getJsonRpcClient().request("turn/start", {
|
|
2939
|
+
threadId: thread.threadId,
|
|
2940
|
+
input: userInputs,
|
|
2941
|
+
cwd: null,
|
|
2942
|
+
approvalPolicy: null,
|
|
2943
|
+
sandboxPolicy: null,
|
|
2944
|
+
model: ctx.getModel() ?? null,
|
|
2945
|
+
effort: effort !== void 0 && effort !== "none" ? mapToCodexEffort(effort) : null,
|
|
2946
|
+
summary: null,
|
|
2947
|
+
outputSchema: null
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Drain the message queue, dispatching the next eligible message as a new turn.
|
|
2952
|
+
*
|
|
2953
|
+
* Late-arriving `immediate` messages (arriving after a turn has already completed)
|
|
2954
|
+
* are rejected and the queue is re-drained to unblock any remaining entries.
|
|
2955
|
+
* @param ctx - Turn flow context
|
|
2956
|
+
*/
|
|
2957
|
+
async function processQueue(ctx) {
|
|
2958
|
+
if (ctx.getCurrentTurn() && !ctx.getCurrentTurn().isCompleted()) return;
|
|
2959
|
+
const nextMessage = ctx.messageQueue.peek();
|
|
2960
|
+
if (!nextMessage) return;
|
|
2961
|
+
if (nextMessage.deliveryMode === "immediate" && ctx.getLastResult() !== null) {
|
|
2962
|
+
ctx.messageQueue.dequeue();
|
|
2963
|
+
nextMessage.markCompleted({ outcome: "rejected" });
|
|
2964
|
+
await processQueue(ctx);
|
|
2965
|
+
if (!ctx.getCurrentTurn() && ctx.messageQueue.isEmpty()) await ctx.updateProcessingState("idle");
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const message = ctx.messageQueue.dequeue();
|
|
2969
|
+
if (!message) return;
|
|
2970
|
+
await startTurn(ctx, message);
|
|
2971
|
+
if (!ctx.messageQueue.isEmpty()) await processQueue(ctx);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Handle the `thread/started` notification from the Codex server.
|
|
2975
|
+
*
|
|
2976
|
+
* Resolves the deferred thread-started promise, sets the adapter session ID,
|
|
2977
|
+
* creates and registers the {@link CodexAppServerThread}, and fires the thread
|
|
2978
|
+
* lifecycle handler.
|
|
2979
|
+
* @param ctx - Turn flow context
|
|
2980
|
+
* @param notification - Parsed notification payload
|
|
2981
|
+
*/
|
|
2982
|
+
async function onThreadStarted(ctx, notification) {
|
|
2983
|
+
const threadId = extractThreadId(notification);
|
|
2984
|
+
ctx.setAdapterSessionId(threadId);
|
|
2985
|
+
ctx.getThreadStartedDeferred()?.resolve(threadId);
|
|
2986
|
+
ctx.setThread(new CodexAppServerThread({
|
|
2987
|
+
bus: ctx.bus,
|
|
2988
|
+
adapterId: ctx.adapterId,
|
|
2989
|
+
agentId: ctx.agentId
|
|
2990
|
+
}));
|
|
2991
|
+
await ctx.getThread().handleThreadStarted(threadId);
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Handle the `turn/completed` notification from the Codex server.
|
|
2995
|
+
*
|
|
2996
|
+
* Manages the superseded-message / merged-message fast path (when an `immediate`
|
|
2997
|
+
* message is waiting), then settles the pending handle with its outcome. Drains
|
|
2998
|
+
* the queue or transitions to idle when no further messages are queued.
|
|
2999
|
+
* @param ctx - Turn flow context
|
|
3000
|
+
* @param _notification - Parsed notification payload (currently unused)
|
|
3001
|
+
*/
|
|
3002
|
+
async function onTurnCompleted(ctx, _notification) {
|
|
3003
|
+
if (!ctx.getCurrentTurn()) return;
|
|
3004
|
+
await ctx.getCurrentTurn().handleTurnCompleted();
|
|
3005
|
+
const immediateMsg = ctx.messageQueue.findImmediate();
|
|
3006
|
+
if (immediateMsg && ctx.getPendingMessageHandle()) {
|
|
3007
|
+
ctx.messageQueue.removeImmediate(immediateMsg);
|
|
3008
|
+
const mergedContent = [];
|
|
3009
|
+
const currentHandle = ctx.getPendingMessageHandle();
|
|
3010
|
+
if (currentHandle.message.message) mergedContent.push(currentHandle.message.message);
|
|
3011
|
+
currentHandle.supersededBy = immediateMsg.messageId;
|
|
3012
|
+
currentHandle.markCompleted({
|
|
3013
|
+
outcome: "superseded",
|
|
3014
|
+
supersededBy: immediateMsg.messageId
|
|
3015
|
+
});
|
|
3016
|
+
const enqueuedHandles = ctx.messageQueue.drainEnqueued();
|
|
3017
|
+
for (const handle of enqueuedHandles) {
|
|
3018
|
+
if (handle.message.message) mergedContent.push(handle.message.message);
|
|
3019
|
+
handle.supersededBy = immediateMsg.messageId;
|
|
3020
|
+
handle.markCompleted({
|
|
3021
|
+
outcome: "merged",
|
|
3022
|
+
mergedInto: immediateMsg.messageId
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
ctx.setLastResult({
|
|
3026
|
+
outcome: "superseded",
|
|
3027
|
+
supersededBy: immediateMsg.messageId
|
|
3028
|
+
});
|
|
3029
|
+
ctx.setPendingMessageHandle(void 0);
|
|
3030
|
+
ctx.setCurrentTurn(void 0);
|
|
3031
|
+
await ctx.updateProcessingState("turn_finished");
|
|
3032
|
+
await ctx.updateProcessingState("processing_finished");
|
|
3033
|
+
await startTurn(ctx, immediateMsg, mergedContent);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
const pendingHandle = ctx.getPendingMessageHandle();
|
|
3037
|
+
if (pendingHandle) {
|
|
3038
|
+
const turnId = ctx.getCurrentTurn()?.getTurnId();
|
|
3039
|
+
const threadId = ctx.getThread()?.threadId;
|
|
3040
|
+
const agentMessageContent = ctx.getAgentMessageContent();
|
|
3041
|
+
if (agentMessageContent && threadId && turnId) await ctx.emit(CodexAppServerSubjects.agent_message, {
|
|
3042
|
+
threadId,
|
|
3043
|
+
turnId,
|
|
3044
|
+
message: agentMessageContent,
|
|
3045
|
+
timestamp: Date.now()
|
|
3046
|
+
});
|
|
3047
|
+
const result = {
|
|
3048
|
+
outcome: "completed",
|
|
3049
|
+
result: { message: agentMessageContent || "(Empty response)" }
|
|
3050
|
+
};
|
|
3051
|
+
pendingHandle.markCompleted(result);
|
|
3052
|
+
ctx.setLastResult(result);
|
|
3053
|
+
ctx.setPendingMessageHandle(void 0);
|
|
3054
|
+
}
|
|
3055
|
+
ctx.setCurrentTurn(void 0);
|
|
3056
|
+
await ctx.updateProcessingState("turn_finished");
|
|
3057
|
+
await ctx.updateProcessingState("processing_finished");
|
|
3058
|
+
if (!ctx.messageQueue.isEmpty()) await processQueue(ctx);
|
|
3059
|
+
else await ctx.updateProcessingState("idle");
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
//#endregion
|
|
3063
|
+
//#region src/connector/connector.ts
|
|
3064
|
+
var CodexAppServerConnector = class extends AIAgentConnector {
|
|
3065
|
+
/** Lazy: initialised in initializeConnection(); public methods access via getJsonRpcClient(). */
|
|
3066
|
+
jsonRpcClient;
|
|
3067
|
+
/** Injected transport for tests; null means "create from subprocess on first connect". */
|
|
3068
|
+
_injectedTransport;
|
|
3069
|
+
/** Injected JSON-RPC client for tests; bypasses subprocess creation entirely. */
|
|
3070
|
+
_injectedJsonRpcClient;
|
|
3071
|
+
/** Transport owned by this connector and safe to tear down after partial startup failures. */
|
|
3072
|
+
ownedTransport;
|
|
3073
|
+
messageQueue = new UserMessageQueue();
|
|
3074
|
+
thread;
|
|
3075
|
+
currentTurn;
|
|
3076
|
+
isConnected = false;
|
|
3077
|
+
/** In-flight connection initialization promise for single-flight deduplication. */
|
|
3078
|
+
initConnectionInflight = { promise: void 0 };
|
|
3079
|
+
/** Handlers are registered once per client instance to avoid duplicate listeners on retries. */
|
|
3080
|
+
clientHandlersRegistered = false;
|
|
3081
|
+
isTerminated = false;
|
|
3082
|
+
agentMessageContent = "";
|
|
3083
|
+
notificationQueue = Promise.resolve();
|
|
3084
|
+
threadStartedDeferred;
|
|
3085
|
+
_approvalPolicy;
|
|
3086
|
+
_sandboxMode;
|
|
3087
|
+
_reasoningEffort;
|
|
3088
|
+
commandExecutionByItemId = /* @__PURE__ */ new Map();
|
|
3089
|
+
dynamicToolCallByItemId = /* @__PURE__ */ new Map();
|
|
3090
|
+
/** Pending resolvers for {@link waitForCommandInfo}, keyed by itemId. */
|
|
3091
|
+
commandInfoWaiters = /* @__PURE__ */ new Map();
|
|
3092
|
+
disabledNativeTools = /* @__PURE__ */ new Set();
|
|
3093
|
+
/**
|
|
3094
|
+
* Stable context object passed to connection-manager module functions.
|
|
3095
|
+
* All state access is via closures over `this` so the object is never stale.
|
|
3096
|
+
*/
|
|
3097
|
+
connCtx;
|
|
3098
|
+
/**
|
|
3099
|
+
* Stable context object passed to turn-flow-handlers module functions.
|
|
3100
|
+
* All state access is via closures over `this` so the object is never stale.
|
|
3101
|
+
*/
|
|
3102
|
+
turnCtx;
|
|
3103
|
+
constructor(config) {
|
|
3104
|
+
super({
|
|
3105
|
+
bus: config.bus,
|
|
3106
|
+
adapterId: config.adapterId,
|
|
3107
|
+
adapterName: config.adapterName ?? "codex-app-server",
|
|
3108
|
+
agentId: config.agentId,
|
|
3109
|
+
model: config.model,
|
|
3110
|
+
cwd: config.cwd,
|
|
3111
|
+
env: config.env,
|
|
3112
|
+
onMessageSent: config.onMessageSent,
|
|
3113
|
+
toolLedger: config.toolLedger,
|
|
3114
|
+
reasoningEffort: config.reasoningEffort,
|
|
3115
|
+
clientId: config.clientId,
|
|
3116
|
+
harnessId: config.harnessId,
|
|
3117
|
+
providerContext: config.providerContext
|
|
3118
|
+
});
|
|
3119
|
+
const fullConfig = config;
|
|
3120
|
+
this._approvalPolicy = fullConfig.providerConfig?.approvalPolicy ?? fullConfig.approvalPolicy;
|
|
3121
|
+
this._sandboxMode = fullConfig.providerConfig?.sandboxMode ?? fullConfig.sandboxMode;
|
|
3122
|
+
this._reasoningEffort = fullConfig.providerConfig?.reasoningEffort ?? fullConfig.reasoningEffort;
|
|
3123
|
+
this.currentReasoningEffort = this._reasoningEffort;
|
|
3124
|
+
this._injectedJsonRpcClient = config.jsonRpcClient;
|
|
3125
|
+
this._injectedTransport = config.transport;
|
|
3126
|
+
if (config.jsonRpcClient) this.jsonRpcClient = config.jsonRpcClient;
|
|
3127
|
+
this.connCtx = this.buildConnectionContext();
|
|
3128
|
+
this.turnCtx = this.buildTurnFlowContext();
|
|
3129
|
+
}
|
|
3130
|
+
buildConnectionContext() {
|
|
3131
|
+
return {
|
|
3132
|
+
getJsonRpcClient: () => this.jsonRpcClient,
|
|
3133
|
+
setJsonRpcClient: (client) => {
|
|
3134
|
+
this.jsonRpcClient = client;
|
|
3135
|
+
},
|
|
3136
|
+
getInjectedJsonRpcClient: () => this._injectedJsonRpcClient,
|
|
3137
|
+
getInjectedTransport: () => this._injectedTransport,
|
|
3138
|
+
getOwnedTransport: () => this.ownedTransport,
|
|
3139
|
+
setOwnedTransport: (transport) => {
|
|
3140
|
+
this.ownedTransport = transport;
|
|
3141
|
+
},
|
|
3142
|
+
getIsConnected: () => this.isConnected,
|
|
3143
|
+
setIsConnected: (value) => {
|
|
3144
|
+
this.isConnected = value;
|
|
3145
|
+
},
|
|
3146
|
+
setClientHandlersRegistered: (value) => {
|
|
3147
|
+
this.clientHandlersRegistered = value;
|
|
3148
|
+
},
|
|
3149
|
+
setDisabledNativeTools: (tools) => {
|
|
3150
|
+
this.disabledNativeTools = tools;
|
|
3151
|
+
},
|
|
3152
|
+
cwd: this.cwd,
|
|
3153
|
+
env: this.env,
|
|
3154
|
+
adapterName: this.adapterName,
|
|
3155
|
+
providerContext: this.config.providerContext,
|
|
3156
|
+
clientId: this.config.clientId,
|
|
3157
|
+
harnessId: this.config.harnessId,
|
|
3158
|
+
bus: this.config.bus,
|
|
3159
|
+
registerClientHandlers: () => this.registerClientHandlers(),
|
|
3160
|
+
handleError: (error, terminate) => this.handleError(error, terminate)
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
buildTurnFlowContext() {
|
|
3164
|
+
return {
|
|
3165
|
+
getCurrentTurn: () => this.currentTurn,
|
|
3166
|
+
setCurrentTurn: (turn) => {
|
|
3167
|
+
this.currentTurn = turn;
|
|
3168
|
+
},
|
|
3169
|
+
getThread: () => this.thread,
|
|
3170
|
+
setThread: (thread) => {
|
|
3171
|
+
this.thread = thread;
|
|
3172
|
+
},
|
|
3173
|
+
getAgentMessageContent: () => this.agentMessageContent,
|
|
3174
|
+
setAgentMessageContent: (content) => {
|
|
3175
|
+
this.agentMessageContent = content;
|
|
3176
|
+
},
|
|
3177
|
+
getPendingMessageHandle: () => this.pendingMessageHandle,
|
|
3178
|
+
setPendingMessageHandle: (handle) => {
|
|
3179
|
+
this.pendingMessageHandle = handle;
|
|
3180
|
+
},
|
|
3181
|
+
setLastResult: (result) => {
|
|
3182
|
+
this.lastResult = result;
|
|
3183
|
+
},
|
|
3184
|
+
getLastResult: () => this.lastResult,
|
|
3185
|
+
setAdapterSessionId: (id) => {
|
|
3186
|
+
this.adapterSessionId = id;
|
|
3187
|
+
},
|
|
3188
|
+
getThreadStartedDeferred: () => this.threadStartedDeferred,
|
|
3189
|
+
setThreadStartedDeferred: (deferred) => {
|
|
3190
|
+
this.threadStartedDeferred = deferred;
|
|
3191
|
+
},
|
|
3192
|
+
messageQueue: this.messageQueue,
|
|
3193
|
+
getJsonRpcClient: () => this.getJsonRpcClient(),
|
|
3194
|
+
emit: this.emit.bind(this),
|
|
3195
|
+
updateProcessingState: this.updateProcessingState.bind(this),
|
|
3196
|
+
agentId: this.agentId,
|
|
3197
|
+
adapterId: this.adapterId,
|
|
3198
|
+
adapterName: this.adapterName,
|
|
3199
|
+
bus: this.config.bus,
|
|
3200
|
+
getModel: () => this.model,
|
|
3201
|
+
getReasoningEffort: () => this.currentReasoningEffort,
|
|
3202
|
+
getApprovalPolicy: () => this._approvalPolicy,
|
|
3203
|
+
getSandboxMode: () => this._sandboxMode,
|
|
3204
|
+
resolveSystemPrompt: () => this.resolveSystemPrompt(),
|
|
3205
|
+
cwd: this.cwd
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
get approvalPolicy() {
|
|
3209
|
+
return this._approvalPolicy;
|
|
3210
|
+
}
|
|
3211
|
+
get sandboxMode() {
|
|
3212
|
+
return this._sandboxMode;
|
|
3213
|
+
}
|
|
3214
|
+
get reasoningEffort() {
|
|
3215
|
+
return this._reasoningEffort;
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Resolve any pending {@link waitForCommandInfo} promise for `itemId`.
|
|
3219
|
+
* Called by the `item/started` lifecycle path after populating `commandExecutionByItemId`.
|
|
3220
|
+
* @param itemId - Item now available in commandExecutionByItemId
|
|
3221
|
+
* @param info - Command execution metadata just written to the cache
|
|
3222
|
+
*/
|
|
3223
|
+
notifyCommandInfoReady(itemId, info) {
|
|
3224
|
+
const resolve = this.commandInfoWaiters.get(itemId);
|
|
3225
|
+
if (resolve) {
|
|
3226
|
+
this.commandInfoWaiters.delete(itemId);
|
|
3227
|
+
resolve(info);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Return `commandExecutionByItemId` entry for `itemId` immediately if present,
|
|
3232
|
+
* otherwise wait up to 5 seconds for `item/started` to populate it.
|
|
3233
|
+
* Returns `undefined` on timeout so callers can degrade gracefully.
|
|
3234
|
+
* @param itemId - Item ID to wait for
|
|
3235
|
+
* @returns Command execution metadata, or `undefined` on timeout
|
|
3236
|
+
*/
|
|
3237
|
+
waitForCommandInfo(itemId) {
|
|
3238
|
+
const existing = this.commandExecutionByItemId.get(itemId);
|
|
3239
|
+
if (existing) return Promise.resolve(existing);
|
|
3240
|
+
return new Promise((resolve) => {
|
|
3241
|
+
const timeout = setTimeout(() => {
|
|
3242
|
+
this.commandInfoWaiters.delete(itemId);
|
|
3243
|
+
resolve(void 0);
|
|
3244
|
+
}, 5e3);
|
|
3245
|
+
this.commandInfoWaiters.set(itemId, (info) => {
|
|
3246
|
+
clearTimeout(timeout);
|
|
3247
|
+
resolve(info);
|
|
3248
|
+
});
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
resolveSystemPrompt() {
|
|
3252
|
+
if (this.systemPrompt === void 0) return null;
|
|
3253
|
+
return typeof this.systemPrompt === "string" ? this.systemPrompt : this.systemPrompt.content;
|
|
3254
|
+
}
|
|
3255
|
+
enqueueNotification(handler) {
|
|
3256
|
+
this.notificationQueue = this.notificationQueue.then(handler).catch((error) => {
|
|
3257
|
+
console.error("[CodexAppServerConnector] Notification handler error:", error);
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
getJsonRpcClient() {
|
|
3261
|
+
if (!this.jsonRpcClient) throw new Error("JSON-RPC client not initialized");
|
|
3262
|
+
return this.jsonRpcClient;
|
|
3263
|
+
}
|
|
3264
|
+
registerClientHandlers() {
|
|
3265
|
+
if (this.clientHandlersRegistered) return;
|
|
3266
|
+
const client = this.getJsonRpcClient();
|
|
3267
|
+
const tfCtx = this.turnCtx;
|
|
3268
|
+
registerNotificationHandlers({
|
|
3269
|
+
client,
|
|
3270
|
+
enqueueNotification: this.enqueueNotification.bind(this),
|
|
3271
|
+
onThreadStarted: (n) => onThreadStarted(tfCtx, n),
|
|
3272
|
+
consumeTurnNumber: this.consumeTurnNumber.bind(this),
|
|
3273
|
+
getCurrentTurn: () => this.currentTurn,
|
|
3274
|
+
emit: this.emit.bind(this),
|
|
3275
|
+
commandExecutionByItemId: this.commandExecutionByItemId,
|
|
3276
|
+
dynamicToolCallByItemId: this.dynamicToolCallByItemId,
|
|
3277
|
+
updateProcessingState: this.updateProcessingState.bind(this),
|
|
3278
|
+
appendAgentMessageDelta: (delta) => {
|
|
3279
|
+
this.agentMessageContent += delta;
|
|
3280
|
+
},
|
|
3281
|
+
onTurnCompleted: (n) => onTurnCompleted(tfCtx, n),
|
|
3282
|
+
getThread: () => this.thread,
|
|
3283
|
+
handleAsyncError: (error) => this.handleError(error),
|
|
3284
|
+
onCommandInfoReady: this.notifyCommandInfoReady.bind(this)
|
|
3285
|
+
});
|
|
3286
|
+
registerServerRequestHandler({
|
|
3287
|
+
client,
|
|
3288
|
+
agentId: this.agentId,
|
|
3289
|
+
cwd: this.cwd ?? "",
|
|
3290
|
+
commandExecutionByItemId: this.commandExecutionByItemId,
|
|
3291
|
+
requestToolApproval: this.requestToolApproval.bind(this),
|
|
3292
|
+
handleError: this.handleError.bind(this),
|
|
3293
|
+
getDisabledNativeTools: () => this.disabledNativeTools,
|
|
3294
|
+
handleDynamicToolCallRequest: this.handleDynamicToolCallRequest.bind(this),
|
|
3295
|
+
waitForCommandInfo: this.waitForCommandInfo.bind(this)
|
|
3296
|
+
});
|
|
3297
|
+
this.clientHandlersRegistered = true;
|
|
3298
|
+
}
|
|
3299
|
+
async handleDynamicToolCallRequest(params) {
|
|
3300
|
+
return handleDynamicToolCallApprovalRequest(params, {
|
|
3301
|
+
requestToolApproval: this.requestToolApproval.bind(this),
|
|
3302
|
+
emit: this.emit.bind(this),
|
|
3303
|
+
sessionId: this.sessionId,
|
|
3304
|
+
agentId: this.agentId,
|
|
3305
|
+
adapterId: this.adapterId,
|
|
3306
|
+
adapterName: this.adapterName,
|
|
3307
|
+
dynamicToolCallByItemId: this.dynamicToolCallByItemId,
|
|
3308
|
+
toolLedger: this.config.toolLedger,
|
|
3309
|
+
currentTurnNumber: this.currentTurnNumber
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
async initialize(options) {
|
|
3313
|
+
this.captureSystemPrompt(options?.systemPrompt);
|
|
3314
|
+
if (!this.isConnected) await initializeConnection(this.connCtx, this.initConnectionInflight);
|
|
3315
|
+
if (!this.thread) await startThread(this.turnCtx);
|
|
3316
|
+
}
|
|
3317
|
+
async start(message, options) {
|
|
3318
|
+
this.captureSystemPrompt(options?.systemPrompt);
|
|
3319
|
+
const messageHandle = await this.sendMessage(message, options);
|
|
3320
|
+
return {
|
|
3321
|
+
adapterSessionId: await this.getAdapterSessionId(),
|
|
3322
|
+
messageHandle,
|
|
3323
|
+
agentId: this.agentId
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
async sendMessage(message, options) {
|
|
3327
|
+
if (!this.isConnected) await initializeConnection(this.connCtx, this.initConnectionInflight);
|
|
3328
|
+
if (!this.thread) await startThread(this.turnCtx);
|
|
3329
|
+
const handle = this.createMessageHandle(message, options);
|
|
3330
|
+
if (this.getProcessingState() === "idle") await this.updateProcessingState("active");
|
|
3331
|
+
this.messageQueue.enqueue(handle);
|
|
3332
|
+
if (!this.currentTurn || this.currentTurn.isCompleted()) await processQueue(this.turnCtx);
|
|
3333
|
+
return handle;
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Codex supports per-turn model switching via `turn/start`.
|
|
3337
|
+
* The caller updates `this.model` after this returns true.
|
|
3338
|
+
* @param _newModel - The model identifier (unused — read from `this.model` at turn start)
|
|
3339
|
+
* @returns Always `true`
|
|
3340
|
+
*/
|
|
3341
|
+
async changeModelInPlace(_newModel) {
|
|
3342
|
+
return true;
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Codex passes reasoning effort per-turn via `turn/start`; `startTurn` reads
|
|
3346
|
+
* `this.currentReasoningEffort` at send time, so no SDK reconfiguration is needed.
|
|
3347
|
+
* @param _newLevel - Unused — read from `this.currentReasoningEffort` at turn start
|
|
3348
|
+
* @returns Always `true`
|
|
3349
|
+
*/
|
|
3350
|
+
async changeReasoningInPlace(_newLevel) {
|
|
3351
|
+
return true;
|
|
3352
|
+
}
|
|
3353
|
+
async interrupt() {
|
|
3354
|
+
if (!this.currentTurn?.getTurnId()) return;
|
|
3355
|
+
await this.getJsonRpcClient().request("turn/interrupt", { turnId: this.currentTurn.getTurnId() });
|
|
3356
|
+
}
|
|
3357
|
+
async getAdapterSessionId() {
|
|
3358
|
+
if (this.adapterSessionId) return this.adapterSessionId;
|
|
3359
|
+
if (this.threadStartedDeferred) return this.threadStartedDeferred.promise;
|
|
3360
|
+
throw new Error("Thread not started");
|
|
3361
|
+
}
|
|
3362
|
+
async complete() {
|
|
3363
|
+
while (this.getProcessingState() !== "idle") await this.onceProcessingStateChanged();
|
|
3364
|
+
return this.lastResult;
|
|
3365
|
+
}
|
|
3366
|
+
abort() {
|
|
3367
|
+
if (this.isTerminated) return;
|
|
3368
|
+
this.isTerminated = true;
|
|
3369
|
+
this.jsonRpcClient?.close();
|
|
3370
|
+
}
|
|
3371
|
+
async close() {
|
|
3372
|
+
if (this.isTerminated) return;
|
|
3373
|
+
this.isTerminated = true;
|
|
3374
|
+
await this.archiveThread();
|
|
3375
|
+
this.jsonRpcClient?.close();
|
|
3376
|
+
}
|
|
3377
|
+
async archiveThread() {
|
|
3378
|
+
const threadId = this.thread?.threadId;
|
|
3379
|
+
if (!threadId) return;
|
|
3380
|
+
const archiveRequest = this.jsonRpcClient?.request("thread/archive", { threadId });
|
|
3381
|
+
if (!archiveRequest) return;
|
|
3382
|
+
try {
|
|
3383
|
+
await Promise.race([archiveRequest, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("archive timeout")), 2e3))]);
|
|
3384
|
+
} catch {}
|
|
3385
|
+
}
|
|
3386
|
+
acceptsImmediate() {
|
|
3387
|
+
if (!this.currentTurn) return true;
|
|
3388
|
+
return this.currentTurn.canAcceptImmediate();
|
|
3389
|
+
}
|
|
3390
|
+
handleError(error, terminate = false) {
|
|
3391
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3392
|
+
if (this.pendingMessageHandle && !this.pendingMessageHandle.isProcessed) this.lastResult = {
|
|
3393
|
+
outcome: "error",
|
|
3394
|
+
error: err
|
|
3395
|
+
};
|
|
3396
|
+
super.handleError(err, terminate);
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
|
|
3400
|
+
//#endregion
|
|
3401
|
+
//#region src/constants.ts
|
|
3402
|
+
/** Adapter name constant for consistent identification */
|
|
3403
|
+
const CodexAppServerAdapterName = "codex-app-server";
|
|
3404
|
+
/** Default model for Codex App-Server adapter */
|
|
3405
|
+
const DefaultModel = "gpt-5.1-codex-mini";
|
|
3406
|
+
/** Default provider for Codex App-Server adapter */
|
|
3407
|
+
const DefaultProvider = "openai";
|
|
3408
|
+
/** Default timeout configuration for Codex App-Server adapter */
|
|
3409
|
+
const DEFAULT_TIMEOUTS = {
|
|
3410
|
+
initialization: 3e4,
|
|
3411
|
+
acknowledgement: 3e4,
|
|
3412
|
+
completion: 6e4,
|
|
3413
|
+
toolApproval: 5e3,
|
|
3414
|
+
eventWait: 3e3
|
|
3415
|
+
};
|
|
3416
|
+
|
|
3417
|
+
//#endregion
|
|
3418
|
+
//#region src/schemas.ts
|
|
3419
|
+
/**
|
|
3420
|
+
* Approval policy enum values for Codex app-server.
|
|
3421
|
+
* Maps to AskForApproval in app-server protocol.
|
|
3422
|
+
* - 'untrusted': Always ask for approval (most restrictive)
|
|
3423
|
+
* - 'on-failure': Ask for approval only if command fails
|
|
3424
|
+
* - 'on-request': Ask for approval when command requests escalated permissions
|
|
3425
|
+
* - 'never': Never ask for approval (least restrictive)
|
|
3426
|
+
*/
|
|
3427
|
+
const ApprovalPolicyValues = [
|
|
3428
|
+
"untrusted",
|
|
3429
|
+
"on-failure",
|
|
3430
|
+
"on-request",
|
|
3431
|
+
"never"
|
|
3432
|
+
];
|
|
3433
|
+
/**
|
|
3434
|
+
* Sandbox mode enum values for Codex app-server.
|
|
3435
|
+
* Maps to SandboxMode in app-server protocol.
|
|
3436
|
+
* - 'read-only': Commands cannot write to filesystem
|
|
3437
|
+
* - 'workspace-write': Commands can write to workspace directories
|
|
3438
|
+
* - 'danger-full-access': Commands have full filesystem access
|
|
3439
|
+
*/
|
|
3440
|
+
const SandboxModeValues = [
|
|
3441
|
+
"read-only",
|
|
3442
|
+
"workspace-write",
|
|
3443
|
+
"danger-full-access"
|
|
3444
|
+
];
|
|
3445
|
+
/**
|
|
3446
|
+
* Reasoning effort enum values for Codex app-server.
|
|
3447
|
+
* Maps to ReasoningEffort in app-server protocol.
|
|
3448
|
+
*/
|
|
3449
|
+
const ReasoningEffortValues = [
|
|
3450
|
+
"low",
|
|
3451
|
+
"medium",
|
|
3452
|
+
"high"
|
|
3453
|
+
];
|
|
3454
|
+
/**
|
|
3455
|
+
* Zod schema for Codex app-server provider-specific configuration.
|
|
3456
|
+
*
|
|
3457
|
+
* Used for:
|
|
3458
|
+
* 1. Type-safe config resolution
|
|
3459
|
+
* 2. Serialization to JSON Schema for web-ui form generation
|
|
3460
|
+
* 3. Runtime validation
|
|
3461
|
+
*
|
|
3462
|
+
* Note: `cwd` and `model` come from the adapter options (BaseAgentConnectorConfig),
|
|
3463
|
+
* not from this provider config. This schema only contains provider-specific settings.
|
|
3464
|
+
*/
|
|
3465
|
+
const CodexAppServerProviderConfigSchema = z.object({
|
|
3466
|
+
/**
|
|
3467
|
+
* Approval policy for tool execution.
|
|
3468
|
+
*/
|
|
3469
|
+
approvalPolicy: z.enum(ApprovalPolicyValues).default("on-request").meta({
|
|
3470
|
+
title: "Approval Policy",
|
|
3471
|
+
description: "How to handle tool approval requests"
|
|
3472
|
+
}),
|
|
3473
|
+
/**
|
|
3474
|
+
* Sandbox mode for command execution.
|
|
3475
|
+
*/
|
|
3476
|
+
sandboxMode: z.enum(SandboxModeValues).default("read-only").meta({
|
|
3477
|
+
title: "Sandbox Mode",
|
|
3478
|
+
description: "Sandbox execution environment"
|
|
3479
|
+
}),
|
|
3480
|
+
/**
|
|
3481
|
+
* Reasoning effort level.
|
|
3482
|
+
*/
|
|
3483
|
+
reasoningEffort: z.enum(ReasoningEffortValues).optional().meta({
|
|
3484
|
+
title: "Reasoning Effort",
|
|
3485
|
+
description: "Level of reasoning effort for the model"
|
|
3486
|
+
})
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
//#endregion
|
|
3490
|
+
//#region src/config.ts
|
|
3491
|
+
const CodexAppServerConfig = createAdapterConfigFactory(() => ({
|
|
3492
|
+
adapterName: CodexAppServerAdapterName,
|
|
3493
|
+
adapterDefaults: {
|
|
3494
|
+
reasoningEffort: "low",
|
|
3495
|
+
providerConfig: {
|
|
3496
|
+
approvalPolicy: "untrusted",
|
|
3497
|
+
sandboxMode: "workspace-write"
|
|
3498
|
+
}
|
|
3499
|
+
},
|
|
3500
|
+
schema: CodexAppServerProviderConfigSchema,
|
|
3501
|
+
adapterDefinition: { defaultTimeouts: DEFAULT_TIMEOUTS },
|
|
3502
|
+
protocol: "openai"
|
|
3503
|
+
}));
|
|
3504
|
+
|
|
3505
|
+
//#endregion
|
|
3506
|
+
//#region src/adapter.ts
|
|
3507
|
+
/**
|
|
3508
|
+
* Codex App-Server Adapter - Domain-level adapter using the three-layer architecture.
|
|
3509
|
+
*
|
|
3510
|
+
* Architecture:
|
|
3511
|
+
* ```
|
|
3512
|
+
* CodexAppServerAdapter extends AIAdapter
|
|
3513
|
+
* -> creates via agentFactory()
|
|
3514
|
+
* CodexAppServerAgent extends AIAgent
|
|
3515
|
+
* -> receives connector via connectorFactory()
|
|
3516
|
+
* CodexAppServerConnector extends AIAgentConnector
|
|
3517
|
+
* ```
|
|
3518
|
+
*
|
|
3519
|
+
* Responsibilities:
|
|
3520
|
+
* - Handle adapter.startAgent RPC (inherited from AIAdapter)
|
|
3521
|
+
* - Provide agent and connector factories for instance creation
|
|
3522
|
+
* - Emit adapter.initialized and adapter.session.created events
|
|
3523
|
+
* - Manage agent lifecycle (tracking, disposal)
|
|
3524
|
+
* @example
|
|
3525
|
+
* ```typescript
|
|
3526
|
+
* // Using the class directly
|
|
3527
|
+
* const adapter = new CodexAppServerAdapter();
|
|
3528
|
+
* await adapter.init();
|
|
3529
|
+
*
|
|
3530
|
+
* // Using the convenience factory
|
|
3531
|
+
* const adapter = await createCodexAppServerAdapter();
|
|
3532
|
+
* ```
|
|
3533
|
+
*/
|
|
3534
|
+
var CodexAppServerAdapter = class extends AIAdapter {
|
|
3535
|
+
constructor(config) {
|
|
3536
|
+
super({
|
|
3537
|
+
name: CodexAppServerAdapterName,
|
|
3538
|
+
capabilities: [
|
|
3539
|
+
"tools",
|
|
3540
|
+
"streaming",
|
|
3541
|
+
"systemPrompt:override",
|
|
3542
|
+
"systemPrompt:append"
|
|
3543
|
+
],
|
|
3544
|
+
nativeTools: ["bash", "patch"],
|
|
3545
|
+
...config,
|
|
3546
|
+
namespace: CodexAppServerNamespace,
|
|
3547
|
+
agentFactory: (agentConfig) => {
|
|
3548
|
+
return new CodexAppServerAgent(agentConfig);
|
|
3549
|
+
},
|
|
3550
|
+
configFactory: CodexAppServerConfig.getConfig,
|
|
3551
|
+
connectorFactory: (fullConfig) => new CodexAppServerConnector(fullConfig),
|
|
3552
|
+
definitionProviders: config?.definitionProviders
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
};
|
|
3556
|
+
/**
|
|
3557
|
+
* Factory function to create and initialize a Codex App-Server adapter.
|
|
3558
|
+
*
|
|
3559
|
+
* Convenience wrapper that creates the adapter and calls init() for you.
|
|
3560
|
+
* @param config - Optional adapter configuration
|
|
3561
|
+
* @returns Initialized CodexAppServerAdapter instance
|
|
3562
|
+
* @example
|
|
3563
|
+
* ```typescript
|
|
3564
|
+
* const adapter = await createCodexAppServerAdapter();
|
|
3565
|
+
*
|
|
3566
|
+
* // Adapter is ready to handle requests via bus
|
|
3567
|
+
* // e.g., MakaioBus.request(AdapterSubjects.startAgent, { adapterId: adapter.adapterId, ... })
|
|
3568
|
+
* ```
|
|
3569
|
+
*/
|
|
3570
|
+
async function createCodexAppServerAdapter(config) {
|
|
3571
|
+
const adapter = new CodexAppServerAdapter(config);
|
|
3572
|
+
await adapter.init();
|
|
3573
|
+
return adapter;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
//#endregion
|
|
3577
|
+
//#region src/event-normalizers.ts
|
|
3578
|
+
/**
|
|
3579
|
+
* Normalize app-server event to canonical AgentSubjects.
|
|
3580
|
+
*
|
|
3581
|
+
* Transforms app-server protocol notifications to global agent events.
|
|
3582
|
+
* Returns array of normalized events (some notifications may map to multiple).
|
|
3583
|
+
* @param notification - The server notification from app-server
|
|
3584
|
+
* @param context - Normalization context with session/adapter info
|
|
3585
|
+
* @returns Array of normalized events (may be empty for unhandled types)
|
|
3586
|
+
*/
|
|
3587
|
+
function normalizeAppServerEvent(notification, context) {
|
|
3588
|
+
const { method, params } = notification;
|
|
3589
|
+
if (method === "item/agentMessage/delta") return [normalizeAgentMessageDelta(params, context)];
|
|
3590
|
+
if (method === "item/started") return normalizeItemStarted(params, context);
|
|
3591
|
+
if (method === "item/commandExecution/outputDelta") return [normalizeCommandOutputDelta(params, context)];
|
|
3592
|
+
if (method === "item/reasoning/textDelta") return [normalizeReasoningDelta(params, context)];
|
|
3593
|
+
if (method === "thread/tokenUsage/updated") return [normalizeTokenUsage(params, context)];
|
|
3594
|
+
if (method === "turn/completed") return [normalizeTurnCompleted(params, context)];
|
|
3595
|
+
return [];
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Normalize agent message delta to agent.message_delta subject.
|
|
3599
|
+
* @param params - The agent message delta notification
|
|
3600
|
+
* @param ctx - Normalization context
|
|
3601
|
+
* @returns Normalized event for message_delta
|
|
3602
|
+
*/
|
|
3603
|
+
function normalizeAgentMessageDelta(params, ctx) {
|
|
3604
|
+
return {
|
|
3605
|
+
subject: AgentSubjects.message_delta,
|
|
3606
|
+
payload: {
|
|
3607
|
+
...ctx,
|
|
3608
|
+
text: params.delta
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Normalize item started to appropriate subject based on item type.
|
|
3614
|
+
* @param params - The item started notification
|
|
3615
|
+
* @param ctx - Normalization context
|
|
3616
|
+
* @returns Array of normalized events (may be empty for unhandled types)
|
|
3617
|
+
*/
|
|
3618
|
+
function normalizeItemStarted(params, ctx) {
|
|
3619
|
+
const { item } = params;
|
|
3620
|
+
if (item.type === "agentMessage") return [{
|
|
3621
|
+
subject: AgentSubjects.message_delta,
|
|
3622
|
+
payload: {
|
|
3623
|
+
...ctx,
|
|
3624
|
+
text: ""
|
|
3625
|
+
}
|
|
3626
|
+
}];
|
|
3627
|
+
if (item.type === "commandExecution") return [{
|
|
3628
|
+
subject: AgentSubjects.tool.started,
|
|
3629
|
+
payload: {
|
|
3630
|
+
...ctx,
|
|
3631
|
+
toolName: "bash",
|
|
3632
|
+
toolCallId: item.id
|
|
3633
|
+
}
|
|
3634
|
+
}];
|
|
3635
|
+
if (item.type === "fileChange") return [{
|
|
3636
|
+
subject: AgentSubjects.tool.started,
|
|
3637
|
+
payload: {
|
|
3638
|
+
...ctx,
|
|
3639
|
+
toolName: "patch",
|
|
3640
|
+
toolCallId: item.id
|
|
3641
|
+
}
|
|
3642
|
+
}];
|
|
3643
|
+
if (item.type === "mcpToolCall") return [{
|
|
3644
|
+
subject: AgentSubjects.tool.started,
|
|
3645
|
+
payload: {
|
|
3646
|
+
...ctx,
|
|
3647
|
+
toolName: `${item.server}:${item.tool}`,
|
|
3648
|
+
toolCallId: item.id
|
|
3649
|
+
}
|
|
3650
|
+
}];
|
|
3651
|
+
return [];
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Normalize command output delta to agent.tool.output subject.
|
|
3655
|
+
* @param params - The command output delta notification
|
|
3656
|
+
* @param ctx - Normalization context
|
|
3657
|
+
* @returns Normalized event for tool output
|
|
3658
|
+
*/
|
|
3659
|
+
function normalizeCommandOutputDelta(params, ctx) {
|
|
3660
|
+
return {
|
|
3661
|
+
subject: AgentSubjects.tool.output,
|
|
3662
|
+
payload: {
|
|
3663
|
+
...ctx,
|
|
3664
|
+
output: params.delta,
|
|
3665
|
+
toolCallId: params.itemId
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Normalize reasoning delta to agent.reasoning_delta subject.
|
|
3671
|
+
* @param params - The reasoning text delta notification
|
|
3672
|
+
* @param ctx - Normalization context
|
|
3673
|
+
* @returns Normalized event for reasoning delta
|
|
3674
|
+
*/
|
|
3675
|
+
function normalizeReasoningDelta(params, ctx) {
|
|
3676
|
+
return {
|
|
3677
|
+
subject: AgentSubjects.reasoning_delta,
|
|
3678
|
+
payload: {
|
|
3679
|
+
...ctx,
|
|
3680
|
+
content: params.delta
|
|
3681
|
+
}
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Derive provider from model name.
|
|
3686
|
+
* @param model - The model name
|
|
3687
|
+
* @returns The provider identifier
|
|
3688
|
+
*/
|
|
3689
|
+
function deriveProvider(model) {
|
|
3690
|
+
if (model.startsWith("claude")) return "anthropic";
|
|
3691
|
+
if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3")) return "openai";
|
|
3692
|
+
return DefaultProvider;
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Normalize token usage to agent.usage subject.
|
|
3696
|
+
* @param params - The token usage updated notification
|
|
3697
|
+
* @param ctx - Normalization context
|
|
3698
|
+
* @returns Normalized event for usage
|
|
3699
|
+
*/
|
|
3700
|
+
function normalizeTokenUsage(params, ctx) {
|
|
3701
|
+
const { last } = params.tokenUsage;
|
|
3702
|
+
const model = ctx.model ?? "gpt-5.1-codex-mini";
|
|
3703
|
+
const provider = deriveProvider(model);
|
|
3704
|
+
return {
|
|
3705
|
+
subject: AgentSubjects.usage,
|
|
3706
|
+
payload: {
|
|
3707
|
+
...ctx,
|
|
3708
|
+
provider,
|
|
3709
|
+
model,
|
|
3710
|
+
inputTokens: last.inputTokens,
|
|
3711
|
+
inputCachedTokens: last.cachedInputTokens,
|
|
3712
|
+
outputTokens: last.outputTokens,
|
|
3713
|
+
reasoningTokens: last.reasoningOutputTokens,
|
|
3714
|
+
totalTokens: last.totalTokens,
|
|
3715
|
+
costUnits: last.totalTokens,
|
|
3716
|
+
costUnitType: "tokens"
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Normalize turn completed to agent.complete subject.
|
|
3722
|
+
* @param params - The turn completed notification
|
|
3723
|
+
* @param ctx - Normalization context
|
|
3724
|
+
* @returns Normalized event for completion
|
|
3725
|
+
*/
|
|
3726
|
+
function normalizeTurnCompleted(params, ctx) {
|
|
3727
|
+
return {
|
|
3728
|
+
subject: AgentSubjects.complete,
|
|
3729
|
+
payload: { ...ctx }
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
//#endregion
|
|
3734
|
+
//#region src/protocol/generated/v2/index.ts
|
|
3735
|
+
var v2_exports = /* @__PURE__ */ __exportAll({});
|
|
3736
|
+
|
|
3737
|
+
//#endregion
|
|
3738
|
+
//#region src/provider.ts
|
|
3739
|
+
/**
|
|
3740
|
+
* Provider IDs and preset configuration for the Codex App-Server adapter.
|
|
3741
|
+
*
|
|
3742
|
+
* Provider compatibility is declared by stable definition ID — the adapter
|
|
3743
|
+
* subsystem resolves each ID to a full ProviderDefinitionInput from the
|
|
3744
|
+
* provider registry at boot time.
|
|
3745
|
+
*/
|
|
3746
|
+
const providerIds = ["openai-codex"];
|
|
3747
|
+
/**
|
|
3748
|
+
* Default provider id to use when no provider is explicitly configured.
|
|
3749
|
+
*/
|
|
3750
|
+
const defaultPresetId = "openai-codex";
|
|
3751
|
+
/** Provider id used for conformance tests (same as host default for this adapter). */
|
|
3752
|
+
const testPresetId = defaultPresetId;
|
|
3753
|
+
|
|
3754
|
+
//#endregion
|
|
3755
|
+
//#region src/index.ts
|
|
3756
|
+
const createTestConfig = async (options) => {
|
|
3757
|
+
const { scopedBus } = CodexAppServerNamespace;
|
|
3758
|
+
const bus = await scopedBus();
|
|
3759
|
+
const testPreset = resolveConformanceTestPreset({
|
|
3760
|
+
adapterName: CodexAppServerAdapterName,
|
|
3761
|
+
defaultProviderId: testPresetId,
|
|
3762
|
+
providerIds,
|
|
3763
|
+
providerDefinitions: options?.providerDefinitions,
|
|
3764
|
+
reasoningEffort: "low"
|
|
3765
|
+
});
|
|
3766
|
+
return {
|
|
3767
|
+
createConnector: async (connectorOptions) => new CodexAppServerConnector(await CodexAppServerConfig.getConfig(resolveTestConfig(connectorOptions, bus, testPreset.provider, testPreset.providers))),
|
|
3768
|
+
bus,
|
|
3769
|
+
registerToolApprovalHandler,
|
|
3770
|
+
capabilities: {
|
|
3771
|
+
supportsReplace: true,
|
|
3772
|
+
supportsInterrupt: true,
|
|
3773
|
+
supportsUsageMetrics: true
|
|
3774
|
+
},
|
|
3775
|
+
options: {
|
|
3776
|
+
defaultTimeout: 45e3,
|
|
3777
|
+
primaryModel: testPreset.primaryModel,
|
|
3778
|
+
secondaryModel: testPreset.secondaryModel
|
|
3779
|
+
},
|
|
3780
|
+
createAdapter: async (adapterOptions) => createCodexAppServerAdapter(adapterOptions),
|
|
3781
|
+
adapterName: CodexAppServerAdapterName,
|
|
3782
|
+
testProviderContext: testPreset.providerContext
|
|
3783
|
+
};
|
|
3784
|
+
};
|
|
3785
|
+
|
|
3786
|
+
//#endregion
|
|
3787
|
+
export { AgentMessageDeltaSchema, AgentMessageSchema, CodexAppServerAdapter, CodexAppServerAdapterName, CodexAppServerAgent, CodexAppServerConfig, CodexAppServerConnector, CodexAppServerNamespace, CodexAppServerProviderConfigSchema, CodexAppServerSubjects, CodexAppServerThread, CodexAppServerTurn, CodexAppServerTurnStateSchema, DynamicToolCallApprovalRequestSchema, DynamicToolCallBeginSchema, DynamicToolCallEndSchema, ExecApprovalRequestSchema, ExecCommandBeginSchema, ExecCommandEndSchema, ExecCommandOutputDeltaSchema, FileChangeApprovalRequestSchema, FileChangeOutputDeltaSchema, ItemCompletedSchema, ItemStartedSchema, ReasoningDeltaSchema, ReasoningSchema, ThreadCompletedSchema, ThreadStartedSchema, TokenUsageSchema, TurnCompletedSchema, TurnStartedSchema, TurnStateChangedSchema, TurnStepFinishedSchema, TurnStepStartedSchema, createCodexAppServerAdapter, createJsonRpcClient, createStdioTransport, createTestConfig, formatMessageHistory, fromGlobalToolApproval, normalizeAppServerEvent, registerToolApprovalHandler, toGlobalFileApproval, toGlobalToolApproval, v2_exports as v2 };
|