Skip to content

Commit fd331fd

Browse files
committed
feat(a2a): implement A2A command and service for agent communication #443
1 parent e1849bb commit fd331fd

File tree

12 files changed

+513
-3
lines changed

12 files changed

+513
-3
lines changed

core/src/233/main/resources/META-INF/autodev-core.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,9 @@
381381
<!-- Library Version Providers -->
382382
<libraryVersionProvider implementation="cc.unitmesh.devti.provider.impl.NpmVersionProvider"/>
383383
<libraryVersionProvider implementation="cc.unitmesh.devti.provider.impl.MavenVersionProvider"/>
384+
385+
<!-- A2A Sketch Toolchain Provider -->
386+
<sketchToolchainProvider implementation="cc.unitmesh.devti.a2a.A2ASketchToolchainProvider"/>
384387
</extensions>
385388

386389
<actions>

core/src/main/kotlin/cc/unitmesh/devti/AutoDevIcons.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,7 @@ object AutoDevIcons {
118118

119119
@JvmField
120120
val GITHUB_ISSUE: Icon = IconLoader.getIcon("/icons/github-issue.svg", AutoDevIcons::class.java)
121+
122+
@JvmField
123+
val A2A: Icon = IconLoader.getIcon("/icons/a2a.svg", AutoDevIcons::class.java)
121124
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package cc.unitmesh.devti.a2a
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Request for A2A agent communication
7+
*/
8+
@Serializable
9+
data class A2ARequest(
10+
val agent: String,
11+
val message: String
12+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cc.unitmesh.devti.a2a
2+
3+
import com.intellij.openapi.components.Service
4+
import com.intellij.openapi.project.Project
5+
import io.a2a.spec.AgentCard
6+
7+
/**
8+
* Service for managing A2A agents and their integration with the Sketch system.
9+
*/
10+
@Service(Service.Level.PROJECT)
11+
class A2AService(private val project: Project) {
12+
private var a2aClientConsumer: A2AClientConsumer? = null
13+
private var availableAgents: List<AgentCard> = emptyList()
14+
15+
companion object {
16+
fun getInstance(project: Project): A2AService {
17+
return project.getService(A2AService::class.java)
18+
}
19+
}
20+
21+
/**
22+
* Initialize A2A service with server configurations
23+
*/
24+
fun initialize(servers: List<A2aServer>) {
25+
try {
26+
a2aClientConsumer = A2AClientConsumer()
27+
a2aClientConsumer?.init(servers)
28+
refreshAvailableAgents()
29+
} catch (e: Exception) {
30+
// Log error but don't fail
31+
a2aClientConsumer = null
32+
availableAgents = emptyList()
33+
}
34+
}
35+
36+
/**
37+
* Get all available A2A agents
38+
*/
39+
fun getAvailableAgents(): List<AgentCard> {
40+
return availableAgents
41+
}
42+
43+
/**
44+
* Send message to a specific A2A agent
45+
*/
46+
fun sendMessage(agentName: String, message: String): String? {
47+
return try {
48+
a2aClientConsumer?.sendMessage(agentName, message)
49+
} catch (e: Exception) {
50+
null
51+
}
52+
}
53+
54+
/**
55+
* Check if A2A service is available and initialized
56+
*/
57+
fun isAvailable(): Boolean {
58+
return a2aClientConsumer != null && availableAgents.isNotEmpty()
59+
}
60+
61+
/**
62+
* Refresh the list of available agents
63+
*/
64+
private fun refreshAvailableAgents() {
65+
availableAgents = try {
66+
a2aClientConsumer?.listAgents() ?: emptyList()
67+
} catch (e: Exception) {
68+
emptyList()
69+
}
70+
}
71+
72+
/**
73+
* Get A2A client consumer for direct access
74+
*/
75+
fun getClientConsumer(): A2AClientConsumer? {
76+
return a2aClientConsumer
77+
}
78+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cc.unitmesh.devti.a2a
2+
3+
import cc.unitmesh.devti.agent.tool.AgentTool
4+
import cc.unitmesh.devti.sketch.SketchToolchainProvider
5+
import com.intellij.openapi.project.Project
6+
import io.a2a.spec.AgentCard
7+
8+
/**
9+
* A2A Sketch Toolchain Provider that converts A2A agents to AgentTool format
10+
* for use in the Sketch system.
11+
*/
12+
class A2ASketchToolchainProvider : SketchToolchainProvider {
13+
override fun collect(): List<AgentTool> {
14+
// Since we can't get project from interface, we need to find it another way
15+
// For now, return empty list as this will be called from SketchRunContext directly
16+
return emptyList()
17+
}
18+
19+
companion object {
20+
fun collectA2ATools(project: Project): List<AgentTool> {
21+
return try {
22+
val a2aService = project.getService(A2AService::class.java)
23+
val agentCards = a2aService.getAvailableAgents()
24+
25+
agentCards.map { agentCard ->
26+
convertAgentCardToTool(agentCard)
27+
}
28+
} catch (e: Exception) {
29+
emptyList()
30+
}
31+
}
32+
33+
private fun convertAgentCardToTool(agentCard: AgentCard): AgentTool {
34+
val name = try {
35+
agentCard.name() ?: "unknown_agent"
36+
} catch (e: Exception) {
37+
"unknown_agent"
38+
}
39+
40+
val description = try {
41+
agentCard.description() ?: "A2A Agent"
42+
} catch (e: Exception) {
43+
"A2A Agent"
44+
}
45+
46+
val skills = try {
47+
agentCard.skills()?.joinToString(", ") { skill ->
48+
try {
49+
when {
50+
skill.javaClass.simpleName == "AgentSkill" -> {
51+
val nameMethod = skill.javaClass.getMethod("name")
52+
nameMethod.invoke(skill) as? String ?: "skill"
53+
}
54+
else -> skill.toString()
55+
}
56+
} catch (e: Exception) {
57+
"skill"
58+
}
59+
} ?: ""
60+
} catch (e: Exception) {
61+
""
62+
}
63+
64+
val fullDescription = if (skills.isNotEmpty()) {
65+
"$description. Available skills: $skills"
66+
} else {
67+
description
68+
}
69+
70+
val example = generateExampleUsage(name)
71+
return AgentTool(
72+
name = name,
73+
description = fullDescription,
74+
example = example,
75+
isMcp = false,
76+
completion = "",
77+
mcpGroup = "a2a",
78+
isDevIns = false,
79+
devinScriptPath = ""
80+
)
81+
}
82+
83+
private fun generateExampleUsage(agentName: String): String {
84+
return """
85+
/a2a $agentName "Please help me with my task"
86+
""".trimIndent()
87+
}
88+
}
89+
}

core/src/main/kotlin/cc/unitmesh/devti/command/dataprovider/BuiltinCommand.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ enum class BuiltinCommand(
221221
true,
222222
enableInSketch = false
223223
),
224+
A2A(
225+
"a2a",
226+
"Send message to A2A (Agent-to-Agent) protocol agents. Use for delegating tasks to specialized AI agents that support A2A protocol. Specify agent name and message content. Returns agent response for further processing or analysis.",
227+
AutoDevIcons.A2A,
228+
true,
229+
true,
230+
enableInSketch = true
231+
),
224232
;
225233

226234
companion object {

core/src/main/kotlin/cc/unitmesh/devti/sketch/SketchRunContext.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,23 @@ data class SketchRunContext(
114114

115115

116116
/**
117-
* Get tool list from McpConfigService if available, otherwise fall back to SketchToolchainProvider
117+
* Get tool list from McpConfigService and A2A agents if available, otherwise fall back to SketchToolchainProvider
118118
*/
119119
fun getToolList(project: Project): String {
120120
val mcpConfigService = project.getService(McpConfigService::class.java)
121121
val selectedTools = mcpConfigService.convertToAgentTool()
122122
val defaultTools = SketchToolchainProvider.collect(project)
123123

124-
return if (selectedTools.isNotEmpty()) {
125-
(defaultTools + selectedTools).joinToString("\n") { it.toString() }
124+
// Get A2A tools
125+
val a2aTools = try {
126+
cc.unitmesh.devti.a2a.A2ASketchToolchainProvider.collectA2ATools(project)
127+
} catch (e: Exception) {
128+
emptyList()
129+
}
130+
131+
val allTools = defaultTools + selectedTools + a2aTools
132+
return if (allTools.isNotEmpty()) {
133+
allTools.joinToString("\n") { it.toString() }
126134
} else {
127135
defaultTools.joinToString("\n")
128136
}
Lines changed: 18 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)