Skip to content

Commit 4804618

Browse files
authored
Update ACP SDK version to 0.13.1 and example (#1363)
<!-- Thank you for opening a pull request! Please add a brief description of the proposed change here. Also, please tick the appropriate points in the checklist below. --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> --- #### Type of the changes - [ ] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Tests improvement - [ ] Refactoring - [ ] CI/CD changes - [x] Dependencies update #### Checklist - [ ] The pull request has a description of the proposed change - [ ] I read the [Contributing Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md) before opening the pull request - [ ] The pull request uses **`develop`** as the base branch - [ ] Tests for the changes have been added - [ ] All new and existing tests passed ##### Additional steps for pull requests adding a new feature - [ ] An issue describing the proposed change exists - [ ] The pull request includes a link to the issue - [ ] The change was discussed and approved in the issue - [ ] Docs have been added / updated
1 parent 3d17d38 commit 4804618

File tree

6 files changed

+137
-67
lines changed

6 files changed

+137
-67
lines changed

agents/agents-features/agents-features-acp/src/jvmMain/kotlin/ai/koog/agents/features/acp/AcpAgent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ public class AcpAgent(
228228
logger.debug { "Emitting SessionUpdateEvent for ToolCall Failed" }
229229
sendEvent(
230230
Event.SessionUpdateEvent(
231-
update = SessionUpdate.ToolCall(
231+
update = SessionUpdate.ToolCallUpdate(
232232
toolCallId = ToolCallId(ctx.toolCallId ?: UNKNOWN_TOOL_CALL_ID),
233233
title = ctx.toolDescription ?: UNKNOWN_TOOL_DESCRIPTION,
234234
// TODO: Support kind for tools
@@ -243,7 +243,7 @@ public class AcpAgent(
243243
logger.debug { "Emitting SessionUpdateEvent for ToolCall Completed" }
244244
sendEvent(
245245
Event.SessionUpdateEvent(
246-
update = SessionUpdate.ToolCall(
246+
update = SessionUpdate.ToolCallUpdate(
247247
toolCallId = ToolCallId(ctx.toolCallId ?: UNKNOWN_TOOL_CALL_ID),
248248
title = ctx.toolDescription ?: UNKNOWN_TOOL_DESCRIPTION,
249249
// TODO: Support kind for tools

examples/simple-examples/src/main/kotlin/ai/koog/agents/example/acp/AcpTerminalClient.kt

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
11
package ai.koog.agents.example.acp
22

3-
import com.agentclientprotocol.client.Client
4-
import com.agentclientprotocol.client.ClientInfo
5-
import com.agentclientprotocol.client.ClientSession
6-
import com.agentclientprotocol.client.ClientSupport
3+
import com.agentclientprotocol.client.*
74
import com.agentclientprotocol.common.ClientSessionOperations
85
import com.agentclientprotocol.common.Event
9-
import com.agentclientprotocol.common.SessionParameters
10-
import com.agentclientprotocol.model.ContentBlock
11-
import com.agentclientprotocol.model.PermissionOption
12-
import com.agentclientprotocol.model.RequestPermissionOutcome
13-
import com.agentclientprotocol.model.RequestPermissionResponse
14-
import com.agentclientprotocol.model.SessionUpdate
15-
import com.agentclientprotocol.model.StopReason
6+
import com.agentclientprotocol.common.FileSystemOperations
7+
import com.agentclientprotocol.common.SessionCreationParameters
8+
import com.agentclientprotocol.common.TerminalOperations
9+
import com.agentclientprotocol.model.*
1610
import com.agentclientprotocol.protocol.Protocol
1711
import com.agentclientprotocol.transport.Transport
1812
import io.github.oshai.kotlinlogging.KotlinLogging
1913
import kotlinx.coroutines.CoroutineScope
2014
import kotlinx.serialization.json.JsonElement
15+
import java.io.File
2116
import java.nio.file.Paths
17+
import java.util.UUID
18+
import java.util.concurrent.ConcurrentHashMap
2219
import kotlin.io.path.absolutePathString
20+
import kotlin.io.path.readText
21+
import kotlin.io.path.writeText
2322

2423
private val logger = KotlinLogging.logger {}
2524

26-
class TerminalClientSupport : ClientSupport {
27-
override suspend fun createClientSession(
28-
session: ClientSession,
29-
_sessionResponseMeta: JsonElement?
30-
): ClientSessionOperations {
31-
return TerminalClientSessionOperations()
32-
}
33-
}
25+
class TerminalClientSessionOperations : ClientSessionOperations, FileSystemOperations, TerminalOperations {
26+
private val activeTerminals = ConcurrentHashMap<String, Process>()
3427

35-
class TerminalClientSessionOperations : ClientSessionOperations {
3628
override suspend fun requestPermissions(
3729
toolCall: SessionUpdate.ToolCallUpdate,
3830
permissions: List<PermissionOption>,
@@ -59,29 +51,119 @@ class TerminalClientSessionOperations : ClientSessionOperations {
5951
println("Agent sent notification:")
6052
notification.render()
6153
}
54+
55+
override suspend fun fsReadTextFile(
56+
path: String,
57+
line: UInt?,
58+
limit: UInt?,
59+
_meta: JsonElement?,
60+
): ReadTextFileResponse {
61+
val content = Paths.get(path).readText()
62+
return ReadTextFileResponse(content)
63+
}
64+
65+
override suspend fun fsWriteTextFile(
66+
path: String,
67+
content: String,
68+
_meta: JsonElement?,
69+
): WriteTextFileResponse {
70+
Paths.get(path).writeText(content)
71+
return WriteTextFileResponse()
72+
}
73+
74+
override suspend fun terminalCreate(
75+
command: String,
76+
args: List<String>,
77+
cwd: String?,
78+
env: List<EnvVariable>,
79+
outputByteLimit: ULong?,
80+
_meta: JsonElement?,
81+
): CreateTerminalResponse {
82+
val processBuilder = ProcessBuilder(listOf(command) + args)
83+
if (cwd != null) {
84+
processBuilder.directory(File(cwd))
85+
}
86+
env.forEach { processBuilder.environment()[it.name] = it.value }
87+
88+
val process = processBuilder.start()
89+
val terminalId = UUID.randomUUID().toString()
90+
activeTerminals[terminalId] = process
91+
92+
return CreateTerminalResponse(terminalId)
93+
}
94+
95+
override suspend fun terminalOutput(
96+
terminalId: String,
97+
_meta: JsonElement?,
98+
): TerminalOutputResponse {
99+
val process = activeTerminals[terminalId] ?: error("Terminal not found: $terminalId")
100+
val stdout = process.inputStream.bufferedReader().readText()
101+
val stderr = process.errorStream.bufferedReader().readText()
102+
val output = if (stderr.isNotEmpty()) "$stdout\nSTDERR:\n$stderr" else stdout
103+
104+
return TerminalOutputResponse(output, truncated = false)
105+
}
106+
107+
override suspend fun terminalRelease(
108+
terminalId: String,
109+
_meta: JsonElement?,
110+
): ReleaseTerminalResponse {
111+
activeTerminals.remove(terminalId)
112+
return ReleaseTerminalResponse()
113+
}
114+
115+
override suspend fun terminalWaitForExit(
116+
terminalId: String,
117+
_meta: JsonElement?,
118+
): WaitForTerminalExitResponse {
119+
val process = activeTerminals[terminalId] ?: error("Terminal not found: $terminalId")
120+
val exitCode = process.waitFor()
121+
return WaitForTerminalExitResponse(exitCode.toUInt())
122+
}
123+
124+
override suspend fun terminalKill(
125+
terminalId: String,
126+
_meta: JsonElement?,
127+
): KillTerminalCommandResponse {
128+
val process = activeTerminals[terminalId]
129+
process?.destroy()
130+
return KillTerminalCommandResponse()
131+
}
62132
}
63133

64134
suspend fun CoroutineScope.runTerminalClient(transport: Transport) {
65135
// Create client-side connection
66136
val protocol = Protocol(this, transport)
67-
val client = Client(protocol, TerminalClientSupport())
137+
val client = Client(
138+
protocol
139+
)
68140

69-
logger.info { "Starting agent process..." }
141+
logger.info { "Starting Gemini agent process..." }
70142

71143
// Connect to agent and start transport
72144
protocol.start()
73145

74-
logger.info { "Connected to agent, initializing..." }
75-
76-
val agentInfo = client.initialize(ClientInfo())
146+
logger.info { "Connected to Gemini agent, initializing..." }
147+
148+
val agentInfo = client.initialize(
149+
ClientInfo(
150+
capabilities = ClientCapabilities(
151+
fs = FileSystemCapability(
152+
readTextFile = true,
153+
writeTextFile = true
154+
),
155+
terminal = true
156+
)
157+
)
158+
)
77159
println("Agent info: $agentInfo")
78160

79161
println()
80162

81163
// Create a session
82164
val session = client.newSession(
83-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
84-
)
165+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
166+
) { session, _ -> TerminalClientSessionOperations() }
85167

86168
println("=== Session created: ${session.sessionId} ===")
87169
println("Type your messages below. Use 'exit', 'quit', or Ctrl+C to stop.")
@@ -116,7 +198,6 @@ suspend fun CoroutineScope.runTerminalClient(transport: Transport) {
116198
when (event.response.stopReason) {
117199
StopReason.END_TURN -> {
118200
// Normal completion - no action needed
119-
println("Success!")
120201
}
121202

122203
StopReason.MAX_TOKENS -> {

examples/simple-examples/src/main/kotlin/ai/koog/agents/example/acp/util.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ fun SessionUpdate.render() {
4040
println("User: ${this.content.render()}")
4141
}
4242

43+
else -> {
44+
println("Unsupported chunk: [${this::class.simpleName}]")
45+
}
4346
}
4447
}
4548

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
acp = "0.3.0"
2+
acp = "0.13.1"
33
agp = "8.12.3"
44
annotations = "26.0.2-1"
55
assertj = "3.27.6"

integration-tests/src/jvmTest/kotlin/ai/koog/integration/tests/acp/AcpProtocolTest.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ai.koog.agents.testing.tools.RandomNumberTool
44
import ai.koog.integration.tests.utils.getLLMClientForProvider
55
import ai.koog.prompt.executor.clients.openai.OpenAIModels
66
import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor
7-
import com.agentclientprotocol.common.SessionParameters
7+
import com.agentclientprotocol.common.SessionCreationParameters
88
import com.agentclientprotocol.model.AcpMethod
99
import com.agentclientprotocol.model.AuthMethod
1010
import com.agentclientprotocol.model.AuthMethodId
@@ -65,15 +65,13 @@ class AcpProtocolTest {
6565

6666
withTimeout(5.seconds) {
6767
while (
68-
setup.clientSupport.lastOperations.flatMap { it.notifications }
69-
.none { it is SessionUpdate.AgentMessageChunk }
68+
setup.clientOperations.notifications.none { it is SessionUpdate.AgentMessageChunk }
7069
) {
7170
delay(10)
7271
}
7372
}
7473

75-
val received = setup.clientSupport.lastOperations.flatMap { it.notifications }
76-
received.shouldForAny {
74+
setup.clientOperations.notifications.shouldForAny {
7775
it is SessionUpdate.AgentMessageChunk && (it.content as? ContentBlock.Text)?.text == NOTIFICATION
7876
}
7977
} finally {
@@ -116,15 +114,15 @@ class AcpProtocolTest {
116114
if (loadSession) {
117115
val loaded = setup.client.loadSession(
118116
session.sessionId,
119-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
120-
)
117+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
118+
) { _, _ -> TestClientSessionOperations() }
121119
loaded.sessionId shouldBe session.sessionId
122120
} else {
123121
shouldThrow<Exception> {
124122
setup.client.loadSession(
125123
session.sessionId,
126-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
127-
)
124+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
125+
) { _, _ -> TestClientSessionOperations() }
128126
}
129127
}
130128

@@ -165,8 +163,8 @@ class AcpProtocolTest {
165163

166164
val loadedSession = setup.client.loadSession(
167165
setup.session.shouldNotBeNull().sessionId,
168-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
169-
)
166+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
167+
) { _, _ -> TestClientSessionOperations() }
170168
loadedSession.sessionId shouldBe setup.session.sessionId
171169
} finally {
172170
setup.cleanup()
@@ -194,15 +192,15 @@ class AcpProtocolTest {
194192
val loadSessionException = shouldThrow<Exception> {
195193
setup.client.loadSession(
196194
SessionId("test-session"),
197-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
198-
)
195+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
196+
) { _, _ -> TestClientSessionOperations() }
199197
}
200198
loadSessionException.message shouldContain AUTH_ERROR
201199

202200
val newSessionException = shouldThrow<Exception> {
203201
setup.client.newSession(
204-
SessionParameters(Paths.get("").absolutePathString(), emptyList())
205-
)
202+
SessionCreationParameters(Paths.get("").absolutePathString(), emptyList())
203+
) { _, _ -> TestClientSessionOperations() }
206204
}
207205
newSessionException.message shouldContain AUTH_ERROR
208206
} finally {

integration-tests/src/jvmTest/kotlin/ai/koog/integration/tests/acp/AcpTestFixture.kt

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import com.agentclientprotocol.agent.AgentSupport
1818
import com.agentclientprotocol.client.Client
1919
import com.agentclientprotocol.client.ClientInfo
2020
import com.agentclientprotocol.client.ClientSession
21-
import com.agentclientprotocol.client.ClientSupport
2221
import com.agentclientprotocol.common.ClientSessionOperations
2322
import com.agentclientprotocol.common.Event
24-
import com.agentclientprotocol.common.SessionParameters
23+
import com.agentclientprotocol.common.SessionCreationParameters
2524
import com.agentclientprotocol.model.AcpMethod
2625
import com.agentclientprotocol.model.AgentCapabilities
2726
import com.agentclientprotocol.model.AuthMethod
@@ -67,7 +66,7 @@ data class AcpClientSetup(
6766
val client: Client,
6867
val session: ClientSession?,
6968
val agentSupport: TestKoogAgentSupport,
70-
val clientSupport: TestClientSupport,
69+
val clientOperations: TestClientSessionOperations,
7170
val agentInfo: AgentInfo,
7271
val cleanup: () -> Unit
7372
)
@@ -138,8 +137,8 @@ suspend fun setupAcpClient(
138137
agentProtocol.start()
139138

140139
val clientProtocol = Protocol(protocolScope, clientTransport)
141-
val clientSupport = TestClientSupport()
142-
val client = Client(clientProtocol, clientSupport)
140+
val client = Client(clientProtocol)
141+
val testClientSessionOperations = TestClientSessionOperations()
143142
clientProtocol.start()
144143

145144
val agentInfo = client.initialize(ClientInfo())
@@ -157,13 +156,13 @@ suspend fun setupAcpClient(
157156

158157
val session = if (createSession) {
159158
client.newSession(
160-
SessionParameters(java.nio.file.Paths.get("").toAbsolutePath().toString(), emptyList())
161-
)
159+
SessionCreationParameters(java.nio.file.Paths.get("").toAbsolutePath().toString(), emptyList())
160+
) { _, _ -> testClientSessionOperations }
162161
} else {
163162
null
164163
}
165164

166-
return AcpClientSetup(client, session, support, clientSupport, agentInfo) {
165+
return AcpClientSetup(client, session, support, testClientSessionOperations, agentInfo) {
167166
agentTransport.close()
168167
clientTransport.close()
169168
clientToAgent.sink().close()
@@ -277,7 +276,7 @@ class TestKoogAgentSupport(
277276
}
278277

279278
@OptIn(ExperimentalUuidApi::class)
280-
override suspend fun createSession(sessionParameters: SessionParameters): AgentSession {
279+
override suspend fun createSession(sessionParameters: SessionCreationParameters): AgentSession {
281280
if (authMethods.isNotEmpty() && !authenticated) {
282281
throw IllegalStateException("Authentication required")
283282
}
@@ -297,7 +296,7 @@ class TestKoogAgentSupport(
297296

298297
override suspend fun loadSession(
299298
sessionId: SessionId,
300-
sessionParameters: SessionParameters,
299+
sessionParameters: SessionCreationParameters,
301300
): AgentSession {
302301
if (authMethods.isNotEmpty() && !authenticated) {
303302
throw IllegalStateException("Authentication required")
@@ -309,17 +308,6 @@ class TestKoogAgentSupport(
309308
}
310309
}
311310

312-
class TestClientSupport : ClientSupport {
313-
val lastOperations = mutableListOf<TestClientSessionOperations>()
314-
315-
override suspend fun createClientSession(
316-
session: ClientSession,
317-
_sessionResponseMeta: JsonElement?
318-
): ClientSessionOperations {
319-
return TestClientSessionOperations().also { lastOperations.add(it) }
320-
}
321-
}
322-
323311
class TestClientSessionOperations : ClientSessionOperations {
324312
val notifications = mutableListOf<SessionUpdate>()
325313

0 commit comments

Comments
 (0)