'use strict'; function createCodexAppRuntime(deps = {}) { const { wsSend, loadSession, saveSession, truncateObj, } = deps; function truncate(value, maxLen) { if (typeof truncateObj === 'function') return truncateObj(value, maxLen); const text = typeof value === 'string' ? value : JSON.stringify(value); return text.length > maxLen ? `${text.slice(0, maxLen)}...` : value; } function sendRuntime(entry, sessionId, payload) { wsSend(entry.ws, { ...payload, sessionId }); } function formatAgentMessageDividerTime(date = new Date()) { const pad = (value) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function createAgentMessageDivider() { const time = formatAgentMessageDividerTime(); return `
${time}
`; } function hasAgentMessageBoundaryAtEnd(text) { return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text) || /(?:^|\n)\s*\s*$/.test(text); } function hasAgentMessageBoundaryAtStart(text) { return /^\s*(?:---|\*\*\*|___)\s*\n/.test(text) || /^\s*\s*/.test(text); } function itemKind(item) { switch (item?.type) { case 'commandExecution': return 'command_execution'; case 'mcpToolCall': return 'mcp_tool_call'; case 'fileChange': return 'file_change'; case 'reasoning': return 'reasoning'; case 'dynamicToolCall': return 'dynamic_tool_call'; case 'collabAgentToolCall': return 'collab_agent_tool_call'; case 'webSearch': return 'web_search'; case 'imageView': return 'image_view'; case 'imageGeneration': return 'image_generation'; default: return item?.type || 'codex_app_item'; } } function itemName(item) { switch (item?.type) { case 'commandExecution': return 'CommandExecution'; case 'mcpToolCall': return 'McpToolCall'; case 'fileChange': return 'FileChange'; case 'reasoning': return 'Reasoning'; case 'dynamicToolCall': return item.tool || 'DynamicToolCall'; case 'collabAgentToolCall': return item.tool || 'CollabAgentToolCall'; case 'webSearch': return 'WebSearch'; case 'imageGeneration': return 'ImageGeneration'; default: return item?.type || 'CodexAppItem'; } } function itemInput(item) { if (!item) return null; switch (item.type) { case 'commandExecution': return { command: item.command || '' }; case 'mcpToolCall': return { server: item.server || '', tool: item.tool || '', arguments: item.arguments ?? null, }; case 'fileChange': return { changes: item.changes || [] }; case 'reasoning': return { content: item.content || [], summary: item.summary || [] }; case 'dynamicToolCall': return { tool: item.tool || '', namespace: item.namespace || null, arguments: item.arguments ?? null }; case 'collabAgentToolCall': return { tool: item.tool || '', prompt: item.prompt || null, receiverThreadIds: item.receiverThreadIds || [], agentsStates: item.agentsStates || {}, }; default: return truncate(item, 500); } } function reasoningTextFromItem(item) { const parts = [...(item?.summary || []), ...(item?.content || [])]; return parts.map((part) => { if (typeof part === 'string') return part; if (typeof part?.text === 'string') return part.text; return ''; }).filter(Boolean).join('\n\n'); } function hasReasoningContent(item) { return Boolean(reasoningTextFromItem(item).trim()); } function itemMeta(item) { if (!item) return null; switch (item.type) { case 'commandExecution': return { kind: 'command_execution', title: 'Shell Command', subtitle: item.command || '', exitCode: typeof item.exitCode === 'number' ? item.exitCode : null, status: item.status || null, }; case 'mcpToolCall': return { kind: 'mcp_tool_call', title: 'MCP Tool', subtitle: [item.server, item.tool].filter(Boolean).join('.'), status: item.status || null, }; case 'fileChange': return { kind: 'file_change', title: 'File Change', subtitle: (item.changes || []).map((change) => change.path).filter(Boolean).join(', '), status: item.status || null, }; case 'reasoning': return { kind: 'reasoning', title: 'Reasoning', subtitle: reasoningTextFromItem(item).replace(/\s+/g, ' ').slice(0, 120), status: item.status || null, }; default: return { kind: itemKind(item), title: itemName(item), subtitle: item.tool || item.query || '', status: item.status || null, }; } } function stringifyMcpResult(result) { if (!result) return ''; if (Array.isArray(result.content)) { const text = result.content.map((part) => { if (typeof part?.text === 'string') return part.text; try { return JSON.stringify(part); } catch { return String(part); } }).filter(Boolean).join('\n'); if (text) return text; } try { return JSON.stringify(result, null, 2); } catch { return String(result); } } function itemResult(item) { if (!item) return ''; switch (item.type) { case 'commandExecution': return item.aggregatedOutput || ''; case 'mcpToolCall': return item.error?.message || stringifyMcpResult(item.result); case 'fileChange': return JSON.stringify(item.changes || [], null, 2); case 'reasoning': return reasoningTextFromItem(item); case 'dynamicToolCall': return JSON.stringify({ success: item.success ?? null, contentItems: item.contentItems || null, }, null, 2); case 'collabAgentToolCall': return JSON.stringify({ status: item.status || null, receiverThreadIds: item.receiverThreadIds || [], agentsStates: item.agentsStates || {}, }, null, 2); default: if (typeof item.text === 'string') return item.text; return JSON.stringify(truncate(item, 1200)); } } function ensureToolCall(entry, item, sessionId) { if (!item?.id) return null; const kind = itemKind(item); let toolCall = entry.toolCalls.find((tool) => tool.id === item.id); if (toolCall) { toolCall.name = itemName(item); toolCall.kind = kind; toolCall.meta = itemMeta(item) || toolCall.meta || null; if (toolCall.input == null) toolCall.input = itemInput(item); return toolCall; } toolCall = { name: itemName(item), id: item.id, kind, meta: itemMeta(item), input: itemInput(item), done: false, }; entry.toolCalls.push(toolCall); sendRuntime(entry, sessionId, { type: 'tool_start', name: toolCall.name, toolUseId: toolCall.id, input: toolCall.input, kind: toolCall.kind, meta: toolCall.meta, }); return toolCall; } function appendAgentText(entry, itemId, text) { const nextText = String(text || ''); if (!nextText) return ''; if (!entry.agentMessageItems) entry.agentMessageItems = new Map(); const currentItemText = entry.agentMessageItems.get(itemId) || ''; const separator = agentMessageSeparator(entry, itemId, nextText); entry.agentMessageItems.set(itemId, currentItemText + nextText); entry.fullText = (entry.fullText || '') + separator + nextText; return separator + nextText; } function appendAgentCompletedText(entry, item) { const text = String(item?.text || ''); if (!text) return ''; if (!entry.agentMessageItems) entry.agentMessageItems = new Map(); const currentItemText = entry.agentMessageItems.get(item.id) || ''; if (currentItemText && text.startsWith(currentItemText)) { const remainder = text.slice(currentItemText.length); entry.agentMessageItems.set(item.id, text); entry.fullText = (entry.fullText || '') + remainder; return remainder; } if (currentItemText === text) return ''; const separator = agentMessageSeparator(entry, item.id, text); entry.agentMessageItems.set(item.id, text); entry.fullText = (entry.fullText || '') + separator + text; return separator + text; } function agentMessageSeparator(entry, itemId, nextText) { if (entry.agentMessageItems?.get(itemId)) return ''; const currentText = entry.fullText || ''; if (!/\S/.test(currentText)) return ''; const hasVisualBoundary = hasAgentMessageBoundaryAtEnd(currentText) || hasAgentMessageBoundaryAtStart(String(nextText || '')); return hasVisualBoundary ? '' : `\n\n${createAgentMessageDivider()}\n\n`; } function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) { if (!itemId) return; let toolCall = entry.toolCalls.find((tool) => tool.id === itemId); if (!toolCall) { toolCall = { name: patch.name || 'CodexAppItem', id: itemId, kind: patch.kind || 'codex_app_item', meta: patch.meta || null, input: patch.input || null, done: false, }; entry.toolCalls.push(toolCall); sendRuntime(entry, sessionId, { type: 'tool_start', name: toolCall.name, toolUseId: toolCall.id, input: toolCall.input, kind: toolCall.kind, meta: toolCall.meta, }); } if (patch.name) toolCall.name = patch.name; if (patch.kind) toolCall.kind = patch.kind; if (patch.meta) toolCall.meta = patch.meta; if (patch.input !== undefined) toolCall.input = patch.input; toolCall.done = done; toolCall.result = result; sendRuntime(entry, sessionId, { type: done ? 'tool_end' : 'tool_update', toolUseId: itemId, name: toolCall.name, input: toolCall.input, result, kind: toolCall.kind, meta: toolCall.meta, }); } function updateUsage(sessionId, entry, usage) { const total = usage?.total || usage || null; if (!total) return; const session = loadSession(sessionId); if (!session) return; session.totalUsage = { inputTokens: total.inputTokens || 0, cachedInputTokens: total.cachedInputTokens || 0, outputTokens: total.outputTokens || 0, }; entry.lastUsage = session.totalUsage; saveSession(session); sendRuntime(entry, sessionId, { type: 'usage', totalUsage: session.totalUsage }); } function processCodexAppNotification(entry, notification, sessionId) { if (!entry || !notification?.method) return { done: false }; const method = notification.method; const params = notification.params || {}; if (params.threadId && !entry.threadId) entry.threadId = params.threadId; if (params.turnId && !entry.turnId) entry.turnId = params.turnId; switch (method) { case 'turn/started': if (params.turn?.id) entry.turnId = params.turn.id; return { done: false }; case 'item/started': { const item = params.item; if (!item || !item.id || item.type === 'agentMessage' || item.type === 'userMessage') return { done: false }; if (item.type === 'reasoning' && !hasReasoningContent(item)) return { done: false }; ensureToolCall(entry, item, sessionId); return { done: false }; } case 'item/agentMessage/delta': { const chunk = appendAgentText(entry, params.itemId || 'agent-message', params.delta || ''); if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk }); return { done: false }; } case 'item/commandExecution/outputDelta': { const itemId = params.itemId; const current = entry.toolOutputDeltas?.get(itemId) || ''; const next = current + String(params.delta || ''); if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map(); entry.toolOutputDeltas.set(itemId, next); updateToolResult(entry, sessionId, itemId, next, false, { name: 'CommandExecution', kind: 'command_execution', }); return { done: false }; } case 'item/fileChange/patchUpdated': { updateToolResult(entry, sessionId, params.itemId, JSON.stringify(params.changes || [], null, 2), false, { name: 'FileChange', kind: 'file_change', input: { changes: params.changes || [] }, meta: { kind: 'file_change', title: 'File Change', subtitle: (params.changes || []).map((change) => change.path).filter(Boolean).join(', '), status: 'inProgress', }, }); return { done: false }; } case 'item/mcpToolCall/progress': { updateToolResult(entry, sessionId, params.itemId, params.message || '', false, { name: 'McpToolCall', kind: 'mcp_tool_call', }); return { done: false }; } case 'item/reasoning/summaryTextDelta': case 'item/reasoning/textDelta': { const itemId = params.itemId; const delta = String(params.delta || ''); if (!itemId || !delta) return { done: false }; const current = entry.toolOutputDeltas?.get(itemId) || ''; const next = current + delta; if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map(); entry.toolOutputDeltas.set(itemId, next); updateToolResult(entry, sessionId, itemId, next, false, { name: 'Reasoning', kind: 'reasoning', }); return { done: false }; } case 'item/completed': { const item = params.item; if (!item || !item.id) return { done: false }; if (item.type === 'agentMessage') { const chunk = appendAgentCompletedText(entry, item); if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk }); return { done: false }; } if (item.type === 'userMessage') return { done: false }; if (item.type === 'reasoning') { const result = (itemResult(item) || entry.toolOutputDeltas?.get(item.id) || '').slice(0, 4000); if (!result.trim()) return { done: false }; const toolCall = ensureToolCall(entry, item, sessionId); if (!toolCall) return { done: false }; toolCall.done = true; toolCall.result = result; toolCall.meta = itemMeta(item) || toolCall.meta; sendRuntime(entry, sessionId, { type: 'tool_end', toolUseId: toolCall.id, result, kind: toolCall.kind, meta: toolCall.meta, }); return { done: false }; } const toolCall = ensureToolCall(entry, item, sessionId); if (!toolCall) return { done: false }; const result = itemResult(item).slice(0, 4000); toolCall.done = true; toolCall.result = result; toolCall.meta = itemMeta(item) || toolCall.meta; sendRuntime(entry, sessionId, { type: 'tool_end', toolUseId: toolCall.id, result, kind: toolCall.kind, meta: toolCall.meta, }); return { done: false }; } case 'thread/tokenUsage/updated': updateUsage(sessionId, entry, params.tokenUsage); return { done: false }; case 'turn/completed': { if (params.turn?.id) entry.turnId = params.turn.id; entry.turnStatus = params.turn?.status || 'completed'; if (params.turn?.status === 'failed') { entry.lastError = params.turn?.error?.message || 'Codex App 任务失败'; } return { done: true }; } case 'error': case 'warning': case 'guardianWarning': case 'configWarning': case 'deprecationNotice': { const message = params.message || params.title || ''; if (message) { if (method === 'error') entry.lastError = message; sendRuntime(entry, sessionId, { type: 'system_message', message }); } return { done: false }; } default: return { done: false }; } } return { processCodexAppNotification, updateUsage, }; } module.exports = { createCodexAppRuntime };