Skip to content

Commit a5a554a

Browse files
committed
feat(a2a): a2a added
1 parent fdac431 commit a5a554a

File tree

21 files changed

+3338
-2
lines changed

21 files changed

+3338
-2
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/**
2+
* A2A Agent Card Endpoint
3+
*
4+
* Returns the Agent Card (discovery document) for an A2A agent.
5+
* Also supports CRUD operations for managing agents.
6+
*/
7+
8+
import { db } from '@sim/db'
9+
import { a2aAgent, workflow } from '@sim/db/schema'
10+
import { createLogger } from '@sim/logger'
11+
import { eq } from 'drizzle-orm'
12+
import { type NextRequest, NextResponse } from 'next/server'
13+
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
14+
import type { AgentAuthentication, AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
15+
import { checkHybridAuth } from '@/lib/auth/hybrid'
16+
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
17+
18+
const logger = createLogger('A2AAgentCardAPI')
19+
20+
export const dynamic = 'force-dynamic'
21+
22+
interface RouteParams {
23+
agentId: string
24+
}
25+
26+
/**
27+
* GET - Returns the Agent Card for discovery
28+
*/
29+
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
30+
const { agentId } = await params
31+
32+
try {
33+
const [agent] = await db
34+
.select({
35+
agent: a2aAgent,
36+
workflow: workflow,
37+
})
38+
.from(a2aAgent)
39+
.innerJoin(workflow, eq(a2aAgent.workflowId, workflow.id))
40+
.where(eq(a2aAgent.id, agentId))
41+
.limit(1)
42+
43+
if (!agent) {
44+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
45+
}
46+
47+
if (!agent.agent.isPublished) {
48+
// Check if requester has access (for preview)
49+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
50+
if (!auth.success) {
51+
return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
52+
}
53+
}
54+
55+
const agentCard = generateAgentCard(
56+
{
57+
id: agent.agent.id,
58+
name: agent.agent.name,
59+
description: agent.agent.description,
60+
version: agent.agent.version,
61+
capabilities: agent.agent.capabilities as AgentCapabilities,
62+
skills: agent.agent.skills as AgentSkill[],
63+
authentication: agent.agent.authentication as AgentAuthentication,
64+
},
65+
{
66+
id: agent.workflow.id,
67+
name: agent.workflow.name,
68+
description: agent.workflow.description,
69+
}
70+
)
71+
72+
return NextResponse.json(agentCard, {
73+
headers: {
74+
'Content-Type': 'application/json',
75+
'Cache-Control': agent.agent.isPublished ? 'public, max-age=3600' : 'private, no-cache',
76+
},
77+
})
78+
} catch (error) {
79+
logger.error('Error getting Agent Card:', error)
80+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
81+
}
82+
}
83+
84+
/**
85+
* PUT - Update an agent
86+
*/
87+
export async function PUT(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
88+
const { agentId } = await params
89+
90+
try {
91+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
92+
if (!auth.success || !auth.userId) {
93+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
94+
}
95+
96+
const [existingAgent] = await db
97+
.select()
98+
.from(a2aAgent)
99+
.where(eq(a2aAgent.id, agentId))
100+
.limit(1)
101+
102+
if (!existingAgent) {
103+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
104+
}
105+
106+
const body = await request.json()
107+
108+
// Update agent
109+
const [updatedAgent] = await db
110+
.update(a2aAgent)
111+
.set({
112+
name: body.name ?? existingAgent.name,
113+
description: body.description ?? existingAgent.description,
114+
version: body.version ?? existingAgent.version,
115+
capabilities: body.capabilities ?? existingAgent.capabilities,
116+
skills: body.skills ?? existingAgent.skills,
117+
authentication: body.authentication ?? existingAgent.authentication,
118+
isPublished: body.isPublished ?? existingAgent.isPublished,
119+
publishedAt:
120+
body.isPublished && !existingAgent.isPublished ? new Date() : existingAgent.publishedAt,
121+
updatedAt: new Date(),
122+
})
123+
.where(eq(a2aAgent.id, agentId))
124+
.returning()
125+
126+
logger.info(`Updated A2A agent: ${agentId}`)
127+
128+
return NextResponse.json({ success: true, agent: updatedAgent })
129+
} catch (error) {
130+
logger.error('Error updating agent:', error)
131+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
132+
}
133+
}
134+
135+
/**
136+
* DELETE - Delete an agent
137+
*/
138+
export async function DELETE(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
139+
const { agentId } = await params
140+
141+
try {
142+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
143+
if (!auth.success || !auth.userId) {
144+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
145+
}
146+
147+
const [existingAgent] = await db
148+
.select()
149+
.from(a2aAgent)
150+
.where(eq(a2aAgent.id, agentId))
151+
.limit(1)
152+
153+
if (!existingAgent) {
154+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
155+
}
156+
157+
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
158+
159+
logger.info(`Deleted A2A agent: ${agentId}`)
160+
161+
return NextResponse.json({ success: true })
162+
} catch (error) {
163+
logger.error('Error deleting agent:', error)
164+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
165+
}
166+
}
167+
168+
/**
169+
* POST - Publish/unpublish an agent
170+
*/
171+
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
172+
const { agentId } = await params
173+
174+
try {
175+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
176+
if (!auth.success || !auth.userId) {
177+
logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
178+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
179+
}
180+
181+
const [existingAgent] = await db
182+
.select()
183+
.from(a2aAgent)
184+
.where(eq(a2aAgent.id, agentId))
185+
.limit(1)
186+
187+
if (!existingAgent) {
188+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
189+
}
190+
191+
const body = await request.json()
192+
const action = body.action as 'publish' | 'unpublish' | 'refresh'
193+
194+
if (action === 'publish') {
195+
// Verify workflow is deployed
196+
const [wf] = await db
197+
.select({ isDeployed: workflow.isDeployed })
198+
.from(workflow)
199+
.where(eq(workflow.id, existingAgent.workflowId))
200+
.limit(1)
201+
202+
if (!wf?.isDeployed) {
203+
return NextResponse.json(
204+
{ error: 'Workflow must be deployed before publishing agent' },
205+
{ status: 400 }
206+
)
207+
}
208+
209+
await db
210+
.update(a2aAgent)
211+
.set({
212+
isPublished: true,
213+
publishedAt: new Date(),
214+
updatedAt: new Date(),
215+
})
216+
.where(eq(a2aAgent.id, agentId))
217+
218+
logger.info(`Published A2A agent: ${agentId}`)
219+
return NextResponse.json({ success: true, isPublished: true })
220+
}
221+
222+
if (action === 'unpublish') {
223+
await db
224+
.update(a2aAgent)
225+
.set({
226+
isPublished: false,
227+
updatedAt: new Date(),
228+
})
229+
.where(eq(a2aAgent.id, agentId))
230+
231+
logger.info(`Unpublished A2A agent: ${agentId}`)
232+
return NextResponse.json({ success: true, isPublished: false })
233+
}
234+
235+
if (action === 'refresh') {
236+
// Refresh skills from workflow
237+
const workflowData = await loadWorkflowFromNormalizedTables(existingAgent.workflowId)
238+
if (!workflowData) {
239+
return NextResponse.json({ error: 'Failed to load workflow' }, { status: 500 })
240+
}
241+
242+
const [wf] = await db
243+
.select({ name: workflow.name, description: workflow.description })
244+
.from(workflow)
245+
.where(eq(workflow.id, existingAgent.workflowId))
246+
.limit(1)
247+
248+
const skills = generateSkillsFromWorkflow(
249+
existingAgent.workflowId,
250+
wf?.name || existingAgent.name,
251+
wf?.description,
252+
workflowData.blocks
253+
)
254+
255+
await db
256+
.update(a2aAgent)
257+
.set({
258+
skills,
259+
updatedAt: new Date(),
260+
})
261+
.where(eq(a2aAgent.id, agentId))
262+
263+
logger.info(`Refreshed skills for A2A agent: ${agentId}`)
264+
return NextResponse.json({ success: true, skills })
265+
}
266+
267+
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
268+
} catch (error) {
269+
logger.error('Error with agent action:', error)
270+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
271+
}
272+
}

0 commit comments

Comments
 (0)