diff --git a/docs/en/API_REFERENCE.md b/docs/en/API_REFERENCE.md index f3db2ca..6c7f349 100644 --- a/docs/en/API_REFERENCE.md +++ b/docs/en/API_REFERENCE.md @@ -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 | @@ -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 @@ -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 @@ -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. diff --git a/docs/en/INTEGRATION_GUIDE.md b/docs/en/INTEGRATION_GUIDE.md index 18c3ffd..183f2f0 100644 --- a/docs/en/INTEGRATION_GUIDE.md +++ b/docs/en/INTEGRATION_GUIDE.md @@ -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 diff --git a/docs/zh/API_REFERENCE.md b/docs/zh/API_REFERENCE.md index 96b0c50..ede9390 100644 --- a/docs/zh/API_REFERENCE.md +++ b/docs/zh/API_REFERENCE.md @@ -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` | 协调等待超时(秒) | @@ -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 @@ -622,6 +624,7 @@ a2at: | `requestHandler` | `RequestHandler` | 默认请求处理器 | | `restHandler` | `RestHandler` | REST 协议处理器 | | `a2aController` | `A2AController` | 消息和任务端点的 Spring MVC 控制器 | +| `a2aSlashActionAliasController` | 内部控制器 | 可选的斜杠形式动作端点别名 | ### A2AController @@ -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 停止事件消费,避免服务端轮询继续占用资源。 diff --git a/docs/zh/INTEGRATION_GUIDE.md b/docs/zh/INTEGRATION_GUIDE.md index bfb215e..188d04c 100644 --- a/docs/zh/INTEGRATION_GUIDE.md +++ b/docs/zh/INTEGRATION_GUIDE.md @@ -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 状态和标准 diff --git a/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AAutoConfiguration.java b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AAutoConfiguration.java index 03514a6..31bbbf4 100644 --- a/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AAutoConfiguration.java +++ b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AAutoConfiguration.java @@ -67,6 +67,7 @@ *
The partner only needs to provide an {@link AgentExecutor} implementation as a @@ -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); + } } diff --git a/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AProperties.java b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AProperties.java index c6fa7e0..af986f1 100644 --- a/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AProperties.java +++ b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2AProperties.java @@ -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; @@ -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; } diff --git a/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2ASlashActionAliasController.java b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2ASlashActionAliasController.java new file mode 100644 index 0000000..51d8683 --- /dev/null +++ b/spring-boot-starter/src/main/java/dev/openan/workflow/engine/spring/A2ASlashActionAliasController.java @@ -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. + * + *
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