Skip to content

Commit a426ce4

Browse files
committed
fix(a2a): added file data part and data data part to a2a agents
1 parent eb52f69 commit a426ce4

File tree

16 files changed

+258
-71
lines changed

16 files changed

+258
-71
lines changed

apps/docs/components/icons.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,17 +1855,25 @@ export function LinearIcon(props: React.SVGProps<SVGSVGElement>) {
18551855

18561856
export function LemlistIcon(props: SVGProps<SVGSVGElement>) {
18571857
return (
1858-
<svg
1859-
{...props}
1860-
xmlns='http://www.w3.org/2000/svg'
1861-
viewBox='0 0 24 24'
1862-
width='24'
1863-
height='24'
1864-
fill='none'
1865-
>
1866-
<rect width='24' height='24' rx='4' fill='#316BFF' />
1867-
<path d='M7 6h2v9h5v2H7V6Z' fill='white' />
1868-
<circle cx='17' cy='8' r='2' fill='white' />
1858+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 181' fill='none'>
1859+
<path
1860+
fillRule='evenodd'
1861+
clipRule='evenodd'
1862+
d='M32.0524 0.919922H147.948C165.65 0.919922 180 15.2703 180 32.9723V148.867C180 166.57 165.65 180.92 147.948 180.92H32.0524C14.3504 180.92 0 166.57 0 148.867V32.9723C0 15.2703 14.3504 0.919922 32.0524 0.919922ZM119.562 82.8879H85.0826C82.4732 82.8879 80.3579 85.0032 80.3579 87.6126V94.2348C80.3579 96.8442 82.4732 98.9595 85.0826 98.9595H119.562C122.171 98.9595 124.286 96.8442 124.286 94.2348V87.6126C124.286 85.0032 122.171 82.8879 119.562 82.8879ZM85.0826 49.1346H127.061C129.67 49.1346 131.785 51.2499 131.785 53.8593V60.4815C131.785 63.0909 129.67 65.2062 127.061 65.2062H85.0826C82.4732 65.2062 80.3579 63.0909 80.3579 60.4815V53.8593C80.3579 51.2499 82.4732 49.1346 85.0826 49.1346ZM131.785 127.981V121.358C131.785 118.75 129.669 116.634 127.061 116.634H76.5706C69.7821 116.634 64.2863 111.138 64.2863 104.349V53.8593C64.2863 51.2513 62.1697 49.1346 59.5616 49.1346H52.9395C50.3314 49.1346 48.2147 51.2513 48.2147 53.8593V114.199C48.8497 124.133 56.7873 132.07 66.7205 132.705H127.061C129.669 132.705 131.785 130.589 131.785 127.981Z'
1863+
fill='#316BFF'
1864+
/>
1865+
<path
1866+
d='M85.0826 49.1346H127.061C129.67 49.1346 131.785 51.2499 131.785 53.8593V60.4815C131.785 63.0909 129.67 65.2062 127.061 65.2062H85.0826C82.4732 65.2062 80.3579 63.0909 80.3579 60.4815V53.8593C80.3579 51.2499 82.4732 49.1346 85.0826 49.1346Z'
1867+
fill='white'
1868+
/>
1869+
<path
1870+
d='M85.0826 82.8879H119.562C122.171 82.8879 124.286 85.0032 124.286 87.6126V94.2348C124.286 96.8442 122.171 98.9595 119.562 98.9595H85.0826C82.4732 98.9595 80.3579 96.8442 80.3579 94.2348V87.6126C80.3579 85.0032 82.4732 82.8879 85.0826 82.8879Z'
1871+
fill='white'
1872+
/>
1873+
<path
1874+
d='M131.785 121.358V127.981C131.785 130.589 129.669 132.705 127.061 132.705H66.7205C56.7873 132.07 48.8497 124.133 48.2147 114.199V53.8593C48.2147 51.2513 50.3314 49.1346 52.9395 49.1346H59.5616C62.1697 49.1346 64.2863 51.2513 64.2863 53.8593V104.349C64.2863 111.138 69.7821 116.634 76.5706 116.634H127.061C129.669 116.634 131.785 118.75 131.785 121.358Z'
1875+
fill='white'
1876+
/>
18691877
</svg>
18701878
)
18711879
}

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

Lines changed: 2 additions & 5 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
@@ -208,8 +210,3 @@ Delete the push notification webhook configuration for a task.
208210
| `success` | boolean | Whether deletion was successful |
209211

210212

211-
212-
## Notes
213-
214-
- Category: `tools`
215-
- Type: `a2a`

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ Retrieves lead information by email address or lead ID.
4949
| Parameter | Type | Required | Description |
5050
| --------- | ---- | -------- | ----------- |
5151
| `apiKey` | string | Yes | Lemlist API key |
52-
| `email` | string | No | Lead email address \(use either email or id\) |
53-
| `id` | string | No | Lead ID \(use either email or id\) |
52+
| `leadIdentifier` | string | Yes | Lead email address or lead ID |
5453

5554
#### Output
5655

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

Lines changed: 93 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,98 @@ 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+
}
117+
}
118+
119+
const filePart: FilePart = {
120+
kind: 'file',
121+
file: {
122+
name: file.name,
123+
mimeType: mimeType || 'application/octet-stream',
124+
bytes,
125+
},
126+
}
127+
parts.push(filePart)
128+
}
129+
}
130+
}
55131

56132
const message: Message = {
57133
kind: 'message',
58134
messageId: crypto.randomUUID(),
59135
role: 'user',
60-
parts: [{ kind: 'text', text: validatedData.message }],
136+
parts,
61137
...(validatedData.taskId && { taskId: validatedData.taskId }),
62138
...(validatedData.contextId && { contextId: validatedData.contextId }),
63139
}
64140

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

67156
if (result.kind === 'message') {
68157
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) => {

apps/sim/tools/a2a/delete_push_notification.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ export const a2aDeletePushNotificationTool: ToolConfig<
3838
headers: () => ({
3939
'Content-Type': 'application/json',
4040
}),
41-
body: (params) => ({
42-
agentUrl: params.agentUrl,
43-
taskId: params.taskId,
44-
pushNotificationConfigId: params.pushNotificationConfigId,
45-
apiKey: params.apiKey,
46-
}),
41+
body: (params) => {
42+
const body: Record<string, string> = {
43+
agentUrl: params.agentUrl,
44+
taskId: params.taskId,
45+
}
46+
if (params.pushNotificationConfigId)
47+
body.pushNotificationConfigId = params.pushNotificationConfigId
48+
if (params.apiKey) body.apiKey = params.apiKey
49+
return body
50+
},
4751
},
4852

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

apps/sim/tools/a2a/get_agent_card.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ export const a2aGetAgentCardTool: ToolConfig<A2AGetAgentCardParams, A2AGetAgentC
2525
headers: () => ({
2626
'Content-Type': 'application/json',
2727
}),
28-
body: (params) => ({
29-
agentUrl: params.agentUrl,
30-
apiKey: params.apiKey,
31-
}),
28+
body: (params) => {
29+
const body: Record<string, string> = {
30+
agentUrl: params.agentUrl,
31+
}
32+
if (params.apiKey) body.apiKey = params.apiKey
33+
return body
34+
},
3235
},
3336

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

apps/sim/tools/a2a/get_push_notification.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ export const a2aGetPushNotificationTool: ToolConfig<
3333
headers: () => ({
3434
'Content-Type': 'application/json',
3535
}),
36-
body: (params) => ({
37-
agentUrl: params.agentUrl,
38-
taskId: params.taskId,
39-
apiKey: params.apiKey,
40-
}),
36+
body: (params) => {
37+
const body: Record<string, string> = {
38+
agentUrl: params.agentUrl,
39+
taskId: params.taskId,
40+
}
41+
if (params.apiKey) body.apiKey = params.apiKey
42+
return body
43+
},
4144
},
4245

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

0 commit comments

Comments
 (0)