From 9e677724a7bcca862fb0f55e2beb2e29409b48bd Mon Sep 17 00:00:00 2001 From: Zhoujie628 Date: Mon, 14 Sep 2026 20:40:58 +0800 Subject: [PATCH 1/4] feat(engine): make Negotiation-T activation host-owned Negotiation-T was previously treated as an implicit capability: a target agent could start negotiation from an incomplete task even when the caller never selected it, and the engine drove the auto-loop either way. Now the business activates Negotiation-T per send via the new MessageContent.withExtension(...); the engine mirrors that selection into message.extensions and the A2A-Extensions request header and validates that the target AgentCard declares the extension before dispatch. An unsolicited INPUT_REQUIRED negotiation (host did not activate Negotiation-T) fails fast instead of silently entering the loop. The sample agent executor requires the request header to have activated Negotiation-T before serving a Propose. Tests cover activation without negotiation metadata on the wire, rejection of activation against an undeclared target, unactivated incomplete tasks staying out of negotiation, E2E wire-level A2A-Extensions evidence, and withExtension immutability. Signed-off-by: Zhoujie628 --- README.md | 2 +- docs/en/API_REFERENCE.md | 5 + docs/en/BUSINESS_CALLBACKS.md | 7 + docs/en/DESIGN.md | 4 + docs/en/DEVELOPER_GUIDE.md | 5 + docs/en/INTEGRATION_GUIDE.md | 13 ++ docs/zh/API_REFERENCE.md | 4 + docs/zh/BUSINESS_CALLBACKS.md | 5 + docs/zh/DESIGN.md | 3 + docs/zh/DEVELOPER_GUIDE.md | 4 + docs/zh/INTEGRATION_GUIDE.md | 10 ++ .../agents/NegotiationBaseAgentExecutor.java | 18 +- .../examples/demo/A2ATProtocolCases.java | 6 +- .../workbench/WorkbenchControlPoint.java | 4 +- .../examples/demo/SpringSpnDemoE2ETest.java | 9 + .../server/EmbeddedA2AServerTest.java | 28 +++- .../workflow/engine/client/A2ATExtension.java | 5 +- .../client/DefaultWorkflowEngineClient.java | 99 ++++++----- .../workflow/engine/model/MessageContent.java | 7 + .../engine/client/A2ATransportHeaderTest.java | 28 +++- ...ltWorkflowEngineClientNegotiationTest.java | 158 +++++++++++------- .../engine/model/MessageContentTest.java | 14 ++ 22 files changed, 321 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 2511aa51..bbf9a9c2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ return final message content and own any A2A-T generation, schema, validation or ## Features -- **A2A-T Extension Support**: Task-T (structured task prompts), Negotiation-T (stateless auto negotiation loop), +- **A2A-T Extension Support**: Task-T (structured task prompts), Negotiation-T (host-activated auto negotiation loop), Authorization-T (independent authorization operation), Notification-T (independent long-lived SSE subscription) - **Content-neutral callbacks**: final MessageContent, complete ReceivedMessage, local multi-output TaskResult and explicit NegotiationReply.Send/Stop diff --git a/docs/en/API_REFERENCE.md b/docs/en/API_REFERENCE.md index 451dd3f3..eaf27b18 100644 --- a/docs/en/API_REFERENCE.md +++ b/docs/en/API_REFERENCE.md @@ -96,6 +96,11 @@ parts/metadata/extensions. The engine manages envelopes and interactions, never instantiates A2ATClient or generates content from AgentCard declarations. Template queries and generation belong to the host SDK. +Declaring Negotiation-T in an AgentCard does not activate it. When the host business allows negotiation for the current +task, call `withExtension(A2ATExtension.NEGOTIATION_T.uri())` on the initial Task-T `MessageContent`; the engine mirrors +that selection into the message extensions and the `A2A-Extensions` request header. `onNegotiation` and +`NegotiationStrategy` only handle a received Propose and do not modify the initial extension set. + Only a remote `INPUT_REQUIRED` carrying valid Negotiation-T Propose enters `onNegotiation`. Terminal responses never restart negotiation; ordinary `INPUT_REQUIRED` fails explicitly. The host validates/interprets the proposal and generates the final Accept/Reject/Abort with its own A2A-T client. Use `A2atMessages.contextOf(request.received())` to diff --git a/docs/en/BUSINESS_CALLBACKS.md b/docs/en/BUSINESS_CALLBACKS.md index 04e23baf..07ff7260 100644 --- a/docs/en/BUSINESS_CALLBACKS.md +++ b/docs/en/BUSINESS_CALLBACKS.md @@ -120,6 +120,13 @@ one. Sample initialization helpers are host-side examples, not engine dependenci ## 5. Negotiation +The host business explicitly decides whether the current task allows negotiation. An AgentCard declaration only says +that the target supports Negotiation-T. To allow it, call +`withExtension(A2ATExtension.NEGOTIATION_T.uri())` on the initial Task-T content. The engine then writes both +`message.extensions` and the `A2A-Extensions` request header without adding premature Negotiation-T metadata. Merely +implementing `onNegotiation` or supplying a `NegotiationStrategy` does not activate negotiation; the target must not +start Negotiation-T when it was not activated. + NegotiationRequest(task, originalSubmission, received, previousExchanges, remainingWait): task is the original TaskRequest; originalSubmission is the exact initial content; received is the complete current response; previousExchanges contains only this session's completed Exchange (received, reply); remainingWait is the diff --git a/docs/en/DESIGN.md b/docs/en/DESIGN.md index 966acb39..b53b92cd 100644 --- a/docs/en/DESIGN.md +++ b/docs/en/DESIGN.md @@ -261,6 +261,10 @@ generation. Negotiation-T: +The host business decides whether a task allows negotiation through the initial `MessageContent.extensions`. Neither +the AgentCard declaration nor the callback itself activates it. The engine only maps that selection to the message and +the `A2A-Extensions` request header, then manages subsequent negotiation correlation. + Only a remote `INPUT_REQUIRED` carrying valid Negotiation-T Propose enters `onNegotiation`. Terminal responses never restart negotiation; ordinary `INPUT_REQUIRED` fails explicitly. The host validates/interprets the proposal and generates the final Accept/Reject/Abort with its own A2A-T client. Use `A2atMessages.contextOf(request.received())` to diff --git a/docs/en/DEVELOPER_GUIDE.md b/docs/en/DEVELOPER_GUIDE.md index 1a66b1b9..d825d991 100644 --- a/docs/en/DEVELOPER_GUIDE.md +++ b/docs/en/DEVELOPER_GUIDE.md @@ -131,6 +131,11 @@ ExecutionResult result = executor.run().join(); ### 6.1 Negotiation Auto-Loop +When the host business allows negotiation for a task, call +`withExtension(A2ATExtension.NEGOTIATION_T.uri())` on the initial Task-T `MessageContent`. An AgentCard declaration or a +negotiation callback does not add the request header automatically; the engine only maps the business selection to +`message.extensions` and `A2A-Extensions`. + Only a remote `INPUT_REQUIRED` carrying valid Negotiation-T Propose enters `onNegotiation`. Terminal responses never restart negotiation; ordinary `INPUT_REQUIRED` fails explicitly. The host validates/interprets the proposal and generates the final Accept/Reject/Abort with its own A2A-T client. Use `A2atMessages.contextOf(request.received())` to diff --git a/docs/en/INTEGRATION_GUIDE.md b/docs/en/INTEGRATION_GUIDE.md index 88049192..cb943542 100644 --- a/docs/en/INTEGRATION_GUIDE.md +++ b/docs/en/INTEGRATION_GUIDE.md @@ -392,6 +392,19 @@ authentication. ## 7. A2A-T Extensions +Declaring Negotiation-T in the AgentCard means the target supports it; it does not enable negotiation for every task. +The host business selects it on the initial final content: + +```java +MessageContent outgoing = A2atMessages.from(generatedTask, parts) + .withExtension(A2ATExtension.NEGOTIATION_T.uri()); +``` + +The engine writes this extension set to both the A2A Message and the `A2A-Extensions` request header. Initial metadata +still contains only the Task-T body; do not manufacture Negotiation-T metadata before receiving a Propose. Configuring +a negotiation callback alone does not activate the extension, and tasks that disallow negotiation must not call +`withExtension`. + Only a remote `INPUT_REQUIRED` carrying valid Negotiation-T Propose enters `onNegotiation`. Terminal responses never restart negotiation; ordinary `INPUT_REQUIRED` fails explicitly. The host validates/interprets the proposal and generates the final Accept/Reject/Abort with its own A2A-T client. Use `A2atMessages.contextOf(request.received())` to diff --git a/docs/zh/API_REFERENCE.md b/docs/zh/API_REFERENCE.md index 1d900ed6..87097034 100644 --- a/docs/zh/API_REFERENCE.md +++ b/docs/zh/API_REFERENCE.md @@ -89,6 +89,10 @@ A2A 标准动作名称 `sendMessage`。 `onTask` 返回后由执行器内部调用 `sendTask`;`onTask` 不自行发送。内容是最终 parts/metadata/extensions,引擎只管理信封和交互,不创建 A2ATClient,不按 AgentCard 声明生成内容。模板查询和生成接口请直接使用宿主 SDK。 +AgentCard 声明 Negotiation-T 不会激活协商。宿主业务需要允许当前任务协商时,在首轮 Task-T `MessageContent` 上调用 +`withExtension(A2ATExtension.NEGOTIATION_T.uri())`;引擎将业务选择同步到消息扩展和 `A2A-Extensions` 请求头。 +`onNegotiation` 或 `NegotiationStrategy` 只处理已收到的 Propose,不隐式修改首轮扩展。 + 只有远端 `INPUT_REQUIRED` 携带有效 Negotiation-T Propose 才进入 `onNegotiation`。 终态不会重启协商,普通 INPUT_REQUIRED 明确报告不支持的交互。 宿主自行校验、理解 Propose,并用自己的 A2A-T client 生成最终 Accept/Reject/Abort。 通过 `A2atMessages.contextOf(request.received())` 取得收到的上下文; 结束回复保持相同 id、round、maxRounds,最后允许的一轮仍可回答,不自行 diff --git a/docs/zh/BUSINESS_CALLBACKS.md b/docs/zh/BUSINESS_CALLBACKS.md index 03629d75..f59364f4 100644 --- a/docs/zh/BUSINESS_CALLBACKS.md +++ b/docs/zh/BUSINESS_CALLBACKS.md @@ -105,6 +105,11 @@ MessageContent outgoing = A2atMessages.from(generated, List.of(new TextPart("处 ## 5. 协商 +是否允许本次任务协商由宿主业务显式决定。AgentCard 声明 Negotiation-T 只表示目标具备能力;需要允许协商时,宿主在首轮 +Task-T 内容上调用 `withExtension(A2ATExtension.NEGOTIATION_T.uri())`。引擎据此同时写入 `message.extensions` 和 +`A2A-Extensions` 请求头,但不添加首轮 Negotiation-T metadata。仅实现 `onNegotiation` 或传入 `NegotiationStrategy` +不会自动激活协商;未激活时对端不得发起 Negotiation-T。 + NegotiationRequest(task, originalSubmission, received, previousExchanges, remainingWait): task 是原始 TaskRequest,originalSubmission 是首次最终提交, received 是当前完整响应,previousExchanges 只含该会话的 Exchange (received, reply), remainingWait 是本次交互剩余时间。引擎不规定业务 proposal 分类或 schema。 diff --git a/docs/zh/DESIGN.md b/docs/zh/DESIGN.md index aee0bb9d..d0810d6b 100644 --- a/docs/zh/DESIGN.md +++ b/docs/zh/DESIGN.md @@ -236,6 +236,9 @@ Task-T:宿主生成最终内容,引擎只封装并发送。AgentCard 声明 Negotiation-T: +宿主业务通过首轮 `MessageContent.extensions` 决定本次任务是否允许协商;AgentCard 声明和协商回调本身不触发激活。引擎只把该选择 +映射为消息扩展和 `A2A-Extensions` 请求头,并管理后续协商关联。 + 只有远端 `INPUT_REQUIRED` 携带有效 Negotiation-T Propose 才进入 `onNegotiation`。 终态不会重启协商,普通 INPUT_REQUIRED 明确报告不支持的交互。 宿主自行校验、理解 Propose,并用自己的 A2A-T client 生成最终 Accept/Reject/Abort。 通过 `A2atMessages.contextOf(request.received())` 取得收到的上下文; 结束回复保持相同 id、round、maxRounds,最后允许的一轮仍可回答,不自行 diff --git a/docs/zh/DEVELOPER_GUIDE.md b/docs/zh/DEVELOPER_GUIDE.md index 5c0e76f4..4467ca9d 100644 --- a/docs/zh/DEVELOPER_GUIDE.md +++ b/docs/zh/DEVELOPER_GUIDE.md @@ -127,6 +127,10 @@ try (var client = new DefaultWorkflowEngineClient(agentCards, a2aRuntime, ### 6.1 协商自动循环 +宿主业务需要允许当前任务协商时,在首轮 Task-T `MessageContent` 上调用 +`withExtension(A2ATExtension.NEGOTIATION_T.uri())`。AgentCard 声明或实现协商回调不会自动增加请求头;引擎只把业务选择映射到 +`message.extensions` 和 `A2A-Extensions`。 + 只有远端 `INPUT_REQUIRED` 携带有效 Negotiation-T Propose 才进入 `onNegotiation`。 终态不会重启协商,普通 INPUT_REQUIRED 明确报告不支持的交互。 宿主自行校验、理解 Propose,并用自己的 A2A-T client 生成最终 Accept/Reject/Abort。 通过 `A2atMessages.contextOf(request.received())` 取得收到的上下文; 结束回复保持相同 id、round、maxRounds,最后允许的一轮仍可回答,不自行 diff --git a/docs/zh/INTEGRATION_GUIDE.md b/docs/zh/INTEGRATION_GUIDE.md index af8e81bd..24cf2705 100644 --- a/docs/zh/INTEGRATION_GUIDE.md +++ b/docs/zh/INTEGRATION_GUIDE.md @@ -376,6 +376,16 @@ AgentCard 通过 `capabilities.extensions` 声明扩展点: ## 7. A2A-T 扩展能力 +AgentCard 声明 Negotiation-T 只表示目标支持协商,不代表本次任务已经启用。是否允许协商由宿主业务在首轮最终内容中选择: + +```java +MessageContent outgoing = A2atMessages.from(generatedTask, parts) + .withExtension(A2ATExtension.NEGOTIATION_T.uri()); +``` + +引擎将 `extensions` 同时写入 A2A Message 和 `A2A-Extensions` 请求头。首轮 metadata 仍然只有 Task-T 正文;不要在收到 +Propose 前伪造 Negotiation-T metadata。仅配置协商回调不会自动激活扩展,不允许协商的任务不要调用 `withExtension`。 + 只有远端 `INPUT_REQUIRED` 携带有效 Negotiation-T Propose 才进入 `onNegotiation`。 终态不会重启协商,普通 INPUT_REQUIRED 明确报告不支持的交互。 宿主自行校验、理解 Propose,并用自己的 A2A-T client 生成最终 Accept/Reject/Abort。 通过 `A2atMessages.contextOf(request.received())` 取得收到的上下文; 结束回复保持相同 id、round、maxRounds,最后允许的一轮仍可回答,不自行 diff --git a/samples/src/main/java/dev/openan/workflow/engine/examples/agents/NegotiationBaseAgentExecutor.java b/samples/src/main/java/dev/openan/workflow/engine/examples/agents/NegotiationBaseAgentExecutor.java index 5b3f0ec9..27dee6af 100644 --- a/samples/src/main/java/dev/openan/workflow/engine/examples/agents/NegotiationBaseAgentExecutor.java +++ b/samples/src/main/java/dev/openan/workflow/engine/examples/agents/NegotiationBaseAgentExecutor.java @@ -59,9 +59,9 @@ * Accept reply it validates and consumes the filled parameters before diagnosis. Reject and Abort * are validated separately and end the task without executing diagnosis. * - *

Negotiation is triggered by incomplete SDK-validated task data, not merely by extension - * activation. A configured A2A-T client/server is mandatory for protocol generation and validation; - * the sample never manufactures protocol-shaped fallback text. + *

Negotiation is triggered only when the caller activates Negotiation-T and SDK-validated task + * data is incomplete. A configured A2A-T client/server is mandatory for protocol generation and + * validation; the sample never manufactures protocol-shaped fallback text. */ public abstract class NegotiationBaseAgentExecutor extends BaseAgentExecutor { @@ -391,6 +391,7 @@ private void handleNewTask(RequestContext ctx, AgentEmitter emitter) { } List missing = invalidTaskFields(data); if (!missing.isEmpty()) { + activateNegotiation(ctx); requestNegotiation(ctx, emitter, data, missing); } else { log.info("[{}] Parameters sufficient, skipping negotiation", getClass().getSimpleName()); @@ -405,6 +406,7 @@ private boolean isNegotiationReply(RequestContext ctx) { /** Replies are read from Negotiation-T metadata, validated by phase, then consumed once. */ private void handleFollowUp(RequestContext ctx, AgentEmitter emitter) { + activateNegotiation(ctx); Map metadata = ctx.getMessage().metadata(); var context = java.util.Objects.requireNonNull( @@ -453,6 +455,16 @@ private void handleFollowUp(RequestContext ctx, AgentEmitter emitter) { } } + private void activateNegotiation(RequestContext ctx) { + var callContext = ctx.getCallContext(); + String extension = A2ATExtension.NEGOTIATION_T.uri(); + if (callContext == null || !callContext.isExtensionRequested(extension)) { + throw new IllegalArgumentException( + "Negotiation-T was not activated in the A2A-Extensions request header"); + } + callContext.activateExtension(extension); + } + private void acceptReply( RequestContext ctx, AgentEmitter emitter, diff --git a/samples/src/main/java/dev/openan/workflow/engine/examples/demo/A2ATProtocolCases.java b/samples/src/main/java/dev/openan/workflow/engine/examples/demo/A2ATProtocolCases.java index 8f6543dc..5e77c798 100644 --- a/samples/src/main/java/dev/openan/workflow/engine/examples/demo/A2ATProtocolCases.java +++ b/samples/src/main/java/dev/openan/workflow/engine/examples/demo/A2ATProtocolCases.java @@ -294,7 +294,8 @@ private void case_7_3() { List.of( new org.a2aproject.sdk.spec.TextPart(SpnCasePrompts.TASK_TEXT + "(参数缺失)")), metadata, - java.util.Set.of(A2ATExtension.TASK_T.uri()))) + java.util.Set.of(A2ATExtension.TASK_T.uri())) + .withExtension(A2ATExtension.NEGOTIATION_T.uri())) .join(); requireCompletedNegotiation("7.3", result, before); log.info( @@ -320,7 +321,8 @@ private void case_7_4() { List.of( new org.a2aproject.sdk.spec.TextPart(SpnCasePrompts.TASK_TEXT + "(语义错误)")), metadata, - java.util.Set.of(A2ATExtension.TASK_T.uri()))) + java.util.Set.of(A2ATExtension.TASK_T.uri())) + .withExtension(A2ATExtension.NEGOTIATION_T.uri())) .join(); requireCompletedNegotiation("7.4", result, before); log.info( diff --git a/samples/src/main/java/dev/openan/workflow/engine/examples/workbench/WorkbenchControlPoint.java b/samples/src/main/java/dev/openan/workflow/engine/examples/workbench/WorkbenchControlPoint.java index e091678d..958737f1 100644 --- a/samples/src/main/java/dev/openan/workflow/engine/examples/workbench/WorkbenchControlPoint.java +++ b/samples/src/main/java/dev/openan/workflow/engine/examples/workbench/WorkbenchControlPoint.java @@ -130,7 +130,9 @@ public CompletableFuture onTask(TaskRequest request) { "[onTask] DEMO_NEGOTIATION agent={}, fault=missing-port," + " source=explicit-sample-switch", request.getAgentName()); - return new MessageContent(content.parts(), metadata, content.extensions()); + return new MessageContent(content.parts(), metadata, content.extensions()) + .withExtension( + dev.openan.workflow.engine.client.A2ATExtension.NEGOTIATION_T.uri()); } return content; })); diff --git a/samples/src/test/java/dev/openan/workflow/engine/examples/demo/SpringSpnDemoE2ETest.java b/samples/src/test/java/dev/openan/workflow/engine/examples/demo/SpringSpnDemoE2ETest.java index 4822529b..7d5d0605 100644 --- a/samples/src/test/java/dev/openan/workflow/engine/examples/demo/SpringSpnDemoE2ETest.java +++ b/samples/src/test/java/dev/openan/workflow/engine/examples/demo/SpringSpnDemoE2ETest.java @@ -137,7 +137,16 @@ protected String negotiationWireBoundary() { private void assertNegotiationWireEvidence(String evidence) { String uri = dev.openan.workflow.engine.client.A2ATExtension.NEGOTIATION_T.uri(); + String taskUri = dev.openan.workflow.engine.client.A2ATExtension.TASK_T.uri(); var records = java.util.Arrays.asList(evidence.split("(?m)^.*? PROTOCOL - ")); + org.junit.jupiter.api.Assertions.assertTrue( + records.stream() + .anyMatch( + record -> + record.startsWith("[" + negotiationWireBoundary() + "] REQUEST") + && record.contains("A2A-Extensions: " + taskUri) + && record.contains(uri)), + "Missing Negotiation-T activation on the initial Task-T request"); String request = records.stream() .filter(record -> record.startsWith("[" + negotiationWireBoundary() + "] REQUEST")) diff --git a/samples/src/test/java/dev/openan/workflow/engine/examples/server/EmbeddedA2AServerTest.java b/samples/src/test/java/dev/openan/workflow/engine/examples/server/EmbeddedA2AServerTest.java index f2d82609..0de706a4 100644 --- a/samples/src/test/java/dev/openan/workflow/engine/examples/server/EmbeddedA2AServerTest.java +++ b/samples/src/test/java/dev/openan/workflow/engine/examples/server/EmbeddedA2AServerTest.java @@ -359,7 +359,8 @@ void testNegotiationAutoLoopCompletes() { new dev.openan.workflow.engine.model.MessageContent( List.of(new org.a2aproject.sdk.spec.TextPart("diagnose SPN fault")), metadata, - java.util.Set.of(A2ATExtension.TASK_T.uri())), + java.util.Set.of(A2ATExtension.TASK_T.uri())) + .withExtension(A2ATExtension.NEGOTIATION_T.uri()), negotiation -> callbacks.onNegotiation( new dev.openan.workflow.engine.model.NegotiationRequest( @@ -389,7 +390,8 @@ private SendMessageResult startIncomplete() { new MessageContent( List.of(new org.a2aproject.sdk.spec.TextPart("diagnose")), SpnCasePrompts.taskTMetadata(SpnCasePrompts.privateLineComplaintPromptBlankObject()), - java.util.Set.of(A2ATExtension.TASK_T.uri())); + java.util.Set.of(A2ATExtension.TASK_T.uri())) + .withExtension(A2ATExtension.NEGOTIATION_T.uri()); var result = transport .send( @@ -400,6 +402,25 @@ private SendMessageResult startIncomplete() { return result; } + @Test + void incompleteTaskWithoutNegotiationActivationDoesNotStartNegotiation() { + var content = + new MessageContent( + List.of(new org.a2aproject.sdk.spec.TextPart("diagnose")), + SpnCasePrompts.taskTMetadata(SpnCasePrompts.privateLineComplaintPromptBlankObject()), + java.util.Set.of(A2ATExtension.TASK_T.uri())); + + var result = + transport + .send( + agentCard, AGENT_NAME, content, java.util.UUID.randomUUID().toString(), null, null) + .join(); + + assertEquals("TASK_STATE_FAILED", result.getTaskState()); + assertTrue(result.getText().contains("Negotiation-T was not activated"), result.getText()); + assertEquals(0, diagnosisCount); + } + private NegotiationContext negotiationContext(SendMessageResult pending) { return pending.getReceivedMessages().stream() .map(A2atMessages::contextOf) @@ -505,7 +526,8 @@ void unknownPortNegotiatesOnlyObjectAndKeepsValidatedContext() { AGENT_NAME, A2atMessages.from( generated, - List.of(new org.a2aproject.sdk.spec.TextPart("diagnose unknown port"))), + List.of(new org.a2aproject.sdk.spec.TextPart("diagnose unknown port"))) + .withExtension(A2ATExtension.NEGOTIATION_T.uri()), java.util.UUID.randomUUID().toString(), null, null) diff --git a/workflow-engine/src/main/java/dev/openan/workflow/engine/client/A2ATExtension.java b/workflow-engine/src/main/java/dev/openan/workflow/engine/client/A2ATExtension.java index 9b0dd486..559f9630 100644 --- a/workflow-engine/src/main/java/dev/openan/workflow/engine/client/A2ATExtension.java +++ b/workflow-engine/src/main/java/dev/openan/workflow/engine/client/A2ATExtension.java @@ -27,7 +27,8 @@ *

Each enum constant encapsulates the full extension URI so callers never need to hardcode URI * strings. Use these when explicitly activating final message content. * - *

The engine handles these extensions automatically: + *

The engine supports these extensions while the host keeps business activation and content + * decisions: * *