From ff2736701d33ea256c3b0840b668a57035943fc7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sun, 28 Jun 2026 15:51:47 -0700 Subject: [PATCH 1/2] chore(deploy): remove a2a --- apps/docs/components/icons.tsx | 25 - apps/docs/content/docs/de/tools/a2a.mdx | 207 - .../content/docs/en/integrations/logs.mdx | 2 +- .../content/docs/en/logs-debugging/index.mdx | 2 +- .../en/platform/enterprise/access-control.mdx | 3 +- .../docs/en/workflows/deployment/index.mdx | 5 +- apps/docs/content/docs/es/tools/a2a.mdx | 207 - apps/docs/content/docs/fr/tools/a2a.mdx | 207 - apps/docs/content/docs/ja/tools/a2a.mdx | 207 - apps/docs/content/docs/zh/tools/a2a.mdx | 207 - .../sim/app/api/a2a/agents/[agentId]/route.ts | 355 - apps/sim/app/api/a2a/agents/route.ts | 223 - apps/sim/app/api/a2a/serve/[agentId]/route.ts | 1576 -- apps/sim/app/api/a2a/serve/[agentId]/utils.ts | 177 - .../app/api/tools/a2a/cancel-task/route.ts | 90 - .../a2a/delete-push-notification/route.ts | 101 - .../app/api/tools/a2a/get-agent-card/route.ts | 101 - .../tools/a2a/get-push-notification/route.ts | 123 - apps/sim/app/api/tools/a2a/get-task/route.ts | 98 - .../app/api/tools/a2a/resubscribe/route.ts | 127 - .../app/api/tools/a2a/send-message/route.ts | 225 - .../tools/a2a/set-push-notification/route.ts | 112 - .../app/api/workflows/[id]/execute/route.ts | 2 +- .../app/workspace/[workspaceId]/logs/utils.ts | 1 - .../deploy-modal/components/a2a/a2a.tsx | 869 - .../deploy-modal/components/a2a/index.ts | 1 - .../deploy-upgrade-gate.tsx | 2 +- .../deploy-modal/components/index.ts | 1 - .../components/deploy-modal/deploy-modal.tsx | 170 +- .../a2a-push-notification-delivery.ts | 33 - .../background/cleanup-soft-deletes.test.ts | 1 - apps/sim/background/cleanup-soft-deletes.ts | 7 - apps/sim/blocks/blocks/a2a.ts | 356 +- .../components/group-detail.tsx | 7 - .../utils/permission-check.test.ts | 1 - apps/sim/hooks/queries/a2a/agents.ts | 266 - apps/sim/lib/a2a/agent-card.ts | 138 - apps/sim/lib/a2a/constants.ts | 29 - apps/sim/lib/a2a/push-notifications.ts | 136 - apps/sim/lib/a2a/types.ts | 103 - apps/sim/lib/a2a/utils.ts | 444 - apps/sim/lib/api/contracts/a2a-agents.ts | 239 - .../lib/api/contracts/permission-groups.ts | 1 - apps/sim/lib/api/contracts/tools/a2a.ts | 128 - apps/sim/lib/api/contracts/tools/index.ts | 1 - apps/sim/lib/api/contracts/workflows.ts | 1 - apps/sim/lib/billing/core/api-access.ts | 2 +- apps/sim/lib/copilot/vfs/serializers.ts | 20 - apps/sim/lib/copilot/vfs/workspace-vfs.ts | 24 +- apps/sim/lib/core/config/env-flags.ts | 2 +- apps/sim/lib/core/config/env.ts | 2 +- apps/sim/lib/logs/get-trigger-options.ts | 1 - apps/sim/lib/permission-groups/types.ts | 4 - apps/sim/lib/posthog/events.ts | 24 - apps/sim/lib/workflows/lifecycle.ts | 42 - .../orchestration/folder-lifecycle.ts | 2 - apps/sim/package.json | 1 - apps/sim/stores/logs/filters/types.ts | 1 - apps/sim/tools/a2a/cancel_task.ts | 56 - .../sim/tools/a2a/delete_push_notification.ts | 69 - apps/sim/tools/a2a/get_agent_card.ts | 56 - apps/sim/tools/a2a/get_push_notification.ts | 78 - apps/sim/tools/a2a/get_task.ts | 65 - apps/sim/tools/a2a/index.ts | 19 - apps/sim/tools/a2a/resubscribe.ts | 76 - apps/sim/tools/a2a/send_message.ts | 84 - apps/sim/tools/a2a/set_push_notification.ts | 95 - apps/sim/tools/a2a/types.ts | 361 - apps/sim/tools/logs/query_runs.ts | 2 +- apps/sim/tools/registry.ts | 18 - bun.lock | 13 +- packages/db/migrations/0252_remove_a2a.sql | 10 + .../db/migrations/meta/0252_snapshot.json | 17002 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 151 - packages/testing/src/mocks/schema.mock.ts | 44 - scripts/check-api-validation-contracts.ts | 1 - 77 files changed, 17054 insertions(+), 8595 deletions(-) delete mode 100644 apps/docs/content/docs/de/tools/a2a.mdx delete mode 100644 apps/docs/content/docs/es/tools/a2a.mdx delete mode 100644 apps/docs/content/docs/fr/tools/a2a.mdx delete mode 100644 apps/docs/content/docs/ja/tools/a2a.mdx delete mode 100644 apps/docs/content/docs/zh/tools/a2a.mdx delete mode 100644 apps/sim/app/api/a2a/agents/[agentId]/route.ts delete mode 100644 apps/sim/app/api/a2a/agents/route.ts delete mode 100644 apps/sim/app/api/a2a/serve/[agentId]/route.ts delete mode 100644 apps/sim/app/api/a2a/serve/[agentId]/utils.ts delete mode 100644 apps/sim/app/api/tools/a2a/cancel-task/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/delete-push-notification/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/get-agent-card/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/get-push-notification/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/get-task/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/resubscribe/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/send-message/route.ts delete mode 100644 apps/sim/app/api/tools/a2a/set-push-notification/route.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/index.ts delete mode 100644 apps/sim/background/a2a-push-notification-delivery.ts delete mode 100644 apps/sim/hooks/queries/a2a/agents.ts delete mode 100644 apps/sim/lib/a2a/agent-card.ts delete mode 100644 apps/sim/lib/a2a/constants.ts delete mode 100644 apps/sim/lib/a2a/push-notifications.ts delete mode 100644 apps/sim/lib/a2a/types.ts delete mode 100644 apps/sim/lib/a2a/utils.ts delete mode 100644 apps/sim/lib/api/contracts/a2a-agents.ts delete mode 100644 apps/sim/lib/api/contracts/tools/a2a.ts delete mode 100644 apps/sim/tools/a2a/cancel_task.ts delete mode 100644 apps/sim/tools/a2a/delete_push_notification.ts delete mode 100644 apps/sim/tools/a2a/get_agent_card.ts delete mode 100644 apps/sim/tools/a2a/get_push_notification.ts delete mode 100644 apps/sim/tools/a2a/get_task.ts delete mode 100644 apps/sim/tools/a2a/index.ts delete mode 100644 apps/sim/tools/a2a/resubscribe.ts delete mode 100644 apps/sim/tools/a2a/send_message.ts delete mode 100644 apps/sim/tools/a2a/set_push_notification.ts delete mode 100644 apps/sim/tools/a2a/types.ts create mode 100644 packages/db/migrations/0252_remove_a2a.sql create mode 100644 packages/db/migrations/meta/0252_snapshot.json diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 165a6db7556..4bfdcfe76c2 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -5730,31 +5730,6 @@ export function McpIcon(props: SVGProps) { ) } -export function A2AIcon(props: SVGProps) { - return ( - - - - - - - - - - ) -} - export function WordpressIcon(props: SVGProps) { return ( diff --git a/apps/docs/content/docs/de/tools/a2a.mdx b/apps/docs/content/docs/de/tools/a2a.mdx deleted file mode 100644 index bac4105ba0a..00000000000 --- a/apps/docs/content/docs/de/tools/a2a.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: A2A -description: Interagiere mit externen A2A-kompatiblen Agenten ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -Das A2A-Protokoll (Agent-to-Agent) ermöglicht es Sim, mit externen KI-Agenten und Systemen zu interagieren, die A2A-kompatible APIs implementieren. Mit A2A kannst du Sims Automatisierungen und Workflows mit Remote-Agenten verbinden – wie LLM-gestützten Bots, Microservices und anderen KI-basierten Tools – unter Verwendung eines standardisierten Nachrichtenformats. - -Mit den A2A-Tools in Sim kannst du: - -- **Nachrichten an externe Agenten senden**: Kommuniziere direkt mit Remote-Agenten und übermittle Prompts, Befehle oder Daten. -- **Antworten empfangen und streamen**: Erhalte strukturierte Antworten, Artefakte oder Echtzeit-Updates vom Agenten, während die Aufgabe fortschreitet. -- **Gespräche oder Aufgaben fortsetzen**: Führe mehrstufige Konversationen oder Workflows fort, indem du auf Aufgaben- und Kontext-IDs verweist. -- **Drittanbieter-KI und Automatisierung integrieren**: Nutze externe A2A-kompatible Dienste als Teil deiner Sim-Workflows. - -Diese Funktionen ermöglichen es dir, fortgeschrittene Workflows zu erstellen, die Sims native Fähigkeiten mit der Intelligenz und Automatisierung externer KIs oder benutzerdefinierter Agenten kombinieren. Um A2A-Integrationen zu nutzen, benötigst du die Endpunkt-URL des externen Agenten und, falls erforderlich, einen API-Schlüssel oder Zugangsdaten. -{/* MANUAL-CONTENT-END */} - -## Nutzungsanleitung - -Verwende das A2A-Protokoll (Agent-to-Agent), um mit externen KI-Agenten zu interagieren. - -## Tools - -### `a2a_send_message` - -Sende eine Nachricht an einen externen A2A-kompatiblen Agenten. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL | -| `message` | string | Ja | Nachricht, die an den Agenten gesendet werden soll | -| `taskId` | string | Nein | Aufgaben-ID zum Fortsetzen einer bestehenden Aufgabe | -| `contextId` | string | Nein | Kontext-ID für Gesprächskontinuität | -| `data` | string | Nein | Strukturierte Daten, die mit der Nachricht einbezogen werden sollen \(JSON-String\) | -| `files` | array | Nein | Dateien, die mit der Nachricht einbezogen werden sollen | -| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `content` | string | Textantwort-Inhalt vom Agenten | -| `taskId` | string | Eindeutige Aufgabenkennung | -| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten | -| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Ausgabe-Artefakte der Aufgabe | -| `history` | array | Gesprächsverlauf \(Message-Array\) | - -### `a2a_get_task` - -Abfrage des Status einer bestehenden A2A-Aufgabe. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL | -| `taskId` | string | Ja | Abzufragende Aufgaben-ID | -| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung | -| `historyLength` | number | Nein | Anzahl der einzubeziehenden Verlaufsnachrichten | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `taskId` | string | Eindeutige Aufgabenkennung | -| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten | -| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Ausgabe-Artefakte der Aufgabe | -| `history` | array | Gesprächsverlauf \(Message-Array\) | - -### `a2a_cancel_task` - -Abbrechen einer laufenden A2A-Aufgabe. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL | -| `taskId` | string | Ja | Abzubrechende Aufgaben-ID | -| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `cancelled` | boolean | Ob die Stornierung erfolgreich war | -| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) | - -### `a2a_get_agent_card` - -Ruft die Agent Card (Discovery-Dokument) für einen A2A-Agenten ab. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die Endpunkt-URL des A2A-Agenten | -| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung \(falls erforderlich\) | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `name` | string | Anzeigename des Agenten | -| `description` | string | Zweck/Fähigkeiten des Agenten | -| `url` | string | Service-Endpunkt-URL | -| `provider` | object | Details zur Ersteller-Organisation | -| `capabilities` | object | Feature-Support-Matrix | -| `skills` | array | Verfügbare Operationen | -| `version` | string | Vom Agenten unterstützte A2A-Protokollversion | -| `defaultInputModes` | array | Standard-Eingabe-Inhaltstypen, die vom Agenten akzeptiert werden | -| `defaultOutputModes` | array | Standard-Ausgabe-Inhaltstypen, die vom Agenten produziert werden | - -### `a2a_resubscribe` - -Stellt die Verbindung zu einem laufenden A2A-Task-Stream nach einer Verbindungsunterbrechung wieder her. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die Endpunkt-URL des A2A-Agenten | -| `taskId` | string | Ja | Task-ID, zu der erneut abonniert werden soll | -| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `taskId` | string | Eindeutige Aufgabenkennung | -| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten | -| `state` | string | Aktueller Lebenszyklusstatus \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `isRunning` | boolean | Ob die Aufgabe noch läuft | -| `artifacts` | array | Ausgabeartefakte der Aufgabe | -| `history` | array | Gesprächsverlauf \(Message-Array\) | - -### `a2a_set_push_notification` - -Konfigurieren Sie einen Webhook, um Benachrichtigungen über Aufgabenaktualisierungen zu erhalten. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL | -| `taskId` | string | Ja | Aufgaben-ID, für die Benachrichtigungen konfiguriert werden sollen | -| `webhookUrl` | string | Ja | HTTPS-Webhook-URL zum Empfang von Benachrichtigungen | -| `token` | string | Nein | Token zur Webhook-Validierung | -| `apiKey` | string | Nein | API-Schlüssel zur Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `url` | string | HTTPS-Webhook-URL für Benachrichtigungen | -| `token` | string | Authentifizierungstoken zur Webhook-Validierung | -| `success` | boolean | Ob der Vorgang erfolgreich war | - -### `a2a_get_push_notification` - -Rufen Sie die Push-Benachrichtigungs-Webhook-Konfiguration für eine Aufgabe ab. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL | -| `taskId` | string | Ja | Aufgaben-ID, für die die Benachrichtigungskonfiguration abgerufen werden soll | -| `apiKey` | string | Nein | API-Schlüssel zur Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `token` | string | Authentifizierungstoken für Webhook-Validierung | -| `exists` | boolean | Ob die Ressource existiert | - -### `a2a_delete_push_notification` - -Löscht die Push-Benachrichtigungs-Webhook-Konfiguration für eine Aufgabe. - -#### Eingabe - -| Parameter | Typ | Erforderlich | Beschreibung | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL | -| `taskId` | string | Ja | Aufgaben-ID, für die die Benachrichtigungskonfiguration gelöscht werden soll | -| `pushNotificationConfigId` | string | Nein | Push-Benachrichtigungskonfigurations-ID zum Löschen \(optional - Server kann aus taskId ableiten\) | -| `apiKey` | string | Nein | API-Schlüssel für Authentifizierung | - -#### Ausgabe - -| Parameter | Typ | Beschreibung | -| --------- | ---- | ----------- | -| `success` | boolean | Ob die Operation erfolgreich war | diff --git a/apps/docs/content/docs/en/integrations/logs.mdx b/apps/docs/content/docs/en/integrations/logs.mdx index 79dd12f2432..2295215662c 100644 --- a/apps/docs/content/docs/en/integrations/logs.mdx +++ b/apps/docs/content/docs/en/integrations/logs.mdx @@ -29,7 +29,7 @@ Query workflow run logs in the current workspace with the full Logs-page filter | `workflowIds` | string | No | Comma-separated workflow IDs to filter by | | `folderIds` | string | No | Comma-separated folder IDs to filter by \(descendants included\) | | `level` | string | No | Comma-separated statuses: 'info', 'error', 'running', 'pending', 'cancelled'. Omit for all. | -| `triggers` | string | No | Comma-separated trigger types \(api, webhook, schedule, manual, chat, mcp, a2a, workflow, sim, …\) | +| `triggers` | string | No | Comma-separated trigger types \(api, webhook, schedule, manual, chat, mcp, workflow, sim, …\) | | `startDate` | string | No | ISO 8601 timestamp; only runs at or after this time | | `endDate` | string | No | ISO 8601 timestamp; only runs at or before this time | | `search` | string | No | Free-text search across log fields | diff --git a/apps/docs/content/docs/en/logs-debugging/index.mdx b/apps/docs/content/docs/en/logs-debugging/index.mdx index bc111469978..df544382d9a 100644 --- a/apps/docs/content/docs/en/logs-debugging/index.mdx +++ b/apps/docs/content/docs/en/logs-debugging/index.mdx @@ -24,7 +24,7 @@ The **Logs page** lists every run across your workspace, one row per run. The [L ### The run -Each row on the Logs page is one **run**. It records the **trigger** that started it (manual, api, schedule, chat, webhook, mcp, mothership, copilot, workflow, or a2a), a **status**, a **duration**, and the **cost** in credits. It also carries an **execution ID** that uniquely names the run. +Each row on the Logs page is one **run**. It records the **trigger** that started it (manual, api, schedule, chat, webhook, mcp, mothership, copilot, or workflow), a **status**, a **duration**, and the **cost** in credits. It also carries an **execution ID** that uniquely names the run. The status shows the run's outcome at a glance, with failed runs badged **Error**. When you are hunting a failure, you filter the list to the errors and start there. diff --git a/apps/docs/content/docs/en/platform/enterprise/access-control.mdx b/apps/docs/content/docs/en/platform/enterprise/access-control.mdx index 7ae3ee72dc6..f71ad748aff 100644 --- a/apps/docs/content/docs/en/platform/enterprise/access-control.mdx +++ b/apps/docs/content/docs/en/platform/enterprise/access-control.mdx @@ -64,7 +64,7 @@ Controls which AI model providers members of this group can use. Controls which workflow blocks members can place and execute. - Blocks are split into two sections: **Core Blocks** (Agent, API, Condition, Function, etc.) and **Tools** (all integration blocks). + Blocks are split into two sections: **Core Blocks** (Agent, API, Condition, Function, etc.) and **Tools** (all integration blocks). - **All checked (default):** All blocks are allowed. - **Subset checked:** Only the selected blocks are allowed. Workflows that already contain a disallowed block will fail when run — they are not automatically modified. @@ -116,7 +116,6 @@ Controls visibility of platform features and modules. |---------|-------------------| | API | Hides the API deployment tab | | MCP | Hides the MCP deployment tab | -| A2A | Hides the A2A deployment tab | | Chat | Hides the Chat deployment tab | | Template | Hides the Template deployment tab | diff --git a/apps/docs/content/docs/en/workflows/deployment/index.mdx b/apps/docs/content/docs/en/workflows/deployment/index.mdx index 92bf4121d02..6975e45c9d2 100644 --- a/apps/docs/content/docs/en/workflows/deployment/index.mdx +++ b/apps/docs/content/docs/en/workflows/deployment/index.mdx @@ -7,7 +7,7 @@ pageType: concept import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' -A deployment is a published version of a [workflow](/workflows) that outside callers can run. While you build, a workflow lives on your canvas as a draft only you can run. Deploying publishes a fixed copy of that draft and gives it an address: a REST endpoint, a chat page, a set of [MCP](/workflows/deployment/mcp) tools, or an [A2A](/workflows/deployment/api) agent. Each is a **surface**, a channel callers reach the same published workflow through. +A deployment is a published version of a [workflow](/workflows) that outside callers can run. While you build, a workflow lives on your canvas as a draft only you can run. Deploying publishes a fixed copy of that draft and gives it an address: a REST endpoint, a chat page, or a set of [MCP](/workflows/deployment/mcp) tools. Each is a **surface**, a channel callers reach the same published workflow through. Like publishing a book, building and deploying are separate acts. You draft and revise freely on the canvas. When you click **Deploy**, Sim prints a fixed edition. Readers get that edition, not your latest draft, until you publish a new one. You can always go back to an earlier printing. @@ -60,13 +60,12 @@ curl -X POST https://sim.ai/api/workflows/{workflow-id}/execute \ -d '{ "input": "Refund request from customer #4821" }' ``` -{/* VISUAL: deploy modal tab bar. General · API · MCP · A2A · Chat. */} +{/* VISUAL: deploy modal tab bar. General · API · MCP · Chat. */} - ## Versioning in practice diff --git a/apps/docs/content/docs/es/tools/a2a.mdx b/apps/docs/content/docs/es/tools/a2a.mdx deleted file mode 100644 index 9a5399da50b..00000000000 --- a/apps/docs/content/docs/es/tools/a2a.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: A2A -description: Interactúa con agentes externos compatibles con A2A ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -El protocolo A2A (Agent-to-Agent) permite a Sim interactuar con agentes de IA externos y sistemas que implementan APIs compatibles con A2A. Con A2A, puedes conectar las automatizaciones y flujos de trabajo de Sim a agentes remotos—como bots potenciados por LLM, microservicios y otras herramientas basadas en IA—utilizando un formato de mensajería estandarizado. - -Usando las herramientas A2A en Sim, puedes: - -- **Enviar mensajes a agentes externos**: Comunícate directamente con agentes remotos, proporcionando prompts, comandos o datos. -- **Recibir y transmitir respuestas**: Obtén respuestas estructuradas, artefactos o actualizaciones en tiempo real del agente a medida que avanza la tarea. -- **Continuar conversaciones o tareas**: Mantén conversaciones o flujos de trabajo de múltiples turnos haciendo referencia a IDs de tarea y contexto. -- **Integrar IA y automatización de terceros**: Aprovecha servicios externos compatibles con A2A como parte de tus flujos de trabajo en Sim. - -Estas funcionalidades te permiten construir flujos de trabajo avanzados que combinan las capacidades nativas de Sim con la inteligencia y automatización de IAs externas o agentes personalizados. Para usar integraciones A2A, necesitarás la URL del endpoint del agente externo y, si es necesario, una clave API o credenciales. -{/* MANUAL-CONTENT-END */} - -## Instrucciones de uso - -Usa el protocolo A2A (Agent-to-Agent) para interactuar con agentes de IA externos. - -## Herramientas - -### `a2a_send_message` - -Envía un mensaje a un agente externo compatible con A2A. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `message` | string | Sí | Mensaje para enviar al agente | -| `taskId` | string | No | ID de tarea para continuar una tarea existente | -| `contextId` | string | No | ID de contexto para continuidad de conversación | -| `data` | string | No | Datos estructurados para incluir con el mensaje \(cadena JSON\) | -| `files` | array | No | Archivos para incluir con el mensaje | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `content` | string | Contenido de respuesta de texto del agente | -| `taskId` | string | Identificador único de tarea | -| `contextId` | string | Agrupa tareas/mensajes relacionados | -| `state` | string | Estado actual del ciclo de vida \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Artefactos de salida de la tarea | -| `history` | array | Historial de conversación \(array de mensajes\) | - -### `a2a_get_task` - -Consulta el estado de una tarea A2A existente. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea a consultar | -| `apiKey` | string | No | Clave API para autenticación | -| `historyLength` | number | No | Número de mensajes del historial a incluir | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `taskId` | string | Identificador único de tarea | -| `contextId` | string | Agrupa tareas/mensajes relacionados | -| `state` | string | Estado actual del ciclo de vida \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Artefactos de salida de la tarea | -| `history` | array | Historial de conversación \(array de mensajes\) | - -### `a2a_cancel_task` - -Cancela una tarea A2A en ejecución. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea a cancelar | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `cancelled` | boolean | Si la cancelación fue exitosa | -| `state` | string | Estado actual del ciclo de vida \(working, completed, failed, canceled, rejected, input_required, auth_required\) | - -### `a2a_get_agent_card` - -Obtener la tarjeta del agente (documento de descubrimiento) para un agente A2A. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `apiKey` | string | No | Clave API para autenticación \(si es requerida\) | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `name` | string | Nombre para mostrar del agente | -| `description` | string | Propósito/capacidades del agente | -| `url` | string | URL del endpoint del servicio | -| `provider` | object | Detalles de la organización creadora | -| `capabilities` | object | Matriz de soporte de características | -| `skills` | array | Operaciones disponibles | -| `version` | string | Versión del protocolo A2A soportada por el agente | -| `defaultInputModes` | array | Tipos de contenido de entrada predeterminados aceptados por el agente | -| `defaultOutputModes` | array | Tipos de contenido de salida predeterminados producidos por el agente | - -### `a2a_resubscribe` - -Reconectar a un flujo de tarea A2A en curso después de una interrupción de conexión. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea para resuscribirse | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `taskId` | string | Identificador único de tarea | -| `contextId` | string | Agrupa tareas/mensajes relacionados | -| `state` | string | Estado actual del ciclo de vida \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `isRunning` | boolean | Si la tarea aún se está ejecutando | -| `artifacts` | array | Artefactos de salida de la tarea | -| `history` | array | Historial de conversación \(array de mensajes\) | - -### `a2a_set_push_notification` - -Configura un webhook para recibir notificaciones de actualización de tareas. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea para configurar notificaciones | -| `webhookUrl` | string | Sí | URL del webhook HTTPS para recibir notificaciones | -| `token` | string | No | Token para validación del webhook | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `url` | string | URL del webhook HTTPS para notificaciones | -| `token` | string | Token de autenticación para validación del webhook | -| `success` | boolean | Si la operación fue exitosa | - -### `a2a_get_push_notification` - -Obtiene la configuración del webhook de notificaciones push para una tarea. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea para obtener la configuración de notificaciones | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `token` | string | Token de autenticación para validación de webhook | -| `exists` | boolean | Si el recurso existe | - -### `a2a_delete_push_notification` - -Elimina la configuración de webhook de notificaciones push para una tarea. - -#### Entrada - -| Parámetro | Tipo | Requerido | Descripción | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Sí | La URL del endpoint del agente A2A | -| `taskId` | string | Sí | ID de tarea para eliminar la configuración de notificación | -| `pushNotificationConfigId` | string | No | ID de configuración de notificación push a eliminar \(opcional - el servidor puede derivarlo del taskId\) | -| `apiKey` | string | No | Clave API para autenticación | - -#### Salida - -| Parámetro | Tipo | Descripción | -| --------- | ---- | ----------- | -| `success` | boolean | Si la operación fue exitosa | diff --git a/apps/docs/content/docs/fr/tools/a2a.mdx b/apps/docs/content/docs/fr/tools/a2a.mdx deleted file mode 100644 index 710db594392..00000000000 --- a/apps/docs/content/docs/fr/tools/a2a.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: A2A -description: Interagir avec des agents externes compatibles A2A ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -Le protocole A2A (Agent-to-Agent) permet à Sim d'interagir avec des agents IA externes et des systèmes qui implémentent des API compatibles A2A. Avec A2A, vous pouvez connecter les automatisations et workflows de Sim à des agents distants — tels que des bots alimentés par LLM, des microservices et d'autres outils basés sur l'IA — en utilisant un format de messagerie standardisé. - -En utilisant les outils A2A dans Sim, vous pouvez : - -- **Envoyer des messages à des agents externes** : communiquer directement avec des agents distants, en fournissant des invites, des commandes ou des données. -- **Recevoir et diffuser des réponses** : obtenir des réponses structurées, des artefacts ou des mises à jour en temps réel de l'agent au fur et à mesure de la progression de la tâche. -- **Poursuivre des conversations ou des tâches** : continuer des conversations ou des workflows multi-tours en référençant les ID de tâche et de contexte. -- **Intégrer l'IA et l'automatisation tierces** : exploiter des services externes compatibles A2A dans le cadre de vos workflows Sim. - -Ces fonctionnalités vous permettent de créer des workflows avancés qui combinent les capacités natives de Sim avec l'intelligence et l'automatisation d'IA externes ou d'agents personnalisés. Pour utiliser les intégrations A2A, vous aurez besoin de l'URL du point de terminaison de l'agent externe et, si nécessaire, d'une clé API ou d'identifiants. -{/* MANUAL-CONTENT-END */} - -## Instructions d'utilisation - -Utilisez le protocole A2A (Agent-to-Agent) pour interagir avec des agents IA externes. - -## Outils - -### `a2a_send_message` - -Envoyer un message à un agent externe compatible A2A. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `message` | string | Oui | Message à envoyer à l'agent | -| `taskId` | string | Non | ID de tâche pour continuer une tâche existante | -| `contextId` | string | Non | ID de contexte pour la continuité de la conversation | -| `data` | string | Non | Données structurées à inclure avec le message \(chaîne JSON\) | -| `files` | array | Non | Fichiers à inclure avec le message | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Contenu de la réponse textuelle de l'agent | -| `taskId` | string | Identifiant unique de la tâche | -| `contextId` | string | Regroupe les tâches/messages associés | -| `state` | string | État actuel du cycle de vie \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Artefacts de sortie de la tâche | -| `history` | array | Historique de la conversation \(tableau de messages\) | - -### `a2a_get_task` - -Interroger le statut d'une tâche A2A existante. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche à interroger | -| `apiKey` | string | Non | Clé API pour l'authentification | -| `historyLength` | number | Non | Nombre de messages d'historique à inclure | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `taskId` | string | Identifiant unique de la tâche | -| `contextId` | string | Regroupe les tâches/messages associés | -| `state` | string | État actuel du cycle de vie \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `artifacts` | array | Artefacts de sortie de la tâche | -| `history` | array | Historique de la conversation \(tableau de messages\) | - -### `a2a_cancel_task` - -Annuler une tâche A2A en cours d'exécution. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche à annuler | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `cancelled` | boolean | Indique si l'annulation a réussi | -| `state` | string | État actuel du cycle de vie \(working, completed, failed, canceled, rejected, input_required, auth_required\) | - -### `a2a_get_agent_card` - -Récupère la carte d'agent (document de découverte) pour un agent A2A. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `apiKey` | string | Non | Clé API pour l'authentification \(si nécessaire\) | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `name` | string | Nom d'affichage de l'agent | -| `description` | string | Objectif/capacités de l'agent | -| `url` | string | URL du point de terminaison du service | -| `provider` | object | Détails de l'organisation créatrice | -| `capabilities` | object | Matrice de prise en charge des fonctionnalités | -| `skills` | array | Opérations disponibles | -| `version` | string | Version du protocole A2A prise en charge par l'agent | -| `defaultInputModes` | array | Types de contenu d'entrée par défaut acceptés par l'agent | -| `defaultOutputModes` | array | Types de contenu de sortie par défaut produits par l'agent | - -### `a2a_resubscribe` - -Se reconnecte à un flux de tâche A2A en cours après une interruption de connexion. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche à laquelle se réabonner | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `taskId` | string | Identifiant unique de la tâche | -| `contextId` | string | Regroupe les tâches/messages associés | -| `state` | string | État actuel du cycle de vie \(working, completed, failed, canceled, rejected, input_required, auth_required\) | -| `isRunning` | boolean | Indique si la tâche est toujours en cours d'exécution | -| `artifacts` | array | Artefacts de sortie de la tâche | -| `history` | array | Historique de la conversation \(tableau de messages\) | - -### `a2a_set_push_notification` - -Configurez un webhook pour recevoir les notifications de mise à jour des tâches. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche pour laquelle configurer les notifications | -| `webhookUrl` | string | Oui | URL du webhook HTTPS pour recevoir les notifications | -| `token` | string | Non | Jeton pour la validation du webhook | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `url` | string | URL du webhook HTTPS pour les notifications | -| `token` | string | Jeton d'authentification pour la validation du webhook | -| `success` | boolean | Indique si l'opération a réussi | - -### `a2a_get_push_notification` - -Obtenez la configuration du webhook de notification push pour une tâche. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche pour laquelle obtenir la configuration des notifications | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `token` | string | Jeton d'authentification pour la validation du webhook | -| `exists` | boolean | Indique si la ressource existe | - -### `a2a_delete_push_notification` - -Supprime la configuration du webhook de notification push pour une tâche. - -#### Entrée - -| Paramètre | Type | Requis | Description | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | Oui | L'URL du point de terminaison de l'agent A2A | -| `taskId` | string | Oui | ID de la tâche pour laquelle supprimer la configuration de notification | -| `pushNotificationConfigId` | string | Non | ID de la configuration de notification push à supprimer \(optionnel - le serveur peut le déduire du taskId\) | -| `apiKey` | string | Non | Clé API pour l'authentification | - -#### Sortie - -| Paramètre | Type | Description | -| --------- | ---- | ----------- | -| `success` | boolean | Indique si l'opération a réussi | diff --git a/apps/docs/content/docs/ja/tools/a2a.mdx b/apps/docs/content/docs/ja/tools/a2a.mdx deleted file mode 100644 index cf845ecc161..00000000000 --- a/apps/docs/content/docs/ja/tools/a2a.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: A2A -description: 外部のA2A互換エージェントと連携 ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -A2A(Agent-to-Agent)プロトコルにより、SimはA2A互換APIを実装した外部AIエージェントやシステムと連携できます。A2Aを使用すると、Simの自動化やワークフローを、LLM駆動のボット、マイクロサービス、その他のAIベースのツールなどのリモートエージェントに、標準化されたメッセージ形式で接続できます。 - -SimのA2Aツールを使用すると、次のことができます。 - -- **外部エージェントへのメッセージ送信**: リモートエージェントと直接通信し、プロンプト、コマンド、データを提供します。 -- **レスポンスの受信とストリーミング**: タスクの進行に応じて、エージェントから構造化されたレスポンス、アーティファクト、リアルタイム更新を取得します。 -- **会話やタスクの継続**: タスクIDとコンテキストIDを参照して、複数ターンの会話やワークフローを継続します。 -- **サードパーティAIと自動化の統合**: 外部のA2A互換サービスをSimワークフローの一部として活用します。 - -これらの機能により、Simのネイティブ機能と外部AIやカスタムエージェントのインテリジェンスと自動化を組み合わせた高度なワークフローを構築できます。A2A統合を使用するには、外部エージェントのエンドポイントURLと、必要に応じてAPIキーまたは認証情報が必要です。 -{/* MANUAL-CONTENT-END */} - -## 使用方法 - -A2A(Agent-to-Agent)プロトコルを使用して、外部AIエージェントと連携します。 - -## ツール - -### `a2a_send_message` - -外部のA2A互換エージェントにメッセージを送信します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2Aエージェントのエンドポイント URL | -| `message` | string | はい | エージェントに送信するメッセージ | -| `taskId` | string | いいえ | 既存のタスクを継続するためのタスクID | -| `contextId` | string | いいえ | 会話の継続性のためのコンテキストID | -| `data` | string | いいえ | メッセージに含める構造化データ(JSON文字列) | -| `files` | array | いいえ | メッセージに含めるファイル | -| `apiKey` | string | いいえ | 認証用のAPIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `content` | string | エージェントからのテキストレスポンスコンテンツ | -| `taskId` | string | 一意のタスク識別子 | -| `contextId` | string | 関連するタスク/メッセージをグループ化 | -| `state` | string | 現在のライフサイクル状態\(working、completed、failed、canceled、rejected、input_required、auth_required\) | -| `artifacts` | array | タスク出力アーティファクト | -| `history` | array | 会話履歴\(メッセージ配列\) | - -### `a2a_get_task` - -既存のA2Aタスクのステータスを照会します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | 照会するタスクID | -| `apiKey` | string | いいえ | 認証用のAPIキー | -| `historyLength` | number | いいえ | 含める履歴メッセージの数 | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `taskId` | string | 一意のタスク識別子 | -| `contextId` | string | 関連するタスク/メッセージをグループ化 | -| `state` | string | 現在のライフサイクル状態\(working、completed、failed、canceled、rejected、input_required、auth_required\) | -| `artifacts` | array | タスク出力アーティファクト | -| `history` | array | 会話履歴\(メッセージ配列\) | - -### `a2a_cancel_task` - -実行中のA2Aタスクをキャンセルします。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | キャンセルするタスクID | -| `apiKey` | string | いいえ | 認証用のAPIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `cancelled` | boolean | キャンセルが成功したかどうか | -| `state` | string | 現在のライフサイクル状態(working、completed、failed、canceled、rejected、input_required、auth_required) | - -### `a2a_get_agent_card` - -A2Aエージェントのエージェントカード(ディスカバリードキュメント)を取得します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `apiKey` | string | いいえ | 認証用のAPIキー(必要な場合) | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `name` | string | エージェントの表示名 | -| `description` | string | エージェントの目的/機能 | -| `url` | string | サービスエンドポイントURL | -| `provider` | object | 作成者組織の詳細 | -| `capabilities` | object | 機能サポートマトリックス | -| `skills` | array | 利用可能な操作 | -| `version` | string | エージェントがサポートするA2Aプロトコルバージョン | -| `defaultInputModes` | array | エージェントが受け入れるデフォルトの入力コンテンツタイプ | -| `defaultOutputModes` | array | エージェントが生成するデフォルトの出力コンテンツタイプ | - -### `a2a_resubscribe` - -接続中断後、進行中のA2Aタスクストリームに再接続します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | 再サブスクライブするタスクID | -| `apiKey` | string | いいえ | 認証用のAPIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `taskId` | string | 一意のタスク識別子 | -| `contextId` | string | 関連するタスク/メッセージをグループ化 | -| `state` | string | 現在のライフサイクル状態 \(working、completed、failed、canceled、rejected、input_required、auth_required\) | -| `isRunning` | boolean | タスクが実行中かどうか | -| `artifacts` | array | タスク出力アーティファクト | -| `history` | array | 会話履歴 \(メッセージ配列\) | - -### `a2a_set_push_notification` - -タスク更新通知を受信するためのWebhookを設定します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | 通知を設定するタスクID | -| `webhookUrl` | string | はい | 通知を受信するHTTPS Webhook URL | -| `token` | string | いいえ | Webhook検証用トークン | -| `apiKey` | string | いいえ | 認証用APIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `url` | string | 通知用HTTPS Webhook URL | -| `token` | string | Webhook検証用認証トークン | -| `success` | boolean | 操作が成功したかどうか | - -### `a2a_get_push_notification` - -タスクのプッシュ通知Webhook設定を取得します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | 通知設定を取得するタスクID | -| `apiKey` | string | いいえ | 認証用APIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `token` | string | Webhook検証用の認証トークン | -| `exists` | boolean | リソースが存在するかどうか | - -### `a2a_delete_push_notification` - -タスクのプッシュ通知Webhook設定を削除します。 - -#### 入力 - -| パラメータ | 型 | 必須 | 説明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | はい | A2AエージェントのエンドポイントURL | -| `taskId` | string | はい | 通知設定を削除するタスクID | -| `pushNotificationConfigId` | string | いいえ | 削除するプッシュ通知設定ID(オプション - サーバーはtaskIdから導出可能) | -| `apiKey` | string | いいえ | 認証用のAPIキー | - -#### 出力 - -| パラメータ | 型 | 説明 | -| --------- | ---- | ----------- | -| `success` | boolean | 操作が成功したかどうか | diff --git a/apps/docs/content/docs/zh/tools/a2a.mdx b/apps/docs/content/docs/zh/tools/a2a.mdx deleted file mode 100644 index f7c9b55bc28..00000000000 --- a/apps/docs/content/docs/zh/tools/a2a.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: A2A -description: 与外部 A2A 兼容代理进行交互 ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -A2A(Agent-to-Agent,代理对代理)协议使 Sim 能够与实现了 A2A 兼容 API 的外部 AI 代理和系统进行交互。通过 A2A,您可以将 Sim 的自动化和工作流连接到远程代理——如 LLM 驱动的机器人、微服务和其他基于 AI 的工具——并使用标准化的消息格式进行通信。 - -在 Sim 中使用 A2A 工具,您可以: - -- **向外部代理发送消息**:直接与远程代理通信,提供提示、指令或数据。 -- **接收和流式响应**:在任务进行过程中,从代理获取结构化响应、产物或实时更新。 -- **继续对话或任务**:通过引用任务和上下文 ID,持续多轮对话或工作流。 -- **集成第三方 AI 与自动化**:将外部 A2A 兼容服务作为 Sim 工作流的一部分进行集成。 - -这些功能让您能够构建高级工作流,将 Sim 的原生能力与外部 AI 或自定义代理的智能和自动化相结合。要使用 A2A 集成,您需要外部代理的 endpoint URL,如果需要,还需提供 API key 或凭证。 -{/* MANUAL-CONTENT-END */} - -## 使用说明 - -使用 A2A(Agent-to-Agent,代理对代理)协议与外部 AI 代理进行交互。 - -## 工具 - -### `a2a_send_message` - -向外部 A2A 兼容代理发送消息。 - -#### 输入 - -| 参数 | 类型 | 必填 | 说明 | -| --------- | ---- | -------- | ----------- | -| `agentUrl` | string | 是 | A2A 代理 endpoint URL | -| `message` | string | 是 | 发送给代理的消息 | -| `taskId` | string | 否 | 用于继续现有任务的 Task ID | -| `contextId` | string | 否 | 用于对话连续性的 Context ID | -| `data` | string | 否 | 随消息附带的结构化数据(JSON 字符串) | -| `files` | array | 否 | 随消息附带的文件 | -| `apiKey` | string | 否 | 用于身份验证的 API key | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `content` | string | 来自 agent 的文本响应内容 | -| `taskId` | string | 任务唯一标识符 | -| `contextId` | string | 相关任务/消息的分组 | -| `state` | string | 当前生命周期状态(working、completed、failed、canceled、rejected、input_required、auth_required) | -| `artifacts` | array | 任务输出产物 | -| `history` | array | 会话历史(消息数组) | - -### `a2a_get_task` - -查询现有 A2A 任务的状态。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ------ | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `taskId` | string | 是 | 要查询的任务 ID | -| `apiKey` | string | 否 | 用于身份验证的 API key | -| `historyLength` | number | 否 | 要包含的历史消息数量 | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `taskId` | string | 任务唯一标识符 | -| `contextId` | string | 相关任务/消息的分组 | -| `state` | string | 当前生命周期状态(working、completed、failed、canceled、rejected、input_required、auth_required) | -| `artifacts` | array | 任务输出产物 | -| `history` | array | 会话历史(消息数组) | - -### `a2a_cancel_task` - -取消正在运行的 A2A 任务。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ------ | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `taskId` | string | 是 | 要取消的任务 ID | -| `apiKey` | string | 否 | 用于身份验证的 API key | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `cancelled` | boolean | 是否取消成功 | -| `state` | string | 当前生命周期状态(working、completed、failed、canceled、rejected、input_required、auth_required) | - -### `a2a_get_agent_card` - -获取 A2A agent 的 Agent Card(发现文档)。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ---- | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `apiKey` | string | 否 | 用于认证的 API key(如需) | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `name` | string | Agent 显示名称 | -| `description` | string | Agent 目的/能力 | -| `url` | string | 服务端点 URL | -| `provider` | object | 创建组织详情 | -| `capabilities` | object | 功能支持矩阵 | -| `skills` | array | 可用操作 | -| `version` | string | Agent 支持的 A2A 协议版本 | -| `defaultInputModes` | array | Agent 默认接受的输入内容类型 | -| `defaultOutputModes` | array | Agent 默认输出的内容类型 | - -### `a2a_resubscribe` - -在连接中断后,重新连接到正在进行的 A2A 任务流。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ---- | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `taskId` | string | 是 | 要重新订阅的任务 ID | -| `apiKey` | string | 否 | 用于认证的 API key | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `taskId` | string | 任务唯一标识符 | -| `contextId` | string | 相关任务/消息的分组 | -| `state` | string | 当前生命周期状态(working、completed、failed、canceled、rejected、input_required、auth_required) | -| `isRunning` | boolean | 任务是否仍在运行 | -| `artifacts` | array | 任务输出产物 | -| `history` | array | 会话历史(消息数组) | - -### `a2a_set_push_notification` - -配置 webhook 以接收任务更新通知。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ---- | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `taskId` | string | 是 | 要配置通知的任务 ID | -| `webhookUrl` | string | 是 | 用于接收通知的 HTTPS webhook URL | -| `token` | string | 否 | webhook 验证用的令牌 | -| `apiKey` | string | 否 | 身份验证用 API key | - -#### 输出 - -| 参数 | 类型 | 描述 | -| --------- | ---- | ----------- | -| `url` | string | 用于通知的 HTTPS webhook URL | -| `token` | string | webhook 验证用的身份令牌 | -| `success` | boolean | 操作是否成功 | - -### `a2a_get_push_notification` - -获取任务的推送通知 webhook 配置。 - -#### 输入 - -| 参数 | 类型 | 必填 | 描述 | -| --------- | ---- | ---- | ----------- | -| `agentUrl` | string | 是 | A2A agent 端点 URL | -| `taskId` | string | 是 | 要获取通知配置的任务 ID | -| `apiKey` | string | 否 | 身份验证用 API key | - -#### 输出 - -| 参数 | 类型 | 说明 | -| --------- | ---- | ----------- | -| `token` | string | 用于 webhook 验证的认证令牌 | -| `exists` | boolean | 资源是否存在 | - -### `a2a_delete_push_notification` - -删除任务的推送通知 webhook 配置。 - -#### 输入 - -| 参数 | 类型 | 必填 | 说明 | -| --------- | ---- | ------ | ----------- | -| `agentUrl` | string | 是 | A2A 代理端点 URL | -| `taskId` | string | 是 | 要删除通知配置的任务 ID | -| `pushNotificationConfigId` | string | 否 | 要删除的推送通知配置 ID(可选 - 服务器可根据 taskId 推断)| -| `apiKey` | string | 否 | 用于认证的 API key | - -#### 输出 - -| 参数 | 类型 | 说明 | -| --------- | ---- | ----------- | -| `success` | boolean | 操作是否成功 | diff --git a/apps/sim/app/api/a2a/agents/[agentId]/route.ts b/apps/sim/app/api/a2a/agents/[agentId]/route.ts deleted file mode 100644 index bf05379f861..00000000000 --- a/apps/sim/app/api/a2a/agents/[agentId]/route.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { db } from '@sim/db' -import { a2aAgent, workflow } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, eq, isNull } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card' -import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types' -import { - a2aAgentParamsSchema, - publishA2AAgentContract, - updateA2AAgentContract, -} from '@/lib/api/contracts/a2a-agents' -import { parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { getRedisClient } from '@/lib/core/config/redis' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('A2AAgentCardAPI') - -export const dynamic = 'force-dynamic' - -interface RouteParams { - agentId: string -} - -/** - * GET - Returns the Agent Card for discovery - */ -export const GET = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aAgentParamsSchema.parse(await params) - - try { - const [agent] = await db - .select({ - agent: a2aAgent, - workflow: workflow, - }) - .from(a2aAgent) - .innerJoin(workflow, and(eq(a2aAgent.workflowId, workflow.id), isNull(workflow.archivedAt))) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!agent) { - return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) - } - - if (!agent.agent.isPublished) { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: 'Agent not published' }, { status: 404 }) - } - - const workspaceAccess = await checkWorkspaceAccess(agent.agent.workspaceId, auth.userId) - if (!workspaceAccess.exists || !workspaceAccess.hasAccess) { - return NextResponse.json({ error: 'Agent not published' }, { status: 404 }) - } - } - - const agentCard = generateAgentCard( - { - id: agent.agent.id, - name: agent.agent.name, - description: agent.agent.description, - version: agent.agent.version, - capabilities: agent.agent.capabilities as AgentCapabilities, - skills: agent.agent.skills as AgentSkill[], - }, - { - id: agent.workflow.id, - name: agent.workflow.name, - description: agent.workflow.description, - } - ) - - return NextResponse.json(agentCard, { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': agent.agent.isPublished ? 'public, max-age=3600' : 'private, no-cache', - }, - }) - } catch (error) { - logger.error('Error getting Agent Card:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) - -/** - * PUT - Update an agent - */ -export const PUT = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aAgentParamsSchema.parse(await params) - - try { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const [existingAgent] = await db - .select() - .from(a2aAgent) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!existingAgent) { - return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) - } - - const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId) - if (!workspaceAccess.canWrite) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const parsed = await parseRequest(updateA2AAgentContract, request, { params }) - if (!parsed.success) return parsed.response - const body = parsed.data.body - - let skills = body.skills ?? existingAgent.skills - if (body.skillTags !== undefined) { - const agentName = body.name ?? existingAgent.name - const agentDescription = body.description ?? existingAgent.description - skills = generateSkillsFromWorkflow(agentName, agentDescription, body.skillTags) - } - - const [updatedAgent] = await db - .update(a2aAgent) - .set({ - name: body.name ?? existingAgent.name, - description: body.description ?? existingAgent.description, - version: body.version ?? existingAgent.version, - capabilities: body.capabilities ?? existingAgent.capabilities, - skills, - authentication: body.authentication ?? existingAgent.authentication, - isPublished: body.isPublished ?? existingAgent.isPublished, - publishedAt: - body.isPublished && !existingAgent.isPublished ? new Date() : existingAgent.publishedAt, - updatedAt: new Date(), - }) - .where(eq(a2aAgent.id, agentId)) - .returning() - - logger.info(`Updated A2A agent: ${agentId}`) - - return NextResponse.json({ success: true, agent: updatedAgent }) - } catch (error) { - logger.error('Error updating agent:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) - -/** - * DELETE - Delete an agent - */ -export const DELETE = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aAgentParamsSchema.parse(await params) - - try { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const [existingAgent] = await db - .select() - .from(a2aAgent) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!existingAgent) { - return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) - } - - const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId) - if (!workspaceAccess.canWrite) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId)) - - logger.info(`Deleted A2A agent: ${agentId}`) - - captureServerEvent( - auth.userId, - 'a2a_agent_deleted', - { - agent_id: agentId, - workflow_id: existingAgent.workflowId, - workspace_id: existingAgent.workspaceId, - }, - { groups: { workspace: existingAgent.workspaceId } } - ) - - return NextResponse.json({ success: true }) - } catch (error) { - logger.error('Error deleting agent:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) - -/** - * POST - Publish/unpublish an agent - */ -export const POST = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aAgentParamsSchema.parse(await params) - - try { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - logger.warn('A2A agent publish auth failed:', { - error: auth.error, - hasUserId: !!auth.userId, - }) - return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) - } - - const [existingAgent] = await db - .select() - .from(a2aAgent) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!existingAgent) { - return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) - } - - const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId) - if (!workspaceAccess.canWrite) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const parsed = await parseRequest(publishA2AAgentContract, request, { params }) - if (!parsed.success) return parsed.response - const { action } = parsed.data.body - - if (action === 'publish') { - const [wf] = await db - .select({ isDeployed: workflow.isDeployed }) - .from(workflow) - .where(eq(workflow.id, existingAgent.workflowId)) - .limit(1) - - if (!wf?.isDeployed) { - return NextResponse.json( - { error: 'Workflow must be deployed before publishing agent' }, - { status: 400 } - ) - } - - await db - .update(a2aAgent) - .set({ - isPublished: true, - publishedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aAgent.id, agentId)) - - const redis = getRedisClient() - if (redis) { - try { - await redis.del(`a2a:agent:${agentId}:card`) - } catch (err) { - logger.warn('Failed to invalidate agent card cache', { agentId, error: err }) - } - } - - logger.info(`Published A2A agent: ${agentId}`) - captureServerEvent( - auth.userId, - 'a2a_agent_published', - { - agent_id: agentId, - workflow_id: existingAgent.workflowId, - workspace_id: existingAgent.workspaceId, - }, - { groups: { workspace: existingAgent.workspaceId } } - ) - return NextResponse.json({ success: true, isPublished: true }) - } - - if (action === 'unpublish') { - await db - .update(a2aAgent) - .set({ - isPublished: false, - updatedAt: new Date(), - }) - .where(eq(a2aAgent.id, agentId)) - - const redis = getRedisClient() - if (redis) { - try { - await redis.del(`a2a:agent:${agentId}:card`) - } catch (err) { - logger.warn('Failed to invalidate agent card cache', { agentId, error: err }) - } - } - - logger.info(`Unpublished A2A agent: ${agentId}`) - captureServerEvent( - auth.userId, - 'a2a_agent_unpublished', - { - agent_id: agentId, - workflow_id: existingAgent.workflowId, - workspace_id: existingAgent.workspaceId, - }, - { groups: { workspace: existingAgent.workspaceId } } - ) - return NextResponse.json({ success: true, isPublished: false }) - } - - if (action === 'refresh') { - const workflowData = await loadWorkflowFromNormalizedTables(existingAgent.workflowId) - if (!workflowData) { - return NextResponse.json({ error: 'Failed to load workflow' }, { status: 500 }) - } - - const [wf] = await db - .select({ name: workflow.name, description: workflow.description }) - .from(workflow) - .where(eq(workflow.id, existingAgent.workflowId)) - .limit(1) - - const skills = generateSkillsFromWorkflow(wf?.name || existingAgent.name, wf?.description) - - await db - .update(a2aAgent) - .set({ - skills, - updatedAt: new Date(), - }) - .where(eq(a2aAgent.id, agentId)) - - logger.info(`Refreshed skills for A2A agent: ${agentId}`) - return NextResponse.json({ success: true, skills }) - } - - return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) - } catch (error) { - logger.error('Error with agent action:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) diff --git a/apps/sim/app/api/a2a/agents/route.ts b/apps/sim/app/api/a2a/agents/route.ts deleted file mode 100644 index ee07fb74a78..00000000000 --- a/apps/sim/app/api/a2a/agents/route.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * A2A Agents List Endpoint - * - * List and create A2A agents for a workspace. - */ - -import { db } from '@sim/db' -import { a2aAgent, workflow } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, eq, isNull, sql } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card' -import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants' -import { sanitizeAgentName } from '@/lib/a2a/utils' -import { createA2AAgentContract, listA2AAgentsQuerySchema } from '@/lib/api/contracts/a2a-agents' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' -import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('A2AAgentsAPI') - -export const dynamic = 'force-dynamic' - -/** - * GET - List all A2A agents for a workspace - */ -export const GET = withRouteHandler(async (request: NextRequest) => { - try { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const queryResult = listA2AAgentsQuerySchema.safeParse({ - workspaceId: request.nextUrl.searchParams.get('workspaceId'), - }) - - if (!queryResult.success) { - return NextResponse.json( - { error: getValidationErrorMessage(queryResult.error) }, - { status: 400 } - ) - } - const { workspaceId } = queryResult.data - - const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId) - if (!workspaceAccess.exists) { - return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) - } - if (!workspaceAccess.hasAccess) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const agents = await db - .select({ - id: a2aAgent.id, - workspaceId: a2aAgent.workspaceId, - workflowId: a2aAgent.workflowId, - name: a2aAgent.name, - description: a2aAgent.description, - version: a2aAgent.version, - capabilities: a2aAgent.capabilities, - skills: a2aAgent.skills, - authentication: a2aAgent.authentication, - isPublished: a2aAgent.isPublished, - publishedAt: a2aAgent.publishedAt, - createdAt: a2aAgent.createdAt, - updatedAt: a2aAgent.updatedAt, - workflowName: workflow.name, - workflowDescription: workflow.description, - isDeployed: workflow.isDeployed, - taskCount: sql`( - SELECT COUNT(*)::int - FROM "a2a_task" - WHERE "a2a_task"."agent_id" = "a2a_agent"."id" - )`.as('task_count'), - }) - .from(a2aAgent) - .leftJoin(workflow, and(eq(a2aAgent.workflowId, workflow.id), isNull(workflow.archivedAt))) - .where(and(eq(a2aAgent.workspaceId, workspaceId), isNull(a2aAgent.archivedAt))) - .orderBy(a2aAgent.createdAt) - - logger.info(`Listed ${agents.length} A2A agents for workspace ${workspaceId}`) - - return NextResponse.json({ success: true, agents }) - } catch (error) { - logger.error('Error listing agents:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) - -/** - * POST - Create a new A2A agent from a workflow - */ -export const POST = withRouteHandler(async (request: NextRequest) => { - try { - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const parsed = await parseRequest(createA2AAgentContract, request, {}) - if (!parsed.success) return parsed.response - - const { workspaceId, workflowId, name, description, capabilities, authentication, skillTags } = - parsed.data.body - - const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId) - if (!workspaceAccess.exists) { - return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) - } - if (!workspaceAccess.canWrite) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const [wf] = await db - .select({ - id: workflow.id, - name: workflow.name, - description: workflow.description, - workspaceId: workflow.workspaceId, - isDeployed: workflow.isDeployed, - }) - .from(workflow) - .where( - and( - eq(workflow.id, workflowId), - eq(workflow.workspaceId, workspaceId), - isNull(workflow.archivedAt) - ) - ) - .limit(1) - - if (!wf) { - return NextResponse.json( - { error: 'Workflow not found or does not belong to workspace' }, - { status: 404 } - ) - } - - const workflowData = await loadWorkflowFromNormalizedTables(workflowId) - if (!workflowData || !hasValidStartBlockInState(workflowData)) { - return NextResponse.json( - { error: 'Workflow must have a Start block to be exposed as an A2A agent' }, - { status: 400 } - ) - } - - const [existing] = await db - .select({ id: a2aAgent.id }) - .from(a2aAgent) - .where( - and( - eq(a2aAgent.workspaceId, workspaceId), - eq(a2aAgent.workflowId, workflowId), - isNull(a2aAgent.archivedAt) - ) - ) - .limit(1) - - if (existing) { - return NextResponse.json( - { error: 'An agent already exists for this workflow' }, - { status: 409 } - ) - } - - const skills = generateSkillsFromWorkflow( - name || wf.name, - description || wf.description, - skillTags - ) - - const agentId = generateId() - const agentName = name || sanitizeAgentName(wf.name) - - const [agent] = await db - .insert(a2aAgent) - .values({ - id: agentId, - workspaceId, - workflowId, - createdBy: auth.userId, - name: agentName, - description: description || wf.description, - version: '1.0.0', - capabilities: { - ...A2A_DEFAULT_CAPABILITIES, - ...capabilities, - }, - skills, - authentication: authentication || { - schemes: ['bearer', 'apiKey'], - }, - isPublished: false, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - - logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`) - - captureServerEvent( - auth.userId, - 'a2a_agent_created', - { agent_id: agentId, workflow_id: workflowId, workspace_id: workspaceId }, - { - groups: { workspace: workspaceId }, - setOnce: { first_a2a_agent_created_at: new Date().toISOString() }, - } - ) - - return NextResponse.json({ success: true, agent }, { status: 201 }) - } catch (error) { - logger.error('Error creating agent:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/a2a/serve/[agentId]/route.ts b/apps/sim/app/api/a2a/serve/[agentId]/route.ts deleted file mode 100644 index 62c41b0d48a..00000000000 --- a/apps/sim/app/api/a2a/serve/[agentId]/route.ts +++ /dev/null @@ -1,1576 +0,0 @@ -import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-js/sdk' -import { db } from '@sim/db' -import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { and, eq, isNull } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { A2A_DEFAULT_TIMEOUT, A2A_MAX_HISTORY_LENGTH } from '@/lib/a2a/constants' -import { notifyTaskStateChange } from '@/lib/a2a/push-notifications' -import { - createAgentMessage, - extractWorkflowInput, - isTerminalState, - parseWorkflowSSEChunk, -} from '@/lib/a2a/utils' -import { - type A2AJsonRpcId, - type A2AMessageSendParams, - type A2APushNotificationSetParams, - type A2ATaskIdParams, - a2aJsonRpcRequestSchema, - a2aMessageSendParamsSchema, - a2aPushNotificationSetParamsSchema, - a2aServeAgentParamsSchema, - a2aTaskIdParamsSchema, -} from '@/lib/api/contracts/a2a-agents' -import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid' -import { - API_EXECUTION_REQUIRES_PAID_PLAN_MESSAGE, - isApiExecutionEntitled, -} from '@/lib/billing/core/api-access' -import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' -import { getClientIp } from '@/lib/core/utils/request' -import { SSE_HEADERS } from '@/lib/core/utils/sse' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { markExecutionCancelled } from '@/lib/execution/cancellation' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' -import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' -import { - A2A_ERROR_CODES, - A2A_METHODS, - buildExecuteRequest, - buildTaskResponse, - createError, - createResponse, - extractAgentContent, - formatTaskResponse, - generateTaskId, -} from '@/app/api/a2a/serve/[agentId]/utils' -import { getBrandConfig } from '@/ee/whitelabeling' - -const logger = createLogger('A2AServeAPI') - -export const dynamic = 'force-dynamic' -export const runtime = 'nodejs' - -interface RouteParams { - agentId: string -} - -function getCallerFingerprint(request: NextRequest, userId?: string | null): string { - if (userId) { - return `user:${userId}` - } - - const clientIp = getClientIp(request) - const userAgent = request.headers.get('user-agent')?.trim() || 'unknown' - return `public:${clientIp}:${userAgent}` -} - -function hasCallerAccessToTask( - task: typeof a2aTask.$inferSelect, - callerFingerprint: string -): boolean { - const metadata = (task.metadata as Record | null) ?? {} - const storedFingerprint = - typeof metadata.callerFingerprint === 'string' ? metadata.callerFingerprint : null - return !storedFingerprint || storedFingerprint === callerFingerprint -} - -/** - * GET - Returns the Agent Card (discovery document) - */ -export const GET = withRouteHandler( - async (_request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aServeAgentParamsSchema.parse(await params) - - const redis = getRedisClient() - const cacheKey = `a2a:agent:${agentId}:card` - - if (redis) { - try { - const cached = await redis.get(cacheKey) - if (cached) { - return NextResponse.json(JSON.parse(cached), { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'private, max-age=60', - 'X-Cache': 'HIT', - }, - }) - } - } catch (err) { - logger.warn('Redis cache read failed', { agentId, error: err }) - } - } - - try { - const [agent] = await db - .select({ - id: a2aAgent.id, - name: a2aAgent.name, - description: a2aAgent.description, - version: a2aAgent.version, - capabilities: a2aAgent.capabilities, - skills: a2aAgent.skills, - authentication: a2aAgent.authentication, - isPublished: a2aAgent.isPublished, - }) - .from(a2aAgent) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!agent) { - return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) - } - - if (!agent.isPublished) { - return NextResponse.json({ error: 'Agent not published' }, { status: 404 }) - } - - const baseUrl = getBaseUrl() - const brandConfig = getBrandConfig() - - const authConfig = agent.authentication as { schemes?: string[] } | undefined - const schemes = authConfig?.schemes || [] - const isPublic = schemes.includes('none') - - const agentCard = { - protocolVersion: '0.3.0', - name: agent.name, - description: agent.description || '', - url: `${baseUrl}/api/a2a/serve/${agent.id}`, - version: agent.version, - preferredTransport: 'JSONRPC', - documentationUrl: `${baseUrl}/docs/a2a`, - provider: { - organization: brandConfig.name, - url: baseUrl, - }, - capabilities: agent.capabilities, - skills: agent.skills || [], - ...(isPublic - ? {} - : { - securitySchemes: { - apiKey: { - type: 'apiKey' as const, - name: 'X-API-Key', - in: 'header' as const, - description: 'API key authentication', - }, - }, - security: [{ apiKey: [] }], - }), - defaultInputModes: ['text/plain', 'application/json'], - defaultOutputModes: ['text/plain', 'application/json'], - } - - if (redis) { - try { - await redis.set(cacheKey, JSON.stringify(agentCard), 'EX', 60) - } catch (err) { - logger.warn('Redis cache write failed', { agentId, error: err }) - } - } - - return NextResponse.json(agentCard, { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'private, max-age=60', - 'X-Cache': 'MISS', - }, - }) - } catch (error) { - logger.error('Error getting Agent Card:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) - -/** - * POST - Handle JSON-RPC requests - */ -export const POST = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise }) => { - const { agentId } = a2aServeAgentParamsSchema.parse(await params) - - try { - const [agent] = await db - .select({ - id: a2aAgent.id, - name: a2aAgent.name, - workflowId: a2aAgent.workflowId, - workspaceId: a2aAgent.workspaceId, - isPublished: a2aAgent.isPublished, - capabilities: a2aAgent.capabilities, - authentication: a2aAgent.authentication, - }) - .from(a2aAgent) - .where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt))) - .limit(1) - - if (!agent) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Agent not found'), - { status: 404 } - ) - } - - if (!agent.isPublished) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Agent not published'), - { status: 404 } - ) - } - - const authSchemes = (agent.authentication as { schemes?: string[] })?.schemes || [] - const requiresAuth = !authSchemes.includes('none') - let authenticatedUserId: string | null = null - let authenticatedAuthType: AuthResult['authType'] - let authenticatedApiKeyType: AuthResult['apiKeyType'] - - if (requiresAuth) { - const auth = await checkHybridAuth(request, { requireWorkflowId: false }) - if (!auth.success || !auth.userId) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AUTHENTICATION_REQUIRED, 'Unauthorized'), - { status: 401 } - ) - } - authenticatedUserId = auth.userId - authenticatedAuthType = auth.authType - authenticatedApiKeyType = auth.apiKeyType - - if (auth.apiKeyType === 'workspace' && auth.workspaceId !== agent.workspaceId) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AUTHENTICATION_REQUIRED, 'Access denied'), - { status: 403 } - ) - } - - const workspaceAccess = await checkWorkspaceAccess(agent.workspaceId, authenticatedUserId) - if (!workspaceAccess.exists || !workspaceAccess.hasAccess) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AUTHENTICATION_REQUIRED, 'Access denied'), - { status: 403 } - ) - } - } - - const [wf] = await db - .select({ isDeployed: workflow.isDeployed }) - .from(workflow) - .where(and(eq(workflow.id, agent.workflowId), isNull(workflow.archivedAt))) - .limit(1) - - if (!wf?.isDeployed) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Workflow is not deployed'), - { status: 400 } - ) - } - - let rawBody: unknown - try { - rawBody = await request.json() - } catch { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.PARSE_ERROR, 'Invalid JSON body'), - { status: 400 } - ) - } - - const bodyResult = a2aJsonRpcRequestSchema.safeParse(rawBody) - - if (!bodyResult.success) { - return NextResponse.json( - createError(null, A2A_ERROR_CODES.INVALID_REQUEST, 'Invalid JSON-RPC request'), - { status: 400 } - ) - } - - const body = bodyResult.data - const { id, method, params: rpcParams } = body - const requestApiKey = request.headers.get('X-API-Key') - const apiKey = authenticatedAuthType === AuthType.API_KEY ? requestApiKey : null - const isPersonalApiKeyCaller = - authenticatedAuthType === AuthType.API_KEY && authenticatedApiKeyType === 'personal' - const callerFingerprint = getCallerFingerprint(request, authenticatedUserId) - const billedUserId = await getWorkspaceBilledAccountUserId(agent.workspaceId) - if (!billedUserId) { - logger.error('Unable to resolve workspace billed account for A2A execution', { - agentId: agent.id, - workspaceId: agent.workspaceId, - }) - return NextResponse.json( - createError( - id, - A2A_ERROR_CODES.INTERNAL_ERROR, - 'Unable to resolve billing account for this workspace' - ), - { status: 500 } - ) - } - if (!(await isApiExecutionEntitled(billedUserId))) { - return NextResponse.json( - createError( - id, - A2A_ERROR_CODES.AGENT_UNAVAILABLE, - API_EXECUTION_REQUIRES_PAID_PLAN_MESSAGE - ), - { status: 402 } - ) - } - - const executionUserId = - isPersonalApiKeyCaller && authenticatedUserId ? authenticatedUserId : billedUserId - - logger.info(`A2A request: ${method} for agent ${agentId}`) - - switch (method) { - case A2A_METHODS.MESSAGE_SEND: { - const paramsValidation = a2aMessageSendParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Message is required'), - { status: 400 } - ) - } - - return handleMessageSend( - id, - agent, - paramsValidation.data, - apiKey, - executionUserId, - callerFingerprint - ) - } - - case A2A_METHODS.MESSAGE_STREAM: { - const paramsValidation = a2aMessageSendParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Message is required'), - { status: 400 } - ) - } - - return handleMessageStream( - request, - id, - agent, - paramsValidation.data, - apiKey, - executionUserId, - callerFingerprint - ) - } - - case A2A_METHODS.TASKS_GET: { - const paramsValidation = a2aTaskIdParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'), - { status: 400 } - ) - } - return handleTaskGet(id, agent.id, paramsValidation.data, callerFingerprint) - } - - case A2A_METHODS.TASKS_CANCEL: { - const paramsValidation = a2aTaskIdParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'), - { status: 400 } - ) - } - return handleTaskCancel(id, agent.id, paramsValidation.data, callerFingerprint) - } - - case A2A_METHODS.TASKS_RESUBSCRIBE: { - const paramsValidation = a2aTaskIdParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'), - { status: 400 } - ) - } - - return handleTaskResubscribe( - request, - id, - agent.id, - paramsValidation.data, - callerFingerprint - ) - } - - case A2A_METHODS.PUSH_NOTIFICATION_SET: { - const paramsValidation = a2aPushNotificationSetParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification params'), - { status: 400 } - ) - } - - return handlePushNotificationSet(id, agent.id, paramsValidation.data, callerFingerprint) - } - - case A2A_METHODS.PUSH_NOTIFICATION_GET: { - const paramsValidation = a2aTaskIdParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'), - { status: 400 } - ) - } - return handlePushNotificationGet(id, agent.id, paramsValidation.data, callerFingerprint) - } - - case A2A_METHODS.PUSH_NOTIFICATION_DELETE: { - const paramsValidation = a2aTaskIdParamsSchema.safeParse(rpcParams) - if (!paramsValidation.success) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'), - { status: 400 } - ) - } - - return handlePushNotificationDelete( - id, - agent.id, - paramsValidation.data, - callerFingerprint - ) - } - - default: - return NextResponse.json( - createError(id, A2A_ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${method}`), - { status: 404 } - ) - } - } catch (error) { - logger.error('Error handling A2A request:', error) - return NextResponse.json( - createError(null, A2A_ERROR_CODES.INTERNAL_ERROR, 'Internal error'), - { - status: 500, - } - ) - } - } -) - -async function getTaskForAgent(taskId: string, agentId: string, callerFingerprint?: string) { - const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, taskId)).limit(1) - if (!task || task.agentId !== agentId) { - return null - } - if (callerFingerprint && !hasCallerAccessToTask(task, callerFingerprint)) { - return null - } - return task -} - -/** - * Handle message/send - Send a message (v0.3) - */ -async function handleMessageSend( - id: A2AJsonRpcId, - agent: { - id: string - name: string - workflowId: string - workspaceId: string - }, - params: A2AMessageSendParams, - apiKey?: string | null, - executionUserId?: string, - callerFingerprint?: string -): Promise { - const message = params.message - const taskId = message.taskId || generateTaskId() - const contextId = message.contextId || generateId() - - // Distributed lock to prevent concurrent task processing - const lockKey = `a2a:task:${taskId}:lock` - const lockValue = generateId() - const acquired = await acquireLock(lockKey, lockValue, 60) - - if (!acquired) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INTERNAL_ERROR, 'Task is currently being processed'), - { status: 409 } - ) - } - - try { - let existingTask: typeof a2aTask.$inferSelect | null = null - if (message.taskId) { - const [found] = await db.select().from(a2aTask).where(eq(a2aTask.id, message.taskId)).limit(1) - existingTask = found || null - - if (!existingTask) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), - { status: 404 } - ) - } - - if (existingTask.agentId !== agent.id) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), - { status: 404 } - ) - } - - if (callerFingerprint && !hasCallerAccessToTask(existingTask, callerFingerprint)) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), - { status: 404 } - ) - } - - if (isTerminalState(existingTask.status as TaskState)) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'), - { status: 400 } - ) - } - } - - const history: Message[] = existingTask?.messages ? (existingTask.messages as Message[]) : [] - - history.push(message) - - if (history.length > A2A_MAX_HISTORY_LENGTH) { - history.splice(0, history.length - A2A_MAX_HISTORY_LENGTH) - } - - if (existingTask) { - await db - .update(a2aTask) - .set({ - status: 'working', - messages: history, - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - } else { - await db.insert(a2aTask).values({ - id: taskId, - agentId: agent.id, - sessionId: contextId || null, - status: 'working', - messages: history, - metadata: callerFingerprint ? { callerFingerprint } : {}, - createdAt: new Date(), - updatedAt: new Date(), - }) - } - - const { - url: executeUrl, - headers, - useInternalAuth, - } = await buildExecuteRequest({ - workflowId: agent.workflowId, - apiKey, - userId: executionUserId, - }) - - logger.info(`Executing workflow ${agent.workflowId} for A2A task ${taskId}`) - - try { - const workflowInput = extractWorkflowInput(message) - if (!workflowInput) { - await db - .update(a2aTask) - .set({ - status: 'failed', - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'failed').catch((err) => { - logger.error('Failed to trigger push notification for invalid input', { - taskId, - error: err, - }) - }) - - return NextResponse.json( - createError( - id, - A2A_ERROR_CODES.INVALID_PARAMS, - 'Message must contain at least one part with content' - ), - { status: 400 } - ) - } - - const response = await fetch(executeUrl, { - method: 'POST', - headers, - body: JSON.stringify({ - ...workflowInput, - triggerType: 'a2a', - ...(useInternalAuth && { workflowId: agent.workflowId }), - }), - signal: AbortSignal.timeout(A2A_DEFAULT_TIMEOUT), - }) - - const executeResult = await response.json() - const executionId = executeResult.executionId || executeResult.metadata?.executionId - const executionSucceeded = response.ok && executeResult.success !== false - const finalState: TaskState = executionSucceeded ? 'completed' : 'failed' - - const agentContent = extractAgentContent(executeResult) - const agentMessage = createAgentMessage(agentContent) - agentMessage.taskId = taskId - if (contextId) agentMessage.contextId = contextId - history.push(agentMessage) - - const artifacts = executeResult.output?.artifacts || [] - - await db - .update(a2aTask) - .set({ - status: finalState, - messages: history, - artifacts, - executionId, - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - if (isTerminalState(finalState)) { - notifyTaskStateChange(taskId, finalState).catch((err) => { - logger.error('Failed to trigger push notification', { taskId, error: err }) - }) - } - - const task = buildTaskResponse({ - taskId, - contextId, - state: finalState, - history, - artifacts, - }) - - return NextResponse.json(createResponse(id, task)) - } catch (error) { - const isTimeout = error instanceof Error && error.name === 'TimeoutError' - logger.error(`Error executing workflow for task ${taskId}:`, { error, isTimeout }) - - const errorMessage = isTimeout - ? `Workflow execution timed out after ${A2A_DEFAULT_TIMEOUT}ms` - : error instanceof Error - ? error.message - : 'Workflow execution failed' - - await db - .update(a2aTask) - .set({ - status: 'failed', - updatedAt: new Date(), - completedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'failed').catch((err) => { - logger.error('Failed to trigger push notification for failure', { taskId, error: err }) - }) - - return NextResponse.json(createError(id, A2A_ERROR_CODES.INTERNAL_ERROR, errorMessage), { - status: 500, - }) - } - } finally { - await releaseLock(lockKey, lockValue) - } -} - -/** - * Handle message/stream - Stream a message response (v0.3) - */ -async function handleMessageStream( - _request: NextRequest, - id: A2AJsonRpcId, - agent: { - id: string - name: string - workflowId: string - workspaceId: string - }, - params: A2AMessageSendParams, - apiKey?: string | null, - executionUserId?: string, - callerFingerprint?: string -): Promise { - const message = params.message - const contextId = message.contextId || generateId() - const taskId = message.taskId || generateTaskId() - - // Distributed lock to prevent concurrent task processing - const lockKey = `a2a:task:${taskId}:lock` - const lockValue = generateId() - const acquired = await acquireLock(lockKey, lockValue, 300) - - if (!acquired) { - const encoder = new TextEncoder() - const errorStream = new ReadableStream({ - start(controller) { - controller.enqueue( - encoder.encode( - `event: error\ndata: ${JSON.stringify({ code: A2A_ERROR_CODES.INTERNAL_ERROR, message: 'Task is currently being processed' })}\n\n` - ) - ) - controller.close() - }, - }) - return new NextResponse(errorStream, { headers: SSE_HEADERS }) - } - - let history: Message[] = [] - let existingTask: typeof a2aTask.$inferSelect | null = null - - if (message.taskId) { - const [found] = await db.select().from(a2aTask).where(eq(a2aTask.id, message.taskId)).limit(1) - existingTask = found || null - - if (!existingTask) { - await releaseLock(lockKey, lockValue) - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - if (existingTask.agentId !== agent.id) { - await releaseLock(lockKey, lockValue) - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - if (callerFingerprint && !hasCallerAccessToTask(existingTask, callerFingerprint)) { - await releaseLock(lockKey, lockValue) - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - if (isTerminalState(existingTask.status as TaskState)) { - await releaseLock(lockKey, lockValue) - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'), - { status: 400 } - ) - } - - history = existingTask.messages as Message[] - } - - history.push(message) - - if (history.length > A2A_MAX_HISTORY_LENGTH) { - history.splice(0, history.length - A2A_MAX_HISTORY_LENGTH) - } - - if (existingTask) { - await db - .update(a2aTask) - .set({ - status: 'working', - messages: history, - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - } else { - await db.insert(a2aTask).values({ - id: taskId, - agentId: agent.id, - sessionId: contextId || null, - status: 'working', - messages: history, - metadata: callerFingerprint ? { callerFingerprint } : {}, - createdAt: new Date(), - updatedAt: new Date(), - }) - } - - const encoder = new TextEncoder() - - const stream = new ReadableStream({ - async start(controller) { - const sendEvent = (event: string, data: unknown) => { - try { - const jsonRpcResponse = { - jsonrpc: '2.0' as const, - id, - result: data, - } - controller.enqueue( - encoder.encode(`event: ${event}\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`) - ) - } catch (error) { - logger.error('Error sending SSE event:', error) - } - } - - sendEvent('status', { - kind: 'status', - taskId, - contextId, - status: { state: 'working', timestamp: new Date().toISOString() }, - }) - - try { - const { - url: executeUrl, - headers, - useInternalAuth, - } = await buildExecuteRequest({ - workflowId: agent.workflowId, - apiKey, - userId: executionUserId, - stream: true, - }) - - const workflowInput = extractWorkflowInput(message) - if (!workflowInput) { - await db - .update(a2aTask) - .set({ - status: 'failed', - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'failed').catch((err) => { - logger.error('Failed to trigger push notification for invalid streamed input', { - taskId, - error: err, - }) - }) - - sendEvent('error', { - code: A2A_ERROR_CODES.INVALID_PARAMS, - message: 'Message must contain at least one part with content', - }) - await releaseLock(lockKey, lockValue) - controller.close() - return - } - - const response = await fetch(executeUrl, { - method: 'POST', - headers, - body: JSON.stringify({ - ...workflowInput, - triggerType: 'a2a', - stream: true, - ...(useInternalAuth && { workflowId: agent.workflowId }), - }), - signal: AbortSignal.timeout(A2A_DEFAULT_TIMEOUT), - }) - - if (!response.ok) { - let errorMessage = 'Workflow execution failed' - try { - const errorResult = await response.json() - errorMessage = errorResult.error || errorMessage - } catch { - // Response may not be JSON - } - throw new Error(errorMessage) - } - - const contentType = response.headers.get('content-type') || '' - const streamingExecutionId = response.headers.get('X-Execution-Id') || undefined - const isStreamingResponse = - contentType.includes('text/event-stream') || contentType.includes('text/plain') - - if (response.body && isStreamingResponse) { - const reader = response.body.getReader() - const decoder = new TextDecoder() - const contentChunks: string[] = [] - let finalContent: string | undefined - let finalArtifacts: Artifact[] = [] - let sseBuffer = '' - - while (true) { - const { done, value } = await reader.read() - if (done) break - - sseBuffer += decoder.decode(value, { stream: true }) - const frames = sseBuffer.split('\n\n') - sseBuffer = frames.pop() ?? '' - - for (const frame of frames) { - const parsed = parseWorkflowSSEChunk(frame) - - if (parsed.content) { - contentChunks.push(parsed.content) - sendEvent('message', { - kind: 'message', - taskId, - contextId, - role: 'agent', - parts: [{ kind: 'text', text: parsed.content }], - final: false, - }) - } - - if (parsed.finalContent) { - finalContent = parsed.finalContent - } - if (parsed.finalArtifacts) { - finalArtifacts = parsed.finalArtifacts - } - if (parsed.terminalState === 'canceled') { - const agentMessage = createAgentMessage(finalContent || 'Task canceled') - agentMessage.taskId = taskId - if (contextId) agentMessage.contextId = contextId - history.push(agentMessage) - - await db - .update(a2aTask) - .set({ - status: 'canceled', - messages: history, - executionId: streamingExecutionId, - artifacts: finalArtifacts, - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'canceled').catch((err) => { - logger.error('Failed to trigger push notification', { taskId, error: err }) - }) - - sendEvent('task', { - kind: 'task', - id: taskId, - contextId, - status: { state: 'canceled', timestamp: new Date().toISOString() }, - history, - artifacts: finalArtifacts, - }) - return - } - - if (parsed.finalSuccess === false) { - throw new Error('Workflow execution failed') - } - } - } - - if (sseBuffer.trim().length > 0) { - const parsed = parseWorkflowSSEChunk(sseBuffer) - if (parsed.content) { - contentChunks.push(parsed.content) - sendEvent('message', { - kind: 'message', - taskId, - contextId, - role: 'agent', - parts: [{ kind: 'text', text: parsed.content }], - final: false, - }) - } - if (parsed.finalContent) { - finalContent = parsed.finalContent - } - if (parsed.finalArtifacts) { - finalArtifacts = parsed.finalArtifacts - } - if (parsed.finalSuccess === false) { - throw new Error('Workflow execution failed') - } - } - - const accumulatedContent = contentChunks.join('') - const messageContent = - (finalContent !== undefined && finalContent.length > 0 - ? finalContent - : accumulatedContent) || 'Task completed' - const agentMessage = createAgentMessage(messageContent) - agentMessage.taskId = taskId - if (contextId) agentMessage.contextId = contextId - history.push(agentMessage) - - await db - .update(a2aTask) - .set({ - status: 'completed', - messages: history, - executionId: streamingExecutionId, - artifacts: finalArtifacts, - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'completed').catch((err) => { - logger.error('Failed to trigger push notification', { taskId, error: err }) - }) - - sendEvent('task', { - kind: 'task', - id: taskId, - contextId, - status: { state: 'completed', timestamp: new Date().toISOString() }, - history, - artifacts: finalArtifacts, - }) - } else { - const result = await response.json() - const executionSucceeded = result.success !== false - - const content = extractAgentContent(result) - - sendEvent('message', { - kind: 'message', - taskId, - contextId, - role: 'agent', - parts: [{ kind: 'text', text: content }], - final: true, - }) - - const agentMessage = createAgentMessage(content) - agentMessage.taskId = taskId - if (contextId) agentMessage.contextId = contextId - history.push(agentMessage) - - const artifacts = (result.output?.artifacts as Artifact[]) || [] - - await db - .update(a2aTask) - .set({ - status: executionSucceeded ? 'completed' : 'failed', - messages: history, - artifacts, - executionId: result.executionId || result.metadata?.executionId, - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, executionSucceeded ? 'completed' : 'failed').catch( - (err) => { - logger.error('Failed to trigger push notification', { taskId, error: err }) - } - ) - - sendEvent('task', { - kind: 'task', - id: taskId, - contextId, - status: { - state: executionSucceeded ? 'completed' : 'failed', - timestamp: new Date().toISOString(), - }, - history, - artifacts, - }) - } - } catch (error) { - const isTimeout = error instanceof Error && error.name === 'TimeoutError' - logger.error(`Streaming error for task ${taskId}:`, { error, isTimeout }) - - const errorMessage = isTimeout - ? `Workflow execution timed out after ${A2A_DEFAULT_TIMEOUT}ms` - : error instanceof Error - ? error.message - : 'Streaming failed' - - await db - .update(a2aTask) - .set({ - status: 'failed', - completedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(a2aTask.id, taskId)) - - notifyTaskStateChange(taskId, 'failed').catch((err) => { - logger.error('Failed to trigger push notification for failure', { taskId, error: err }) - }) - - sendEvent('error', { - code: A2A_ERROR_CODES.INTERNAL_ERROR, - message: errorMessage, - }) - } finally { - await releaseLock(lockKey, lockValue) - controller.close() - } - }, - cancel() {}, - }) - - return new NextResponse(stream, { - headers: { - ...SSE_HEADERS, - 'X-Task-Id': taskId, - }, - }) -} - -/** - * Handle tasks/get - Query task status - */ -async function handleTaskGet( - id: A2AJsonRpcId, - agentId: string, - params: A2ATaskIdParams, - callerFingerprint?: string -): Promise { - const historyLength = - params.historyLength !== undefined && params.historyLength >= 0 - ? params.historyLength - : undefined - - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - const taskResponse = buildTaskResponse({ - taskId: task.id, - contextId: task.sessionId || task.id, - state: task.status as TaskState, - history: task.messages as Message[], - artifacts: (task.artifacts as Artifact[]) || [], - }) - - const result = formatTaskResponse(taskResponse, historyLength) - - return NextResponse.json(createResponse(id, result)) -} - -/** - * Handle tasks/cancel - Cancel a running task - */ -async function handleTaskCancel( - id: A2AJsonRpcId, - agentId: string, - params: A2ATaskIdParams, - callerFingerprint?: string -): Promise { - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - if (isTerminalState(task.status as TaskState)) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'), - { status: 400 } - ) - } - - if (task.executionId) { - try { - await markExecutionCancelled(task.executionId) - logger.info('Cancelled workflow execution', { - taskId: task.id, - executionId: task.executionId, - }) - } catch (error) { - logger.warn('Failed to cancel workflow execution', { - taskId: task.id, - executionId: task.executionId, - error, - }) - } - } - - await db - .update(a2aTask) - .set({ - status: 'canceled', - updatedAt: new Date(), - completedAt: new Date(), - }) - .where(eq(a2aTask.id, params.id)) - - notifyTaskStateChange(params.id, 'canceled').catch((err) => { - logger.error('Failed to trigger push notification for cancellation', { - taskId: params.id, - error: err, - }) - }) - - const canceledTask = buildTaskResponse({ - taskId: task.id, - contextId: task.sessionId || task.id, - state: 'canceled', - history: task.messages as Message[], - artifacts: (task.artifacts as Artifact[]) || [], - }) - - return NextResponse.json(createResponse(id, canceledTask)) -} - -/** - * Handle tasks/resubscribe - Reconnect to SSE stream for an ongoing task - */ -async function handleTaskResubscribe( - request: NextRequest, - id: A2AJsonRpcId, - agentId: string, - params: A2ATaskIdParams, - callerFingerprint?: string -): Promise { - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - const encoder = new TextEncoder() - - if (isTerminalState(task.status as TaskState)) { - const completedTask = buildTaskResponse({ - taskId: task.id, - contextId: task.sessionId || task.id, - state: task.status as TaskState, - history: task.messages as Message[], - artifacts: (task.artifacts as Artifact[]) || [], - }) - const jsonRpcResponse = { jsonrpc: '2.0' as const, id, result: completedTask } - const sseData = `event: task\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n` - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sseData)) - controller.close() - }, - }) - return new NextResponse(stream, { headers: SSE_HEADERS }) - } - let isCancelled = false - let pollTimeoutId: ReturnType | null = null - - const abortSignal = request.signal - abortSignal.addEventListener( - 'abort', - () => { - isCancelled = true - if (pollTimeoutId) { - clearTimeout(pollTimeoutId) - pollTimeoutId = null - } - }, - { once: true } - ) - - const cleanup = () => { - isCancelled = true - if (pollTimeoutId) { - clearTimeout(pollTimeoutId) - pollTimeoutId = null - } - } - - const stream = new ReadableStream({ - async start(controller) { - const sendEvent = (event: string, data: unknown): boolean => { - if (isCancelled || abortSignal.aborted) return false - try { - const jsonRpcResponse = { jsonrpc: '2.0' as const, id, result: data } - controller.enqueue( - encoder.encode(`event: ${event}\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`) - ) - return true - } catch (error) { - logger.error('Error sending SSE event:', error) - isCancelled = true - return false - } - } - - if ( - !sendEvent('status', { - kind: 'status', - taskId: task.id, - contextId: task.sessionId, - status: { state: task.status, timestamp: new Date().toISOString() }, - }) - ) { - cleanup() - return - } - - const pollInterval = 3000 // 3 seconds - const maxPolls = 100 // 5 minutes max - - let polls = 0 - const poll = async () => { - if (isCancelled || abortSignal.aborted) { - cleanup() - return - } - - polls++ - if (polls > maxPolls) { - cleanup() - try { - controller.close() - } catch { - // Already closed - } - return - } - - try { - const [updatedTask] = await db - .select() - .from(a2aTask) - .where(eq(a2aTask.id, params.id)) - .limit(1) - - if (isCancelled) { - cleanup() - return - } - - if (!updatedTask) { - sendEvent('error', { code: A2A_ERROR_CODES.TASK_NOT_FOUND, message: 'Task not found' }) - cleanup() - try { - controller.close() - } catch { - // Already closed - } - return - } - - if (updatedTask.status !== task.status) { - if ( - !sendEvent('status', { - kind: 'status', - taskId: updatedTask.id, - contextId: updatedTask.sessionId, - status: { state: updatedTask.status, timestamp: new Date().toISOString() }, - final: isTerminalState(updatedTask.status as TaskState), - }) - ) { - cleanup() - return - } - } - - if (isTerminalState(updatedTask.status as TaskState)) { - const messages = updatedTask.messages as Message[] - const lastMessage = messages[messages.length - 1] - if (lastMessage && lastMessage.role === 'agent') { - sendEvent('message', { - ...lastMessage, - taskId: updatedTask.id, - contextId: updatedTask.sessionId || updatedTask.id, - final: true, - }) - } - - cleanup() - try { - controller.close() - } catch { - // Already closed - } - return - } - - pollTimeoutId = setTimeout(poll, pollInterval) - } catch (error) { - logger.error('Error during SSE poll:', error) - sendEvent('error', { - code: A2A_ERROR_CODES.INTERNAL_ERROR, - message: getErrorMessage(error, 'Polling failed'), - }) - cleanup() - try { - controller.close() - } catch { - // Already closed - } - } - } - - poll() - }, - cancel() { - cleanup() - }, - }) - - return new NextResponse(stream, { - headers: { - ...SSE_HEADERS, - 'X-Task-Id': params.id, - }, - }) -} - -/** - * Handle tasks/pushNotificationConfig/set - Set webhook for task updates - */ -async function handlePushNotificationSet( - id: A2AJsonRpcId, - agentId: string, - params: A2APushNotificationSetParams, - callerFingerprint?: string -): Promise { - const urlValidation = await validateUrlWithDNS( - params.pushNotificationConfig.url, - 'Push notification URL' - ) - if (!urlValidation.isValid) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'), - { status: 400 } - ) - } - - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - const [existingConfig] = await db - .select() - .from(a2aPushNotificationConfig) - .where(eq(a2aPushNotificationConfig.taskId, params.id)) - .limit(1) - - const config = params.pushNotificationConfig - - if (existingConfig) { - await db - .update(a2aPushNotificationConfig) - .set({ - url: config.url, - token: config.token || null, - isActive: true, - updatedAt: new Date(), - }) - .where(eq(a2aPushNotificationConfig.id, existingConfig.id)) - } else { - await db.insert(a2aPushNotificationConfig).values({ - id: generateId(), - taskId: params.id, - url: config.url, - token: config.token || null, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }) - } - - const result: PushNotificationConfig = { - url: config.url, - token: config.token, - } - - return NextResponse.json(createResponse(id, result)) -} - -/** - * Handle tasks/pushNotificationConfig/get - Get webhook config for a task - */ -async function handlePushNotificationGet( - id: A2AJsonRpcId, - agentId: string, - params: A2ATaskIdParams, - callerFingerprint?: string -): Promise { - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - const [config] = await db - .select() - .from(a2aPushNotificationConfig) - .where(eq(a2aPushNotificationConfig.taskId, params.id)) - .limit(1) - - if (!config) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Push notification config not found'), - { status: 404 } - ) - } - - const result: PushNotificationConfig = { - url: config.url, - token: config.token || undefined, - } - - return NextResponse.json(createResponse(id, result)) -} - -/** - * Handle tasks/pushNotificationConfig/delete - Delete webhook config for a task - */ -async function handlePushNotificationDelete( - id: A2AJsonRpcId, - agentId: string, - params: A2ATaskIdParams, - callerFingerprint?: string -): Promise { - const task = await getTaskForAgent(params.id, agentId, callerFingerprint) - - if (!task) { - return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), { - status: 404, - }) - } - - const [config] = await db - .select() - .from(a2aPushNotificationConfig) - .where(eq(a2aPushNotificationConfig.taskId, params.id)) - .limit(1) - - if (!config) { - return NextResponse.json( - createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Push notification config not found'), - { status: 404 } - ) - } - - await db.delete(a2aPushNotificationConfig).where(eq(a2aPushNotificationConfig.id, config.id)) - - return NextResponse.json(createResponse(id, { success: true })) -} diff --git a/apps/sim/app/api/a2a/serve/[agentId]/utils.ts b/apps/sim/app/api/a2a/serve/[agentId]/utils.ts deleted file mode 100644 index de4be12323a..00000000000 --- a/apps/sim/app/api/a2a/serve/[agentId]/utils.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk' -import { generateId } from '@sim/utils/id' -import { generateInternalToken } from '@/lib/auth/internal' -import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' - -/** A2A v0.3 JSON-RPC method names */ -export const A2A_METHODS = { - MESSAGE_SEND: 'message/send', - MESSAGE_STREAM: 'message/stream', - TASKS_GET: 'tasks/get', - TASKS_CANCEL: 'tasks/cancel', - TASKS_RESUBSCRIBE: 'tasks/resubscribe', - PUSH_NOTIFICATION_SET: 'tasks/pushNotificationConfig/set', - PUSH_NOTIFICATION_GET: 'tasks/pushNotificationConfig/get', - PUSH_NOTIFICATION_DELETE: 'tasks/pushNotificationConfig/delete', -} as const - -/** A2A v0.3 error codes */ -export const A2A_ERROR_CODES = { - PARSE_ERROR: -32700, - INVALID_REQUEST: -32600, - METHOD_NOT_FOUND: -32601, - INVALID_PARAMS: -32602, - INTERNAL_ERROR: -32603, - TASK_NOT_FOUND: -32001, - TASK_ALREADY_COMPLETE: -32002, - AGENT_UNAVAILABLE: -32003, - AUTHENTICATION_REQUIRED: -32004, -} as const - -interface JSONRPCRequest { - jsonrpc: '2.0' - id: string | number - method: string - params?: unknown -} - -export interface JSONRPCResponse { - jsonrpc: '2.0' - id: string | number | null - result?: unknown - error?: { - code: number - message: string - data?: unknown - } -} - -interface MessageSendParams { - message: Message - configuration?: { - acceptedOutputModes?: string[] - historyLength?: number - pushNotificationConfig?: PushNotificationConfig - } -} - -interface TaskIdParams { - id: string - historyLength?: number -} - -interface PushNotificationSetParams { - id: string - pushNotificationConfig: PushNotificationConfig -} - -export function createResponse(id: string | number | null, result: unknown): JSONRPCResponse { - return { jsonrpc: '2.0', id, result } -} - -export function createError( - id: string | number | null, - code: number, - message: string, - data?: unknown -): JSONRPCResponse { - return { jsonrpc: '2.0', id, error: { code, message, data } } -} - -export function isJSONRPCRequest(obj: unknown): obj is JSONRPCRequest { - if (!obj || typeof obj !== 'object') return false - const r = obj as Record - return r.jsonrpc === '2.0' && typeof r.method === 'string' && r.id !== undefined -} - -export function generateTaskId(): string { - return generateId() -} - -export function createTaskStatus(state: TaskState): { state: TaskState; timestamp: string } { - return { state, timestamp: new Date().toISOString() } -} - -export function formatTaskResponse(task: Task, historyLength?: number): Task { - if (historyLength !== undefined && task.history) { - return { - ...task, - history: task.history.slice(-historyLength), - } - } - return task -} - -export interface ExecuteRequestConfig { - workflowId: string - apiKey?: string | null - userId?: string - stream?: boolean -} - -export interface ExecuteRequestResult { - url: string - headers: Record - useInternalAuth: boolean -} - -export async function buildExecuteRequest( - config: ExecuteRequestConfig -): Promise { - const url = `${getInternalApiBaseUrl()}/api/workflows/${config.workflowId}/execute` - const headers: Record = { 'Content-Type': 'application/json' } - let useInternalAuth = false - - if (config.apiKey) { - headers['X-API-Key'] = config.apiKey - } else { - const internalToken = await generateInternalToken(config.userId) - headers.Authorization = `Bearer ${internalToken}` - useInternalAuth = true - } - - if (config.stream) { - headers['X-Stream-Response'] = 'true' - } - - return { url, headers, useInternalAuth } -} - -export function extractAgentContent(executeResult: { - output?: { content?: string; [key: string]: unknown } - error?: string -}): string { - // Prefer explicit content field - if (executeResult.output?.content) { - return executeResult.output.content - } - - // If output is an object with meaningful data, stringify it - if (typeof executeResult.output === 'object' && executeResult.output !== null) { - const keys = Object.keys(executeResult.output) - // Skip empty objects or objects with only undefined values - if (keys.length > 0 && keys.some((k) => executeResult.output![k] !== undefined)) { - return JSON.stringify(executeResult.output) - } - } - - // Fallback to error message or default - return executeResult.error || 'Task completed' -} - -export function buildTaskResponse(params: { - taskId: string - contextId: string - state: TaskState - history: Message[] - artifacts?: Artifact[] -}): Task { - return { - kind: 'task', - id: params.taskId, - contextId: params.contextId, - status: createTaskStatus(params.state), - history: params.history, - artifacts: params.artifacts || [], - } -} diff --git a/apps/sim/app/api/tools/a2a/cancel-task/route.ts b/apps/sim/app/api/tools/a2a/cancel-task/route.ts deleted file mode 100644 index 92935a001a0..00000000000 --- a/apps/sim/app/api/tools/a2a/cancel-task/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Task } from '@a2a-js/sdk' -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aCancelTaskContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('A2ACancelTaskAPI') - -export const dynamic = 'force-dynamic' - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-cancel-task', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - const parsed = await parseRequest( - a2aCancelTaskContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Canceling A2A task`, { - agentUrl: validatedData.agentUrl, - taskId: validatedData.taskId, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const task = (await client.cancelTask({ id: validatedData.taskId })) as Task - - logger.info(`[${requestId}] Successfully canceled A2A task`, { - taskId: validatedData.taskId, - state: task.status.state, - }) - - return NextResponse.json({ - success: true, - output: { - cancelled: true, - state: task.status.state, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error canceling A2A task:`, error) - return NextResponse.json( - { - success: false, - error: 'Failed to cancel task', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts b/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts deleted file mode 100644 index cf93e9e2f36..00000000000 --- a/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aDeletePushNotificationContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2ADeletePushNotificationAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn( - `[${requestId}] Unauthorized A2A delete push notification attempt: ${authResult.error}` - ) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-delete-push-notification', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - logger.info( - `[${requestId}] Authenticated A2A delete push notification request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const parsed = await parseRequest( - a2aDeletePushNotificationContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Deleting A2A push notification config`, { - agentUrl: validatedData.agentUrl, - taskId: validatedData.taskId, - pushNotificationConfigId: validatedData.pushNotificationConfigId, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - await client.deleteTaskPushNotificationConfig({ - id: validatedData.taskId, - pushNotificationConfigId: validatedData.pushNotificationConfigId || validatedData.taskId, - }) - - logger.info(`[${requestId}] Push notification config deleted successfully`, { - taskId: validatedData.taskId, - }) - - return NextResponse.json({ - success: true, - output: { - success: true, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error deleting A2A push notification:`, error) - - return NextResponse.json( - { - success: false, - error: 'Failed to delete push notification', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/get-agent-card/route.ts b/apps/sim/app/api/tools/a2a/get-agent-card/route.ts deleted file mode 100644 index fed318b8330..00000000000 --- a/apps/sim/app/api/tools/a2a/get-agent-card/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aGetAgentCardContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2AGetAgentCardAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-get-agent-card', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - logger.info( - `[${requestId}] Authenticated A2A get agent card request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const parsed = await parseRequest( - a2aGetAgentCardContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Fetching Agent Card`, { - agentUrl: validatedData.agentUrl, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const agentCard = await client.getAgentCard() - - logger.info(`[${requestId}] Agent Card fetched successfully`, { - agentName: agentCard.name, - }) - - return NextResponse.json({ - success: true, - output: { - name: agentCard.name, - description: agentCard.description, - url: agentCard.url, - version: agentCard.protocolVersion, - capabilities: agentCard.capabilities, - skills: agentCard.skills, - defaultInputModes: agentCard.defaultInputModes, - defaultOutputModes: agentCard.defaultOutputModes, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error fetching Agent Card:`, error) - - return NextResponse.json( - { - success: false, - error: 'Failed to fetch Agent Card', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/get-push-notification/route.ts b/apps/sim/app/api/tools/a2a/get-push-notification/route.ts deleted file mode 100644 index 6c48da2648c..00000000000 --- a/apps/sim/app/api/tools/a2a/get-push-notification/route.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aGetPushNotificationContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2AGetPushNotificationAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn( - `[${requestId}] Unauthorized A2A get push notification attempt: ${authResult.error}` - ) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-get-push-notification', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - logger.info( - `[${requestId}] Authenticated A2A get push notification request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const parsed = await parseRequest( - a2aGetPushNotificationContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Getting push notification config`, { - agentUrl: validatedData.agentUrl, - taskId: validatedData.taskId, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const result = await client.getTaskPushNotificationConfig({ - id: validatedData.taskId, - }) - - if (!result || !result.pushNotificationConfig) { - logger.info(`[${requestId}] No push notification config found for task`, { - taskId: validatedData.taskId, - }) - return NextResponse.json({ - success: true, - output: { - exists: false, - }, - }) - } - - logger.info(`[${requestId}] Push notification config retrieved successfully`, { - taskId: validatedData.taskId, - }) - - return NextResponse.json({ - success: true, - output: { - url: result.pushNotificationConfig.url, - token: result.pushNotificationConfig.token, - exists: true, - }, - }) - } catch (error) { - if (error instanceof Error && error.message.includes('not found')) { - logger.info(`[${requestId}] Task not found, returning exists: false`) - return NextResponse.json({ - success: true, - output: { - exists: false, - }, - }) - } - - logger.error(`[${requestId}] Error getting A2A push notification:`, error) - - return NextResponse.json( - { - success: false, - error: 'Failed to get push notification', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/get-task/route.ts b/apps/sim/app/api/tools/a2a/get-task/route.ts deleted file mode 100644 index 3e38b82f80c..00000000000 --- a/apps/sim/app/api/tools/a2a/get-task/route.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Task } from '@a2a-js/sdk' -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aGetTaskContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2AGetTaskAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit('a2a-get-task', authResult.userId, request) - if (rateLimited) return rateLimited - - logger.info(`[${requestId}] Authenticated A2A get task request via ${authResult.authType}`, { - userId: authResult.userId, - }) - - const parsed = await parseRequest( - a2aGetTaskContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Getting A2A task`, { - agentUrl: validatedData.agentUrl, - taskId: validatedData.taskId, - historyLength: validatedData.historyLength, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const task = (await client.getTask({ - id: validatedData.taskId, - historyLength: validatedData.historyLength, - })) as Task - - logger.info(`[${requestId}] Successfully retrieved A2A task`, { - taskId: task.id, - state: task.status.state, - }) - - return NextResponse.json({ - success: true, - output: { - taskId: task.id, - contextId: task.contextId, - state: task.status.state, - artifacts: task.artifacts, - history: task.history, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error getting A2A task:`, error) - - return NextResponse.json( - { - success: false, - error: 'Failed to get task', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/resubscribe/route.ts b/apps/sim/app/api/tools/a2a/resubscribe/route.ts deleted file mode 100644 index bd4bdebabc7..00000000000 --- a/apps/sim/app/api/tools/a2a/resubscribe/route.ts +++ /dev/null @@ -1,127 +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 { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { a2aResubscribeContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('A2AResubscribeAPI') - -export const dynamic = 'force-dynamic' - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-resubscribe', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - const parsed = await parseRequest( - a2aResubscribeContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const stream = client.resubscribeTask({ id: validatedData.taskId }) - - let taskId = validatedData.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}] Successfully resubscribed to A2A task ${taskId}`) - - return NextResponse.json({ - success: true, - output: { - taskId, - contextId, - state, - isRunning: !isTerminalState(state), - artifacts, - history, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error resubscribing to A2A task:`, error) - return NextResponse.json( - { - success: false, - error: 'Failed to resubscribe', - }, - { 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 deleted file mode 100644 index 708863a8715..00000000000 --- a/apps/sim/app/api/tools/a2a/send-message/route.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { DataPart, FilePart, Message, Part, Task, TextPart } from '@a2a-js/sdk' -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { a2aSendMessageContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2ASendMessageAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-send-message', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - logger.info( - `[${requestId}] Authenticated A2A send message request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const parsed = await parseRequest( - a2aSendMessageContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - logger.info(`[${requestId}] Sending A2A message`, { - agentUrl: validatedData.agentUrl, - hasTaskId: !!validatedData.taskId, - hasContextId: !!validatedData.contextId, - }) - - 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', - }, - { 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: toError(parseError).message, - }) - } - } - - if (validatedData.files && validatedData.files.length > 0) { - for (const file of validatedData.files) { - if (file.type === 'url') { - const urlValidation = await validateUrlWithDNS(file.data, 'fileUrl') - if (!urlValidation.isValid) { - return NextResponse.json( - { success: false, error: urlValidation.error }, - { status: 400 } - ) - } - - 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: generateId(), - role: 'user', - parts, - ...(validatedData.taskId && { taskId: validatedData.taskId }), - ...(validatedData.contextId && { contextId: validatedData.contextId }), - } - - 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 to agent', - }, - { status: 502 } - ) - } - - if (result.kind === 'message') { - const responseMessage = result as Message - - logger.info(`[${requestId}] A2A message sent successfully (message response)`) - - return NextResponse.json({ - success: true, - output: { - content: extractTextContent(responseMessage), - taskId: responseMessage.taskId || '', - contextId: responseMessage.contextId, - state: 'completed', - }, - }) - } - - const task = result as Task - const lastAgentMessage = task.history?.filter((m) => m.role === 'agent').pop() - const content = lastAgentMessage ? extractTextContent(lastAgentMessage) : '' - - logger.info(`[${requestId}] A2A message sent successfully (task response)`, { - taskId: task.id, - state: task.status.state, - }) - - return NextResponse.json({ - success: isTerminalState(task.status.state) && task.status.state !== 'failed', - output: { - content, - taskId: task.id, - contextId: task.contextId, - state: task.status.state, - artifacts: task.artifacts, - history: task.history, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error sending A2A message:`, error) - - return NextResponse.json( - { - success: false, - error: 'Internal server error', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/tools/a2a/set-push-notification/route.ts b/apps/sim/app/api/tools/a2a/set-push-notification/route.ts deleted file mode 100644 index 5511da2d2cc..00000000000 --- a/apps/sim/app/api/tools/a2a/set-push-notification/route.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { createA2AClient } from '@/lib/a2a/utils' -import { a2aSetPushNotificationContract } from '@/lib/api/contracts/tools/a2a' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' -import { generateRequestId } from '@/lib/core/utils/request' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2ASetPushNotificationAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const requestId = generateRequestId() - - try { - const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, { - error: authResult.error || 'Authentication required', - }) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - const rateLimited = await enforceUserOrIpRateLimit( - 'a2a-set-push-notification', - authResult.userId, - request - ) - if (rateLimited) return rateLimited - - const parsed = await parseRequest( - a2aSetPushNotificationContract, - request, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request data'), - details: error.issues, - }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - const validatedData = parsed.data.body - - const urlValidation = await validateUrlWithDNS(validatedData.webhookUrl, 'Webhook URL') - if (!urlValidation.isValid) { - logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error }) - return NextResponse.json( - { - success: false, - error: urlValidation.error, - }, - { status: 400 } - ) - } - - logger.info(`[${requestId}] A2A set push notification request`, { - agentUrl: validatedData.agentUrl, - taskId: validatedData.taskId, - webhookUrl: validatedData.webhookUrl, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const result = await client.setTaskPushNotificationConfig({ - taskId: validatedData.taskId, - pushNotificationConfig: { - url: validatedData.webhookUrl, - token: validatedData.token, - }, - }) - - logger.info(`[${requestId}] A2A set push notification successful`, { - taskId: validatedData.taskId, - }) - - return NextResponse.json({ - success: true, - output: { - url: result.pushNotificationConfig.url, - token: result.pushNotificationConfig.token, - success: true, - }, - }) - } catch (error) { - logger.error(`[${requestId}] Error setting A2A push notification:`, error) - - return NextResponse.json( - { - success: false, - error: 'Failed to set push notification', - }, - { status: 500 } - ) - } -}) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 13ee516f661..246a62ab694 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -454,7 +454,7 @@ async function handleExecutePost( } // Programmatic execution (API key or public API) is gated on the workflow's - // workspace billed account — the same entity MCP/A2A/webhooks/chat gate on — + // workspace billed account — the same entity MCP/webhooks/chat gate on — // so a paid workspace is never blocked because an individual is on free. if (auth.authType === AuthType.API_KEY || isPublicApiAccess) { if (!gateWorkspaceId) { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index da6b50613a5..cedeb949c5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -72,7 +72,6 @@ const TRIGGER_VARIANT_MAP: Record['va chat: 'purple', webhook: 'orange', mcp: 'cyan', - a2a: 'teal', copilot: 'pink', mothership: 'pink', workflow: 'blue-secondary', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx deleted file mode 100644 index 90c3222cf4c..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ /dev/null @@ -1,869 +0,0 @@ -'use client' - -import { useEffect, useMemo, useState } from 'react' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { Check, Clipboard } from 'lucide-react' -import { useParams } from 'next/navigation' -import { - Button, - ButtonGroup, - ButtonGroupItem, - Checkbox, - ChipInput, - Code, - Input, - Label, - Skeleton, - TagInput, - Textarea, - Tooltip, -} from '@/components/emcn' -import type { AgentAuthentication, AgentCapabilities } from '@/lib/a2a/types' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format' -import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers' -import { - useA2AAgentByWorkflow, - useCreateA2AAgent, - useDeleteA2AAgent, - usePublishA2AAgent, - useUpdateA2AAgent, -} from '@/hooks/queries/a2a/agents' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' - -const logger = createLogger('A2ADeploy') - -interface InputFormatField { - id?: string - name?: string - type?: string - value?: unknown - collapsed?: boolean -} - -/** - * Check if a description is a default/placeholder value that should be filtered out - */ -function isDefaultDescription(desc: string | null | undefined, workflowName: string): boolean { - if (!desc) return true - const normalized = desc.toLowerCase().trim() - return ( - normalized === '' || - normalized === 'new workflow' || - normalized === 'your first workflow - start building here!' || - normalized === workflowName.toLowerCase() - ) -} - -type CodeLanguage = 'curl' | 'python' | 'javascript' | 'typescript' - -const LANGUAGE_LABELS: Record = { - curl: 'cURL', - python: 'Python', - javascript: 'JavaScript', - typescript: 'TypeScript', -} - -const LANGUAGE_SYNTAX: Record = { - curl: 'javascript', - python: 'python', - javascript: 'javascript', - typescript: 'javascript', -} - -interface A2aDeployProps { - workflowId: string - workflowName: string - workflowDescription?: string | null - isDeployed: boolean - workflowNeedsRedeployment?: boolean - onSubmittingChange?: (submitting: boolean) => void - onCanSaveChange?: (canSave: boolean) => void - onNeedsRepublishChange?: (needsRepublish: boolean) => void - onDeployWorkflow?: () => Promise -} - -type AuthScheme = 'none' | 'apiKey' - -export function A2aDeploy({ - workflowId, - workflowName, - workflowDescription, - isDeployed, - workflowNeedsRedeployment, - onSubmittingChange, - onCanSaveChange, - onNeedsRepublishChange, - onDeployWorkflow, -}: A2aDeployProps) { - const params = useParams() - const workspaceId = params.workspaceId as string - - const { data: existingAgent, isLoading } = useA2AAgentByWorkflow(workspaceId, workflowId) - - const createAgent = useCreateA2AAgent() - const updateAgent = useUpdateA2AAgent() - const deleteAgent = useDeleteA2AAgent() - const publishAgent = usePublishA2AAgent() - - const blocks = useWorkflowStore((state) => state.blocks) - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() - - const startBlockId = useMemo(() => { - if (!blocks || Object.keys(blocks).length === 0) return null - const candidate = TriggerUtils.findStartBlock(blocks, 'api') - if (!candidate || candidate.path !== StartBlockPath.UNIFIED) return null - return candidate.blockId - }, [blocks]) - - const startBlockInputFormat = useSubBlockStore((state) => { - if (!workflowId || !startBlockId) return null - const workflowValues = state.workflowValues[workflowId] - const fromStore = workflowValues?.[startBlockId]?.inputFormat - if (fromStore !== undefined) return fromStore - const startBlock = blocks[startBlockId] - return startBlock?.subBlocks?.inputFormat?.value ?? null - }) - - const missingFields = useMemo(() => { - if (!startBlockId) return { input: false, data: false, files: false, any: false } - const normalizedFields = normalizeInputFormatValue(startBlockInputFormat) - const existingNames = new Set( - normalizedFields - .map((field) => field.name) - .filter((n): n is string => typeof n === 'string' && n.trim() !== '') - .map((n) => n.trim().toLowerCase()) - ) - const missing = { - input: !existingNames.has('input'), - data: !existingNames.has('data'), - files: !existingNames.has('files'), - any: false, - } - missing.any = missing.input || missing.data || missing.files - return missing - }, [startBlockId, startBlockInputFormat]) - - const handleAddA2AInputs = () => { - if (!startBlockId) return - - const normalizedExisting = normalizeInputFormatValue(startBlockInputFormat) - const newFields: InputFormatField[] = [] - - if (missingFields.input) { - newFields.push({ - id: generateId(), - name: 'input', - type: 'string', - value: '', - collapsed: false, - }) - } - - if (missingFields.data) { - newFields.push({ - id: generateId(), - name: 'data', - type: 'object', - value: '', - collapsed: false, - }) - } - - if (missingFields.files) { - newFields.push({ - id: generateId(), - name: 'files', - type: 'file[]', - value: '', - collapsed: false, - }) - } - - if (newFields.length > 0) { - const updatedFields = [...newFields, ...normalizedExisting] - collaborativeSetSubblockValue(startBlockId, 'inputFormat', updatedFields) - logger.info( - `Added A2A input fields to Start block: ${newFields.map((f) => f.name).join(', ')}` - ) - } - } - - const [name, setName] = useState('') - const [description, setDescription] = useState('') - const [authScheme, setAuthScheme] = useState('apiKey') - const [pushNotificationsEnabled, setPushNotificationsEnabled] = useState(false) - const [skillTags, setSkillTags] = useState([]) - const [language, setLanguage] = useState('curl') - const [useStreamingExample, setUseStreamingExample] = useState(false) - const [urlCopied, setUrlCopied] = useState(false) - const [codeCopied, setCodeCopied] = useState(false) - - useEffect(() => { - if (existingAgent) { - setName(existingAgent.name) - const savedDesc = existingAgent.description || '' - setDescription(isDefaultDescription(savedDesc, workflowName) ? '' : savedDesc) - setPushNotificationsEnabled(existingAgent.capabilities?.pushNotifications ?? false) - const schemes = existingAgent.authentication?.schemes || [] - if (schemes.includes('apiKey')) { - setAuthScheme('apiKey') - } else { - setAuthScheme('none') - } - const skills = existingAgent.skills as Array<{ tags?: string[] }> | undefined - const savedTags = skills?.[0]?.tags - setSkillTags(savedTags?.length ? savedTags : []) - } else { - setName(workflowName) - setDescription( - isDefaultDescription(workflowDescription, workflowName) ? '' : workflowDescription || '' - ) - setAuthScheme('apiKey') - setPushNotificationsEnabled(false) - setSkillTags([]) - } - }, [existingAgent, workflowName, workflowDescription]) - - const hasFormChanges = useMemo(() => { - if (!existingAgent) return false - const savedSchemes = existingAgent.authentication?.schemes || [] - const savedAuthScheme = savedSchemes.includes('apiKey') ? 'apiKey' : 'none' - const savedDesc = existingAgent.description || '' - const normalizedSavedDesc = isDefaultDescription(savedDesc, workflowName) ? '' : savedDesc - const skills = existingAgent.skills as Array<{ tags?: string[] }> | undefined - const savedTags = skills?.[0]?.tags || [] - const tagsChanged = - skillTags.length !== savedTags.length || skillTags.some((t, i) => t !== savedTags[i]) - return ( - name !== existingAgent.name || - description !== normalizedSavedDesc || - pushNotificationsEnabled !== (existingAgent.capabilities?.pushNotifications ?? false) || - authScheme !== savedAuthScheme || - tagsChanged - ) - }, [ - existingAgent, - name, - description, - pushNotificationsEnabled, - authScheme, - skillTags, - workflowName, - ]) - - const hasWorkflowChanges = existingAgent ? !!workflowNeedsRedeployment : false - - const needsRepublish = existingAgent && (hasFormChanges || hasWorkflowChanges) - - useEffect(() => { - onNeedsRepublishChange?.(!!needsRepublish) - }, [needsRepublish, onNeedsRepublishChange]) - - const canSave = name.trim().length > 0 && description.trim().length > 0 - useEffect(() => { - onCanSaveChange?.(canSave) - }, [canSave, onCanSaveChange]) - - const isSubmitting = - createAgent.isPending || - updateAgent.isPending || - deleteAgent.isPending || - publishAgent.isPending - - useEffect(() => { - onSubmittingChange?.(isSubmitting) - }, [isSubmitting, onSubmittingChange]) - - const handleCreateOrUpdate = async () => { - const capabilities: AgentCapabilities = { - streaming: true, - pushNotifications: pushNotificationsEnabled, - stateTransitionHistory: true, - } - - const authentication: AgentAuthentication = { - schemes: authScheme === 'none' ? ['none'] : [authScheme], - } - - try { - if (existingAgent) { - await updateAgent.mutateAsync({ - agentId: existingAgent.id, - name: name.trim(), - description: description.trim() || undefined, - capabilities, - authentication, - skillTags, - }) - } else { - await createAgent.mutateAsync({ - workspaceId, - workflowId, - name: name.trim(), - description: description.trim() || undefined, - capabilities, - authentication, - skillTags, - }) - } - } catch (error) { - logger.error('Failed to save A2A agent:', error) - } - } - - const handlePublish = async () => { - if (!existingAgent) return - try { - await publishAgent.mutateAsync({ - agentId: existingAgent.id, - workspaceId, - action: 'publish', - }) - } catch (error) { - logger.error('Failed to publish A2A agent:', error) - } - } - - const handleUnpublish = async () => { - if (!existingAgent) return - try { - await publishAgent.mutateAsync({ - agentId: existingAgent.id, - workspaceId, - action: 'unpublish', - }) - } catch (error) { - logger.error('Failed to unpublish A2A agent:', error) - } - } - - const handleDelete = async () => { - if (!existingAgent) return - try { - await deleteAgent.mutateAsync({ - agentId: existingAgent.id, - workspaceId, - }) - setName(workflowName) - setDescription(workflowDescription || '') - } catch (error) { - logger.error('Failed to delete A2A agent:', error) - } - } - - const handlePublishNewAgent = async () => { - const capabilities: AgentCapabilities = { - streaming: true, - pushNotifications: pushNotificationsEnabled, - stateTransitionHistory: true, - } - - const authentication: AgentAuthentication = { - schemes: authScheme === 'none' ? ['none'] : [authScheme], - } - - try { - if (!isDeployed && onDeployWorkflow) { - await onDeployWorkflow() - } - - const newAgent = await createAgent.mutateAsync({ - workspaceId, - workflowId, - name: name.trim(), - description: description.trim() || undefined, - capabilities, - authentication, - skillTags, - }) - - await publishAgent.mutateAsync({ - agentId: newAgent.id, - workspaceId, - action: 'publish', - }) - } catch (error) { - logger.error('Failed to publish A2A agent:', error) - } - } - - const handleUpdateAndRepublish = async () => { - if (!existingAgent) return - - const capabilities: AgentCapabilities = { - streaming: true, - pushNotifications: pushNotificationsEnabled, - stateTransitionHistory: true, - } - - const authentication: AgentAuthentication = { - schemes: authScheme === 'none' ? ['none'] : [authScheme], - } - - try { - if ((!isDeployed || workflowNeedsRedeployment) && onDeployWorkflow) { - await onDeployWorkflow() - } - - await updateAgent.mutateAsync({ - agentId: existingAgent.id, - name: name.trim(), - description: description.trim() || undefined, - capabilities, - authentication, - skillTags, - }) - - await publishAgent.mutateAsync({ - agentId: existingAgent.id, - workspaceId, - action: 'publish', - }) - } catch (error) { - logger.error('Failed to update and republish A2A agent:', error) - } - } - - const baseUrl = getBaseUrl() - const endpoint = existingAgent ? `${baseUrl}/api/a2a/serve/${existingAgent.id}` : null - - const additionalInputFields = useMemo(() => { - const allFields = normalizeInputFormatValue(startBlockInputFormat) - return allFields.filter( - (field): field is InputFormatField & { name: string } => - !!field.name && - field.name.toLowerCase() !== 'input' && - field.name.toLowerCase() !== 'data' && - field.name.toLowerCase() !== 'files' - ) - }, [startBlockInputFormat]) - - const getExampleInputData = (): Record => { - const data: Record = {} - for (const field of additionalInputFields) { - switch (field.type) { - case 'string': - data[field.name] = 'example' - break - case 'number': - data[field.name] = 42 - break - case 'boolean': - data[field.name] = true - break - case 'object': - data[field.name] = { key: 'value' } - break - case 'array': - data[field.name] = [1, 2, 3] - break - default: - data[field.name] = 'example' - } - } - return data - } - - const getJsonRpcPayload = (): Record => { - const inputData = getExampleInputData() - const hasAdditionalData = Object.keys(inputData).length > 0 - - const parts: Array> = [{ kind: 'text', text: 'Hello, agent!' }] - if (hasAdditionalData) { - parts.push({ kind: 'data', data: inputData }) - } - - return { - jsonrpc: '2.0', - id: '1', - method: useStreamingExample ? 'message/stream' : 'message/send', - params: { - message: { - role: 'user', - parts, - }, - }, - } - } - - const getCurlCommand = (): string => { - if (!endpoint) return '' - const payload = getJsonRpcPayload() - const requiresAuth = authScheme !== 'none' - - switch (language) { - case 'curl': - return requiresAuth - ? `curl -X POST \\ - -H "X-API-Key: $SIM_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '${JSON.stringify(payload)}' \\ - ${endpoint}` - : `curl -X POST \\ - -H "Content-Type: application/json" \\ - -d '${JSON.stringify(payload)}' \\ - ${endpoint}` - - case 'python': - return requiresAuth - ? `import os -import requests - -response = requests.post( - "${endpoint}", - headers={ - "X-API-Key": os.environ.get("SIM_API_KEY"), - "Content-Type": "application/json" - }, - json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} -) - -print(response.json())` - : `import requests - -response = requests.post( - "${endpoint}", - headers={"Content-Type": "application/json"}, - json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} -) - -print(response.json())` - - case 'javascript': - return requiresAuth - ? `const response = await fetch("${endpoint}", { - method: "POST", - headers: { - "X-API-Key": process.env.SIM_API_KEY, - "Content-Type": "application/json" - }, - body: JSON.stringify(${JSON.stringify(payload)}) -}); - -const data = await response.json(); -console.log(data);` - : `const response = await fetch("${endpoint}", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(${JSON.stringify(payload)}) -}); - -const data = await response.json(); -console.log(data);` - - case 'typescript': - return requiresAuth - ? `const response = await fetch("${endpoint}", { - method: "POST", - headers: { - "X-API-Key": process.env.SIM_API_KEY, - "Content-Type": "application/json" - }, - body: JSON.stringify(${JSON.stringify(payload)}) -}); - -const data: Record = await response.json(); -console.log(data);` - : `const response = await fetch("${endpoint}", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(${JSON.stringify(payload)}) -}); - -const data: Record = await response.json(); -console.log(data);` - - default: - return '' - } - } - - const handleCopyCommand = () => { - navigator.clipboard.writeText(getCurlCommand()) - setCodeCopied(true) - setTimeout(() => setCodeCopied(false), 2000) - } - - if (isLoading) { - return ( -
-
- - - -
-
- - -
-
- - -
-
- - -
-
- ) - } - - return ( -
{ - e.preventDefault() - handleCreateOrUpdate() - }} - className='-mx-1 space-y-3 overflow-y-auto px-1 pb-4' - > - {existingAgent && endpoint && ( -
-
- - - - - - - {urlCopied ? 'Copied' : 'Copy'} - - -
-
-
- {baseUrl.replace(/^https?:\/\//, '')}/api/a2a/serve/ -
-
- -
-
-

- The A2A endpoint URL where clients can discover and call your agent -

-
- )} - -
- - setName(e.target.value)} - placeholder='Enter agent name' - required - /> -

- Human-readable name shown in the Agent Card -

-
- -
- -