Skip to content

Commit e03ee3a

Browse files
authored
feat(controller): set appProtocol on agent Service when a2aConfig is set (#1845)
Closes #1808 When an agent has `a2aConfig` set, the operator-managed Service port now gets `appProtocol: kgateway.dev/a2a`. This lets agentgateway detect and handle A2A traffic automatically, without the user having to create a separate wrapper Service per agent. Without this change, the agent card URL rewriting that agentgateway does for A2A does not work, because the proxy sees the traffic as plain HTTP. The only workaround today is a manually maintained sibling Service for each A2A agent. Agents without `a2aConfig` are not affected. Signed-off-by: mesutoezdil <mesudozdil@gmail.com>
1 parent 625c500 commit e03ee3a

3 files changed

Lines changed: 343 additions & 6 deletions

File tree

go/core/internal/controller/translator/agent/manifest_builder.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,16 @@ func (a *adkApiTranslator) buildWorkloadObjects(
516516
return sbObjs, nil
517517
}
518518

519+
svcPort := corev1.ServicePort{
520+
Name: "http",
521+
Port: manifestCtx.deployment.Port,
522+
TargetPort: intstr.FromInt(int(manifestCtx.deployment.Port)),
523+
}
524+
if s := manifestCtx.agent.GetAgentSpec(); s != nil && s.Declarative != nil && s.Declarative.A2AConfig != nil {
525+
proto := "kgateway.dev/a2a"
526+
svcPort.AppProtocol = &proto
527+
}
528+
519529
return []client.Object{
520530
&appsv1.Deployment{
521531
TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"},
@@ -538,12 +548,8 @@ func (a *adkApiTranslator) buildWorkloadObjects(
538548
ObjectMeta: manifestCtx.objectMeta(),
539549
Spec: corev1.ServiceSpec{
540550
Selector: manifestCtx.selectorLabels,
541-
Ports: []corev1.ServicePort{{
542-
Name: "http",
543-
Port: manifestCtx.deployment.Port,
544-
TargetPort: intstr.FromInt(int(manifestCtx.deployment.Port)),
545-
}},
546-
Type: corev1.ServiceTypeClusterIP,
551+
Ports: []corev1.ServicePort{svcPort},
552+
Type: corev1.ServiceTypeClusterIP,
547553
},
548554
},
549555
}, nil
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
operation: translateAgent
2+
targetObject: a2a-agent
3+
namespace: test
4+
objects:
5+
- apiVersion: v1
6+
kind: Secret
7+
metadata:
8+
name: openai-secret
9+
namespace: test
10+
data:
11+
api-key: c2stdGVzdC1hcGkta2V5
12+
- apiVersion: kagent.dev/v1alpha2
13+
kind: ModelConfig
14+
metadata:
15+
name: default-model
16+
namespace: test
17+
spec:
18+
provider: OpenAI
19+
model: gpt-4o
20+
apiKeySecret: openai-secret
21+
apiKeySecretKey: api-key
22+
- apiVersion: kagent.dev/v1alpha2
23+
kind: Agent
24+
metadata:
25+
name: a2a-agent
26+
namespace: test
27+
spec:
28+
type: Declarative
29+
declarative:
30+
description: An agent with A2A enabled
31+
systemMessage: You are a helpful assistant.
32+
modelConfig: default-model
33+
tools: []
34+
a2aConfig:
35+
skills:
36+
- id: summarize
37+
name: Summarize
38+
description: Summarizes text
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
{
2+
"agentCard": {
3+
"capabilities": {
4+
"pushNotifications": false,
5+
"stateTransitionHistory": true,
6+
"streaming": true
7+
},
8+
"defaultInputModes": [
9+
"text"
10+
],
11+
"defaultOutputModes": [
12+
"text"
13+
],
14+
"description": "",
15+
"name": "a2a_agent",
16+
"preferredTransport": "JSONRPC",
17+
"skills": [
18+
{
19+
"description": "Summarizes text",
20+
"name": "Summarize",
21+
"tags": null
22+
}
23+
],
24+
"url": "http://a2a-agent.test:8080",
25+
"version": ""
26+
},
27+
"config": {
28+
"description": "",
29+
"instruction": "You are a helpful assistant.",
30+
"model": {
31+
"base_url": "",
32+
"model": "gpt-4o",
33+
"type": "openai"
34+
},
35+
"stream": false
36+
},
37+
"manifest": [
38+
{
39+
"apiVersion": "v1",
40+
"kind": "Secret",
41+
"metadata": {
42+
"labels": {
43+
"app": "kagent",
44+
"app.kubernetes.io/managed-by": "kagent",
45+
"app.kubernetes.io/name": "a2a-agent",
46+
"app.kubernetes.io/part-of": "kagent",
47+
"kagent": "a2a-agent"
48+
},
49+
"name": "a2a-agent",
50+
"namespace": "test",
51+
"ownerReferences": [
52+
{
53+
"apiVersion": "kagent.dev/v1alpha2",
54+
"blockOwnerDeletion": true,
55+
"controller": true,
56+
"kind": "Agent",
57+
"name": "a2a-agent",
58+
"uid": ""
59+
}
60+
]
61+
},
62+
"stringData": {
63+
"agent-card.json": "{\"name\":\"a2a_agent\",\"description\":\"\",\"url\":\"http://a2a-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"summarize\",\"name\":\"Summarize\",\"description\":\"Summarizes text\",\"tags\":null}],\"preferredTransport\":\"JSONRPC\"}",
64+
"config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}"
65+
}
66+
},
67+
{
68+
"apiVersion": "v1",
69+
"kind": "ServiceAccount",
70+
"metadata": {
71+
"labels": {
72+
"app": "kagent",
73+
"app.kubernetes.io/managed-by": "kagent",
74+
"app.kubernetes.io/name": "a2a-agent",
75+
"app.kubernetes.io/part-of": "kagent",
76+
"kagent": "a2a-agent"
77+
},
78+
"name": "a2a-agent",
79+
"namespace": "test",
80+
"ownerReferences": [
81+
{
82+
"apiVersion": "kagent.dev/v1alpha2",
83+
"blockOwnerDeletion": true,
84+
"controller": true,
85+
"kind": "Agent",
86+
"name": "a2a-agent",
87+
"uid": ""
88+
}
89+
]
90+
}
91+
},
92+
{
93+
"apiVersion": "apps/v1",
94+
"kind": "Deployment",
95+
"metadata": {
96+
"labels": {
97+
"app": "kagent",
98+
"app.kubernetes.io/managed-by": "kagent",
99+
"app.kubernetes.io/name": "a2a-agent",
100+
"app.kubernetes.io/part-of": "kagent",
101+
"kagent": "a2a-agent"
102+
},
103+
"name": "a2a-agent",
104+
"namespace": "test",
105+
"ownerReferences": [
106+
{
107+
"apiVersion": "kagent.dev/v1alpha2",
108+
"blockOwnerDeletion": true,
109+
"controller": true,
110+
"kind": "Agent",
111+
"name": "a2a-agent",
112+
"uid": ""
113+
}
114+
]
115+
},
116+
"spec": {
117+
"selector": {
118+
"matchLabels": {
119+
"app": "kagent",
120+
"kagent": "a2a-agent"
121+
}
122+
},
123+
"strategy": {
124+
"rollingUpdate": {
125+
"maxSurge": 1,
126+
"maxUnavailable": 0
127+
},
128+
"type": "RollingUpdate"
129+
},
130+
"template": {
131+
"metadata": {
132+
"annotations": {
133+
"kagent.dev/config-hash": "16405455094195710426"
134+
},
135+
"labels": {
136+
"app": "kagent",
137+
"app.kubernetes.io/managed-by": "kagent",
138+
"app.kubernetes.io/name": "a2a-agent",
139+
"app.kubernetes.io/part-of": "kagent",
140+
"kagent": "a2a-agent"
141+
}
142+
},
143+
"spec": {
144+
"containers": [
145+
{
146+
"args": [
147+
"--host",
148+
"0.0.0.0",
149+
"--port",
150+
"8080",
151+
"--filepath",
152+
"/config"
153+
],
154+
"env": [
155+
{
156+
"name": "OPENAI_API_KEY",
157+
"valueFrom": {
158+
"secretKeyRef": {
159+
"key": "api-key",
160+
"name": "openai-secret"
161+
}
162+
}
163+
},
164+
{
165+
"name": "KAGENT_NAMESPACE",
166+
"valueFrom": {
167+
"fieldRef": {
168+
"fieldPath": "metadata.namespace"
169+
}
170+
}
171+
},
172+
{
173+
"name": "KAGENT_NAME",
174+
"value": "a2a-agent"
175+
},
176+
{
177+
"name": "KAGENT_URL",
178+
"value": "http://kagent-controller.kagent:8083"
179+
}
180+
],
181+
"image": "cr.kagent.dev/kagent-dev/kagent/app:dev",
182+
"imagePullPolicy": "IfNotPresent",
183+
"name": "kagent",
184+
"ports": [
185+
{
186+
"containerPort": 8080,
187+
"name": "http"
188+
}
189+
],
190+
"readinessProbe": {
191+
"httpGet": {
192+
"path": "/.well-known/agent-card.json",
193+
"port": "http"
194+
},
195+
"initialDelaySeconds": 15,
196+
"periodSeconds": 15,
197+
"timeoutSeconds": 15
198+
},
199+
"resources": {
200+
"limits": {
201+
"cpu": "2",
202+
"memory": "1Gi"
203+
},
204+
"requests": {
205+
"cpu": "100m",
206+
"memory": "384Mi"
207+
}
208+
},
209+
"volumeMounts": [
210+
{
211+
"mountPath": "/config",
212+
"name": "config"
213+
},
214+
{
215+
"mountPath": "/var/run/secrets/tokens",
216+
"name": "kagent-token"
217+
}
218+
]
219+
}
220+
],
221+
"serviceAccountName": "a2a-agent",
222+
"volumes": [
223+
{
224+
"name": "config",
225+
"secret": {
226+
"secretName": "a2a-agent"
227+
}
228+
},
229+
{
230+
"name": "kagent-token",
231+
"projected": {
232+
"sources": [
233+
{
234+
"serviceAccountToken": {
235+
"audience": "kagent",
236+
"expirationSeconds": 3600,
237+
"path": "kagent-token"
238+
}
239+
}
240+
]
241+
}
242+
}
243+
]
244+
}
245+
}
246+
},
247+
"status": {}
248+
},
249+
{
250+
"apiVersion": "v1",
251+
"kind": "Service",
252+
"metadata": {
253+
"labels": {
254+
"app": "kagent",
255+
"app.kubernetes.io/managed-by": "kagent",
256+
"app.kubernetes.io/name": "a2a-agent",
257+
"app.kubernetes.io/part-of": "kagent",
258+
"kagent": "a2a-agent"
259+
},
260+
"name": "a2a-agent",
261+
"namespace": "test",
262+
"ownerReferences": [
263+
{
264+
"apiVersion": "kagent.dev/v1alpha2",
265+
"blockOwnerDeletion": true,
266+
"controller": true,
267+
"kind": "Agent",
268+
"name": "a2a-agent",
269+
"uid": ""
270+
}
271+
]
272+
},
273+
"spec": {
274+
"ports": [
275+
{
276+
"appProtocol": "kgateway.dev/a2a",
277+
"name": "http",
278+
"port": 8080,
279+
"targetPort": 8080
280+
}
281+
],
282+
"selector": {
283+
"app": "kagent",
284+
"kagent": "a2a-agent"
285+
},
286+
"type": "ClusterIP"
287+
},
288+
"status": {
289+
"loadBalancer": {}
290+
}
291+
}
292+
]
293+
}

0 commit comments

Comments
 (0)