Skip to content

Commit 9a79bd1

Browse files
committed
feat: introduce agent client protocol (acp)
1 parent 80b2490 commit 9a79bd1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+4510
-573
lines changed

src/main/java/ee/carlrobert/codegpt/Icons.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public final class Icons {
1717
IconLoader.getIcon("/icons/expandAll.svg", Icons.class);
1818
public static final Icon Anthropic = IconLoader.getIcon("/icons/anthropic.svg", Icons.class);
1919
public static final Icon DeepSeek = IconLoader.getIcon("/icons/deepseek.png", Icons.class);
20-
public static final Icon Qwen = IconLoader.getIcon("/icons/qwen.png", Icons.class);
2120
public static final Icon Google = IconLoader.getIcon("/icons/google.svg", Icons.class);
2221
public static final Icon Llama = IconLoader.getIcon("/icons/llama.svg", Icons.class);
2322
public static final Icon OpenAI = IconLoader.getIcon("/icons/openai.svg", Icons.class);

src/main/kotlin/ee/carlrobert/codegpt/agent/AgentService.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,23 @@ import com.intellij.openapi.components.Service
88
import com.intellij.openapi.components.service
99
import com.intellij.openapi.diagnostic.thisLogger
1010
import com.intellij.openapi.project.Project
11+
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentService
1112
import ee.carlrobert.codegpt.agent.history.AgentCheckpointHistoryService
1213
import ee.carlrobert.codegpt.agent.history.CheckpointRef
1314
import ee.carlrobert.codegpt.settings.models.ModelSettings
1415
import ee.carlrobert.codegpt.settings.service.FeatureType
1516
import ee.carlrobert.codegpt.settings.service.ServiceType
17+
import ee.carlrobert.codegpt.toolwindow.agent.AgentSession
1618
import ee.carlrobert.codegpt.toolwindow.agent.AgentToolWindowContentManager
1719
import ee.carlrobert.codegpt.ui.textarea.header.tag.McpTagDetails
1820
import kotlinx.coroutines.*
1921
import kotlinx.coroutines.flow.MutableSharedFlow
2022
import kotlinx.coroutines.flow.asSharedFlow
21-
import kotlin.time.Clock
2223
import kotlinx.serialization.json.JsonNull
2324
import java.util.*
2425
import java.util.concurrent.ConcurrentHashMap
2526
import kotlin.io.path.Path
27+
import kotlin.time.Clock
2628
import ai.koog.prompt.message.Message as PromptMessage
2729

2830
internal fun interface AgentRuntimeFactory {
@@ -112,6 +114,12 @@ class AgentService(private val project: Project) {
112114

113115
val provider = service<ModelSettings>().getServiceForFeature(FeatureType.AGENT)
114116
val contentManager = project.service<AgentToolWindowContentManager>()
117+
val session = contentManager.getSession(sessionId) ?: return
118+
if (!session.externalAgentId.isNullOrBlank()) {
119+
submitExternalMessage(session, message, events, provider)
120+
return
121+
}
122+
115123
val runtimeAgentId = contentManager.getSession(sessionId)?.runtimeAgentId
116124
val runtime = runCatching {
117125
ensureSessionRuntime(sessionId, provider, events)
@@ -156,6 +164,17 @@ class AgentService(private val project: Project) {
156164
}
157165

158166
fun cancelCurrentRun(sessionId: String) {
167+
val session = project.service<AgentToolWindowContentManager>().getSession(sessionId)
168+
if (session?.externalAgentId != null) {
169+
runCatching {
170+
runBlocking {
171+
project.service<ExternalAcpAgentService>()
172+
.cancelSession(sessionId, session.externalAgentSessionId)
173+
}
174+
}.onFailure { ex ->
175+
logger.warn("Failed cancelling external ACP session for session=$sessionId", ex)
176+
}
177+
}
159178
sessionJobs[sessionId]?.cancel()
160179
sessionJobs.remove(sessionId)
161180
}
@@ -171,6 +190,9 @@ class AgentService(private val project: Project) {
171190
logger.warn("Failed closing managed agent service for session=$sessionId", ex)
172191
}
173192
}
193+
project.service<ExternalAcpAgentService>().closeSession(sessionId)
194+
project.service<AgentToolWindowContentManager>()
195+
.getSession(sessionId)?.externalAgentSessionId = null
174196
project.service<AgentMcpContextService>().clear(sessionId)
175197
}
176198

@@ -190,6 +212,40 @@ class AgentService(private val project: Project) {
190212
return sessionAgents[sessionId]
191213
}
192214

215+
private fun submitExternalMessage(
216+
session: AgentSession,
217+
message: MessageWithContext,
218+
events: AgentEvents,
219+
provider: ServiceType
220+
) {
221+
val externalAgentService = project.service<ExternalAcpAgentService>()
222+
sessionJobs[session.sessionId] = CoroutineScope(Dispatchers.IO).launch {
223+
try {
224+
externalAgentService.runPromptLoop(
225+
session = session,
226+
firstMessage = message,
227+
events = events,
228+
pollNextQueued = {
229+
val queue = pendingMessages[session.sessionId] ?: return@runPromptLoop null
230+
if (queue.isEmpty()) {
231+
null
232+
} else {
233+
queue.removeFirst()
234+
}
235+
}
236+
)
237+
} catch (_: CancellationException) {
238+
return@launch
239+
} catch (ex: Throwable) {
240+
logger.error(ex)
241+
events.onAgentException(provider, ex)
242+
} finally {
243+
events.onRunCheckpointUpdated(message.id, null)
244+
sessionJobs.remove(session.sessionId)
245+
}
246+
}
247+
}
248+
193249
private fun updateMcpContext(sessionId: String, message: MessageWithContext) {
194250
val selectedServerIds = message.tags
195251
.filterIsInstance<McpTagDetails>()
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
package ee.carlrobert.codegpt.agent.external
2+
3+
data class ExternalAcpAgentPreset(
4+
val id: String,
5+
val displayName: String,
6+
val vendor: String,
7+
val command: String,
8+
val args: List<String>,
9+
val env: Map<String, String> = emptyMap(),
10+
val enabledByDefault: Boolean = false,
11+
val description: String? = null,
12+
) {
13+
fun fullCommand(): String = buildString {
14+
append(command)
15+
if (args.isNotEmpty()) {
16+
append(' ')
17+
append(args.joinToString(" "))
18+
}
19+
}
20+
}
21+
22+
object ExternalAcpAgents {
23+
24+
private val presets = listOf(
25+
ExternalAcpAgentPreset(
26+
id = "codex",
27+
displayName = "Codex",
28+
vendor = "OpenAI",
29+
command = "npx",
30+
args = listOf("-y", "@zed-industries/codex-acp"),
31+
enabledByDefault = true,
32+
description = "OpenAI Codex via the Zed ACP adapter."
33+
),
34+
ExternalAcpAgentPreset(
35+
id = "opencode",
36+
displayName = "OpenCode",
37+
vendor = "OpenCode",
38+
command = "opencode",
39+
args = listOf("acp"),
40+
enabledByDefault = true,
41+
description = "OpenCode CLI running its ACP server."
42+
),
43+
ExternalAcpAgentPreset(
44+
id = "cursor",
45+
displayName = "Cursor",
46+
vendor = "Cursor",
47+
command = "agent",
48+
args = listOf("acp"),
49+
description = "Cursor Agent in ACP mode."
50+
),
51+
ExternalAcpAgentPreset(
52+
id = "claude-code",
53+
displayName = "Claude Code",
54+
vendor = "Anthropic",
55+
command = "npx",
56+
args = listOf("-y", "@zed-industries/claude-code-acp"),
57+
enabledByDefault = true,
58+
description = "Anthropic Claude Code via the Zed ACP adapter."
59+
),
60+
ExternalAcpAgentPreset(
61+
id = "gemini-cli",
62+
displayName = "Gemini CLI",
63+
vendor = "Google",
64+
command = "gemini",
65+
args = listOf("--experimental-acp"),
66+
description = "Google Gemini CLI in experimental ACP mode."
67+
),
68+
ExternalAcpAgentPreset(
69+
id = "goose",
70+
displayName = "Goose",
71+
vendor = "Block",
72+
command = "goose",
73+
args = listOf("acp"),
74+
description = "Block Goose running as an ACP server."
75+
),
76+
ExternalAcpAgentPreset(
77+
id = "github-copilot",
78+
displayName = "GitHub Copilot",
79+
vendor = "GitHub",
80+
command = "copilot",
81+
args = listOf("--acp"),
82+
description = "GitHub Copilot CLI ACP server."
83+
),
84+
ExternalAcpAgentPreset(
85+
id = "qwen-code",
86+
displayName = "Qwen Code",
87+
vendor = "Qwen",
88+
command = "qwen",
89+
args = listOf("--acp"),
90+
description = "Qwen Code running its ACP server."
91+
),
92+
ExternalAcpAgentPreset(
93+
id = "auggie",
94+
displayName = "Auggie CLI",
95+
vendor = "Augment",
96+
command = "auggie",
97+
args = listOf("--acp"),
98+
description = "Augment's Auggie CLI in ACP mode."
99+
),
100+
ExternalAcpAgentPreset(
101+
id = "agentpool",
102+
displayName = "AgentPool",
103+
vendor = "AgentPool",
104+
command = "agentpool",
105+
args = listOf("serve-acp", "agents.yml"),
106+
description = "AgentPool serving ACP from a local agents.yml configuration."
107+
),
108+
ExternalAcpAgentPreset(
109+
id = "blackbox-ai",
110+
displayName = "Blackbox AI",
111+
vendor = "Blackbox AI",
112+
command = "blackbox",
113+
args = listOf("--experimental-acp"),
114+
description = "Blackbox CLI running in experimental ACP mode."
115+
),
116+
ExternalAcpAgentPreset(
117+
id = "claude-agent",
118+
displayName = "Claude Agent",
119+
vendor = "Anthropic",
120+
command = "npx",
121+
args = listOf("-y", "@zed-industries/claude-agent-acp"),
122+
description = "Anthropic Claude Agent via the Zed ACP adapter."
123+
),
124+
ExternalAcpAgentPreset(
125+
id = "cline",
126+
displayName = "Cline",
127+
vendor = "Cline",
128+
command = "npx",
129+
args = listOf("-y", "cline", "--acp"),
130+
description = "Cline running as an ACP server."
131+
),
132+
ExternalAcpAgentPreset(
133+
id = "code-assistant",
134+
displayName = "Code Assistant",
135+
vendor = "stippi",
136+
command = "code-assistant",
137+
args = listOf("acp"),
138+
description = "Code Assistant running in ACP agent mode."
139+
),
140+
ExternalAcpAgentPreset(
141+
id = "docker-cagent",
142+
displayName = "Docker's cagent",
143+
vendor = "Docker",
144+
command = "cagent",
145+
args = listOf("acp"),
146+
description = "Docker cagent serving ACP; project agent configuration may still be required."
147+
),
148+
ExternalAcpAgentPreset(
149+
id = "fast-agent",
150+
displayName = "fast-agent",
151+
vendor = "fast-agent",
152+
command = "uvx",
153+
args = listOf("fast-agent-acp", "-x"),
154+
description = "fast-agent's ACP bridge via uvx."
155+
),
156+
ExternalAcpAgentPreset(
157+
id = "factory-droid",
158+
displayName = "Factory Droid",
159+
vendor = "Factory AI",
160+
command = "npx",
161+
args = listOf("-y", "droid", "exec", "--output-format", "acp"),
162+
env = mapOf(
163+
"DROID_DISABLE_AUTO_UPDATE" to "true",
164+
"FACTORY_DROID_AUTO_UPDATE_ENABLED" to "false",
165+
),
166+
description = "Factory Droid running in ACP mode."
167+
),
168+
ExternalAcpAgentPreset(
169+
id = "junie",
170+
displayName = "Junie",
171+
vendor = "JetBrains",
172+
command = "junie",
173+
args = listOf("--acp=true"),
174+
description = "JetBrains Junie running as an ACP agent."
175+
),
176+
ExternalAcpAgentPreset(
177+
id = "kimi-cli",
178+
displayName = "Kimi CLI",
179+
vendor = "Moonshot AI",
180+
command = "kimi",
181+
args = listOf("acp"),
182+
description = "Moonshot AI's Kimi CLI in ACP mode."
183+
),
184+
ExternalAcpAgentPreset(
185+
id = "kiro-cli",
186+
displayName = "Kiro CLI",
187+
vendor = "Kiro",
188+
command = "kiro",
189+
args = listOf("--acp"),
190+
description = "Kiro CLI running as an ACP-compliant agent."
191+
),
192+
ExternalAcpAgentPreset(
193+
id = "minion-code",
194+
displayName = "Minion Code",
195+
vendor = "Minion",
196+
command = "uvx",
197+
args = listOf("minion-code", "acp"),
198+
description = "Minion Code running its ACP server."
199+
),
200+
ExternalAcpAgentPreset(
201+
id = "mistral-vibe",
202+
displayName = "Mistral Vibe",
203+
vendor = "Mistral AI",
204+
command = "vibe-acp",
205+
args = emptyList(),
206+
description = "Mistral Vibe's ACP bridge."
207+
),
208+
ExternalAcpAgentPreset(
209+
id = "openclaw",
210+
displayName = "OpenClaw",
211+
vendor = "OpenClaw",
212+
command = "openclaw",
213+
args = listOf("acp"),
214+
description = "OpenClaw running as an ACP server."
215+
),
216+
ExternalAcpAgentPreset(
217+
id = "openhands",
218+
displayName = "OpenHands",
219+
vendor = "All Hands AI",
220+
command = "openhands",
221+
args = listOf("acp"),
222+
description = "OpenHands in ACP mode."
223+
),
224+
ExternalAcpAgentPreset(
225+
id = "pi",
226+
displayName = "Pi",
227+
vendor = "pi",
228+
command = "npx",
229+
args = listOf("-y", "pi-acp"),
230+
description = "Pi via the pi ACP adapter."
231+
),
232+
ExternalAcpAgentPreset(
233+
id = "qoder-cli",
234+
displayName = "Qoder CLI",
235+
vendor = "Qoder AI",
236+
command = "npx",
237+
args = listOf("-y", "@qoder-ai/qodercli", "--acp"),
238+
description = "Qoder CLI running its ACP server."
239+
),
240+
ExternalAcpAgentPreset(
241+
id = "stakpak",
242+
displayName = "Stakpak",
243+
vendor = "Stakpak",
244+
command = "stakpak",
245+
args = listOf("acp"),
246+
description = "Stakpak running in ACP mode."
247+
),
248+
ExternalAcpAgentPreset(
249+
id = "vt-code",
250+
displayName = "VT Code",
251+
vendor = "VT Code",
252+
command = "vtcode",
253+
args = listOf("acp"),
254+
description = "VT Code running its ACP bridge."
255+
)
256+
)
257+
258+
fun all(): List<ExternalAcpAgentPreset> = presets.sortedBy { it.displayName.lowercase() }
259+
260+
fun find(id: String?): ExternalAcpAgentPreset? = presets.firstOrNull { it.id == id }
261+
262+
fun enabledByDefaultIds(): List<String> =
263+
presets.filter { it.enabledByDefault }.map { it.id }
264+
}

0 commit comments

Comments
 (0)