Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,6 @@ public fun AIAgentSubgraphBuilderBase<*, *>.nodeA2AMessageStorageReplace(
}
}

/**
* Creates a node that loads all messages from storage for the current context.
*
* This is an alias for [nodeA2AMessageStorageLoad].
*
* @param name Optional node name for debugging and tracing
* @return A node that returns the list of all stored messages
* @see ai.koog.a2a.server.messages.MessageStorage.getByContext
*/
@AIAgentBuilderDslMarker
public fun AIAgentSubgraphBuilderBase<*, *>.nodeA2AMessagesContextLoad(
name: String? = null,
): AIAgentNodeDelegate<Unit, List<A2AMessage>> =
node(name) {
withA2AAgentServer {
context.messageStorage.getAll()
}
}

/**
* Parameters for retrieving a single task from storage.
*
Expand Down
9 changes: 9 additions & 0 deletions examples/simple-examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ Welcome to the **Koog Framework Simple Examples** collection! This project showc
| **Bedrock Agent** | AI agents using AWS Bedrock integration | `runExampleBedrockAgent` | [📓 BedrockAgent.ipynb](../notebooks/BedrockAgent.ipynb) |
| **Web Search** | Agent with web search capabilities | `runExampleWebSearchAgent` | - |

### Agent-to-Agent (A2A)

Examples demonstrating the A2A protocol for inter-agent communication. See the [A2A README](src/main/kotlin/ai/koog/agents/example/a2a/README.md) for details.

| Example | Description | Files |
|------------------------|------------------------------------------------------------|---------------------------------------|
| **Simple Joke Agent** | Basic A2A agent with message-based joke generation | `simplejoke/` (Server + Client) |
| **Advanced Joke Agent** | Task-based agent with clarification flow and artifacts | `advancedjoke/` (Server + Client) |

### Advanced Patterns

| Feature | Description | Gradle Task | Notebook |
Expand Down
6 changes: 5 additions & 1 deletion examples/simple-examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ registerRunExampleTask("runExampleStreamingWithTools", "ai.koog.agents.example.s
A2A examples
*/

// joke generation
// Simple joke generation
registerRunExampleTask("runExampleSimpleJokeAgentServer", "ai.koog.agents.example.a2a.simplejoke.ServerKt")
registerRunExampleTask("runExampleSimpleJokeAgentClient", "ai.koog.agents.example.a2a.simplejoke.ClientKt")

// Advanced joke generation
registerRunExampleTask("runExampleAdvancedJokeAgentServer", "ai.koog.agents.example.a2a.advancedjoke.ServerKt")
registerRunExampleTask("runExampleAdvancedJokeAgentClient", "ai.koog.agents.example.a2a.advancedjoke.ClientKt")
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Agent-to-Agent (A2A) Examples

Examples demonstrating the A2A protocol for inter-agent communication with standardized message/task workflows, streaming responses, and artifact delivery.

## Examples

### Simple Joke Agent (`simplejoke/`)

Basic message-based communication without tasks.

**Run:**
```bash
# Terminal 1: Start server (port 9998)
./gradlew runExampleSimpleJokeServer

# Terminal 2: Run client
./gradlew runExampleSimpleJokeClient
```

### Advanced Joke Agent (`advancedjoke/`)

Task-based workflow with:
- Interactive clarification (InputRequired state)
- Artifact delivery for results
- Graph-based agent strategy with documented nodes/edges
- Streaming response events

**Run:**
```bash
# Terminal 1: Start server (port 9999)
./gradlew runExampleAdvancedJokeServer

# Terminal 2: Run client
./gradlew runExampleAdvancedJokeClient
```

## Key Patterns

**Simple Agent:** `sendMessage()` → single response
**Advanced Agent:** `sendMessageStreaming()` → Flow of events (Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)

**Task States:** Submitted → Working → InputRequired (optional) → Completed

See code comments in `JokeWriterAgentExecutor.kt` for detailed flow documentation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
@file:OptIn(ExperimentalUuidApi::class)

package ai.koog.agents.example.a2a.advancedjoke

import ai.koog.a2a.client.A2AClient
import ai.koog.a2a.client.UrlAgentCardResolver
import ai.koog.a2a.model.Artifact
import ai.koog.a2a.model.Message
import ai.koog.a2a.model.MessageSendParams
import ai.koog.a2a.model.Role
import ai.koog.a2a.model.Task
import ai.koog.a2a.model.TaskArtifactUpdateEvent
import ai.koog.a2a.model.TaskState
import ai.koog.a2a.model.TaskStatusUpdateEvent
import ai.koog.a2a.model.TextPart
import ai.koog.a2a.transport.Request
import ai.koog.a2a.transport.client.jsonrpc.http.HttpJSONRPCClientTransport
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

private const val CYAN = "\u001B[36m"
private const val YELLOW = "\u001B[33m"
private const val MAGENTA = "\u001B[35m"
private const val GREEN = "\u001B[32m"
private const val RED = "\u001B[31m"
private const val BLUE = "\u001B[34m"
private const val RESET = "\u001B[0m"

private val json = Json { prettyPrint = true }

@OptIn(ExperimentalUuidApi::class)
suspend fun main() {
println("\n${YELLOW}Starting Advanced Joke Generator A2A Client$RESET\n")

val transport = HttpJSONRPCClientTransport(url = "http://localhost:9999$ADVANCED_JOKE_AGENT_PATH")
val agentCardResolver = UrlAgentCardResolver(baseUrl = "http://localhost:9999", path = ADVANCED_JOKE_AGENT_CARD_PATH)
val client = A2AClient(transport = transport, agentCardResolver = agentCardResolver)

client.connect()
val agentCard = client.cachedAgentCard()
println("${YELLOW}Connected: ${agentCard.name}$RESET\n")

if (agentCard.capabilities.streaming != true) {
println("${RED}Error: Streaming not supported$RESET")
transport.close()
return
}

println("${CYAN}Context ID:$RESET")
val contextId = readln()
println()

var currentTaskId: String? = null
val artifacts = mutableMapOf<String, Artifact>()

while (true) {
println("${CYAN}Request (/q to quit):$RESET")
val request = readln()
println()

if (request == "/q") break

val message = Message(
messageId = Uuid.random().toString(),
role = Role.User,
parts = listOf(TextPart(request)),
contextId = contextId,
taskId = currentTaskId
)

try {
client.sendMessageStreaming(Request(MessageSendParams(message = message))).collect { response ->
val event = response.data
println("${BLUE}[${event.kind}]$RESET")
println("${json.encodeToString(event)}\n")

when (event) {
is Task -> {
currentTaskId = event.id
event.artifacts?.forEach { artifacts[it.artifactId] = it }
}

is Message -> {
val textContent = event.parts.filterIsInstance<TextPart>().joinToString("\n") { it.text }
if (textContent.isNotBlank()) {
println("${MAGENTA}Message:$RESET\n$textContent\n")
}
}

is TaskStatusUpdateEvent -> {
when (event.status.state) {
TaskState.InputRequired -> {
val question = event.status.message?.parts
?.filterIsInstance<TextPart>()
?.joinToString("\n") { it.text }
if (!question.isNullOrBlank()) {
println("${MAGENTA}Question:$RESET\n$question\n")
}
}

TaskState.Completed -> {
if (artifacts.isNotEmpty()) {
println("${GREEN}=== Artifacts ===$RESET")
artifacts.values.forEach { artifact ->
val content = artifact.parts.filterIsInstance<TextPart>()
.joinToString("\n") { it.text }
if (content.isNotBlank()) {
println("${GREEN}[${artifact.artifactId}]$RESET\n$content\n")
}
}
}
if (event.final) {
currentTaskId = null
artifacts.clear()
}
}

TaskState.Failed, TaskState.Canceled, TaskState.Rejected -> {
if (event.final) {
currentTaskId = null
artifacts.clear()
}
}

else -> {}
}
}

is TaskArtifactUpdateEvent -> {
if (event.append == true) {
val existing = artifacts[event.artifact.artifactId]
if (existing != null) {
artifacts[event.artifact.artifactId] = existing.copy(
parts = existing.parts + event.artifact.parts
)
} else {
artifacts[event.artifact.artifactId] = event.artifact
}
} else {
artifacts[event.artifact.artifactId] = event.artifact
}
}
}
}
} catch (e: Exception) {
println("${RED}Error: ${e.message}$RESET\n")
}
}

println("${YELLOW}Done$RESET")
transport.close()
}
Loading
Loading