Skip to content

Commit 790f04b

Browse files
committed
add block
1 parent ff27367 commit 790f04b

22 files changed

Lines changed: 1667 additions & 19 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: A2A
3+
description: Interact with external A2A-compatible agents
4+
---
5+
6+
import { BlockInfoCard } from "@/components/ui/block-info-card"
7+
8+
<BlockInfoCard
9+
type="a2a"
10+
color="#4151B5"
11+
/>
12+
13+
{/* MANUAL-CONTENT-START:intro */}
14+
The A2A (Agent-to-Agent) protocol lets Sim call external AI agents that expose an A2A-compatible endpoint. Use it to connect your workflows to remote agents — LLM-powered bots, microservices, and other AI systems — through a standardized message format.
15+
16+
With the A2A block you can:
17+
18+
- **Send messages to external agents**: Pass prompts, structured data, or files to a remote agent and get its response.
19+
- **Track and cancel tasks**: Poll the state of a long-running task or request its cancellation.
20+
- **Discover capabilities**: Fetch an agent's Agent Card to inspect its skills, capabilities, and supported modes.
21+
22+
You need the external agent's endpoint URL and, if it requires authentication, an API key.
23+
{/* MANUAL-CONTENT-END */}
24+
25+
## Usage Instructions
26+
27+
Use the A2A (Agent-to-Agent) protocol to call external AI agents over the latest A2A specification.
28+
29+
## Tools
30+
31+
### `a2a_send_message`
32+
33+
Send a message to an external A2A agent and return its response.
34+
35+
#### Input
36+
37+
| Parameter | Type | Required | Description |
38+
| --------- | ---- | -------- | ----------- |
39+
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
40+
| `message` | string | Yes | The message text to send |
41+
| `data` | string | No | Optional structured JSON data to attach \(JSON string or object\) |
42+
| `files` | array | No | Files to attach, uploaded or referenced from a previous block |
43+
| `taskId` | string | No | Existing task ID to continue |
44+
| `contextId` | string | No | Conversation context ID to continue |
45+
| `apiKey` | string | No | API key for authentication \(if required\) |
46+
47+
#### Output
48+
49+
| Parameter | Type | Description |
50+
| --------- | ---- | ----------- |
51+
| `content` | string | Agent response text |
52+
| `taskId` | string | Task identifier |
53+
| `contextId` | string | Conversation/context identifier |
54+
| `state` | string | Task lifecycle state: `submitted`, `working`, `input-required`, `auth-required`, `completed`, `failed`, `canceled`, or `rejected` |
55+
| `artifacts` | array | Structured task output artifacts |
56+
57+
### `a2a_get_task`
58+
59+
Retrieve the current state and result of an A2A task.
60+
61+
#### Input
62+
63+
| Parameter | Type | Required | Description |
64+
| --------- | ---- | -------- | ----------- |
65+
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
66+
| `taskId` | string | Yes | The task ID to retrieve |
67+
| `historyLength` | number | No | Maximum number of history messages to include |
68+
| `apiKey` | string | No | API key for authentication \(if required\) |
69+
70+
#### Output
71+
72+
| Parameter | Type | Description |
73+
| --------- | ---- | ----------- |
74+
| `content` | string | Agent response text |
75+
| `taskId` | string | Task identifier |
76+
| `contextId` | string | Conversation/context identifier |
77+
| `state` | string | Task lifecycle state |
78+
| `artifacts` | array | Structured task output artifacts |
79+
80+
### `a2a_cancel_task`
81+
82+
Request cancellation of an in-progress A2A task.
83+
84+
#### Input
85+
86+
| Parameter | Type | Required | Description |
87+
| --------- | ---- | -------- | ----------- |
88+
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
89+
| `taskId` | string | Yes | The task ID to cancel |
90+
| `apiKey` | string | No | API key for authentication \(if required\) |
91+
92+
#### Output
93+
94+
| Parameter | Type | Description |
95+
| --------- | ---- | ----------- |
96+
| `taskId` | string | Task identifier |
97+
| `state` | string | Task lifecycle state after cancellation |
98+
| `canceled` | boolean | Whether the task reached the canceled state |
99+
100+
### `a2a_get_agent_card`
101+
102+
Fetch the Agent Card (discovery document) for an external A2A agent.
103+
104+
#### Input
105+
106+
| Parameter | Type | Required | Description |
107+
| --------- | ---- | -------- | ----------- |
108+
| `agentUrl` | string | Yes | The A2A agent endpoint URL |
109+
| `apiKey` | string | No | API key for authentication \(if required\) |
110+
111+
#### Output
112+
113+
| Parameter | Type | Description |
114+
| --------- | ---- | ----------- |
115+
| `name` | string | Agent display name |
116+
| `description` | string | Agent description |
117+
| `url` | string | Agent endpoint URL |
118+
| `version` | string | The agent's own version |
119+
| `protocolVersion` | string | A2A protocol version the agent exposes |
120+
| `capabilities` | json | Agent capability flags |
121+
| `skills` | array | Skills the agent can perform |
122+
| `defaultInputModes` | array | Default accepted input media types |
123+
| `defaultOutputModes` | array | Default produced output media types |
124+
125+
## Notes
126+
127+
- Category: `blocks`
128+
- Type: `a2a`
129+
- Send Message blocks until the agent reaches a terminal (`completed`, `failed`, `canceled`, `rejected`) or interrupted (`input-required`, `auth-required`) state. Use Get Task to poll a task you continue later, and branch on `state`.
130+
- Task IDs are scoped to the external agent, not to Sim. Anyone who knows an agent URL and task ID can read or cancel that task unless the agent enforces its own authentication — set an API key for agents that require one.

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"pages": [
33
"index",
4+
"a2a",
45
"agentmail",
56
"agentphone",
67
"agiloft",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { createA2AClient, taskOutput } from '@/lib/a2a/client'
5+
import { a2aCancelTaskContract } from '@/lib/api/contracts/tools/a2a'
6+
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
7+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
9+
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
12+
export const dynamic = 'force-dynamic'
13+
export const maxDuration = 60
14+
15+
const logger = createLogger('A2ACancelTaskAPI')
16+
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
18+
const requestId = generateRequestId()
19+
20+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
21+
if (!auth.success) {
22+
return NextResponse.json(
23+
{ success: false, error: auth.error || 'Authentication required' },
24+
{ status: 401 }
25+
)
26+
}
27+
28+
const rateLimited = await enforceUserOrIpRateLimit('a2a-cancel-task', auth.userId, request)
29+
if (rateLimited) return rateLimited
30+
31+
const parsed = await parseRequest(
32+
a2aCancelTaskContract,
33+
request,
34+
{},
35+
{
36+
validationErrorResponse: (error) =>
37+
NextResponse.json(
38+
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
39+
{ status: 400 }
40+
),
41+
}
42+
)
43+
if (!parsed.success) return parsed.response
44+
const body = parsed.data.body
45+
46+
try {
47+
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
48+
const task = await client.cancelTask({ tenant: '', id: body.taskId, metadata: undefined })
49+
const out = taskOutput(task)
50+
51+
logger.info(`[${requestId}] Cancel requested for A2A task ${task.id}`)
52+
return NextResponse.json({
53+
success: true,
54+
output: { taskId: out.taskId, state: out.state, canceled: out.state === 'canceled' },
55+
})
56+
} catch (error) {
57+
logger.error(`[${requestId}] A2A cancel-task failed`, { error: getErrorMessage(error) })
58+
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
59+
}
60+
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { agentCardOutput, createA2AClient } from '@/lib/a2a/client'
5+
import { a2aGetAgentCardContract } from '@/lib/api/contracts/tools/a2a'
6+
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
7+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
9+
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
12+
export const dynamic = 'force-dynamic'
13+
export const maxDuration = 60
14+
15+
const logger = createLogger('A2AGetAgentCardAPI')
16+
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
18+
const requestId = generateRequestId()
19+
20+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
21+
if (!auth.success) {
22+
return NextResponse.json(
23+
{ success: false, error: auth.error || 'Authentication required' },
24+
{ status: 401 }
25+
)
26+
}
27+
28+
const rateLimited = await enforceUserOrIpRateLimit('a2a-get-agent-card', auth.userId, request)
29+
if (rateLimited) return rateLimited
30+
31+
const parsed = await parseRequest(
32+
a2aGetAgentCardContract,
33+
request,
34+
{},
35+
{
36+
validationErrorResponse: (error) =>
37+
NextResponse.json(
38+
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
39+
{ status: 400 }
40+
),
41+
}
42+
)
43+
if (!parsed.success) return parsed.response
44+
const body = parsed.data.body
45+
46+
try {
47+
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
48+
const card = await client.getAgentCard()
49+
50+
logger.info(`[${requestId}] Fetched agent card for ${card.name}`)
51+
return NextResponse.json({ success: true, output: agentCardOutput(card, body.agentUrl) })
52+
} catch (error) {
53+
logger.error(`[${requestId}] A2A get-agent-card failed`, { error: getErrorMessage(error) })
54+
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
55+
}
56+
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { createA2AClient, taskOutput } from '@/lib/a2a/client'
5+
import { a2aGetTaskContract } from '@/lib/api/contracts/tools/a2a'
6+
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
7+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
9+
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
12+
export const dynamic = 'force-dynamic'
13+
export const maxDuration = 60
14+
15+
const logger = createLogger('A2AGetTaskAPI')
16+
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
18+
const requestId = generateRequestId()
19+
20+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
21+
if (!auth.success) {
22+
return NextResponse.json(
23+
{ success: false, error: auth.error || 'Authentication required' },
24+
{ status: 401 }
25+
)
26+
}
27+
28+
const rateLimited = await enforceUserOrIpRateLimit('a2a-get-task', auth.userId, request)
29+
if (rateLimited) return rateLimited
30+
31+
const parsed = await parseRequest(
32+
a2aGetTaskContract,
33+
request,
34+
{},
35+
{
36+
validationErrorResponse: (error) =>
37+
NextResponse.json(
38+
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
39+
{ status: 400 }
40+
),
41+
}
42+
)
43+
if (!parsed.success) return parsed.response
44+
const body = parsed.data.body
45+
46+
try {
47+
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
48+
const task = await client.getTask({
49+
tenant: '',
50+
id: body.taskId,
51+
historyLength: body.historyLength,
52+
})
53+
54+
logger.info(`[${requestId}] Retrieved A2A task ${task.id}`)
55+
return NextResponse.json({ success: true, output: taskOutput(task) })
56+
} catch (error) {
57+
logger.error(`[${requestId}] A2A get-task failed`, { error: getErrorMessage(error) })
58+
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
59+
}
60+
})

0 commit comments

Comments
 (0)