@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/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