Skip to content

Commit 6c5001b

Browse files
committed
fix: Add A2a for flow agents too
1 parent c36eed1 commit 6c5001b

File tree

39 files changed

+501
-20
lines changed

39 files changed

+501
-20
lines changed

frontend/admin-ui/src/views/flows/FlowDialog.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
<FormInput v-model="form.description" placeholder="What this flow does..." />
1212
</div>
1313
</div>
14+
<div class="border border-piedra-700/40 rounded-xl px-4 py-3">
15+
<div class="flex items-center justify-between">
16+
<div>
17+
<span class="text-xs font-medium text-arena-400">A2A Protocol</span>
18+
<p class="text-[10px] text-arena-500 mt-0.5">Expose this flow via the Agent-to-Agent protocol for external discovery and invocation</p>
19+
</div>
20+
<FormToggle v-model="form.a2aEnabled" />
21+
</div>
22+
</div>
1423
<FlowCanvas
1524
v-model="form.root"
1625
:agents="store.agents"
@@ -46,6 +55,7 @@ import { flowsApi } from '../../lib/api/index.js'
4655
import AppDialog from '../../components/AppDialog.vue'
4756
import FormInput from '../../components/FormInput.vue'
4857
import FormLabel from '../../components/FormLabel.vue'
58+
import FormToggle from '../../components/FormToggle.vue'
4959
import FlowCanvas from './FlowCanvas.vue'
5060
5161
const emit = defineEmits(['saved'])
@@ -59,6 +69,7 @@ const form = reactive({
5969
name: '',
6070
description: '',
6171
root: null,
72+
a2aEnabled: false,
6273
})
6374
6475
function open(flow = null) {
@@ -67,6 +78,7 @@ function open(flow = null) {
6778
form.name = flow?.name || ''
6879
form.description = flow?.description || ''
6980
form.root = flow ? JSON.parse(JSON.stringify(flow.root)) : null
81+
form.a2aEnabled = flow?.a2a?.enabled || false
7082
dialogRef.value?.open()
7183
}
7284
@@ -75,6 +87,7 @@ async function save() {
7587
name: form.name.trim(),
7688
description: form.description.trim(),
7789
root: cleanStep(form.root),
90+
a2a: form.a2aEnabled ? { enabled: true } : undefined,
7891
}
7992
try {
8093
if (isEdit.value) {

server/a2a/handler.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,38 @@ func NewHandler(publicURL string) *Handler {
3737
}
3838
}
3939

40-
func (h *Handler) Rebuild(agents []store.AgentDefinition, adkAgents map[string]agent.Agent, sessionSvc session.Service, memorySvc memory.Service) {
40+
func (h *Handler) Rebuild(agents []store.AgentDefinition, flows []store.FlowDefinition, adkAgents map[string]agent.Agent, sessionSvc session.Service, memorySvc memory.Service) {
4141
handlers := make(map[string]http.Handler)
4242
cards := make(map[string]*a2a.AgentCard)
4343

44-
for _, agentDef := range agents {
45-
if agentDef.A2A == nil || !agentDef.A2A.Enabled {
44+
type a2aEntry struct {
45+
id, name, description string
46+
a2aCfg *store.A2AConfig
47+
}
48+
49+
var entries []a2aEntry
50+
for _, ag := range agents {
51+
entries = append(entries, a2aEntry{ag.ID, ag.Name, ag.Description, ag.A2A})
52+
}
53+
for _, fl := range flows {
54+
entries = append(entries, a2aEntry{fl.ID, fl.Name, fl.Description, fl.A2A})
55+
}
56+
57+
for _, entry := range entries {
58+
if entry.a2aCfg == nil || !entry.a2aCfg.Enabled {
4659
continue
4760
}
48-
adkAgent, ok := adkAgents[agentDef.ID]
61+
adkAgent, ok := adkAgents[entry.id]
4962
if !ok {
50-
slog.Warn("A2A: agent not found in ADK map", "agent", agentDef.ID)
63+
slog.Warn("A2A: agent not found in ADK map", "agent", entry.id)
5164
continue
5265
}
5366

54-
invokeURL := fmt.Sprintf("%s/api/v1/a2a/%s", h.publicURL, agentDef.ID)
67+
invokeURL := fmt.Sprintf("%s/api/v1/a2a/%s", h.publicURL, entry.id)
5568

5669
card := &a2a.AgentCard{
57-
Name: agentDef.Name,
58-
Description: agentDef.Description,
70+
Name: entry.name,
71+
Description: entry.description,
5972
URL: invokeURL,
6073
Version: "1.0.0",
6174
ProtocolVersion: protocolVersion,
@@ -76,21 +89,21 @@ func (h *Handler) Rebuild(agents []store.AgentDefinition, adkAgents map[string]a
7689
{"bearer": a2a.SecuritySchemeScopes{}},
7790
},
7891
}
79-
cards[agentDef.ID] = card
92+
cards[entry.id] = card
8093

8194
execCfg := adka2a.ExecutorConfig{
8295
RunnerConfig: runner.Config{
83-
AppName: agentDef.ID,
96+
AppName: entry.id,
8497
Agent: adkAgent,
8598
SessionService: sessionSvc,
8699
MemoryService: memorySvc,
87100
},
88101
}
89102
executor := adka2a.NewExecutor(execCfg)
90103
reqHandler := a2asrv.NewHandler(executor, a2asrv.WithLogger(slog.Default()))
91-
handlers[agentDef.ID] = a2asrv.NewJSONRPCHandler(reqHandler)
104+
handlers[entry.id] = a2asrv.NewJSONRPCHandler(reqHandler)
92105

93-
slog.Info("A2A endpoint enabled", "agent", agentDef.ID, "name", agentDef.Name)
106+
slog.Info("A2A endpoint enabled", "agent", entry.id, "name", entry.name)
94107
}
95108

96109
h.mu.Lock()

server/agent/agent.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ func New(ctx context.Context, agents []store.AgentDefinition, backends []store.B
185185
continue
186186
}
187187
otherAgents = append(otherAgents, flowAgent)
188+
adkAgentMap[flow.ID] = flowAgent
188189
slog.Info("Flow initialized", "id", flow.ID, "name", flow.Name)
189190
}
190191

server/api/admin/docs/docs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3087,6 +3087,9 @@ const docTemplate = `{
30873087
"store.FlowDefinition": {
30883088
"type": "object",
30893089
"properties": {
3090+
"a2a": {
3091+
"$ref": "#/definitions/store.A2AConfig"
3092+
},
30903093
"description": {
30913094
"type": "string"
30923095
},

server/api/admin/docs/swagger.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3084,6 +3084,9 @@
30843084
"store.FlowDefinition": {
30853085
"type": "object",
30863086
"properties": {
3087+
"a2a": {
3088+
"$ref": "#/definitions/store.A2AConfig"
3089+
},
30873090
"description": {
30883091
"type": "string"
30893092
},

server/api/admin/docs/swagger.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ definitions:
288288
type: object
289289
store.FlowDefinition:
290290
properties:
291+
a2a:
292+
$ref: '#/definitions/store.A2AConfig'
291293
description:
292294
type: string
293295
id:

server/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ func (h *agentRouterHandler) rebuild(ctx context.Context, dataStore *store.Store
581581
h.adminHandler.SetSessionService(svc.SessionService())
582582
}
583583
if h.a2aHandler != nil {
584-
h.a2aHandler.Rebuild(storeData.Agents, svc.ADKAgents(), svc.SessionService(), svc.MemoryService())
584+
h.a2aHandler.Rebuild(storeData.Agents, storeData.Flows, svc.ADKAgents(), svc.SessionService(), svc.MemoryService())
585585
}
586586
}
587587
} else {

server/store/types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,11 @@ func collectAgentIDs(step *FlowStep, seen map[string]bool, ids *[]string) {
257257
// FlowDefinition represents a multi-agent workflow stored as a recursive tree
258258
// of steps that maps directly to ADK workflow agents.
259259
type FlowDefinition struct {
260-
ID string `json:"id" yaml:"id"`
261-
Name string `json:"name" yaml:"name"`
262-
Description string `json:"description,omitempty" yaml:"description,omitempty"`
263-
Root FlowStep `json:"root" yaml:"root"`
260+
ID string `json:"id" yaml:"id"`
261+
Name string `json:"name" yaml:"name"`
262+
Description string `json:"description,omitempty" yaml:"description,omitempty"`
263+
Root FlowStep `json:"root" yaml:"root"`
264+
A2A *A2AConfig `json:"a2a,omitempty" yaml:"a2a,omitempty"`
264265
}
265266

266267
// Settings holds global configuration that applies to the launcher/runtime

website/content/docs/a2a.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: "A2A Protocol"
3+
---
4+
5+
A2A (Agent-to-Agent) lets your Magec agents talk to external AI systems — and lets external systems talk to yours. When you enable A2A on an agent, it becomes discoverable and callable by any application that speaks the [A2A protocol](https://a2a-protocol.org/latest/specification/).
6+
7+
In practice: you flip a toggle, and your agent gets a URL that other tools can connect to. They discover what the agent can do, authenticate, and send it messages — all without you writing any code.
8+
9+
## Why would I use this?
10+
11+
Think of A2A as giving your agent a phone number that other AI systems can call.
12+
13+
- **Connect agents across platforms.** An agent running in another tool (Claude Desktop, a custom app, another Magec instance) can discover and invoke your agent directly.
14+
- **Build multi-agent systems.** A coordinator agent somewhere else can delegate tasks to your specialized agents — your "database expert" handles queries, your "email writer" drafts responses, each on their own Magec instance.
15+
- **Expose agents as services.** Instead of building a custom API for each agent, A2A gives you a standard protocol that any compatible client already knows how to use.
16+
17+
If you're running Magec for personal use or within a single app, you probably don't need A2A. It becomes useful when you want agents to collaborate across different systems.
18+
19+
## How it works
20+
21+
When you enable A2A on an agent, Magec does three things:
22+
23+
1. **Publishes an agent card** — a JSON document describing the agent's name, capabilities, skills, and how to authenticate. This is the discovery mechanism.
24+
2. **Exposes a JSON-RPC endpoint** — the URL where clients send messages to the agent. Supports both synchronous and streaming responses.
25+
3. **Auto-generates skills from the agent's tools** — MCP servers, built-in tools, and the agent's instructions are all reflected in the card so clients know what the agent can do.
26+
27+
You don't configure any of this manually. The card and skills are derived from the agent's existing setup.
28+
29+
## Enabling A2A
30+
31+
1. Open an agent in the Admin UI
32+
2. Toggle **A2A Protocol** on
33+
3. Save
34+
35+
That's it. The agent is now discoverable and invocable via A2A.
36+
37+
## Endpoints
38+
39+
| Endpoint | Auth | Description |
40+
|----------|------|-------------|
41+
| `/.well-known/agent-card.json` | No | Lists all A2A-enabled agents |
42+
| `/api/v1/a2a/{agentId}/.well-known/agent-card.json` | No | Agent card for a specific agent |
43+
| `/api/v1/a2a/{agentId}` | Bearer token | JSON-RPC invocation endpoint |
44+
45+
Discovery endpoints are public so that external clients can find your agents. The invocation endpoint requires a **Bearer token** — this is a regular Magec client token, the same ones you create in the Admin UI under Clients.
46+
47+
## Connecting a client
48+
49+
Give the external A2A client this URL:
50+
51+
```
52+
https://your-server/api/v1/a2a/{agentId}
53+
```
54+
55+
The client will automatically fetch the agent card from the `.well-known` path relative to that URL, read the security requirements, and use the Bearer token you provide to send messages.
56+
57+
## Public URL
58+
59+
By default, agent cards reference `http://localhost:{port}` as the agent's URL. This works for local development but not when your server is behind a reverse proxy or exposed to the internet.
60+
61+
Set `publicURL` in your config so that agent cards contain the correct address:
62+
63+
```yaml
64+
server:
65+
publicURL: https://magec.example.com
66+
```
67+
68+
{{< callout type="info" >}}
69+
If you don't set `publicURL`, everything still works locally. You only need it when external systems need to reach your server from outside.
70+
{{< /callout >}}
71+
72+
## What's in an agent card?
73+
74+
The agent card is what clients read to understand your agent. Here's what it contains:
75+
76+
| Field | Description |
77+
|-------|-------------|
78+
| `name` | Agent's display name |
79+
| `description` | What the agent does |
80+
| `url` | JSON-RPC endpoint for invocation |
81+
| `skills` | Auto-generated list of capabilities (from system prompt + tools) |
82+
| `securitySchemes` | How to authenticate (Bearer token) |
83+
| `capabilities` | Protocol features supported (streaming) |
84+
85+
Skills are generated automatically from the agent's configuration. Each MCP tool the agent has access to appears as a separate skill. The agent's system prompt is reflected in the primary skill description, so external clients can understand what the agent is good at without any manual setup.
86+
87+
## Hot-reload
88+
89+
Like everything in Magec, A2A configuration reloads automatically. Enable or disable A2A on an agent, save, and the change is live — no restart needed.

website/hugo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,21 @@ theme = 'magec'
112112
parent = 'core'
113113
url = '/docs/skills/'
114114
weight = 6
115+
[[menu.docs]]
116+
name = 'A2A Protocol'
117+
parent = 'core'
118+
url = '/docs/a2a/'
119+
weight = 7
115120
[[menu.docs]]
116121
name = 'Secrets'
117122
parent = 'core'
118123
url = '/docs/secrets/'
119-
weight = 7
124+
weight = 8
120125
[[menu.docs]]
121126
name = 'Commands'
122127
parent = 'core'
123128
url = '/docs/commands/'
124-
weight = 8
129+
weight = 9
125130

126131
[[menu.docs]]
127132
identifier = 'clients'

0 commit comments

Comments
 (0)