Skip to content

Tutorial: A2A Server

Expose a PromptKit conversation as an A2A-compliant agent server.

Time: 15 minutes Level: Intermediate

An A2A server that exposes a PromptKit SDK conversation as a remotely-callable agent, discoverable via its agent card.

  • Create an A2A server with sdk.NewA2AServer
  • Use sdk.A2AOpener to bridge SDK conversations to the server
  • Configure agent cards
  • Start and gracefully shut down the server

The agent card describes your agent’s identity and capabilities:

package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/AltairaLabs/PromptKit/runtime/a2a"
"github.com/AltairaLabs/PromptKit/sdk"
)
func main() {
card := a2a.AgentCard{
Name: "Assistant Agent",
Description: "A helpful assistant powered by PromptKit",
DefaultInputModes: []string{"text/plain"},
DefaultOutputModes: []string{"text/plain"},
Capabilities: a2a.AgentCapabilities{Streaming: true},
Skills: []a2a.AgentSkill{
{
ID: "chat",
Name: "Chat",
Description: "General-purpose chat assistant",
},
},
}

Use sdk.A2AOpener to create conversations from a pack file:

opener := sdk.A2AOpener("./assistant.pack.json", "chat")

Each A2A request creates a new SDK conversation via this opener. The contextID from the A2A protocol groups related requests.


server := sdk.NewA2AServer(opener,
sdk.WithA2ACard(&card),
sdk.WithA2APort(9999),
)
// Handle graceful shutdown.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("\nShutting down...")
server.Shutdown(context.Background())
os.Exit(0)
}()
fmt.Println("A2A server listening on http://localhost:9999")
fmt.Println("Agent card: http://localhost:9999/.well-known/agent.json")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

Run the server:

Terminal window
go run main.go

Verify the agent card:

Terminal window
curl http://localhost:9999/.well-known/agent.json | jq .

In a separate terminal, use the A2A client to send a message:

client := a2a.NewClient("http://localhost:9999")
card, _ := client.Discover(ctx)
fmt.Printf("Agent: %s\n", card.Name)
text := "Hello!"
task, _ := client.SendMessage(ctx, &a2a.SendMessageRequest{
Message: a2a.Message{
Role: a2a.RoleUser,
Parts: []a2a.Part{{Text: &text}},
},
Configuration: &a2a.SendMessageConfiguration{Blocking: true},
})
for _, artifact := range task.Artifacts {
for _, part := range artifact.Parts {
if part.Text != nil {
fmt.Println(*part.Text)
}
}
}

See the A2A Client Tutorial for a complete walkthrough.


By default, the server uses an in-memory task store. For persistence, implement the sdk.A2ATaskStore interface:

server := sdk.NewA2AServer(opener,
sdk.WithA2ACard(&card),
sdk.WithA2APort(9999),
sdk.WithA2ATaskStore(myCustomStore),
)

See the SDK A2A Reference for the A2ATaskStore interface.


If you already have an HTTP server, use Handler() to get the http.Handler:

server := sdk.NewA2AServer(opener, sdk.WithA2ACard(&card))
mux := http.NewServeMux()
mux.Handle("/", server.Handler())
http.ListenAndServe(":8080", mux)

OptionDescription
sdk.WithA2ACard(card)Sets the agent card served at /.well-known/agent.json
sdk.WithA2APort(port)Sets the TCP port for ListenAndServe
sdk.WithA2ATaskStore(store)Sets a custom task store (default: in-memory)

  • How to configure an agent card
  • How to use sdk.A2AOpener to bridge SDK conversations
  • How to create, start, and gracefully shut down an A2A server
  • How to embed the server in an existing HTTP server