From 4c293dff733661b251bfde584ca841392efd384b Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 13 Jan 2026 10:58:07 +0000 Subject: [PATCH] fix: improve proxyai error handling and clean up code --- .../chat/ui/ChatMessageResponseBody.java | 10 ++ .../ui/ChatToolWindowScrollablePanel.java | 2 +- .../carlrobert/codegpt/agent/AgentFactory.kt | 13 +- .../carlrobert/codegpt/agent/ProxyAIAgent.kt | 8 + .../agent/clients/CustomOpenAILLMClient.kt | 5 +- .../codegpt/agent/clients/ProxyAILLMClient.kt | 5 +- .../strategy/SingleRunStrategyProvider.kt | 30 ++-- .../toolwindow/agent/AgentEventHandler.kt | 164 ++++++++---------- 8 files changed, 111 insertions(+), 126 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java index 855f8462..1ecb1687 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java @@ -166,6 +166,16 @@ public class ChatMessageResponseBody extends JPanel { }); } + public void displayInvalidCredential() { + String message = "Invalid API key. Open Settings to update your API key."; + displayErrorMessage(message, e -> { + if (e.getEventType() == ACTIVATED) { + ShowSettingsUtil.getInstance() + .showSettingsDialog(project, GeneralSettingsConfigurable.class); + } + }); + } + public void displayQuotaExceeded() { String message = "You exceeded your current quota, please check your plan and billing details, " + "or change to a different LLM provider."; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java index 146019a5..899b4f52 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java @@ -47,7 +47,7 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel { It looks like you haven't configured your API key yet. Visit ProxyAI settings to do so.

- Don't have an account? Sign up to get the most out of ProxyAI. + Don't have an account? Sign up to get started.

""", false, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt index 45afe73d..f9117725 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt @@ -39,7 +39,6 @@ import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.toolwindow.agent.AgentCreditsEvent import java.time.LocalDate -import kotlin.time.Duration.Companion.seconds object AgentFactory { @@ -184,17 +183,7 @@ object AgentFactory { } private fun createRetryingExecutor(client: LLMClient): PromptExecutor { - val retryingClient = RetryingLLMClient( - client, - RetryConfig( - maxAttempts = 3, - initialDelay = 1.seconds, - maxDelay = 20.seconds, - backoffMultiplier = 2.0, - jitterFactor = 0.2 - ) - ) - return SingleLLMPromptExecutor(retryingClient) + return SingleLLMPromptExecutor(RetryingLLMClient(client)) } private fun createGeneralPurposeAgent( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/ProxyAIAgent.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/ProxyAIAgent.kt index 5a9aae0e..68cbe432 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/ProxyAIAgent.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/ProxyAIAgent.kt @@ -168,6 +168,10 @@ object ProxyAIAgent { } } + onNodeExecutionFailed { ctx -> + logger.error(ctx.throwable) { "Node execution failed: $ctx" } + } + onToolCallStarting { ctx -> val id = ctx.toolCallId ?: UUID.randomUUID().toString() if (ctx.toolCallId == null) { @@ -196,6 +200,10 @@ object ProxyAIAgent { onAgentCompleted { context -> events.onAgentCompleted(context) } + + onAgentExecutionFailed { + logger.error(it.throwable) { "Agent execution failed: $it" } + } } } return agent diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt index 392c915f..1dc327e6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt @@ -16,6 +16,7 @@ import ai.koog.prompt.message.Message import ai.koog.prompt.message.ResponseMetaInfo import ai.koog.prompt.params.LLMParams import ai.koog.prompt.streaming.StreamFrameFlowBuilder +import com.intellij.openapi.util.text.StringUtil import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.* @@ -185,9 +186,9 @@ public class CustomOpenAILLMClient( If the tool has no arguments, OpenRouter puts an empty string in the arguments instead of an empty object But we always expect arguments to be a JSON object. Fixing this. */ - content = toolCall.function.arguments + content = StringUtil.escapeStringCharacters(toolCall.function.arguments .takeIf { it.isNotEmpty() } - ?: "{}", + ?: "{}"), metaInfo = metaInfo ) ) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt index dda38c17..105a4757 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt @@ -31,10 +31,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi public class ProxyAIClientSettings( baseUrl: String = DEFAULT_BASE_URL, chatCompletionsPath: String = DEFAULT_CHAT_COMPLETIONS_PATH, - timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig( - requestTimeoutMillis = 120_000, - socketTimeoutMillis = 120_000 - ) + timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig() ) : OpenAIBaseSettings(baseUrl, chatCompletionsPath, timeoutConfig) { public companion object { public const val DEFAULT_BASE_URL: String = "https://codegpt-api.carlrobert.ee" diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/strategy/SingleRunStrategyProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/strategy/SingleRunStrategyProvider.kt index 9da89500..6cd371e7 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/strategy/SingleRunStrategyProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/strategy/SingleRunStrategyProvider.kt @@ -67,19 +67,6 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider { llm.writeSession { if (previousCheckpoint == null) { projectInstructions?.let { - appendPrompt { - message( - Message.User( - it, - RequestMetaInfo(clock.now(), buildJsonObject { - put("cache_control", buildJsonObject { - put("type", JsonPrimitive("ephemeral")) - }) - }) - ) - ) - } - appendPrompt { user(it) } @@ -136,7 +123,7 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider { msg is Message.Tool.Call && msg.tool == "TodoWrite" } - if (toolCallMessages >= 2 && !todoWriteToolUsed) { + if (toolCallMessages >= 3 && !todoWriteToolUsed) { appendPrompt { user("It seems that you haven't created a todo list yet. If the task on hand requires multiple steps then create a todo list to track your changes.") } @@ -166,6 +153,7 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider { edge(nodeCallLLM forwardTo nodeExecuteTool onMultipleToolCalls { true }) edge(nodeCallLLM forwardTo nodeFinish onSingleAssistantResponse { true }) edge(nodeExecuteTool forwardTo nodeSendToolResult) + edge(nodeSendToolResult forwardTo nodeFinish onEmptyOutput { true }) edge(nodeSendToolResult forwardTo nodeFinish onSingleAssistantResponse { true }) edge(nodeSendToolResult forwardTo nodeExecuteTool onMultipleToolCalls { true }) } @@ -299,3 +287,17 @@ private infix fun AIAgentEdgeBuilderIntermediate .onCondition { messages -> block(messages[0]) } .transformed { it[0].content } } + +@EdgeTransformationDslMarker +private infix fun AIAgentEdgeBuilderIntermediate, OutgoingInput>.onEmptyOutput( + block: suspend (Message.Response) -> Boolean +): AIAgentEdgeBuilderIntermediate { + return onIsInstance(List::class) + .transformed { response -> + response.filter { item -> item is Message.Response && item !is Message.Reasoning } + .filterIsInstance() + } + .onCondition { it.isEmpty() } + .onCondition { messages -> block(messages[0]) } + .transformed { it[0].content } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt index 8a6c907c..cfa6ba99 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt @@ -43,6 +43,10 @@ class AgentEventHandler( private val mainToolCards = ConcurrentHashMap() + private val toolOutputPublisher = ApplicationManager.getApplication() + .messageBus + .syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC) + @Volatile private var lastReportedPromptTokens: Long = 0 @@ -90,14 +94,64 @@ class AgentEventHandler( approvalQueue.clear() currentQuestion = null questionQueue.clear() - approvalContainer.removeAll() - approvalContainer.isVisible = false + clearApprovalContainer() todoListPanel.clearTodos() runViewHolder = null subagentViewHolders.clear() lastReportedPromptTokens = 0 } + private fun clearApprovalContainer() { + approvalContainer.removeAll() + approvalContainer.isVisible = false + approvalContainer.revalidate() + approvalContainer.repaint() + } + + private fun monitorBackgroundProcessOutput( + bgId: String, + toolId: String, + onComplete: (() -> Unit)? = null + ) { + serviceScope.launch { + try { + var outPos = 0 + var errPos = 0 + while (true) { + val po = BackgroundProcessManager.getOutput(bgId) ?: break + val stdout = po.stdout.toString() + val stderr = po.stderr.toString() + if (outPos < stdout.length) { + stdout.substring(outPos).split('\n').forEach { line -> + if (line.isNotEmpty()) toolOutputPublisher.toolOutput( + toolId, + line, + false + ) + } + outPos = stdout.length + } + if (errPos < stderr.length) { + stderr.substring(errPos).split('\n').forEach { line -> + if (line.isNotEmpty()) toolOutputPublisher.toolOutput( + toolId, + line, + true + ) + } + errPos = stderr.length + } + if (po.isComplete) break + delay(300) + } + } catch (ex: Exception) { + logger.warn("Failed to monitor background process output", ex) + } finally { + onComplete?.invoke() + } + } + } + fun setCurrentResponseBody(responseBody: ChatMessageResponseBody) { currentResponseBody = responseBody } @@ -135,11 +189,16 @@ class AgentEventHandler( private fun handleProxyAIException(ex: KoogHttpClientException) { when (ex.statusCode) { - 403 -> { + 401 -> { currentResponseBody?.displayMissingCredential() handleDone() } + 403 -> { + currentResponseBody?.displayInvalidCredential() + handleDone() + } + 429 -> { currentResponseBody?.displayCreditsExhausted() handleDone() @@ -276,38 +335,9 @@ class AgentEventHandler( if (bgId == null) { mainToolCards.remove(keyFor(id)) } else { - val publisher = - ApplicationManager.getApplication() - .messageBus - .syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC) - serviceScope.launch { - try { - var outPos = 0 - var errPos = 0 - while (true) { - val po = BackgroundProcessManager.getOutput(bgId) ?: break - val stdout = po.stdout.toString() - val stderr = po.stderr.toString() - if (outPos < stdout.length) { - stdout.substring(outPos).split('\n').forEach { line -> - if (line.isNotEmpty()) publisher.toolOutput(id, line, false) - } - outPos = stdout.length - } - if (errPos < stderr.length) { - stderr.substring(errPos).split('\n').forEach { line -> - if (line.isNotEmpty()) publisher.toolOutput(id, line, true) - } - errPos = stderr.length - } - if (po.isComplete) break - delay(300) - } - } catch (_: Exception) { - } finally { - runInEdt { - mainToolCards.remove(keyFor(id)) - } + monitorBackgroundProcessOutput(bgId, id) { + runInEdt { + mainToolCards.remove(keyFor(id)) } } } @@ -405,44 +435,7 @@ class AgentEventHandler( val bgId = (result as? BashTool.Result)?.bashId if (bgId != null) { - val publisher = - ApplicationManager.getApplication() - .messageBus - .syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC) - serviceScope.launch { - try { - var outPos = 0 - var errPos = 0 - while (true) { - val po = BackgroundProcessManager.getOutput(bgId) ?: break - val stdout = po.stdout.toString() - val stderr = po.stderr.toString() - if (outPos < stdout.length) { - stdout.substring(outPos).split('\n').forEach { line -> - if (line.isNotEmpty()) publisher.toolOutput( - childId, - line, - false - ) - } - outPos = stdout.length - } - if (errPos < stderr.length) { - stderr.substring(errPos).split('\n').forEach { line -> - if (line.isNotEmpty()) publisher.toolOutput( - childId, - line, - true - ) - } - errPos = stderr.length - } - if (po.isComplete) break - delay(300) - } - } catch (_: Exception) { - } - } + monitorBackgroundProcessOutput(bgId, childId) } } scrollablePanel.update() @@ -486,10 +479,7 @@ class AgentEventHandler( private fun maybeShowNextApproval() { if (currentApproval != null) return val next = approvalQueue.pollFirst() ?: run { - approvalContainer.removeAll() - approvalContainer.isVisible = false - approvalContainer.revalidate() - approvalContainer.repaint() + clearApprovalContainer() maybeShowNextQuestion() return } @@ -536,10 +526,7 @@ class AgentEventHandler( next.deferred.complete(true) currentApproval = null - approvalContainer.removeAll() - approvalContainer.isVisible = false - approvalContainer.revalidate() - approvalContainer.repaint() + clearApprovalContainer() runCatching { project.service() .setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING) @@ -549,10 +536,7 @@ class AgentEventHandler( onReject = { next.deferred.complete(false) currentApproval = null - approvalContainer.removeAll() - approvalContainer.isVisible = false - approvalContainer.revalidate() - approvalContainer.repaint() + clearApprovalContainer() try { project.service().cancelCurrentRun(sessionId) @@ -585,10 +569,7 @@ class AgentEventHandler( onSubmit = { answers -> next.deferred.complete(answers) currentQuestion = null - approvalContainer.removeAll() - approvalContainer.isVisible = false - approvalContainer.revalidate() - approvalContainer.repaint() + clearApprovalContainer() runCatching { project.service() .setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING) @@ -599,10 +580,7 @@ class AgentEventHandler( onCancel = { next.deferred.complete(emptyMap()) currentQuestion = null - approvalContainer.removeAll() - approvalContainer.isVisible = false - approvalContainer.revalidate() - approvalContainer.repaint() + clearApprovalContainer() runCatching { project.service() .setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING)