diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 7addb30eaa..0143e517a5 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1855,17 +1855,25 @@ export function LinearIcon(props: React.SVGProps) { export function LemlistIcon(props: SVGProps) { return ( - - - - + + + + + ) } diff --git a/apps/docs/content/docs/en/tools/a2a.mdx b/apps/docs/content/docs/en/tools/a2a.mdx index 558f1f907e..9e7ea9ee4e 100644 --- a/apps/docs/content/docs/en/tools/a2a.mdx +++ b/apps/docs/content/docs/en/tools/a2a.mdx @@ -44,6 +44,8 @@ Send a message to an external A2A-compatible agent. | `message` | string | Yes | Message to send to the agent | | `taskId` | string | No | Task ID for continuing an existing task | | `contextId` | string | No | Context ID for conversation continuity | +| `data` | string | No | Structured data to include with the message \(JSON string\) | +| `files` | array | No | Files to include with the message | | `apiKey` | string | No | API key for authentication | #### Output @@ -208,8 +210,3 @@ Delete the push notification webhook configuration for a task. | `success` | boolean | Whether deletion was successful | - -## Notes - -- Category: `tools` -- Type: `a2a` diff --git a/apps/docs/content/docs/en/tools/lemlist.mdx b/apps/docs/content/docs/en/tools/lemlist.mdx index c3b38bb720..25e1a4ca11 100644 --- a/apps/docs/content/docs/en/tools/lemlist.mdx +++ b/apps/docs/content/docs/en/tools/lemlist.mdx @@ -49,8 +49,7 @@ Retrieves lead information by email address or lead ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Lemlist API key | -| `email` | string | No | Lead email address \(use either email or id\) | -| `id` | string | No | Lead ID \(use either email or id\) | +| `leadIdentifier` | string | Yes | Lead email address or lead ID | #### Output diff --git a/apps/sim/app/api/tools/a2a/send-message-stream/route.ts b/apps/sim/app/api/tools/a2a/send-message-stream/route.ts deleted file mode 100644 index e30689a801..0000000000 --- a/apps/sim/app/api/tools/a2a/send-message-stream/route.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { - Artifact, - Message, - Task, - TaskArtifactUpdateEvent, - TaskState, - TaskStatusUpdateEvent, -} from '@a2a-js/sdk' -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2ASendMessageStreamAPI') - -const A2ASendMessageStreamSchema = z.object({ - agentUrl: z.string().min(1, 'Agent URL is required'), - message: z.string().min(1, 'Message is required'), - taskId: z.string().optional(), - contextId: z.string().optional(), - apiKey: z.string().optional(), -}) - -export async function POST(request: NextRequest) { - const requestId = generateRequestId() - - try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn( - `[${requestId}] Unauthorized A2A send message stream attempt: ${authResult.error}` - ) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - logger.info( - `[${requestId}] Authenticated A2A send message stream request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const body = await request.json() - const validatedData = A2ASendMessageStreamSchema.parse(body) - - logger.info(`[${requestId}] Sending A2A streaming message`, { - agentUrl: validatedData.agentUrl, - hasTaskId: !!validatedData.taskId, - hasContextId: !!validatedData.contextId, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const message: Message = { - kind: 'message', - messageId: crypto.randomUUID(), - role: 'user', - parts: [{ kind: 'text', text: validatedData.message }], - ...(validatedData.taskId && { taskId: validatedData.taskId }), - ...(validatedData.contextId && { contextId: validatedData.contextId }), - } - - const stream = client.sendMessageStream({ message }) - - let taskId = '' - let contextId: string | undefined - let state: TaskState = 'working' - let content = '' - let artifacts: Artifact[] = [] - let history: Message[] = [] - - for await (const event of stream) { - if (event.kind === 'message') { - const msg = event as Message - content = extractTextContent(msg) - taskId = msg.taskId || taskId - contextId = msg.contextId || contextId - state = 'completed' - } else if (event.kind === 'task') { - const task = event as Task - taskId = task.id - contextId = task.contextId - state = task.status.state - artifacts = task.artifacts || [] - history = task.history || [] - const lastAgentMessage = history.filter((m) => m.role === 'agent').pop() - if (lastAgentMessage) { - content = extractTextContent(lastAgentMessage) - } - } else if ('status' in event) { - const statusEvent = event as TaskStatusUpdateEvent - state = statusEvent.status.state - } else if ('artifact' in event) { - const artifactEvent = event as TaskArtifactUpdateEvent - artifacts.push(artifactEvent.artifact) - } - } - - logger.info(`[${requestId}] A2A streaming message completed`, { - taskId, - state, - artifactCount: artifacts.length, - }) - - return NextResponse.json({ - success: isTerminalState(state) && state !== 'failed', - output: { - content, - taskId, - contextId, - state, - artifacts, - history, - }, - }) - } catch (error) { - if (error instanceof z.ZodError) { - logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) - return NextResponse.json( - { - success: false, - error: 'Invalid request data', - details: error.errors, - }, - { status: 400 } - ) - } - - logger.error(`[${requestId}] Error in A2A streaming:`, error) - - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Streaming failed', - }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/tools/a2a/send-message/route.ts b/apps/sim/app/api/tools/a2a/send-message/route.ts index 4d52fc710c..a66c2b3d37 100644 --- a/apps/sim/app/api/tools/a2a/send-message/route.ts +++ b/apps/sim/app/api/tools/a2a/send-message/route.ts @@ -1,4 +1,4 @@ -import type { Message, Task } from '@a2a-js/sdk' +import type { DataPart, FilePart, Message, Part, Task, TextPart } from '@a2a-js/sdk' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -10,11 +10,20 @@ export const dynamic = 'force-dynamic' const logger = createLogger('A2ASendMessageAPI') +const FileInputSchema = z.object({ + type: z.enum(['file', 'url']), + data: z.string(), + name: z.string(), + mime: z.string().optional(), +}) + const A2ASendMessageSchema = z.object({ agentUrl: z.string().min(1, 'Agent URL is required'), message: z.string().min(1, 'Message is required'), taskId: z.string().optional(), contextId: z.string().optional(), + data: z.string().optional(), + files: z.array(FileInputSchema).optional(), apiKey: z.string().optional(), }) @@ -51,18 +60,100 @@ export async function POST(request: NextRequest) { hasContextId: !!validatedData.contextId, }) - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) + let client + try { + client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) + logger.info(`[${requestId}] A2A client created successfully`) + } catch (clientError) { + logger.error(`[${requestId}] Failed to create A2A client:`, clientError) + return NextResponse.json( + { + success: false, + error: `Failed to connect to agent: ${clientError instanceof Error ? clientError.message : 'Unknown error'}`, + }, + { status: 502 } + ) + } + + const parts: Part[] = [] + + const textPart: TextPart = { kind: 'text', text: validatedData.message } + parts.push(textPart) + + if (validatedData.data) { + try { + const parsedData = JSON.parse(validatedData.data) + const dataPart: DataPart = { kind: 'data', data: parsedData } + parts.push(dataPart) + } catch (parseError) { + logger.warn(`[${requestId}] Failed to parse data as JSON, skipping DataPart`, { + error: parseError instanceof Error ? parseError.message : String(parseError), + }) + } + } + + if (validatedData.files && validatedData.files.length > 0) { + for (const file of validatedData.files) { + if (file.type === 'url') { + const filePart: FilePart = { + kind: 'file', + file: { + name: file.name, + mimeType: file.mime, + uri: file.data, + }, + } + parts.push(filePart) + } else if (file.type === 'file') { + let bytes = file.data + let mimeType = file.mime + + if (file.data.startsWith('data:')) { + const match = file.data.match(/^data:([^;]+);base64,(.+)$/) + if (match) { + mimeType = mimeType || match[1] + bytes = match[2] + } else { + bytes = file.data + } + } + + const filePart: FilePart = { + kind: 'file', + file: { + name: file.name, + mimeType: mimeType || 'application/octet-stream', + bytes, + }, + } + parts.push(filePart) + } + } + } const message: Message = { kind: 'message', messageId: crypto.randomUUID(), role: 'user', - parts: [{ kind: 'text', text: validatedData.message }], + parts, ...(validatedData.taskId && { taskId: validatedData.taskId }), ...(validatedData.contextId && { contextId: validatedData.contextId }), } - const result = await client.sendMessage({ message }) + let result + try { + result = await client.sendMessage({ message }) + logger.info(`[${requestId}] A2A sendMessage completed`, { resultKind: result?.kind }) + } catch (sendError) { + logger.error(`[${requestId}] Failed to send A2A message:`, sendError) + return NextResponse.json( + { + success: false, + error: `Failed to send message: ${sendError instanceof Error ? sendError.message : 'Unknown error'}`, + }, + { status: 502 } + ) + } if (result.kind === 'message') { const responseMessage = result as Message diff --git a/apps/sim/blocks/blocks/a2a.ts b/apps/sim/blocks/blocks/a2a.ts index a520028cff..6996b685a4 100644 --- a/apps/sim/blocks/blocks/a2a.ts +++ b/apps/sim/blocks/blocks/a2a.ts @@ -98,6 +98,23 @@ export const A2ABlock: BlockConfig = { condition: { field: 'operation', value: 'a2a_send_message' }, required: true, }, + { + id: 'data', + title: 'Data (JSON)', + type: 'code', + placeholder: '{\n "key": "value"\n}', + description: 'Structured data to include with the message (DataPart)', + condition: { field: 'operation', value: 'a2a_send_message' }, + }, + { + id: 'files', + title: 'Files', + type: 'file-upload', + placeholder: 'Upload files to send', + description: 'Files to include with the message (FilePart)', + condition: { field: 'operation', value: 'a2a_send_message' }, + multiple: true, + }, { id: 'taskId', title: 'Task ID', @@ -208,6 +225,14 @@ export const A2ABlock: BlockConfig = { type: 'string', description: 'Context ID for conversation continuity', }, + data: { + type: 'json', + description: 'Structured data to include with the message', + }, + files: { + type: 'array', + description: 'Files to include with the message', + }, historyLength: { type: 'number', description: 'Number of history messages to include', diff --git a/apps/sim/lib/a2a/utils.ts b/apps/sim/lib/a2a/utils.ts index 119059d994..3eddb5d8d1 100644 --- a/apps/sim/lib/a2a/utils.ts +++ b/apps/sim/lib/a2a/utils.ts @@ -36,9 +36,10 @@ class ApiKeyInterceptor implements CallInterceptor { /** * Create an A2A client from an agent URL with optional API key authentication * - * The agent URL should be the full endpoint URL (e.g., /api/a2a/serve/{agentId}). - * We pass an empty path to createFromUrl so it uses the URL directly for agent card - * discovery (GET on the URL) instead of appending .well-known/agent-card.json. + * Supports both standard A2A agents (agent card at /.well-known/agent.json) + * and Sim Studio agents (agent card at root URL via GET). + * + * Tries standard path first, falls back to root URL for compatibility. */ export async function createA2AClient(agentUrl: string, apiKey?: string): Promise { const factoryOptions = apiKey @@ -49,6 +50,18 @@ export async function createA2AClient(agentUrl: string, apiKey?: string): Promis }) : ClientFactoryOptions.default const factory = new ClientFactory(factoryOptions) + + // Try standard A2A path first (/.well-known/agent.json) + try { + return await factory.createFromUrl(agentUrl, '/.well-known/agent.json') + } catch (standardError) { + logger.debug('Standard agent card path failed, trying root URL', { + agentUrl, + error: standardError instanceof Error ? standardError.message : String(standardError), + }) + } + + // Fall back to root URL (Sim Studio compatibility) return factory.createFromUrl(agentUrl, '') } diff --git a/apps/sim/tools/a2a/cancel_task.ts b/apps/sim/tools/a2a/cancel_task.ts index a43bccc587..6ac1b78937 100644 --- a/apps/sim/tools/a2a/cancel_task.ts +++ b/apps/sim/tools/a2a/cancel_task.ts @@ -30,11 +30,14 @@ export const a2aCancelTaskTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params: A2ACancelTaskParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params: A2ACancelTaskParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/delete_push_notification.ts b/apps/sim/tools/a2a/delete_push_notification.ts index 186e9834bc..89052e8cf1 100644 --- a/apps/sim/tools/a2a/delete_push_notification.ts +++ b/apps/sim/tools/a2a/delete_push_notification.ts @@ -38,12 +38,16 @@ export const a2aDeletePushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - pushNotificationConfigId: params.pushNotificationConfigId, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.pushNotificationConfigId) + body.pushNotificationConfigId = params.pushNotificationConfigId + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_agent_card.ts b/apps/sim/tools/a2a/get_agent_card.ts index e6ee38795b..f0e07ff6c5 100644 --- a/apps/sim/tools/a2a/get_agent_card.ts +++ b/apps/sim/tools/a2a/get_agent_card.ts @@ -25,10 +25,13 @@ export const a2aGetAgentCardTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_push_notification.ts b/apps/sim/tools/a2a/get_push_notification.ts index e117923674..3682476b32 100644 --- a/apps/sim/tools/a2a/get_push_notification.ts +++ b/apps/sim/tools/a2a/get_push_notification.ts @@ -33,11 +33,14 @@ export const a2aGetPushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_task.ts b/apps/sim/tools/a2a/get_task.ts index 1c62b408f4..43e0059dac 100644 --- a/apps/sim/tools/a2a/get_task.ts +++ b/apps/sim/tools/a2a/get_task.ts @@ -34,12 +34,15 @@ export const a2aGetTaskTool: ToolConfig = headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: A2AGetTaskParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - historyLength: params.historyLength, - }), + body: (params: A2AGetTaskParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + if (params.historyLength) body.historyLength = params.historyLength + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/index.ts b/apps/sim/tools/a2a/index.ts index 7b78f26424..ccbf235a1b 100644 --- a/apps/sim/tools/a2a/index.ts +++ b/apps/sim/tools/a2a/index.ts @@ -5,7 +5,6 @@ import { a2aGetPushNotificationTool } from './get_push_notification' import { a2aGetTaskTool } from './get_task' import { a2aResubscribeTool } from './resubscribe' import { a2aSendMessageTool } from './send_message' -import { a2aSendMessageStreamTool } from './send_message_stream' import { a2aSetPushNotificationTool } from './set_push_notification' export { @@ -16,6 +15,5 @@ export { a2aGetTaskTool, a2aResubscribeTool, a2aSendMessageTool, - a2aSendMessageStreamTool, a2aSetPushNotificationTool, } diff --git a/apps/sim/tools/a2a/resubscribe.ts b/apps/sim/tools/a2a/resubscribe.ts index e2ed455855..99456b8b53 100644 --- a/apps/sim/tools/a2a/resubscribe.ts +++ b/apps/sim/tools/a2a/resubscribe.ts @@ -30,11 +30,14 @@ export const a2aResubscribeTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params: A2AResubscribeParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params: A2AResubscribeParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response) => { diff --git a/apps/sim/tools/a2a/send_message.ts b/apps/sim/tools/a2a/send_message.ts index 6da9cf11c8..0b317a4413 100644 --- a/apps/sim/tools/a2a/send_message.ts +++ b/apps/sim/tools/a2a/send_message.ts @@ -26,6 +26,14 @@ export const a2aSendMessageTool: ToolConfig ({}), + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + message: params.message, + } + if (params.taskId) body.taskId = params.taskId + if (params.contextId) body.contextId = params.contextId + if (params.data) body.data = params.data + if (params.files && params.files.length > 0) body.files = params.files + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/send_message_stream.ts b/apps/sim/tools/a2a/send_message_stream.ts deleted file mode 100644 index dd44856b0c..0000000000 --- a/apps/sim/tools/a2a/send_message_stream.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { A2ASendMessageParams, A2ASendMessageResponse } from './types' - -export const a2aSendMessageStreamTool: ToolConfig = { - id: 'a2a_send_message_stream', - name: 'A2A Send Message (Streaming)', - description: 'Send a message to an external A2A-compatible agent with real-time streaming.', - version: '1.0.0', - - params: { - agentUrl: { - type: 'string', - required: true, - description: 'The A2A agent endpoint URL', - }, - message: { - type: 'string', - required: true, - description: 'Message to send to the agent', - }, - taskId: { - type: 'string', - description: 'Task ID for continuing an existing task', - }, - contextId: { - type: 'string', - description: 'Context ID for conversation continuity', - }, - apiKey: { - type: 'string', - description: 'API key for authentication', - }, - }, - - request: { - url: '/api/tools/a2a/send-message-stream', - method: 'POST', - headers: () => ({ - 'Content-Type': 'application/json', - }), - body: (params) => ({ - agentUrl: params.agentUrl, - message: params.message, - taskId: params.taskId, - contextId: params.contextId, - apiKey: params.apiKey, - }), - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - return data - }, - - outputs: { - content: { - type: 'string', - description: 'The text response from the agent', - }, - taskId: { - type: 'string', - description: 'Task ID for follow-up interactions', - }, - contextId: { - type: 'string', - description: 'Context ID for conversation continuity', - }, - state: { - type: 'string', - description: 'Task state', - }, - artifacts: { - type: 'array', - description: 'Structured output artifacts', - }, - history: { - type: 'array', - description: 'Full message history', - }, - }, -} diff --git a/apps/sim/tools/a2a/set_push_notification.ts b/apps/sim/tools/a2a/set_push_notification.ts index a1a69ed40b..e3dd25360b 100644 --- a/apps/sim/tools/a2a/set_push_notification.ts +++ b/apps/sim/tools/a2a/set_push_notification.ts @@ -42,13 +42,16 @@ export const a2aSetPushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: A2ASetPushNotificationParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - webhookUrl: params.webhookUrl, - token: params.token, - apiKey: params.apiKey, - }), + body: (params: A2ASetPushNotificationParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + webhookUrl: params.webhookUrl, + } + if (params.token) body.token = params.token + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/types.ts b/apps/sim/tools/a2a/types.ts index 7230eb563a..82844d6faf 100644 --- a/apps/sim/tools/a2a/types.ts +++ b/apps/sim/tools/a2a/types.ts @@ -25,11 +25,20 @@ export interface A2AGetAgentCardResponse extends ToolResponse { } } +export interface A2ASendMessageFileInput { + type: 'file' | 'url' + data: string + name: string + mime?: string +} + export interface A2ASendMessageParams { agentUrl: string message: string taskId?: string contextId?: string + data?: string + files?: A2ASendMessageFileInput[] apiKey?: string } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6570eea636..69e27d6e24 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -5,7 +5,6 @@ import { a2aGetPushNotificationTool, a2aGetTaskTool, a2aResubscribeTool, - a2aSendMessageStreamTool, a2aSendMessageTool, a2aSetPushNotificationTool, } from '@/tools/a2a' @@ -1541,7 +1540,6 @@ export const tools: Record = { a2a_get_task: a2aGetTaskTool, a2a_resubscribe: a2aResubscribeTool, a2a_send_message: a2aSendMessageTool, - a2a_send_message_stream: a2aSendMessageStreamTool, a2a_set_push_notification: a2aSetPushNotificationTool, arxiv_search: arxivSearchTool, arxiv_get_paper: arxivGetPaperTool,