@messenger-agent/codex-agent 0.24.0-alpha.2 → 0.24.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,909 +1,2 @@
1
- import { Hono } from "hono";
2
- import { HTTPException } from "hono/http-exception";
3
- import { zValidator } from "@hono/zod-validator";
4
- import { streamSSE } from "hono/streaming";
5
- import { randomUUID } from "node:crypto";
6
- import { sessionManager, WorkdirNotFoundError } from "../codex.js";
7
- import { logger } from "@messenger-agent/shared/logger";
8
- import { ChatBodySchema, RequestUserInputToolPartSchema } from "../schemas.js";
9
- import { appConfig } from "../config.js";
10
- import { setManagedTaskToolContext } from "../dynamic-tools.js";
11
- import { resolveAgentWorkspaceRoot } from "@messenger-agent/shared/agent-workspaces";
12
- import { saveUploadedFilePart } from "@messenger-agent/shared/uploaded-files";
13
- import { commandExecutionItem, addTokenUsage, displayCommand, dynamicToolCallItem, dynamicToolCallOutput, fileChangeItem, itemText, mcpToolCallItem, mcpToolCallOutput, toUsage, turnOptions, webSearchItem, webSearchPayload, } from "./shared.js";
14
- const chat = new Hono();
15
- const liveConversations = new Map();
16
- const activeStreams = new Set();
17
- const pendingDiffs = new Map();
18
- const pendingClientToolWaits = new Map();
19
- export function codexActivitySnapshot() {
20
- const waitingConversations = new Set([...pendingClientToolWaits.values()].map((wait) => wait.conversationId).filter((id) => !!id));
21
- return { active: activeStreams.size, waiting: waitingConversations.size };
22
- }
23
- function conversationNotFound(conversationId) {
24
- return {
25
- type: "not_found_error",
26
- code: "conversation_not_found",
27
- message: `Conversation not found or already completed: ${conversationId}`,
28
- param: "conversation_id",
29
- };
30
- }
31
- function matrixThreadRef(headers) {
32
- const roomId = chatHeader(headers, "X-Matrix-Room-Id")?.trim();
33
- const threadId = chatHeader(headers, "X-Matrix-Thread-ID")?.trim();
34
- return roomId && threadId ? `${roomId}/${threadId}` : undefined;
35
- }
36
- function matrixEventRef(headers) {
37
- const roomId = chatHeader(headers, "X-Matrix-Room-Id")?.trim();
38
- const eventId = chatHeader(headers, "X-Matrix-Event-ID")?.trim();
39
- const threadRootEventId = chatHeader(headers, "X-Matrix-Thread-ID")?.trim();
40
- if (!roomId || !eventId)
41
- return undefined;
42
- return { roomId, eventId, ...(threadRootEventId ? { threadRootEventId } : null) };
43
- }
44
- export class ChatRequestError extends Error {
45
- status;
46
- constructor(status, message) {
47
- super(message);
48
- this.status = status;
49
- this.name = "ChatRequestError";
50
- }
51
- }
52
- function chatHeader(headers, name) {
53
- return headers?.get(name) ?? undefined;
54
- }
55
- export async function cancelCodexConversation(conversationId) {
56
- const state = liveConversations.get(conversationId);
57
- if (!state)
58
- return false;
59
- state.cancelRequested = true;
60
- while (true) {
61
- const transition = state.transition;
62
- await transition;
63
- if (transition !== state.transition)
64
- continue;
65
- break;
66
- }
67
- if (state.thread && state.turnId) {
68
- if (!state.interrupt) {
69
- state.interrupt = state.thread.interrupt(state.turnId).finally(() => {
70
- state.interrupt = undefined;
71
- });
72
- }
73
- await state.interrupt;
74
- }
75
- state.owner?.abortController.abort();
76
- state.resolveOwnerReady?.();
77
- if (liveConversations.get(conversationId) === state) {
78
- liveConversations.delete(conversationId);
79
- }
80
- return true;
81
- }
82
- function reserveLiveConversation(conversationId) {
83
- let state = liveConversations.get(conversationId);
84
- if (!state) {
85
- state = {
86
- conversationId,
87
- generation: 0,
88
- cancelRequested: false,
89
- transition: Promise.resolve(),
90
- };
91
- liveConversations.set(conversationId, state);
92
- }
93
- const generation = ++state.generation;
94
- const abortController = new AbortController();
95
- const ownsOutput = !state.owner;
96
- if (ownsOutput) {
97
- state.owner = { generation, abortController };
98
- state.ownerReady = new Promise((resolve) => {
99
- state.resolveOwnerReady = resolve;
100
- });
101
- }
102
- return { state, generation, abortController, ownsOutput };
103
- }
104
- function isCurrentReservation(reservation) {
105
- return (liveConversations.get(reservation.state.conversationId) === reservation.state &&
106
- reservation.state.owner?.generation === reservation.generation);
107
- }
108
- async function runLiveTransition(state, operation) {
109
- const previous = state.transition;
110
- let release;
111
- const current = new Promise((resolve) => {
112
- release = resolve;
113
- });
114
- state.transition = previous.then(() => current);
115
- await previous;
116
- try {
117
- return await operation();
118
- }
119
- finally {
120
- release();
121
- }
122
- }
123
- async function startOrSteerLiveTurn(reservation, thread, prompt, options) {
124
- if (!reservation.ownsOutput && !reservation.state.turnId) {
125
- await reservation.state.ownerReady;
126
- }
127
- try {
128
- return await runLiveTransition(reservation.state, async () => {
129
- const { state } = reservation;
130
- if (liveConversations.get(state.conversationId) !== state || state.cancelRequested) {
131
- throw new DOMException("Chat stream was cancelled before the turn started", "AbortError");
132
- }
133
- state.thread = thread;
134
- const result = state.turnId
135
- ? await thread.steerStreamed(prompt, state.turnId, { signal: reservation.abortController.signal })
136
- : await thread.runStreamed(prompt, { ...options, signal: reservation.abortController.signal });
137
- state.turnId = result.turnId;
138
- return result;
139
- });
140
- }
141
- finally {
142
- if (reservation.ownsOutput)
143
- reservation.state.resolveOwnerReady?.();
144
- }
145
- }
146
- async function continueLiveTurn(reservation, thread, operation) {
147
- return runLiveTransition(reservation.state, async () => {
148
- const { state } = reservation;
149
- if (!isCurrentReservation(reservation) || state.cancelRequested) {
150
- throw new DOMException("Chat stream was cancelled before user input was submitted", "AbortError");
151
- }
152
- state.thread = thread;
153
- const result = await operation();
154
- state.turnId = result.turnId;
155
- state.resolveOwnerReady?.();
156
- return result;
157
- });
158
- }
159
- async function submitLiveUserInput(reservation, thread, itemId, response) {
160
- return runLiveTransition(reservation.state, async () => {
161
- const { state } = reservation;
162
- if (liveConversations.get(state.conversationId) !== state || state.cancelRequested) {
163
- throw new DOMException("Chat stream was cancelled before user input was submitted", "AbortError");
164
- }
165
- state.thread = thread;
166
- const turnId = await thread.submitPendingUserInput(itemId, response);
167
- if (turnId)
168
- state.turnId = turnId;
169
- return turnId;
170
- });
171
- }
172
- async function restartAfterPendingUserInput(reservation, thread, pendingWait, prompt, options) {
173
- return runLiveTransition(reservation.state, async () => {
174
- const { state } = reservation;
175
- if (liveConversations.get(state.conversationId) !== state || state.cancelRequested) {
176
- throw new DOMException("Chat stream was cancelled before the turn started", "AbortError");
177
- }
178
- state.thread = thread;
179
- await thread.interruptPendingUserInput(pendingWait.turnId, pendingWait.toolCallId);
180
- state.turnId = undefined;
181
- const result = await thread.runStreamed(prompt, { ...options, signal: reservation.abortController.signal });
182
- state.turnId = result.turnId;
183
- return result;
184
- });
185
- }
186
- function updateLiveTurn(reservation, thread, turnId) {
187
- reservation.state.thread = thread;
188
- reservation.state.turnId = turnId;
189
- }
190
- function completeLiveTurn(reservation) {
191
- const { state } = reservation;
192
- if (!isCurrentReservation(reservation))
193
- return;
194
- state.owner = undefined;
195
- state.resolveOwnerReady?.();
196
- state.turnId = undefined;
197
- if (liveConversations.get(state.conversationId) === state) {
198
- liveConversations.delete(state.conversationId);
199
- }
200
- }
201
- function releaseLiveOwner(reservation) {
202
- if (isCurrentReservation(reservation)) {
203
- reservation.state.owner = undefined;
204
- reservation.state.resolveOwnerReady?.();
205
- if (!reservation.state.turnId && liveConversations.get(reservation.state.conversationId) === reservation.state) {
206
- liveConversations.delete(reservation.state.conversationId);
207
- }
208
- }
209
- }
210
- function abandonLiveReservation(reservation) {
211
- if (!isCurrentReservation(reservation))
212
- return;
213
- releaseLiveOwner(reservation);
214
- reservation.state.resolveOwnerReady?.();
215
- if (!reservation.state.turnId && liveConversations.get(reservation.state.conversationId) === reservation.state) {
216
- liveConversations.delete(reservation.state.conversationId);
217
- }
218
- }
219
- function pendingWaitKey(conversationId, toolCallId) {
220
- return `${conversationId ?? "unknown"}:${toolCallId}`;
221
- }
222
- function pendingWaitForConversation(conversationId) {
223
- if (!conversationId)
224
- return undefined;
225
- return [...pendingClientToolWaits.values()].find((wait) => wait.conversationId === conversationId);
226
- }
227
- function pendingDiffKey(activeConversationId, threadId) {
228
- return activeConversationId ?? threadId;
229
- }
230
- function isUploadedFilePart(part) {
231
- return (part.type === "file" &&
232
- "mediaType" in part &&
233
- typeof part.mediaType === "string" &&
234
- "url" in part &&
235
- typeof part.url === "string" &&
236
- (!("filename" in part) || part.filename === undefined || typeof part.filename === "string"));
237
- }
238
- async function partSummary(part) {
239
- if (part.type === "text" && typeof part.text === "string")
240
- return part.text;
241
- if (isUploadedFilePart(part)) {
242
- return saveUploadedFilePart(part, appConfig.fileUploads.tempDir);
243
- }
244
- throw new HTTPException(400, {
245
- message: `Unsupported message part type: ${part.type}`,
246
- });
247
- }
248
- async function messageToPrompt(parts) {
249
- const summaries = await Promise.all(parts.map(partSummary));
250
- const prompt = summaries
251
- .filter((text) => Boolean(text && text.trim().length > 0))
252
- .join("\n")
253
- .trim();
254
- if (!prompt) {
255
- throw new HTTPException(400, { message: "No supported message content found" });
256
- }
257
- return prompt;
258
- }
259
- function requestUserInputResponse(parts) {
260
- const hasToolPart = parts.some((part) => part.type === "tool");
261
- if (!hasToolPart)
262
- return undefined;
263
- if (parts.length !== 1) {
264
- throw new HTTPException(400, {
265
- message: "When a tool part is present, the message must contain exactly one part",
266
- });
267
- }
268
- const result = RequestUserInputToolPartSchema.safeParse(parts[0]);
269
- if (!result.success) {
270
- throw new HTTPException(400, {
271
- message: `Invalid requestUserInput tool part: ${result.error.message}`,
272
- });
273
- }
274
- return {
275
- itemId: result.data.toolCallId,
276
- response: result.data.output,
277
- sourceTurnId: result.data.sourceTurnId,
278
- };
279
- }
280
- function toolCallId(ids, conversationId, itemId, suffix) {
281
- const key = [conversationId, itemId, suffix].filter(Boolean).join(":");
282
- const existing = ids.get(key);
283
- if (existing)
284
- return existing;
285
- const id = randomUUID();
286
- ids.set(key, id);
287
- return id;
288
- }
289
- function toolErrorText(value, fallback) {
290
- return value && value.length > 0 ? value : fallback;
291
- }
292
- function logStreamEvent(event) {
293
- logger.debug("Received chat stream event:", event);
294
- }
295
- function workdirNotFoundPath(err) {
296
- if (err instanceof WorkdirNotFoundError)
297
- return err.path;
298
- if (err && typeof err === "object" && "path" in err && typeof err.path === "string")
299
- return err.path;
300
- if (err instanceof Error && err.message.startsWith("Workdir not found: ")) {
301
- return err.message.slice("Workdir not found: ".length);
302
- }
303
- return undefined;
304
- }
305
- chat.post("/v1/chat/:conversation_id/cancel", async (c) => {
306
- const conversationId = c.req.param("conversation_id");
307
- try {
308
- if (!(await cancelCodexConversation(conversationId))) {
309
- return c.json({ error: conversationNotFound(conversationId) }, 404);
310
- }
311
- return c.json({ success: true });
312
- }
313
- catch (err) {
314
- const message = err instanceof Error ? err.message : "Failed to interrupt Codex conversation";
315
- logger.error(`Failed to cancel Codex conversation ${conversationId}:`, err);
316
- return c.json({ error: { type: "server_error", code: "cancel_failed", message } }, 500);
317
- }
318
- });
319
- chat.get("/admin/status", (c) => c.json({
320
- activeTaskCount: activeStreams.size,
321
- waitingClientToolCount: pendingClientToolWaits.size,
322
- clientToolWaits: [...pendingClientToolWaits.values()].map(({ conversationId, toolCallId, toolName, since }) => ({
323
- conversationId,
324
- toolCallId,
325
- toolName,
326
- since,
327
- })),
328
- }));
329
- export async function prepareCodexChatStream({ body, headers }) {
330
- const { conversationId, fork, model, message } = body;
331
- const botName = chatHeader(headers, "X-Matrix-Bot-Name");
332
- const workdir = chatHeader(headers, "X-Matrix-Workdir");
333
- const workspaceId = chatHeader(headers, "X-Matrix-Workspace-ID");
334
- const agentMode = chatHeader(headers, "X-Matrix-Agent-Mode");
335
- const requestedLlmProxyBinding = chatHeader(headers, "X-Elevo-LLM-Binding")?.trim() || undefined;
336
- const storedLlmProxyBinding = !fork ? sessionManager.bindingFor(conversationId) : undefined;
337
- if (requestedLlmProxyBinding && storedLlmProxyBinding && requestedLlmProxyBinding !== storedLlmProxyBinding) {
338
- throw new ChatRequestError(409, "LLM proxy binding does not match the existing conversation");
339
- }
340
- const llmProxyBinding = storedLlmProxyBinding ?? requestedLlmProxyBinding;
341
- const threadRef = matrixThreadRef(headers);
342
- const matrixEvent = matrixEventRef(headers);
343
- const explicitUserInputResponse = requestUserInputResponse(message.parts);
344
- const prompt = explicitUserInputResponse ? undefined : await messageToPrompt(message.parts);
345
- const pendingTextWait = explicitUserInputResponse ? undefined : pendingWaitForConversation(conversationId);
346
- const userInputResponse = explicitUserInputResponse;
347
- const interruptedTextWait = prompt && pendingTextWait ? pendingTextWait : undefined;
348
- let liveReservation = !fork && conversationId ? reserveLiveConversation(conversationId) : undefined;
349
- const workspaceRoot = (() => {
350
- try {
351
- return resolveAgentWorkspaceRoot(appConfig, workspaceId);
352
- }
353
- catch (err) {
354
- throw new ChatRequestError(400, err instanceof Error ? err.message : "Invalid workspace");
355
- }
356
- })();
357
- const { thread, isNewSession } = await (async () => {
358
- try {
359
- if (fork) {
360
- if (!("threadId" in fork)) {
361
- throw new ChatRequestError(400, "Codex fork requires threadId and turnId");
362
- }
363
- return await sessionManager.fork(fork, botName, workdir, workspaceId, model, "danger-full-access", llmProxyBinding);
364
- }
365
- return await sessionManager.getOrCreate(conversationId, botName, workdir, workspaceId, model, "danger-full-access", llmProxyBinding);
366
- }
367
- catch (err) {
368
- if (liveReservation)
369
- abandonLiveReservation(liveReservation);
370
- const path = workdirNotFoundPath(err);
371
- if (path) {
372
- throw new ChatRequestError(400, `Workdir not found: ${path}`);
373
- }
374
- throw err;
375
- }
376
- })();
377
- liveReservation ??= reserveLiveConversation(thread.id);
378
- setManagedTaskToolContext(thread.id, {
379
- workspaceRoot,
380
- threadRef,
381
- matrixEvent,
382
- llmProxyBinding,
383
- });
384
- const turnRunOptions = turnOptions(agentMode, model);
385
- return {
386
- run: async (writer, signal) => {
387
- const reservation = liveReservation;
388
- const abortController = reservation.abortController;
389
- activeStreams.add(abortController);
390
- const abortFromParent = () => {
391
- if (isCurrentReservation(reservation)) {
392
- cancelCodexConversation(reservation.state.conversationId).catch((err) => logger.error("Failed to cancel disconnected Codex stream:", err));
393
- }
394
- };
395
- if (signal?.aborted)
396
- abortFromParent();
397
- signal?.addEventListener("abort", abortFromParent, { once: true });
398
- let responseStarted = !!userInputResponse;
399
- let stepStarted = !!userInputResponse;
400
- let completed = false;
401
- let activeConversationId;
402
- let currentThreadId = thread.id;
403
- let currentTurnId;
404
- let liveTurnCompleted = false;
405
- let heartbeatTimer;
406
- const textStartedItems = new Set();
407
- const reasoningStartedItems = new Set();
408
- const deltaReceivedItems = new Set();
409
- const toolIds = new Map();
410
- const compactStarts = new Set();
411
- const compactEnds = new Set();
412
- const subagentActivities = new Set();
413
- let lastErrorMessage;
414
- let pendingUsage;
415
- let turnUsage;
416
- const scheduleHeartbeat = () => {
417
- heartbeatTimer = setTimeout(() => {
418
- writeData({ type: "heartbeat" }).catch(() => { });
419
- }, 60_000);
420
- };
421
- const writeEnvelope = async (message) => {
422
- if (heartbeatTimer)
423
- clearTimeout(heartbeatTimer);
424
- if (message.data === "[DONE]") {
425
- await writer.writeDone();
426
- }
427
- else {
428
- await writer.writeData(JSON.parse(String(message.data)));
429
- }
430
- scheduleHeartbeat();
431
- };
432
- const writeData = async (data) => {
433
- await writeEnvelope({ data: JSON.stringify(data) });
434
- };
435
- const writeDone = async () => {
436
- await writeEnvelope({ data: "[DONE]" });
437
- };
438
- const writePendingDiff = async () => {
439
- const key = pendingDiffKey(activeConversationId, thread.id);
440
- const diff = pendingDiffs.get(key);
441
- if (!diff)
442
- return;
443
- pendingDiffs.delete(key);
444
- await writeData({ type: "data", kind: "diff", data: { diff } });
445
- };
446
- const writeCompactActivity = async (item, phase) => {
447
- if (item.type !== "contextCompaction")
448
- return false;
449
- if (phase === "end" && !compactStarts.has(item.id))
450
- return true;
451
- const emitted = phase === "start" ? compactStarts : compactEnds;
452
- if (emitted.has(item.id))
453
- return true;
454
- emitted.add(item.id);
455
- await writeData({
456
- type: "data",
457
- kind: "compact",
458
- data: phase === "start" ? { phase, activityId: item.id } : { phase, activityId: item.id, status: "completed" },
459
- });
460
- return true;
461
- };
462
- const writeSubagentActivity = async (item) => {
463
- if (item.type !== "subAgentActivity")
464
- return false;
465
- if (subagentActivities.has(item.id))
466
- return true;
467
- subagentActivities.add(item.id);
468
- const id = toolCallId(toolIds, activeConversationId, item.id);
469
- const input = {
470
- activity: item.kind,
471
- agentThreadId: item.agentThreadId,
472
- ...(item.agentPath ? { agentPath: item.agentPath } : null),
473
- };
474
- await writeData({
475
- type: "tool-input-available",
476
- toolCallId: id,
477
- toolName: `subagent_${item.kind}`,
478
- ...(item.agentPath ? { title: item.agentPath } : null),
479
- input,
480
- });
481
- await writeData(item.kind === "interrupted"
482
- ? { type: "tool-output-error", toolCallId: id, errorText: "Subagent interrupted" }
483
- : { type: "tool-output-available", toolCallId: id, output: input });
484
- return true;
485
- };
486
- scheduleHeartbeat();
487
- const activateConversation = (id) => {
488
- if (activeConversationId === id)
489
- return;
490
- activeConversationId = id;
491
- if (isNewSession) {
492
- sessionManager.persist(id, botName, workdir, workspaceId, llmProxyBinding);
493
- }
494
- };
495
- const writeStart = async (id, turnId) => {
496
- if (responseStarted)
497
- return;
498
- responseStarted = true;
499
- activateConversation(id);
500
- await writeData({
501
- type: "start",
502
- messageMetadata: {
503
- conversationId: id,
504
- threadId: id,
505
- ...(turnId ? { turnId } : null),
506
- },
507
- });
508
- await writeData({ type: "start-step" });
509
- stepStarted = true;
510
- };
511
- const writeFinish = async (finishReason) => {
512
- if (completed)
513
- return;
514
- completed = true;
515
- if (stepStarted) {
516
- await writePendingDiff();
517
- if (finishReason === "stop" && pendingUsage) {
518
- await writeData({ type: "data", kind: "token_usage", data: pendingUsage });
519
- }
520
- await writeData({ type: "finish-step" });
521
- }
522
- await writeData({ type: "finish", finishReason });
523
- await writeDone();
524
- };
525
- const ensureTextStarted = async (item) => {
526
- if (textStartedItems.has(item.id))
527
- return;
528
- textStartedItems.add(item.id);
529
- const textMetadata = item.type === "plan" ? { type: "plan" } : undefined;
530
- await writeData({ type: "text-start", id: item.id, ...(textMetadata ? { textMetadata } : {}) });
531
- };
532
- const ensureReasoningStarted = async (item) => {
533
- if (reasoningStartedItems.has(item.id))
534
- return;
535
- reasoningStartedItems.add(item.id);
536
- await writeData({ type: "reasoning-start", id: item.id });
537
- };
538
- const handleToolStarted = async (item) => {
539
- const commandItem = commandExecutionItem(item);
540
- if (commandItem) {
541
- await writeData({
542
- type: "tool-input-available",
543
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
544
- toolName: "bash",
545
- input: displayCommand(commandItem.command),
546
- });
547
- return true;
548
- }
549
- const fileItem = fileChangeItem(item);
550
- if (fileItem) {
551
- for (const [index, change] of fileItem.changes.entries()) {
552
- await writeData({
553
- type: "tool-input-available",
554
- toolCallId: toolCallId(toolIds, activeConversationId, item.id, String(index)),
555
- toolName: change.kind.type === "update" ? "Edit" : change.kind.type === "add" ? "Add" : "Delete",
556
- title: change.path,
557
- input: change,
558
- });
559
- }
560
- return true;
561
- }
562
- const mcpItem = mcpToolCallItem(item);
563
- if (mcpItem) {
564
- await writeData({
565
- type: "tool-input-available",
566
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
567
- toolName: mcpItem.tool,
568
- input: mcpItem.arguments,
569
- });
570
- return true;
571
- }
572
- const dynamicItem = dynamicToolCallItem(item);
573
- if (dynamicItem) {
574
- await writeData({
575
- type: "tool-input-available",
576
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
577
- toolName: dynamicItem.tool,
578
- input: dynamicItem.arguments,
579
- });
580
- return true;
581
- }
582
- const searchItem = webSearchItem(item);
583
- if (searchItem) {
584
- await writeData({
585
- type: "tool-input-available",
586
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
587
- toolName: "web_search",
588
- title: searchItem.query,
589
- input: webSearchPayload(searchItem),
590
- });
591
- return true;
592
- }
593
- return false;
594
- };
595
- const handleToolCompleted = async (item) => {
596
- const commandItem = commandExecutionItem(item);
597
- if (commandItem) {
598
- const isError = commandItem.status === "failed" || commandItem.status === "declined";
599
- await writeData(isError
600
- ? {
601
- type: "tool-output-error",
602
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
603
- errorText: toolErrorText(commandItem.aggregatedOutput, "Command execution failed"),
604
- }
605
- : {
606
- type: "tool-output-available",
607
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
608
- output: commandItem.aggregatedOutput ?? "",
609
- });
610
- return true;
611
- }
612
- const fileItem = fileChangeItem(item);
613
- if (fileItem) {
614
- const isError = fileItem.status === "failed" || fileItem.status === "declined";
615
- for (const [index] of fileItem.changes.entries()) {
616
- await writeData(isError
617
- ? {
618
- type: "tool-output-error",
619
- toolCallId: toolCallId(toolIds, activeConversationId, item.id, String(index)),
620
- errorText: fileItem.status,
621
- }
622
- : {
623
- type: "tool-output-available",
624
- toolCallId: toolCallId(toolIds, activeConversationId, item.id, String(index)),
625
- output: fileItem.status,
626
- preliminary: false,
627
- });
628
- }
629
- return true;
630
- }
631
- const mcpItem = mcpToolCallItem(item);
632
- if (mcpItem) {
633
- const isError = mcpItem.status === "failed";
634
- await writeData(isError
635
- ? {
636
- type: "tool-output-error",
637
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
638
- errorText: mcpItem.error?.message ?? "MCP tool call failed",
639
- }
640
- : {
641
- type: "tool-output-available",
642
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
643
- output: mcpToolCallOutput(mcpItem),
644
- });
645
- return true;
646
- }
647
- const dynamicItem = dynamicToolCallItem(item);
648
- if (dynamicItem) {
649
- const isError = dynamicItem.status === "failed" || dynamicItem.success === false;
650
- const output = dynamicToolCallOutput(dynamicItem);
651
- await writeData(isError
652
- ? {
653
- type: "tool-output-error",
654
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
655
- errorText: output || "Dynamic tool call failed",
656
- }
657
- : {
658
- type: "tool-output-available",
659
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
660
- output,
661
- });
662
- return true;
663
- }
664
- const searchItem = webSearchItem(item);
665
- if (searchItem) {
666
- await writeData({
667
- type: "tool-output-available",
668
- toolCallId: toolCallId(toolIds, activeConversationId, item.id),
669
- output: webSearchPayload(searchItem),
670
- });
671
- return true;
672
- }
673
- return false;
674
- };
675
- try {
676
- if (userInputResponse && !reservation.ownsOutput) {
677
- await submitLiveUserInput(reservation, thread, userInputResponse.itemId, userInputResponse.response);
678
- pendingClientToolWaits.delete(pendingWaitKey(conversationId, userInputResponse.itemId));
679
- await writeDone();
680
- return;
681
- }
682
- const result = interruptedTextWait
683
- ? await restartAfterPendingUserInput(reservation, thread, interruptedTextWait, prompt, turnRunOptions)
684
- : userInputResponse
685
- ? await continueLiveTurn(reservation, thread, () => thread.continueWithUserInput(userInputResponse.itemId, userInputResponse.response, {
686
- signal: abortController.signal,
687
- sourceTurnId: userInputResponse.sourceTurnId,
688
- }))
689
- : await startOrSteerLiveTurn(reservation, thread, prompt, turnRunOptions);
690
- if (userInputResponse) {
691
- pendingClientToolWaits.delete(pendingWaitKey(conversationId, userInputResponse.itemId));
692
- }
693
- else if (interruptedTextWait) {
694
- pendingClientToolWaits.delete(pendingWaitKey(conversationId, interruptedTextWait.toolCallId));
695
- }
696
- updateLiveTurn(reservation, thread, result.turnId);
697
- if (!reservation.ownsOutput) {
698
- await writeDone();
699
- return;
700
- }
701
- if (userInputResponse) {
702
- await writeData({
703
- type: "data",
704
- kind: "agent_session",
705
- data: {
706
- agentType: "codex",
707
- threadId: thread.id,
708
- turnId: result.turnId,
709
- },
710
- });
711
- await writeData({
712
- type: "tool-output-available",
713
- toolCallId: userInputResponse.itemId,
714
- toolName: "request_user_input",
715
- output: userInputResponse.response,
716
- });
717
- }
718
- else if (interruptedTextWait) {
719
- await writeData({
720
- type: "tool-output-error",
721
- toolCallId: interruptedTextWait.toolCallId,
722
- toolName: "request_user_input",
723
- errorText: "Question interrupted by text response",
724
- });
725
- }
726
- for await (const event of result.events) {
727
- if (event.type !== "item.delta") {
728
- logStreamEvent(event);
729
- }
730
- if (event.type === "thread.started") {
731
- currentThreadId = event.thread_id;
732
- activateConversation(currentThreadId);
733
- }
734
- else if (event.type === "turn.started") {
735
- currentTurnId = event.turn_id;
736
- updateLiveTurn(reservation, thread, currentTurnId);
737
- await writeStart(currentThreadId, currentTurnId);
738
- }
739
- else if (event.type === "item.started") {
740
- if (!responseStarted) {
741
- await writeStart(currentThreadId, currentTurnId);
742
- }
743
- if (await writeCompactActivity(event.item, "start"))
744
- continue;
745
- if (await writeSubagentActivity(event.item))
746
- continue;
747
- if (await handleToolStarted(event.item))
748
- continue;
749
- if (event.item.type === "agentMessage" || event.item.type === "plan") {
750
- await ensureTextStarted(event.item);
751
- }
752
- else if (event.item.type === "reasoning") {
753
- await ensureReasoningStarted(event.item);
754
- }
755
- }
756
- else if (event.type === "item.delta") {
757
- if (!responseStarted) {
758
- await writeStart(currentThreadId, currentTurnId);
759
- }
760
- if (event.item_type === "agentMessage" || event.item_type === "plan") {
761
- deltaReceivedItems.add(event.item_id);
762
- await writeData({ type: "text-delta", id: event.item_id, delta: event.delta });
763
- }
764
- else if (event.item_type === "reasoning") {
765
- deltaReceivedItems.add(event.item_id);
766
- await writeData({ type: "reasoning-delta", id: event.item_id, delta: event.delta });
767
- }
768
- }
769
- else if (event.type === "item.updated") {
770
- if (await writeSubagentActivity(event.item))
771
- continue;
772
- }
773
- else if (event.type === "item.completed") {
774
- if (!responseStarted) {
775
- await writeStart(currentThreadId, currentTurnId);
776
- }
777
- if (await writeCompactActivity(event.item, "end"))
778
- continue;
779
- if (await writeSubagentActivity(event.item))
780
- continue;
781
- if (await handleToolCompleted(event.item))
782
- continue;
783
- if (event.item.type === "agentMessage" || event.item.type === "plan") {
784
- await ensureTextStarted(event.item);
785
- const text = itemText(event.item) ?? "";
786
- if (text && !deltaReceivedItems.has(event.item.id)) {
787
- await writeData({ type: "text-delta", id: event.item.id, delta: text });
788
- }
789
- await writeData({ type: "text-end", id: event.item.id });
790
- }
791
- else if (event.item.type === "reasoning") {
792
- await ensureReasoningStarted(event.item);
793
- const text = itemText(event.item) ?? "";
794
- if (text && !deltaReceivedItems.has(event.item.id)) {
795
- await writeData({ type: "reasoning-delta", id: event.item.id, delta: text });
796
- }
797
- await writeData({ type: "reasoning-end", id: event.item.id });
798
- }
799
- }
800
- else if (event.type === "item.tool.requestUserInput") {
801
- if (!responseStarted) {
802
- await writeStart(currentThreadId, currentTurnId);
803
- }
804
- pendingClientToolWaits.set(pendingWaitKey(activeConversationId, event.params.itemId), {
805
- conversationId: activeConversationId,
806
- toolCallId: event.params.itemId,
807
- toolName: "request_user_input",
808
- turnId: event.params.turnId,
809
- since: new Date().toISOString(),
810
- });
811
- await writeData({
812
- type: "tool-input-available",
813
- toolCallId: event.params.itemId,
814
- toolName: "request_user_input",
815
- input: event.params,
816
- });
817
- // Chat paused until user answers the input request
818
- await writeDone();
819
- return;
820
- }
821
- else if (event.type === "turn.diff.updated") {
822
- if (!responseStarted) {
823
- await writeStart(currentThreadId, currentTurnId);
824
- }
825
- pendingDiffs.set(pendingDiffKey(activeConversationId, thread.id), event.diff);
826
- }
827
- else if (event.type === "thread.tokenUsage.updated") {
828
- const usage = toUsage(event.usage);
829
- if (usage) {
830
- const nextTurnUsage = addTokenUsage(turnUsage, usage.last);
831
- if (nextTurnUsage) {
832
- turnUsage = nextTurnUsage;
833
- pendingUsage = { ...usage, last: nextTurnUsage };
834
- }
835
- }
836
- }
837
- else if (event.type === "turn.failed") {
838
- if (lastErrorMessage !== event.error.message) {
839
- await writeData({ type: "error", errorText: event.error.message });
840
- }
841
- await writeFinish("error");
842
- liveTurnCompleted = true;
843
- completeLiveTurn(reservation);
844
- return;
845
- }
846
- else if (event.type === "error") {
847
- lastErrorMessage = event.message;
848
- await writeData({ type: "error", errorText: event.message });
849
- }
850
- }
851
- liveTurnCompleted = !abortController.signal.aborted;
852
- if (liveTurnCompleted)
853
- completeLiveTurn(reservation);
854
- if (!responseStarted) {
855
- await writeStart(currentThreadId, currentTurnId);
856
- }
857
- await writeFinish("stop");
858
- }
859
- catch (err) {
860
- const message = err instanceof Error ? err.message : "Chat stream failed";
861
- if (!responseStarted) {
862
- await writeStart(currentThreadId, currentTurnId);
863
- }
864
- await writeData({ type: "error", errorText: message });
865
- await writeFinish("error");
866
- }
867
- finally {
868
- if (heartbeatTimer)
869
- clearTimeout(heartbeatTimer);
870
- signal?.removeEventListener("abort", abortFromParent);
871
- activeStreams.delete(abortController);
872
- if (!liveTurnCompleted)
873
- releaseLiveOwner(reservation);
874
- }
875
- },
876
- };
877
- }
878
- export async function handleCodexChatStream({ body, headers, signal }, writer) {
879
- const prepared = await prepareCodexChatStream({ body, headers });
880
- await prepared.run(writer, signal);
881
- }
882
- chat.post("/v1/chat/stream", zValidator("json", ChatBodySchema), async (c) => {
883
- const body = c.req.valid("json");
884
- const prepared = await (async () => {
885
- try {
886
- return await prepareCodexChatStream({ body, headers: c.req.raw.headers });
887
- }
888
- catch (err) {
889
- if (err instanceof ChatRequestError) {
890
- throw new HTTPException(err.status, { message: err.message });
891
- }
892
- throw err;
893
- }
894
- })();
895
- return streamSSE(c, async (sse) => {
896
- const writeSSE = async (message) => {
897
- await sse.writeSSE(message);
898
- };
899
- await prepared.run({
900
- writeData: async (data) => {
901
- await writeSSE({ data: JSON.stringify(data) });
902
- },
903
- writeDone: async () => {
904
- await writeSSE({ data: "[DONE]" });
905
- },
906
- }, c.req.raw.signal);
907
- });
908
- });
909
- export default chat;
1
+ import{Hono as zt}from"hono";import{HTTPException as D}from"hono/http-exception";import{zValidator as Ht}from"@hono/zod-validator";import{streamSSE as Jt}from"hono/streaming";import{randomUUID as Kt}from"node:crypto";import{sessionManager as j,WorkdirNotFoundError as Qt}from"../codex.js";import{logger as at}from"@messenger-agent/shared/logger";import{ChatBodySchema as Vt,RequestUserInputToolPartSchema as Gt}from"../schemas.js";import{appConfig as vt}from"../config.js";import{setManagedTaskToolContext as Yt}from"../dynamic-tools.js";import{resolveAgentWorkspaceRoot as Zt}from"@messenger-agent/shared/agent-workspaces";import{saveUploadedFilePart as te}from"@messenger-agent/shared/uploaded-files";import{commandExecutionItem as xt,addTokenUsage as ee,displayCommand as ne,dynamicToolCallItem as bt,dynamicToolCallOutput as ae,fileChangeItem as Tt,itemText as St,mcpToolCallItem as Et,mcpToolCallOutput as re,toUsage as oe,turnOptions as ie,webSearchItem as Rt,webSearchPayload as kt}from"./shared.js";const B=new zt,f=new Map,z=new Set,rt=new Map,k=new Map;function Ue(){const t=new Set([...k.values()].map(e=>e.conversationId).filter(e=>!!e));return{active:z.size,waiting:t.size}}function se(t){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}}function de(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Thread-ID")?.trim();return e&&r?`${e}/${r}`:void 0}function ue(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Event-ID")?.trim(),o=v(t,"X-Matrix-Thread-ID")?.trim();if(!(!e||!r))return{roomId:e,eventId:r,...o?{threadRootEventId:o}:null}}class q extends Error{status;constructor(e,r){super(r),this.status=e,this.name="ChatRequestError"}}function v(t,e){return t?.get(e)??void 0}async function Ot(t){const e=f.get(t);if(!e)return!1;for(e.cancelRequested=!0;;){const r=e.transition;if(await r,r===e.transition)break}return e.thread&&e.turnId&&(e.interrupt||(e.interrupt=e.thread.interrupt(e.turnId).finally(()=>{e.interrupt=void 0})),await e.interrupt),e.owner?.abortController.abort(),e.resolveOwnerReady?.(),f.get(t)===e&&f.delete(t),!0}function _t(t){let e=f.get(t);e||(e={conversationId:t,generation:0,cancelRequested:!1,transition:Promise.resolve()},f.set(t,e));const r=++e.generation,o=new AbortController,i=!e.owner;return i&&(e.owner={generation:r,abortController:o},e.ownerReady=new Promise(d=>{e.resolveOwnerReady=d})),{state:e,generation:r,abortController:o,ownsOutput:i}}function N(t){return f.get(t.state.conversationId)===t.state&&t.state.owner?.generation===t.generation}async function H(t,e){const r=t.transition;let o;const i=new Promise(d=>{o=d});t.transition=r.then(()=>i),await r;try{return await e()}finally{o()}}async function le(t,e,r,o){!t.ownsOutput&&!t.state.turnId&&await t.state.ownerReady;try{return await H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");i.thread=e;const d=i.turnId?await e.steerStreamed(r,i.turnId,{signal:t.abortController.signal}):await e.runStreamed(r,{...o,signal:t.abortController.signal});return i.turnId=d.turnId,d})}finally{t.ownsOutput&&t.state.resolveOwnerReady?.()}}async function ce(t,e,r){return H(t.state,async()=>{const{state:o}=t;if(!N(t)||o.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");o.thread=e;const i=await r();return o.turnId=i.turnId,o.resolveOwnerReady?.(),i})}async function pe(t,e,r,o){return H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");i.thread=e;const d=await e.submitPendingUserInput(r,o);return d&&(i.turnId=d),d})}async function fe(t,e,r,o,i){return H(t.state,async()=>{const{state:d}=t;if(f.get(d.conversationId)!==d||d.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");d.thread=e,await e.interruptPendingUserInput(r.turnId,r.toolCallId),d.turnId=void 0;const x=await e.runStreamed(o,{...i,signal:t.abortController.signal});return d.turnId=x.turnId,x})}function Mt(t,e,r){t.state.thread=e,t.state.turnId=r}function Pt(t){const{state:e}=t;N(t)&&(e.owner=void 0,e.resolveOwnerReady?.(),e.turnId=void 0,f.get(e.conversationId)===e&&f.delete(e.conversationId))}function Dt(t){N(t)&&(t.state.owner=void 0,t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function me(t){N(t)&&(Dt(t),t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function J(t,e){return`${t??"unknown"}:${e}`}function we(t){if(t)return[...k.values()].find(e=>e.conversationId===t)}function qt(t,e){return t??e}function ye(t){return t.type==="file"&&"mediaType"in t&&typeof t.mediaType=="string"&&"url"in t&&typeof t.url=="string"&&(!("filename"in t)||t.filename===void 0||typeof t.filename=="string")}async function ge(t){if(t.type==="text"&&typeof t.text=="string")return t.text;if(ye(t))return te(t,vt.fileUploads.tempDir);throw new D(400,{message:`Unsupported message part type: ${t.type}`})}async function Ie(t){const r=(await Promise.all(t.map(ge))).filter(o=>!!(o&&o.trim().length>0)).join(`
2
+ `).trim();if(!r)throw new D(400,{message:"No supported message content found"});return r}function he(t){if(!t.some(o=>o.type==="tool"))return;if(t.length!==1)throw new D(400,{message:"When a tool part is present, the message must contain exactly one part"});const r=Gt.safeParse(t[0]);if(!r.success)throw new D(400,{message:`Invalid requestUserInput tool part: ${r.error.message}`});return{itemId:r.data.toolCallId,response:r.data.output,sourceTurnId:r.data.sourceTurnId}}function m(t,e,r,o){const i=[e,r,o].filter(Boolean).join(":"),d=t.get(i);if(d)return d;const x=Kt();return t.set(i,x),x}function Ce(t,e){return t&&t.length>0?t:e}function ve(t){at.debug("Received chat stream event:",t)}function xe(t){if(t instanceof Qt||t&&typeof t=="object"&&"path"in t&&typeof t.path=="string")return t.path;if(t instanceof Error&&t.message.startsWith("Workdir not found: "))return t.message.slice(19)}B.post("/v1/chat/:conversation_id/cancel",async t=>{const e=t.req.param("conversation_id");try{return await Ot(e)?t.json({success:!0}):t.json({error:se(e)},404)}catch(r){const o=r instanceof Error?r.message:"Failed to interrupt Codex conversation";return at.error(`Failed to cancel Codex conversation ${e}:`,r),t.json({error:{type:"server_error",code:"cancel_failed",message:o}},500)}}),B.get("/admin/status",t=>t.json({activeTaskCount:z.size,waitingClientToolCount:k.size,clientToolWaits:[...k.values()].map(({conversationId:e,toolCallId:r,toolName:o,since:i})=>({conversationId:e,toolCallId:r,toolName:o,since:i}))}));async function Nt({body:t,headers:e}){const{conversationId:r,fork:o,model:i,message:d}=t,x=v(e,"X-Matrix-Bot-Name"),K=v(e,"X-Matrix-Workdir"),U=v(e,"X-Matrix-Workspace-ID"),Ut=v(e,"X-Matrix-Agent-Mode"),Q=v(e,"X-Elevo-LLM-Binding")?.trim()||void 0,V=o?void 0:j.bindingFor(r);if(Q&&V&&Q!==V)throw new q(409,"LLM proxy binding does not match the existing conversation");const L=V??Q,Lt=de(e),At=ue(e),G=he(d.parts),Y=G?void 0:await Ie(d.parts),ot=G?void 0:we(r),c=G,O=Y&&ot?ot:void 0;let A=!o&&r?_t(r):void 0;const Wt=(()=>{try{return Zt(vt,U)}catch(S){throw new q(400,S instanceof Error?S.message:"Invalid workspace")}})(),{thread:y,isNewSession:Ft}=await(async()=>{try{if(o){if(!("threadId"in o))throw new q(400,"Codex fork requires threadId and turnId");return await j.fork(o,x,K,U,i,"danger-full-access",L)}return await j.getOrCreate(r,x,K,U,i,"danger-full-access",L)}catch(S){A&&me(A);const _=xe(S);throw _?new q(400,`Workdir not found: ${_}`):S}})();A??=_t(y.id),Yt(y.id,{workspaceRoot:Wt,threadRef:Lt,matrixEvent:At,llmProxyBinding:L});const it=ie(Ut,i);return{run:async(S,_)=>{const w=A,W=w.abortController;z.add(W);const Z=()=>{N(w)&&Ot(w.state.conversationId).catch(a=>at.error("Failed to cancel disconnected Codex stream:",a))};_?.aborted&&Z(),_?.addEventListener("abort",Z,{once:!0});let b=!!c,st=!!c,dt=!1,l,I=y.id,h,F=!1,P;const ut=new Set,lt=new Set,X=new Set,p=new Map,ct=new Set,Xt=new Set,pt=new Set;let ft,tt,mt;const wt=()=>{P=setTimeout(()=>{s({type:"heartbeat"}).catch(()=>{})},6e4)},yt=async a=>{P&&clearTimeout(P),a.data==="[DONE]"?await S.writeDone():await S.writeData(JSON.parse(String(a.data))),wt()},s=async a=>{await yt({data:JSON.stringify(a)})},$=async()=>{await yt({data:"[DONE]"})},$t=async()=>{const a=qt(l,y.id),n=rt.get(a);n&&(rt.delete(a),await s({type:"data",kind:"diff",data:{diff:n}}))},gt=async(a,n)=>{if(a.type!=="contextCompaction")return!1;if(n==="end"&&!ct.has(a.id))return!0;const u=n==="start"?ct:Xt;return u.has(a.id)||(u.add(a.id),await s({type:"data",kind:"compact",data:n==="start"?{phase:n,activityId:a.id}:{phase:n,activityId:a.id,status:"completed"}})),!0},et=async a=>{if(a.type!=="subAgentActivity")return!1;if(pt.has(a.id))return!0;pt.add(a.id);const n=m(p,l,a.id),u={activity:a.kind,agentThreadId:a.agentThreadId,...a.agentPath?{agentPath:a.agentPath}:null};return await s({type:"tool-input-available",toolCallId:n,toolName:`subagent_${a.kind}`,...a.agentPath?{title:a.agentPath}:null,input:u}),await s(a.kind==="interrupted"?{type:"tool-output-error",toolCallId:n,errorText:"Subagent interrupted"}:{type:"tool-output-available",toolCallId:n,output:u}),!0};wt();const It=a=>{l!==a&&(l=a,Ft&&j.persist(a,x,K,U,L))},E=async(a,n)=>{b||(b=!0,It(a),await s({type:"start",messageMetadata:{conversationId:a,threadId:a,...n?{turnId:n}:null}}),await s({type:"start-step"}),st=!0)},nt=async a=>{dt||(dt=!0,st&&(await $t(),a==="stop"&&tt&&await s({type:"data",kind:"token_usage",data:tt}),await s({type:"finish-step"})),await s({type:"finish",finishReason:a}),await $())},ht=async a=>{if(ut.has(a.id))return;ut.add(a.id);const n=a.type==="plan"?{type:"plan"}:void 0;await s({type:"text-start",id:a.id,...n?{textMetadata:n}:{}})},Ct=async a=>{lt.has(a.id)||(lt.add(a.id),await s({type:"reasoning-start",id:a.id}))},jt=async a=>{const n=xt(a);if(n)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:"bash",input:ne(n.command)}),!0;const u=Tt(a);if(u){for(const[T,C]of u.changes.entries())await s({type:"tool-input-available",toolCallId:m(p,l,a.id,String(T)),toolName:C.kind.type==="update"?"Edit":C.kind.type==="add"?"Add":"Delete",title:C.path,input:C});return!0}const g=Et(a);if(g)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:g.tool,input:g.arguments}),!0;const R=bt(a);if(R)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:R.tool,input:R.arguments}),!0;const M=Rt(a);return M?(await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:"web_search",title:M.query,input:kt(M)}),!0):!1},Bt=async a=>{const n=xt(a);if(n){const T=n.status==="failed"||n.status==="declined";return await s(T?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:Ce(n.aggregatedOutput,"Command execution failed")}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:n.aggregatedOutput??""}),!0}const u=Tt(a);if(u){const T=u.status==="failed"||u.status==="declined";for(const[C]of u.changes.entries())await s(T?{type:"tool-output-error",toolCallId:m(p,l,a.id,String(C)),errorText:u.status}:{type:"tool-output-available",toolCallId:m(p,l,a.id,String(C)),output:u.status,preliminary:!1});return!0}const g=Et(a);if(g){const T=g.status==="failed";return await s(T?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:g.error?.message??"MCP tool call failed"}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:re(g)}),!0}const R=bt(a);if(R){const T=R.status==="failed"||R.success===!1,C=ae(R);return await s(T?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:C||"Dynamic tool call failed"}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:C}),!0}const M=Rt(a);return M?(await s({type:"tool-output-available",toolCallId:m(p,l,a.id),output:kt(M)}),!0):!1};try{if(c&&!w.ownsOutput){await pe(w,y,c.itemId,c.response),k.delete(J(r,c.itemId)),await $();return}const a=O?await fe(w,y,O,Y,it):c?await ce(w,y,()=>y.continueWithUserInput(c.itemId,c.response,{signal:W.signal,sourceTurnId:c.sourceTurnId})):await le(w,y,Y,it);if(c?k.delete(J(r,c.itemId)):O&&k.delete(J(r,O.toolCallId)),Mt(w,y,a.turnId),!w.ownsOutput){await $();return}c?(await s({type:"data",kind:"agent_session",data:{agentType:"codex",threadId:y.id,turnId:a.turnId}}),await s({type:"tool-output-available",toolCallId:c.itemId,toolName:"request_user_input",output:c.response})):O&&await s({type:"tool-output-error",toolCallId:O.toolCallId,toolName:"request_user_input",errorText:"Question interrupted by text response"});for await(const n of a.events)if(n.type!=="item.delta"&&ve(n),n.type==="thread.started")I=n.thread_id,It(I);else if(n.type==="turn.started")h=n.turn_id,Mt(w,y,h),await E(I,h);else if(n.type==="item.started"){if(b||await E(I,h),await gt(n.item,"start")||await et(n.item)||await jt(n.item))continue;n.item.type==="agentMessage"||n.item.type==="plan"?await ht(n.item):n.item.type==="reasoning"&&await Ct(n.item)}else if(n.type==="item.delta")b||await E(I,h),n.item_type==="agentMessage"||n.item_type==="plan"?(X.add(n.item_id),await s({type:"text-delta",id:n.item_id,delta:n.delta})):n.item_type==="reasoning"&&(X.add(n.item_id),await s({type:"reasoning-delta",id:n.item_id,delta:n.delta}));else if(n.type==="item.updated"){if(await et(n.item))continue}else if(n.type==="item.completed"){if(b||await E(I,h),await gt(n.item,"end")||await et(n.item)||await Bt(n.item))continue;if(n.item.type==="agentMessage"||n.item.type==="plan"){await ht(n.item);const u=St(n.item)??"";u&&!X.has(n.item.id)&&await s({type:"text-delta",id:n.item.id,delta:u}),await s({type:"text-end",id:n.item.id})}else if(n.item.type==="reasoning"){await Ct(n.item);const u=St(n.item)??"";u&&!X.has(n.item.id)&&await s({type:"reasoning-delta",id:n.item.id,delta:u}),await s({type:"reasoning-end",id:n.item.id})}}else if(n.type==="item.tool.requestUserInput"){b||await E(I,h),k.set(J(l,n.params.itemId),{conversationId:l,toolCallId:n.params.itemId,toolName:"request_user_input",turnId:n.params.turnId,since:new Date().toISOString()}),await s({type:"tool-input-available",toolCallId:n.params.itemId,toolName:"request_user_input",input:n.params}),await $();return}else if(n.type==="turn.diff.updated")b||await E(I,h),rt.set(qt(l,y.id),n.diff);else if(n.type==="thread.tokenUsage.updated"){const u=oe(n.usage);if(u){const g=ee(mt,u.last);g&&(mt=g,tt={...u,last:g})}}else if(n.type==="turn.failed"){ft!==n.error.message&&await s({type:"error",errorText:n.error.message}),await nt("error"),F=!0,Pt(w);return}else n.type==="error"&&(ft=n.message,await s({type:"error",errorText:n.message}));F=!W.signal.aborted,F&&Pt(w),b||await E(I,h),await nt("stop")}catch(a){const n=a instanceof Error?a.message:"Chat stream failed";b||await E(I,h),await s({type:"error",errorText:n}),await nt("error")}finally{P&&clearTimeout(P),_?.removeEventListener("abort",Z),z.delete(W),F||Dt(w)}}}}async function Le({body:t,headers:e,signal:r},o){await(await Nt({body:t,headers:e})).run(o,r)}B.post("/v1/chat/stream",Ht("json",Vt),async t=>{const e=t.req.valid("json"),r=await(async()=>{try{return await Nt({body:e,headers:t.req.raw.headers})}catch(o){throw o instanceof q?new D(o.status,{message:o.message}):o}})();return Jt(t,async o=>{const i=async d=>{await o.writeSSE(d)};await r.run({writeData:async d=>{await i({data:JSON.stringify(d)})},writeDone:async()=>{await i({data:"[DONE]"})}},t.req.raw.signal)})});var Ae=B;export{q as ChatRequestError,Ot as cancelCodexConversation,Ue as codexActivitySnapshot,Ae as default,Le as handleCodexChatStream,Nt as prepareCodexChatStream};