Skip to content

Commit 26c95e7

Browse files
committed
Extend example
1 parent 1a40f82 commit 26c95e7

File tree

7 files changed

+298
-81
lines changed

7 files changed

+298
-81
lines changed

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

Lines changed: 11 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import ai.koog.agents.core.feature.pipeline.AIAgentFunctionalPipeline
1111
import ai.koog.agents.core.feature.pipeline.AIAgentGraphPipeline
1212
import ai.koog.agents.core.feature.pipeline.AIAgentPipeline
1313
import ai.koog.agents.core.tools.annotations.InternalAgentToolsApi
14-
import ai.koog.prompt.message.ContentPart
15-
import ai.koog.prompt.message.Message
1614
import com.agentclientprotocol.common.Event
17-
import com.agentclientprotocol.model.ContentBlock
1815
import com.agentclientprotocol.model.PromptResponse
1916
import com.agentclientprotocol.model.SessionId
2017
import com.agentclientprotocol.model.SessionUpdate
@@ -29,14 +26,14 @@ import kotlin.contracts.InvocationKind
2926
import kotlin.contracts.contract
3027

3128
/**
32-
* AcpAgent is the main class for interacting with the Agent Client Protocol.
29+
* [AcpAgent] is the main class for interacting with the Agent Client Protocol.
30+
* https://agentclientprotocol.com/
31+
* This feature allows sending requests and notifications to the ACP Client via [sendEvent] or [protocol]
32+
* Notification can be handled automatically by default and can be configured via [AcpConfig.setDefaultNotifications]
3333
*
3434
* @property sessionId The session ID of the ACP agent.
3535
* @property protocol The protocol instance to use for sending requests and notifications to ACP Client.
3636
* @param eventsProducer A coroutine-based producer scope for sending [Event] instances.
37-
*
38-
* This feature allows sending requests and notifications to the ACP Client via [sendEvent] or [protocol]
39-
* Notification can be handled automatically by default and can be configured via [AcpConfig.setDefaultNotifications]
4037
*/
4138
public class AcpAgent(
4239
public val sessionId: SessionId,
@@ -45,7 +42,6 @@ public class AcpAgent(
4542
) {
4643
/**
4744
* Configuration for the ACP Agent feature.
48-
* @property setDefaultNotifications
4945
*/
5046
public class AcpConfig : FeatureConfig() {
5147
/**
@@ -72,6 +68,8 @@ public class AcpAgent(
7268

7369
/**
7470
* Sends [Event] to the connected ACP client.
71+
*
72+
* @param event The event to send.
7573
*/
7674
public suspend fun sendEvent(event: Event) {
7775
eventsProducer.send(event)
@@ -177,8 +175,7 @@ public class AcpAgent(
177175
}
178176

179177
pipeline.interceptAgentExecutionFailed(this@Feature) { ctx ->
180-
// TODO: Analyze the exception and emit appropriate event
181-
when (val thr = ctx.throwable) {
178+
when (ctx.throwable) {
182179
is AIAgentMaxNumberOfIterationsReachedException -> {
183180
logger.debug { "Emitting PromptResponseEvent with StopReason.MAX_TURN_REQUESTS" }
184181
sendEvent(
@@ -205,52 +202,9 @@ public class AcpAgent(
205202

206203
pipeline.interceptLLMCallCompleted(this@Feature) { ctx ->
207204
ctx.responses.forEach {
208-
when (it) {
209-
is Message.Assistant -> {
210-
it.parts.forEach { part ->
211-
when (part) {
212-
is ContentPart.Text -> {
213-
logger.debug { "Emitting SessionUpdateEvent for Assistant message chunk" }
214-
sendEvent(
215-
Event.SessionUpdateEvent(
216-
update = SessionUpdate.AgentMessageChunk(
217-
content = ContentBlock.Text(part.text)
218-
)
219-
)
220-
)
221-
}
222-
223-
else -> TODO("Implement other content parts")
224-
}
225-
}
226-
}
227-
228-
is Message.Reasoning -> {
229-
logger.debug { "Emitting AgentThoughtChunk event for Reasoning message chunk" }
230-
sendEvent(
231-
Event.SessionUpdateEvent(
232-
update = SessionUpdate.AgentThoughtChunk(
233-
content = ContentBlock.Text(it.content)
234-
)
235-
)
236-
)
237-
}
238-
239-
is Message.Tool.Call -> {
240-
logger.debug { "Emitting SessionUpdateEvent for ToolCall" }
241-
sendEvent(
242-
Event.SessionUpdateEvent(
243-
update = SessionUpdate.ToolCall(
244-
toolCallId = ToolCallId(it.id ?: "unknown"),
245-
// TODO: Support tool description in the event
246-
title = it.tool,
247-
// TODO: Support kind for tools
248-
status = ToolCallStatus.PENDING,
249-
rawInput = it.contentJson,
250-
)
251-
)
252-
)
253-
}
205+
it.toAcpEvents().forEach { event ->
206+
logger.debug { "Emitting event $event for LLM Call Completed" }
207+
sendEvent(event)
254208
}
255209
}
256210
}
@@ -308,7 +262,7 @@ public class AcpAgent(
308262
/**
309263
* Retrieves the [AcpAgent] feature from the agent context.
310264
*
311-
* @return The installed
265+
* @return The installed [AcpAgent] feature
312266
* @throws IllegalStateException if the feature is not installed
313267
*/
314268
public fun AIAgentContext.acpAgent(): AcpAgent = featureOrThrow(AcpAgent.Feature)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package ai.koog.agents.features.acp
2+
3+
/**
4+
* Exception class for errors specific to the ACP (Agent Communication Protocol) feature.
5+
*/
6+
public class AcpException(message: String, cause: Throwable? = null) : Exception(message, cause)
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package ai.koog.agents.features.acp
2+
3+
import ai.koog.prompt.message.AttachmentContent
4+
import ai.koog.prompt.message.ContentPart
5+
import ai.koog.prompt.message.Message
6+
import ai.koog.prompt.message.RequestMetaInfo
7+
import com.agentclientprotocol.common.Event.SessionUpdateEvent
8+
import com.agentclientprotocol.model.ContentBlock
9+
import com.agentclientprotocol.model.EmbeddedResourceResource
10+
import com.agentclientprotocol.model.SessionUpdate
11+
import com.agentclientprotocol.model.SessionUpdate.AgentMessageChunk
12+
import com.agentclientprotocol.model.ToolCallId
13+
import com.agentclientprotocol.model.ToolCallStatus
14+
import kotlinx.datetime.Clock
15+
16+
private const val UNKNOWN_FORMAT = "unknown"
17+
private const val UNKNOWN_MIME_TYPE = "unknown/unknown"
18+
private const val UNKNOWN_URI = "unknown"
19+
private const val UNKNOWN_FILE_NAME = "unknown"
20+
private const val UNKNOWN_TOOL_CALL_ID = "unknown"
21+
22+
private fun parseFormat(mimeType: String?): String {
23+
return mimeType?.split("/")?.lastOrNull() ?: UNKNOWN_FORMAT
24+
}
25+
26+
/**
27+
* Converts a list of [ContentBlock] of ACP prompt to a Koog [Message.User].
28+
*/
29+
public fun List<ContentBlock>.toKoogMessage(clock: Clock): Message {
30+
return Message.User(
31+
parts = this.map { it.toKoogContentPart() },
32+
metaInfo = RequestMetaInfo(clock.now())
33+
)
34+
}
35+
36+
/**
37+
* Converts a single [ContentBlock] of ACP prompt to a Koog [ContentPart].
38+
*/
39+
public fun ContentBlock.toKoogContentPart(): ContentPart {
40+
return when (this) {
41+
// https://agentclientprotocol.com/protocol/content#audio-content
42+
is ContentBlock.Audio -> {
43+
ContentPart.Audio(
44+
content = AttachmentContent.Binary.Base64(data),
45+
format = parseFormat(mimeType),
46+
mimeType = mimeType
47+
)
48+
}
49+
50+
// https://agentclientprotocol.com/protocol/content#image-content
51+
is ContentBlock.Image -> {
52+
ContentPart.Image(
53+
content = AttachmentContent.Binary.Base64(data),
54+
format = parseFormat(mimeType),
55+
mimeType = mimeType
56+
)
57+
}
58+
59+
// https://agentclientprotocol.com/protocol/content#embedded-resource
60+
is ContentBlock.Resource -> {
61+
when (val resource = this.resource) {
62+
is EmbeddedResourceResource.BlobResourceContents -> {
63+
ContentPart.File(
64+
content = AttachmentContent.Binary.Base64(resource.blob),
65+
format = parseFormat(resource.mimeType),
66+
mimeType = resource.mimeType ?: UNKNOWN_MIME_TYPE
67+
)
68+
}
69+
70+
is EmbeddedResourceResource.TextResourceContents -> {
71+
ContentPart.File(
72+
content = AttachmentContent.PlainText(resource.text),
73+
format = parseFormat(resource.mimeType),
74+
mimeType = resource.mimeType ?: UNKNOWN_MIME_TYPE
75+
)
76+
}
77+
}
78+
}
79+
80+
// https://agentclientprotocol.com/protocol/content#resource-link
81+
is ContentBlock.ResourceLink -> {
82+
ContentPart.File(
83+
content = AttachmentContent.URL(uri),
84+
format = parseFormat(mimeType),
85+
mimeType = mimeType ?: UNKNOWN_MIME_TYPE
86+
)
87+
}
88+
89+
// https://agentclientprotocol.com/protocol/content#text-content
90+
is ContentBlock.Text -> {
91+
ContentPart.Text(text)
92+
}
93+
}
94+
}
95+
96+
/**
97+
* Converts a [Message.Response] to a list of ACP [SessionUpdateEvent].
98+
*/
99+
public fun Message.Response.toAcpEvents(): List<SessionUpdateEvent> {
100+
val response = this
101+
return buildList {
102+
when (response) {
103+
is Message.Assistant -> {
104+
response.parts.forEach { part ->
105+
add(
106+
SessionUpdateEvent(
107+
update = AgentMessageChunk(part.toAcpContentBlock())
108+
)
109+
)
110+
}
111+
}
112+
113+
is Message.Reasoning -> {
114+
add(
115+
SessionUpdateEvent(
116+
update = SessionUpdate.AgentThoughtChunk(
117+
content = ContentBlock.Text(response.content)
118+
)
119+
)
120+
)
121+
}
122+
123+
is Message.Tool.Call -> {
124+
add(
125+
SessionUpdateEvent(
126+
update = SessionUpdate.ToolCall(
127+
toolCallId = ToolCallId(response.id ?: UNKNOWN_TOOL_CALL_ID),
128+
// TODO: Support tool description in the event
129+
title = response.tool,
130+
// TODO: Support kind for tools
131+
status = ToolCallStatus.PENDING,
132+
rawInput = response.contentJson,
133+
)
134+
)
135+
)
136+
}
137+
}
138+
}
139+
}
140+
141+
/**
142+
* Converts a ContentPart to an ACP ContentBlock.
143+
*/
144+
public fun ContentPart.toAcpContentBlock(): ContentBlock {
145+
return when (this) {
146+
is ContentPart.Text -> {
147+
ContentBlock.Text(this.text)
148+
}
149+
150+
is ContentPart.Audio -> {
151+
ContentBlock.Audio(
152+
data = this.content.toString(),
153+
mimeType = this.mimeType,
154+
)
155+
}
156+
157+
is ContentPart.File ->
158+
when (val content = this.content) {
159+
is AttachmentContent.Binary.Base64 -> ContentBlock.Resource(
160+
resource = EmbeddedResourceResource.BlobResourceContents(
161+
blob = content.base64,
162+
// TODO: add uri to the file
163+
uri = UNKNOWN_URI,
164+
mimeType = this.mimeType
165+
)
166+
)
167+
168+
is AttachmentContent.Binary.Bytes -> ContentBlock.Resource(
169+
resource = EmbeddedResourceResource.BlobResourceContents(
170+
blob = content.asBase64(),
171+
// TODO: add uri to the file
172+
uri = UNKNOWN_URI,
173+
mimeType = this.mimeType
174+
)
175+
)
176+
177+
is AttachmentContent.PlainText -> ContentBlock.Resource(
178+
resource = EmbeddedResourceResource.TextResourceContents(
179+
text = content.text,
180+
// TODO: add uri to the file
181+
uri = UNKNOWN_URI
182+
)
183+
)
184+
185+
is AttachmentContent.URL -> {
186+
ContentBlock.ResourceLink(
187+
name = this.fileName ?: UNKNOWN_FILE_NAME,
188+
uri = content.url
189+
)
190+
}
191+
}
192+
193+
is ContentPart.Image -> {
194+
ContentBlock.Image(
195+
data = this.content.toString(),
196+
mimeType = this.mimeType,
197+
)
198+
}
199+
200+
is ContentPart.Video -> {
201+
throw AcpException("Video content is not supported yet in Acp content blocks.")
202+
}
203+
}
204+
}

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

File renamed without changes.

0 commit comments

Comments
 (0)