Skip to content

Commit 3eaef37

Browse files
committed
[agents] Add a2a client feature, update a2a server feature docs
1 parent ec2a62f commit 3eaef37

File tree

8 files changed

+474
-31
lines changed

8 files changed

+474
-31
lines changed

a2a/a2a-core/src/commonMain/kotlin/ai/koog/a2a/transport/ClientTransport.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ai.koog.a2a.model.TaskPushNotificationConfig
1111
import ai.koog.a2a.model.TaskPushNotificationConfigParams
1212
import ai.koog.a2a.model.TaskQueryParams
1313
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.serialization.Serializable
1415
import kotlinx.serialization.SerializationException
1516

1617
/**
@@ -131,7 +132,8 @@ public interface ClientTransport : AutoCloseable {
131132
*
132133
* @property additionalHeaders Additional call-specific headers associated with the call.
133134
*/
134-
public class ClientCallContext(
135+
@Serializable
136+
public data class ClientCallContext(
135137
public val additionalHeaders: Map<String, List<String>> = emptyMap(),
136138
) {
137139
@Suppress("MissingKDocForPublicAPI")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import ai.koog.gradle.publish.maven.Publishing.publishToMaven
2+
3+
group = rootProject.group
4+
version = rootProject.version
5+
6+
plugins {
7+
id("ai.kotlin.multiplatform")
8+
alias(libs.plugins.kotlin.serialization)
9+
}
10+
11+
kotlin {
12+
sourceSets {
13+
commonMain {
14+
dependencies {
15+
api(project(":agents:agents-core"))
16+
api(project(":a2a:a2a-client"))
17+
api(project(":agents:agents-features:agents-features-a2a-core"))
18+
19+
api(libs.kotlinx.serialization.json)
20+
}
21+
}
22+
23+
commonTest {
24+
dependencies {
25+
implementation(kotlin("test"))
26+
implementation(libs.kotlinx.coroutines.test)
27+
}
28+
}
29+
30+
jvmTest {
31+
dependencies {
32+
implementation(kotlin("test-junit5"))
33+
}
34+
}
35+
}
36+
37+
explicitApi()
38+
}
39+
40+
publishToMaven()
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package ai.koog.agents.a2a.client.feature
2+
3+
import ai.koog.a2a.client.A2AClient
4+
import ai.koog.agents.core.agent.context.AIAgentContext
5+
import ai.koog.agents.core.agent.entity.AIAgentStorageKey
6+
import ai.koog.agents.core.agent.entity.createStorageKey
7+
import ai.koog.agents.core.feature.AIAgentGraphFeature
8+
import ai.koog.agents.core.feature.AIAgentGraphPipeline
9+
import ai.koog.agents.core.feature.AIAgentNonGraphFeature
10+
import ai.koog.agents.core.feature.AIAgentNonGraphPipeline
11+
import ai.koog.agents.core.feature.config.FeatureConfig
12+
import kotlin.contracts.ExperimentalContracts
13+
import kotlin.contracts.InvocationKind
14+
import kotlin.contracts.contract
15+
16+
/**
17+
* Agent feature that enables A2A client mode by providing access to registered A2A clients
18+
* from within agent strategies.
19+
*
20+
* This feature allows agents to communicate with other A2A-enabled agents by making
21+
* [A2AClient] instances available to agent nodes. When installed, it provides access to
22+
* a registry of A2A clients, allowing agent nodes to:
23+
* - Send messages to remote A2A agents
24+
* - Retrieve agent cards and capabilities
25+
* - Manage tasks on remote agents
26+
* - Subscribe to task events and streaming responses
27+
* - Configure push notifications
28+
*
29+
* The feature provides convenience nodes for common A2A client
30+
* operations like sending messages, retrieving tasks, and managing subscriptions.
31+
*
32+
* @property a2aClients Map of A2A clients keyed by agent ID
33+
*
34+
* @see ai.koog.a2a.client.A2AClient
35+
*/
36+
public class A2AAgentClient(
37+
public val a2aClients: Map<String, A2AClient>
38+
) {
39+
/**
40+
* Configuration for the [A2AAgentClient] feature.
41+
*/
42+
public class Config : FeatureConfig() {
43+
/**
44+
* Map of [A2AClient] instances keyed by agent ID for accessing remote A2A agents.
45+
*/
46+
public val a2aClients: MutableMap<String, A2AClient> = mutableMapOf()
47+
}
48+
49+
public companion object Feature :
50+
AIAgentGraphFeature<Config, A2AAgentClient>,
51+
AIAgentNonGraphFeature<Config, A2AAgentClient> {
52+
53+
override val key: AIAgentStorageKey<A2AAgentClient> =
54+
createStorageKey<A2AAgentClient>("agents-features-a2a-client")
55+
56+
override fun createInitialConfig(): Config = Config()
57+
58+
override fun install(
59+
config: Config,
60+
pipeline: AIAgentGraphPipeline
61+
) {
62+
pipeline.interceptContextAgentFeature(this) { _ ->
63+
A2AAgentClient(config.a2aClients.toMap())
64+
}
65+
}
66+
67+
override fun install(
68+
config: Config,
69+
pipeline: AIAgentNonGraphPipeline
70+
) {
71+
pipeline.interceptContextAgentFeature(this) {
72+
A2AAgentClient(config.a2aClients.toMap())
73+
}
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Retrieves the [A2AAgentClient] feature from the agent context.
80+
*
81+
* @return The installed A2AAgentClient feature
82+
* @throws IllegalStateException if the feature is not installed
83+
*/
84+
public fun AIAgentContext.a2aAgentClient(): A2AAgentClient = featureOrThrow(A2AAgentClient.Feature)
85+
86+
/**
87+
* Executes an action with the [A2AAgentClient] feature as the receiver.
88+
* This is a convenience function that retrieves the feature and provides it as the receiver for the action block.
89+
*
90+
* @param action The action to execute with A2AAgentClient as receiver
91+
* @return The result of the action
92+
* @throws IllegalStateException if the feature is not installed
93+
*/
94+
@OptIn(ExperimentalContracts::class)
95+
public inline fun <T> AIAgentContext.withA2AAgentClient(action: A2AAgentClient.() -> T): T {
96+
contract {
97+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
98+
}
99+
100+
return a2aAgentClient().action()
101+
}
102+
103+
/**
104+
* Retrieves an A2A client by agent ID or throws if not found.
105+
*
106+
* @param agentId The identifier of the A2A agent to retrieve
107+
* @return The A2AClient instance for the specified agent ID
108+
* @throws NoSuchElementException if no client is registered with the given agent ID
109+
*/
110+
public fun A2AAgentClient.a2aClientOrThrow(agentId: String): A2AClient =
111+
a2aClients[agentId] ?: throw NoSuchElementException("A2A agent with id $agentId not found in the current agent context. Make sure to register it in the A2AAgentClient feature.")

0 commit comments

Comments
 (0)