Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/en/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ Configuration properties prefixed with `a2at.server`:
|----------------------------------------------|----------------------------|------------------------------------------------------------------------|
| `a2at.server.agent-card` | `classpath:agentcard.json` | Path to the AgentCard JSON file (classpath: or file: prefix supported) |
| `a2at.server.path-prefix` | `/a2a/json` | URL path prefix for A2A endpoints |
| `a2at.server.slash-action-aliases-enabled` | `false` | Expose slash-style action aliases for gateways that reject colons |
| `a2at.server.agent-timeout-seconds` | `30` | Agent execution timeout in seconds |
| `a2at.server.consumption-timeout-seconds` | `5` | Consumption timeout in seconds |
| `a2at.server.reconciliation-timeout-seconds` | `1` | Reconciliation wait timeout in seconds |
Expand All @@ -633,6 +634,7 @@ a2at:
server:
agent-card: classpath:agentcard/my_agent.json
path-prefix: /a2a/json
slash-action-aliases-enabled: false
agent-timeout-seconds: 30
executor-core-size: 8
executor-max-size: 16
Expand All @@ -656,6 +658,7 @@ Auto-configures the following beans (all `@ConditionalOnMissingBean`, so you can
| `requestHandler` | `RequestHandler` | Default request handler |
| `restHandler` | `RestHandler` | REST protocol handler |
| `a2aController` | `A2AController` | Spring MVC controller for message and task endpoints |
| `a2aSlashActionAliasController` | internal controller | Optional slash-style aliases for action endpoints |

### A2AController

Expand All @@ -668,6 +671,20 @@ Spring MVC controller that exposes A2A REST endpoints:
- `POST {path-prefix}/tasks/{id}:cancel` — cancel a task
- `POST {path-prefix}/tasks/{id}:subscribe` — subscribe to task updates over SSE

The colon-style routes are the canonical A2A HTTP+JSON endpoints and are always enabled. When
`a2at.server.slash-action-aliases-enabled=true`, the starter additionally exposes compatibility aliases that delegate
to the same controller and therefore have identical authentication, request bodies, responses, errors, streaming and
lifecycle behavior:

- `POST {path-prefix}/message/send`
- `POST {path-prefix}/message/stream`
- `POST {path-prefix}/tasks/{id}/cancel`
- `POST {path-prefix}/tasks/{id}/subscribe`

Prefer an API-gateway rewrite from slash-style public paths to canonical colon-style backend paths. Enable the aliases
only when the gateway cannot rewrite the backend path. The aliases do not change the AgentCard schema or make operation
suffixes configurable in the A2A Java REST client.

HTTP+JSON responses use `application/a2a+json`; successful streaming responses use `text/event-stream`. When a stream
closes, the controller cancels its publisher subscription and notifies the SDK event consumer so that server-side
polling does not remain active.
Expand Down
27 changes: 27 additions & 0 deletions docs/en/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,33 @@ Cleanup is enabled and fail-fast by default so stale tasks cannot silently lead
the demo may cancel active tasks created by another installation; use an isolated identity or disable cleanup after
providing an equivalent ownership-aware policy.

### 13.4 API gateways that reject colon-style paths

A2A HTTP+JSON defines action endpoints such as `message:send`, `message:stream`, `tasks/{id}:cancel` and
`tasks/{id}:subscribe`. Keep these canonical routes enabled. Prefer configuring the API gateway to publish slash-style
paths and rewrite them to the canonical backend paths, for example:

```text
/gateway/service/a2a/json/message/send -> /a2a/json/message:send
/gateway/service/a2a/json/message/stream -> /a2a/json/message:stream
```

If the gateway cannot rewrite the backend path, enable the starter's aliases:

```yaml
a2at:
server:
path-prefix: /a2a/json
slash-action-aliases-enabled: true
```

The gateway can then forward `/message/send` and `/message/stream` unchanged. The same option also exposes
`/tasks/{id}/cancel` and `/tasks/{id}/subscribe`. Do not put the gateway-owned public prefix into `path-prefix` when the
gateway removes it before forwarding; add it only when the application actually receives that prefix. The A2A-T SDK
does not send HTTP requests, and the A2A Java REST transport appends canonical operation suffixes to the AgentCard base
URL. A caller that must emit slash-style paths therefore needs gateway rewriting or a transport adapter; changing the
AgentCard base URL alone is insufficient.

## 14. A2A errors and task failures

Keep failures before and after task creation separate. A request rejected before a task is created uses a
Expand Down
15 changes: 15 additions & 0 deletions docs/zh/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ classpath 时,自动将所有 A2A SDK 服务端组件注册为 Spring Bean。
|----------------------------------------------|----------------------------|----------------------------------------------------------|
| `a2at.server.agent-card` | `classpath:agentcard.json` | AgentCard JSON 文件路径(支持 classpath: 或 file: 前缀) |
| `a2at.server.path-prefix` | `/a2a/json` | A2A 端点的 URL 路径前缀 |
| `a2at.server.slash-action-aliases-enabled` | `false` | 为拒绝冒号路径的网关暴露斜杠动作别名 |
| `a2at.server.agent-timeout-seconds` | `30` | Agent 执行超时(秒) |
| `a2at.server.consumption-timeout-seconds` | `5` | 消费超时(秒) |
| `a2at.server.reconciliation-timeout-seconds` | `1` | 协调等待超时(秒) |
Expand All @@ -599,6 +600,7 @@ a2at:
server:
agent-card: classpath:agentcard/my_agent.json
path-prefix: /a2a/json
slash-action-aliases-enabled: false
agent-timeout-seconds: 30
executor-core-size: 8
executor-max-size: 16
Expand All @@ -622,6 +624,7 @@ a2at:
| `requestHandler` | `RequestHandler` | 默认请求处理器 |
| `restHandler` | `RestHandler` | REST 协议处理器 |
| `a2aController` | `A2AController` | 消息和任务端点的 Spring MVC 控制器 |
| `a2aSlashActionAliasController` | 内部控制器 | 可选的斜杠形式动作端点别名 |

### A2AController

Expand All @@ -634,6 +637,18 @@ a2at:
- `POST {path-prefix}/tasks/{id}:cancel` — 取消任务
- `POST {path-prefix}/tasks/{id}:subscribe` — 通过 SSE 订阅任务更新

冒号形式是 A2A HTTP+JSON 标准端点,始终启用。配置
`a2at.server.slash-action-aliases-enabled=true` 后,starter 还会暴露以下兼容别名;它们委托给同一个控制器,
认证、请求体、响应、错误、流式处理和生命周期行为完全相同:

- `POST {path-prefix}/message/send`
- `POST {path-prefix}/message/stream`
- `POST {path-prefix}/tasks/{id}/cancel`
- `POST {path-prefix}/tasks/{id}/subscribe`

优先在 API 网关把公开的斜杠路径重写到后端标准冒号路径;只有网关不能重写后端路径时才开启别名。
别名不会改变 AgentCard 结构,也不会让 A2A Java REST 客户端的操作后缀变成可配置项。

HTTP+JSON 响应使用 `application/a2a+json`,成功建立的流式响应使用 `text/event-stream`。连接结束时,控制器会取消
Publisher 订阅并通知 SDK 停止事件消费,避免服务端轮询继续占用资源。

Expand Down
26 changes: 26 additions & 0 deletions docs/zh/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,32 @@ Demo 在打开独立协议通道和启动工作流之前,先查询每个被调
`A2A_TASK_CLEANUP_MAX_TASKS` 配置。任务查询受认证身份权限约束;若多个实例共用同一身份,Demo 可能取消其他实例创建的活跃任务。
应使用隔离身份,或在提供等价的任务归属清理策略后关闭该功能。

### 13.4 不接受冒号路径的 API 网关

A2A HTTP+JSON 定义了 `message:send`、`message:stream`、`tasks/{id}:cancel` 和
`tasks/{id}:subscribe` 等动作端点,标准端点应始终保留。首选由 API 网关发布斜杠形式的公开路径,
并重写到后端标准路径,例如:

```text
/gateway/service/a2a/json/message/send -> /a2a/json/message:send
/gateway/service/a2a/json/message/stream -> /a2a/json/message:stream
```

如果网关不能重写后端路径,可开启 starter 的兼容别名:

```yaml
a2at:
server:
path-prefix: /a2a/json
slash-action-aliases-enabled: true
```

此时网关可以原样转发 `/message/send` 和 `/message/stream`;同一配置还会开放
`/tasks/{id}/cancel` 和 `/tasks/{id}/subscribe`。如果网关转发前会去掉它管理的公开前缀,
不要把该前缀写进 `path-prefix`;只有应用实际收到该前缀时才需要配置。A2A-T SDK 不发送 HTTP 请求,
A2A Java REST transport 会在 AgentCard 基础 URL 后追加标准动作后缀。因此,必须发出斜杠路径的调用方
需要使用网关重写或传输适配器,仅修改 AgentCard 基础 URL 不够。

## 14. A2A 错误与任务失败

任务创建前和创建后的失败必须分开处理。请求在任务创建前被拒绝时,返回非 2xx HTTP 状态和标准
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
* <li>{@link MainEventBusProcessor} - the event bus
* <li>{@link A2AController} - the Spring MVC controller (message send/stream plus task
* query, list, cancel, and subscribe endpoints)
* <li>Optional slash-style aliases for gateways that cannot publish colon-style action paths
* </ul>
*
* <p>The partner only needs to provide an {@link AgentExecutor} implementation as a
Expand Down Expand Up @@ -214,4 +215,17 @@ public A2AController a2aController(
RestHandler restHandler, RequestHandler requestHandler, AgentCard agentCard) {
return new A2AController(restHandler, requestHandler, agentCard);
}

/**
* Adds non-standard slash-style action aliases without changing the canonical A2A endpoints.
* This compatibility surface is disabled unless explicitly enabled for a constrained gateway.
*/
@Bean
@ConditionalOnMissingBean(A2ASlashActionAliasController.class)
@ConditionalOnProperty(
name = "a2at.server.slash-action-aliases-enabled",
havingValue = "true")
A2ASlashActionAliasController a2aSlashActionAliasController(A2AController delegate) {
return new A2ASlashActionAliasController(delegate);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public class A2AProperties {
/** URL path prefix for A2A endpoints; all controller mappings are anchored under it. */
private String pathPrefix = "/a2a/json";

/**
* Whether to expose slash-style aliases for A2A action endpoints for gateways that reject
* colons in published paths. Standard colon-style endpoints always remain available.
*/
private boolean slashActionAliasesEnabled = false;

/** Timeout for blocking agent execution. */
private int agentTimeoutSeconds = 30;

Expand Down Expand Up @@ -89,6 +95,14 @@ public void setPathPrefix(String pathPrefix) {
this.pathPrefix = pathPrefix;
}

public boolean isSlashActionAliasesEnabled() {
return slashActionAliasesEnabled;
}

public void setSlashActionAliasesEnabled(boolean slashActionAliasesEnabled) {
this.slashActionAliasesEnabled = slashActionAliasesEnabled;
}

public int getAgentTimeoutSeconds() {
return agentTimeoutSeconds;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2026 OpenAN. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.openan.workflow.engine.spring;

import jakarta.servlet.http.HttpServletRequest;
import org.a2aproject.sdk.spec.A2AError;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
* Optional slash-style aliases for A2A action endpoints.
*
* <p>Every operation delegates to {@link A2AController}; the aliases therefore retain the
* canonical endpoint's authentication, A2A error envelope, streaming, and cleanup behavior.
*/
@RestController
@ConditionalOnProperty(
name = "a2at.server.slash-action-aliases-enabled",
havingValue = "true")
final class A2ASlashActionAliasController {

private final A2AController delegate;

A2ASlashActionAliasController(A2AController delegate) {
this.delegate = delegate;
}

@PostMapping("${a2at.server.path-prefix}/message/send")
ResponseEntity<String> sendMessage(HttpServletRequest request, @RequestBody String body) {
return delegate.sendMessage(request, body);
}

@PostMapping(
value = "${a2at.server.path-prefix}/message/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter streamMessage(HttpServletRequest request, @RequestBody String body) {
return delegate.streamMessage(request, body);
}

@PostMapping("${a2at.server.path-prefix}/tasks/{id}/cancel")
ResponseEntity<String> cancelTask(
HttpServletRequest request,
@PathVariable("id") String taskId,
@RequestBody(required = false) String body) {
return delegate.cancelTask(request, taskId, body);
}

@PostMapping(
value = "${a2at.server.path-prefix}/tasks/{id}/subscribe",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter subscribeToTask(HttpServletRequest request, @PathVariable("id") String taskId) {
return delegate.subscribeToTask(request, taskId);
}

@ExceptionHandler(A2AError.class)
ResponseEntity<String> a2aError(A2AError error) {
return delegate.a2aError(error);
}
}
Loading