@pwrdrvr/agent-client 0.1.0
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/dist/index.d.ts +532 -0
- package/dist/index.js +1527 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
// src/codex-thread-client.ts
|
|
2
|
+
import {
|
|
3
|
+
noopLogger
|
|
4
|
+
} from "@pwrdrvr/agent-core";
|
|
5
|
+
import {
|
|
6
|
+
JsonRpcConnection,
|
|
7
|
+
StdioJsonRpcTransport
|
|
8
|
+
} from "@pwrdrvr/agent-transport";
|
|
9
|
+
import { resolveCodexCommand } from "@pwrdrvr/codex-discovery";
|
|
10
|
+
|
|
11
|
+
// src/normalize.ts
|
|
12
|
+
import {
|
|
13
|
+
inferToolKind
|
|
14
|
+
} from "@pwrdrvr/agent-core";
|
|
15
|
+
var CODEX_NOTIFICATION_METHODS = {
|
|
16
|
+
agentMessageDelta: "item/agentMessage/delta",
|
|
17
|
+
reasoningDelta: "item/reasoning/delta",
|
|
18
|
+
reasoningTextDelta: "item/reasoning/textDelta",
|
|
19
|
+
itemStarted: "item/started",
|
|
20
|
+
itemCompleted: "item/completed",
|
|
21
|
+
turnStarted: "turn/started",
|
|
22
|
+
turnCompleted: "turn/completed",
|
|
23
|
+
tokenUsage: "thread/tokenUsage/updated",
|
|
24
|
+
threadSettings: "thread/settings/updated",
|
|
25
|
+
modelRerouted: "model/rerouted",
|
|
26
|
+
error: "error"
|
|
27
|
+
};
|
|
28
|
+
var CODEX_APPROVAL_METHODS = /* @__PURE__ */ new Set([
|
|
29
|
+
"item/commandExecution/requestApproval",
|
|
30
|
+
"item/fileChange/requestApproval",
|
|
31
|
+
"item/permissions/requestApproval",
|
|
32
|
+
"item/tool/requestUserInput",
|
|
33
|
+
// Legacy v1 method names — older Codex builds still emit these.
|
|
34
|
+
"applyPatchApproval",
|
|
35
|
+
"execCommandApproval"
|
|
36
|
+
]);
|
|
37
|
+
var CODEX_TOOL_CALL_METHOD = "item/tool/call";
|
|
38
|
+
function normalizeTokenUsage(usage) {
|
|
39
|
+
const last = usage.last;
|
|
40
|
+
return {
|
|
41
|
+
inputTokens: last.inputTokens,
|
|
42
|
+
cachedInputTokens: last.cachedInputTokens,
|
|
43
|
+
outputTokens: last.outputTokens,
|
|
44
|
+
reasoningOutputTokens: last.reasoningOutputTokens,
|
|
45
|
+
totalTokens: last.totalTokens,
|
|
46
|
+
// Top-level on ThreadTokenUsage (sibling to last/total), not on the breakdown.
|
|
47
|
+
...usage.modelContextWindow != null ? { contextWindow: usage.modelContextWindow } : {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function normalizeTurnStatus(status) {
|
|
51
|
+
switch (status) {
|
|
52
|
+
case "completed":
|
|
53
|
+
return "completed";
|
|
54
|
+
case "failed":
|
|
55
|
+
return "failed";
|
|
56
|
+
case "interrupted":
|
|
57
|
+
return "interrupted";
|
|
58
|
+
case "inProgress":
|
|
59
|
+
return "in_progress";
|
|
60
|
+
case "aborted":
|
|
61
|
+
case "cancelled":
|
|
62
|
+
case "canceled":
|
|
63
|
+
return "cancelled";
|
|
64
|
+
default:
|
|
65
|
+
return "failed";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var DYNAMIC_TOOL_STATUS = {
|
|
69
|
+
inProgress: "in_progress",
|
|
70
|
+
completed: "completed",
|
|
71
|
+
failed: "failed"
|
|
72
|
+
};
|
|
73
|
+
var COMMAND_STATUS = {
|
|
74
|
+
inProgress: "in_progress",
|
|
75
|
+
completed: "completed",
|
|
76
|
+
failed: "failed",
|
|
77
|
+
declined: "cancelled"
|
|
78
|
+
};
|
|
79
|
+
function joinDynamicResult(contentItems) {
|
|
80
|
+
if (contentItems === null) return void 0;
|
|
81
|
+
const text = contentItems.map((item) => item.type === "inputText" ? item.text : item.imageUrl).join("");
|
|
82
|
+
return text.length > 0 ? text : void 0;
|
|
83
|
+
}
|
|
84
|
+
function normalizeThreadItemToolCall(item) {
|
|
85
|
+
if (item.type === "dynamicToolCall") {
|
|
86
|
+
const kind = inferToolKind(item.tool);
|
|
87
|
+
const status = DYNAMIC_TOOL_STATUS[item.status] ?? "in_progress";
|
|
88
|
+
const call = {
|
|
89
|
+
id: item.id,
|
|
90
|
+
name: item.tool,
|
|
91
|
+
kind,
|
|
92
|
+
label: item.tool,
|
|
93
|
+
status,
|
|
94
|
+
args: item.arguments
|
|
95
|
+
};
|
|
96
|
+
const result = joinDynamicResult(item.contentItems);
|
|
97
|
+
if (result !== void 0) call.result = result;
|
|
98
|
+
return call;
|
|
99
|
+
}
|
|
100
|
+
if (item.type === "commandExecution") {
|
|
101
|
+
const status = COMMAND_STATUS[item.status] ?? "in_progress";
|
|
102
|
+
const call = {
|
|
103
|
+
id: item.id,
|
|
104
|
+
name: "command",
|
|
105
|
+
kind: "command",
|
|
106
|
+
label: item.command,
|
|
107
|
+
status,
|
|
108
|
+
command: {
|
|
109
|
+
displayCommand: item.command,
|
|
110
|
+
rawCommand: item.command,
|
|
111
|
+
cwd: item.cwd,
|
|
112
|
+
...item.aggregatedOutput !== null ? { output: item.aggregatedOutput } : {},
|
|
113
|
+
...item.exitCode !== null ? { exitCode: item.exitCode } : {},
|
|
114
|
+
...item.durationMs !== null ? { durationMs: item.durationMs } : {}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return call;
|
|
118
|
+
}
|
|
119
|
+
if (item.type === "webSearch") {
|
|
120
|
+
return {
|
|
121
|
+
id: item.id,
|
|
122
|
+
name: "web_search",
|
|
123
|
+
kind: "search",
|
|
124
|
+
label: item.query,
|
|
125
|
+
status: "completed",
|
|
126
|
+
args: { query: item.query }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (item.type === "fileChange") {
|
|
130
|
+
return {
|
|
131
|
+
id: item.id,
|
|
132
|
+
name: "file_change",
|
|
133
|
+
kind: "write",
|
|
134
|
+
label: "Edit files",
|
|
135
|
+
status: item.status === "completed" ? "completed" : "in_progress"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function normalizeDynamicToolCall(params) {
|
|
141
|
+
return {
|
|
142
|
+
id: params.callId,
|
|
143
|
+
name: params.tool,
|
|
144
|
+
kind: inferToolKind(params.tool),
|
|
145
|
+
label: params.tool,
|
|
146
|
+
status: "in_progress",
|
|
147
|
+
args: params.arguments
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function approvalKindForMethod(method) {
|
|
151
|
+
if (method.includes("commandExecution") || method === "execCommandApproval") return "exec";
|
|
152
|
+
if (method.includes("fileChange") || method === "applyPatchApproval") return "patch";
|
|
153
|
+
if (method.includes("tool")) return "tool";
|
|
154
|
+
return "other";
|
|
155
|
+
}
|
|
156
|
+
function normalizeApprovalRequest(method, params, approvalId) {
|
|
157
|
+
const p = params ?? {};
|
|
158
|
+
const summary = typeof p.reason === "string" ? p.reason : typeof p.command === "string" ? p.command : void 0;
|
|
159
|
+
const request = {
|
|
160
|
+
id: approvalId,
|
|
161
|
+
method,
|
|
162
|
+
kind: approvalKindForMethod(method),
|
|
163
|
+
params
|
|
164
|
+
};
|
|
165
|
+
if (summary !== void 0) request.summary = summary;
|
|
166
|
+
return request;
|
|
167
|
+
}
|
|
168
|
+
function normalizeThreadSettings(notification) {
|
|
169
|
+
return {
|
|
170
|
+
threadId: notification.threadId,
|
|
171
|
+
model: notification.threadSettings.model,
|
|
172
|
+
modelProvider: notification.threadSettings.modelProvider,
|
|
173
|
+
serviceTier: notification.threadSettings.serviceTier
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function isToolish(item) {
|
|
177
|
+
return item.type === "dynamicToolCall" || item.type === "commandExecution" || item.type === "webSearch" || item.type === "fileChange";
|
|
178
|
+
}
|
|
179
|
+
function toUpdate(call) {
|
|
180
|
+
return call;
|
|
181
|
+
}
|
|
182
|
+
function agentMessageFromItem(item) {
|
|
183
|
+
if (item.type !== "agentMessage") return null;
|
|
184
|
+
return { id: item.id, role: "assistant", text: item.text };
|
|
185
|
+
}
|
|
186
|
+
function normalizeNotification(method, params) {
|
|
187
|
+
switch (method) {
|
|
188
|
+
case CODEX_NOTIFICATION_METHODS.agentMessageDelta: {
|
|
189
|
+
const n = params;
|
|
190
|
+
return {
|
|
191
|
+
kind: "agent_message_delta",
|
|
192
|
+
threadId: n.threadId,
|
|
193
|
+
turnId: n.turnId,
|
|
194
|
+
itemId: n.itemId,
|
|
195
|
+
delta: n.delta
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
case CODEX_NOTIFICATION_METHODS.reasoningDelta:
|
|
199
|
+
case CODEX_NOTIFICATION_METHODS.reasoningTextDelta: {
|
|
200
|
+
const n = params;
|
|
201
|
+
return {
|
|
202
|
+
kind: "reasoning_delta",
|
|
203
|
+
threadId: n.threadId,
|
|
204
|
+
turnId: n.turnId,
|
|
205
|
+
itemId: n.itemId,
|
|
206
|
+
delta: n.delta
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
case CODEX_NOTIFICATION_METHODS.turnStarted: {
|
|
210
|
+
const n = params;
|
|
211
|
+
return { kind: "turn_started", threadId: n.threadId, turnId: n.turn.id };
|
|
212
|
+
}
|
|
213
|
+
case CODEX_NOTIFICATION_METHODS.turnCompleted: {
|
|
214
|
+
const n = params;
|
|
215
|
+
return {
|
|
216
|
+
kind: "turn_completed",
|
|
217
|
+
threadId: n.threadId,
|
|
218
|
+
turnId: n.turn.id,
|
|
219
|
+
status: normalizeTurnStatus(n.turn.status)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
case CODEX_NOTIFICATION_METHODS.tokenUsage: {
|
|
223
|
+
const n = params;
|
|
224
|
+
return {
|
|
225
|
+
kind: "token_usage",
|
|
226
|
+
threadId: n.threadId,
|
|
227
|
+
turnId: n.turnId,
|
|
228
|
+
usage: normalizeTokenUsage(n.tokenUsage)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case CODEX_NOTIFICATION_METHODS.threadSettings: {
|
|
232
|
+
const n = params;
|
|
233
|
+
return { kind: "thread_settings", settings: normalizeThreadSettings(n) };
|
|
234
|
+
}
|
|
235
|
+
case CODEX_NOTIFICATION_METHODS.modelRerouted: {
|
|
236
|
+
const n = params;
|
|
237
|
+
return {
|
|
238
|
+
kind: "thread_settings",
|
|
239
|
+
settings: {
|
|
240
|
+
threadId: n.threadId,
|
|
241
|
+
model: n.toModel,
|
|
242
|
+
modelProvider: "openai",
|
|
243
|
+
serviceTier: null
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case CODEX_NOTIFICATION_METHODS.itemStarted: {
|
|
248
|
+
const n = params;
|
|
249
|
+
if (isToolish(n.item)) {
|
|
250
|
+
const call = normalizeThreadItemToolCall(n.item);
|
|
251
|
+
if (call === null) return null;
|
|
252
|
+
return { kind: "tool_call", threadId: n.threadId, turnId: n.turnId, toolCall: call };
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
case CODEX_NOTIFICATION_METHODS.itemCompleted: {
|
|
257
|
+
const n = params;
|
|
258
|
+
const message = agentMessageFromItem(n.item);
|
|
259
|
+
if (message !== null) {
|
|
260
|
+
return {
|
|
261
|
+
kind: "agent_message",
|
|
262
|
+
threadId: n.threadId,
|
|
263
|
+
turnId: n.turnId,
|
|
264
|
+
message
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (isToolish(n.item)) {
|
|
268
|
+
const call = normalizeThreadItemToolCall(n.item);
|
|
269
|
+
if (call === null) return null;
|
|
270
|
+
return {
|
|
271
|
+
kind: "tool_call_update",
|
|
272
|
+
threadId: n.threadId,
|
|
273
|
+
turnId: n.turnId,
|
|
274
|
+
toolCall: toUpdate(call)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
case CODEX_NOTIFICATION_METHODS.error: {
|
|
280
|
+
const p = params ?? {};
|
|
281
|
+
const message = typeof p.message === "string" ? p.message : "codex error";
|
|
282
|
+
const event = {
|
|
283
|
+
kind: "error",
|
|
284
|
+
message
|
|
285
|
+
};
|
|
286
|
+
if (typeof p.threadId === "string") event.threadId = p.threadId;
|
|
287
|
+
if (typeof p.turnId === "string") event.turnId = p.turnId;
|
|
288
|
+
if (typeof p.code === "string") event.code = p.code;
|
|
289
|
+
return event;
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/codex-thread-client.ts
|
|
297
|
+
var DEFAULT_CLIENT_NAME = "agent-kit";
|
|
298
|
+
var DEFAULT_SERVICE_NAME = "agent-kit";
|
|
299
|
+
function approvalResponseFor(decision) {
|
|
300
|
+
switch (decision) {
|
|
301
|
+
case "approved":
|
|
302
|
+
return { decision: "approved" };
|
|
303
|
+
case "abort":
|
|
304
|
+
return { decision: "abort" };
|
|
305
|
+
case "denied":
|
|
306
|
+
default:
|
|
307
|
+
return { decision: "denied" };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
var CodexThreadClient = class {
|
|
311
|
+
constructor(options = {}) {
|
|
312
|
+
this.options = options;
|
|
313
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 2e4;
|
|
314
|
+
this.turnTimeoutMs = options.turnTimeoutMs ?? 12e4;
|
|
315
|
+
this.logger = options.logger ?? noopLogger;
|
|
316
|
+
this.transportFactory = options.transportFactory ?? null;
|
|
317
|
+
}
|
|
318
|
+
options;
|
|
319
|
+
requestTimeoutMs;
|
|
320
|
+
turnTimeoutMs;
|
|
321
|
+
logger;
|
|
322
|
+
transportFactory;
|
|
323
|
+
resolvedCommand = null;
|
|
324
|
+
connection = null;
|
|
325
|
+
initializeResponse = null;
|
|
326
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
327
|
+
toolCallHandler = null;
|
|
328
|
+
approvalHandler = null;
|
|
329
|
+
loadedThreadIds = /* @__PURE__ */ new Set();
|
|
330
|
+
/** Subscribe to the normalized event stream. Every native notification is
|
|
331
|
+
* routed through `normalizeNotification` before listeners see it. */
|
|
332
|
+
onEvent(cb) {
|
|
333
|
+
this.eventListeners.add(cb);
|
|
334
|
+
return () => {
|
|
335
|
+
this.eventListeners.delete(cb);
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Register the dynamic-tool ServerRequest handler (one at a time). */
|
|
339
|
+
onToolCall(handler) {
|
|
340
|
+
this.toolCallHandler = handler;
|
|
341
|
+
return () => {
|
|
342
|
+
if (this.toolCallHandler === handler) this.toolCallHandler = null;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** Register the approval ServerRequest handler (one at a time). */
|
|
346
|
+
onApprovalRequest(handler) {
|
|
347
|
+
this.approvalHandler = handler;
|
|
348
|
+
return () => {
|
|
349
|
+
if (this.approvalHandler === handler) this.approvalHandler = null;
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
|
|
354
|
+
* and maps them onto Codex-native `CodexStartThreadOptions` before delegating
|
|
355
|
+
* to `startThreadNative`. Mapping:
|
|
356
|
+
* instructions→baseInstructions, cwd, workspaceRoots→runtimeWorkspaceRoots,
|
|
357
|
+
* model/modelProvider, serviceTier (drop `null`), approvalPolicy, sandbox,
|
|
358
|
+
* serviceName, config, environments, tools (cast to DynamicToolSpec[])→
|
|
359
|
+
* dynamicTools.
|
|
360
|
+
*/
|
|
361
|
+
async startThread(opts = {}) {
|
|
362
|
+
const native = {};
|
|
363
|
+
if (opts.instructions !== void 0) native.baseInstructions = opts.instructions;
|
|
364
|
+
if (opts.cwd !== void 0) native.cwd = opts.cwd;
|
|
365
|
+
if (opts.workspaceRoots !== void 0) {
|
|
366
|
+
native.runtimeWorkspaceRoots = [...opts.workspaceRoots];
|
|
367
|
+
}
|
|
368
|
+
if (opts.model !== void 0) native.model = opts.model;
|
|
369
|
+
if (opts.modelProvider !== void 0) native.modelProvider = opts.modelProvider;
|
|
370
|
+
if (opts.serviceTier != null) native.serviceTier = opts.serviceTier;
|
|
371
|
+
if (opts.approvalPolicy !== void 0) native.approvalPolicy = opts.approvalPolicy;
|
|
372
|
+
if (opts.sandbox !== void 0) native.sandbox = opts.sandbox;
|
|
373
|
+
if (opts.serviceName !== void 0) native.serviceName = opts.serviceName;
|
|
374
|
+
if (opts.config !== void 0) native.config = opts.config;
|
|
375
|
+
if (opts.environments !== void 0) native.environments = opts.environments;
|
|
376
|
+
if (opts.tools !== void 0) native.dynamicTools = opts.tools;
|
|
377
|
+
return this.startThreadNative(native);
|
|
378
|
+
}
|
|
379
|
+
/** Codex-native thread/start. Builds `ThreadStartParams` directly. Exposed for
|
|
380
|
+
* hosts that want full Codex control; the neutral `startThread` delegates here. */
|
|
381
|
+
async startThreadNative(opts = {}) {
|
|
382
|
+
const connection = await this.getConnection();
|
|
383
|
+
await this.initialize();
|
|
384
|
+
const params = {
|
|
385
|
+
experimentalRawEvents: false,
|
|
386
|
+
persistExtendedHistory: false
|
|
387
|
+
};
|
|
388
|
+
if (opts.cwd !== void 0) params.cwd = opts.cwd;
|
|
389
|
+
if (opts.model !== void 0) params.model = opts.model;
|
|
390
|
+
if (opts.modelProvider !== void 0) params.modelProvider = opts.modelProvider;
|
|
391
|
+
if (opts.serviceTier !== void 0) params.serviceTier = opts.serviceTier;
|
|
392
|
+
if (opts.runtimeWorkspaceRoots !== void 0) {
|
|
393
|
+
params.runtimeWorkspaceRoots = opts.runtimeWorkspaceRoots;
|
|
394
|
+
}
|
|
395
|
+
const serviceName = opts.serviceName ?? this.options.serviceName ?? DEFAULT_SERVICE_NAME;
|
|
396
|
+
params.serviceName = serviceName;
|
|
397
|
+
if (opts.approvalPolicy !== void 0) {
|
|
398
|
+
params.approvalPolicy = opts.approvalPolicy;
|
|
399
|
+
}
|
|
400
|
+
if (opts.sandbox !== void 0) params.sandbox = opts.sandbox;
|
|
401
|
+
if (opts.baseInstructions !== void 0) params.baseInstructions = opts.baseInstructions;
|
|
402
|
+
if (opts.personality !== void 0) params.personality = opts.personality;
|
|
403
|
+
if (opts.dynamicTools !== void 0) params.dynamicTools = opts.dynamicTools;
|
|
404
|
+
if (opts.config !== void 0) {
|
|
405
|
+
params.config = opts.config;
|
|
406
|
+
}
|
|
407
|
+
if (opts.environments !== void 0) {
|
|
408
|
+
params.environments = opts.environments;
|
|
409
|
+
}
|
|
410
|
+
const response = await connection.request(
|
|
411
|
+
"thread/start",
|
|
412
|
+
params,
|
|
413
|
+
this.requestTimeoutMs
|
|
414
|
+
);
|
|
415
|
+
const threadId = response.thread.id;
|
|
416
|
+
this.loadedThreadIds.add(threadId);
|
|
417
|
+
this.logger.debug("thread started", { threadId });
|
|
418
|
+
return {
|
|
419
|
+
threadId,
|
|
420
|
+
model: response.model,
|
|
421
|
+
modelProvider: response.modelProvider,
|
|
422
|
+
serviceTier: response.serviceTier
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async resumeThread(threadId) {
|
|
426
|
+
if (this.loadedThreadIds.has(threadId)) return;
|
|
427
|
+
const connection = await this.getConnection();
|
|
428
|
+
await this.initialize();
|
|
429
|
+
const params = {
|
|
430
|
+
threadId,
|
|
431
|
+
persistExtendedHistory: false
|
|
432
|
+
};
|
|
433
|
+
const response = await connection.request(
|
|
434
|
+
"thread/resume",
|
|
435
|
+
params,
|
|
436
|
+
this.requestTimeoutMs
|
|
437
|
+
);
|
|
438
|
+
this.loadedThreadIds.add(response.thread.id);
|
|
439
|
+
this.logger.debug("thread resumed", { threadId: response.thread.id });
|
|
440
|
+
}
|
|
441
|
+
async clearThreadGitInfo(threadId) {
|
|
442
|
+
const connection = await this.getConnection();
|
|
443
|
+
await this.initialize();
|
|
444
|
+
await connection.request(
|
|
445
|
+
"thread/metadata/update",
|
|
446
|
+
{ threadId, gitInfo: { sha: null, branch: null, originUrl: null } },
|
|
447
|
+
this.requestTimeoutMs
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Public `AgentBackend.startTurn`: accepts NEUTRAL `AgentStartTurnOptions` and
|
|
452
|
+
* maps them onto Codex-native `UserInput[]`. The neutral `input.text` becomes a
|
|
453
|
+
* leading `{ type: "text" }` item; each `input.imagePaths` entry becomes a
|
|
454
|
+
* `{ type: "localImage", path }` item appended after the text. `reasoning`
|
|
455
|
+
* maps to Codex's `effort`.
|
|
456
|
+
*/
|
|
457
|
+
async startTurn(opts) {
|
|
458
|
+
const input = [{ type: "text", text: opts.input.text, text_elements: [] }];
|
|
459
|
+
for (const path of opts.input.imagePaths ?? []) {
|
|
460
|
+
input.push({ type: "localImage", path });
|
|
461
|
+
}
|
|
462
|
+
const native = { threadId: opts.threadId, input };
|
|
463
|
+
if (opts.reasoning !== void 0) native.effort = opts.reasoning;
|
|
464
|
+
return this.startTurnNative(native);
|
|
465
|
+
}
|
|
466
|
+
/** Codex-native turn/start. Takes pre-built `UserInput[]`. The neutral
|
|
467
|
+
* `startTurn` delegates here after mapping text + image paths. */
|
|
468
|
+
async startTurnNative(opts) {
|
|
469
|
+
await this.resumeThread(opts.threadId);
|
|
470
|
+
const connection = await this.getConnection();
|
|
471
|
+
await this.initialize();
|
|
472
|
+
const params = {
|
|
473
|
+
threadId: opts.threadId,
|
|
474
|
+
input: opts.input
|
|
475
|
+
};
|
|
476
|
+
if (opts.effort !== void 0) params.effort = opts.effort;
|
|
477
|
+
const response = await connection.request(
|
|
478
|
+
"turn/start",
|
|
479
|
+
params,
|
|
480
|
+
this.turnTimeoutMs
|
|
481
|
+
);
|
|
482
|
+
const turnId = response.turn.id;
|
|
483
|
+
this.logger.debug("turn started", { threadId: opts.threadId, turnId });
|
|
484
|
+
return { turnId };
|
|
485
|
+
}
|
|
486
|
+
async interruptTurn(threadId) {
|
|
487
|
+
const connection = await this.getConnection();
|
|
488
|
+
await connection.request("turn/interrupt", { threadId }, this.requestTimeoutMs);
|
|
489
|
+
}
|
|
490
|
+
async archiveThread(threadId) {
|
|
491
|
+
const connection = await this.getConnection();
|
|
492
|
+
await connection.request("thread/archive", { threadId }, this.requestTimeoutMs);
|
|
493
|
+
}
|
|
494
|
+
async close() {
|
|
495
|
+
const connection = this.connection;
|
|
496
|
+
this.connection = null;
|
|
497
|
+
this.initializeResponse = null;
|
|
498
|
+
this.loadedThreadIds.clear();
|
|
499
|
+
if (connection) await connection.close();
|
|
500
|
+
}
|
|
501
|
+
// ---- internals ----
|
|
502
|
+
emit(event) {
|
|
503
|
+
for (const listener of this.eventListeners) listener(event);
|
|
504
|
+
}
|
|
505
|
+
async resolveCommand() {
|
|
506
|
+
if (this.resolvedCommand !== null) return this.resolvedCommand;
|
|
507
|
+
const resolved = await resolveCodexCommand({
|
|
508
|
+
command: this.options.command ?? "codex",
|
|
509
|
+
env: this.options.env ?? process.env
|
|
510
|
+
});
|
|
511
|
+
this.resolvedCommand = resolved.command;
|
|
512
|
+
return resolved.command;
|
|
513
|
+
}
|
|
514
|
+
async initialize() {
|
|
515
|
+
if (this.initializeResponse) return this.initializeResponse;
|
|
516
|
+
const connection = await this.getConnection();
|
|
517
|
+
const name = this.options.clientName ?? DEFAULT_CLIENT_NAME;
|
|
518
|
+
const params = {
|
|
519
|
+
clientInfo: {
|
|
520
|
+
name,
|
|
521
|
+
title: this.options.clientTitle ?? name,
|
|
522
|
+
version: this.options.clientVersion ?? "0.0.0"
|
|
523
|
+
},
|
|
524
|
+
capabilities: {
|
|
525
|
+
experimentalApi: true,
|
|
526
|
+
// We don't proxy through OpenAI's edge attestation flow, so opting in
|
|
527
|
+
// would add per-turn latency for an unused round-trip.
|
|
528
|
+
requestAttestation: false
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const response = await connection.request(
|
|
532
|
+
"initialize",
|
|
533
|
+
params,
|
|
534
|
+
this.requestTimeoutMs
|
|
535
|
+
);
|
|
536
|
+
this.initializeResponse = response;
|
|
537
|
+
return response;
|
|
538
|
+
}
|
|
539
|
+
async getConnection() {
|
|
540
|
+
if (this.connection) return this.connection;
|
|
541
|
+
let transport;
|
|
542
|
+
if (this.transportFactory !== null) {
|
|
543
|
+
transport = this.transportFactory(this.options.command ?? "codex");
|
|
544
|
+
} else {
|
|
545
|
+
const command = await this.resolveCommand();
|
|
546
|
+
transport = new StdioJsonRpcTransport({
|
|
547
|
+
command,
|
|
548
|
+
args: ["app-server"],
|
|
549
|
+
...this.options.env !== void 0 ? { env: this.options.env } : {},
|
|
550
|
+
logger: this.logger
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const connection = new JsonRpcConnection(transport, this.requestTimeoutMs, void 0, {
|
|
554
|
+
logger: this.logger,
|
|
555
|
+
logContext: { owner: "codex-thread-client" }
|
|
556
|
+
});
|
|
557
|
+
connection.setNotificationHandler((method, params) => {
|
|
558
|
+
this.handleNotification(method, params);
|
|
559
|
+
});
|
|
560
|
+
connection.setRequestHandler((method, params) => this.handleServerRequest(method, params));
|
|
561
|
+
await connection.connect();
|
|
562
|
+
this.connection = connection;
|
|
563
|
+
return connection;
|
|
564
|
+
}
|
|
565
|
+
handleNotification(method, params) {
|
|
566
|
+
const event = normalizeNotification(method, params);
|
|
567
|
+
if (event !== null) this.emit(event);
|
|
568
|
+
}
|
|
569
|
+
async handleServerRequest(method, params) {
|
|
570
|
+
if (method === CODEX_TOOL_CALL_METHOD) {
|
|
571
|
+
const handler = this.toolCallHandler;
|
|
572
|
+
if (!handler) {
|
|
573
|
+
this.logger.warn("tool call received with no handler registered");
|
|
574
|
+
return {
|
|
575
|
+
contentItems: [
|
|
576
|
+
{ type: "inputText", text: "No tool handler is registered for this thread." }
|
|
577
|
+
],
|
|
578
|
+
success: false
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const call = { method, params };
|
|
582
|
+
return await handler(call);
|
|
583
|
+
}
|
|
584
|
+
if (CODEX_APPROVAL_METHODS.has(method)) {
|
|
585
|
+
const handler = this.approvalHandler;
|
|
586
|
+
if (!handler) {
|
|
587
|
+
this.logger.warn("approval request received with no handler registered", { method });
|
|
588
|
+
return approvalResponseFor("denied");
|
|
589
|
+
}
|
|
590
|
+
const decision = await handler(method, params);
|
|
591
|
+
return approvalResponseFor(decision);
|
|
592
|
+
}
|
|
593
|
+
this.logger.debug("unhandled codex server request", { method });
|
|
594
|
+
return {};
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/codex-oneshot-client.ts
|
|
599
|
+
import { mkdir } from "fs/promises";
|
|
600
|
+
import { tmpdir } from "os";
|
|
601
|
+
import { join } from "path";
|
|
602
|
+
import {
|
|
603
|
+
noopLogger as noopLogger2
|
|
604
|
+
} from "@pwrdrvr/agent-core";
|
|
605
|
+
import {
|
|
606
|
+
JsonRpcConnection as JsonRpcConnection2,
|
|
607
|
+
StdioJsonRpcTransport as StdioJsonRpcTransport2
|
|
608
|
+
} from "@pwrdrvr/agent-transport";
|
|
609
|
+
import { resolveCodexCommand as resolveCodexCommand2 } from "@pwrdrvr/codex-discovery";
|
|
610
|
+
var DEFAULT_CLIENT_NAME2 = "agent-kit";
|
|
611
|
+
var DEFAULT_SERVICE_NAME2 = "agent-kit";
|
|
612
|
+
var CodexOneShotClient = class {
|
|
613
|
+
constructor(options = {}) {
|
|
614
|
+
this.options = options;
|
|
615
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 2e4;
|
|
616
|
+
this.turnTimeoutMs = options.turnTimeoutMs ?? 12e4;
|
|
617
|
+
this.logger = options.logger ?? noopLogger2;
|
|
618
|
+
this.transportFactory = options.transportFactory ?? null;
|
|
619
|
+
}
|
|
620
|
+
options;
|
|
621
|
+
requestTimeoutMs;
|
|
622
|
+
turnTimeoutMs;
|
|
623
|
+
logger;
|
|
624
|
+
transportFactory;
|
|
625
|
+
resolvedCommand = null;
|
|
626
|
+
connection = null;
|
|
627
|
+
initializeResponse = null;
|
|
628
|
+
pendingTurn = null;
|
|
629
|
+
workerThread = null;
|
|
630
|
+
queue = Promise.resolve();
|
|
631
|
+
/** Run one structured-output turn. Calls are serialized — only one turn is in
|
|
632
|
+
* flight at a time against the shared worker thread. */
|
|
633
|
+
async run(request) {
|
|
634
|
+
const run = this.queue.catch(() => void 0).then(() => this.runInner(request));
|
|
635
|
+
this.queue = run.then(
|
|
636
|
+
() => void 0,
|
|
637
|
+
() => void 0
|
|
638
|
+
);
|
|
639
|
+
return run;
|
|
640
|
+
}
|
|
641
|
+
async runInner(request) {
|
|
642
|
+
const connection = await this.getConnection();
|
|
643
|
+
const initialized = await this.initialize();
|
|
644
|
+
let thread = null;
|
|
645
|
+
let turnId = null;
|
|
646
|
+
let rolledBack = false;
|
|
647
|
+
let aborted = false;
|
|
648
|
+
const abortHandler = () => {
|
|
649
|
+
aborted = true;
|
|
650
|
+
if (thread && turnId) {
|
|
651
|
+
void connection.request("turn/interrupt", { threadId: thread.threadId, turnId }, this.requestTimeoutMs).catch((error) => {
|
|
652
|
+
this.logger.warn("turn interrupt failed", {
|
|
653
|
+
error: error instanceof Error ? error.message : String(error)
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
request.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
|
659
|
+
try {
|
|
660
|
+
if (request.abortSignal?.aborted) {
|
|
661
|
+
throw new DOMException("one-shot turn aborted", "AbortError");
|
|
662
|
+
}
|
|
663
|
+
thread = await this.getWorkerThread(
|
|
664
|
+
request.model ?? null,
|
|
665
|
+
request.modelProvider ?? null,
|
|
666
|
+
request.baseInstructions ?? ""
|
|
667
|
+
);
|
|
668
|
+
const input = [
|
|
669
|
+
{ type: "text", text: request.prompt, text_elements: [] },
|
|
670
|
+
...imagePathsToLocalImageInputs(request.imagePaths ?? [])
|
|
671
|
+
];
|
|
672
|
+
const turnResponse = await connection.request(
|
|
673
|
+
"turn/start",
|
|
674
|
+
{
|
|
675
|
+
threadId: thread.threadId,
|
|
676
|
+
model: request.model ?? null,
|
|
677
|
+
input,
|
|
678
|
+
effort: request.effort ?? "low",
|
|
679
|
+
...request.outputSchema !== void 0 ? { outputSchema: request.outputSchema } : {}
|
|
680
|
+
},
|
|
681
|
+
this.requestTimeoutMs
|
|
682
|
+
);
|
|
683
|
+
turnId = turnResponse.turn.id;
|
|
684
|
+
if (request.abortSignal?.aborted || aborted) {
|
|
685
|
+
throw new DOMException("one-shot turn aborted", "AbortError");
|
|
686
|
+
}
|
|
687
|
+
const { rawText, tokenUsage } = await this.waitForTurn(thread.threadId, turnId);
|
|
688
|
+
await this.rollbackWorkerThread(thread.threadId);
|
|
689
|
+
rolledBack = true;
|
|
690
|
+
return {
|
|
691
|
+
rawText,
|
|
692
|
+
threadId: thread.threadId,
|
|
693
|
+
turnId,
|
|
694
|
+
userAgent: initialized.userAgent,
|
|
695
|
+
model: thread.model,
|
|
696
|
+
modelProvider: thread.modelProvider,
|
|
697
|
+
serviceTier: thread.serviceTier,
|
|
698
|
+
tokenUsage: tokenUsage === null ? null : normalizeTokenUsage(tokenUsage)
|
|
699
|
+
};
|
|
700
|
+
} finally {
|
|
701
|
+
request.abortSignal?.removeEventListener("abort", abortHandler);
|
|
702
|
+
if (thread && turnId && !rolledBack) {
|
|
703
|
+
await this.rollbackWorkerThread(thread.threadId).catch((error) => {
|
|
704
|
+
this.logger.warn("worker thread rollback failed", {
|
|
705
|
+
threadId: thread?.threadId,
|
|
706
|
+
error: error instanceof Error ? error.message : String(error)
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async listModels(input = {}) {
|
|
713
|
+
const connection = await this.getConnection();
|
|
714
|
+
await this.initialize();
|
|
715
|
+
const models = [];
|
|
716
|
+
let cursor = null;
|
|
717
|
+
do {
|
|
718
|
+
const response = await connection.request(
|
|
719
|
+
"model/list",
|
|
720
|
+
{ cursor, limit: 100, includeHidden: input.includeHidden ?? false },
|
|
721
|
+
this.requestTimeoutMs
|
|
722
|
+
);
|
|
723
|
+
models.push(...response.data.map(modelToOption));
|
|
724
|
+
cursor = response.nextCursor;
|
|
725
|
+
} while (cursor !== null);
|
|
726
|
+
return models;
|
|
727
|
+
}
|
|
728
|
+
async close() {
|
|
729
|
+
const connection = this.connection;
|
|
730
|
+
const thread = this.workerThread;
|
|
731
|
+
this.connection = null;
|
|
732
|
+
this.initializeResponse = null;
|
|
733
|
+
this.workerThread = null;
|
|
734
|
+
this.queue = Promise.resolve();
|
|
735
|
+
if (connection) {
|
|
736
|
+
if (thread) {
|
|
737
|
+
await connection.request("thread/archive", { threadId: thread.threadId }, this.requestTimeoutMs).catch((error) => {
|
|
738
|
+
this.logger.warn("thread archive failed", {
|
|
739
|
+
threadId: thread.threadId,
|
|
740
|
+
error: error instanceof Error ? error.message : String(error)
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
await connection.close();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// ---- worker-thread management ----
|
|
748
|
+
async getWorkerThread(model, modelProvider, baseInstructions) {
|
|
749
|
+
const modelKey = `${model ?? "__default__"}@${modelProvider ?? "__default__"}::${baseInstructions}`;
|
|
750
|
+
if (this.workerThread?.modelKey === modelKey) {
|
|
751
|
+
return this.workerThread;
|
|
752
|
+
}
|
|
753
|
+
if (this.workerThread) {
|
|
754
|
+
const stale = this.workerThread;
|
|
755
|
+
this.workerThread = null;
|
|
756
|
+
const connection2 = await this.getConnection();
|
|
757
|
+
await connection2.request("thread/archive", { threadId: stale.threadId }, this.requestTimeoutMs).catch((error) => {
|
|
758
|
+
this.logger.warn("thread archive failed", {
|
|
759
|
+
threadId: stale.threadId,
|
|
760
|
+
error: error instanceof Error ? error.message : String(error)
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const connection = await this.getConnection();
|
|
765
|
+
const workspaceDir = await this.prepareWorkspace();
|
|
766
|
+
const threadResponse = await connection.request(
|
|
767
|
+
"thread/start",
|
|
768
|
+
{
|
|
769
|
+
model,
|
|
770
|
+
...modelProvider !== null ? { modelProvider } : {},
|
|
771
|
+
ephemeral: false,
|
|
772
|
+
cwd: workspaceDir,
|
|
773
|
+
runtimeWorkspaceRoots: [workspaceDir],
|
|
774
|
+
serviceName: this.options.serviceName ?? DEFAULT_SERVICE_NAME2,
|
|
775
|
+
approvalPolicy: "never",
|
|
776
|
+
sandbox: "read-only",
|
|
777
|
+
...baseInstructions.length > 0 ? { baseInstructions } : {},
|
|
778
|
+
// Persistent worker thread for a prompt-cache experiment: keep the
|
|
779
|
+
// thread id stable across requests, then roll back each turn. The
|
|
780
|
+
// dedicated cwd keeps the worker out of any host repo/worktree.
|
|
781
|
+
...this.options.threadConfig !== void 0 ? { config: this.options.threadConfig } : {},
|
|
782
|
+
environments: [],
|
|
783
|
+
experimentalRawEvents: false,
|
|
784
|
+
persistExtendedHistory: false
|
|
785
|
+
},
|
|
786
|
+
this.requestTimeoutMs
|
|
787
|
+
);
|
|
788
|
+
await this.clearThreadGitInfo(threadResponse.thread.id);
|
|
789
|
+
await this.setWorkerThreadName(threadResponse.thread.id);
|
|
790
|
+
this.workerThread = {
|
|
791
|
+
threadId: threadResponse.thread.id,
|
|
792
|
+
modelKey,
|
|
793
|
+
baseInstructions,
|
|
794
|
+
model: threadResponse.model,
|
|
795
|
+
modelProvider: threadResponse.modelProvider,
|
|
796
|
+
serviceTier: threadResponse.serviceTier
|
|
797
|
+
};
|
|
798
|
+
return this.workerThread;
|
|
799
|
+
}
|
|
800
|
+
async rollbackWorkerThread(threadId) {
|
|
801
|
+
const connection = await this.getConnection();
|
|
802
|
+
await connection.request("thread/rollback", { threadId, numTurns: 1 }, this.requestTimeoutMs);
|
|
803
|
+
}
|
|
804
|
+
async prepareWorkspace() {
|
|
805
|
+
const workspaceDir = this.options.workspaceDir ?? join(tmpdir(), "agent-kit", "oneshot-worker");
|
|
806
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
807
|
+
return workspaceDir;
|
|
808
|
+
}
|
|
809
|
+
async clearThreadGitInfo(threadId) {
|
|
810
|
+
const connection = await this.getConnection();
|
|
811
|
+
await connection.request(
|
|
812
|
+
"thread/metadata/update",
|
|
813
|
+
{ threadId, gitInfo: { sha: null, branch: null, originUrl: null } },
|
|
814
|
+
this.requestTimeoutMs
|
|
815
|
+
).catch((error) => {
|
|
816
|
+
this.logger.warn("thread git metadata clear failed", {
|
|
817
|
+
threadId,
|
|
818
|
+
error: error instanceof Error ? error.message : String(error)
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
async setWorkerThreadName(threadId) {
|
|
823
|
+
const connection = await this.getConnection();
|
|
824
|
+
await connection.request(
|
|
825
|
+
"thread/name/set",
|
|
826
|
+
{ threadId, name: this.options.workerThreadName ?? "agent-kit One-Shot Worker" },
|
|
827
|
+
this.requestTimeoutMs
|
|
828
|
+
).catch((error) => {
|
|
829
|
+
this.logger.warn("worker thread name set failed", {
|
|
830
|
+
threadId,
|
|
831
|
+
error: error instanceof Error ? error.message : String(error)
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
// ---- connection / turn plumbing ----
|
|
836
|
+
async resolveCommand() {
|
|
837
|
+
if (this.resolvedCommand !== null) return this.resolvedCommand;
|
|
838
|
+
const resolved = await resolveCodexCommand2({
|
|
839
|
+
command: this.options.command ?? "codex",
|
|
840
|
+
env: this.options.env ?? process.env
|
|
841
|
+
});
|
|
842
|
+
this.resolvedCommand = resolved.command;
|
|
843
|
+
return resolved.command;
|
|
844
|
+
}
|
|
845
|
+
async initialize() {
|
|
846
|
+
if (this.initializeResponse) return this.initializeResponse;
|
|
847
|
+
const connection = await this.getConnection();
|
|
848
|
+
const name = this.options.clientName ?? DEFAULT_CLIENT_NAME2;
|
|
849
|
+
const params = {
|
|
850
|
+
clientInfo: {
|
|
851
|
+
name,
|
|
852
|
+
title: this.options.clientTitle ?? name,
|
|
853
|
+
version: this.options.clientVersion ?? "0.0.0"
|
|
854
|
+
},
|
|
855
|
+
capabilities: {
|
|
856
|
+
experimentalApi: true,
|
|
857
|
+
requestAttestation: false
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const response = await connection.request(
|
|
861
|
+
"initialize",
|
|
862
|
+
params,
|
|
863
|
+
this.requestTimeoutMs
|
|
864
|
+
);
|
|
865
|
+
this.initializeResponse = response;
|
|
866
|
+
return response;
|
|
867
|
+
}
|
|
868
|
+
async getConnection() {
|
|
869
|
+
if (this.connection) return this.connection;
|
|
870
|
+
let transport;
|
|
871
|
+
if (this.transportFactory !== null) {
|
|
872
|
+
transport = this.transportFactory(this.options.command ?? "codex");
|
|
873
|
+
} else {
|
|
874
|
+
const command = await this.resolveCommand();
|
|
875
|
+
transport = new StdioJsonRpcTransport2({
|
|
876
|
+
command,
|
|
877
|
+
args: ["app-server"],
|
|
878
|
+
...this.options.env !== void 0 ? { env: this.options.env } : {},
|
|
879
|
+
logger: this.logger
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const connection = new JsonRpcConnection2(transport, this.requestTimeoutMs, void 0, {
|
|
883
|
+
logger: this.logger,
|
|
884
|
+
logContext: { owner: "codex-oneshot-client" }
|
|
885
|
+
});
|
|
886
|
+
connection.setNotificationHandler((method, params) => {
|
|
887
|
+
this.handleNotification(method, params);
|
|
888
|
+
});
|
|
889
|
+
connection.setRequestHandler((method, params) => this.handleServerRequest(method, params));
|
|
890
|
+
await connection.connect();
|
|
891
|
+
this.connection = connection;
|
|
892
|
+
return connection;
|
|
893
|
+
}
|
|
894
|
+
waitForTurn(threadId, turnId) {
|
|
895
|
+
if (this.pendingTurn) {
|
|
896
|
+
throw new Error("codex one-shot client already has an active turn");
|
|
897
|
+
}
|
|
898
|
+
return new Promise(
|
|
899
|
+
(resolve, reject) => {
|
|
900
|
+
const timer = setTimeout(() => {
|
|
901
|
+
this.pendingTurn = null;
|
|
902
|
+
reject(new Error("codex one-shot turn timed out"));
|
|
903
|
+
}, this.turnTimeoutMs);
|
|
904
|
+
this.pendingTurn = {
|
|
905
|
+
threadId,
|
|
906
|
+
turnId,
|
|
907
|
+
agentMessages: [],
|
|
908
|
+
tokenUsage: null,
|
|
909
|
+
resolve,
|
|
910
|
+
reject,
|
|
911
|
+
timer
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
handleNotification(method, params) {
|
|
917
|
+
if (method === "item/completed") {
|
|
918
|
+
this.handleItemCompleted(params);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (method === "rawResponseItem/completed") {
|
|
922
|
+
this.handleRawResponseItemCompleted(params);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (method === "thread/tokenUsage/updated") {
|
|
926
|
+
this.handleThreadTokenUsageUpdated(params);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (method === "turn/completed") {
|
|
930
|
+
this.handleTurnCompleted(params);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
handleItemCompleted(params) {
|
|
934
|
+
const pending = this.pendingTurn;
|
|
935
|
+
if (!pending || params.threadId !== pending.threadId || params.turnId !== pending.turnId) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (params.item.type === "agentMessage") {
|
|
939
|
+
pending.agentMessages.push(params.item.text);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
handleRawResponseItemCompleted(params) {
|
|
943
|
+
const pending = this.pendingTurn;
|
|
944
|
+
if (!pending || typeof params !== "object" || params === null) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const maybe = params;
|
|
948
|
+
if (maybe.threadId !== pending.threadId || maybe.turnId !== pending.turnId) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const item = maybe.item;
|
|
952
|
+
if (item?.type !== "message" || item.role !== "assistant") {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const text = item.content.filter((content) => content.type === "output_text").map((content) => content.text).join("");
|
|
956
|
+
if (text) {
|
|
957
|
+
pending.agentMessages.push(text);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
handleTurnCompleted(params) {
|
|
961
|
+
const pending = this.pendingTurn;
|
|
962
|
+
if (!pending || params.threadId !== pending.threadId || params.turn.id !== pending.turnId) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
clearTimeout(pending.timer);
|
|
966
|
+
this.pendingTurn = null;
|
|
967
|
+
if (params.turn.status === "failed") {
|
|
968
|
+
pending.reject(new Error(params.turn.error?.message ?? "codex one-shot turn failed"));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (params.turn.status === "interrupted") {
|
|
972
|
+
pending.reject(new DOMException("one-shot turn aborted", "AbortError"));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const rawText = pending.agentMessages.at(-1)?.trim();
|
|
976
|
+
if (!rawText) {
|
|
977
|
+
pending.reject(new Error("codex one-shot turn returned no assistant message"));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
pending.resolve({ rawText, tokenUsage: pending.tokenUsage });
|
|
981
|
+
}
|
|
982
|
+
handleThreadTokenUsageUpdated(params) {
|
|
983
|
+
const pending = this.pendingTurn;
|
|
984
|
+
if (!pending || params.threadId !== pending.threadId || params.turnId !== pending.turnId) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
pending.tokenUsage = params.tokenUsage;
|
|
988
|
+
}
|
|
989
|
+
async handleServerRequest(method, _params) {
|
|
990
|
+
if (method === "item/tool/call") {
|
|
991
|
+
return {
|
|
992
|
+
contentItems: [
|
|
993
|
+
{ type: "inputText", text: "This one-shot run does not expose tools." }
|
|
994
|
+
],
|
|
995
|
+
success: false
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
this.logger.debug("unhandled codex server request", { method });
|
|
999
|
+
return {};
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
function modelToOption(model) {
|
|
1003
|
+
return {
|
|
1004
|
+
id: model.id,
|
|
1005
|
+
model: model.model,
|
|
1006
|
+
displayName: model.displayName,
|
|
1007
|
+
description: model.description,
|
|
1008
|
+
hidden: model.hidden,
|
|
1009
|
+
inputModalities: model.inputModalities,
|
|
1010
|
+
defaultServiceTier: model.defaultServiceTier,
|
|
1011
|
+
isDefault: model.isDefault
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function imagePathsToLocalImageInputs(imagePaths) {
|
|
1015
|
+
return imagePaths.map((path) => ({ type: "localImage", path }));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/codex-thread-config.ts
|
|
1019
|
+
var DISABLE_CODING_AGENT_THREAD_CONFIG = {
|
|
1020
|
+
web_search: "disabled",
|
|
1021
|
+
include_permissions_instructions: false,
|
|
1022
|
+
include_apps_instructions: false,
|
|
1023
|
+
include_collaboration_mode_instructions: false,
|
|
1024
|
+
include_environment_context: false,
|
|
1025
|
+
skills: {
|
|
1026
|
+
include_instructions: false
|
|
1027
|
+
},
|
|
1028
|
+
features: {
|
|
1029
|
+
apps: false,
|
|
1030
|
+
plugins: false,
|
|
1031
|
+
tool_suggest: false,
|
|
1032
|
+
image_generation: false,
|
|
1033
|
+
multi_agent: false,
|
|
1034
|
+
goals: false
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// src/chat/define-tool.ts
|
|
1039
|
+
import { z } from "zod";
|
|
1040
|
+
function defineTool(spec) {
|
|
1041
|
+
return spec;
|
|
1042
|
+
}
|
|
1043
|
+
function toDynamicToolSpec(spec) {
|
|
1044
|
+
return {
|
|
1045
|
+
namespace: spec.namespace,
|
|
1046
|
+
name: spec.name,
|
|
1047
|
+
description: spec.description,
|
|
1048
|
+
inputSchema: z.toJSONSchema(spec.argsSchema)
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/chat/tool-catalog.ts
|
|
1053
|
+
import "zod";
|
|
1054
|
+
function buildToolCatalog(catalog) {
|
|
1055
|
+
return catalog.map(toDynamicToolSpec);
|
|
1056
|
+
}
|
|
1057
|
+
async function dispatchToolCall(params, catalog) {
|
|
1058
|
+
const entry = catalog.find((tool) => tool.name === params.tool);
|
|
1059
|
+
if (entry === void 0) {
|
|
1060
|
+
return errorResponse(`Unknown tool: ${params.tool}`);
|
|
1061
|
+
}
|
|
1062
|
+
if (params.namespace !== null && params.namespace !== entry.namespace) {
|
|
1063
|
+
return errorResponse(`Tool "${params.tool}" is not in namespace "${params.namespace}".`);
|
|
1064
|
+
}
|
|
1065
|
+
const parsed = entry.argsSchema.safeParse(params.arguments);
|
|
1066
|
+
if (!parsed.success) {
|
|
1067
|
+
return errorResponse(`Invalid arguments for "${params.tool}": ${formatZodError(parsed.error)}`);
|
|
1068
|
+
}
|
|
1069
|
+
let result;
|
|
1070
|
+
try {
|
|
1071
|
+
result = await entry.dispatch(parsed.data, { threadId: params.threadId });
|
|
1072
|
+
} catch (cause) {
|
|
1073
|
+
return errorResponse(
|
|
1074
|
+
`Tool "${params.tool}" failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
if (!result.ok) {
|
|
1078
|
+
return errorResponse(result.error);
|
|
1079
|
+
}
|
|
1080
|
+
if ("contentItems" in result) {
|
|
1081
|
+
return { success: true, contentItems: result.contentItems };
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
success: true,
|
|
1085
|
+
contentItems: [{ type: "inputText", text: JSON.stringify(result.data) }]
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function errorResponse(message) {
|
|
1089
|
+
return {
|
|
1090
|
+
success: false,
|
|
1091
|
+
contentItems: [{ type: "inputText", text: message }]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function formatZodError(error) {
|
|
1095
|
+
return error.issues.map((issue) => {
|
|
1096
|
+
const path = issue.path.join(".");
|
|
1097
|
+
return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
|
|
1098
|
+
}).join("; ");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/chat/chat-thread-controller.ts
|
|
1102
|
+
import {
|
|
1103
|
+
noopLogger as noopLogger3
|
|
1104
|
+
} from "@pwrdrvr/agent-core";
|
|
1105
|
+
var RATE_LIMIT_TURNS = 5;
|
|
1106
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1107
|
+
var ChatThreadController = class {
|
|
1108
|
+
deps;
|
|
1109
|
+
logger;
|
|
1110
|
+
turns = /* @__PURE__ */ new Map();
|
|
1111
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1112
|
+
/** Per-thread recent turn timestamps for rate limiting. */
|
|
1113
|
+
turnTimestamps = /* @__PURE__ */ new Map();
|
|
1114
|
+
threadModels = /* @__PURE__ */ new Map();
|
|
1115
|
+
wired = false;
|
|
1116
|
+
constructor(deps) {
|
|
1117
|
+
this.deps = deps;
|
|
1118
|
+
this.logger = deps.logger ?? noopLogger3;
|
|
1119
|
+
}
|
|
1120
|
+
/** Wire the shared backend's subscription hooks ONCE. Idempotent. */
|
|
1121
|
+
wire() {
|
|
1122
|
+
if (this.wired) return;
|
|
1123
|
+
this.wired = true;
|
|
1124
|
+
const { client } = this.deps;
|
|
1125
|
+
client.onEvent((event) => this.onBackendEvent(event));
|
|
1126
|
+
client.onToolCall((call) => this.onToolCall(call));
|
|
1127
|
+
client.onApprovalRequest((method, params) => this.onApprovalRequest(method, params));
|
|
1128
|
+
}
|
|
1129
|
+
now() {
|
|
1130
|
+
return this.deps.now ? this.deps.now() : Date.now();
|
|
1131
|
+
}
|
|
1132
|
+
// ---- thread lifecycle ----
|
|
1133
|
+
async createThread(opts = {}) {
|
|
1134
|
+
const anchorId = opts.anchorId ?? null;
|
|
1135
|
+
const settings = await this.deps.readSettings();
|
|
1136
|
+
const baseInstructions = this.deps.buildSystemPrompt({ settings, anchorId });
|
|
1137
|
+
const displayName = opts.name && opts.name.trim().length > 0 ? opts.name.trim() : this.defaultName();
|
|
1138
|
+
const preparedDir = await this.deps.store.prepareThreadDir(displayName);
|
|
1139
|
+
const startOptions = {
|
|
1140
|
+
instructions: baseInstructions,
|
|
1141
|
+
cwd: preparedDir.path,
|
|
1142
|
+
workspaceRoots: [preparedDir.path]
|
|
1143
|
+
};
|
|
1144
|
+
if (this.deps.approvalPolicy !== void 0) startOptions.approvalPolicy = this.deps.approvalPolicy;
|
|
1145
|
+
if (this.deps.sandbox !== void 0) startOptions.sandbox = this.deps.sandbox;
|
|
1146
|
+
if (this.deps.model !== void 0) startOptions.model = this.deps.model;
|
|
1147
|
+
if (this.deps.modelProvider !== void 0) startOptions.modelProvider = this.deps.modelProvider;
|
|
1148
|
+
if (this.deps.serviceName !== void 0) startOptions.serviceName = this.deps.serviceName;
|
|
1149
|
+
if (this.deps.catalog !== void 0) startOptions.tools = this.deps.catalog;
|
|
1150
|
+
if (this.deps.threadConfig !== void 0) startOptions.config = this.deps.threadConfig;
|
|
1151
|
+
if (this.deps.threadEnvironments !== void 0) {
|
|
1152
|
+
startOptions.environments = this.deps.threadEnvironments;
|
|
1153
|
+
}
|
|
1154
|
+
let started;
|
|
1155
|
+
try {
|
|
1156
|
+
started = await this.deps.client.startThread(startOptions);
|
|
1157
|
+
} catch (cause) {
|
|
1158
|
+
await this.deps.store.discardPreparedThreadDir(preparedDir).catch(() => void 0);
|
|
1159
|
+
throw cause;
|
|
1160
|
+
}
|
|
1161
|
+
await this.deps.client.clearThreadGitInfo?.(started.threadId).catch((cause) => {
|
|
1162
|
+
this.logger.warn("chat thread git metadata clear failed", {
|
|
1163
|
+
threadId: started.threadId,
|
|
1164
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
this.threadModels.set(started.threadId, {
|
|
1168
|
+
model: started.model ?? null,
|
|
1169
|
+
modelProvider: started.modelProvider ?? null,
|
|
1170
|
+
serviceTier: started.serviceTier ?? null
|
|
1171
|
+
});
|
|
1172
|
+
const record = await this.deps.store.create({
|
|
1173
|
+
threadId: started.threadId,
|
|
1174
|
+
name: displayName,
|
|
1175
|
+
anchorId,
|
|
1176
|
+
preparedDir
|
|
1177
|
+
});
|
|
1178
|
+
const view = this.toView(record);
|
|
1179
|
+
this.deps.broadcast({ type: "thread_updated", thread: view });
|
|
1180
|
+
return view;
|
|
1181
|
+
}
|
|
1182
|
+
async listThreads(opts = {}) {
|
|
1183
|
+
const listOpts = {
|
|
1184
|
+
includeArchived: opts.includeArchived ?? false,
|
|
1185
|
+
...opts.anchorId !== void 0 ? { anchorId: opts.anchorId } : {}
|
|
1186
|
+
};
|
|
1187
|
+
const records = await this.deps.store.list(listOpts);
|
|
1188
|
+
return records.map((r) => this.toView(r));
|
|
1189
|
+
}
|
|
1190
|
+
async rename(threadId, name) {
|
|
1191
|
+
const record = await this.deps.store.update(threadId, { name: name.trim() });
|
|
1192
|
+
const view = this.toView(record);
|
|
1193
|
+
this.deps.broadcast({ type: "thread_updated", thread: view });
|
|
1194
|
+
return view;
|
|
1195
|
+
}
|
|
1196
|
+
async archive(threadId, archived) {
|
|
1197
|
+
const record = await this.deps.store.update(threadId, { archived });
|
|
1198
|
+
if (archived) await this.deps.client.archiveThread?.(threadId).catch(() => void 0);
|
|
1199
|
+
const view = this.toView(record);
|
|
1200
|
+
this.deps.broadcast({ type: "thread_updated", thread: view });
|
|
1201
|
+
return view;
|
|
1202
|
+
}
|
|
1203
|
+
// ---- turns ----
|
|
1204
|
+
async sendMessage(input) {
|
|
1205
|
+
const { threadId } = input;
|
|
1206
|
+
if (this.turns.has(threadId)) {
|
|
1207
|
+
throw new Error("a turn is already in progress for this thread");
|
|
1208
|
+
}
|
|
1209
|
+
this.enforceRateLimit(threadId);
|
|
1210
|
+
if (input.anchorId !== void 0 && input.anchorId !== null) {
|
|
1211
|
+
await this.deps.store.appendAnchor(threadId, input.anchorId);
|
|
1212
|
+
}
|
|
1213
|
+
const userMessage = {
|
|
1214
|
+
id: this.randomId(),
|
|
1215
|
+
role: "user",
|
|
1216
|
+
text: input.text,
|
|
1217
|
+
createdAt: this.now()
|
|
1218
|
+
};
|
|
1219
|
+
await this.commitMessage(threadId, userMessage);
|
|
1220
|
+
const settingsSnapshot = await this.deps.readSettings();
|
|
1221
|
+
const anchorForTurn = input.anchorId ?? await this.currentAnchor(threadId);
|
|
1222
|
+
let turnText = input.text;
|
|
1223
|
+
if (anchorForTurn !== null && this.deps.buildTurnContext !== void 0) {
|
|
1224
|
+
turnText = `${this.deps.buildTurnContext(anchorForTurn)}
|
|
1225
|
+
|
|
1226
|
+
${input.text}`;
|
|
1227
|
+
}
|
|
1228
|
+
const turnInput = { text: turnText };
|
|
1229
|
+
if (input.imagePaths !== void 0) turnInput.imagePaths = input.imagePaths;
|
|
1230
|
+
const startTurnOptions = {
|
|
1231
|
+
threadId,
|
|
1232
|
+
input: turnInput,
|
|
1233
|
+
reasoning: this.deps.effort ?? "medium"
|
|
1234
|
+
};
|
|
1235
|
+
let turnId;
|
|
1236
|
+
try {
|
|
1237
|
+
const started = await this.deps.client.startTurn(startTurnOptions);
|
|
1238
|
+
turnId = started.turnId;
|
|
1239
|
+
} catch (cause) {
|
|
1240
|
+
const failed = {
|
|
1241
|
+
id: this.randomId(),
|
|
1242
|
+
role: "assistant",
|
|
1243
|
+
text: "",
|
|
1244
|
+
createdAt: this.now()
|
|
1245
|
+
};
|
|
1246
|
+
await this.commitMessage(threadId, failed);
|
|
1247
|
+
this.logger.warn("chat turn start failed", {
|
|
1248
|
+
threadId,
|
|
1249
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
1250
|
+
});
|
|
1251
|
+
throw cause;
|
|
1252
|
+
}
|
|
1253
|
+
const assistantMessageId = this.randomId();
|
|
1254
|
+
this.turns.set(threadId, {
|
|
1255
|
+
turnId,
|
|
1256
|
+
assistantMessageId,
|
|
1257
|
+
buffer: "",
|
|
1258
|
+
settingsSnapshot,
|
|
1259
|
+
tokenUsage: null
|
|
1260
|
+
});
|
|
1261
|
+
this.recordTurn(threadId);
|
|
1262
|
+
await this.broadcastThreadStatus(threadId, { kind: "streaming", turnId });
|
|
1263
|
+
return { turnId };
|
|
1264
|
+
}
|
|
1265
|
+
async getHistory(threadId) {
|
|
1266
|
+
return this.readJournalMessages(threadId);
|
|
1267
|
+
}
|
|
1268
|
+
async interrupt(threadId) {
|
|
1269
|
+
const turn = this.turns.get(threadId);
|
|
1270
|
+
if (turn === void 0) return;
|
|
1271
|
+
await this.deps.client.interruptTurn(threadId).catch(() => void 0);
|
|
1272
|
+
await this.finalizeAssistant(threadId, "interrupted");
|
|
1273
|
+
this.deps.broadcast({ type: "turn_interrupted", threadId, turnId: turn.turnId });
|
|
1274
|
+
}
|
|
1275
|
+
// ---- approval flow ----
|
|
1276
|
+
async resolveApproval(input) {
|
|
1277
|
+
const key = approvalKey(input.threadId, input.turnId, input.approvalId);
|
|
1278
|
+
const pending = this.pendingApprovals.get(key);
|
|
1279
|
+
if (pending === void 0) {
|
|
1280
|
+
this.logger.warn("resolveApproval: no matching pending approval (stale?)", { key });
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this.pendingApprovals.delete(key);
|
|
1284
|
+
pending.resolve(input.decision);
|
|
1285
|
+
}
|
|
1286
|
+
// ---- backend subscription handlers ----
|
|
1287
|
+
onBackendEvent(event) {
|
|
1288
|
+
switch (event.kind) {
|
|
1289
|
+
case "agent_message_delta":
|
|
1290
|
+
this.onDelta(event.threadId, event.turnId, event.itemId, event.delta);
|
|
1291
|
+
return;
|
|
1292
|
+
case "token_usage":
|
|
1293
|
+
this.onTokenUsage(event.threadId, event.turnId, event.usage);
|
|
1294
|
+
return;
|
|
1295
|
+
case "thread_settings":
|
|
1296
|
+
this.threadModels.set(event.settings.threadId, {
|
|
1297
|
+
model: event.settings.model ?? null,
|
|
1298
|
+
modelProvider: event.settings.modelProvider ?? null,
|
|
1299
|
+
serviceTier: event.settings.serviceTier ?? null
|
|
1300
|
+
});
|
|
1301
|
+
return;
|
|
1302
|
+
case "turn_completed":
|
|
1303
|
+
void this.onTurnCompleted(event.threadId, event.turnId, event.status);
|
|
1304
|
+
return;
|
|
1305
|
+
default:
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
onDelta(threadId, turnId, itemId, delta) {
|
|
1310
|
+
const turn = this.turns.get(threadId);
|
|
1311
|
+
if (turn === void 0 || turn.turnId !== turnId) return;
|
|
1312
|
+
turn.buffer += delta;
|
|
1313
|
+
this.deps.broadcast({
|
|
1314
|
+
type: "stream_delta",
|
|
1315
|
+
threadId,
|
|
1316
|
+
turnId,
|
|
1317
|
+
messageId: turn.assistantMessageId,
|
|
1318
|
+
delta
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async onTurnCompleted(threadId, turnId, status) {
|
|
1322
|
+
const turn = this.turns.get(threadId);
|
|
1323
|
+
if (turn === void 0 || turn.turnId !== turnId) return;
|
|
1324
|
+
await this.finalizeAssistant(threadId, status);
|
|
1325
|
+
}
|
|
1326
|
+
onTokenUsage(threadId, turnId, usage) {
|
|
1327
|
+
const turn = this.turns.get(threadId);
|
|
1328
|
+
if (turn === void 0 || turn.turnId !== turnId) return;
|
|
1329
|
+
turn.tokenUsage = usage;
|
|
1330
|
+
}
|
|
1331
|
+
async onToolCall(call) {
|
|
1332
|
+
const params = call.params;
|
|
1333
|
+
const response = this.deps.dispatchToolCall ? await this.deps.dispatchToolCall(params) : {
|
|
1334
|
+
contentItems: [{ type: "inputText", text: "No tools are enabled for this chat yet." }],
|
|
1335
|
+
success: false
|
|
1336
|
+
};
|
|
1337
|
+
this.deps.broadcast({
|
|
1338
|
+
type: "tool_call",
|
|
1339
|
+
threadId: params.threadId,
|
|
1340
|
+
turnId: params.turnId,
|
|
1341
|
+
toolCall: {
|
|
1342
|
+
id: params.callId,
|
|
1343
|
+
name: params.tool,
|
|
1344
|
+
kind: "other",
|
|
1345
|
+
label: humanizeToolCall(params.tool, response.success, this.deps.toolLabels),
|
|
1346
|
+
status: response.success ? "completed" : "failed",
|
|
1347
|
+
args: params.arguments,
|
|
1348
|
+
result: response
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
return response;
|
|
1352
|
+
}
|
|
1353
|
+
async onApprovalRequest(method, params) {
|
|
1354
|
+
const p = params ?? {};
|
|
1355
|
+
let threadId = typeof p.threadId === "string" ? p.threadId : "";
|
|
1356
|
+
let turnId = typeof p.turnId === "string" ? p.turnId : "";
|
|
1357
|
+
if (threadId.length === 0) {
|
|
1358
|
+
const onlyEntry = this.turns.size === 1 ? [...this.turns.entries()][0] : void 0;
|
|
1359
|
+
if (onlyEntry !== void 0) {
|
|
1360
|
+
const [onlyThreadId, onlyTurn] = onlyEntry;
|
|
1361
|
+
threadId = onlyThreadId;
|
|
1362
|
+
if (turnId.length === 0) turnId = onlyTurn.turnId;
|
|
1363
|
+
} else {
|
|
1364
|
+
this.logger.warn("approval request without a routable threadId \u2014 auto-denying", {
|
|
1365
|
+
method,
|
|
1366
|
+
inFlightTurns: this.turns.size
|
|
1367
|
+
});
|
|
1368
|
+
return "denied";
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const approvalId = this.randomId();
|
|
1372
|
+
const request = normalizeApprovalParams(method, params, approvalId);
|
|
1373
|
+
const decision = await new Promise((resolve) => {
|
|
1374
|
+
this.pendingApprovals.set(approvalKey(threadId, turnId, approvalId), {
|
|
1375
|
+
threadId,
|
|
1376
|
+
turnId,
|
|
1377
|
+
approvalId,
|
|
1378
|
+
resolve
|
|
1379
|
+
});
|
|
1380
|
+
this.deps.broadcast({ type: "approval_requested", threadId, turnId, approval: request });
|
|
1381
|
+
void this.broadcastThreadStatus(threadId, { kind: "awaiting_approval", approvalId });
|
|
1382
|
+
});
|
|
1383
|
+
const turn = this.turns.get(threadId);
|
|
1384
|
+
void this.broadcastThreadStatus(
|
|
1385
|
+
threadId,
|
|
1386
|
+
turn ? { kind: "streaming", turnId: turn.turnId } : { kind: "idle" }
|
|
1387
|
+
);
|
|
1388
|
+
return decision;
|
|
1389
|
+
}
|
|
1390
|
+
// ---- internals ----
|
|
1391
|
+
async finalizeAssistant(threadId, status) {
|
|
1392
|
+
const turn = this.turns.get(threadId);
|
|
1393
|
+
if (turn === void 0) return;
|
|
1394
|
+
this.turns.delete(threadId);
|
|
1395
|
+
const message = {
|
|
1396
|
+
id: turn.assistantMessageId,
|
|
1397
|
+
role: "assistant",
|
|
1398
|
+
text: turn.buffer,
|
|
1399
|
+
createdAt: this.now()
|
|
1400
|
+
};
|
|
1401
|
+
this.recordUsage(threadId, turn).catch((cause) => {
|
|
1402
|
+
this.logger.warn("chat usage accounting failed", {
|
|
1403
|
+
threadId,
|
|
1404
|
+
turnId: turn.turnId,
|
|
1405
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
await this.commitMessage(threadId, message);
|
|
1409
|
+
await this.broadcastThreadStatus(threadId, { kind: "idle" });
|
|
1410
|
+
}
|
|
1411
|
+
async recordUsage(threadId, turn) {
|
|
1412
|
+
if (turn.tokenUsage === null) return;
|
|
1413
|
+
const model = this.threadModels.get(threadId);
|
|
1414
|
+
const usage = turn.tokenUsage;
|
|
1415
|
+
await this.deps.store.recordUsage({
|
|
1416
|
+
threadId,
|
|
1417
|
+
turnId: turn.turnId,
|
|
1418
|
+
...model?.model != null ? { model: model.model } : {},
|
|
1419
|
+
usage,
|
|
1420
|
+
...usage.contextWindow !== void 0 ? { contextWindow: usage.contextWindow } : {},
|
|
1421
|
+
at: this.now()
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
async commitMessage(threadId, message) {
|
|
1425
|
+
const entry = { kind: "message", message };
|
|
1426
|
+
await this.deps.store.journalAppend(threadId, entry);
|
|
1427
|
+
this.deps.broadcast({ type: "message_committed", threadId, message });
|
|
1428
|
+
}
|
|
1429
|
+
async readJournalMessages(threadId) {
|
|
1430
|
+
const entries = await this.deps.store.readJournal(threadId).catch(() => []);
|
|
1431
|
+
const messages = [];
|
|
1432
|
+
for (const entry of entries) {
|
|
1433
|
+
if (entry !== null && typeof entry === "object" && entry.kind === "message") {
|
|
1434
|
+
const m = entry.message;
|
|
1435
|
+
if (m !== void 0) messages.push(m);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return messages;
|
|
1439
|
+
}
|
|
1440
|
+
async currentAnchor(threadId) {
|
|
1441
|
+
const record = await this.deps.store.get(threadId);
|
|
1442
|
+
return record?.anchorId ?? null;
|
|
1443
|
+
}
|
|
1444
|
+
enforceRateLimit(threadId) {
|
|
1445
|
+
const stamps = this.turnTimestamps.get(threadId) ?? [];
|
|
1446
|
+
const cutoff = this.now() - RATE_LIMIT_WINDOW_MS;
|
|
1447
|
+
const recent = stamps.filter((t) => t >= cutoff);
|
|
1448
|
+
if (recent.length >= RATE_LIMIT_TURNS) {
|
|
1449
|
+
throw new Error(`rate limit: max ${RATE_LIMIT_TURNS} turns per minute for this thread`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
recordTurn(threadId) {
|
|
1453
|
+
const stamps = this.turnTimestamps.get(threadId) ?? [];
|
|
1454
|
+
const cutoff = this.now() - RATE_LIMIT_WINDOW_MS;
|
|
1455
|
+
const recent = stamps.filter((t) => t >= cutoff);
|
|
1456
|
+
recent.push(this.now());
|
|
1457
|
+
this.turnTimestamps.set(threadId, recent);
|
|
1458
|
+
}
|
|
1459
|
+
async broadcastThreadStatus(threadId, status) {
|
|
1460
|
+
const record = await this.deps.store.get(threadId);
|
|
1461
|
+
if (record === null) return;
|
|
1462
|
+
this.deps.broadcast({ type: "thread_updated", thread: this.toView(record, status) });
|
|
1463
|
+
}
|
|
1464
|
+
toView(record, status) {
|
|
1465
|
+
const turn = this.turns.get(record.threadId);
|
|
1466
|
+
const resolved = status ?? (turn !== void 0 ? { kind: "streaming", turnId: turn.turnId } : { kind: "idle" });
|
|
1467
|
+
return {
|
|
1468
|
+
threadId: record.threadId,
|
|
1469
|
+
name: record.name,
|
|
1470
|
+
createdAt: record.createdAt,
|
|
1471
|
+
modifiedAt: record.modifiedAt,
|
|
1472
|
+
anchorId: record.anchorId,
|
|
1473
|
+
archived: record.archived,
|
|
1474
|
+
pinned: record.pinned,
|
|
1475
|
+
lastMessagePreview: "",
|
|
1476
|
+
status: resolved
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
defaultName() {
|
|
1480
|
+
return `Chat ${localDateStamp(new Date(this.now()))}`;
|
|
1481
|
+
}
|
|
1482
|
+
randomId() {
|
|
1483
|
+
return globalThis.crypto.randomUUID();
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
function localDateStamp(d) {
|
|
1487
|
+
const year = d.getFullYear();
|
|
1488
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
1489
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1490
|
+
return `${year}-${month}-${day}`;
|
|
1491
|
+
}
|
|
1492
|
+
function approvalKey(threadId, turnId, approvalId) {
|
|
1493
|
+
return `${threadId}::${turnId}::${approvalId}`;
|
|
1494
|
+
}
|
|
1495
|
+
function humanizeToolCall(tool, ok, labels = {}) {
|
|
1496
|
+
const label = labels[tool] ?? tool;
|
|
1497
|
+
return ok ? label : `Couldn't: ${label.toLowerCase()}`;
|
|
1498
|
+
}
|
|
1499
|
+
function normalizeApprovalParams(method, params, approvalId) {
|
|
1500
|
+
const p = params ?? {};
|
|
1501
|
+
const summary = typeof p.summary === "string" ? p.summary : typeof p.reason === "string" ? p.reason : typeof p.command === "string" ? p.command : void 0;
|
|
1502
|
+
const kind = method.includes("commandExecution") ? "exec" : method.includes("fileChange") ? "patch" : method.includes("tool") ? "tool" : "other";
|
|
1503
|
+
const request = { id: approvalId, method, kind, params };
|
|
1504
|
+
if (summary !== void 0) request.summary = summary;
|
|
1505
|
+
return request;
|
|
1506
|
+
}
|
|
1507
|
+
export {
|
|
1508
|
+
CODEX_APPROVAL_METHODS,
|
|
1509
|
+
CODEX_NOTIFICATION_METHODS,
|
|
1510
|
+
CODEX_TOOL_CALL_METHOD,
|
|
1511
|
+
ChatThreadController,
|
|
1512
|
+
CodexOneShotClient,
|
|
1513
|
+
CodexThreadClient,
|
|
1514
|
+
DISABLE_CODING_AGENT_THREAD_CONFIG,
|
|
1515
|
+
buildToolCatalog,
|
|
1516
|
+
defineTool,
|
|
1517
|
+
dispatchToolCall,
|
|
1518
|
+
localDateStamp,
|
|
1519
|
+
normalizeApprovalRequest,
|
|
1520
|
+
normalizeDynamicToolCall,
|
|
1521
|
+
normalizeNotification,
|
|
1522
|
+
normalizeThreadItemToolCall,
|
|
1523
|
+
normalizeThreadSettings,
|
|
1524
|
+
normalizeTokenUsage,
|
|
1525
|
+
toDynamicToolSpec
|
|
1526
|
+
};
|
|
1527
|
+
//# sourceMappingURL=index.js.map
|