Skip to content

Commit 4b026ad

Browse files
authored
fix(a2a): added file data part and data data part to a2a agents (#2805)
* fix(a2a): added file data part and data data part to a2a agents * removed unused streaming tool * ack comment
1 parent f6b7c15 commit 4b026ad

File tree

17 files changed

+230
-281
lines changed

17 files changed

+230
-281
lines changed

apps/docs/content/docs/en/tools/a2a.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Send a message to an external A2A-compatible agent.
4444
| `message` | string | Yes | Message to send to the agent |
4545
| `taskId` | string | No | Task ID for continuing an existing task |
4646
| `contextId` | string | No | Context ID for conversation continuity |
47+
| `data` | string | No | Structured data to include with the message \(JSON string\) |
48+
| `files` | array | No | Files to include with the message |
4749
| `apiKey` | string | No | API key for authentication |
4850

4951
#### Output

apps/sim/app/api/tools/a2a/send-message-stream/route.ts

Lines changed: 0 additions & 150 deletions
This file was deleted.

apps/sim/app/api/tools/a2a/send-message/route.ts

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Message, Task } from '@a2a-js/sdk'
1+
import type { DataPart, FilePart, Message, Part, Task, TextPart } from '@a2a-js/sdk'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
@@ -10,11 +10,20 @@ export const dynamic = 'force-dynamic'
1010

1111
const logger = createLogger('A2ASendMessageAPI')
1212

13+
const FileInputSchema = z.object({
14+
type: z.enum(['file', 'url']),
15+
data: z.string(),
16+
name: z.string(),
17+
mime: z.string().optional(),
18+
})
19+
1320
const A2ASendMessageSchema = z.object({
1421
agentUrl: z.string().min(1, 'Agent URL is required'),
1522
message: z.string().min(1, 'Message is required'),
1623
taskId: z.string().optional(),
1724
contextId: z.string().optional(),
25+
data: z.string().optional(),
26+
files: z.array(FileInputSchema).optional(),
1827
apiKey: z.string().optional(),
1928
})
2029

@@ -51,18 +60,100 @@ export async function POST(request: NextRequest) {
5160
hasContextId: !!validatedData.contextId,
5261
})
5362

54-
const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
63+
let client
64+
try {
65+
client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
66+
logger.info(`[${requestId}] A2A client created successfully`)
67+
} catch (clientError) {
68+
logger.error(`[${requestId}] Failed to create A2A client:`, clientError)
69+
return NextResponse.json(
70+
{
71+
success: false,
72+
error: `Failed to connect to agent: ${clientError instanceof Error ? clientError.message : 'Unknown error'}`,
73+
},
74+
{ status: 502 }
75+
)
76+
}
77+
78+
const parts: Part[] = []
79+
80+
const textPart: TextPart = { kind: 'text', text: validatedData.message }
81+
parts.push(textPart)
82+
83+
if (validatedData.data) {
84+
try {
85+
const parsedData = JSON.parse(validatedData.data)
86+
const dataPart: DataPart = { kind: 'data', data: parsedData }
87+
parts.push(dataPart)
88+
} catch (parseError) {
89+
logger.warn(`[${requestId}] Failed to parse data as JSON, skipping DataPart`, {
90+
error: parseError instanceof Error ? parseError.message : String(parseError),
91+
})
92+
}
93+
}
94+
95+
if (validatedData.files && validatedData.files.length > 0) {
96+
for (const file of validatedData.files) {
97+
if (file.type === 'url') {
98+
const filePart: FilePart = {
99+
kind: 'file',
100+
file: {
101+
name: file.name,
102+
mimeType: file.mime,
103+
uri: file.data,
104+
},
105+
}
106+
parts.push(filePart)
107+
} else if (file.type === 'file') {
108+
let bytes = file.data
109+
let mimeType = file.mime
110+
111+
if (file.data.startsWith('data:')) {
112+
const match = file.data.match(/^data:([^;]+);base64,(.+)$/)
113+
if (match) {
114+
mimeType = mimeType || match[1]
115+
bytes = match[2]
116+
} else {
117+
bytes = file.data
118+
}
119+
}
120+
121+
const filePart: FilePart = {
122+
kind: 'file',
123+
file: {
124+
name: file.name,
125+
mimeType: mimeType || 'application/octet-stream',
126+
bytes,
127+
},
128+
}
129+
parts.push(filePart)
130+
}
131+
}
132+
}
55133

56134
const message: Message = {
57135
kind: 'message',
58136
messageId: crypto.randomUUID(),
59137
role: 'user',
60-
parts: [{ kind: 'text', text: validatedData.message }],
138+
parts,
61139
...(validatedData.taskId && { taskId: validatedData.taskId }),
62140
...(validatedData.contextId && { contextId: validatedData.contextId }),
63141
}
64142

65-
const result = await client.sendMessage({ message })
143+
let result
144+
try {
145+
result = await client.sendMessage({ message })
146+
logger.info(`[${requestId}] A2A sendMessage completed`, { resultKind: result?.kind })
147+
} catch (sendError) {
148+
logger.error(`[${requestId}] Failed to send A2A message:`, sendError)
149+
return NextResponse.json(
150+
{
151+
success: false,
152+
error: `Failed to send message: ${sendError instanceof Error ? sendError.message : 'Unknown error'}`,
153+
},
154+
{ status: 502 }
155+
)
156+
}
66157

67158
if (result.kind === 'message') {
68159
const responseMessage = result as Message

apps/sim/blocks/blocks/a2a.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
9898
condition: { field: 'operation', value: 'a2a_send_message' },
9999
required: true,
100100
},
101+
{
102+
id: 'data',
103+
title: 'Data (JSON)',
104+
type: 'code',
105+
placeholder: '{\n "key": "value"\n}',
106+
description: 'Structured data to include with the message (DataPart)',
107+
condition: { field: 'operation', value: 'a2a_send_message' },
108+
},
109+
{
110+
id: 'files',
111+
title: 'Files',
112+
type: 'file-upload',
113+
placeholder: 'Upload files to send',
114+
description: 'Files to include with the message (FilePart)',
115+
condition: { field: 'operation', value: 'a2a_send_message' },
116+
multiple: true,
117+
},
101118
{
102119
id: 'taskId',
103120
title: 'Task ID',
@@ -208,6 +225,14 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
208225
type: 'string',
209226
description: 'Context ID for conversation continuity',
210227
},
228+
data: {
229+
type: 'json',
230+
description: 'Structured data to include with the message',
231+
},
232+
files: {
233+
type: 'array',
234+
description: 'Files to include with the message',
235+
},
211236
historyLength: {
212237
type: 'number',
213238
description: 'Number of history messages to include',

apps/sim/lib/a2a/utils.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ class ApiKeyInterceptor implements CallInterceptor {
3636
/**
3737
* Create an A2A client from an agent URL with optional API key authentication
3838
*
39-
* The agent URL should be the full endpoint URL (e.g., /api/a2a/serve/{agentId}).
40-
* We pass an empty path to createFromUrl so it uses the URL directly for agent card
41-
* discovery (GET on the URL) instead of appending .well-known/agent-card.json.
39+
* Supports both standard A2A agents (agent card at /.well-known/agent.json)
40+
* and Sim Studio agents (agent card at root URL via GET).
41+
*
42+
* Tries standard path first, falls back to root URL for compatibility.
4243
*/
4344
export async function createA2AClient(agentUrl: string, apiKey?: string): Promise<Client> {
4445
const factoryOptions = apiKey
@@ -49,6 +50,18 @@ export async function createA2AClient(agentUrl: string, apiKey?: string): Promis
4950
})
5051
: ClientFactoryOptions.default
5152
const factory = new ClientFactory(factoryOptions)
53+
54+
// Try standard A2A path first (/.well-known/agent.json)
55+
try {
56+
return await factory.createFromUrl(agentUrl, '/.well-known/agent.json')
57+
} catch (standardError) {
58+
logger.debug('Standard agent card path failed, trying root URL', {
59+
agentUrl,
60+
error: standardError instanceof Error ? standardError.message : String(standardError),
61+
})
62+
}
63+
64+
// Fall back to root URL (Sim Studio compatibility)
5265
return factory.createFromUrl(agentUrl, '')
5366
}
5467

apps/sim/tools/a2a/cancel_task.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ export const a2aCancelTaskTool: ToolConfig<A2ACancelTaskParams, A2ACancelTaskRes
3030
headers: () => ({
3131
'Content-Type': 'application/json',
3232
}),
33-
body: (params: A2ACancelTaskParams) => ({
34-
agentUrl: params.agentUrl,
35-
taskId: params.taskId,
36-
apiKey: params.apiKey,
37-
}),
33+
body: (params: A2ACancelTaskParams) => {
34+
const body: Record<string, string> = {
35+
agentUrl: params.agentUrl,
36+
taskId: params.taskId,
37+
}
38+
if (params.apiKey) body.apiKey = params.apiKey
39+
return body
40+
},
3841
},
3942

4043
transformResponse: async (response: Response) => {

0 commit comments

Comments
 (0)