You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@eventmesh.apache.org by ch...@apache.org on 2026/08/17 09:38:55 UTC
(eventmesh) branch develop updated: feat: lite-topic — internal EventMeshFrame architecture, streaming calls, sticky load balancing
This is an automated email from the ASF dual-hosted git repository.
qqeasonchen pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/eventmesh.git
The following commit(s) were added to refs/heads/develop by this push:
new 7735ee123 feat: lite-topic — internal EventMeshFrame architecture, streaming calls, sticky load balancing
7735ee123 is described below
commit 7735ee12333d33c4037413e96f8b8f120fadb210
Author: qqeasonchen <qq...@gmail.com>
AuthorDate: Mon Aug 17 17:02:26 2026 +0800
feat: lite-topic — internal EventMeshFrame architecture, streaming calls, sticky load balancing
Squash of lite-topic branch (34 commits) + checkstyle fixes, secrets scrubbed.
Architecture (internal data model overhaul):
- Internal wire unified as EventMeshFrame (14B fixed header + KV attrs + raw data,
msgType = STREAM_REQ | STREAM_CHUNK | EVENT); CloudEvent demoted to an external
ingress format only. Storage SPI and the full runtime dispatch pipeline all carry
EventMeshFrame.
- FrameAdaptor SPI: per-protocol plugins convert external formats directly to/from
EventMeshFrame with zero CloudEvent intermediary — protocol-cloudevents (new
standalone plugin), protocol-meshmessage, protocol-a2a. Empty protocol-grpc removed.
- WireCodec SPI: pluggable internal MQ byte encoding (default EventMeshFrameCodec).
Streaming calls (two orthogonal modes):
- Mode 1 streaming call: openSession -> call -> forEach, multi-turn context via
agent ConversationStore; channel multiplexing, matchmaker + agent registrar.
- Mode 2 publish/subscribe: deterministic lite naming (session.<sessionId>), no agent,
POST /session/publish + GET /session/subscribe SSE bridging.
- SDK: StreamingSession/StreamingResponse with forEach as the sole consumption
posture; SessionPublisher for mode-2 producing.
Offset management (path 2: self-managed + MQ replay):
- Two-layer offsets stay local; meta upload off by default.
- Deferred MQ ACK for RocketMQ 5.x POP (restores at-least-once); monotonic
writeOffset; restart cursor alignment via reserved __mqcursor__ key +
frame-carried emmqoffset/emmqpartition.
Sticky load balancing:
- Session-distribution-layer recommend (LoadMeter self-collected metrics, global
scoring, overload feedback); instanceUrl pinning; forwarding layer disabled.
Robustness fixes (P0-P2): SSE/WS write-failure nack, SDK HttpURLConnection leaks,
Delivery volatile fields, immutable SessionRegistry beans, synchronized
PushService.offer, dispatcher tick putIfAbsent, RocketMQ5 ack retry.
Ops: Prometheus /metrics scrape endpoint, mTLS client-auth, WebSocket IT verified,
real-Kafka E2E verified.
Style: all checkstyle violations fixed (imports/order/single-line/renames);
checkstyleMain + checkstyleTest 0 violations; full 'gradle check' green.
Secrets scrubbed from E2EConfig defaults (internal IPs/users/passwords/gateway
replaced with localhost placeholders / empty / api.openai.com).
---
.gitignore | 4 +
build.gradle | 66 +-
docs/a2a-protocol/ARCHITECTURE.md | 316 ---
docs/a2a-protocol/IMPLEMENTATION_SUMMARY.md | 103 -
docs/a2a-protocol/IMPLEMENTATION_SUMMARY_EN.md | 103 -
docs/a2a-protocol/README.md | 762 +++++-
docs/a2a-protocol/README_EN.md | 255 --
docs/a2a-protocol/TEST_RESULTS.md | 112 -
docs/a2a-protocol/eventmesh-a2a-design.md | 478 ----
docs/eventmesh-cloudevents-client-guide.md | 149 +-
docs/eventmesh-offset-lb-frame-design.md | 388 +++
docs/eventmesh-uni-architecture-redesign.md | 398 ++-
docs/plugins/core-engines-configuration.md | 135 -
docs/production-readiness.md | 340 +--
docs/uni-architecture-features-and-tests.md | 73 -
eventmesh-agent/build.gradle | 43 +
eventmesh-agent/conf/agent.properties | 17 +
eventmesh-agent/conf/log4j2.xml | 19 +
.../apache/eventmesh/agent/AgentApplication.java | 96 +
.../apache/eventmesh/agent/AgentControlClient.java | 115 +
.../apache/eventmesh/agent/ConversationStore.java | 76 +
.../org/apache/eventmesh/agent/StreamingAgent.java | 135 +
.../eventmesh/agent/llm/OpenAiLlmClient.java | 133 +
.../eventmesh/agent/ConversationStoreTest.java | 82 +
.../eventmesh/agent/llm/OpenAiLlmClientTest.java | 152 ++
.../eventmesh/common/protocol/ByteTransport.java | 62 +
.../eventmesh/common/stream/StreamChunk.java | 66 +
.../eventmesh/common/stream/StreamEventCodec.java | 174 ++
.../eventmesh/common/stream/StreamRequest.java | 53 +
.../eventmesh/common/wire/EventMeshFrame.java | 360 +++
.../eventmesh/common/wire/EventMeshFrameCodec.java | 64 +
.../common/wire/MeshMessageFrameCodec.java | 103 +
.../org/apache/eventmesh/common/wire/MetaJson.java | 233 ++
.../apache/eventmesh/common/wire/WireCodec.java | 61 +
.../apache/eventmesh/common/wire/WireCodecs.java | 70 +
.../common/stream/StreamEventCodecTest.java | 180 ++
.../eventmesh/common/wire/EventMeshFrameTest.java | 186 ++
.../cloudevents/demo/stream/StreamingCallDemo.java | 160 ++
.../eventmesh/protocol/a2a/A2AFrameAdaptor.java | 119 +
.../org.apache.eventmesh.protocol.api.FrameAdaptor | 21 +-
.../eventmesh/protocol/api/FrameAdaptor.java | 104 +
.../eventmesh/protocol/api/FrameAdaptors.java | 100 +
.../eventmesh-protocol-cloudevents/build.gradle | 7 +
.../cloudevents/CloudEventsFrameAdaptor.java | 86 +
.../org.apache.eventmesh.protocol.api.FrameAdaptor | 22 +-
.../meshmessage/MeshMessageFrameAdaptor.java | 118 +
.../org.apache.eventmesh.protocol.api.FrameAdaptor | 22 +-
eventmesh-runtime/build.gradle | 163 +-
eventmesh-runtime/conf/eventmesh.properties | 4 +
.../eventmesh/runtime/a2a/A2ACardHttpHandler.java | 218 --
.../runtime/a2a/A2AGatewayHttpHandler.java | 648 -----
.../eventmesh/runtime/a2a/A2AGatewayServer.java | 338 ---
.../eventmesh/runtime/a2a/A2AGatewayService.java | 422 ---
.../runtime/a2a/A2APublishSubscribeService.java | 449 ----
.../runtime/a2a/EventMeshA2ATransport.java | 3 +-
.../runtime/a2a/InMemoryA2AMessageTransport.java | 118 -
.../apache/eventmesh/runtime/a2a/TaskRegistry.java | 348 ---
.../eventmesh/runtime/admin/UniAdminServer.java | 38 +
.../eventmesh/runtime/admin/UniAdminService.java | 22 +-
.../runtime/boot/EventMeshApplication.java | 161 +-
.../apache/eventmesh/runtime/boot/UniRuntime.java | 82 +-
.../runtime/cluster/ClusterCoordinator.java | 7 +-
.../runtime/cluster/ClusterMembership.java | 83 +-
.../runtime/cluster/ClusterSubscriptionStore.java | 3 +-
.../eventmesh/runtime/cluster/Forwarder.java | 3 +-
.../eventmesh/runtime/cluster/HttpForwarder.java | 7 +-
.../eventmesh/runtime/cluster/LocalDeliverer.java | 3 +-
.../eventmesh/runtime/delivery/DeadLetterSink.java | 6 +-
.../eventmesh/runtime/delivery/Delivery.java | 45 +-
.../eventmesh/runtime/delivery/PushChannel.java | 9 +-
.../runtime/delivery/ReliableDispatcher.java | 51 +-
.../eventmesh/runtime/delivery/WebHookChannel.java | 15 +-
.../eventmesh/runtime/http/UniHttpServer.java | 629 ++++-
.../eventmesh/runtime/ingress/LoadMeter.java | 173 ++
.../runtime/ingress/UniIngressService.java | 303 ++-
.../apache/eventmesh/runtime/metrics/UniTrace.java | 25 +
.../runtime/offset/InMemoryOffsetStore.java | 5 +-
.../runtime/offset/InMemoryPushOffsetStore.java | 111 -
.../eventmesh/runtime/offset/PushOffsetStore.java | 93 -
.../runtime/offset/RocksDBOffsetStore.java | 25 +-
.../eventmesh/runtime/push/BufferedEvent.java | 13 +-
.../apache/eventmesh/runtime/push/Connection.java | 7 +-
.../eventmesh/runtime/push/ConnectionPushPump.java | 14 +-
.../eventmesh/runtime/push/LongPollingChannel.java | 5 +-
.../apache/eventmesh/runtime/push/PushService.java | 66 +-
.../eventmesh/runtime/push/SseConnection.java | 42 +-
.../eventmesh/runtime/push/WsConnection.java | 16 +-
.../runtime/session/AgentAnchoredStrategy.java | 43 +
.../eventmesh/runtime/session/AgentRecord.java | 51 +
.../eventmesh/runtime/session/AgentRegistrar.java | 127 +
.../AgentStatus.java} | 32 +-
.../BrokerGroupHealth.java} | 24 +-
.../eventmesh/runtime/session/ChannelStrategy.java | 55 +
.../eventmesh/runtime/session/Matchmaker.java | 145 ++
.../eventmesh/runtime/session/SessionRegistry.java | 307 +++
.../eventmesh/runtime/session/SessionRouter.java | 468 ++++
.../runtime/subscription/CloudEventFilter.java | 24 +-
.../runtime/subscription/Subscription.java | 7 +-
.../runtime/subscription/SubscriptionManager.java | 32 +-
.../transport/tcp/MeshMessagePackageRouter.java | 30 +-
.../runtime/transport/tcp/NettyTcpPushChannel.java | 34 +-
.../runtime/transport/tcp/TcpPushChannel.java | 14 +-
.../runtime/transport/tcp/TcpRequest.java | 16 +-
.../runtime/transport/tcp/UniTcpServer.java | 2 +-
.../a2a/A2AClientServerIntegrationTest.java | 482 ----
.../runtime/a2a/A2AGatewayEndToEndTest.java | 234 --
.../runtime/a2a/A2AGatewayServiceTest.java | 407 ---
.../eventmesh/runtime/a2a/AgentCardTestUtils.java | 62 -
.../a2a/InMemoryA2AMessageTransportTest.java | 195 --
.../eventmesh/runtime/a2a/TaskRegistryTest.java | 297 ---
.../runtime/admin/PrometheusEndpointTest.java | 128 +
.../runtime/admin/UniAdminServerTest.java | 6 +-
.../runtime/admin/UniAdminServiceTest.java | 22 +-
.../runtime/boot/EventMeshApplicationTest.java | 6 +-
.../runtime/boot/PullOffsetAlignmentTest.java | 264 --
.../runtime/boot/RestartCursorAlignmentTest.java | 216 ++
.../eventmesh/runtime/boot/UniRuntimeTest.java | 12 +-
.../runtime/cluster/ClusterCoordinatorTest.java | 24 +-
.../runtime/cluster/ClusterMembershipLoadTest.java | 127 +
.../runtime/cluster/LoadBalancingScoringTest.java | 156 ++
.../delivery/DeferredAckDispatcherTest.java | 160 ++
.../delivery/DeliveryVolatileAndResurrectTest.java | 106 +
.../runtime/delivery/MqCursorRecordingTest.java | 118 +
.../runtime/delivery/ReliableDispatcherTest.java | 13 +-
.../runtime/delivery/WebHookChannelTest.java | 8 +-
.../http/LegacyHttpServerIntegrationTest.java | 22 +-
.../eventmesh/runtime/http/SecurityWiringTest.java | 6 +-
.../eventmesh/runtime/http/SseStreamTest.java | 10 +-
.../runtime/http/UniHttpServerEndpointTest.java | 6 +-
.../eventmesh/runtime/http/UniHttpServerTest.java | 10 +-
.../eventmesh/runtime/ingress/LoadMeterTest.java | 80 +
.../runtime/ingress/UniIngressServiceTest.java | 27 +-
.../it/AckTimeoutRedeliveryIntegrationTest.java | 5 +-
.../apache/eventmesh/runtime/it/AgentLauncher.java | 110 +
.../runtime/it/BatchPublishIntegrationTest.java | 20 +-
.../runtime/it/ClusterForwardIntegrationTest.java | 12 +-
.../it/ConnectorSchedulingIntegrationTest.java | 14 +-
.../eventmesh/runtime/it/DlqIntegrationTest.java | 25 +-
.../org/apache/eventmesh/runtime/it/E2EConfig.java | 133 +
.../runtime/it/KafkaClientE2EIntegrationTest.java | 8 +-
.../runtime/it/LegacyTcpClientIntegrationTest.java | 10 +-
.../runtime/it/LiteStreamCallIntegrationTest.java | 404 +++
.../it/NacosClusterForwardIntegrationTest.java | 12 +-
.../runtime/it/RateLimitIntegrationTest.java | 6 +-
.../it/RequestReplyHttpIntegrationTest.java | 20 +-
.../runtime/it/RocketMQ5BrokerIntegrationTest.java | 15 +-
.../eventmesh/runtime/it/RuntimeLauncher.java | 96 +
.../eventmesh/runtime/it/StreamingDemoRunner.java | 199 ++
.../eventmesh/runtime/it/StreamingSdkE2ETest.java | 586 +++++
.../eventmesh/runtime/it/TlsIntegrationTest.java | 13 +-
.../it/UnsubscribeTopicIntegrationTest.java | 10 +-
.../runtime/it/WebSocketPushIntegrationTest.java | 19 +-
.../offset/OffsetMonotonicAndRecoveryTest.java | 134 +
.../runtime/offset/PushOffsetStoreTest.java | 113 -
.../runtime/push/ConnectionPushPumpTest.java | 11 +-
.../push/PushServiceBufferOverflowTest.java | 105 +
.../eventmesh/runtime/push/PushServiceTest.java | 17 +-
.../runtime/session/AgentRegistrarTest.java | 78 +
.../runtime/session/ChannelStrategyTest.java | 53 +
.../session/FrameProtocolConversionTest.java | 164 ++
.../eventmesh/runtime/session/MatchmakerTest.java | 152 ++
.../runtime/session/SessionReaperTest.java | 105 +
.../session/SessionRegistryAtomicityTest.java | 94 +
.../runtime/session/SessionRegistryTest.java | 156 ++
.../subscription/SubscriptionManagerTest.java | 37 +-
.../transport/http/LegacyHttpBridgeTest.java | 21 +-
.../transport/tcp/TcpCompatibilityBridgeTest.java | 11 +-
.../runtime/transport/tcp/UniTcpServerTest.java | 24 +-
.../client/cloudevents/CloudEventsClient.java | 323 ++-
.../stream/DefaultStreamingOperations.java | 47 +
.../stream/DefaultStreamingResponse.java | 319 +++
.../client/cloudevents/stream/OpenSession.java | 54 +
.../cloudevents/stream/SessionPublisher.java | 121 +
.../client/cloudevents/stream/StreamException.java | 30 +-
.../client/cloudevents/stream/StreamRequest.java | 66 +
.../cloudevents/stream/StreamingOperations.java | 45 +
.../cloudevents/stream/StreamingResponse.java | 48 +
.../cloudevents/stream/StreamingSession.java | 110 +
.../impl/cloudevent/CloudEventTCPSubClient.java | 2 +-
.../EventMeshMessageTCPSubClient.java | 2 +-
.../client/cloudevents/CloudEventsClientTest.java | 10 +-
.../stream/DefaultStreamingResponseTest.java | 117 +
.../stream/StreamingSessionFakeServerTest.java | 153 ++
.../eventmesh/api/storage/LiteTopicCapable.java | 27 +-
.../eventmesh/api/storage/MeshStoragePlugin.java | 34 +-
.../eventmesh/api/storage/OffsetExtensions.java | 89 -
.../kafka/storage/KafkaMeshStoragePlugin.java | 175 +-
.../storage/kafka/KafkaBrokerIntegrationTest.java | 27 +-
.../storage/RocketMQRemotingStoragePlugin.java | 87 +-
.../storage/RocketMQ5RemotingStoragePlugin.java | 341 ++-
gradle.properties | 2 +-
resources/eventmesh-architecture-7.excalidraw | 2733 --------------------
resources/eventmesh-architecture-7.png | Bin 801505 -> 0 bytes
resources/logo.svg | 1 -
settings.gradle | 20 +-
195 files changed, 13803 insertions(+), 10481 deletions(-)
diff --git a/.gitignore b/.gitignore
index a3aa6cbf2..ae11e2691 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,7 @@ bld/
Cargo.lock
rocketmq-pull-offsets.properties
kafka-pull-offsets.properties
+
+# runtime state / dev tooling (never commit)
+.claude/
+eventmesh-runtime/data/
diff --git a/build.gradle b/build.gradle
index d884a799a..1065ba66c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,7 +31,10 @@ import java.util.concurrent.TimeUnit
buildscript {
repositories {
- mavenCentral()
+ maven {
+ allowInsecureProtocol = true
+ url "http://maven.weoa.com/nexus/content/groups/public/"
+ }
maven {
url "https://plugins.gradle.org/m2/"
}
@@ -40,7 +43,7 @@ buildscript {
dependencies {
classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.2.5"
classpath "io.spring.gradle:dependency-management-plugin:1.1.6"
- classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0"
+ classpath "com.diffplug.spotless:spotless-plugin-gradle:6.13.0"
classpath "org.apache.httpcomponents:httpclient:4.5.14"
classpath "commons-io:commons-io:2.18.0"
@@ -101,7 +104,10 @@ allprojects {
dependencies {
repositories {
- mavenCentral()
+ maven {
+ allowInsecureProtocol = true
+ url "http://maven.weoa.com/nexus/content/groups/public/"
+ }
}
testImplementation "org.junit.jupiter:junit-jupiter:5.11.0"
}
@@ -136,12 +142,16 @@ allprojects {
test {
useJUnitPlatform()
+ jvmArgs '-Dfile.encoding=UTF-8', '-Dstdout.encoding=UTF-8', '-Dstderr.encoding=UTF-8'
}
- // Disable doclint: it is overly strict on JDK 11+ and breaks javadoc generation
- // on comments that were valid under JDK 8. The uni-architecture baseline is JDK 21.
- tasks.withType(Javadoc).configureEach {
- options.addStringOption('Xdoclint:none', '-quiet')
+ // Remove doclint warnings that pollute javadoc logs when building
+ if (JavaVersion.current().isJava8()) {
+ tasks.withType(Javadoc).tap {
+ configureEach {
+ options.addStringOption('xdoclint:none', '-quiet')
+ }
+ }
}
}
@@ -286,6 +296,43 @@ tasks.register('dist-connector') {
}
}
+tasks.register('dist-agent') {
+ subprojects.forEach { subProject ->
+ dependsOn("${subProject.path}:jar")
+ }
+ dependsOn('generateDistLicense', 'generateDistNotice')
+ doLast {
+ // Core: the agent process jar + its third-party deps (sdk-java, common, cloudevents,
+ // jackson, log4j binding). The agent talks to the runtime only over HTTP + lite topics.
+ def core = findProject('eventmesh-agent')
+ logger.lifecycle('Install module: module: {}', core.name)
+ copy {
+ from core.jar.archivePath
+ into rootProject.file('dist-agent/apps')
+ }
+ copy {
+ from core.configurations.runtimeClasspath
+ into rootProject.file('dist-agent/lib')
+ exclude 'eventmesh-*'
+ }
+ copy {
+ from core.file('bin')
+ into rootProject.file('dist-agent/bin')
+ }
+ copy {
+ from core.file('conf')
+ from core.sourceSets.main.resources.srcDirs
+ into rootProject.file('dist-agent/conf')
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ exclude 'META-INF'
+ }
+ copy {
+ from 'tools/dist-license'
+ into rootProject.file('dist-agent')
+ }
+ }
+}
+
tasks.register('tar', Tar) {
archiveBaseName.set(project.name)
archiveVersion.set(project.version.toString())
@@ -634,7 +681,10 @@ subprojects {
}
repositories {
- mavenCentral()
+ maven {
+ allowInsecureProtocol = true
+ url "http://maven.weoa.com/nexus/content/groups/public/"
+ }
}
configurations.configureEach {
diff --git a/docs/a2a-protocol/ARCHITECTURE.md b/docs/a2a-protocol/ARCHITECTURE.md
deleted file mode 100644
index bcb485b10..000000000
--- a/docs/a2a-protocol/ARCHITECTURE.md
+++ /dev/null
@@ -1,316 +0,0 @@
-# EventMesh A2A Protocol Architecture & Functional Specification
-
-## 1. Overview
-
-The **EventMesh A2A (Agent-to-Agent) Protocol** is a specialized, high-performance protocol plugin designed to enable asynchronous communication, collaboration, and task coordination between autonomous agents.
-
-With the release of v2.0, A2A adopts the **MCP (Model Context Protocol)** architecture, transforming EventMesh into a robust **Agent Collaboration Bus**. It bridges the gap between synchronous LLM-based tool calls (JSON-RPC 2.0) and asynchronous Event-Driven Architectures (EDA), enabling scalable, distributed, and decoupled agent systems.
-
-## 2. Core Philosophy
-
-The architecture adheres to the principles outlined in the broader agent community (e.g., A2A Project, FIPA-ACL, and CloudEvents):
-
-1. **JSON-RPC 2.0 as Lingua Franca**: Uses standard JSON-RPC for payload semantics, ensuring compatibility with modern LLM ecosystems (LangChain, AutoGen).
-2. **Transport Agnostic**: Encapsulates all messages within **CloudEvents**, allowing transport over any EventMesh-supported protocol (HTTP, TCP, gRPC, Kafka).
-3. **Async by Default**: Maps synchronous Request/Response patterns to asynchronous Event streams using correlation IDs.
-4. **Native Pub/Sub Semantics**: Supports O(1) broadcast complexity, temporal decoupling (Late Join), and backpressure isolation, solving the scalability limits of traditional P2P webhook callbacks.
-
-### 2.1 Native Pub/Sub Semantics
-
-Traditional A2A implementations often rely on HTTP Webhooks (`POST /inbox`) for asynchronous callbacks. While functional, this **Point-to-Point (P2P)** model suffers from significant scaling issues:
-
-* **Insufficient Fan-Out**: A publisher must send $N$ requests to reach $N$ subscribers, leading to $O(N)$ complexity.
-* **Temporal Coupling**: Consumers must be online at the exact moment of publication.
-* **Backpressure Propagation**: A slow subscriber can block the publisher.
-
-**EventMesh A2A** solves this by introducing **Native Pub/Sub** capabilities:
-
-```mermaid
-graph LR
- Publisher[Publisher Agent] -->|1. Publish (Once)| Bus[EventMesh Bus]
-
- subgraph Fanout_Layer [EventMesh Fanout Layer]
- Queue[Topic Queue]
- end
-
- Bus --> Queue
-
- Queue -->|Push| Sub1[Subscriber 1]
- Queue -->|Push| Sub2[Subscriber 2]
- Queue -->|Push| Sub3[Subscriber 3]
-
- style Bus fill:#f9f,stroke:#333
- style Fanout_Layer fill:#ccf,stroke:#333
-```
-
-## 3. Architecture Design
-
-### 3.1 System Context
-
-```mermaid
-graph TD
- Client[Client Agent / LLM] -- "JSON-RPC Request" --> EM[EventMesh Runtime]
- EM -- "CloudEvent (Request)" --> Server[Server Agent / Tool]
- Server -- "CloudEvent (Response)" --> EM
- EM -- "JSON-RPC Response" --> Client
-
- subgraph Runtime [EventMesh Runtime]
- Plugin[A2A Protocol Plugin]
- end
-
- style EM fill:#f9f,stroke:#333,stroke-width:4px
- style Plugin fill:#ccf,stroke:#333,stroke-width:2px
-```
-
-### 3.2 Component Design (`eventmesh-protocol-a2a`)
-
-The core protocol logic resides in the `eventmesh-protocol-plugin` module.
-
-* **`EnhancedA2AProtocolAdaptor`**: The central brain of the protocol.
- * **Intelligent Parsing**: Automatically detects message format (MCP vs. Raw CloudEvent).
- * **Protocol Delegation**: Delegates to `CloudEvents` or `HTTP` adaptors when necessary.
- * **Semantic Mapping**: Transforms JSON-RPC methods and IDs into CloudEvent attributes.
-* **`A2AProtocolConstants`**: Defines standard operations like `task/get`, `message/sendStream`.
-* **`JsonRpc*` Models**: Strictly typed POJOs for JSON-RPC 2.0 compliance.
-* **`AgentCard` / `AgentSkill` / `AgentInterface`**: Agent capability discovery models.
-* **`A2ATopicFactory`**: Topic naming and parsing utility for request/response/status topics.
-* **`A2AClient`**: Java SDK for agent developers — AgentCard registration, task submission (sync/async), task status query, heartbeat, and transport-based request handling. Returns typed `TaskResult` objects.
-* **`A2AMessageTransport`**: Transport-agnostic pub/sub interface (InMemory implementation for dev/testing).
-
-### 3.3 Gateway Runtime Architecture (`eventmesh-runtime`)
-
-The Gateway runtime provides a standalone Netty HTTP server bridging external clients to the A2A event bus.
-
-```mermaid
-graph TD
- Client["Client / A2AClient SDK"] -- "HTTP REST" --> Server["A2AGatewayServer<br/>(Netty HTTP)"]
- Server --> Handler["A2AGatewayHttpHandler"]
- Handler --> GwService["A2AGatewayService"]
- GwService --> Registry["TaskRegistry<br/>(state machine + TTL)"]
- GwService --> Transport["InMemoryA2AMessageTransport"]
- GwService --> PubSub["A2APublishSubscribeService<br/>(AgentCard discovery)"]
- Transport -- "publish/subscribe" --> Agent["Target Agent"]
- Agent -- "response event" --> Transport
- Transport --> GwService
-
- style Server fill:#f9f,stroke:#333,stroke-width:2px
- style Registry fill:#cfc,stroke:#333
- style Transport fill:#ccf,stroke:#333
-```
-
-#### Core Components
-
-| Component | Module | Responsibility |
-| :--- | :--- | :--- |
-| `A2AGatewayServer` | runtime | Standalone Netty HTTP server entry point. Pre-registers mock agents, wires all components. |
-| `A2AGatewayHttpHandler` | runtime | HTTP request router. Maps REST endpoints to service calls. Supports SSE streaming. |
-| `A2AGatewayService` | runtime | Core orchestration: task submission, response handling, status subscription, SSE push. |
-| `TaskRegistry` | runtime | In-memory task lifecycle state machine with TTL auto-cleanup. |
-| `A2APublishSubscribeService` | runtime | AgentCard registration, discovery, and heartbeat management. |
-| `InMemoryA2AMessageTransport` | runtime | In-memory pub/sub (replaceable by EventMesh broker). |
-| `A2ACardHttpHandler` | runtime | AgentCard CRUD REST endpoints (`/a2a/cards/*`). |
-| `A2AClient` | protocol-a2a | Java SDK for agent developers (HTTP + transport). |
-
-#### Task Lifecycle State Machine
-
-```
-SUBMITTED → WORKING → COMPLETED
- ↘ FAILED
- ↘ CANCELLED
-```
-
-* **TTL Auto-Cleanup**: Terminal-state tasks are automatically removed after a configurable TTL (default: 5 minutes). A daemon thread runs cleanup every 60 seconds.
-* **Race Condition Prevention**: `pendingTasks.put(taskId, future)` is called **before** `transport.publish()` to ensure the future is registered before any synchronous delivery could trigger `handleResponse()`.
-
-#### REST API
-
-| Method | Path | Description |
-| :--- | :--- | :--- |
-| `POST` | `/a2a/tasks?mode=sync` | Submit task synchronously (wait for result) |
-| `POST` | `/a2a/tasks?mode=async` | Submit task asynchronously (return taskId immediately) |
-| `GET` | `/a2a/tasks/{taskId}` | Get task status and result |
-| `DELETE` | `/a2a/tasks/{taskId}` | Cancel a task |
-| `GET` | `/a2a/tasks/{taskId}/wait` | Long-poll wait for task result |
-| `GET` | `/a2a/tasks/{taskId}/stream` | **SSE** stream of task status updates |
-| `GET` | `/a2a/agents` | List all registered agents |
-| `POST` | `/a2a/heartbeat` | Agent heartbeat |
-| `GET` | `/a2a/cards/list` | List all AgentCards |
-| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | Register an AgentCard |
-
-### 3.4 Asynchronous RPC Mapping ( The "Async Bridge" )
-
-To support MCP on an Event Bus, synchronous RPC concepts are mapped to asynchronous events:
-
-| Concept | MCP / JSON-RPC | CloudEvent Mapping |
-| :--- | :--- | :--- |
-| **Action** | `method` (e.g., `tools/call`) | **Type**: `org.apache.eventmesh.a2a.tools.call.req`<br>**Extension**: `a2amethod` |
-| **Correlation** | `id` (e.g., `req-123`) | **Extension**: `collaborationid` (on Response)<br>**ID**: Preserved on Request |
-| **Direction** | Implicit (Request vs Result) | **Extension**: `mcptype` (`request` or `response`) |
-| **P2P Routing** | `params._agentId` | **Extension**: `targetagent` |
-| **Pub/Sub Topic** | `params._topic` | **Subject**: The topic value (e.g. `market.btc`) |
-| **Streaming Seq** | `params._seq` | **Extension**: `seq` |
-
-## 4. Functional Specification
-
-### 4.1 Message Processing Flow
-
-1. **Ingestion**: The adaptor receives a `ProtocolTransportObject` (byte array/string).
-2. **Detection**: Checks for `jsonrpc: "2.0"`.
-3. **Transformation (MCP Mode)**:
- * **Request**: Parses `method`.
- * If `message/sendStream`, sets type suffix to `.stream` and extracts `_seq`.
- * If `_topic` present, sets `subject` (Pub/Sub).
- * If `_agentId` present, sets `targetagent` (P2P).
- * **Response**: Parses `result`/`error`. Sets `collaborationid` = `id`.
-4. **Batch Processing**: Splits JSON Array into a `List<CloudEvent>`.
-
-### 4.2 Key Features
-
-#### A. Intelligent Routing Support
-* **Mechanism**: Promotes `_agentId` or `_topic` from JSON body to CloudEvent attributes.
-* **Benefit**: Enables EventMesh Router to perform content-based routing (CBR) efficiently.
-
-#### B. Batching
-* **Benefit**: Significantly increases throughput for high-frequency interactions.
-
-#### C. Streaming Support
-* **Operation**: `message/sendStream`
-* **Mechanism**: Maps to `.stream` event type and preserves sequence order via `seq` extension attribute.
-
-#### D. SSE Task Streaming (Gateway)
-* **Endpoint**: `GET /a2a/tasks/{taskId}/stream`
-* **Mechanism**: Server-Sent Events (`text/event-stream`) pushes real-time task state transitions.
-* **Flow**: Initial state → WORKING updates → terminal state → connection close.
-* **Implementation**: Handler writes `DefaultHttpContent` chunks directly to the Netty channel, returning `null` to skip the standard `FullHttpResponse` path.
-
-#### E. Task TTL Auto-Cleanup (Gateway)
-* Terminal-state tasks are automatically removed by a daemon scheduler after a configurable TTL (default: 5 minutes, cleanup interval: 60 seconds), preventing memory leaks.
-
-#### F. AgentCard Discovery & Heartbeat (Gateway)
-* AgentCards expire after 60 seconds without heartbeat. `POST /a2a/heartbeat` refreshes the last-seen timestamp.
-
-## 5. Usage Examples
-
-### 5.1 Sending a Tool Call (Request)
-
-**Raw Payload:**
-```json
-{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "weather_service",
- "arguments": { "city": "New York" }
- },
- "id": "msg-101"
-}
-```
-
-### 5.2 Pub/Sub Broadcast
-
-**Raw Payload:**
-```json
-{
- "jsonrpc": "2.0",
- "method": "market/update",
- "params": {
- "symbol": "BTC",
- "price": 50000,
- "_topic": "market.crypto.btc"
- }
-}
-```
-
-**Generated CloudEvent:**
-* `subject`: `market.crypto.btc`
-* `targetagent`: (Empty)
-
-### 5.3 Gateway REST API (HTTP)
-
-The A2A Gateway provides a REST API for external clients and non-Java agents.
-
-#### 5.3.1 Submit Task (Sync)
-
-```bash
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=sync' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Beijing"}'
-```
-
-Response:
-```json
-{
- "taskId": "task-a1b2c3d4",
- "state": "COMPLETED",
- "data": "The weather in Beijing is sunny, 25°C"
-}
-```
-
-#### 5.3.2 Submit Task (Async)
-
-```bash
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=async' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Shanghai"}'
-```
-
-#### 5.3.3 SSE Stream
-
-```bash
-curl -N http://localhost:10105/a2a/tasks/{taskId}/stream
-```
-
-Response (`text/event-stream`):
-```
-data: {"taskId":"task-a1b2c3d4","state":"SUBMITTED"}
-
-data: {"taskId":"task-a1b2c3d4","state":"WORKING","data":"processing..."}
-
-data: {"taskId":"task-a1b2c3d4","state":"completed","data":"result..."}
-```
-
-#### 5.3.4 List Agents
-
-```bash
-curl http://localhost:10105/a2a/agents
-```
-
-### 5.4 A2AClient SDK (Java)
-
-```java
-A2AClient client = A2AClient.builder()
- .gatewayUrl("http://localhost:10105")
- .namespace("global")
- .agentName("my-agent")
- .agentCard(card)
- .heartbeatInterval(30_000)
- .build();
-
-client.start();
-
-// Synchronous task (returns typed TaskResult)
-TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
-
-// Asynchronous task (returns taskId immediately)
-String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
-
-// Poll status
-TaskResult status = client.getTaskStatus(taskId);
-
-// Cancel
-boolean cancelled = client.cancelTask(taskId);
-
-// List registered agents (typed List<String>)
-List<String> agents = client.listAgents();
-
-client.shutdown();
-```
-
-## 6. Future Roadmap
-
-* **EventMesh Broker Integration**: Replace `InMemoryA2AMessageTransport` with the real EventMesh broker for production deployment.
-* **Schema Registry**: Implement dynamic discovery of Agent capabilities via `methods/list`.
-* **Sidecar Injection**: Fully integrate the adaptor into the EventMesh Sidecar.
-* **WebSocket Streaming**: Extend SSE to bidirectional WebSocket for real-time agent dialogue.
-* **Task Persistence**: Persist `TaskRegistry` state to a durable store for crash recovery.
-* **Authentication**: Add API key / JWT authentication to the Gateway REST API.
\ No newline at end of file
diff --git a/docs/a2a-protocol/IMPLEMENTATION_SUMMARY.md b/docs/a2a-protocol/IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index fd5081c61..000000000
--- a/docs/a2a-protocol/IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# 实现总结:EventMesh A2A 协议 v2.0 (MCP 版)
-
-## 核心成果
-
-A2A 协议已成功重构为采用 **MCP (Model Context Protocol)** 架构,将 EventMesh 定位为现代化的 **智能体协作总线 (Agent Collaboration Bus)**。
-
-### 1. 核心协议重构 (`EnhancedA2AProtocolAdaptor`)
-- **混合引擎 (JSON-RPC & CloudEvents)**: 实现了智能解析引擎,支持:
- - **MCP/JSON-RPC 2.0**: 面向 LLM 和脚本的低门槛接入,自动封装 CloudEvent。
- - **原生 CloudEvents**: 面向 EventMesh 原生应用的灵活接入,支持自定义元数据和透传。
- - 适配器根据 `jsonrpc` 字段自动分发处理逻辑。
-- **异步 RPC 映射**: 建立了同步 RPC 语义与异步事件驱动架构 (EDA) 之间的桥梁。
- - **请求 (Requests)** 映射为 `*.req` 事件,属性 `mcptype=request`。
- - **响应 (Responses)** 映射为 `*.resp` 事件,属性 `mcptype=response`。
- - **关联 (Correlation)** 通过将 JSON-RPC `id` 映射到 CloudEvent `collaborationid` 来处理。
-- **路由优化**: 实现了"深度内容路由提取":
- - `params._agentId` -> CloudEvent 扩展属性 `targetagent` (P2P)。
- - `params._topic` -> CloudEvent Subject (Pub/Sub)。
-
-### 2. 原生 Pub/Sub 与流式支持
-- **Pub/Sub**: 通过将 `_topic` 映射到 CloudEvent Subject,支持 O(1) 广播复杂度。
-- **流式 (Streaming)**: 支持 `message/sendStream` 操作,映射为 `.stream` 事件类型,并通过 `_seq` -> `seq` 扩展属性保证顺序。
-
-### 3. 标准化与兼容性
-- **数据模型**: 定义了符合 JSON-RPC 2.0 规范的 `JsonRpcRequest`、`JsonRpcResponse`、`JsonRpcError` POJO 对象。
-- **方法定义**: 引入了 `McpMethods` 常量,支持标准操作如 `tools/call`、`resources/read`。
-- **AgentCard 模型**: 实现了 `AgentCard`、`AgentSkill`、`AgentInterface`、`AgentCapabilities` 等完整的 Agent 能力描述模型。
-
-### 4. Gateway 运行时架构 (`eventmesh-runtime`)
-
-完整的独立 HTTP Gateway 服务,桥接外部客户端到 A2A 事件总线。
-
-#### 核心组件
-
-| 组件 | 职责 |
-| :--- | :--- |
-| `A2AGatewayServer` | Netty HTTP 服务器入口,预注册 mock agent,组装所有组件 |
-| `A2AGatewayHttpHandler` | HTTP 请求路由,支持 SSE 流式响应 |
-| `A2AGatewayService` | 核心编排:任务提交、响应处理、状态订阅、SSE 推送 |
-| `TaskRegistry` | 内存任务状态机 + TTL 自动清理 |
-| `A2APublishSubscribeService` | AgentCard 注册、发现、心跳管理 |
-| `InMemoryA2AMessageTransport` | 内存 pub/sub 实现(可替换为 EventMesh broker) |
-| `A2ACardHttpHandler` | AgentCard CRUD REST 端点 |
-| `A2AClient` | Java SDK,提供类型化 API |
-
-#### REST API
-
-| 方法 | 路径 | 说明 |
-| :--- | :--- | :--- |
-| `POST` | `/a2a/tasks?mode=sync` | 同步提交任务 |
-| `POST` | `/a2a/tasks?mode=async` | 异步提交任务 |
-| `GET` | `/a2a/tasks/{taskId}` | 查询任务状态 |
-| `DELETE` | `/a2a/tasks/{taskId}` | 取消任务 |
-| `GET` | `/a2a/tasks/{taskId}/wait` | 长轮询等待结果 |
-| `GET` | `/a2a/tasks/{taskId}/stream` | SSE 流式推送状态更新 |
-| `GET` | `/a2a/agents` | 列出已注册 agents |
-| `POST` | `/a2a/heartbeat` | Agent 心跳 |
-| `GET` | `/a2a/cards/list` | 列出所有 AgentCard |
-| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | 注册 AgentCard |
-
-### 5. 关键改进
-
-#### 5.1 TaskRegistry TTL 自动清理
-- **问题**: 终态任务(COMPLETED/FAILED/CANCELLED)无限累积导致内存泄漏。
-- **方案**: 守护线程 `ScheduledExecutorService` 每 60 秒扫描一次,清理超过 TTL(默认 5 分钟)的终态任务。
-- **配置**: `TaskRegistry(taskTtlMs, cleanupIntervalMs)` 构造函数支持自定义调优。
-
-#### 5.2 竞态条件修复
-- **问题**: `InMemoryTransport` 同步投递消息,若 `transport.publish()` 在 `pendingTasks.put()` 之前执行,`handleResponse()` 会先于 `put()` 运行,导致 future 永不完成。
-- **方案**: 严格保证 `pendingTasks.put(taskId, future)` 在 `transport.publish()` 之前执行,并添加注释说明顺序重要性。
-
-#### 5.3 A2AClient 类型化返回
-- **改进**: `getTaskStatus()` 返回 `TaskResult` 对象(而非原始 JSON 字符串),`listAgents()` 返回 `List<String>`(而非原始 JSON)。
-- **兼容**: `TaskResult.data` 字段使用 `@JsonAlias("result")` 注解,兼容服务端 `result` 字段名。
-
-#### 5.4 SSE 流式响应
-- **端点**: `GET /a2a/tasks/{taskId}/stream`
-- **实现**: Handler 直接写入 Netty channel(`DefaultHttpContent` chunks),返回 `null` 跳过标准 `FullHttpResponse` 路径。通过 `StatusSubscriber` 回调实时推送状态变更。
-
-#### 5.5 使用文档
-- 新建 `eventmesh-examples/.../demo/README.md`,包含架构图、API 表、curl 示例、SDK 用法、运行方式。
-
-### 6. 测试与质量
-- **协议层单元测试**: `EnhancedA2AProtocolAdaptorTest` 覆盖请求/响应循环、错误处理、通知和批处理。
-- **Topic 工具测试**: `A2ATopicFactoryTest` 覆盖 topic 生成与解析。
-- **Gateway 运行时测试**:
- - `TaskRegistryTest` — 任务状态机 + TTL 清理验证
- - `InMemoryA2AMessageTransportTest` — 内存传输投递
- - `A2AGatewayServiceTest` — Gateway 服务层
- - `A2AGatewayEndToEndTest` — 进程内全链路
- - `A2AClientServerIntegrationTest` — 真实 HTTP 客户端-服务端集成测试
-- **集成演示**: `McpIntegrationDemoTest`、`McpPatternsIntegrationTest`、`McpComprehensiveDemoTest`、`CloudEventsComprehensiveDemoTest`
-- **总计**: 73 个测试场景,全部通过。
-
-## 下一步计划
-
-1. **EventMesh Broker 集成**: 用真实 EventMesh broker 替换 `InMemoryA2AMessageTransport`,实现生产级部署。
-2. **路由集成**: 更新 EventMesh Runtime Router,利用 `targetagent` 和 `a2amethod` 扩展属性实现高级路由规则。
-3. **Schema 注册中心**: 实现"注册中心智能体 (Registry Agent)",允许智能体动态发布 MCP 能力 (`methods/list`)。
-4. **Sidecar 支持**: 将 A2A 适配器逻辑暴露在 Sidecar 代理中,允许非 Java 智能体通过 HTTP/JSON 交互。
-5. **WebSocket 流式**: 将 SSE 扩展为双向 WebSocket,支持实时 agent 对话。
-6. **任务持久化**: 将 `TaskRegistry` 状态持久化到 Redis/DB,支持崩溃恢复。
-7. **认证授权**: 为 Gateway REST API 添加 API Key / JWT 认证。
diff --git a/docs/a2a-protocol/IMPLEMENTATION_SUMMARY_EN.md b/docs/a2a-protocol/IMPLEMENTATION_SUMMARY_EN.md
deleted file mode 100644
index aec8aef94..000000000
--- a/docs/a2a-protocol/IMPLEMENTATION_SUMMARY_EN.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Implementation Summary: EventMesh A2A Protocol v2.0 (MCP Edition)
-
-## Key Achievements
-
-The A2A protocol has been successfully refactored to adopt the **MCP (Model Context Protocol)** architecture, positioning EventMesh as a modern **Agent Collaboration Bus**.
-
-### 1. Core Protocol Refactoring (`EnhancedA2AProtocolAdaptor`)
-- **Hybrid Engine (JSON-RPC & CloudEvents)**: Implemented a smart parsing engine that supports:
- - **MCP/JSON-RPC 2.0**: For LLM-friendly, low-code integration.
- - **Native CloudEvents**: For advanced, protocol-compliant integration.
- - The adaptor automatically delegates processing based on the payload content (`jsonrpc` detection).
-- **Async RPC Mapping**: Established a bridge between synchronous RPC semantics and asynchronous Event-Driven Architecture (EDA).
- - **Requests** map to `*.req` events with `mcptype=request`.
- - **Responses** map to `*.resp` events with `mcptype=response`.
- - **Correlation** is handled by mapping JSON-RPC `id` to CloudEvent `collaborationid`.
-- **Routing Optimization**: Implemented "Deep Body Routing" extraction:
- - `params._agentId` -> CloudEvent Extension `targetagent` (P2P).
- - `params._topic` -> CloudEvent Subject (Pub/Sub).
-
-### 2. Native Pub/Sub & Streaming
-- **Pub/Sub**: Added support for O(1) broadcast complexity by mapping `_topic` to CloudEvent Subject.
-- **Streaming**: Added support for `message/sendStream` operation, mapping to `.stream` event type and preserving sequence via `_seq` -> `seq` extension.
-
-### 3. Standardization & Compatibility
-- **Models**: Defined `JsonRpcRequest`, `JsonRpcResponse`, `JsonRpcError` POJOs compliant with JSON-RPC 2.0 spec.
-- **Methods**: Introduced `McpMethods` constants for standard operations like `tools/call`, `resources/read`.
-- **AgentCard Models**: Implemented `AgentCard`, `AgentSkill`, `AgentInterface`, `AgentCapabilities` for complete agent capability description.
-
-### 4. Gateway Runtime Architecture (`eventmesh-runtime`)
-
-A standalone HTTP Gateway service bridging external clients to the A2A event bus.
-
-#### Core Components
-
-| Component | Responsibility |
-| :--- | :--- |
-| `A2AGatewayServer` | Netty HTTP server entry point, pre-registers mock agents, wires all components |
-| `A2AGatewayHttpHandler` | HTTP request router, supports SSE streaming responses |
-| `A2AGatewayService` | Core orchestration: task submission, response handling, status subscription, SSE push |
-| `TaskRegistry` | In-memory task state machine + TTL auto-cleanup |
-| `A2APublishSubscribeService` | AgentCard registration, discovery, heartbeat management |
-| `InMemoryA2AMessageTransport` | In-memory pub/sub (replaceable by EventMesh broker) |
-| `A2ACardHttpHandler` | AgentCard CRUD REST endpoints |
-| `A2AClient` | Java SDK with typed API |
-
-#### REST API
-
-| Method | Path | Description |
-| :--- | :--- | :--- |
-| `POST` | `/a2a/tasks?mode=sync` | Submit task synchronously |
-| `POST` | `/a2a/tasks?mode=async` | Submit task asynchronously |
-| `GET` | `/a2a/tasks/{taskId}` | Get task status |
-| `DELETE` | `/a2a/tasks/{taskId}` | Cancel a task |
-| `GET` | `/a2a/tasks/{taskId}/wait` | Long-poll wait for result |
-| `GET` | `/a2a/tasks/{taskId}/stream` | SSE stream of task status updates |
-| `GET` | `/a2a/agents` | List registered agents |
-| `POST` | `/a2a/heartbeat` | Agent heartbeat |
-| `GET` | `/a2a/cards/list` | List all AgentCards |
-| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | Register an AgentCard |
-
-### 5. Key Improvements
-
-#### 5.1 TaskRegistry TTL Auto-Cleanup
-- **Problem**: Terminal-state tasks (COMPLETED/FAILED/CANCELLED) accumulate indefinitely, causing memory leaks.
-- **Solution**: A daemon `ScheduledExecutorService` runs every 60 seconds, removing terminal-state tasks older than the TTL (default: 5 minutes).
-- **Configuration**: `TaskRegistry(taskTtlMs, cleanupIntervalMs)` constructor allows custom tuning.
-
-#### 5.2 Race Condition Fix
-- **Problem**: `InMemoryTransport` delivers messages synchronously. If `transport.publish()` executes before `pendingTasks.put()`, `handleResponse()` runs before `put()` and the future never completes.
-- **Solution**: Ensure `pendingTasks.put(taskId, future)` is called **before** `transport.publish()`, with comments documenting the ordering importance.
-
-#### 5.3 A2AClient Typed Returns
-- **Improvement**: `getTaskStatus()` returns `TaskResult` object (instead of raw JSON string), `listAgents()` returns `List<String>` (instead of raw JSON).
-- **Compatibility**: `TaskResult.data` field uses `@JsonAlias("result")` annotation to handle the server's `result` field name.
-
-#### 5.4 SSE Streaming Response
-- **Endpoint**: `GET /a2a/tasks/{taskId}/stream`
-- **Implementation**: Handler writes `DefaultHttpContent` chunks directly to the Netty channel, returning `null` to skip the standard `FullHttpResponse` path. Uses `StatusSubscriber` callbacks for real-time state push.
-
-#### 5.5 Usage Documentation
-- Created `eventmesh-examples/.../demo/README.md` with architecture diagram, API table, curl examples, SDK usage, and run instructions.
-
-### 6. Testing & Quality
-- **Protocol Unit Tests**: `EnhancedA2AProtocolAdaptorTest` covers Request/Response cycles, Error handling, Notifications, and Batching.
-- **Topic Utility Tests**: `A2ATopicFactoryTest` covers topic generation and parsing.
-- **Gateway Runtime Tests**:
- - `TaskRegistryTest` — Task state machine + TTL cleanup verification
- - `InMemoryA2AMessageTransportTest` — In-memory transport delivery
- - `A2AGatewayServiceTest` — Gateway service layer
- - `A2AGatewayEndToEndTest` — In-process end-to-end flow
- - `A2AClientServerIntegrationTest` — Real HTTP client-server integration test
-- **Integration Demos**: `McpIntegrationDemoTest`, `McpPatternsIntegrationTest`, `McpComprehensiveDemoTest`, `CloudEventsComprehensiveDemoTest`
-- **Total**: 73 test scenarios, all passing.
-
-## Next Steps
-
-1. **EventMesh Broker Integration**: Replace `InMemoryA2AMessageTransport` with the real EventMesh broker for production deployment.
-2. **Router Integration**: Update EventMesh Runtime Router to leverage `targetagent` and `a2amethod` extension attributes for advanced routing rules.
-3. **Schema Registry**: Implement a "Registry Agent" that allows agents to publish their MCP capabilities (`methods/list`) dynamically.
-4. **Sidecar Support**: Expose the A2A adaptor logic in the Sidecar proxy to allow non-Java agents (Python, Node.js) to interact via simple HTTP/JSON.
-5. **WebSocket Streaming**: Extend SSE to bidirectional WebSocket for real-time agent dialogue.
-6. **Task Persistence**: Persist `TaskRegistry` state to a durable store (Redis/DB) for crash recovery.
-7. **Authentication**: Add API key / JWT authentication to the Gateway REST API.
diff --git a/docs/a2a-protocol/README.md b/docs/a2a-protocol/README.md
index 73247de79..6072b7252 100644
--- a/docs/a2a-protocol/README.md
+++ b/docs/a2a-protocol/README.md
@@ -1,156 +1,716 @@
-# EventMesh A2A (Agent-to-Agent Communication Protocol)
+# EventMesh A2A 协议(Agent-to-Agent Communication Protocol)
-## 概述
+> 合并自 ARCHITECTURE.md + eventmesh-a2a-design.md + README.md + README_EN.md + IMPLEMENTATION_SUMMARY.md + IMPLEMENTATION_SUMMARY_EN.md + TEST_RESULTS.md(2026-08-13)。
-A2A (Agent-to-Agent Communication Protocol) 是 EventMesh 的一个高性能协议插件,专门设计用于支持智能体(Agents)之间的异步通信、协作和任务协调。该协议采用 **MCP (Model Context Protocol)** 架构理念,结合 EventMesh 的事件驱动特性,打造了一个**异步、解耦、高性能的智能体协作总线**。
+## 目录
-A2A 协议不仅支持传统的 FIPA-ACL 风格语义,更全面拥抱现代大模型(LLM)生态,通过支持 **JSON-RPC 2.0** 标准,实现了对工具调用(Tool Use)、上下文共享(Context Sharing)等 LLM 核心场景的原生支持。
+- [1. Overview](#1-overview)
+- [2. Core Philosophy](#2-core-philosophy)
+- [3. Architecture](#3-architecture)
+- [4. Protocol Specification](#4-protocol-specification)
+- [5. Implementation Summary](#实现总结eventmesh-a2a-协议-v20-mcp-版)
+- [6. Test Results](#test-results-eventmesh-a2a-protocol-v20)
+- [7. 中文使用指南](#中文使用指南整合自-readmemd)
-## 核心特性
+---
-### 1. MCP over CloudEvents
-- **标准兼容**: 完全支持 **MCP (Model Context Protocol)** 定义的 `tools/call`, `resources/read` 等标准方法。
-- **事件驱动**: 将同步的 RPC 调用映射为异步的 **Request/Response 事件流**,充分利用 EventMesh 的高并发处理能力。
-- **协议无关**: 所有的 MCP 消息都被封装在 **CloudEvents** 标准信封中,可以在 HTTP, TCP, gRPC, Kafka 等任意 EventMesh 支持的传输层上运行。
+# EventMesh A2A Protocol Architecture & Functional Specification
-### 2. 混合架构设计 (Hybrid Architecture)
-- **双模支持**:
- - **Modern Mode**: 支持 MCP 标准的 JSON-RPC 2.0 消息,面向 LLM 应用。
- - **Legacy Mode**: 兼容旧版 A2A 协议(基于 `messageType` 和 FIPA 动词),保障存量业务平滑迁移。
-- **自动识别**: 协议适配器根据消息内容特征(如 `jsonrpc` 字段)自动智能选择处理模式。
+## 1. Overview
-### 3. 高性能与路由
-- **批量处理**: 原生支持 JSON-RPC Batch 请求,EventMesh 会将其自动拆分为并行事件流,极大提升吞吐量。
-- **智能路由**: 支持从 MCP 请求参数(如 `_agentId`)中提取路由线索,自动注入 CloudEvents 扩展属性 (`targetagent`),实现零解包路由。
+The **EventMesh A2A (Agent-to-Agent) Protocol** is a specialized, high-performance protocol plugin designed to enable asynchronous communication, collaboration, and task coordination between autonomous agents.
-### 4. CloudEvents 集成
-- **类型映射**: 自动将 MCP 方法映射为 CloudEvent Type (例如 `tools/call` -> `org.apache.eventmesh.a2a.tools.call.req`)。
-- **上下文传递**: 利用 CloudEvents Extension 传递 `traceparent`,实现跨 Agent 的全链路追踪。
+With the release of v2.0, A2A adopts the **MCP (Model Context Protocol)** architecture, transforming EventMesh into a robust **Agent Collaboration Bus**. It bridges the gap between synchronous LLM-based tool calls (JSON-RPC 2.0) and asynchronous Event-Driven Architectures (EDA), enabling scalable, distributed, and decoupled agent systems.
-## 架构设计
+## 2. Core Philosophy
-### 核心组件
+The architecture adheres to the principles outlined in the broader agent community (e.g., A2A Project, FIPA-ACL, and CloudEvents):
+1. **JSON-RPC 2.0 as Lingua Franca**: Uses standard JSON-RPC for payload semantics, ensuring compatibility with modern LLM ecosystems (LangChain, AutoGen).
+2. **Transport Agnostic**: Encapsulates all messages within **CloudEvents**, allowing transport over any EventMesh-supported protocol (HTTP, TCP, gRPC, Kafka).
+3. **Async by Default**: Maps synchronous Request/Response patterns to asynchronous Event streams using correlation IDs.
+4. **Native Pub/Sub Semantics**: Supports O(1) broadcast complexity, temporal decoupling (Late Join), and backpressure isolation, solving the scalability limits of traditional P2P webhook callbacks.
+
+### 2.1 Native Pub/Sub Semantics
+
+Traditional A2A implementations often rely on HTTP Webhooks (`POST /inbox`) for asynchronous callbacks. While functional, this **Point-to-Point (P2P)** model suffers from significant scaling issues:
+
+* **Insufficient Fan-Out**: A publisher must send $N$ requests to reach $N$ subscribers, leading to $O(N)$ complexity.
+* **Temporal Coupling**: Consumers must be online at the exact moment of publication.
+* **Backpressure Propagation**: A slow subscriber can block the publisher.
+
+**EventMesh A2A** solves this by introducing **Native Pub/Sub** capabilities:
+
+```mermaid
+flowchart LR
+ Publisher["Publisher Agent"] -->|1. Publish (Once)| Bus["EventMesh Bus"]
+
+ subgraph FanoutLayer ["EventMesh Fanout Layer"]
+ Queue["Topic Queue"]
+ end
+
+ Bus --> Queue
+
+ Queue -->|"Push"| Sub1["Subscriber 1"]
+ Queue -->|"Push"| Sub2["Subscriber 2"]
+ Queue -->|"Push"| Sub3["Subscriber 3"]
+
+ style Bus fill:#f9f,stroke:#333
+ style FanoutLayer fill:#ccf,stroke:#333
```
-┌─────────────────────────────────────────────────────────────┐
-│ EventMesh A2A Protocol v2.0 │
-│ (MCP over CloudEvents Architecture) │
-├─────────────────────────────────────────────────────────────┤
-│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
-│ │ MCP/JSON-RPC│ │ Legacy A2A │ │ Protocol │ │
-│ │ Handler │ │ Handler │ │ Delegator │ │
-│ └─────────────┘ └─────────────┘ └─────────────┘ │
-├─────────────────────────────────────────────────────────────┤
-│ ┌───────────────────────────────────────────────────────┐ │
-│ │ Enhanced A2A Protocol Adaptor │ │
-│ │ (Intelligent Parsing & CloudEvent Mapping) │ │
-│ └───────────────────────────────────────────────────────┘ │
-├─────────────────────────────────────────────────────────────┤
-│ EventMesh Protocol Infrastructure │
-│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
-│ │ CloudEvents │ │ HTTP │ │ gRPC │ │
-│ │ Protocol │ │ Protocol │ │ Protocol │ │
-│ └─────────────┘ └─────────────┘ └─────────────┘ │
-└─────────────────────────────────────────────────────────────┘
+
+### 2.1 Hybrid Protocol Support (JSON-RPC & CloudEvents)
+
+A2A Protocol introduces a unique **Hybrid Architecture** that bridges the gap between the AI ecosystem (which prefers simple JSON) and the Cloud Native ecosystem (which prefers structured CloudEvents).
+
+| Feature | JSON-RPC 2.0 Mode | Native CloudEvents Mode |
+| :--- | :--- | :--- |
+| **Primary Audience** | LLMs, Scripts (Python/JS), LangChain | EventMesh Apps, Knative, Java SDK |
+| **Philosophy** | **"Battery Included"** | **"Power User"** |
+| **Usage** | Send raw JSON (`{"method":...}`) | Send `CloudEvent` object |
+| **Complexity** | Low (No SDK required) | Medium (Requires CE SDK) |
+| **Mechanism** | Adaptor automatically wraps JSON in CE | Adaptor passes through the event |
+
+**Benefits:**
+* **Zero-Barrier Entry**: Developers can interact with the mesh using just `curl` or simple JSON libraries.
+* **Full Flexibility**: Advanced users retain full control over CloudEvent attributes (Source, Type, Extensions) for complex routing or tracing scenarios.
+
+## 3. Architecture Design
+
+### 3.1 System Context
+
+```mermaid
+graph TD
+ Client["Client Agent / LLM"] -- "JSON-RPC Request" --> EM["EventMesh Runtime"]
+ EM -- "CloudEvent (Request)" --> Server["Server Agent / Tool"]
+ Server -- "CloudEvent (Response)" --> EM
+ EM -- "JSON-RPC Response" --> Client
+
+ subgraph Runtime ["EventMesh Runtime"]
+ Plugin["A2A Protocol Plugin"]
+ end
+
+ style EM fill:#f9f,stroke:#333,stroke-width:4px
+ style Plugin fill:#ccf,stroke:#333,stroke-width:2px
+```
+
+### 3.2 Component Design (`eventmesh-protocol-a2a`)
+
+The core protocol logic resides in the `eventmesh-protocol-plugin` module.
+
+* **`EnhancedA2AProtocolAdaptor`**: The central brain of the protocol.
+ * **Intelligent Parsing**: Automatically detects message format (MCP vs. Raw CloudEvent).
+ * **Protocol Delegation**: Delegates to `CloudEvents` or `HTTP` adaptors when necessary.
+ * **Semantic Mapping**: Transforms JSON-RPC methods and IDs into CloudEvent attributes.
+* **`A2AProtocolConstants`**: Defines standard operations like `task/get`, `message/sendStream`.
+* **`JsonRpc*` Models**: Strictly typed POJOs for JSON-RPC 2.0 compliance.
+* **`AgentCard` / `AgentSkill` / `AgentInterface`**: Agent capability discovery models.
+* **`A2ATopicFactory`**: Topic naming and parsing utility (request/response/status topics).
+* **`A2AClient`**: Java SDK for agent developers — AgentCard registration, task submission (sync/async), task status query, heartbeat, and transport-based request handling. Returns typed `TaskResult` objects.
+* **`A2AMessageTransport`**: Transport-agnostic pub/sub interface (InMemory implementation for dev/testing).
+
+### 3.3 Gateway Runtime Architecture (`eventmesh-runtime`)
+
+The Gateway runtime provides a standalone HTTP server that bridges external clients to the A2A event bus.
+
+```mermaid
+graph TD
+ Client["Client / A2AClient SDK"] -- "HTTP REST" --> Server["A2AGatewayServer<br/>(Netty HTTP)"]
+ Server --> Handler["A2AGatewayHttpHandler"]
+ Handler --> GwService["A2AGatewayService"]
+ GwService --> Registry["TaskRegistry<br/>(state machine + TTL)"]
+ GwService --> Transport["InMemoryA2AMessageTransport"]
+ GwService --> PubSub["A2APublishSubscribeService<br/>(AgentCard discovery)"]
+ Transport -- "publish/subscribe" --> Agent["Target Agent"]
+ Agent -- "response event" --> Transport
+ Transport --> GwService
+
+ style Server fill:#f9f,stroke:#333,stroke-width:2px
+ style Registry fill:#cfc,stroke:#333
+ style Transport fill:#ccf,stroke:#333
```
-### Gateway 运行时架构
+#### Core Components
+
+| Component | Module | Responsibility |
+| :--- | :--- | :--- |
+| `A2AGatewayServer` | runtime | Standalone Netty HTTP server entry point. Pre-registers mock agents, wires all components. |
+| `A2AGatewayHttpHandler` | runtime | HTTP request router. Maps REST endpoints to service calls. Supports SSE streaming. |
+| `A2AGatewayService` | runtime | Core orchestration: task submission, response handling, status subscription, SSE push. |
+| `TaskRegistry` | runtime | In-memory task lifecycle state machine with TTL auto-cleanup. |
+| `A2APublishSubscribeService` | runtime | AgentCard registration, discovery, and heartbeat management. |
+| `InMemoryA2AMessageTransport` | runtime | In-memory pub/sub implementation (replaceable by EventMesh broker). |
+| `A2ACardHttpHandler` | runtime | AgentCard CRUD REST endpoints (`/a2a/cards/*`). |
+| `A2AClient` | protocol-a2a | Java SDK for agent developers (HTTP + transport). |
+
+#### Task Lifecycle State Machine
```
- protocol-a2a 模块 runtime 模块 examples 模块
- ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐
- │ A2AMessageTransport(接口) │ A2AGatewayServer │ │ A2AGatewayDemo│
- │ A2AClient (SDK) │<──HTTP──>│ (main, Netty HTTP)│<──HTTP──>│ (纯客户端) │
- │ AgentCard/Topic │ │ InMemoryTransport │ └──────────────┘
- └─────────────────┘ │ GatewayService │
- │ TaskRegistry(TTL) │
- └──────────────────┘
+SUBMITTED → WORKING → COMPLETED
+ ↘ FAILED
+ ↘ CANCELLED
```
-#### 核心运行时组件
+* **TaskRegistry TTL Cleanup**: Terminal-state tasks (COMPLETED/FAILED/CANCELLED) are automatically removed after a configurable TTL (default: 5 minutes). A daemon `ScheduledExecutorService` runs cleanup every 60 seconds, preventing memory leaks from accumulated historical tasks.
+* **Race Condition Prevention**: In `A2AGatewayService.submitTask()`, the pending future is registered (`pendingTasks.put()`) **before** `transport.publish()`. This ordering is critical because `InMemoryTransport` delivers messages synchronously — if publish happened first, `handleResponse()` could execute before `put()` and the future would never complete.
+
+#### REST API
-| 组件 | 模块 | 职责 |
+| Method | Path | Description |
| :--- | :--- | :--- |
-| `A2AGatewayServer` | runtime | Netty HTTP 服务器入口,预注册 mock agent |
-| `A2AGatewayHttpHandler` | runtime | HTTP 请求路由,支持 SSE 流式响应 |
-| `A2AGatewayService` | runtime | 核心编排:任务提交、响应处理、SSE 推送 |
-| `TaskRegistry` | runtime | 内存任务状态机 + TTL 自动清理(5 分钟) |
-| `A2APublishSubscribeService` | runtime | AgentCard 注册、发现、心跳管理 |
-| `InMemoryA2AMessageTransport` | runtime | 内存 pub/sub(可替换为 EventMesh broker) |
-| `A2AClient` | protocol-a2a | Java SDK,提供类型化 API |
+| `POST` | `/a2a/tasks?mode=sync` | Submit task synchronously (wait for result, 30s timeout) |
+| `POST` | `/a2a/tasks?mode=async` | Submit task asynchronously (return taskId immediately) |
+| `GET` | `/a2a/tasks/{taskId}` | Get task status and result |
+| `DELETE` | `/a2a/tasks/{taskId}` | Cancel a task |
+| `GET` | `/a2a/tasks/{taskId}/wait` | Long-poll wait for task result (configurable timeout) |
+| `GET` | `/a2a/tasks/{taskId}/stream` | **SSE** stream of task status updates (`text/event-stream`) |
+| `GET` | `/a2a/agents` | List all registered agents |
+| `POST` | `/a2a/heartbeat` | Agent heartbeat (keeps AgentCard alive) |
+| `GET` | `/a2a/cards/list` | List all AgentCards |
+| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | Register an AgentCard |
+
+#### SSE Streaming
+
+The `GET /a2a/tasks/{taskId}/stream` endpoint provides real-time task status updates via Server-Sent Events:
+
+1. Client opens an HTTP connection with `Accept: text/event-stream`.
+2. Server sends initial state immediately.
+3. As task transitions (WORKING → COMPLETED/FAILED/CANCELLED), server pushes `data:` events.
+4. On terminal state, server sends final event and closes the connection.
+
+The handler writes directly to the Netty channel (returns `null` to skip the default `writeAndFlush` path), using `DefaultHttpContent` chunks with `text/event-stream` content type.
+
+#### A2AClient SDK
+
+The `A2AClient` provides a typed Java API for agent developers:
+
+```java
+A2AClient client = A2AClient.builder()
+ .gatewayUrl("http://localhost:10105")
+ .namespace("global")
+ .agentName("my-agent")
+ .agentCard(card)
+ .heartbeatInterval(30_000)
+ .build();
+client.start();
+
+// Typed return: TaskResult instead of raw JSON
+TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
+String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
+TaskResult status = client.getTaskStatus(taskId);
+List<String> agents = client.listAgents(); // typed List<String>
+boolean ok = client.cancelTask(taskId);
+```
-### 异步 RPC 模式
+`TaskResult` uses `@JsonAlias("result")` to handle the server's `result` field name while exposing a `data` property to callers.
-为了在事件驱动架构中支持 MCP 的 Request/Response 模型,A2A 协议定义了以下映射规则:
+### 3.4 Asynchronous RPC Mapping ( The "Async Bridge" )
-| MCP 概念 | CloudEvent 映射 | 说明 |
+To support MCP on an Event Bus, synchronous RPC concepts are mapped to asynchronous events:
+
+| Concept | MCP / JSON-RPC | CloudEvent Mapping |
| :--- | :--- | :--- |
-| **Request** (`tools/call`) | `type`: `org.apache.eventmesh.a2a.tools.call.req` <br> `mcptype`: `request` | 这是一个请求事件 |
-| **Response** (`result`) | `type`: `org.apache.eventmesh.a2a.common.response` <br> `mcptype`: `response` | 这是一个响应事件 |
-| **Correlation** (`id`) | `extension`: `collaborationid` / `id` | 用于将 Response 关联回 Request |
-| **Target** | `extension`: `targetagent` | 路由目标 Agent ID |
+| **Action** | `method` (e.g., `tools/call`) | **Type**: `org.apache.eventmesh.a2a.tools.call.req`<br>**Extension**: `a2amethod` |
+| **Correlation** | `id` (e.g., `req-123`) | **Extension**: `collaborationid` (on Response)<br>**ID**: Preserved on Request |
+| **Direction** | Implicit (Request vs Result) | **Extension**: `mcptype` (`request` or `response`) |
+| **P2P Routing** | `params._agentId` | **Extension**: `targetagent` |
+| **Pub/Sub Topic** | `params._topic` | **Subject**: The topic value (e.g. `market.btc`) |
+| **Streaming Seq** | `params._seq` | **Extension**: `seq` |
+
+## 4. Functional Specification
+
+### 4.1 Message Processing Flow
+
+1. **Ingestion**: The adaptor receives a `ProtocolTransportObject` (byte array/string).
+2. **Detection**: Checks for `jsonrpc: "2.0"`.
+3. **Transformation (MCP Mode)**:
+ * **Request**: Parses `method`.
+ * If `message/sendStream`, sets type suffix to `.stream` and extracts `_seq`.
+ * If `_topic` present, sets `subject` (Pub/Sub).
+ * If `_agentId` present, sets `targetagent` (P2P).
+ * **Response**: Parses `result`/`error`. Sets `collaborationid` = `id`.
+4. **Batch Processing**: Splits JSON Array into a `List<CloudEvent>`.
+
+### 4.2 Key Features
+
+#### A. Intelligent Routing Support
+* **Mechanism**: Promotes `_agentId` or `_topic` from JSON body to CloudEvent attributes.
+* **Benefit**: Enables EventMesh Router to perform content-based routing (CBR) efficiently.
+
+#### B. Batching
+* **Benefit**: Significantly increases throughput for high-frequency interactions.
+
+#### C. Streaming Support
+* **Operation**: `message/sendStream`
+* **Mechanism**: Maps to `.stream` event type and preserves sequence order via `seq` extension attribute.
+
+#### D. SSE Task Streaming (Gateway)
+* **Endpoint**: `GET /a2a/tasks/{taskId}/stream`
+* **Mechanism**: Server-Sent Events (`text/event-stream`) pushes real-time task state transitions to the client.
+* **Flow**: Initial state → WORKING updates → terminal state (COMPLETED/FAILED/CANCELLED) → connection close.
+* **Implementation**: Handler writes `DefaultHttpContent` chunks directly to the Netty channel, bypassing the standard `FullHttpResponse` path.
+
+#### E. Task TTL Auto-Cleanup (Gateway)
+* **Problem**: Completed/failed tasks accumulate in `TaskRegistry` indefinitely, causing memory leaks.
+* **Solution**: A daemon `ScheduledExecutorService` (`a2a-task-ttl-cleanup` thread) runs every 60 seconds, removing terminal-state tasks older than the TTL (default: 5 minutes).
+* **Configuration**: `TaskRegistry(taskTtlMs, cleanupIntervalMs)` constructor allows custom tuning.
+
+#### F. AgentCard Discovery & Heartbeat (Gateway)
+* **Registration**: `POST /a2a/cards/card/{org}/{unit}/{agent}` registers an `AgentCard`.
+* **Heartbeat**: `POST /a2a/heartbeat` refreshes the agent's last-seen timestamp. Cards expire after 60 seconds without heartbeat.
+* **Discovery**: `GET /a2a/agents` returns all live agent cards.
-## 协议消息格式
+## 5. Usage Examples
-### 1. MCP Request (JSON-RPC 2.0)
+### 5.1 JSON-RPC 2.0 (MCP) Mode
+This mode is ideal for LLMs, scripts, and simple integrations where you want to send raw JSON without worrying about CloudEvent headers.
+
+#### 5.1.1 Sending a Tool Call (RPC Request)
+
+**Client Sends (Raw JSON):**
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
- "name": "get_weather",
- "arguments": {
- "city": "Shanghai"
- },
- "_agentId": "weather-service" // 路由提示
+ "name": "weather",
+ "city": "Shanghai",
+ "_agentId": "weather-agent"
},
- "id": "req-123456"
+ "id": "req-101"
}
```
-**转换后的 CloudEvent:**
-- `id`: `req-123456`
-- `type`: `org.apache.eventmesh.a2a.tools.call.req`
-- `source`: `eventmesh-a2a`
-- `extension: a2amethod`: `tools/call`
-- `extension: mcptype`: `request`
-- `extension: targetagent`: `weather-service`
+**EventMesh Converts to:**
+* **Type**: `org.apache.eventmesh.a2a.tools.call.req`
+* **Extension (targetagent)**: `weather-agent`
+* **Extension (mcptype)**: `request`
-### 2. MCP Response (JSON-RPC 2.0)
+#### 5.1.2 Pub/Sub Broadcast (Notification)
+**Client Sends (Raw JSON):**
```json
{
"jsonrpc": "2.0",
- "result": {
- "content": [
- {
- "type": "text",
- "text": "Shanghai: 25°C, Sunny"
- }
- ]
- },
- "id": "req-123456"
+ "method": "notifications/alert",
+ "params": {
+ "message": "System Maintenance in 10 mins",
+ "_topic": "system.alerts"
+ }
}
```
-**转换后的 CloudEvent:**
-- `id`: `uuid-new-event-id`
-- `type`: `org.apache.eventmesh.a2a.common.response`
-- `extension: collaborationid`: `req-123456` (关联 ID)
-- `extension: mcptype`: `response`
+**EventMesh Converts to:**
+* **Type**: `org.apache.eventmesh.a2a.notifications.alert`
+* **Subject**: `system.alerts`
+* **Extension (mcptype)**: `notification`
+
+#### 5.1.3 Java SDK Example (MCP Mode)
+
+```java
+// See eventmesh-examples/src/main/java/org/apache/eventmesh/a2a/demo/mcp/McpCaller.java
+
+Map<String, Object> request = new HashMap<>();
+request.put("jsonrpc", "2.0");
+request.put("method", "tools/call");
+request.put("params", Map.of("name", "weather", "_agentId", "weather-agent"));
+request.put("id", UUID.randomUUID().toString());
+
+CloudEvent event = CloudEventBuilder.v1()
+ .withType("org.apache.eventmesh.a2a.tools.call.req")
+ .withData(JsonUtils.toJSONString(request).getBytes())
+ .withExtension("protocol", "A2A") // Critical to trigger A2A adaptor
+ .build();
+
+producer.publish(event);
+```
+
+### 5.2 Native CloudEvents Mode
+
+This mode provides full control over all CloudEvent attributes and is recommended for robust, typed applications using the EventMesh SDK.
+
+#### 5.2.1 Native RPC Request
+
+**Client Sends (CloudEvent):**
+```json
+{
+ "specversion": "1.0",
+ "type": "com.example.rpc.request",
+ "source": "my-app",
+ "id": "evt-123",
+ "data": "...",
+ "protocol": "A2A",
+ "targetagent": "target-agent-001"
+}
+```
+
+**Java SDK Example:**
+```java
+// See eventmesh-examples/src/main/java/org/apache/eventmesh/a2a/demo/ce/CloudEventsCaller.java
+
+CloudEvent event = CloudEventBuilder.v1()
+ .withId(UUID.randomUUID().toString())
+ .withSource(URI.create("ce-client"))
+ .withType("com.example.rpc.request")
+ .withData("application/text", "RPC Payload".getBytes())
+ .withExtension("protocol", "A2A")
+ .withExtension("targetagent", "target-agent-001") // Explicit routing
+ .build();
+
+producer.publish(event);
+```
+
+#### 5.2.2 Native Pub/Sub
+
+**Client Sends (CloudEvent):**
+```json
+{
+ "specversion": "1.0",
+ "type": "com.example.notification",
+ "source": "my-app",
+ "subject": "broadcast.topic",
+ "protocol": "A2A"
+}
+```
-### 3. Legacy A2A Message (兼容模式)
+#### 5.2.3 Native Streaming
+**Client Sends (CloudEvent):**
```json
{
+ "specversion": "1.0",
+ "type": "com.example.stream",
+ "source": "my-app",
+ "subject": "stream-topic",
"protocol": "A2A",
- "messageType": "PROPOSE",
- "sourceAgent": { "agentId": "agent-001" },
- "payload": { "task": "data-process" }
+ "sessionid": "session-555",
+ "seq": "1"
}
```
+### 5.3 Gateway REST API (HTTP)
+
+The A2A Gateway provides a REST API for external clients and non-Java agents.
+
+#### 5.3.1 Submit Task (Sync)
+
+```bash
+curl -X POST 'http://localhost:10105/a2a/tasks?mode=sync' \
+ -H 'Content-Type: application/json' \
+ -d '{"targetAgent":"weather-agent","message":"Beijing"}'
+```
+
+Response:
+```json
+{
+ "taskId": "task-a1b2c3d4",
+ "state": "COMPLETED",
+ "data": "The weather in Beijing is sunny, 25°C"
+}
+```
+
+#### 5.3.2 Submit Task (Async)
+
+```bash
+curl -X POST 'http://localhost:10105/a2a/tasks?mode=async' \
+ -H 'Content-Type: application/json' \
+ -d '{"targetAgent":"weather-agent","message":"Shanghai"}'
+```
+
+Response (HTTP 202):
+```json
+{
+ "taskId": "task-e5f6g7h8",
+ "status": "accepted",
+ "message": "Task submitted. Use GET /a2a/tasks/task-e5f6g7h8 to check status."
+}
+```
+
+#### 5.3.3 SSE Stream
+
+```bash
+curl -N http://localhost:10105/a2a/tasks/task-a1b2c3d4/stream
+```
+
+Response (`text/event-stream`):
+```
+data: {"taskId":"task-a1b2c3d4","state":"SUBMITTED"}
+
+data: {"taskId":"task-a1b2c3d4","state":"WORKING","data":"processing..."}
+
+data: {"taskId":"task-a1b2c3d4","state":"completed","data":"The weather in Beijing is sunny, 25°C"}
+```
+
+#### 5.3.4 List Agents
+
+```bash
+curl http://localhost:10105/a2a/agents
+```
+
+### 5.4 A2AClient SDK (Java)
+
+```java
+A2AClient client = A2AClient.builder()
+ .gatewayUrl("http://localhost:10105")
+ .namespace("global")
+ .agentName("my-agent")
+ .agentCard(card)
+ .heartbeatInterval(30_000)
+ .build();
+
+client.start();
+
+// Synchronous task (returns typed TaskResult)
+TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
+
+// Asynchronous task (returns taskId immediately)
+String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
+
+// Poll status
+TaskResult status = client.getTaskStatus(taskId);
+
+// Cancel
+boolean cancelled = client.cancelTask(taskId);
+
+// List registered agents (typed List<String>)
+List<String> agents = client.listAgents();
+
+client.shutdown();
+```
+
+## 6. Future Roadmap
+
+* **EventMesh Broker Integration**: Replace `InMemoryA2AMessageTransport` with the real EventMesh broker for production deployment.
+* **Schema Registry**: Implement dynamic discovery of Agent capabilities via `methods/list`.
+* **Sidecar Injection**: Fully integrate the adaptor into the EventMesh Sidecar for non-Java agents (Python, Node.js).
+* **WebSocket Streaming**: Extend SSE to bidirectional WebSocket for real-time agent-to-agent dialogue.
+* **Task Persistence**: Persist `TaskRegistry` state to a durable store (Redis/DB) for crash recovery.
+* **Authentication**: Add API key / JWT authentication to the Gateway REST API.
+
+---
+
+
+## 核心成果
+
+A2A 协议已成功重构为采用 **MCP (Model Context Protocol)** 架构,将 EventMesh 定位为现代化的 **智能体协作总线 (Agent Collaboration Bus)**。
+
+### 1. 核心协议重构 (`EnhancedA2AProtocolAdaptor`)
+- **混合引擎 (JSON-RPC & CloudEvents)**: 实现了智能解析引擎,支持:
+ - **MCP/JSON-RPC 2.0**: 面向 LLM 和脚本的低门槛接入,自动封装 CloudEvent。
+ - **原生 CloudEvents**: 面向 EventMesh 原生应用的灵活接入,支持自定义元数据和透传。
+ - 适配器根据 `jsonrpc` 字段自动分发处理逻辑。
+- **异步 RPC 映射**: 建立了同步 RPC 语义与异步事件驱动架构 (EDA) 之间的桥梁。
+ - **请求 (Requests)** 映射为 `*.req` 事件,属性 `mcptype=request`。
+ - **响应 (Responses)** 映射为 `*.resp` 事件,属性 `mcptype=response`。
+ - **关联 (Correlation)** 通过将 JSON-RPC `id` 映射到 CloudEvent `collaborationid` 来处理。
+- **路由优化**: 实现了"深度内容路由提取":
+ - `params._agentId` -> CloudEvent 扩展属性 `targetagent` (P2P)。
+ - `params._topic` -> CloudEvent Subject (Pub/Sub)。
+
+### 2. 原生 Pub/Sub 与流式支持
+- **Pub/Sub**: 通过将 `_topic` 映射到 CloudEvent Subject,支持 O(1) 广播复杂度。
+- **流式 (Streaming)**: 支持 `message/sendStream` 操作,映射为 `.stream` 事件类型,并通过 `_seq` -> `seq` 扩展属性保证顺序。
+
+### 3. 标准化与兼容性
+- **数据模型**: 定义了符合 JSON-RPC 2.0 规范的 `JsonRpcRequest`、`JsonRpcResponse`、`JsonRpcError` POJO 对象。
+- **方法定义**: 引入了 `McpMethods` 常量,支持标准操作如 `tools/call`、`resources/read`。
+- **AgentCard 模型**: 实现了 `AgentCard`、`AgentSkill`、`AgentInterface`、`AgentCapabilities` 等完整的 Agent 能力描述模型。
+
+### 4. Gateway 运行时架构 (`eventmesh-runtime`)
+
+完整的独立 HTTP Gateway 服务,桥接外部客户端到 A2A 事件总线。
+
+#### 核心组件
+
+| 组件 | 职责 |
+| :--- | :--- |
+| `A2AGatewayServer` | Netty HTTP 服务器入口,预注册 mock agent,组装所有组件 |
+| `A2AGatewayHttpHandler` | HTTP 请求路由,支持 SSE 流式响应 |
+| `A2AGatewayService` | 核心编排:任务提交、响应处理、状态订阅、SSE 推送 |
+| `TaskRegistry` | 内存任务状态机 + TTL 自动清理 |
+| `A2APublishSubscribeService` | AgentCard 注册、发现、心跳管理 |
+| `InMemoryA2AMessageTransport` | 内存 pub/sub 实现(可替换为 EventMesh broker) |
+| `A2ACardHttpHandler` | AgentCard CRUD REST 端点 |
+| `A2AClient` | Java SDK,提供类型化 API |
+
+#### REST API
+
+| 方法 | 路径 | 说明 |
+| :--- | :--- | :--- |
+| `POST` | `/a2a/tasks?mode=sync` | 同步提交任务 |
+| `POST` | `/a2a/tasks?mode=async` | 异步提交任务 |
+| `GET` | `/a2a/tasks/{taskId}` | 查询任务状态 |
+| `DELETE` | `/a2a/tasks/{taskId}` | 取消任务 |
+| `GET` | `/a2a/tasks/{taskId}/wait` | 长轮询等待结果 |
+| `GET` | `/a2a/tasks/{taskId}/stream` | SSE 流式推送状态更新 |
+| `GET` | `/a2a/agents` | 列出已注册 agents |
+| `POST` | `/a2a/heartbeat` | Agent 心跳 |
+| `GET` | `/a2a/cards/list` | 列出所有 AgentCard |
+| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | 注册 AgentCard |
+
+### 5. 关键改进
+
+#### 5.1 TaskRegistry TTL 自动清理
+- **问题**: 终态任务(COMPLETED/FAILED/CANCELLED)无限累积导致内存泄漏。
+- **方案**: 守护线程 `ScheduledExecutorService` 每 60 秒扫描一次,清理超过 TTL(默认 5 分钟)的终态任务。
+- **配置**: `TaskRegistry(taskTtlMs, cleanupIntervalMs)` 构造函数支持自定义调优。
+
+#### 5.2 竞态条件修复
+- **问题**: `InMemoryTransport` 同步投递消息,若 `transport.publish()` 在 `pendingTasks.put()` 之前执行,`handleResponse()` 会先于 `put()` 运行,导致 future 永不完成。
+- **方案**: 严格保证 `pendingTasks.put(taskId, future)` 在 `transport.publish()` 之前执行,并添加注释说明顺序重要性。
+
+#### 5.3 A2AClient 类型化返回
+- **改进**: `getTaskStatus()` 返回 `TaskResult` 对象(而非原始 JSON 字符串),`listAgents()` 返回 `List<String>`(而非原始 JSON)。
+- **兼容**: `TaskResult.data` 字段使用 `@JsonAlias("result")` 注解,兼容服务端 `result` 字段名。
+
+#### 5.4 SSE 流式响应
+- **端点**: `GET /a2a/tasks/{taskId}/stream`
+- **实现**: Handler 直接写入 Netty channel(`DefaultHttpContent` chunks),返回 `null` 跳过标准 `FullHttpResponse` 路径。通过 `StatusSubscriber` 回调实时推送状态变更。
+
+#### 5.5 使用文档
+- 新建 `eventmesh-examples/.../demo/README.md`,包含架构图、API 表、curl 示例、SDK 用法、运行方式。
+
+### 6. 测试与质量
+- **协议层单元测试**: `EnhancedA2AProtocolAdaptorTest` 覆盖请求/响应循环、错误处理、通知和批处理。
+- **Topic 工具测试**: `A2ATopicFactoryTest` 覆盖 topic 生成与解析。
+- **Gateway 运行时测试**:
+ - `TaskRegistryTest` — 任务状态机 + TTL 清理验证
+ - `InMemoryA2AMessageTransportTest` — 内存传输投递
+ - `A2AGatewayServiceTest` — Gateway 服务层
+ - `A2AGatewayEndToEndTest` — 进程内全链路
+ - `A2AClientServerIntegrationTest` — 真实 HTTP 客户端-服务端集成测试
+- **集成演示**: `McpIntegrationDemoTest`、`McpPatternsIntegrationTest`、`McpComprehensiveDemoTest`、`CloudEventsComprehensiveDemoTest`
+- **总计**: 73 个测试场景,全部通过。
+
+## 下一步计划
+
+1. **EventMesh Broker 集成**: 用真实 EventMesh broker 替换 `InMemoryA2AMessageTransport`,实现生产级部署。
+2. **路由集成**: 更新 EventMesh Runtime Router,利用 `targetagent` 和 `a2amethod` 扩展属性实现高级路由规则。
+3. **Schema 注册中心**: 实现"注册中心智能体 (Registry Agent)",允许智能体动态发布 MCP 能力 (`methods/list`)。
+4. **Sidecar 支持**: 将 A2A 适配器逻辑暴露在 Sidecar 代理中,允许非 Java 智能体通过 HTTP/JSON 交互。
+5. **WebSocket 流式**: 将 SSE 扩展为双向 WebSocket,支持实时 agent 对话。
+6. **任务持久化**: 将 `TaskRegistry` 状态持久化到 Redis/DB,支持崩溃恢复。
+7. **认证授权**: 为 Gateway REST API 添加 API Key / JWT 认证。
+
+---
+
+
+**Date**: 2026-06-19
+**Version**: v2.0.0 (MCP Edition + Gateway Runtime)
+**Status**: ✅ **PASS**
+
+## Test Suite Summary
+
+The test suite provides comprehensive coverage across two layers: the **Protocol Adaptor** (JSON-RPC 2.0 & Native CloudEvents) and the **Gateway Runtime** (HTTP REST API, Task lifecycle, SSE streaming, AgentCard discovery).
+
+### Protocol Adaptor Tests
+
+| Test Class | Scenarios | Result | Description |
+| :--- | :--- | :--- | :--- |
+| `EnhancedA2AProtocolAdaptorTest` | 12 | **PASS** | Unit tests covering core protocol logic, MCP parsing, Batching, Error handling, and A2A Standard Ops. |
+| `McpIntegrationDemoTest` | 1 | **PASS** | End-to-end RPC demo using MCP (JSON-RPC). |
+| `McpPatternsIntegrationTest` | 2 | **PASS** | End-to-end Pub/Sub and Streaming demos using MCP (JSON-RPC). |
+| `McpComprehensiveDemoTest` | 3 | **PASS** | Validation of all 3 patterns in MCP mode. |
+| `CloudEventsComprehensiveDemoTest` | 3 | **PASS** | Validation of all 3 patterns in Native CloudEvents mode. |
+| `A2ATopicFactoryTest` | 8 | **PASS** | Topic naming and parsing (request/response/status topics). |
+
+### Gateway Runtime Tests
+
+| Test Class | Scenarios | Result | Description |
+| :--- | :--- | :--- | :--- |
+| `TaskRegistryTest` | 6 | **PASS** | Task state machine transitions, parent-child relationships, TTL auto-cleanup. |
+| `InMemoryA2AMessageTransportTest` | 4 | **PASS** | In-memory pub/sub delivery, subscribe/unsubscribe, wildcard topics. |
+| `A2AGatewayServiceTest` | 8 | **PASS** | Gateway service layer: task submission (sync/async), response handling, cancel, status subscription. |
+| `A2AGatewayEndToEndTest` | 6 | **PASS** | In-process end-to-end: client → gateway → transport → agent → response → client. |
+| `A2AClientServerIntegrationTest` | 20 | **PASS** | Real HTTP client-server integration: AgentCard registration, sync/async tasks, status query, cancel, list agents, SSE streaming. |
+
+**Total Scenarios**: 73 (All Passed)
+
+## Detailed Test Cases
+
+### 1. `EnhancedA2AProtocolAdaptorTest` (Unit)
+- **MCP Core**: Validated Request/Response/Notification mapping.
+- **Error Handling**: Validated JSON-RPC Error object mapping.
+- **Batching**: Validated JSON Array splitting.
+- **Legacy Removal**: Confirmed legacy A2A format is no longer processed.
+- **A2A Ops**: Verified `task/get`, `message/sendStream` mappings.
+
+### 2. `A2ATopicFactoryTest` (Unit)
+- Validated topic generation for request, response, and status topics.
+- Validated topic parsing (extracting namespace, agent name, task ID, topic type).
+- Verified wildcard topic patterns for gateway subscriptions.
+
+### 3. `TaskRegistryTest` (Unit)
+- **State Machine**: SUBMITTED → WORKING → COMPLETED/FAILED/CANCELLED transitions.
+- **Parent-Child**: Task hierarchy tracking and child task listing.
+- **TTL Cleanup**: Verified that terminal-state tasks are removed after TTL expires.
+- **Concurrency**: Thread-safe state transitions under concurrent access.
+
+### 4. `InMemoryA2AMessageTransportTest` (Unit)
+- Publish/subscribe message delivery.
+- Multiple subscribers on the same topic.
+- Unsubscribe behavior.
+- Wildcard topic matching.
+
+### 5. `A2AGatewayServiceTest` (Integration)
+- **Sync Task**: submitTask → publish → handleResponse → future.complete.
+- **Async Task**: submitTask returns immediately, status queried separately.
+- **Cancel**: cancelTask transitions state and completes future with CANCELLED.
+- **Race Condition**: Verified put-before-publish ordering prevents lost responses.
+- **Status Subscription**: StatusSubscriber receives state transition callbacks.
+
+### 6. `A2AGatewayEndToEndTest` (Integration)
+- Full flow: A2AClient → Gateway HTTP → GatewayService → Transport → Agent → Response → Client.
+- Verified task ID correlation across all components.
+- Multiple concurrent tasks.
+- Error scenarios (unknown agent, task not found).
+
+### 7. `A2AClientServerIntegrationTest` (HTTP Integration)
+- **Real HTTP**: Uses Apache HttpClient to hit the real Netty server.
+- **AgentCard**: Registration and heartbeat via REST API.
+- **Sync Task**: `POST /a2a/tasks?mode=sync` returns completed result.
+- **Async Task**: `POST /a2a/tasks?mode=async` returns taskId, then `GET /a2a/tasks/{taskId}` polls status.
+- **Cancel**: `DELETE /a2a/tasks/{taskId}` cancels the task.
+- **List Agents**: `GET /a2a/agents` returns registered agent list.
+- **Typed Returns**: `A2AClient.getTaskStatus()` returns `TaskResult`, `listAgents()` returns `List<String>`.
+- **SSE Stream**: `GET /a2a/tasks/{taskId}/stream` receives real-time state updates via `text/event-stream`.
+
+### 8. `McpIntegrationDemoTest` (Integration - RPC)
+- Simulated Client → EventMesh → Server flow.
+- Verified correlation ID linking (`req-id` <-> `collaborationid`).
+
+### 9. `McpPatternsIntegrationTest` (Integration - Advanced)
+- **Pub/Sub**: Verified `_topic` -> `subject` mapping for Broadcast.
+- **Streaming**: Verified `_seq` -> `seq` mapping for ordered chunks.
+
+### 10. `McpComprehensiveDemoTest` (Protocol: JSON-RPC)
+- **RPC**: Request/Response flow verification.
+- **Pub/Sub**: Broadcast to Topic routing verification.
+- **Streaming**: Sequence ID preservation verification.
+
+### 11. `CloudEventsComprehensiveDemoTest` (Protocol: Native CloudEvents)
+- **RPC**: Verified manual construction of `.req` / `.resp` CloudEvents works.
+- **Pub/Sub**: Verified manual setting of `subject` works.
+- **Streaming**: Verified manual setting of `seq` extension works.
+
+## Environment
+
+- **JDK**: Java 8 (Source/Target 1.8), Compatible with Java 21 Runtime
+- **Build System**: Gradle 7.x+
+- **Dependencies**: Jackson 2.18+, CloudEvents SDK 3.0+, Netty 4.1+, Apache HttpClient
+
+## Conclusion
+
+The A2A Protocol v2.0 implementation is stable, functionally complete, and ready for production deployment. It successfully supports:
+- **Hybrid Architecture** (MCP & CloudEvents) with all three interaction patterns (RPC, Pub/Sub, Streaming)
+- **Gateway Runtime** with full REST API, SSE streaming, task lifecycle management, TTL auto-cleanup, and typed Java SDK
+- **73 test scenarios** across protocol and runtime layers, all passing
+
+---
+
+## 中文使用指南(整合自 README.md)
+
## 使用指南
### 1. 作为 Client 发起 MCP 调用
diff --git a/docs/a2a-protocol/README_EN.md b/docs/a2a-protocol/README_EN.md
deleted file mode 100644
index b7d8aa5fe..000000000
--- a/docs/a2a-protocol/README_EN.md
+++ /dev/null
@@ -1,255 +0,0 @@
-# EventMesh A2A (Agent-to-Agent Communication Protocol)
-
-## Overview
-
-The **EventMesh A2A (Agent-to-Agent) Protocol** is a specialized, high-performance protocol plugin designed to enable asynchronous communication, collaboration, and task coordination between autonomous agents.
-
-With the release of v2.0, A2A adopts the **MCP (Model Context Protocol)** architecture, transforming EventMesh into a robust **Agent Collaboration Bus**. It bridges the gap between synchronous LLM-based tool calls (JSON-RPC 2.0) and asynchronous Event-Driven Architectures (EDA), enabling scalable, distributed, and decoupled agent systems.
-
-## Core Features
-
-### 1. MCP over CloudEvents
-- **Standard Compliance**: Fully supports standard methods defined by **MCP (Model Context Protocol)**, such as `tools/call`, `resources/read`.
-- **Event-Driven**: Maps synchronous RPC calls to asynchronous **Request/Response Event Streams**, leveraging EventMesh's high-concurrency processing capabilities.
-- **Transport Agnostic**: All MCP messages are encapsulated within standard **CloudEvents** envelopes, running over any transport layer supported by EventMesh (HTTP, TCP, gRPC, Kafka).
-
-### 2. Dual-Mode Support (Hybrid Architecture)
-
-A2A Protocol features a unique **Dual-Mode** architecture that simultaneously supports:
-
-1. **JSON-RPC 2.0 (MCP Mode)**:
- * **Target**: LLMs, Scripts (Python/JS), LangChain integration.
- * **Benefit**: Extremely low barrier to entry. Clients send simple JSON objects; the adaptor automatically wraps them in CloudEvents.
-2. **Native CloudEvents (Power Mode)**:
- * **Target**: EventMesh native apps, Knative, Serverless functions.
- * **Benefit**: Full control over event metadata. Allows pass-through of custom or binary data.
-
-**Mechanism**: The `EnhancedA2AProtocolAdaptor` intelligently detects the payload format. If `jsonrpc: "2.0"` is present, it engages the MCP translation engine; otherwise, it treats the payload as a standard CloudEvent (delegating to the underlying CloudEvents adaptor).
-
-### 3. Native Pub/Sub Semantics
-- **O(1) Broadcast**: Publishers send messages once to a Topic, and EventMesh efficiently fans out to all subscribers.
-- **Decoupling**: Solves the scalability issues of traditional P2P Webhook callbacks.
-- **Isolation**: Provides backpressure isolation between publishers and subscribers.
-
-### 3. High Performance & Routing
-- **Batch Processing**: Natively supports JSON-RPC Batch requests. EventMesh automatically splits them into parallel event streams.
-- **Intelligent Routing**: Extracts routing hints (`_agentId` for P2P, `_topic` for Pub/Sub) from MCP parameters and injects them into CloudEvents attributes for zero-decoding routing.
-
-### 4. Streaming Support
-- **Sequencing**: Preserves message order for streaming operations (`message/sendStream`) using sequence IDs.
-
-## Architecture
-
-### Core Components
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ EventMesh A2A Protocol v2.0 │
-│ (MCP over CloudEvents Architecture) │
-├─────────────────────────────────────────────────────────────┤
-│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
-│ │ MCP/JSON-RPC│ │ Native │ │ Protocol │ │
-│ │ Handler │ │ Pub/Sub │ │ Delegator │ │
-│ └─────────────┘ └─────────────┘ └─────────────┘ │
-├─────────────────────────────────────────────────────────────┤
-│ ┌───────────────────────────────────────────────────────┐ │
-│ │ Enhanced A2A Protocol Adaptor │ │
-│ │ (Intelligent Parsing & CloudEvent Mapping) │ │
-│ └───────────────────────────────────────────────────────┘ │
-├─────────────────────────────────────────────────────────────┤
-│ EventMesh Protocol Infrastructure │
-│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
-│ │ CloudEvents │ │ HTTP │ │ gRPC │ │
-│ │ Protocol │ │ Protocol │ │ Protocol │ │
-│ └─────────────┘ └─────────────┘ └─────────────┘ │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### Gateway Runtime Architecture
-
-```
- protocol-a2a module runtime module examples module
- ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐
- │ A2AMessageTransport(Iface) │ A2AGatewayServer │ │ A2AGatewayDemo│
- │ A2AClient (SDK) │<──HTTP──>│ (main, Netty HTTP)│<──HTTP──>│ (client only) │
- │ AgentCard/Topic │ │ InMemoryTransport │ └──────────────┘
- └─────────────────┘ │ GatewayService │
- │ TaskRegistry(TTL) │
- └──────────────────┘
-```
-
-#### Core Runtime Components
-
-| Component | Module | Responsibility |
-| :--- | :--- | :--- |
-| `A2AGatewayServer` | runtime | Netty HTTP server entry point, pre-registers mock agents |
-| `A2AGatewayHttpHandler` | runtime | HTTP request router, supports SSE streaming |
-| `A2AGatewayService` | runtime | Core orchestration: task submission, response handling, SSE push |
-| `TaskRegistry` | runtime | In-memory task state machine + TTL auto-cleanup (5 min) |
-| `A2APublishSubscribeService` | runtime | AgentCard registration, discovery, heartbeat |
-| `InMemoryA2AMessageTransport` | runtime | In-memory pub/sub (replaceable by EventMesh broker) |
-| `A2AClient` | protocol-a2a | Java SDK with typed API |
-
-### Asynchronous RPC Pattern
-
-To support the MCP Request/Response model within an event-driven architecture, A2A defines the following mapping rules:
-
-| MCP Concept | CloudEvent Mapping | Description |
-| :--- | :--- | :--- |
-| **Request** (`tools/call`) | `type`: `org.apache.eventmesh.a2a.tools.call.req` <br> `mcptype`: `request` | Request event. |
-| **Response** (`result`) | `type`: `org.apache.eventmesh.a2a.common.response` <br> `mcptype`: `response` | Response event. |
-| **Correlation** (`id`) | `extension`: `collaborationid` / `id` | Links Response to Request. |
-| **P2P Target** | `extension`: `targetagent` | Routing target Agent ID. |
-| **Pub/Sub Topic** | `subject`: `<topic_name>` | Broadcast Topic. |
-
-## Protocol Message Format
-
-### 1. MCP Request (P2P)
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "get_weather",
- "_agentId": "weather-service" // P2P Routing
- },
- "id": "req-123"
-}
-```
-
-### 2. MCP Request (Pub/Sub)
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "market/update",
- "params": {
- "price": 50000,
- "_topic": "market.btc" // Pub/Sub Routing
- }
-}
-```
-
-## Usage Guide
-
-### 1. Initiate Call (Client)
-
-```java
-// 1. Construct MCP Request JSON
-String mcpRequest = "{" +
- "\"jsonrpc\": \"2.0\"," +
- "\"method\": \"tools/call\"," +
- "\"params\": { \"name\": \"weather\", \"_agentId\": \"weather-agent\" }," +
- "\"id\": \"req-001\"" +
- "}";
-
-// 2. Send via EventMesh SDK
-eventMeshProducer.publish(new A2AProtocolTransportObject(mcpRequest));
-```
-
-### 2. Handle Request (Server)
-
-Subscribe to the topic `org.apache.eventmesh.a2a.tools.call.req`, process logic, and send back response with matching `id`.
-
-### 3. Gateway REST API
-
-The A2A Gateway provides a full REST API for external clients and non-Java agents:
-
-```bash
-# Sync task
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=sync' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Beijing"}'
-
-# Async task
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=async' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Shanghai"}'
-
-# Query status
-curl http://localhost:10105/a2a/tasks/{taskId}
-
-# SSE stream
-curl -N http://localhost:10105/a2a/tasks/{taskId}/stream
-
-# List agents
-curl http://localhost:10105/a2a/agents
-```
-
-#### REST API Endpoints
-
-| Method | Path | Description |
-|------|------|------|
-| POST | `/a2a/tasks?mode=sync` | Submit task synchronously (wait for result) |
-| POST | `/a2a/tasks?mode=async` | Submit task asynchronously (return taskId) |
-| GET | `/a2a/tasks/{taskId}` | Get task status |
-| DELETE | `/a2a/tasks/{taskId}` | Cancel task |
-| GET | `/a2a/tasks/{taskId}/wait` | Long-poll wait for result |
-| GET | `/a2a/tasks/{taskId}/stream` | SSE stream of task status updates |
-| GET | `/a2a/agents` | List registered agents |
-| POST | `/a2a/heartbeat` | Agent heartbeat |
-| GET | `/a2a/cards/list` | List all AgentCards |
-| POST | `/a2a/cards/card/{org}/{unit}/{agent}` | Register AgentCard |
-
-### 4. A2AClient Java SDK
-
-```java
-A2AClient client = A2AClient.builder()
- .gatewayUrl("http://localhost:10105")
- .namespace("global")
- .agentName("my-agent")
- .agentCard(card)
- .heartbeatInterval(30_000)
- .build();
-
-client.start();
-
-// Sync task (returns typed TaskResult)
-TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
-
-// Async task (returns taskId)
-String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
-
-// Query status
-TaskResult status = client.getTaskStatus(taskId);
-
-// Cancel
-boolean cancelled = client.cancelTask(taskId);
-
-// List agents (returns List<String>)
-List<String> agents = client.listAgents();
-
-client.shutdown();
-```
-
-## Version History
-
-- **v2.0.0**: Fully Embraced MCP (Model Context Protocol)
- - Introduced `EnhancedA2AProtocolAdaptor` supporting JSON-RPC 2.0.
- - Implemented Async RPC over CloudEvents pattern.
- - Added **Native Pub/Sub** support via `_topic` parameter.
- - Added **Streaming** support via `_seq` parameter.
-
-- **v2.1.0**: Gateway Runtime Architecture
- - Added `A2AGatewayServer` (Netty HTTP) standalone Gateway service.
- - Implemented `TaskRegistry` task state machine + TTL auto-cleanup (5 min).
- - Added SSE streaming response (`GET /a2a/tasks/{taskId}/stream`).
- - `A2AClient` SDK returns typed objects (`TaskResult`, `List<String>`).
- - Fixed `pendingTasks` race condition (put-before-publish).
- - AgentCard registration, discovery, heartbeat management.
- - 73 test scenarios all passing.
-
-## Contribution
-
-Welcome to contribute code and documentation!
-
-## License
-
-Apache License 2.0
-
-## Contact
-
-- Project Homepage: https://eventmesh.apache.org
-- Issues: https://github.com/apache/eventmesh/issues
-- Mailing List: dev@eventmesh.apache.org
\ No newline at end of file
diff --git a/docs/a2a-protocol/TEST_RESULTS.md b/docs/a2a-protocol/TEST_RESULTS.md
deleted file mode 100644
index 01a79014f..000000000
--- a/docs/a2a-protocol/TEST_RESULTS.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# Test Results: EventMesh A2A Protocol v2.0
-
-**Date**: 2026-06-19
-**Version**: v2.0.0 (MCP Edition + Gateway Runtime)
-**Status**: ✅ **PASS**
-
-## Test Suite Summary
-
-The test suite provides comprehensive coverage across two layers: the **Protocol Adaptor** (JSON-RPC 2.0 & Native CloudEvents) and the **Gateway Runtime** (HTTP REST API, Task lifecycle, SSE streaming, AgentCard discovery).
-
-### Protocol Adaptor Tests
-
-| Test Class | Scenarios | Result | Description |
-| :--- | :--- | :--- | :--- |
-| `EnhancedA2AProtocolAdaptorTest` | 12 | **PASS** | Unit tests covering core protocol logic, MCP parsing, Batching, Error handling, and A2A Standard Ops. |
-| `McpIntegrationDemoTest` | 1 | **PASS** | End-to-end RPC demo using MCP (JSON-RPC). |
-| `McpPatternsIntegrationTest` | 2 | **PASS** | End-to-end Pub/Sub and Streaming demos using MCP (JSON-RPC). |
-| `McpComprehensiveDemoTest` | 3 | **PASS** | Validation of all 3 patterns in MCP mode. |
-| `CloudEventsComprehensiveDemoTest` | 3 | **PASS** | Validation of all 3 patterns in Native CloudEvents mode. |
-| `A2ATopicFactoryTest` | 8 | **PASS** | Topic naming and parsing (request/response/status topics). |
-
-### Gateway Runtime Tests
-
-| Test Class | Scenarios | Result | Description |
-| :--- | :--- | :--- | :--- |
-| `TaskRegistryTest` | 6 | **PASS** | Task state machine transitions, parent-child relationships, TTL auto-cleanup. |
-| `InMemoryA2AMessageTransportTest` | 4 | **PASS** | In-memory pub/sub delivery, subscribe/unsubscribe, wildcard topics. |
-| `A2AGatewayServiceTest` | 8 | **PASS** | Gateway service layer: task submission (sync/async), response handling, cancel, status subscription. |
-| `A2AGatewayEndToEndTest` | 6 | **PASS** | In-process end-to-end: client → gateway → transport → agent → response → client. |
-| `A2AClientServerIntegrationTest` | 20 | **PASS** | Real HTTP client-server integration: AgentCard registration, sync/async tasks, status query, cancel, list agents, SSE streaming. |
-
-**Total Scenarios**: 73 (All Passed)
-
-## Detailed Test Cases
-
-### 1. `EnhancedA2AProtocolAdaptorTest` (Unit)
-- **MCP Core**: Validated Request/Response/Notification mapping.
-- **Error Handling**: Validated JSON-RPC Error object mapping.
-- **Batching**: Validated JSON Array splitting.
-- **Legacy Removal**: Confirmed legacy A2A format is no longer processed.
-- **A2A Ops**: Verified `task/get`, `message/sendStream` mappings.
-
-### 2. `A2ATopicFactoryTest` (Unit)
-- Validated topic generation for request, response, and status topics.
-- Validated topic parsing (extracting namespace, agent name, task ID, topic type).
-- Verified wildcard topic patterns for gateway subscriptions.
-
-### 3. `TaskRegistryTest` (Unit)
-- **State Machine**: SUBMITTED → WORKING → COMPLETED/FAILED/CANCELLED transitions.
-- **Parent-Child**: Task hierarchy tracking and child task listing.
-- **TTL Cleanup**: Verified that terminal-state tasks are removed after TTL expires.
-- **Concurrency**: Thread-safe state transitions under concurrent access.
-
-### 4. `InMemoryA2AMessageTransportTest` (Unit)
-- Publish/subscribe message delivery.
-- Multiple subscribers on the same topic.
-- Unsubscribe behavior.
-- Wildcard topic matching.
-
-### 5. `A2AGatewayServiceTest` (Integration)
-- **Sync Task**: submitTask → publish → handleResponse → future.complete.
-- **Async Task**: submitTask returns immediately, status queried separately.
-- **Cancel**: cancelTask transitions state and completes future with CANCELLED.
-- **Race Condition**: Verified put-before-publish ordering prevents lost responses.
-- **Status Subscription**: StatusSubscriber receives state transition callbacks.
-
-### 6. `A2AGatewayEndToEndTest` (Integration)
-- Full flow: A2AClient → Gateway HTTP → GatewayService → Transport → Agent → Response → Client.
-- Verified task ID correlation across all components.
-- Multiple concurrent tasks.
-- Error scenarios (unknown agent, task not found).
-
-### 7. `A2AClientServerIntegrationTest` (HTTP Integration)
-- **Real HTTP**: Uses Apache HttpClient to hit the real Netty server.
-- **AgentCard**: Registration and heartbeat via REST API.
-- **Sync Task**: `POST /a2a/tasks?mode=sync` returns completed result.
-- **Async Task**: `POST /a2a/tasks?mode=async` returns taskId, then `GET /a2a/tasks/{taskId}` polls status.
-- **Cancel**: `DELETE /a2a/tasks/{taskId}` cancels the task.
-- **List Agents**: `GET /a2a/agents` returns registered agent list.
-- **Typed Returns**: `A2AClient.getTaskStatus()` returns `TaskResult`, `listAgents()` returns `List<String>`.
-- **SSE Stream**: `GET /a2a/tasks/{taskId}/stream` receives real-time state updates via `text/event-stream`.
-
-### 8. `McpIntegrationDemoTest` (Integration - RPC)
-- Simulated Client → EventMesh → Server flow.
-- Verified correlation ID linking (`req-id` <-> `collaborationid`).
-
-### 9. `McpPatternsIntegrationTest` (Integration - Advanced)
-- **Pub/Sub**: Verified `_topic` -> `subject` mapping for Broadcast.
-- **Streaming**: Verified `_seq` -> `seq` mapping for ordered chunks.
-
-### 10. `McpComprehensiveDemoTest` (Protocol: JSON-RPC)
-- **RPC**: Request/Response flow verification.
-- **Pub/Sub**: Broadcast to Topic routing verification.
-- **Streaming**: Sequence ID preservation verification.
-
-### 11. `CloudEventsComprehensiveDemoTest` (Protocol: Native CloudEvents)
-- **RPC**: Verified manual construction of `.req` / `.resp` CloudEvents works.
-- **Pub/Sub**: Verified manual setting of `subject` works.
-- **Streaming**: Verified manual setting of `seq` extension works.
-
-## Environment
-
-- **JDK**: Java 8 (Source/Target 1.8), Compatible with Java 21 Runtime
-- **Build System**: Gradle 7.x+
-- **Dependencies**: Jackson 2.18+, CloudEvents SDK 3.0+, Netty 4.1+, Apache HttpClient
-
-## Conclusion
-
-The A2A Protocol v2.0 implementation is stable, functionally complete, and ready for production deployment. It successfully supports:
-- **Hybrid Architecture** (MCP & CloudEvents) with all three interaction patterns (RPC, Pub/Sub, Streaming)
-- **Gateway Runtime** with full REST API, SSE streaming, task lifecycle management, TTL auto-cleanup, and typed Java SDK
-- **73 test scenarios** across protocol and runtime layers, all passing
diff --git a/docs/a2a-protocol/eventmesh-a2a-design.md b/docs/a2a-protocol/eventmesh-a2a-design.md
deleted file mode 100644
index a3b2135fe..000000000
--- a/docs/a2a-protocol/eventmesh-a2a-design.md
+++ /dev/null
@@ -1,478 +0,0 @@
----
-title: EventMesh A2A Protocol
-sidebar_position: 4
----
-
-# EventMesh A2A Protocol Architecture & Functional Specification
-
-## 1. Overview
-
-The **EventMesh A2A (Agent-to-Agent) Protocol** is a specialized, high-performance protocol plugin designed to enable asynchronous communication, collaboration, and task coordination between autonomous agents.
-
-With the release of v2.0, A2A adopts the **MCP (Model Context Protocol)** architecture, transforming EventMesh into a robust **Agent Collaboration Bus**. It bridges the gap between synchronous LLM-based tool calls (JSON-RPC 2.0) and asynchronous Event-Driven Architectures (EDA), enabling scalable, distributed, and decoupled agent systems.
-
-## 2. Core Philosophy
-
-The architecture adheres to the principles outlined in the broader agent community (e.g., A2A Project, FIPA-ACL, and CloudEvents):
-
-1. **JSON-RPC 2.0 as Lingua Franca**: Uses standard JSON-RPC for payload semantics, ensuring compatibility with modern LLM ecosystems (LangChain, AutoGen).
-2. **Transport Agnostic**: Encapsulates all messages within **CloudEvents**, allowing transport over any EventMesh-supported protocol (HTTP, TCP, gRPC, Kafka).
-3. **Async by Default**: Maps synchronous Request/Response patterns to asynchronous Event streams using correlation IDs.
-4. **Native Pub/Sub Semantics**: Supports O(1) broadcast complexity, temporal decoupling (Late Join), and backpressure isolation, solving the scalability limits of traditional P2P webhook callbacks.
-
-### 2.1 Native Pub/Sub Semantics
-
-Traditional A2A implementations often rely on HTTP Webhooks (`POST /inbox`) for asynchronous callbacks. While functional, this **Point-to-Point (P2P)** model suffers from significant scaling issues:
-
-* **Insufficient Fan-Out**: A publisher must send $N$ requests to reach $N$ subscribers, leading to $O(N)$ complexity.
-* **Temporal Coupling**: Consumers must be online at the exact moment of publication.
-* **Backpressure Propagation**: A slow subscriber can block the publisher.
-
-**EventMesh A2A** solves this by introducing **Native Pub/Sub** capabilities:
-
-```mermaid
-flowchart LR
- Publisher["Publisher Agent"] -->|1. Publish (Once)| Bus["EventMesh Bus"]
-
- subgraph FanoutLayer ["EventMesh Fanout Layer"]
- Queue["Topic Queue"]
- end
-
- Bus --> Queue
-
- Queue -->|"Push"| Sub1["Subscriber 1"]
- Queue -->|"Push"| Sub2["Subscriber 2"]
- Queue -->|"Push"| Sub3["Subscriber 3"]
-
- style Bus fill:#f9f,stroke:#333
- style FanoutLayer fill:#ccf,stroke:#333
-```
-
-### 2.1 Hybrid Protocol Support (JSON-RPC & CloudEvents)
-
-A2A Protocol introduces a unique **Hybrid Architecture** that bridges the gap between the AI ecosystem (which prefers simple JSON) and the Cloud Native ecosystem (which prefers structured CloudEvents).
-
-| Feature | JSON-RPC 2.0 Mode | Native CloudEvents Mode |
-| :--- | :--- | :--- |
-| **Primary Audience** | LLMs, Scripts (Python/JS), LangChain | EventMesh Apps, Knative, Java SDK |
-| **Philosophy** | **"Battery Included"** | **"Power User"** |
-| **Usage** | Send raw JSON (`{"method":...}`) | Send `CloudEvent` object |
-| **Complexity** | Low (No SDK required) | Medium (Requires CE SDK) |
-| **Mechanism** | Adaptor automatically wraps JSON in CE | Adaptor passes through the event |
-
-**Benefits:**
-* **Zero-Barrier Entry**: Developers can interact with the mesh using just `curl` or simple JSON libraries.
-* **Full Flexibility**: Advanced users retain full control over CloudEvent attributes (Source, Type, Extensions) for complex routing or tracing scenarios.
-
-## 3. Architecture Design
-
-### 3.1 System Context
-
-```mermaid
-graph TD
- Client["Client Agent / LLM"] -- "JSON-RPC Request" --> EM["EventMesh Runtime"]
- EM -- "CloudEvent (Request)" --> Server["Server Agent / Tool"]
- Server -- "CloudEvent (Response)" --> EM
- EM -- "JSON-RPC Response" --> Client
-
- subgraph Runtime ["EventMesh Runtime"]
- Plugin["A2A Protocol Plugin"]
- end
-
- style EM fill:#f9f,stroke:#333,stroke-width:4px
- style Plugin fill:#ccf,stroke:#333,stroke-width:2px
-```
-
-### 3.2 Component Design (`eventmesh-protocol-a2a`)
-
-The core protocol logic resides in the `eventmesh-protocol-plugin` module.
-
-* **`EnhancedA2AProtocolAdaptor`**: The central brain of the protocol.
- * **Intelligent Parsing**: Automatically detects message format (MCP vs. Raw CloudEvent).
- * **Protocol Delegation**: Delegates to `CloudEvents` or `HTTP` adaptors when necessary.
- * **Semantic Mapping**: Transforms JSON-RPC methods and IDs into CloudEvent attributes.
-* **`A2AProtocolConstants`**: Defines standard operations like `task/get`, `message/sendStream`.
-* **`JsonRpc*` Models**: Strictly typed POJOs for JSON-RPC 2.0 compliance.
-* **`AgentCard` / `AgentSkill` / `AgentInterface`**: Agent capability discovery models.
-* **`A2ATopicFactory`**: Topic naming and parsing utility (request/response/status topics).
-* **`A2AClient`**: Java SDK for agent developers — AgentCard registration, task submission (sync/async), task status query, heartbeat, and transport-based request handling. Returns typed `TaskResult` objects.
-* **`A2AMessageTransport`**: Transport-agnostic pub/sub interface (InMemory implementation for dev/testing).
-
-### 3.3 Gateway Runtime Architecture (`eventmesh-runtime`)
-
-The Gateway runtime provides a standalone HTTP server that bridges external clients to the A2A event bus.
-
-```mermaid
-graph TD
- Client["Client / A2AClient SDK"] -- "HTTP REST" --> Server["A2AGatewayServer<br/>(Netty HTTP)"]
- Server --> Handler["A2AGatewayHttpHandler"]
- Handler --> GwService["A2AGatewayService"]
- GwService --> Registry["TaskRegistry<br/>(state machine + TTL)"]
- GwService --> Transport["InMemoryA2AMessageTransport"]
- GwService --> PubSub["A2APublishSubscribeService<br/>(AgentCard discovery)"]
- Transport -- "publish/subscribe" --> Agent["Target Agent"]
- Agent -- "response event" --> Transport
- Transport --> GwService
-
- style Server fill:#f9f,stroke:#333,stroke-width:2px
- style Registry fill:#cfc,stroke:#333
- style Transport fill:#ccf,stroke:#333
-```
-
-#### Core Components
-
-| Component | Module | Responsibility |
-| :--- | :--- | :--- |
-| `A2AGatewayServer` | runtime | Standalone Netty HTTP server entry point. Pre-registers mock agents, wires all components. |
-| `A2AGatewayHttpHandler` | runtime | HTTP request router. Maps REST endpoints to service calls. Supports SSE streaming. |
-| `A2AGatewayService` | runtime | Core orchestration: task submission, response handling, status subscription, SSE push. |
-| `TaskRegistry` | runtime | In-memory task lifecycle state machine with TTL auto-cleanup. |
-| `A2APublishSubscribeService` | runtime | AgentCard registration, discovery, and heartbeat management. |
-| `InMemoryA2AMessageTransport` | runtime | In-memory pub/sub implementation (replaceable by EventMesh broker). |
-| `A2ACardHttpHandler` | runtime | AgentCard CRUD REST endpoints (`/a2a/cards/*`). |
-| `A2AClient` | protocol-a2a | Java SDK for agent developers (HTTP + transport). |
-
-#### Task Lifecycle State Machine
-
-```
-SUBMITTED → WORKING → COMPLETED
- ↘ FAILED
- ↘ CANCELLED
-```
-
-* **TaskRegistry TTL Cleanup**: Terminal-state tasks (COMPLETED/FAILED/CANCELLED) are automatically removed after a configurable TTL (default: 5 minutes). A daemon `ScheduledExecutorService` runs cleanup every 60 seconds, preventing memory leaks from accumulated historical tasks.
-* **Race Condition Prevention**: In `A2AGatewayService.submitTask()`, the pending future is registered (`pendingTasks.put()`) **before** `transport.publish()`. This ordering is critical because `InMemoryTransport` delivers messages synchronously — if publish happened first, `handleResponse()` could execute before `put()` and the future would never complete.
-
-#### REST API
-
-| Method | Path | Description |
-| :--- | :--- | :--- |
-| `POST` | `/a2a/tasks?mode=sync` | Submit task synchronously (wait for result, 30s timeout) |
-| `POST` | `/a2a/tasks?mode=async` | Submit task asynchronously (return taskId immediately) |
-| `GET` | `/a2a/tasks/{taskId}` | Get task status and result |
-| `DELETE` | `/a2a/tasks/{taskId}` | Cancel a task |
-| `GET` | `/a2a/tasks/{taskId}/wait` | Long-poll wait for task result (configurable timeout) |
-| `GET` | `/a2a/tasks/{taskId}/stream` | **SSE** stream of task status updates (`text/event-stream`) |
-| `GET` | `/a2a/agents` | List all registered agents |
-| `POST` | `/a2a/heartbeat` | Agent heartbeat (keeps AgentCard alive) |
-| `GET` | `/a2a/cards/list` | List all AgentCards |
-| `POST` | `/a2a/cards/card/{org}/{unit}/{agent}` | Register an AgentCard |
-
-#### SSE Streaming
-
-The `GET /a2a/tasks/{taskId}/stream` endpoint provides real-time task status updates via Server-Sent Events:
-
-1. Client opens an HTTP connection with `Accept: text/event-stream`.
-2. Server sends initial state immediately.
-3. As task transitions (WORKING → COMPLETED/FAILED/CANCELLED), server pushes `data:` events.
-4. On terminal state, server sends final event and closes the connection.
-
-The handler writes directly to the Netty channel (returns `null` to skip the default `writeAndFlush` path), using `DefaultHttpContent` chunks with `text/event-stream` content type.
-
-#### A2AClient SDK
-
-The `A2AClient` provides a typed Java API for agent developers:
-
-```java
-A2AClient client = A2AClient.builder()
- .gatewayUrl("http://localhost:10105")
- .namespace("global")
- .agentName("my-agent")
- .agentCard(card)
- .heartbeatInterval(30_000)
- .build();
-client.start();
-
-// Typed return: TaskResult instead of raw JSON
-TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
-String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
-TaskResult status = client.getTaskStatus(taskId);
-List<String> agents = client.listAgents(); // typed List<String>
-boolean ok = client.cancelTask(taskId);
-```
-
-`TaskResult` uses `@JsonAlias("result")` to handle the server's `result` field name while exposing a `data` property to callers.
-
-### 3.4 Asynchronous RPC Mapping ( The "Async Bridge" )
-
-To support MCP on an Event Bus, synchronous RPC concepts are mapped to asynchronous events:
-
-| Concept | MCP / JSON-RPC | CloudEvent Mapping |
-| :--- | :--- | :--- |
-| **Action** | `method` (e.g., `tools/call`) | **Type**: `org.apache.eventmesh.a2a.tools.call.req`<br>**Extension**: `a2amethod` |
-| **Correlation** | `id` (e.g., `req-123`) | **Extension**: `collaborationid` (on Response)<br>**ID**: Preserved on Request |
-| **Direction** | Implicit (Request vs Result) | **Extension**: `mcptype` (`request` or `response`) |
-| **P2P Routing** | `params._agentId` | **Extension**: `targetagent` |
-| **Pub/Sub Topic** | `params._topic` | **Subject**: The topic value (e.g. `market.btc`) |
-| **Streaming Seq** | `params._seq` | **Extension**: `seq` |
-
-## 4. Functional Specification
-
-### 4.1 Message Processing Flow
-
-1. **Ingestion**: The adaptor receives a `ProtocolTransportObject` (byte array/string).
-2. **Detection**: Checks for `jsonrpc: "2.0"`.
-3. **Transformation (MCP Mode)**:
- * **Request**: Parses `method`.
- * If `message/sendStream`, sets type suffix to `.stream` and extracts `_seq`.
- * If `_topic` present, sets `subject` (Pub/Sub).
- * If `_agentId` present, sets `targetagent` (P2P).
- * **Response**: Parses `result`/`error`. Sets `collaborationid` = `id`.
-4. **Batch Processing**: Splits JSON Array into a `List<CloudEvent>`.
-
-### 4.2 Key Features
-
-#### A. Intelligent Routing Support
-* **Mechanism**: Promotes `_agentId` or `_topic` from JSON body to CloudEvent attributes.
-* **Benefit**: Enables EventMesh Router to perform content-based routing (CBR) efficiently.
-
-#### B. Batching
-* **Benefit**: Significantly increases throughput for high-frequency interactions.
-
-#### C. Streaming Support
-* **Operation**: `message/sendStream`
-* **Mechanism**: Maps to `.stream` event type and preserves sequence order via `seq` extension attribute.
-
-#### D. SSE Task Streaming (Gateway)
-* **Endpoint**: `GET /a2a/tasks/{taskId}/stream`
-* **Mechanism**: Server-Sent Events (`text/event-stream`) pushes real-time task state transitions to the client.
-* **Flow**: Initial state → WORKING updates → terminal state (COMPLETED/FAILED/CANCELLED) → connection close.
-* **Implementation**: Handler writes `DefaultHttpContent` chunks directly to the Netty channel, bypassing the standard `FullHttpResponse` path.
-
-#### E. Task TTL Auto-Cleanup (Gateway)
-* **Problem**: Completed/failed tasks accumulate in `TaskRegistry` indefinitely, causing memory leaks.
-* **Solution**: A daemon `ScheduledExecutorService` (`a2a-task-ttl-cleanup` thread) runs every 60 seconds, removing terminal-state tasks older than the TTL (default: 5 minutes).
-* **Configuration**: `TaskRegistry(taskTtlMs, cleanupIntervalMs)` constructor allows custom tuning.
-
-#### F. AgentCard Discovery & Heartbeat (Gateway)
-* **Registration**: `POST /a2a/cards/card/{org}/{unit}/{agent}` registers an `AgentCard`.
-* **Heartbeat**: `POST /a2a/heartbeat` refreshes the agent's last-seen timestamp. Cards expire after 60 seconds without heartbeat.
-* **Discovery**: `GET /a2a/agents` returns all live agent cards.
-
-## 5. Usage Examples
-
-### 5.1 JSON-RPC 2.0 (MCP) Mode
-
-This mode is ideal for LLMs, scripts, and simple integrations where you want to send raw JSON without worrying about CloudEvent headers.
-
-#### 5.1.1 Sending a Tool Call (RPC Request)
-
-**Client Sends (Raw JSON):**
-```json
-{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "weather",
- "city": "Shanghai",
- "_agentId": "weather-agent"
- },
- "id": "req-101"
-}
-```
-
-**EventMesh Converts to:**
-* **Type**: `org.apache.eventmesh.a2a.tools.call.req`
-* **Extension (targetagent)**: `weather-agent`
-* **Extension (mcptype)**: `request`
-
-#### 5.1.2 Pub/Sub Broadcast (Notification)
-
-**Client Sends (Raw JSON):**
-```json
-{
- "jsonrpc": "2.0",
- "method": "notifications/alert",
- "params": {
- "message": "System Maintenance in 10 mins",
- "_topic": "system.alerts"
- }
-}
-```
-
-**EventMesh Converts to:**
-* **Type**: `org.apache.eventmesh.a2a.notifications.alert`
-* **Subject**: `system.alerts`
-* **Extension (mcptype)**: `notification`
-
-#### 5.1.3 Java SDK Example (MCP Mode)
-
-```java
-// See eventmesh-examples/src/main/java/org/apache/eventmesh/a2a/demo/mcp/McpCaller.java
-
-Map<String, Object> request = new HashMap<>();
-request.put("jsonrpc", "2.0");
-request.put("method", "tools/call");
-request.put("params", Map.of("name", "weather", "_agentId", "weather-agent"));
-request.put("id", UUID.randomUUID().toString());
-
-CloudEvent event = CloudEventBuilder.v1()
- .withType("org.apache.eventmesh.a2a.tools.call.req")
- .withData(JsonUtils.toJSONString(request).getBytes())
- .withExtension("protocol", "A2A") // Critical to trigger A2A adaptor
- .build();
-
-producer.publish(event);
-```
-
-### 5.2 Native CloudEvents Mode
-
-This mode provides full control over all CloudEvent attributes and is recommended for robust, typed applications using the EventMesh SDK.
-
-#### 5.2.1 Native RPC Request
-
-**Client Sends (CloudEvent):**
-```json
-{
- "specversion": "1.0",
- "type": "com.example.rpc.request",
- "source": "my-app",
- "id": "evt-123",
- "data": "...",
- "protocol": "A2A",
- "targetagent": "target-agent-001"
-}
-```
-
-**Java SDK Example:**
-```java
-// See eventmesh-examples/src/main/java/org/apache/eventmesh/a2a/demo/ce/CloudEventsCaller.java
-
-CloudEvent event = CloudEventBuilder.v1()
- .withId(UUID.randomUUID().toString())
- .withSource(URI.create("ce-client"))
- .withType("com.example.rpc.request")
- .withData("application/text", "RPC Payload".getBytes())
- .withExtension("protocol", "A2A")
- .withExtension("targetagent", "target-agent-001") // Explicit routing
- .build();
-
-producer.publish(event);
-```
-
-#### 5.2.2 Native Pub/Sub
-
-**Client Sends (CloudEvent):**
-```json
-{
- "specversion": "1.0",
- "type": "com.example.notification",
- "source": "my-app",
- "subject": "broadcast.topic",
- "protocol": "A2A"
-}
-```
-
-#### 5.2.3 Native Streaming
-
-**Client Sends (CloudEvent):**
-```json
-{
- "specversion": "1.0",
- "type": "com.example.stream",
- "source": "my-app",
- "subject": "stream-topic",
- "protocol": "A2A",
- "sessionid": "session-555",
- "seq": "1"
-}
-```
-
-### 5.3 Gateway REST API (HTTP)
-
-The A2A Gateway provides a REST API for external clients and non-Java agents.
-
-#### 5.3.1 Submit Task (Sync)
-
-```bash
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=sync' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Beijing"}'
-```
-
-Response:
-```json
-{
- "taskId": "task-a1b2c3d4",
- "state": "COMPLETED",
- "data": "The weather in Beijing is sunny, 25°C"
-}
-```
-
-#### 5.3.2 Submit Task (Async)
-
-```bash
-curl -X POST 'http://localhost:10105/a2a/tasks?mode=async' \
- -H 'Content-Type: application/json' \
- -d '{"targetAgent":"weather-agent","message":"Shanghai"}'
-```
-
-Response (HTTP 202):
-```json
-{
- "taskId": "task-e5f6g7h8",
- "status": "accepted",
- "message": "Task submitted. Use GET /a2a/tasks/task-e5f6g7h8 to check status."
-}
-```
-
-#### 5.3.3 SSE Stream
-
-```bash
-curl -N http://localhost:10105/a2a/tasks/task-a1b2c3d4/stream
-```
-
-Response (`text/event-stream`):
-```
-data: {"taskId":"task-a1b2c3d4","state":"SUBMITTED"}
-
-data: {"taskId":"task-a1b2c3d4","state":"WORKING","data":"processing..."}
-
-data: {"taskId":"task-a1b2c3d4","state":"completed","data":"The weather in Beijing is sunny, 25°C"}
-```
-
-#### 5.3.4 List Agents
-
-```bash
-curl http://localhost:10105/a2a/agents
-```
-
-### 5.4 A2AClient SDK (Java)
-
-```java
-A2AClient client = A2AClient.builder()
- .gatewayUrl("http://localhost:10105")
- .namespace("global")
- .agentName("my-agent")
- .agentCard(card)
- .heartbeatInterval(30_000)
- .build();
-
-client.start();
-
-// Synchronous task (returns typed TaskResult)
-TaskResult result = client.sendTaskSync("weather-agent", "Beijing", null);
-
-// Asynchronous task (returns taskId immediately)
-String taskId = client.sendTaskAsync("weather-agent", "Shanghai", null);
-
-// Poll status
-TaskResult status = client.getTaskStatus(taskId);
-
-// Cancel
-boolean cancelled = client.cancelTask(taskId);
-
-// List registered agents (typed List<String>)
-List<String> agents = client.listAgents();
-
-client.shutdown();
-```
-
-## 6. Future Roadmap
-
-* **EventMesh Broker Integration**: Replace `InMemoryA2AMessageTransport` with the real EventMesh broker for production deployment.
-* **Schema Registry**: Implement dynamic discovery of Agent capabilities via `methods/list`.
-* **Sidecar Injection**: Fully integrate the adaptor into the EventMesh Sidecar for non-Java agents (Python, Node.js).
-* **WebSocket Streaming**: Extend SSE to bidirectional WebSocket for real-time agent-to-agent dialogue.
-* **Task Persistence**: Persist `TaskRegistry` state to a durable store (Redis/DB) for crash recovery.
-* **Authentication**: Add API key / JWT authentication to the Gateway REST API.
diff --git a/docs/eventmesh-cloudevents-client-guide.md b/docs/eventmesh-cloudevents-client-guide.md
index 689c33e4f..cf0bfb488 100644
--- a/docs/eventmesh-cloudevents-client-guide.md
+++ b/docs/eventmesh-cloudevents-client-guide.md
@@ -271,4 +271,151 @@ EVENTMESH_STORAGE_TYPE=kafka bin/start.sh
- 4.x 存储插件:`eventmesh-storage-plugin/eventmesh-storage-rocketmq`(SPI key `rocketmq`)
- 5.x 存储插件:`eventmesh-storage-plugin/eventmesh-storage-rocketmq5`(SPI key `rocketmq5`,含 `LiteTopicCapable`)
- Kafka 存储插件:`eventmesh-storage-plugin/eventmesh-storage-kafka`(SPI key `kafka`,assign+seek+poll + SASL 透传)
-- 设计文档:`docs/eventmesh-uni-architecture-redesign.md`
+- 设计文档:`docs/eventmesh-uni-architecture-redesign.md`(§20–§21 流式调用设计)
+
+---
+
+# 流式调用使用指南(整合 sdk-streaming-call-guide)
+
+> 面向**应用开发者**:如何用 EventMesh 客户端 SDK 做 LLM 风格的**流式调用**(token 逐片返回、支持多轮上下文)。
+
+## 7. 流式调用:它是什么
+
+流式调用让客户端发起一次请求后,**持续接收一串文本分片(token / delta)**,直到收到结束标记——和 OpenAI Chat Completions 的 `stream: true`、SSE 打字机效果一致。
+
+EventMesh 提供两种流式模式:
+
+| 模式 | 用途 | 入口 | 角色 |
+|------|------|------|------|
+| **Mode 1 — 流式调用** | 客户端发起请求,Agent(接 LLM)逐 token 回复 | `openSession` → `call` → `forEach` | HTTP/SSE 客户端(无 MQ 依赖) |
+| **Mode 2 — 发布/订阅** | 生产者往 session 的 MQ 通道写 chunks,消费者通过 SSE 订阅读取 | `subscribeSession` / `openSessionPublisher` | 生产者 + 消费者(解耦、跨进程、跨时间) |
+
+**前置条件**:一个已运行的 EventMesh Runtime(默认 `http://localhost:8080`),其上注册了至少一个流式 Agent(Mode 1)或启用了 Mode 2 的发布/订阅。
+
+---
+
+## 8. Mode 1:流式调用(30 秒快速开始)
+
+```java
+import org.apache.eventmesh.client.cloudevents.CloudEventsClient;
+import org.apache.eventmesh.client.cloudevents.stream.*;
+
+CloudEventsClient client = CloudEventsClient.builder()
+ .runtimeUrl("http://localhost:8080")
+ .clientId("my-app")
+ .build();
+
+// 开一个 session,发一轮,收 token,关 session
+StreamingSession session = client.streaming()
+ .openSession(OpenSession.builder().clientId(client.clientId()).build());
+try {
+ try (StreamingResponse r = session.call("用三句话介绍 EventMesh")) {
+ r.forEach(chunk -> System.out.print(chunk.getChunk())).join();
+ }
+} finally {
+ session.close();
+}
+```
+
+`forEach` 每收到一个分片就回调一次,`.join()` 阻塞到流结束。
+
+---
+
+## 9. Mode 1:多轮会话(Session)
+
+```java
+StreamingSession session = client.streaming()
+ .openSession(OpenSession.builder().clientId("my-app").build());
+
+try {
+ System.out.println("sessionId = " + session.sessionId());
+
+ // 第一轮
+ try (StreamingResponse r1 = session.call("我叫张三,是一名 Java 工程师")) {
+ r1.forEach(chunk -> System.out.print(chunk.getChunk())).join();
+ }
+
+ // 第二轮(同一 session,Agent 记得上一轮)
+ try (StreamingResponse r2 = session.call("我叫什么名字?做什么工作?")) {
+ r2.forEach(chunk -> System.out.print(chunk.getChunk())).join();
+ }
+} finally {
+ session.close(); // 最终销毁会话(幂等)
+}
+```
+
+**三条极易踩的语义**:
+1. 关闭某一轮的 `StreamingResponse` 只结束这一轮的读取,**不会关闭 session**。
+2. 多轮上下文由 Agent 端的 `ConversationStore` 按 `sessionId` 维护。
+3. `OpenSession` 有 `clientId`(必填)、`model`(可选);`StreamRequest` 有 `prompt`(必填)、`model`(可选)、`timeout`(可选)。
+
+---
+
+## 10. Mode 2:发布/订阅
+
+Mode 2 把 session 的流式数据**外化到 MQ**(LiteTopic),实现**跨进程/跨时间**的流式消费。
+
+### 10.1 订阅(消费)
+
+```java
+StreamingResponse sub = client.subscribeSession("my-session-id");
+sub.forEach(chunk -> {
+ System.out.println("[" + chunk.getSeq() + "] " + chunk.getChunk());
+}).join();
+sub.close();
+```
+
+### 10.2 发布(生产)
+
+```java
+SessionPublisher pub = client.openSessionPublisher("my-session-id");
+pub.publish("Hello", false); // 非终止帧
+pub.publish(" world", false);
+pub.publish("", true); // 终止帧 → 订阅者的 forEach 完成
+pub.close();
+```
+
+---
+
+## 11. Mode 1:实现一个流式 Agent
+
+Agent 收到请求后契约四步:
+1. 解析 `sessionId`、`prompt`、`replyTo`
+2. 每产生一个 token → 发非终止帧 `{chunk: token, done: false}`
+3. 流正常结束 → 发终止帧 `{chunk: "", done: true}`
+4. 出异常 → 发终止错误帧 `{chunk: "", done: true, error: "..."}`
+
+参考实现 `eventmesh-agent/.../StreamingAgent.java` 接 OpenAI 兼容 LLM:
+
+```java
+StreamingAgent agent = new StreamingAgent(agentClient, agentParent, agentId, llm, conversations);
+agent.start(); // 订阅 agent 控制信道
+control.ready(agentId); // 向 runtime 报告就绪
+```
+
+---
+
+## 12. 部署配置
+
+### Mode 1(流式调用)
+
+预创建 agent + client parent topics,配 `AgentAnchoredStrategy`,4 参 SessionRouter。
+
+### Mode 2(发布/订阅,可选)
+
+额外预创建 `sessionStreamParent`,用 6 参 SessionRouter(`sessionTtlMs` + `sessionStreamParent`)。
+
+两种模式可同时启用(入口不同,互不干扰)。
+
+---
+
+## 13. 流式调用常见问题
+
+**Q: Mode 1 和 Mode 2 怎么选?**
+需要 Agent 承载 LLM、多轮对话上下文 → Mode 1。需要跨进程/跨时间消费流式数据、崩溃恢复 → Mode 2。
+
+**Q: 报 `IllegalStateException: posture already active`?**
+同一个 `StreamingResponse` 只能用一次 `forEach`。
+
+**Q: 如何排错"收不到响应"?**
+① Runtime 是否起、Agent 是否 `ready` + 心跳;② `/session/open` 是否返回 200 且有 `agentId`;③ parent 是否预创建为 LiteTopic;④ broker 是否开了 LiteTopic。
diff --git a/docs/eventmesh-offset-lb-frame-design.md b/docs/eventmesh-offset-lb-frame-design.md
new file mode 100644
index 000000000..7441d9192
--- /dev/null
+++ b/docs/eventmesh-offset-lb-frame-design.md
@@ -0,0 +1,388 @@
+# EventMesh Offset 管理 · 负载均衡 · Frame 协议转换 设计总结
+
+> 整合 offset 管理、全面粘性负载均衡、EventMeshFrame 协议转换三块设计的完整方案(2026-08-13)。
+> 关联文档:[`eventmesh-uni-architecture-redesign.md`](./eventmesh-uni-architecture-redesign.md) §19 架构深化。
+
+---
+
+## 目录
+
+- [一、Offset 管理](#一offset-管理)
+- [二、负载均衡](#二负载均衡)
+- [三、Frame 协议转换](#三frame-协议转换)
+
+---
+
+## 一、Offset 管理
+
+### 1.1 架构定位
+
+**EventMesh 完全自管 offset,不给 group.id,不上报 meta。** MQ(RocketMQ/Kafka)仅作持久 FIFO 日志,不使用其 Consumer Group 语义。
+
+### 1.2 两层 offset
+
+| 层 | 含义 | 存储 | 接管行为 |
+|---|---|---|---|
+| **pull offset**(从 MQ 拉到哪) | `parent#lite@queue`(lite)/ `topic#partition`(普通) | storage 插件本地(rocketmq5 = properties 文件,5s 原子写 checkpoint) | 实例挂 → 新实例从 head 重拉 |
+| **deliver/ack offset**(投递+ACK 进度) | `topic#clientId#partition` | `RocksDBOffsetStore`(本地) | 实例挂 → 新实例 readOffset=-1 → 从 head 重拉 → in-flight 重投 |
+
+两层**不强行合并**(语义不同、解耦):pull 层在 storage 插件内部(自管游标),deliver/ack 层在 runtime OffsetStore。`pullAndDispatchPartition` 传 `startOffset=-1` 给 `storage.poll`(插件自管),OffsetStore 不在接管路径上。
+
+### 1.3 核心机制
+
+**正常路径**:
+
+```
+storage.poll()
+ → pull offset 自管(不向 broker 提交 commitOffset)
+ → pullAndDispatchPartition(传 startOffset=-1,插件自管游标)
+ → ReliableDispatcher.deliver()
+ → Delivery(携带 event + mqAckCallback)
+ → PushChannel.deliver() → 投递给客户端
+ → 客户端 ACK → ReliableDispatcher.ack()
+ → offsetStore.writeOffset()(CAS max,只进不退)
+ → storage.ackPulledMessage(topic, popCk)(RocketMQ 5.x:此时才 ACK broker)
+```
+
+**接管(实例挂)**:
+
+```
+实例挂
+ → client 经 /session/recommend 重连新实例(§二负载均衡)
+ → 新实例从业务 topic 重拉
+ · lite:pollLite 从 lite 头重放(单游标天然可重放)
+ · 普通:poll 从最早未 ACK/head 起
+ → in-flight 未 ACK 消息 → at-least-once 重投(业务幂等兜底)
+ → 不写快照、不迁移 offset 状态、不上报 meta
+```
+
+### 1.4 三后端差异
+
+| 后端 | pull 机制 | ACK 机制 | 重启恢复 |
+|---|---|---|---|
+| **RocketMQ 5.x** | POP_MESSAGE(broker 管分配,partitionCount=-1 → poll-all) | **延迟 ACK**:poll() 不立即 ACK broker;`ackPulledMessage()` 在客户端 ACK 后触发 broker ACK。崩溃不丢(broker invisibleTime 30s 过期自动重投)。 | pull offset 本地 properties 文件续传(5s checkpoint) |
+| **RocketMQ 4.x** | PULL_MESSAGE(自管游标,固定 CONSUMER_GROUP,不提交 commitOffset) | 无 MQ ACK(PULL 模式) | pull offset 本地 properties 文件续传 |
+| **Kafka** | assign+seek(无 group.id,ENABLE_AUTO_COMMIT=false) | 无 MQ ACK | pull offset 本地 properties 文件续传 |
+
+### 1.5 延迟 ACK 详解(RocketMQ 5.x,P2 修复)
+
+**问题**:原来 `poll()` 内立即 `ackNormal(brokerAddr, msg)` → broker 认为已消费 → EventMesh 崩溃丢消息(At-Most-Once)。
+
+**修复链路**:
+
+```
+① poll()
+ → POP_MESSAGE 拉取消息
+ → 不立即 ACK broker
+ → 存 deferred ACK callback(key = PROPERTY_POP_CK)
+ → EventMeshFrame 属性 stamp "empopck" = POP check key
+
+② pullAndDispatchPartition()
+ → 读取 frame.attributes().get("empopck")
+ → 构建 mqAck = () → storage.ackPulledMessage(topic, popCk)
+ → dispatcher.deliver(topic, partition, offset, frame, clientId, channel, mqAck)
+ → Delivery 存储 mqAckCallback
+
+③ 客户端 ACK → ReliableDispatcher.ack(deliveryId)
+ → offsetStore.writeOffset()(offset 推进)
+ → delivery.getMqAckCallback().run() → storage.ackPulledMessage(topic, popCk)
+ → pendingPopAcks.remove(popCk) → 执行 broker ACK_MESSAGE
+ → broker 确认消费完成
+```
+
+**at-least-once 保证**:如果 EventMesh 在 ① 和 ③ 之间崩溃,broker 的 invisibleTime(30s)过期 → 自动重投 → 新实例重新拉取处理。
+
+### 1.6 OffsetStore 单调写入(P4 修复)
+
+**问题**:`writeOffset` 直接覆盖(`set(offset)`),重启后快消费组的 offset 被慢消费组的重放消息拉低。
+
+**修复**:
+- `InMemoryOffsetStore`:`AtomicLong.accumulateAndGet(offset, Math::max)` —— CAS max
+- `RocksDBOffsetStore`:read current → if `offset <= current` skip → else put —— 条件写
+
+### 1.7 已淘汰
+
+- **MetaBackedOffsetStore**(百万 key 1s 上报 meta)→ 默认关(`-Deventmesh.offset.meta=true` 显式开),类保留备用。
+- 不给 group.id(EventMesh 自主 offset 铁律不变)。
+
+### 1.8 待办
+
+- **P1:RocketMQ 4.x ConsumeQueue 清理** —— `commitOffset` 仍是 no-op;需实际验证 RocketMQ 4.x 的 ConsumeQueue 清理是否真依赖消费位点(CommitLog 基于时间清理 `fileReservedTime`,可能不依赖)。
+
+---
+
+## 二、负载均衡
+
+### 2.1 架构定位
+
+> **均衡做在 session 分配层(入口 recommend),不在拉取/分发层。**
+
+实例只为自己代理的 client 被动按需拉取,不对等、不转发。先均衡"哪个 client 归哪个实例"(session 层),自然导致"各实例拉取量大致相当"——而不是"先对等拉取再均衡分发"。
+
+### 2.2 全面粘性模型
+
+```
+旧模型(已停用): 新模型(全面粘性):
+ PartitionOwnership(partition%n 分配) × 无分区归属
+ ClusterCoordinator(跨实例转发) × 不转发
+ HttpForwarder(HTTP 转发到 peer) × 不转发
+ ClusterSubscriptionStore(集群订阅表) × 本地订阅
+
+enableCluster 只保留:
+ ClusterMembership(心跳 + 负载指标) 供 /session/recommend 全局评分
+```
+
+转发类(PartitionOwnership / ClusterCoordinator / HttpForwarder)保留不删(备用 + 测试)。
+
+### 2.3 客户端零负担——负载全自采
+
+EventMesh 实例本地自采负载指标(`LoadMeter`),客户端不发任何上报。
+
+| 指标 | 采集来源 |
+|---|---|
+| `activeSessions` | `SessionRouter` sinks / subscribeSinks size |
+| `inflowBytes/s` | `UniIngressService` publish 字节累加(按 clientId 分桶 → 每 client 流量画像) |
+| `outflowBytes/s` | SSE / poll 出口字节累加 |
+| `cpuLoad` | `OperatingSystemMXBean` |
+
+随现有 5s 心跳(`PartitionOwnership` 线程)写入 `/em/instances/<id>` = `<ts>|<addr>|<activeSessions>|<byteRate>|<cpuLoad>`。
+
+### 2.4 均衡闭环
+
+```
+① client 首次连接任意实例
+ │
+② GET /session/recommend?clientId=xxx
+ │ → 该实例读集群全局 /em/instances/(全部实例 + 负载)
+ │ → 评分:score = activeSessions×w1 + byteRate×w2 + cpuLoad×w3
+ │ → 过载负反馈:cpuLoad>0.8 或 inflow>5MB/s → score += 10000(让出)
+ │ → 大 client 分散:检查 client 现有 session 分布,优先未占满实例
+ │
+③ 返回推荐 instanceUrl
+ │
+④ POST /session/open → {sessionId, agentId, instanceUrl}
+ │ → SDK pin 后续 turn/close 到该实例
+ │
+⑤ POST /events/subscribe → {subscriptionId, instanceUrl}
+ │ → SDK pin 后续 poll/ack 到该实例
+ │
+⑥ 后续所有请求直连该实例
+ │ → 实例只为自己 client 拉,不对等,不转发
+ │
+⑦ 失败/实例不可达 → SDK 重新 GET /session/recommend 拿另一实例
+```
+
+### 2.5 session 粒度粘性
+
+- `/session/open` 和 `/events/subscribe` 返回 `instanceUrl`。
+- SDK `SessionHandle.instanceUrl` → `StreamingSession` 用 `client.withBaseUrl(instanceUrl)` pin 后续 turn/close。
+- SDK `subscribe` → `capturePollInstance` 设 `pollBaseUrl`,后续 poll+ack 走该实例。
+- **`advertisedAddr` 默认空**(单实例/测试/LB 兼容);显式配 `-Deventmesh.http.advertisedAddr=host:port` 才 pin。
+
+### 2.6 代价
+
+- 无转发 = 订阅者必须粘性(否则 poll 落别的实例收不到)——由 subscribe→instanceUrl 保证。
+- 同 topic 多订阅者散在多实例 → 每实例各自拉全分区(MQ 读放大,N 实例 = N 份读)——换掉转发跳 + 全局协调复杂度。
+- 单 session 流量超单实例上限(不可拆)→ 实例限流/拒绝——无解边界。
+
+---
+
+## 三、Frame 协议转换
+
+### 3.1 架构分层
+
+```
+┌─ 对外协议层(FrameAdaptor SPI)──────────────────────────────────────┐
+│ │
+│ CloudEvents(HTTP/SSE/WS) → CloudEventsFrameAdaptor → EventMeshFrame │
+│ MeshMessage(legacy TCP) → MeshMessageFrameAdaptor → EventMeshFrame │
+│ A2A(JSON-RPC 2.0) → A2AFrameAdaptor → EventMeshFrame │
+│ 未来新协议 → 新 FrameAdaptor → EventMeshFrame │
+│ │
+│ 三种协议平级,各自直接转 Frame,互不经过 CloudEvent │
+└───────────────────────────────┬─────────────────────────────────────┘
+ │ 对外协议直接转 Frame
+┌───────────────────────────────▼─────────────────────────────────────┐
+│ 内部(runtime + storage):全程 EventMeshFrame │
+│ │
+│ ingress publish → Frame → storage.send(Frame) → MQ 字节 │
+│ MQ 字节 → storage.poll()→Frame → dispatch/filter/TTL → Frame │
+└───────────────────────────────┬─────────────────────────────────────┘
+ │ egress:Frame → 对应 FrameAdaptor
+┌───────────────────────────────▼─────────────────────────────────────┐
+│ egress(FrameAdaptor SPI,按客户端连接协议) │
+│ │
+│ Frame → CloudEvents-JSON(SSE / WS / HTTP poll) │
+│ Frame → MeshMessage Package(legacy TCP) │
+│ Frame → A2A JSON-RPC bytes(A2A 回调) │
+└───────────────────────────────────────────────────────────────────────┘
+```
+
+**CloudEvent 不是内部表示**——它是 CloudEvents 客户端的对外入口格式之一,经 `CloudEventsFrameAdaptor` 转 Frame 后进入内部。MeshMessage 和 A2A 同理,各自有独立 adaptor,互不经过 CloudEvent。
+
+### 3.2 EventMeshFrame wire 格式
+
+```
+定长头 14B:
+ [magic:1=0xEF][ver:1=1][msgType:1][flags:1][seq:4][keyCount:2][dataLen:4]
+
+KV 属性段(keyCount ×):
+ [nameLen:2][name:UTF-8][valLen:4][value:UTF-8]
+
+data:
+ raw bytes(streaming 的 chunk/prompt / 事件的业务 data)
+
+msgType = STREAM_REQ(1) | STREAM_CHUNK(2) | EVENT(3)
+flags = bit0 done | bit1 hasError | bit2 hasMeta(streaming 用)
+```
+
+### 3.3 三种 msgType 的字段映射
+
+| msgType | 定长头字段 | KV 属性 | data |
+|---|---|---|---|
+| **STREAM_REQ** | — | `sid`=sessionId, `replyTo`=回复地址, `model`?, `conv`? | prompt 文本 |
+| **STREAM_CHUNK** | `seq`=流内序号, `flags.done`=终止标记 | `sid`, `etype`?, `err`?, `meta`?(JSON) | chunk 文本 |
+| **EVENT** | — | `id`/`type`/`source`/`subject`/`time`/`emttl`/`emcorrelationid`/`empopck`/用户扩展 | 事件 payload |
+
+### 3.4 FrameAdaptor SPI
+
+对外协议 ↔ EventMeshFrame 的双向转换由 `FrameAdaptor` SPI 定义(`eventmesh-protocol-api/.../FrameAdaptor.java`):
+
+```java
+@EventMeshSPI(eventMeshExtensionType = EventMeshExtensionType.PROTOCOL)
+public interface FrameAdaptor {
+ EventMeshFrame toFrame(ProtocolTransportObject proto); // ingress
+ ProtocolTransportObject fromFrame(EventMeshFrame frame); // egress
+ String getProtocolType();
+}
+```
+
+runtime 不直接调 `EventMeshFrame.fromCloudEvent()` / `.toCloudEvent()` / `MeshMessageFrameCodec`——全部经 `FrameAdaptors.get(协议名)` 加载对应 adaptor。**加新协议只需实现 `FrameAdaptor` + 注册 SPI,不改 runtime 代码。**
+
+### 3.5 协议插件模块
+
+| 模块 | FrameAdaptor | 对外协议 | 转换方式 |
+|---|---|---|---|
+| **protocol-api** | —(只 SPI 接口 + `FrameAdaptors` 加载器) | — | 零实现 |
+| **protocol-cloudevents** | `CloudEventsFrameAdaptor` | CloudEvents-JSON(HTTP/SSE/WS) | CE-JSON bytes ↔ Frame(经 CE 对象做字段映射) |
+| **protocol-meshmessage** | `MeshMessageFrameAdaptor` | MeshMessage Package(TCP) | Package ↔ Frame(直接字段映射,零 CE 中转) |
+| **protocol-a2a** | `A2AFrameAdaptor` | A2A JSON-RPC 2.0 | JSON-RPC bytes ↔ Frame(直接字段映射,零 CE 中转) |
+| ~~protocol-grpc~~ | — | — | **已删除**(空壳无源码) |
+
+### 3.6 WireCodec SPI(内部 MQ wire 编码)
+
+内部 MQ wire 的字节编解码由 `WireCodec` SPI 定义(`eventmesh-common/.../wire/`):
+
+```java
+public interface WireCodec {
+ byte[] encode(StreamRequest request);
+ StreamRequest decodeRequest(byte[] bytes);
+ byte[] encode(StreamChunk chunk);
+ StreamChunk decodeChunk(byte[] bytes);
+ byte[] encode(CloudEvent event);
+ CloudEvent decodeEvent(byte[] bytes);
+}
+```
+
+- 默认实现 `EventMeshFrameCodec`(EventMeshFrame ↔ byte[])。
+- 可通过 `-Deventmesh.wire.codec=<fqcn>` 替换。
+
+### 3.7 各协议的完整转换链路
+
+#### CloudEvents(HTTP/SSE/WS 客户端)
+
+**Ingress**(客户端 → runtime):
+```
+SDK 发 CloudEvents-JSON bytes
+ → UniHttpServer.publish: body = CE-JSON bytes
+ → UniIngressService.publish(topic, CloudEvent): CE → EventMeshFrame.fromCloudEvent(event)
+ → storage.send(topic, frame): frame.encode() → MQ 字节
+```
+
+**Egress**(runtime → 客户端):
+```
+storage.poll() → frame (EventMeshFrame)
+ → SseConnection.send / WsConnection.send / UniHttpServer.poll
+ → FrameAdaptors.toCloudEventsJson(frame): frame.toCloudEvent() → CE-JSON serialize
+ → 写 SSE data: / WS TextFrame / HTTP JSON
+```
+
+#### MeshMessage(legacy TCP 客户端)
+
+**Ingress**(客户端 → runtime):
+```
+旧 TCP SDK 发 Package(EventMeshMessage)
+ → MeshMessagePackageRouter.route(pkg)
+ → FrameAdaptors.get("meshmessage").toFrameSilent(pkg)
+ → MeshMessageFrameAdaptor.toFrame:
+ topic → attributes["subject"]
+ body → data
+ header.seq → attributes["id"]
+ properties → attributes KV
+ → TcpRequest.publish(topic, frame)
+ → UniIngressService.publish(topic, frame)(Frame 重载,跳过 CE 转换)
+ → storage.send(topic, frame)
+```
+
+**Egress**(runtime → 客户端):
+```
+ReliableDispatcher.deliver → NettyTcpPushChannel.deliver(deliveryId, frame, callback)
+ → FrameAdaptors.get("meshmessage").fromFrameSilent(frame)
+ → MeshMessageFrameAdaptor.fromFrame:
+ attributes["subject"] → topic
+ data → body
+ attributes KV → properties
+ → Package(header=ASYNC_MESSAGE_TO_CLIENT, body=EventMeshMessage)
+ → channel.writeAndFlush(pkg)
+```
+
+#### A2A(JSON-RPC 客户端)
+
+**Ingress**:
+```
+A2A 客户端发 JSON-RPC 2.0 bytes
+ → FrameAdaptors.get("a2a").toFrameSilent(new ByteTransport(jsonBytes))
+ → A2AFrameAdaptor.toFrame:
+ parse JSON-RPC → extract method/params/_topic
+ method → attributes["ema2amethod"]
+ params._topic → attributes["subject"]
+ raw JSON → data
+ → EventMeshFrame
+```
+
+**Egress**:
+```
+EventMeshA2ATransport 收到 frame
+ → frame.data() = 原始 A2A JSON-RPC bytes(ingress 时原样保存)
+ → ByteTransport(jsonBytes) → A2A 客户端
+```
+
+### 3.8 Storage SPI 的 Frame 转换
+
+storage 插件(`MeshStoragePlugin` + `LiteTopicCapable`)的 send/poll 直接携带 EventMeshFrame:
+
+```
+send(topic, EventMeshFrame frame):
+ → rocketmq5: frame.encode() → MQ message body bytes
+ → kafka: frame.encode() → ProducerRecord value
+ → rocketmq4: frame.encode() → MQ message body bytes
+
+poll(topic, partition, ...):
+ → MQ bytes → EventMeshFrame.decode(bytes)
+ → 带 legacy CE-JSON fallback(旧消息 → EventMeshFrame.fromCloudEvent(deserialize))
+```
+
+### 3.9 落地范围
+
+| 层 | 改动 | 状态 |
+|---|---|---|
+| storage SPI | `MeshStoragePlugin.send/poll` + `LiteTopicCapable.sendLite/pullLite` 全改 EventMeshFrame | ✅ |
+| runtime dispatch | Delivery/BufferedEvent/PushChannel/Connection/DeadLetterSink/ReliableDispatcher/PushService + CloudEventFilter/SubscriptionManager 全翻 Frame | ✅ |
+| streaming | Mode-1(runtime↔agent 跨进程)+ Mode-2(runtime 内部 pub/sub)全用 EventMeshFrame | ✅ |
+| legacy 连接器 | Producer/Consumer SPI 不动(独立子系统,说 CE,边界转换) | ✅ 保留 |
+| WireCodec SPI | 默认 EventMeshFrameCodec,可替换 | ✅ |
+| FrameAdaptor SPI | CloudEvents 独立插件 + MeshMessage + A2A 各有独立 adaptor | ✅ |
+
+---
+
+**文档版本**: v1.0 · **整理时间**: 2026-08-13 · 关联: [`eventmesh-uni-architecture-redesign.md`](./eventmesh-uni-architecture-redesign.md) §19
diff --git a/docs/eventmesh-uni-architecture-redesign.md b/docs/eventmesh-uni-architecture-redesign.md
index 62c5eb42f..02bbae08b 100644
--- a/docs/eventmesh-uni-architecture-redesign.md
+++ b/docs/eventmesh-uni-architecture-redesign.md
@@ -1,8 +1,14 @@
# EventMesh 架构简化重构方案
-> 分支:`refactor/unified-runtime-pipeline` → 基于本次讨论的全新方向
+> 分支:`refactor/unified-runtime-pipeline` → 基于 本次讨论的全新方向
>
-> 核心变化:**丢弃 MQ 的 Producer Group / Consumer Group 语义,MQ 仅作存储层,EventMesh 自主维护订阅分发语义(负载均衡 / 多播 / 广播)。SDK 简化为 HTTP-only,全部基于 CloudEvents。**
+> 核心变化:**丢弃 MQ 的 Producer Group / Consumer Group 语义,MQ 仅作存储层,EventMesh 自主维护订阅分发语义。内部全程 `EventMeshFrame`(CloudEvent 退化为对外入口格式之一);对外多协议(CloudEvents / MeshMessage / A2A)经 `FrameAdaptor` SPI 直接转 Frame,互不转换。SDK 简化为 HTTP-only。**
+>
+> **v2.0 架构深化(2026-08-13 整合 `eventmesh-architecture-refinement.md`)**:
+> - 内部全程 `EventMeshFrame`(storage SPI + dispatch 管线 + 载体全翻 Frame)。
+> - 对外协议转换收进 `FrameAdaptor` SPI(CloudEvents/MeshMessage/A2A 各独立插件,零 CE 互转)。
+> - Offset 路②(纯本地、不上报 meta、不给 group.id、接管靠 MQ 重放)。
+> - 全面粘性负载均衡(session 分配层 recommend、实例自采负载、不转发)。
---
@@ -28,10 +34,10 @@
| **EventMesh 自主订阅** | 订阅分发逻辑(谁收哪条消息、按什么策略分发)全部由 EventMesh 自己维护,不委托给 MQ |
| **EventMesh 自主 Offset** | EventMesh 自主管理每个订阅关系的消费位点(topic#clientId#partition → offset),参考 RocketMQ Client OffsetStore 实现,使用 RocksDB 持久化,不依赖 MQ Consumer Group offset |
| **SDK 极简** | 只有一个 SDK(HTTP),只暴露两个对象(CloudEvent + Subscription),只有三个 API(publish / subscribe / unsubscribe) |
-| **全链路 CloudEvents** | 从 SDK → EventMesh Runtime → MQ 存储 → 下发客户端,全部使用 CloudEvents 1.0 规范,不再有自定义的 EventMeshMessage / Package 等中间格式 |
+| **内部 EventMeshFrame** | 内部全程 `EventMeshFrame`(定长头+KV属性+raw data),CloudEvent 退化为对外入口格式之一;对外多协议(CloudEvents/MeshMessage/A2A)经 `FrameAdaptor` SPI 直接转 Frame,互不转换 |
| **Connector 独立部署** | Connector Runtime 是独立进程,与 EventMesh Runtime 通过 HTTP + CloudEvents 接口通信,不共享内部组件,各自有独立的生命周期和 OffsetStore |
-> **🔎 实现状态速览(v1.11 / 2026-07-06 盘点)**:五条铁律中,"全链路 CloudEvents / Connector 独立部署 / SDK 极简(HTTP-only)"已落实;"EventMesh 自主订阅 / 自主 Offset"单实例成立。**但"MQ 无语义"在 Kafka 实现侧有矛盾**——`KafkaMeshStoragePlugin` 仍设 `group.id`(附录 F G7),多实例下会触发 Kafka rebalance,须修复。各铁律的逐 Phase 落实状态见 §11 各 Phase 标题处标注与附录 F。
+> **🔎 实现状态速览(v2.0 / 2026-08-13 盘点)**:五条铁律中,"内部 EventMeshFrame / Connector 独立部署 / SDK 极简(HTTP-only)"已落实;"EventMesh 自主订阅 / 自主 Offset"单实例成立(offset 路②纯本地 + MQ 重放接管,meta 上报默认关)。多实例协调改为**全面粘性**(session 分配层 recommend,停用转发层)。对外协议经 `FrameAdaptor` SPI(CloudEvents 独立插件 + MeshMessage + A2A 各自直接转 Frame,零 CE 互转)。详见 §19 架构深化。
### 1.2 与当前设计的本质区别
@@ -4565,7 +4571,389 @@ CompletableFuture<CloudEvent> request(CloudEvent event, Duration timeout);
---
-*文档版本:v1.10 | EventMesh 简化重构方案 | 基于 unified-runtime-design.md v2.1 演进 | 2026-07-03*
+## 十九、架构深化:内部 EventMeshFrame + FrameAdaptor SPI + 全面粘性(v2.0,2026-08-13)
+
+> 本节整合自 `eventmesh-architecture-refinement.md` v2.0。三块深化落地全绿(全 unit + 5.x E2E,含 RocketMQ5BrokerIntegrationTest 普通 pub/sub 2/2 + LegacyTcpClientIntegrationTest 旧 TCP SDK)。
+
+### 19.1 内部全程 EventMeshFrame
+
+#### 架构分层
+
+```
+┌─ 对外协议层(FrameAdaptor SPI)──────────────────────────────────────┐
+│ CloudEvents(HTTP/SSE/WS) → CloudEventsFrameAdaptor → EventMeshFrame │
+│ MeshMessage(legacy TCP) → MeshMessageFrameAdaptor → EventMeshFrame │
+│ A2A(JSON-RPC 2.0) → A2AFrameAdaptor → EventMeshFrame │
+│ 未来新协议 → 新 FrameAdaptor → EventMeshFrame │
+└───────────────────────────────┬───────────────────────────────────────┘
+ │ 对外协议直接转 Frame,不经 CloudEvent 互转
+┌───────────────────────────────▼───────────────────────────────────────┐
+│ 内部(runtime + storage):全程 EventMeshFrame │
+│ ingress publish → Frame → storage.send(Frame) → MQ 字节 │
+│ MQ 字节 → storage.poll()→Frame → dispatch/filter/TTL → Frame → egress │
+└───────────────────────────────┬───────────────────────────────────────┘
+ │ egress: Frame → 对应 FrameAdaptor → 客户端协议
+┌───────────────────────────────▼───────────────────────────────────────┐
+│ egress(FrameAdaptor SPI,按客户端连接协议) │
+│ Frame → CloudEvents-JSON(SSE/WS/HTTP poll) │
+│ Frame → MeshMessage Package(legacy TCP) │
+│ Frame → A2A JSON-RPC bytes(A2A 回调) │
+└────────────────────────────────────────────────────────────────────────┘
+```
+
+**CloudEvent 不是内部表示**——它是 CloudEvents 客户端的对外入口格式,经 `CloudEventsFrameAdaptor` 转 Frame 后进入内部。MeshMessage 和 A2A 同理,各自有独立 adaptor,互不经过 CloudEvent。
+
+#### EventMeshFrame wire 格式
+
+```
+定长头 14B: [magic=0xEF][ver][msgType:1][flags:1][seq:4][keyCount:2][dataLen:4]
+KV 属性段: keyCount × [nameLen:2][name][valLen:4][value] ← 通用,所有 msgType 共用
+data: raw bytes(streaming 的 chunk/prompt / 事件的业务 data)
+
+msgType = STREAM_REQ | STREAM_CHUNK | EVENT
+flags = done | hasError | hasMeta(streaming 用)
+```
+
+- **STREAM_REQ**:KV(`sid`/`replyTo`/`model`/`conv`)+ data=prompt。
+- **STREAM_CHUNK**:`seq`(定长)+ `done`(flag)+ KV(`sid`/`etype`/`err`/`meta`)+ data=chunk。一个 `"He"` token ~25B vs CE-JSON ~250B(**~10×**)。
+- **EVENT**:KV(`id`/`type`/`subject`/`time`/`emttl`/`emcorrelationid`/用户扩展...)+ data=业务载荷。
+
+#### WireCodec SPI(内部 MQ wire 编码)
+
+内部 MQ wire 的字节编解码由 `WireCodec` SPI 定义(`eventmesh-common/.../wire/`),默认实现 `EventMeshFrameCodec`(EventMeshFrame↔byte[]),可通过 `-Deventmesh.wire.codec=<fqcn>` 替换。
+
+#### 落地范围
+
+- **storage SPI**:`MeshStoragePlugin.send/poll` + `LiteTopicCapable.sendLite/pullLite` 全改 EventMeshFrame(3 插件迁,带 legacy CE-JSON fallback)。
+- **runtime dispatch 管线**:Delivery/BufferedEvent/PushChannel/Connection/DeadLetterSink/ReliableDispatcher/PushService/所有 channel + CloudEventFilter/SubscriptionManager 全翻 Frame。
+- **streaming**:Mode-1(runtime↔agent 跨进程)+ Mode-2(runtime 内部 pub/sub)全用 EventMeshFrame。
+- **legacy 连接器 SPI**(Producer/Consumer)不动(独立子系统,说 CE,边界转换)。
+
+### 19.2 FrameAdaptor SPI(协议转换收进插件)
+
+对外协议 ↔ EventMeshFrame 的双向转换由 `FrameAdaptor` SPI 定义(`eventmesh-protocol-api/.../FrameAdaptor.java`),各协议插件各自实现。runtime 不直接调 `EventMeshFrame.fromCloudEvent()` / `.toCloudEvent()` / `MeshMessageFrameCodec`——全部经 `FrameAdaptors.get(协议名)` 加载对应 adaptor。**加新协议只需实现 `FrameAdaptor` + 注册 SPI,不改 runtime 代码。**
+
+#### 协议插件模块清单
+
+| 模块 | 职责 |
+|------|------|
+| **eventmesh-protocol-api** | SPI 接口(`FrameAdaptor` + `ProtocolAdaptor`)+ `FrameAdaptors` 加载器(零 adaptor 实现) |
+| **eventmesh-protocol-cloudevents** | `CloudEventsFrameAdaptor`:CloudEvents-JSON ↔ EventMeshFrame(注册 `cloudevents`) |
+| **eventmesh-protocol-meshmessage** | `MeshMessageFrameAdaptor`:Package ↔ EventMeshFrame(注册 `meshmessage`,零 CE 中转)+ 旧 `MeshMessageProtocolAdaptor`(兼容) |
+| **eventmesh-protocol-a2a** | `A2AFrameAdaptor`:JSON-RPC ↔ EventMeshFrame(注册 `a2a`,零 CE 中转)+ `EnhancedA2AProtocolAdaptor`(兼容) |
+| ~~eventmesh-protocol-grpc~~ | **已删除**(空壳无源码,gRPC 解析在 meshmessage 模块) |
+
+#### MeshMessage ↔ Frame 直接转换(零 CloudEvent)
+
+`MeshMessageFrameAdaptor`(meshmessage 插件)直接映射 Package ↔ EventMeshFrame,不经 CloudEvent:
+
+- **ingress**:`MeshMessagePackageRouter` → `FrameAdaptors.get("meshmessage").toFrameSilent(pkg)` → EventMeshFrame。字段映射:topic→`subject`,body→`data`,header.seq→`id`,properties→KV。
+- **egress**:`NettyTcpPushChannel` → `FrameAdaptors.get("meshmessage").fromFrameSilent(frame)` → Package。字段反向映射。
+
+旧 TCP SDK 全程 MeshMessage,内部全程 EventMeshFrame,零 CloudEvent 中转。
+
+### 19.3 Offset 路②:自管 + MQ 重放接管
+
+#### 两层 offset
+
+| 层 | 含义 | key 维度 | 实现 |
+|----|------|----------|------|
+| **pull offset** | 从 MQ 存储拉到哪 | `parent#lite@queue` / `topic#partition` | storage 插件本地(properties 文件) |
+| **deliver/ack offset** | 投递给 client 到哪 + client ack 到哪 | `topic#clientId#partition` | `OffsetStore`(RocksDB 本地) |
+
+两层**不强行合并**(语义不同、解耦),各留各的本地存储。
+
+#### 路②细则
+
+- **不给 group.id**,EventMesh 完全自管 offset(MQ 仅作持久 FIFO)。
+- **offset 纯本地**,**meta 上报默认关**(`-Deventmesh.offset.meta=true` 显式开)。淘汰 `MetaBackedOffsetStore`(百万 key 上报),类保留备用。
+- **接管 = MQ 重放(无状态迁移)**:实例挂 → client 经 §19.4 负载推荐重连新实例 → 从业务 topic 重拉 → in-flight 走 at-least-once 重投(业务幂等兜底)。
+- 核实结论:`pullAndDispatchPartition` 传 `startOffset=-1` 给 `storage.poll`(插件自管游标),`OffsetStore` 不在接管路径上;rocketmq5 用 POP(broker 侧 at-least-once),`assignPartitions` 是 no-op。
+
+### 19.4 负载均衡:session 分配层 + 全面粘性
+
+#### 架构定位
+
+> **均衡做在 session 分配层(入口 recommend),不在拉取/分发层。**
+
+实例只为自己代理的 client 被动按需拉取,不对等、不转发。先均衡"哪个 client 归哪个实例"(session 层),自然导致"各实例拉取量大致相当"。
+
+#### 客户端零负担——负载全自采
+
+EventMesh 实例本地自采负载指标(`LoadMeter`):
+- `activeSessions` ← `SessionRouter` sinks/subscribeSinks size
+- `inflowBytes/s`、`outflowBytes/s` ← `UniIngressService` 入口/出口 data 字节累加(按 clientId 分桶)
+- `cpuLoad` ← `OperatingSystemMXBean`
+
+随 5s 心跳写入 `/em/instances/<id>` = `<ts>|<addr>|<activeSessions>|<byteRate>|<cpuLoad>`。
+
+#### 均衡闭环
+
+- `/session/recommend`:读集群全局负载评分,返回推荐实例 `instanceUrl`(score = sessions + byteRate + cpuLoad 加权,过载负反馈)。
+- `/session/open` + `/events/subscribe`:返回 `instanceUrl`,SDK pin 后续 turn/close/poll/ack。
+- 大 client 分散:recommend 检查 client 现有 session 分布,散到多实例。
+- SDK 失败跨实例转移:open 失败重新 recommend。
+- `advertisedAddr` 默认空(单实例/测试/LB 兼容),显式配 `-Deventmesh.http.advertisedAddr` 才 pin。
+
+#### 全面粘性(广播域也走实例自治)
+
+`enableCluster` 不再接线 `PartitionOwnership`/`ClusterCoordinator`/`HttpForwarder`(类保留备用),只保留 `ClusterMembership` 心跳(供 recommend 评分)。实例拉全分区本地分发,**不跨实例转发**。
+
+### 19.5 验证
+
+1. **单测**:`EventMeshFrame` 全 msgType 互转(12 例);`OffsetStore` 两 key 空间共存;`LoadMeter` 指标 + 每 client 画像;`ClusterMembership` 心跳负载;dispatch 管线 Frame 化(ReliableDispatcher/SubscriptionManager/ClusterCoordinator)。
+2. **E2E**(真 broker):streaming 多轮 + Mode 2 pub/sub + **普通 pub/sub(RocketMQ5BrokerIntegrationTest 2/2)**全绿,内部全程 EventMeshFrame 往返正确;**LegacyTcpClientIntegrationTest(旧 TCP SDK)全绿**(MeshMessage↔Frame 直接转换)。
+3. **构建**:系统 gradle 8.5 + WEOA Nexus(offline)。
+
+---
+
+## 二十、流式调用设计(整合 streaming-session-design + sdk-streaming-call-design + lite-streaming-call)
+
+> 本节整合自 `streaming-session-design.md`、`sdk-streaming-call-design.md`、`lite-streaming-call.md`(POC v1 参考)。
+
+### 20.1 概述
+
+EventMesh 流式调用让客户端发起请求后,**持续接收一串文本分片(token / delta)**,直到收到结束标记——和 OpenAI Chat Completions 的 `stream: true`、SSE 打字机效果一致。
+
+两种正交模式:
+
+| 模式 | 用途 | 入口 | 机制 |
+|------|------|------|------|
+| **Mode 1 — 流式调用** | 客户端发起请求,Agent(接 LLM)逐 token 回复 | `openSession` → `call` → `forEach` | runtime 中介,通道多路复用 + client 亲和 |
+| **Mode 2 — 发布/订阅** | 生产者往 session lite 写 chunks,消费者 SSE 订阅 | `subscribeSession` / `openSessionPublisher` | 确定性 lite 命名,无 agent、无撮合 |
+
+**SDK 零 MQ 依赖**——只发 HTTP/SSE,runtime 是唯一接 MQ 的。内部全程 EventMeshFrame(§19.1),streaming 帧 msgType = STREAM_REQ / STREAM_CHUNK。
+
+### 20.2 Mode 1 — 流式调用
+
+#### 通道拓扑(AgentAnchoredStrategy)
+
+| 通道 | lite | 用途 |
+|------|------|------|
+| 请求 | `agent.<agentId>`(挂 `agent-parent-<i>`) | agent **启动时订阅一次**,所有 session 请求进这条、按 sessionId 解复用 |
+| 回复 | `client.<clientId>`(挂 `client-parent`) | runtime 持有该 client SSE 的实例消费,同 client 多 session 共用、按 sessionId 解复用 |
+
+**sessionId 格式 = `<agentId>:<uuid>`**(runtime 从 `:` 前缀零查表路由到 agent)。
+
+#### Session 生命周期
+
+```
+① agent 上线: POST /agent/register → runtime 分配 parent + 写 MetaStore → 回 parent
+ → agent subscribe (agent-parent-<i>, agent.<agentId>) + ready
+② 握手(open): client POST /session/open {clientId}
+ runtime 查 MetaStore clientId→agent: 撮合或复用 → 生成 sessionId
+ 回 {sessionId, agentId, instanceUrl}
+③ 流式(stream): client POST /session/stream/{sessionId} {prompt}
+ runtime: 注册 StreamSink → publish STREAM_REQ → agent lite
+ agent: 取上下文 + 调 LLM → chunk 发到 replyTo(client.<clientId>)
+ runtime: poll reply lite → demux → SSE 给 client
+ done: endTurn(drop sink,保留 consumer 跨 turn 复用)
+④ 多轮: 同 sessionId 再 POST /session/stream → 同 agent + 累积上下文
+⑤ 关闭(close): POST /session/close/{sessionId} → cancel(清本地状态)
+```
+
+**关键分离**: `endTurn`(自然终止,drop sink 保留 consumer)vs `cancel`(销毁 session)。修复了多轮重复 chunk bug。
+
+#### agent 注册协议(ready-before-route)
+
+agent 是薄 HTTP 客户端,不直连 broker/MetaStore。`status:READY` 必须在 subscribe 之后才置位——撮合只选 `READY && 心跳新鲜` 的 agent,保证不丢首包。
+
+#### 内部 wire(EventMeshFrame)
+
+- STREAM_REQ: `WireCodec.encode(StreamRequest)` → EventMeshFrame(STREAM_REQ)。KV(`sid`/`replyTo`/`model`/`conv`)+ data=prompt。
+- STREAM_CHUNK: `WireCodec.encode(StreamChunk)` → EventMeshFrame(STREAM_CHUNK)。`seq`(定长)+ `done`(flag)+ KV(`sid`/`etype`/`err`/`meta`)+ data=chunk。
+
+### 20.3 Mode 2 — 发布/订阅
+
+**确定性 lite 命名**(无绑定表、无撮合):
+- **parent** = 固定配置常量(`sessionStreamParent`,6 参 SessionRouter 构造器传入)
+- **liteKey** = `"session." + sessionId`(sessionId 的纯函数)
+
+两端用同一规则算出相同 (parent, liteKey) → 同一物理 lite topic。
+
+| 入口 | 方法 | 作用 |
+|------|------|------|
+| `/session/publish/{sessionId}` | POST | 发布一帧 chunk → `publishLite(parent, "session."+sid)`;首次懒惰 `createLiteTopic`;返回 201 |
+| `/session/subscribe/{sessionId}` | GET | SSE 订阅 → `pollLite(parent, "session."+sid)` → SSE 流;每 session 至多一个订阅消费者 |
+
+**单游标 + 免费重放**:lite 单分区 1-queue,崩溃重启可从 lite 头重新播放。
+
+### 20.4 SessionRouter 内部机制
+
+#### 构造器
+
+```java
+// 4 参:Mode 1 专用
+SessionRouter(ingress, registry, strategy, defaultTimeoutMs)
+// 6 参:Mode 1 + Mode 2
+SessionRouter(ingress, registry, strategy, defaultTimeoutMs, sessionTtlMs, sessionStreamParent)
+```
+
+#### 核心方法
+
+| 方法 | 模式 | 作用 |
+|------|------|------|
+| `startStream` | 1 | 发 STREAM_REQ 到 agent lite,创建 StreamSink |
+| `endTurn` | 1 | 自然结束:drop sink + demux 移除(保留 reply consumer 跨 turn) |
+| `cancel` | 1 | 销毁 session:drop sink + demux + 清 SessionRegistry |
+| `addReplySession` | 1 | 注册 demux 映射(clientId → sessionId → StreamSink) |
+| `ensureReplyConsumer` / `runReplyConsumer` | 1 | poll reply lite → 按 sessionId demux → StreamSink |
+| `publishSession` | 2 | 懒惰 createLiteTopic + publishLite |
+| `startSubscribe` / `cancelSubscribe` | 2 | putIfAbsent 守卫单订阅 + pollLite 循环 → StreamSink |
+
+#### StreamSink(有界队列)
+
+容量 1024(`MAX_BUFFERED_CHUNKS`)。SSE 写出线程 drain。队满丢帧(记录 warning,bounded memory)。
+
+#### Session Reaper
+
+后台定时(`Math.min(sessionTtlMs/2, 60s)`)回收 idle session(`registry.expireStaleSessions` → `cancel`)。
+
+### 20.5 消息协议(CloudEvents 分帧)
+
+内部 wire 只有 EventMeshFrame(STREAM_REQ / STREAM_CHUNK)。**没有独立的 OPEN/CLOSE/DONE 帧走 MQ**。终止(DONE)折叠进 STREAM_CHUNK 的 `done=true`。session 的 open/close 是 HTTP 控制面操作。
+
+```java
+STREAM_REQ(msgType=1):
+ data = prompt(text/plain)
+ KV: sid(=sessionId), replyTo(=parent#lite), model?, conv?
+
+STREAM_CHUNK(msgType=2):
+ data = chunk text
+ KV: sid, etype?, err?, meta?(JSON)
+ seq = 流内序号(定长头)
+ done = flag bit
+```
+
+### 20.6 上下文存储(Mode 1)
+
+| 方案 | 绑定 | 上下文 | 抗 agent 重启 |
+|------|------|------|------|
+| **A(当前)** | MetaStore | agent 进程内(ConversationStore) | ❌ 重启丢(re-handshake 或降级) |
+| B | MetaStore | MetaStore(截断/限轮) | ✅ 受大小限制 |
+| C | MetaStore | Redis | ✅ 但引入 Redis |
+
+agent 重启则该 agent 的上下文丢失(绑定仍在)。接口预留方案 C(Redis)。
+
+### 20.7 POC v1 历史(lite-streaming-call)
+
+v1(POC)已被 v2 取代。v1 问题:每流一个 lite(高基数)、req 广播(多 agent 重复处理)、无持久 session、无 client→agent 绑定。v2 用通道多路复用 + client 亲和 + 持久 sessionId 解决。v1 文档保留作历史参考。
+
+---
+
+## 二十一、SDK 流式调用接口设计(整合 sdk-streaming-call-design)
+
+> 本节整合自 `sdk-streaming-call-design.md`,描述客户端侧 SDK 封装。
+
+### 21.1 架构分层
+
+```
+┌─ SDK (eventmesh-sdk-java) ─────────────────────────────────┐
+│ CloudEventsClient.streaming() → HTTP + SSE 客户端 │
+│ • Mode 1: openSession → call → forEach │
+│ • Mode 2: subscribeSession / openSessionPublisher │
+└───────────────────────┬────────────────────────────────────┘
+ │ POST /session/* (HTTP + SSE)
+┌───────────────────────▼────────────────────────────────────┐
+│ Runtime session 层 (SessionRouter / Matchmaker / │
+│ AgentRegistrar / ChannelStrategy) │
+└───────────────────────┬────────────────────────────────────┘
+ │ publishLite / pollLite (EventMeshFrame)
+┌───────────────────────▼────────────────────────────────────┐
+│ Storage (RocketMQ5RemotingStoragePlugin, LiteTopicCapable) │
+└───────────────────────▲────────────────────────────────────┘
+ │ publish CHUNKs → reply lite
+┌───────────────────────┴────────────────────────────────────┐
+│ Agent (eventmesh-agent: StreamingAgent) │
+│ 订阅 req lite → 调 LLM → chunk 流 publish 到 replyTo │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 21.2 SDK 消费侧
+
+**唯一消费姿态 `forEach`**:
+
+```java
+public interface StreamingResponse extends AutoCloseable {
+ String sessionId();
+ String agentId();
+ CompletableFuture<Void> forEach(Consumer<StreamChunk> onChunk);
+ @Override void close();
+}
+```
+
+**设计决策——为什么只有 `forEach`**:
+- `forEach` 覆盖所有场景:回调消费、`.join()` 阻塞、`.orTimeout()` 超时、`CompletableFuture` 组合。
+- 删除了 `subscribe(StreamHandler)`、`iterator()`、`publisher(Flow)`、`blockAsString()`——API 表面积最小。
+- 删除 `callOneShot`——`openSession` + `call` + `forEach` + `close` 更清晰(session 生命周期显式)。
+
+### 21.3 多轮入口
+
+```java
+public interface StreamingOperations {
+ StreamingSession openSession(OpenSession req);
+}
+
+public interface StreamingSession extends AutoCloseable {
+ String sessionId();
+ String agentId();
+ String instanceUrl(); // pin 的实例 URL(空=用原 baseUrl)
+ StreamingResponse call(String prompt);
+ StreamingResponse call(StreamRequest req);
+ @Override void close();
+}
+```
+
+- `openSession` → `POST /session/open` → 匹配 Agent → `{sessionId, agentId, instanceUrl}`
+- `session.call(prompt)` → `POST /session/stream/{sessionId}` → SSE 流
+- `session.close()` → `POST /session/close/{sessionId}` → 销毁会话
+- 若 `instanceUrl` 非空,`StreamingSession` 用 `client.withBaseUrl(instanceUrl)` pin 后续 turn/close
+
+### 21.4 内部机制:SSE 读取 → 有界队列 → `forEach`
+
+```
+SSE 读取 VT ──offer──► [有界 LinkedBlockingQueue (1024)]
+ (POST /session/stream) │
+ └── forEach: 独立 VT drain 队列 → 调 onChunk
+```
+
+- 队满丢帧(bounded memory)。
+- `forEach` 在独立虚拟线程上 drain;收到终止帧后 complete future。
+- 单消费者守卫:每个 `StreamingResponse` 只能调一次 `forEach`。
+
+### 21.5 Mode 2 SDK
+
+```java
+// 订阅(消费)
+StreamingResponse sub = client.subscribeSession("my-session-id");
+sub.forEach(c -> ...).join();
+
+// 发布(生产)
+SessionPublisher pub = client.openSessionPublisher("my-session-id");
+pub.publish("Hello", false);
+pub.publish("", true); // 终止帧
+```
+
+`subscribeSession` 返回的 `StreamingResponse` 与 Mode 1 完全相同——`forEach` 是唯一消费方式。`SessionPublisher` 每次 `publish()` 发一个独立 HTTP POST,拥有 seq 计数器。
+
+### 21.6 SDK 零 MQ 依赖
+
+SDK 只发 HTTP/读 SSE,**不引入任何 MQ 客户端依赖**。这是硬性设计规则。Mode 1: `POST /session/open` → `POST /session/stream/{sid}` → 读 SSE → `POST /session/close/{sid}`。Mode 2: `POST /session/publish/{sid}`(生产)或 `GET /session/subscribe/{sid}`(消费 SSE)。
+
+### 21.7 HTTP 端点
+
+| 路径 | 方法 | 作用 |
+|------|------|------|
+| `/session/open` | POST | 开 session(握手 + 匹配 Agent),返回 `{sessionId, agentId, instanceUrl}` |
+| `/session/stream/{sessionId}` | POST | 发起一轮流式调用(Mode 1),返回 SSE 流 |
+| `/session/close/{sessionId}` | POST | 关闭 session |
+| `/session/publish/{sessionId}` | POST | 发布一帧 chunk(Mode 2),返回 201 |
+| `/session/subscribe/{sessionId}` | GET | 订阅 session 流(Mode 2),返回 SSE 流 |
+| `/session/recommend` | GET | 推荐实例(负载均衡),返回 `{instanceUrl}` |
+
+---
+
+*文档版本:v2.1 | EventMesh 简化重构方案 | 基于 unified-runtime-design.md v2.1 演进 + architecture-refinement v2.0 + streaming docs 整合 | 2026-08-13*
*v1.10 变更:**老协议不直接删——保留为边缘协议适配器以兼容老客户端。** ① TCP 退化为新架构 ingress/egress 传输(与 HTTP/WebHook/长轮询并列):保留线协议(`Package`/`Command`/`Codec`)+翻译层(`TcpMessageProtocolResolver`/`MeshMessageProtocolAdaptor`)+netty server 骨架,删除 TCP 自有核心(`ClientSession`/`ClientGroupPack`/rebalance、Consumer Group)。新增 `transport.tcp` 包(`TcpPushChannel`/`TcpAckRegistry`/`TcpIngressBridge`/`TcpFrameCodec`+`TcpFrameDecoder`)。② legacy HTTP(`EventMeshHttpClient`/`EventMeshMessage`/`/eventmesh/*` webhook-push)同理:`transport.http. [...]
*v1.9 变更:四项决策固化——① **可观测只用 OpenTelemetry,默认接 Prometheus**(metrics 走 OTel Meter 仪表、trace 走 OTel Tracer Span,默认经 OTel Prometheus exporter 暴露;legacy `eventmesh-metrics-prometheus` / `eventmesh-trace-plugin`(zipkin/jaeger/pinpoint) 不再接线、无 SPI 扩展点,附录 D.8);② **弃用 `eventmesh-registry`,只用 `eventmesh-meta`(MetaService)** 作为唯一控制面(附录 D.7 registry 行改为弃用);③ **retry 内置 `ReliableDispatcher`,不暴露 SPI 插件扩展**(`eventmesh-retry` 不接线,附录 D.9);④ **security 不用 SPI 插件扩展,用内置 FilterChain + IngressFilter**(`eventmes [...]
*v1.1 变更:新增 §13「能力缺口与设计补充」——对照 develop 分支现有能力,补齐多实例协调、下发可靠性、安全、可观测性、运维、接入扩展、协议工程化 7 类缺口设计,并更新 §14 实施 Phase 影响。*
diff --git a/docs/plugins/core-engines-configuration.md b/docs/plugins/core-engines-configuration.md
deleted file mode 100644
index 758efd9de..000000000
--- a/docs/plugins/core-engines-configuration.md
+++ /dev/null
@@ -1,135 +0,0 @@
-# EventMesh Core Engines Configuration Guide
-
-EventMesh provides powerful core engines (`Filter`, `Transformer`, `Router`) to dynamically process messages. These engines are configured via **MetaStorage** (Governance Center, e.g., Nacos, Etcd), supporting on-demand loading and hot-reloading.
-
-## 0. Core Concepts
-
-Before configuration, it is important to understand the specific role of each engine in the message flow:
-
-* **Filter (The Gatekeeper)**: Decides **"Whether to pass"**.
- * It inspects the message (CloudEvent) attributes. If the message matches the rules, it passes; otherwise, it is dropped.
- * *Use Case*: Block debug logs from production traffic; Only subscribe to specific event types.
-
-* **Transformer (The Translator)**: Decides **"What it looks like"**.
- * It modifies the message content (Payload or Metadata) according to templates or scripts.
- * *Use Case*: Convert XML to JSON; Mask sensitive data (PII); Adapt legacy protocols to new standards.
-
-* **Router (The Dispatcher)**: Decides **"Where to go"**.
- * It dynamically changes the destination (Topic) of the message.
- * *Use Case*: Route traffic to a Canary/Gray release topic; Route high-priority orders to a dedicated queue.
-
----
-
-## 1. Overview
-
-The configuration is not in local property files but distributed via the MetaStorage. EventMesh listens to specific **Keys** based on client Groups.
-
-- **Data Source**: Configured via `eventMesh.metaStorage.plugin.type`.
-- **Loading Mechanism**: Lazy loading & Hot-reloading.
-- **Key Format**: `{EnginePrefix}-{GroupName}`.
-- **Value Format**: JSON Array.
-
-| Engine | Prefix | Scope | Description |
-| :--- | :--- | :--- | :--- |
-| **Router** | `router-` | Pub Only | Routes messages to different topics. |
-| **Filter** | `filter-` | Pub & Sub | Filters messages based on CloudEvent attributes. |
-| **Transformer** | `transformer-` | Pub & Sub | Transforms message content (Payload/Header). |
-
----
-
-## 2. Router (Routing)
-
-**Scope**: Publish Only (Upstream)
-**Key**: `router-{producerGroup}`
-
-Decides the target storage topic for a message sent by a producer.
-
-### Configuration Example (JSON)
-
-```json
-[
- {
- "topic": "original-topic",
- "routerConfig": {
- "targetTopic": "redirect-topic",
- "expression": "data.type == 'urgent'"
- }
- }
-]
-```
-
-* **topic**: The original topic the producer sends to.
-* **targetTopic**: The actual topic to write to Storage.
-* **expression**: Condition to trigger routing (e.g., SpEL).
-
----
-
-## 3. Filter (Filtering)
-
-**Scope**: Both Publish (Upstream) & Subscribe (Downstream)
-
-### A. Publish Side (Upstream)
-**Key**: `filter-{producerGroup}`
-**Effect**: Intercepts messages **before** they are sent to Storage.
-
-### B. Subscribe Side (Downstream)
-**Key**: `filter-{consumerGroup}`
-**Effect**: Intercepts messages **before** they are pushed to the Consumer.
-
-### Configuration Example (JSON)
-
-```json
-[
- {
- "topic": "test-topic",
- "filterPattern": {
- "source": ["app-a", "app-b"],
- "type": [{"prefix": "com.example"}]
- }
- }
-]
-```
-
-* **filterPattern**: Rules matching CloudEvent attributes. If a message doesn't match, it is dropped.
-
----
-
-## 4. Transformer (Transformation)
-
-**Scope**: Both Publish (Upstream) & Subscribe (Downstream)
-
-### A. Publish Side (Upstream)
-**Key**: `transformer-{producerGroup}`
-**Effect**: Modifies message content **before** sending to Storage.
-
-### B. Subscribe Side (Downstream)
-**Key**: `transformer-{consumerGroup}`
-**Effect**: Modifies message content **before** pushing to the Consumer.
-
-### Configuration Example (JSON)
-
-```json
-[
- {
- "topic": "raw-topic",
- "transformerConfig": {
- "transformerType": "template",
- "template": "{\"id\": \"${id}\", \"new_content\": \"${data.content}\"}"
- }
- }
-]
-```
-
-* **transformerType**: e.g., `original`, `template`.
-* **template**: The transformation template definition.
-
----
-
-## 5. Verification
-
-1. **Publish Config**: Add the JSON config to your Governance Center (e.g., Nacos) with the Data ID `router-MyGroup`.
-2. **Send Message**: Use EventMesh SDK to send a message from `MyGroup`.
-3. **Observe**:
- * For **Router**: Check if the message appears in the `targetTopic` in your MQ.
- * For **Filter**: Check if blocked messages are skipped.
- * For **Transformer**: Check if the message body in MQ (for Pub) or Consumer (for Sub) is modified.
diff --git a/docs/production-readiness.md b/docs/production-readiness.md
index d2426e66a..fff0b446b 100644
--- a/docs/production-readiness.md
+++ b/docs/production-readiness.md
@@ -1,71 +1,136 @@
-# EventMesh Uni-Architecture 生产准入清单
+# EventMesh 生产准入与功能测试总结
-> 状态: 2026-07-10 (更新: §2 阻塞点全部解决 — remoting 插件 offset 游标 + 多 broker queue 寻址修复并验证). 基于真 RocketMQ (127.0.0.1:9876) + 真 Nacos (127.0.0.1:8848) 集成测试.
+> 更新: 2026-08-14 — 全 P0-P2 代码质量修复完成 + offset/ACK/负载均衡修复。基于全量 unit + 5.x E2E 验证。
+> 架构: 内部全程 EventMeshFrame(详见 `eventmesh-uni-architecture-redesign.md` §19)。
-## 1. 已验证 ✅
+---
+
+## 1. 架构定位
+
+**MQ-as-stateless-WAL + HTTP SDK(CloudEvents/MeshMessage/A2A 多协议)+ EventMesh 自管订阅/offset + 内部 EventMeshFrame** 的重写(详见 `docs/eventmesh-uni-architecture-redesign.md`)。采用叠加式策略:新核心并行存在,旧 TCP/HTTP 保留为兼容适配层(老客户端零改动)。
+
+## 2. 模块(活跃 gradle)
+
+| 类别 | 模块 |
+|---|---|
+| 核心 | `eventmesh-runtime`(~90 主类)、`common`、`spi` |
+| SDK | `sdk-java`(新 `CloudEventsClient` 含 streaming + lite + 旧 TCP/HTTP/gRPC SDK) |
+| 存储 | `storage-api`、`-kafka`、`-rocketmq`、`-rocketmq5`(含 `LiteTopicCapable`) |
+| 协议 | `protocol-api`(SPI 接口+加载器)、`-cloudevents`(FrameAdaptor)、`-meshmessage`、`-a2a` |
+| 连接器 | `connector-runtime` + 24 个 connector 插件 |
+| Wire | `common/.../wire/`(EventMeshFrame + WireCodec + MeshMessageFrameCodec) |
+
+## 3. 核心功能(runtime 按包)
+
+- **boot**: `UniRuntime`(pull/tick 调度循环)、`EventMeshApplication`(runtime+http+admin+session+streaming 一体启动)
+- **ingress**: `UniIngressService` — publish / subscribe / poll / ack / request-reply / **streaming session**(`LoadMeter` 自采负载)
+- **session**: `SessionRouter` — Mode-1 流式调用(多路复用+client 亲和)+ Mode-2 发布/订阅(确定性 lite 命名)
+- **subscription**: LOAD_BALANCE / BROADCAST / MULTICAST / STICKY + 心跳清理(全翻 EventMeshFrame)
+- **delivery**: `ReliableDispatcher` — ACK 跟踪 + 指数退避重试 + DLQ(全翻 EventMeshFrame + **延迟 MQ ACK**)
+- **push**: 长轮询 / SSE / WS(egress 处 Frame→CloudEvents-JSON via FrameAdaptor SPI + **写失败 nack dispatcher**)
+- **cluster**: `ClusterMembership`(心跳+负载指标)/ `LoadMeter` / `/session/recommend`(全面粘性,停用转发层)
+- **wire**: `EventMeshFrame`(统一内部帧)+ `WireCodec` SPI + `FrameAdaptor` SPI(协议转换收进插件)
+
+## 4. 已验证 ✅
| 能力 | 验证方式 | 结果 |
|---|---|---|
| 发布/订阅/轮询/ACK | RealBrokerIntegrationTest (真 RocketMQ) | ✅ 全链路 |
| 客户端 SDK (HTTP CloudEvents) | ClientBrokerIntegrationTest | ✅ publish/subscribe + request/reply |
-| 老 TCP 协议兼容 | LegacyTcpClientIntegrationTest + LegacyTcpClusterBrokerIntegrationTest | ✅ 老 SDK 零改动 |
-| 动态 connector 调度 | ConnectorSchedulingIntegrationTest | ✅ runtime push /control/start |
+| 老 TCP 协议兼容 | LegacyTcpClientIntegrationTest | ✅ 老 SDK 零改动, MeshMessage↔Frame 直接转换 |
+| 普通 pub/sub (EventMeshFrame) | RocketMQ5BrokerIntegrationTest (-Dit.storage5) | ✅ 2/2 真 broker, 内部 Frame 往返 |
+| 流式调用 Mode-1 | StreamingSdkE2ETest$Mode1 (真 broker + mock LLM) | ✅ 单轮+多轮+session 契约 |
+| 流式调用 Mode-2 | StreamingSdkE2ETest$PubSub (真 broker) | ✅ pub/sub 有序传递 |
+| Lite topic HTTP | RocketMQ5LiteHttpIntegrationTest | ✅ publish/poll lite |
+| 负载均衡推荐 | ClusterMembershipLoadTest + LoadMeterTest + LoadBalancingScoringTest | ✅ 负载采集+心跳+评分 |
+| Offset 单调写入 (P4) | OffsetMonotonicAndRecoveryTest | ✅ 6 例: 单调/多 client/重启恢复 |
+| 延迟 ACK (P2) | DeferredAckDispatcherTest | ✅ 4 例: 客户端ACK触发/超时不触发/DLQ不触发/null兼容 |
+| Frame 协议转换 | FrameProtocolConversionTest | ✅ 7 例: CE round-trip/filter/TTL/correlation/POP_CK |
+| PushService 缓冲 (P1-4/5) | PushServiceBufferOverflowTest | ✅ 3 例: DROP_OLDEST nack/BLOCK 拒绝/ack 移除 |
+| SessionRegistry 原子性 (P1-2) | SessionRegistryAtomicityTest | ✅ 3 例: immutable bean 更新 |
+| Delivery 可见性+复活 (P0-3/P1-6) | DeliveryVolatileAndResurrectTest | ✅ 2 例: volatile/tick 不复活 |
| 多实例无重复消费 | MultiInstanceRocketMqIntegrationTest (2 实例) | ✅ 5 条恰好投递一次 |
-| 跨实例 forward (Nacos) | NacosClusterForwardIntegrationTest | ✅ sub 在 A, publish 在 B, A 收到 |
| ACK 超时重投递 + DLQ | AckTimeoutRedeliveryIntegrationTest | ✅ at-least-once + DLQ |
| 限流 (429) | RateLimitIntegrationTest | ✅ admin + metrics |
| TLS/HTTPS | TlsIntegrationTest | ✅ 端到端 |
-| 死信 replay | DlqIntegrationTest | ✅ DLQ -> 回原 topic |
-| 吞吐 | LoadThroughputIntegrationTest (真实多 broker, 修复后实测) | ~101 ev/s, 0 丢失, 0 重复 (500 事件突发) |
-
-## 2. 阻塞点 ⛔ → ✅ 已全部解决
+| 吞吐 | LoadThroughputIntegrationTest (真实多 broker) | ~101 ev/s, 0 丢失, 0 重复 |
+
+## 5. 测试总览
+
+### 单元/进程内测试(无 broker,全绿)
+- runtime: ~50 测试类 / ~150 @Test, **0 failures**
+- sdk-java: 29+ @Test | connector-runtime: 13 @Test | common(wire): 12+ @Test(EventMeshFrame)
+- 合计 **~160 @Test**
+- 覆盖: subscription / offset(RocksDB+单调) / delivery(ACK+DLQ+退避+延迟MQACK) / push(缓冲+溢出) / ratelimit / security / cluster / TLS / DLQ / EventMeshFrame 全 msgType / WireCodec / FrameAdaptor / LoadMeter / ClusterMembership 负载 / SessionRouter / SessionRegistry 原子性 / Delivery volatile
+
+### 真 broker E2E(gated on `rocketmq5Available()`,全绿)
+- `RocketMQ5BrokerIntegrationTest` ✅ (普通 pub/sub, EventMeshFrame 往返, 2/2)
+- `RocketMQ5LiteHttpIntegrationTest` ✅ (lite publish/poll)
+- `StreamingSdkE2ETest` ✅ (Mode-1 streaming ×3 + Mode-2 pub/sub ×1 + Demo ×1)
+- `LiteStreamCallIntegrationTest` ✅ (Mode-1 裸 HTTP streaming ×2)
+- `LegacyTcpClientIntegrationTest` ✅ (旧 TCP SDK, MeshMessage↔Frame 直接转换)
+- `KafkaClientE2EIntegrationTest` ✅ (真 Kafka 3-broker SASL, HTTP publish→subscribe 全链路, EventMeshFrame 往返)
+
+## 6. 已解决的问题
+
+### 6.1 消息丢失(订阅/重启窗口)✅
+- **真因**: `RocketMQRemotingStoragePlugin` pull 游标卡 offset 0 + 多 broker queue 寻址错误。
+- **修复**: 始终用 `nextBeginOffset` 推进 + `QueueLoc(brokerAddr, localQueueId)` 局部寻址。
+- **验证**: ClientBrokerIntegrationTest GREEN(真实多 broker)。
+
+### 6.2 offset 持久化(重启恢复)✅
+- **修复**: remoting 插件自管 pull offset(properties 文件),重启加载续传。
+
+### 6.3 P2: RocketMQ 5.x 延迟 ACK ✅
+- **修复**: poll 不立即 ACK broker;`ReliableDispatcher.ack()` → `ackPulledMessage()` 触发 broker ACK。恢复 at-least-once。
+
+### 6.4 P4: OffsetStore 非单调写入 ✅
+- **修复**: `writeOffset` 改 CAS max(InMemory `accumulateAndGet(Math::max)`,RocksDB read-conditional-put)。
+
+### 6.5 代码质量全面修复(P0-P2, 15 项)✅
+**P0(3 项)**:
+- SSE/WS push 写失败 → ConnectionPushPump nack dispatcher(原来静默丢失到超时)
+- SDK HttpURLConnection 泄漏 → finally disconnect
+- Delivery.attempt/nextAttemptAtMs → volatile
+
+**P1(9 项)**:
+- UniIngressService/ClusterMembership 字段 → volatile
+- SessionRegistry → immutable bean 更新(不原地修改共享对象)
+- PushService.offer → synchronized(queue)(TOCTOU + 状态机竞态)
+- PushService.DROP_OLDEST → nack 丢弃回调
+- ReliableDispatcher.tick → putIfAbsent(防复活已 ACK)
+- SDK TCP reconnect → log.error(不再空 catch)
+- RocketMQ5 ack → 3 次重试 + 指数退避
+- RocksDBOffsetStore → offsetWriteFailures 计数器
+
+**P2(3 项)**:
+- SseConnection → synchronized(out)(防并发写交错)
+- UniRuntime shutdown → log.debug
+- SubscriptionManager.pollAndDispatch → log.warn
+
+## 7. 已知限制 ⚠️ (可灰度, 需文档化)
-> 截至 2026-07-10,针对 **RocketMQ 部署无已知硬阻塞**。以下两项曾阻塞上生产,现已定位真因并修复 + 验证。
+| 项 | 说明 |
+|---|---|
+| RocketMQ 4.x ConsumeQueue | ✅ **已验证为误报**:RocketMQ 存储清理不依赖消费位点——CommitLog 按 `fileReservedTime`(72h)+ 磁盘水位(75%/90%)删除,ConsumeQueue 经 `minPhysicOffset` 随 CommitLog 联动删除;`OFFSET_MOVED` 行为本身证明删除不被消费阻塞。`commitOffset` no-op 无存储增长风险 |
+| WebSocket 推送 | ✅ 已有集成测试(WebSocketPushIntegrationTest 1/1 通过, subscribeWs 端到端 push) |
+| Kafka 分区 | assign+seek 自管; ✅ 已对真 Kafka 实测(KafkaClientE2EIntegrationTest, 3-broker SASL 集群, 2026-08-14) |
+| Nacos watch 时序 | naming.subscribe push 延迟波动;多实例 watch 组合跑偶发 flaky |
-### 2.1 消息丢失(订阅/重启窗口)✅ 已解决
-- **最初现象** (2026-07-08, 旧 DefaultLitePullConsumer 插件): 500 条突发, 57 条 (11%) 丢失, 0 重复。当时归因为 RocketMQ `CONSUME_FROM_LAST_OFFSET` + rebalance 延迟。
-- **真因** (2026-07-10 定位): `RocketMQRemotingStoragePlugin` 两个 latent bug —— remoting 插件号称"解决 offset-seek 丢失",实则未根治:
- 1. **pull 游标卡在 offset 0**: `poll()` 仅在 `SUCCESS` 时记录 `nextBeginOffset`; broker 对越界 offset 返回 `OFFSET_MOVED` (code 20/21) 被静默丢弃 → 游标反复请求 offset 0, 永远拉不到新消息。(当 topic 有历史、min offset > 0 时触发。)
- 2. **多 broker queue 寻址错误**: `poll`/`send` 把全局扁平 queueId 当 `header.queueId`, 但 RocketMQ queueId 按 broker 局部编号 → 跨 broker 的 topic 有 ~80% 队列映射到无效局部 id → 返回 null → **静默不消费** (每跑 2080 条 ERROR 日志)。
-- **为何之前没暴露**: LoadThroughput 用无历史的新 topic (min offset=0, 不触发 bug 1) 且多为单 broker 路由 (不触发 bug 2), 故显示 0 丢失; ClientBroker 的 topic `em-it-client` 跨多次运行有历史 + TBW102 自动创建 topic 扩散到 5 个 broker → 同时触发两个 bug → pub-1 收不到。
-- **修复**:
- 1. 始终用 `nextBeginOffset` 推进游标 (NO_NEW_MSG=空操作 / OFFSET_MOVED=修正 / FOUND=前进) + null-`nextBeginOffset` guard。
- 2. `getBrokerForQueue` 返回 `QueueLoc(brokerAddr, localQueueId)` (按 `readQueueNums` 扁平, 每 broker 局部 id 从 0 起), send/poll 用 localQueueId。
- - 附带: `RPC_TIMEOUT_MS` 100→1000ms (100ms 伪超时); 去掉单次 pull 失败永久拉黑 broker。
-- **验证**: `ClientBrokerIntegrationTest` GREEN (真实多 broker, pub/sub + request-reply); offset 文件从仅 `#0–#3` 变为 `#0–#19` (20 队列全消费); 幽灵队列 ERROR 2080→0; 全套 runtime 82 tests 0 failures。
+## 8. 可观测性 📊
-### 2.2 offset 持久化(重启恢复)✅ 已解决
-- **现象**: autoCommit=false + 不 commit MQ offset → 重启后位置不持久 → CONSUME_FROM_LAST_OFFSET (跳到最新, 丢窗口) 或 FIRST_OFFSET (从 0 重放, 重复)。
-- **修复**: remoting 插件自管 pull offset (per `topic#queueId`), 持久化到 `./data/offset/rocketmq-pull-offsets.properties`, 重启加载 + 始终从 `nextBeginOffset` 续传 (见 2.1)。
-- **验证**: 多次测试运行间 offset 正确续传 (load 1 topic → persist 1 topic, offset 持续前进)。
+### 8.1 Metrics (已有)
+- **`GET /metrics`** → **Prometheus scrape 端点**(text exposition v0.0.4):7 counter + 1 gauge,名称与告警规则(§9.3)一致
+- `GET /admin/metrics` → JSON: publishCount/publishFailed/rateLimited/eventsDispatched/ackCount/redeliveries/dlqCount/pendingDeliveries
+- OTel instruments 已埋点(部署侧可挂 OTel SDK/agent 增强)
+- `RocksDBOffsetStore.getOffsetWriteFailures()` — offset 写失败计数(P1-9 加)
-## 3. 已知限制 ⚠️ (可灰度, 需文档化)
+### 8.2 需补
+- [x] ~~OTel metrics exporter 接入~~ → **已加 `/metrics` Prometheus 端点**(PrometheusEndpointTest 验证)
+- [ ] 端到端 distributed tracing(OTel spans 已埋;需接 collector)
+- [x] Grafana dashboard 模板 / 告警规则 / SLO 定义(§9)
-| 项 | 说明 |
-|---|---|
-| RocketMQ 分区分配 | subscribe-mode + 共享 consumer group -> broker rebalance (稳态无重复). EventMesh PartitionOwnership 旁路 (partitionCount=-1). 设计如此. |
-| Gen fencing | 软 fence (Meta gen, 能读 Meta 时生效) + lease gate (Meta 不可达 -> 停 poll). 网络分区期间仍有 at-least-once 重复窗口 (直到 lease 过期). |
-| WebSocket 推送 | `UniWsServer` **已实现** (netty 握手 + 每连接 `ConnectionPushPump` + ACK 控制帧 + wss; SDK `subscribeWs`), 但**无集成测试** (端到端 WS 推送未验证); SSE + long-polling 已验证. |
-| Nacos watch 时序 | naming.subscribe push 延迟波动 -> 测试需 30s 等待. 多实例 watch 在组合跑时偶发 flaky. |
-| Kafka 分区 | assign+seek (EventMesh 自管). 未对真 Kafka 实测 (无 Kafka broker). |
-
-## 4. 可观测性 📊
-
-### 4.1 Metrics (已有)
-- `GET /admin/metrics` -> JSON: publishCount, publishFailed, rateLimited, eventsDispatched, ackCount, redeliveries, dlqCount, pendingDeliveries. (**唯一内置读出**)
-- OTel instruments (LongCounter/LongHistogram) 已埋点, 名如 `eventmesh_publish_count` / `eventmesh_publish_failed_count` / `eventmesh_rate_limited_count` / `eventmesh_dispatched_count` / `eventmesh_ack_count` / `eventmesh_redeliveries_count` / `eventmesh_dlq_count` / `eventmesh_dispatch_latency_nanos`. **运行时未捆绑 exporter** —— 需部署侧挂 OTel SDK/agent (Prometheus 或 OTLP collector); 旧 `eventmesh-metrics-prometheus` 插件未接.
-- `GET /admin/subscriptions?topic=` -> live subscriptions.
-- `GET /admin/offsets?topic=` -> distribution offsets.
-- `GET /admin/health` -> liveness + pending + partition ownership.
-
-### 4.2 需补
-- [ ] OTel metrics exporter 接入 (Prometheus / OTLP) —— 当前仅 `/admin/metrics` JSON, 无 Prometheus scrape 端点.
-- [x] Grafana dashboard 模板 (§7.4).
-- [x] 告警规则 (§7.3: 发布失败率 / DLQ 率 / 积压 / 实例 down).
-- [ ] 端到端 distributed tracing (OTel spans 存在: startPublish/startDispatch/startAck/startDlq; 需接 collector).
-- [x] SLO 定义 (§7).
-
-### 4.3 Admin API 一览
+### 8.3 Admin API 一览
| Endpoint | 方法 | 用途 |
|---|---|---|
| `/admin/metrics` | GET | 运行指标 |
@@ -79,69 +144,10 @@
| `/admin/ratelimit` | PUT | 设限流规则 |
| `/admin/connectors` | GET/POST/DELETE | connector CRUD |
| `/admin/connector-workers` | GET/POST | worker 注册/心跳 |
-| `/connector/offset?connectorId=` | GET/POST | connector offset |
-
-## 5. 运维 Runbook
-
-### 5.1 部署
-```
-# runtime 镜像
-docker run -p 8080:8080 -p 8081:8081 \
- -e EVENTMESH_STORAGE_TYPE=kafka \
- -e JAVA_OPTS="-Deventmesh.meta.type=nacos -Deventmesh.meta.addr=nacos:8848" \
- -v $PWD/conf:/data/app/eventmesh/conf \
- eventmesh:uni
-
-# connector 镜像
-docker run -e EVENTMESH_RUNTIME_URL=http://runtime:8080 \
- -e CONNECTOR_OPTS="-Dconnector.1.class=...KafkaSourceConnector -Dconnector.1.mode=source -Dconnector.1.topic=t1" \
- eventmesh-connector:uni
-```
-
-### 5.2 关键配置
-| 配置 | 默认 | 说明 |
-|---|---|---|
-| `eventmesh.storage.type` | standalone | kafka/rocketmq (standalone 已删) |
-| `eventmesh.http.port` | 8080 | 流量 HTTP |
-| `eventmesh.admin.port` | 8081 | 管理 HTTP |
-| `eventmesh.offset.path` | ./data/offset | RocksDB offset 目录 |
-| `eventmesh.meta.type` | (空) | nacos (空=单实例 InMemory) |
-| `eventmesh.meta.addr` | (空) | nacos 地址 |
-| `eventmesh.tls.keystore` | (空) | TLS keystore 路径 |
-| `eventmesh.ws.port` | (空) | WebSocket (-1=关) |
-
-### 5.3 存储配置
-- Kafka: `conf/kafka-client.properties` -> `bootstrap.servers`
-- RocketMQ: `conf/eventmesh.properties` -> `eventMesh.server.rocketmq.namesrvAddr`
-
-### 5.4 常见故障
-| 症状 | 排查 | 处理 |
-|---|---|---|
-| 启动报 "no MeshStoragePlugin" | `eventmesh.storage.type` 错或 SPI jar 不在 classpath | 设正确 type + 确保 storage jar 在 `plugin/storage/<type>/` |
-| 启动报 "topic not exist" (CODE 17) | broker autoCreateTopicEnable=false | 预建 topic (4+ queues) |
-| 消息不投递 | `/admin/health` 查 partitionOwnership + pendingDeliveries | 消费者未分配 / ACK 积压 |
-| DLQ 堆积 | `/admin/dlq/browse?topic=` 查死信 | 修消费者 -> `/admin/dlq/replay` 回放 |
-| 限流 | `/admin/metrics` 查 rateLimited | `PUT /admin/ratelimit` 调整 |
-| 多实例重复 | 查 `partitionOwnership` (null=poll-all, broker rebalance) | 确认 broker rebalance 正常 |
-| Meta 不可达 | 日志 "heartbeat failed - lease invalid" | 修 Nacos; 实例自动停 poll (lease gate) |
-
-### 5.5 升级/回滚
-- 滚动重启: 逐实例 `docker stop` -> `docker run` (新镜像). Lease gate 确保停 poll 期间不重复.
-- 回滚: 同上, 换旧镜像. offset 兼容 (RocksDB 格式不变).
-- **注意**: offset 持久化 + 续传已修 (§2.1/§2.2), 滚动重启不丢窗口内消息。Lease gate 确保实例停 poll 期间不重复。
-
-## 6. 下一步优先级
-1. ✅ ~~消息丢失 / offset-seek (§2.1)~~ -- 真因: pull 游标卡 offset 0 + 多 broker queue 寻址; 已修并验证 (ClientBroker IT GREEN).
-2. ✅ ~~offset persist + 续传 (§2.2)~~ -- pull-offset 持久化 + 始终从 nextBeginOffset 续传.
-3. 📊 OTel exporter 接入 (metrics + trace → Prometheus / OTLP collector): 仪器 + spans 已埋点, 但**运行时未捆绑 exporter** (§4.2); dashboard / 告警 / SLO 模板已就绪 (§7), 接 exporter 后才可实测.
-4. 🧪 broker/Nacos failover 混沌测试.
-5. 🔒 mTLS 双向认证: 单向 TLS 已实现+测试 (`TlsContextFactory`+`withTls`); **client-auth 强制未接** (`UniHttpServer` 用默认 `HttpsConfigurator`, 未设 `needClientAuth`). auth filter (Token/ACL/HMAC) 已实现.
-6. 🧪 WebSocket 集成测试: `UniWsServer` 已实现 + wss, 但无 IT (SDK `subscribeWs` 端到端未验证).
+## 9. SLO 定义
-## 7. SLO 定义
-
-### 7.1 指标采集 (已有)
+### 9.1 指标采集
| 指标 | 来源 | 用途 |
|---|---|---|
| `publishCount` / `publishFailed` | `/admin/metrics` | 发布成功率 |
@@ -151,57 +157,97 @@ docker run -e EVENTMESH_RUNTIME_URL=http://runtime:8080 \
| `rateLimited` | `/admin/metrics` | 限流频率 |
| `consumeLag` | `storage.poll` offset vs `endOffset` | 消费延迟 |
-### 7.2 SLO 目标(建议初值,灰度后调整)
-
-| SLO | 目标 | 告警阈值 | 计算方式 |
-|---|---|---|---|
-| **发布可用性** | ≥ 99.9% | publishFailed / publishCount > 0.1% 持续 5min | `1 - publishFailed / publishCount` |
-| **投递延迟 P99** | ≤ 500ms | P99 > 1s 持续 5min | publish 时间戳 -> ACK 时间戳 |
-| **端到端延迟 P99** | ≤ 2s | P99 > 5s 持续 5min | publish -> subscriber 收到 |
-| **DLQ 率** | ≤ 0.01% | dlqCount / eventsDispatched > 0.1% | `dlqCount / eventsDispatched` |
-| **积压** | ≤ 1000 | pendingDeliveries > 5000 持续 5min | `pendingDeliveries` |
-| **消费延迟** | ≤ 10s | consumeLag > 60s 持续 5min | `endOffset - pollOffset` |
-| **限流频率** | ≤ 1% | rateLimited / publishCount > 5% | `rateLimited / publishCount` |
-| **可用性** | ≥ 99.9% | `/admin/health` 非 UP 持续 3min | HTTP 200 |
-
-### 7.3 告警规则(Prometheus / Alertmanager)
-
-> **前提**: 需先接 OTel → Prometheus exporter (§4.2, 当前未捆绑). counter 仪器名 `eventmesh_publish_count` 经 Prometheus exporter 导出为 `eventmesh_publish_count_total` (monotonic counter 自动加 `_total`).
-
+### 9.2 SLO 目标(建议初值,灰度后调整)
+| SLO | 目标 | 告警阈值 |
+|---|---|---|
+| 发布可用性 | ≥ 99.9% | publishFailed/publishCount > 0.1% 持续 5min |
+| 投递延迟 P99 | ≤ 500ms | P99 > 1s 持续 5min |
+| 端到端延迟 P99 | ≤ 2s | P99 > 5s 持续 5min |
+| DLQ 率 | ≤ 0.01% | dlqCount/eventsDispatched > 0.1% |
+| 积压 | ≤ 1000 | pendingDeliveries > 5000 持续 5min |
+| 消费延迟 | ≤ 10s | consumeLag > 60s 持续 5min |
+| 限流频率 | ≤ 1% | rateLimited/publishCount > 5% |
+| 可用性 | ≥ 99.9% | `/admin/health` 非 UP 持续 3min |
+
+### 9.3 告警规则(Prometheus / Alertmanager)
```yaml
-# publish failure rate
- alert: PublishFailureRateHigh
expr: rate(eventmesh_publish_failed_total[5m]) / rate(eventmesh_publish_count_total[5m]) > 0.001
for: 5m
labels: { severity: critical }
- annotations: { summary: "Publish failure rate > 0.1%" }
-
-# DLQ rate
- alert: DlqRateHigh
expr: rate(eventmesh_dlq_count_total[5m]) / rate(eventmesh_events_dispatched_total[5m]) > 0.001
for: 5m
labels: { severity: warning }
- annotations: { summary: "DLQ rate > 0.1%" }
-
-# pending deliveries backlog
- alert: PendingDeliveriesHigh
expr: eventmesh_pending_deliveries > 5000
for: 5m
labels: { severity: warning }
- annotations: { summary: "Pending deliveries > 5000" }
-
-# health
- alert: EventMeshDown
expr: up{job="eventmesh"} == 0
for: 3m
labels: { severity: critical }
- annotations: { summary: "EventMesh instance down" }
```
-### 7.4 Grafana dashboard 模板
+## 10. 运维 Runbook
-Dashboard 建议包含 4 行:
-1. **发布**: publishCount (rate) + publishFailed (rate) + rateLimited (rate)
-2. **投递**: eventsDispatched (rate) + ackCount (rate) + redeliveries (rate)
-3. **积压**: pendingDeliveries (gauge) + dlqCount (counter)
-4. **健康**: health (UP/DOWN) + consumeLag per topic
+### 10.1 部署
+```bash
+# runtime 镜像
+docker run -p 8080:8080 -p 8081:8081 \
+ -e EVENTMESH_STORAGE_TYPE=rocketmq5 \
+ -e EVENTMESH_ROCKETMQ5_NAMESRV=broker:9876 \
+ -e JAVA_OPTS="-Deventmesh.meta.type=nacos -Deventmesh.meta.addr=nacos:8848" \
+ eventmesh:uni
+
+# connector 镜像
+docker run -e EVENTMESH_RUNTIME_URL=http://runtime:8080 \
+ eventmesh-connector:uni
+```
+
+### 10.2 关键配置
+| 配置 | 默认 | 说明 |
+|---|---|---|
+| `eventmesh.storage.type` | standalone | rocketmq5 / rocketmq / kafka |
+| `eventmesh.http.port` | 8080 | 流量 HTTP |
+| `eventmesh.admin.port` | 8081 | 管理 HTTP |
+| `eventmesh.offset.path` | ./data/offset | offset 目录 |
+| `eventmesh.meta.type` | (空) | nacos (空=单实例) |
+| `eventmesh.meta.addr` | (空) | nacos 地址 |
+| `eventmesh.tls.keystore` | (空) | TLS |
+| `eventmesh.ws.port` | (空) | WebSocket |
+| `eventmesh.rocketmq5.lite.checkpoint.interval.ms` | 5000 | lite offset checkpoint |
+| `eventmesh.offset.meta` | false | meta-backed offset(显式开) |
+| `eventmesh.http.advertisedAddr` | (空) | instanceUrl pin(显式配) |
+| `eventmesh.wire.codec` | EventMeshFrameCodec | 内部 wire 编码 |
+| `eventmesh.storage5` / `eventmesh.namesrv5` | (空) | 5.x E2E gating |
+
+### 10.3 常见故障
+| 症状 | 排查 | 处理 |
+|---|---|---|
+| 启动报 "no MeshStoragePlugin" | `eventmesh.storage.type` 错或 SPI jar 不在 classpath | 设正确 type + 确保 storage jar |
+| 启动报 "topic not exist" (CODE 17) | broker autoCreateTopicEnable=false | 预建 topic (4+ queues) |
+| 消息不投递 | `/admin/health` 查 pendingDeliveries | 消费者未分配 / ACK 积压 |
+| DLQ 堆积 | `/admin/dlq/browse?topic=` | 修消费者 → `/admin/dlq/replay` |
+| 限流 | `/admin/metrics` 查 rateLimited | `PUT /admin/ratelimit` |
+| offset 写失败持续增长 | 查 `RocksDBOffsetStore.getOffsetWriteFailures()` | 检查磁盘/权限 |
+| SSE 客户端收不到 | 连接写失败现在会 nack dispatcher → 立即重投 | 检查客户端网络 |
+
+### 10.4 升级/回滚
+- 滚动重启: 逐实例 stop→run。offset 持久化+续传已修,不丢窗口内消息。
+- 回滚: 换旧镜像。offset RocksDB 格式不变。
+
+## 11. 下一步优先级
+
+1. ✅ ~~消息丢失 / offset-seek~~ — pull 游标 + 多 broker 寻址修复
+2. ✅ ~~offset persist + 续传~~ — properties 文件续传
+3. ✅ ~~P2: RocketMQ 5.x 延迟 ACK~~ — ReliableDispatcher.ack() 触发 ackPulledMessage()
+4. ✅ ~~P4: OffsetStore 单调写入~~ — CAS max
+5. ✅ ~~P0-P2 代码质量(15 项)~~ — volatile/immutable/synchronized/nack/retry 全修
+6. ✅ ~~Kafka 真实实测~~ — KafkaClientE2EIntegrationTest 通过(3-broker SASL)
+7. ✅ ~~OTel exporter 接入~~ — `/metrics` Prometheus scrape 端点(PrometheusEndpointTest 验证)
+8. ✅ ~~mTLS 双向认证~~ — `-Deventmesh.tls.needClientAuth=true` + truststore → HttpsConfigurator needClientAuth
+9. ✅ ~~WebSocket 集成测试~~ — WebSocketPushIntegrationTest 1/1 通过
+10. ✅ ~~RocketMQ 4.x ConsumeQueue 清理~~ — **验证为误报**(CommitLog 时间清理 + ConsumeQueue 联动,不依赖消费位点;OFFSET_MOVED 行为佐证)
+11. 🧪 broker/Nacos failover 混沌测试 — 需要破坏性环境
+12. 📊 端到端 distributed tracing — OTel spans 已埋,需接 collector
diff --git a/docs/uni-architecture-features-and-tests.md b/docs/uni-architecture-features-and-tests.md
deleted file mode 100644
index 37b45677a..000000000
--- a/docs/uni-architecture-features-and-tests.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# uni-architecture 分支:功能与测试总结
-
-> 快照时间:2026-07-10 | 分支:`uni-architecture`
-
----
-
-# 架构定位
-
-**MQ-as-stateless-WAL + HTTP-only CloudEvents SDK + EventMesh 自管订阅/offset** 的重写(`docs/eventmesh-uni-architecture-redesign.md`)。采用**叠加式**策略:新核心并行存在,旧 TCP/HTTP **保留为兼容适配层**(老客户端零改动),物理删除(Phase 8)是最后切换。
-
-# 模块(活跃 gradle,共 ~38 个)
-
-| 类别 | 模块 |
-|---|---|
-| 核心 | `eventmesh-runtime`(75 主类)、`common`、`spi` |
-| SDK | `sdk-java`(新 `CloudEventsClient` + 旧 TCP/HTTP/gRPC SDK) |
-| 存储 | `storage-api`、`-kafka`、`-rocketmq`(rocketmq = remoting 直连 RPC 插件) |
-| 协议 | `protocol-api`、`-meshmessage`、`-grpc`、`-a2a` |
-| 连接器 | `connector-runtime` + **24 个 connector 插件**(rocketmq/kafka/rabbitmq/redis/mongodb/pulsar/s3/pravega/knative/jdbc/file/spring/prometheus/dingtalk/lark/wecom/slack/wechat/http/chatgpt/canal/mcp/openfunction…) |
-
-已删除:旧 security/meta/metrics/trace/retry/registry/function/v1/v2/admin/starter/examples/operator + storage-{standalone,pulsar,redis,rabbitmq}。
-
-# 核心功能(runtime 按包)
-
-- **boot**: `UniRuntime`(pull/tick 调度循环)、`EventMeshApplication`(runtime+http+admin 一体启动,支持 TLS / WS / 集群 / 连接器调度)
-- **ingress**: `UniIngressService` — publish / subscribe / poll / ack / **request-reply**(`emcorrelationid`) 编排核心
-- **subscription**: LOAD_BALANCE / BROADCAST / MULTICAST / STICKY + 心跳清理
-- **delivery**: `ReliableDispatcher` — ACK 跟踪 + 指数退避重试 + **DLQ**(offset 仅 ACK 后前进,at-least-once)
-- **offset**: 内存 / **RocksDB**(崩溃恢复)/ Meta 两级
-- **push**: 长轮询 / **SSE** / ConnectionPushPump
-- **http**: `UniHttpServer`(新 `/events/*` + 旧 `/eventmesh/*`)、`UniWsServer`、`TlsContextFactory`
-- **transport.tcp** (12): `UniTcpServer` + netty pipeline — **旧 TCP 客户端零改动**
-- **transport.http**: `LegacyHttpBridge` — **旧 HTTP 客户端零改动**
-- **cluster** (15): `NacosMetaStore` + `ClusterMembership`(心跳租约) + `PartitionOwnership`(确定性分区 + gen fencing) + `ClusterCoordinator`(本地投递/跨实例 forward) → **多实例无重复**
-- **security**: FilterChain(Token / ACL / HMAC 签名),无 SPI
-- **ratelimit**: 令牌桶(每 topic,超额返回 429)
-- **metrics**: OTel(默认 Prometheus 导出)+ `UniTrace`
-- **connector**: `ConnectorScheduler`(runtime-push 动态调度连接器到 worker)
-- **admin**: 独立端口的 `/admin/*`(metrics / dlq replay / ratelimit / connectors…)
-
-# 测试情况
-
-## 单元/进程内测试(无 broker,跑在常规套件里) — 全绿
-
-- runtime:**38 个测试类 / 82 @Test**,刚跑 **0 failures**(8 skipped = 下面那些 gated IT)
-- sdk-java:29 @Test | connector-runtime:13 @Test
-- 合计 **~124 @Test**
-- 覆盖:subscription / offset(RocksDB 崩溃恢复) / delivery(ACK+nack+超时+DLQ+退避) / push / ratelimit / security / cluster / TLS / DLQ / 限流 / cluster-forward(2 实例 in-memory)/ 连接器调度 / 旧 TCP+HTTP 兼容
-
-## 真 broker/Nacos 集成测试(gated,已在多 broker 实测中验证 GREEN)
-
-- `-Dit.storage=rocketmq`(真 RocketMQ `127.0.0.1:9876`):
- - `RealBrokerIntegrationTest` ✅
- - `ClientBrokerIntegrationTest` ✅(**本次刚修绿** — pull 游标卡 offset 0 + 多 broker queue 寻址)
- - `LoadThroughputIntegrationTest` ✅(500 事件,0 丢失,0 重复)
- - `AckTimeoutRedeliveryIntegrationTest` ✅(ACK 超时→重投→DLQ)
- - `BrokerListTest` ✅
-- `-Dit.nacos`(真 RocketMQ + Nacos `127.0.0.1:8848`):
- - `LegacyTcpClusterBrokerIntegrationTest` ✅(旧 TCP SDK + 真 broker + Nacos)
- - `MultiInstanceRocketMqIntegrationTest` ✅(2 实例无重复)
- - `NacosClusterForwardIntegrationTest` ✅(跨实例订阅转发)
-
-## 已知待办(需环境/更大改动,已记录)
-
-- WebSocket netty 握手的服务端接线(SSE 通道已就绪)
-- 真 Meta MetaStore:etcd 适配(nacos 已完成)
-- Java 21 虚拟线程(项目源码级还是 Java 8)
-- Phase 8 物理删除旧 TCP/gRPC SDK(最终切换,需旧 runtime 全量回归)
-- `UniHttpServer.publish` 对 `RateLimitedException` 仍走 catch-all 返回 500(应为 429;测试靠 metrics 断言,未覆盖状态码)
-
-## 一句话
-
-新架构已是**可运行系统**,全部单元/进程内测试 + 7 个真 broker/Nacos 集成测试均 GREEN;老 TCP/HTTP 客户端零改动接入新核心。剩余项均为需环境或最终切换的收尾工作。
diff --git a/eventmesh-agent/build.gradle b/eventmesh-agent/build.gradle
new file mode 100644
index 000000000..17858d5e8
--- /dev/null
+++ b/eventmesh-agent/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Agent — 独立进程, 仅经 lite topic + CloudEvents 与 EventMesh Runtime 通信.
+// 不依赖 eventmesh-runtime, 复用 sdk-java (CloudEventsClient) + common (stream framing) + 一个
+// OpenAI 兼容的 LLM SSE client.
+dependencies {
+ implementation project(':eventmesh-sdks:eventmesh-sdk-java')
+ implementation 'com.fasterxml.jackson.core:jackson-databind'
+ implementation 'org.slf4j:slf4j-api'
+ runtimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl'
+
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+
+ testImplementation 'org.assertj:assertj-core'
+}
+
+// Dev runner: boots AgentApplication against a running runtime + an OpenAI-compatible LLM gateway.
+// Forwards eventmesh*/eventMesh*/llm.*/agent.* -D flags.
+task runAgent(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.agent.AgentApplication'
+ classpath = sourceSets.main.runtimeClasspath
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('llm.') || k.startsWith('agent.')
+ }
+ jvmArgs('-Xmx512m')
+}
diff --git a/eventmesh-agent/conf/agent.properties b/eventmesh-agent/conf/agent.properties
new file mode 100644
index 000000000..af96095e9
--- /dev/null
+++ b/eventmesh-agent/conf/agent.properties
@@ -0,0 +1,17 @@
+#
+# EventMesh agent configuration template.
+#
+# AgentApplication reads ALL config from -D system properties (NOT this file). bin/start-agent.sh
+# translates the ENV vars below into -D flags; pass extra -D flags through AGENT_OPTS.
+#
+# AGENT_RUNTIME_URL -> -Dagent.runtime.url (default http://localhost:8080)
+# AGENT_STREAM_PARENT -> -Dagent.stream.parent (default em-stream; must match the runtime's eventmesh.stream.parent)
+# AGENT_CLIENT_ID -> -Dagent.client.id (default em-agent-<ts>)
+# LLM_BASE_URL -> -Dllm.base.url (OpenAI-compatible; default https://api.openai.com)
+# LLM_API_KEY -> -Dllm.api.key (Bearer token; default "")
+# LLM_MODEL -> -Dllm.model (default gpt-4o-mini)
+#
+# Example (real LLM):
+# LLM_BASE_URL=https://api.openai.com LLM_API_KEY=sk-... LLM_MODEL=gpt-4o-mini \
+# AGENT_RUNTIME_URL=http://localhost:8080 ./bin/start-agent.sh
+#
diff --git a/eventmesh-agent/conf/log4j2.xml b/eventmesh-agent/conf/log4j2.xml
new file mode 100644
index 000000000..81c82618f
--- /dev/null
+++ b/eventmesh-agent/conf/log4j2.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ EventMesh agent logging. Discovered on the classpath (conf/log4j2.xml).
+-->
+<Configuration status="WARN" monitorInterval="60">
+ <Appenders>
+ <Console name="console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%t] %c{1} - %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="org.apache.eventmesh" level="INFO" additivity="false">
+ <AppenderRef ref="console"/>
+ </Logger>
+ <Root level="INFO">
+ <AppenderRef ref="console"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentApplication.java b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentApplication.java
new file mode 100644
index 000000000..5b25bbe0d
--- /dev/null
+++ b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentApplication.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent;
+
+import org.apache.eventmesh.agent.llm.OpenAiLlmClient;
+import org.apache.eventmesh.client.cloudevents.CloudEventsClient;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Agent process entry (v2). Boot sequence (§5.2): register with the runtime (get its agent-parent) →
+ * subscribe its {@code agent.<agentId>} channel - flip READY (ready-before-route) - heartbeat loop.
+ * All config via {@code -D} system properties.
+ *
+ * <p>Config keys: {@code agent.runtime.url}, {@code agent.id}, {@code agent.heartbeat.intervalMs},
+ * {@code agent.capacity}, {@code agent.conversation.maxHistory}, {@code llm.base.url},
+ * {@code llm.api.key}, {@code llm.model}.</p>
+ */
+@Slf4j
+public class AgentApplication {
+
+ public static void main(String[] args) throws Exception {
+ String runtimeUrl = System.getProperty("agent.runtime.url", "http://localhost:8080");
+ String agentId = System.getProperty("agent.id", "agent-" + Long.toString(System.currentTimeMillis(), 36));
+ String llmBase = System.getProperty("llm.base.url", "https://api.openai.com");
+ String llmKey = System.getProperty("llm.api.key", "");
+ String llmModel = System.getProperty("llm.model", "gpt-4o-mini");
+ final long heartbeatMs = Long.getLong("agent.heartbeat.intervalMs", 10_000L);
+ int maxHistory = Integer.getInteger("agent.conversation.maxHistory", 20);
+ int capacity = Integer.getInteger("agent.capacity", 100);
+
+ // Step 1: register (gets the assigned agent-parent + client-reply-parent)
+ AgentControlClient control = new AgentControlClient(runtimeUrl);
+ AgentControlClient.RegisterResult reg = control.register(agentId, List.of(llmModel), capacity);
+ String agentParent = reg.parent();
+ log.info("agent registered: agentId={} parent={} clientParent={}", agentId, agentParent, reg.clientParent());
+
+ // Step 2: subscribe the agent's channel
+ CloudEventsClient client = CloudEventsClient.builder().runtimeUrl(runtimeUrl).clientId(agentId).build();
+ OpenAiLlmClient llm = new OpenAiLlmClient(llmBase, llmKey, llmModel);
+ ConversationStore store = new ConversationStore(maxHistory);
+ StreamingAgent agent = new StreamingAgent(client, agentParent, agentId, llm, store);
+ agent.start(); // subscribe (agentParent, agent.<agentId>)
+
+ // Step 3: ready-before-route: only now is this agent eligible for matchmaking
+ control.ready(agentId);
+ log.info("agent READY: agentId={} (subscribed agent.{})", agentId, agentId);
+
+ // Step 4: heartbeat loop (refresh TTL + report load)
+ Thread heartbeatThread = Thread.startVirtualThread(() -> {
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ Thread.sleep(heartbeatMs);
+ control.heartbeat(agentId, agent.activeSessions());
+ } catch (InterruptedException ie) {
+ return;
+ } catch (Exception e) {
+ log.warn("heartbeat failed: {}", e.toString());
+ }
+ }
+ });
+
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ log.info("shutting down agent...");
+ heartbeatThread.interrupt();
+ try {
+ control.unregister(agentId);
+ } catch (Exception e) {
+ log.warn("unregister failed: {}", e.toString());
+ }
+ agent.shutdown();
+ client.shutdown();
+ }, "agent-shutdown"));
+
+ log.info("AgentApplication running: runtime={} agentId={} parent={} model={} (Ctrl+C to stop)",
+ runtimeUrl, agentId, agentParent, llmModel);
+ Thread.currentThread().join();
+ }
+}
diff --git a/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentControlClient.java b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentControlClient.java
new file mode 100644
index 000000000..40566980c
--- /dev/null
+++ b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/AgentControlClient.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Thin HTTP client for the runtime's agent control endpoints ({@code POST /agent/*}, §5.2). The agent
+ * is a "thin HTTP client of the runtime" — it never touches the broker or the MetaStore directly; this
+ * client drives its register → ready → heartbeat → unregister lifecycle.
+ */
+@Slf4j
+public class AgentControlClient {
+
+ private final String runtimeUrl;
+ private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ private final ObjectMapper json = new ObjectMapper();
+
+ public AgentControlClient(String runtimeUrl) {
+ this.runtimeUrl = runtimeUrl.replaceAll("/+$", "");
+ }
+
+ /** Register; returns the assigned agent-parent + client-reply-parent. */
+ public RegisterResult register(String agentId, List<String> capabilities, int capacity) {
+ ObjectNode body = json.createObjectNode().put("agentId", agentId).put("capacity", capacity);
+ ArrayNode caps = body.putArray("capabilities");
+ capabilities.forEach(caps::add);
+ JsonNode resp = post("/agent/register", body);
+ return new RegisterResult(resp.get("parent").asText(), resp.get("clientParent").asText());
+ }
+
+ public void ready(String agentId) {
+ // Retry: Nacos config propagation may lag the register's put by a few hundred ms — the
+ // immediate get in markReady can return null until it catches up.
+ RuntimeException last = null;
+ for (int attempt = 0; attempt < 5; attempt++) {
+ try {
+ post("/agent/ready", json.createObjectNode().put("agentId", agentId));
+ return;
+ } catch (RuntimeException e) {
+ last = e;
+ if (e.getMessage() != null && e.getMessage().contains("404") && attempt < 4) {
+ try {
+ Thread.sleep(500L);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw e;
+ }
+ continue;
+ }
+ throw e;
+ }
+ }
+ throw last;
+ }
+
+ public void heartbeat(String agentId, int activeSessions) {
+ post("/agent/heartbeat", json.createObjectNode().put("agentId", agentId).put("activeSessions", activeSessions));
+ }
+
+ public void unregister(String agentId) {
+ post("/agent/unregister", json.createObjectNode().put("agentId", agentId));
+ }
+
+ private JsonNode post(String path, JsonNode body) {
+ try {
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create(runtimeUrl + path))
+ .header("Content-Type", "application/json")
+ .timeout(Duration.ofSeconds(30))
+ .POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body)))
+ .build();
+ HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
+ if (resp.statusCode() >= 400) {
+ throw new RuntimeException("control " + path + " -> HTTP " + resp.statusCode() + ": " + resp.body());
+ }
+ return json.readTree(resp.body());
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("control " + path + " failed: " + e, e);
+ }
+ }
+
+ /** Assigned agent-parent + client-reply-parent. */
+ public record RegisterResult(String parent, String clientParent) {
+ }
+}
diff --git a/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/ConversationStore.java b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/ConversationStore.java
new file mode 100644
index 000000000..394ca67e6
--- /dev/null
+++ b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/ConversationStore.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory multi-turn conversation history keyed by {@code conversationId}. Each completed turn
+ * appends a {@code {user, assistant}} message pair; history is trimmed to the most recent
+ * {@code maxMessages} entries (sliding window) to bound LLM context size.
+ *
+ * <p>Lost on agent restart (MVP — persistence to Redis/DB is a TODO). Thread-safe per conversation
+ * (synchronized on the per-id list).</p>
+ */
+public class ConversationStore {
+
+ private final int maxMessages;
+ private final ConcurrentHashMap<String, List<Map<String, String>>> history = new ConcurrentHashMap<>();
+
+ public ConversationStore(int maxMessages) {
+ // keep at least one full turn (user + assistant)
+ this.maxMessages = Math.max(2, maxMessages);
+ }
+
+ /** Snapshot of the conversation history (empty list if convId null/unknown). Caller may mutate. */
+ public List<Map<String, String>> get(String conversationId) {
+ if (conversationId == null) {
+ return new ArrayList<>();
+ }
+ List<Map<String, String>> msgs = history.get(conversationId);
+ return msgs == null ? new ArrayList<>() : new ArrayList<>(msgs);
+ }
+
+ /** Append a completed turn (user prompt + assistant answer). No-op if convId is null. */
+ public void appendTurn(String conversationId, String userPrompt, String assistantAnswer) {
+ if (conversationId == null) {
+ return;
+ }
+ List<Map<String, String>> msgs = history.computeIfAbsent(conversationId,
+ k -> Collections.synchronizedList(new ArrayList<>()));
+ synchronized (msgs) {
+ msgs.add(message("user", userPrompt));
+ msgs.add(message("assistant", assistantAnswer));
+ while (msgs.size() > maxMessages) {
+ msgs.remove(0); // drop oldest → sliding window
+ }
+ }
+ }
+
+ private static Map<String, String> message(String role, String content) {
+ Map<String, String> m = new HashMap<>();
+ m.put("role", role);
+ m.put("content", content == null ? "" : content);
+ return m;
+ }
+}
diff --git a/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/StreamingAgent.java b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/StreamingAgent.java
new file mode 100644
index 000000000..acac3dac9
--- /dev/null
+++ b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/StreamingAgent.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent;
+
+import org.apache.eventmesh.agent.llm.OpenAiLlmClient;
+import org.apache.eventmesh.client.cloudevents.CloudEventsClient;
+import org.apache.eventmesh.common.stream.StreamChunk;
+import org.apache.eventmesh.common.stream.StreamRequest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Consumes streaming-call requests from its own {@code agent.<agentId>} lite (mode 1), calls the
+ * LLM, and publishes token chunks back to the request's {@code replyTo} (a {@code client.<clientId>}
+ * lite). Each request runs on its own virtual thread. Multi-turn history is keyed by
+ * {@code sessionId} (sessionId IS the conversation key).
+ *
+ * <p>Mode 2 (publish/subscribe) has no agent involvement — a gateway publishes chunks directly to a
+ * per-session lite topic; consumers subscribe via the runtime. This class is mode-1-only.</p>
+ */
+@Slf4j
+public class StreamingAgent {
+
+ private final CloudEventsClient client;
+ private final String agentParent;
+ private final String agentId;
+ private final OpenAiLlmClient llm;
+ private final ConversationStore store;
+ /** One virtual thread per in-flight stream. */
+ private final ExecutorService streamExecutor = Executors.newThreadPerTaskExecutor(
+ Thread.ofVirtual().name("em-agent-stream-", 1).factory());
+ private final AtomicInteger activeSessions = new AtomicInteger(0);
+
+ public StreamingAgent(CloudEventsClient client, String agentParent, String agentId,
+ OpenAiLlmClient llm, ConversationStore store) {
+ this.client = client;
+ this.agentParent = agentParent;
+ this.agentId = agentId;
+ this.llm = llm;
+ this.store = store;
+ }
+
+ /** Subscribe to this agent's channel; runs until {@link #shutdown()}. */
+ public void start() {
+ // Internal wire: the runtime publishes compact EventMeshFrame bytes (not CloudEvents) onto
+ // agent.<agentId>; decode each frame to a StreamRequest.
+ client.subscribeLiteBytes(agentParent, "agent." + agentId,
+ frame -> streamExecutor.submit(() -> handleRequest(frame)));
+ log.info("StreamingAgent subscribed (private-wire): parent={} lite=agent.{}", agentParent, agentId);
+ }
+
+ /** In-flight stream count, reported to the runtime via heartbeat. */
+ public int activeSessions() {
+ return activeSessions.get();
+ }
+
+ private void handleRequest(byte[] frame) {
+ StreamRequest req = org.apache.eventmesh.common.wire.WireCodecs.get().decodeRequest(frame);
+ String sessionId = req.getSessionId();
+ String replyTo = req.getReplyTo();
+ int[] seq = {0};
+ activeSessions.incrementAndGet();
+ log.info("stream request: sessionId={} replyTo={} model={}", sessionId, replyTo, req.getModel());
+ // Multi-turn: prepend conversation history (empty for a new sessionId), then this turn's prompt.
+ List<Map<String, String>> messages = store.get(sessionId);
+ messages.add(message("user", req.getPrompt()));
+ StringBuilder answer = new StringBuilder();
+ try {
+ llm.stream(messages, req.getModel(), token -> {
+ answer.append(token);
+ publish(replyTo, sessionId, seq, token, false, null);
+ });
+ publish(replyTo, sessionId, seq, "", true, null);
+ log.info("stream completed: sessionId={} chunks={}", sessionId, seq[0] - 1);
+ store.appendTurn(sessionId, req.getPrompt(), answer.toString());
+ } catch (Exception e) {
+ log.warn("stream failed: sessionId={} err={}", sessionId, e.toString());
+ publish(replyTo, sessionId, seq, "", true, "llm error: " + e.getMessage());
+ } finally {
+ activeSessions.decrementAndGet();
+ }
+ }
+
+ private void publish(String replyTo, String sessionId, int[] seq, String chunk, boolean done, String error) {
+ StreamChunk c = StreamChunk.builder()
+ .sessionId(sessionId).seq(seq[0]++).chunk(chunk).done(done).error(error).build();
+ // replyTo = "parent#lite" (client-parent#client.<clientId>); fall back to agentParent.
+ int hash = replyTo == null ? -1 : replyTo.indexOf('#');
+ String parent = hash >= 0 ? replyTo.substring(0, hash) : agentParent;
+ String lite = hash >= 0 ? replyTo.substring(hash + 1) : replyTo;
+ try {
+ // Encode the chunk via the WireCodec SPI and publish raw bytes over the internal wire.
+ byte[] frame = org.apache.eventmesh.common.wire.WireCodecs.get().encode(c);
+ if (!client.publishLiteBytes(parent, lite, frame)) {
+ log.warn("publishLiteBytes non-202: sessionId={} seq={} (runtime not lite-capable?)",
+ sessionId, c.getSeq());
+ }
+ } catch (Exception e) {
+ log.warn("publishLiteBytes failed: sessionId={} seq={} err={}", sessionId, c.getSeq(), e.toString());
+ }
+ }
+
+ private static Map<String, String> message(String role, String content) {
+ Map<String, String> m = new HashMap<>();
+ m.put("role", role);
+ m.put("content", content == null ? "" : content);
+ return m;
+ }
+
+ public void shutdown() {
+ streamExecutor.shutdownNow();
+ }
+}
diff --git a/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/llm/OpenAiLlmClient.java b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/llm/OpenAiLlmClient.java
new file mode 100644
index 000000000..bf2b71833
--- /dev/null
+++ b/eventmesh-agent/src/main/java/org/apache/eventmesh/agent/llm/OpenAiLlmClient.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent.llm;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Minimal OpenAI-compatible chat-completion streaming client. Issues {@code POST {base}/v1/chat/
+ * completions} with {@code stream:true} and {@code Authorization: Bearer <key>}, reads the SSE token
+ * stream and invokes {@code chunkCb} per {@code choices[0].delta.content}. Compatible with OpenAI,
+ * Azure-OpenAI, vLLM, Ollama (OpenAI mode), DeepSeek, Moonshot and most internal gateways. Blocking;
+ * intended to run on a virtual thread.
+ */
+@Slf4j
+public class OpenAiLlmClient {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final String baseUrl;
+ private final String apiKey;
+ private final String defaultModel;
+ private final java.net.http.HttpClient http;
+
+ public OpenAiLlmClient(String baseUrl, String apiKey, String defaultModel) {
+ this.baseUrl = baseUrl == null || baseUrl.trim().isEmpty() ? "https://api.openai.com" : baseUrl;
+ this.apiKey = apiKey == null ? "" : apiKey;
+ this.defaultModel = defaultModel;
+ this.http = java.net.http.HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+
+ /**
+ * Stream the completion for {@code prompt}; {@code chunkCb} receives each token fragment in
+ * order. Blocks until the server closes the stream ([DONE] or end-of-body).
+ *
+ * @param model overrides the default model when non-null
+ */
+ public void stream(String prompt, String model, Consumer<String> chunkCb) throws Exception {
+ java.util.List<java.util.Map<String, String>> msgs = new java.util.ArrayList<>();
+ java.util.Map<String, String> m = new java.util.HashMap<>();
+ m.put("role", "user");
+ m.put("content", prompt == null ? "" : prompt);
+ msgs.add(m);
+ stream(msgs, model, chunkCb);
+ }
+
+ /**
+ * Stream a completion for the given message list (supports multi-turn: pass prior history +
+ * the current user message). Each map must contain {@code role} + {@code content}.
+ */
+ public void stream(java.util.List<java.util.Map<String, String>> messages, String model,
+ Consumer<String> chunkCb) throws Exception {
+ String useModel = (model != null && !model.isEmpty()) ? model : defaultModel;
+ ObjectNode body = MAPPER.createObjectNode();
+ body.put("model", useModel);
+ ArrayNode msgs = body.putArray("messages");
+ for (java.util.Map<String, String> message : messages) {
+ ObjectNode msg = msgs.addObject();
+ msg.put("role", message.getOrDefault("role", "user"));
+ msg.put("content", message.getOrDefault("content", ""));
+ }
+ body.put("stream", true);
+
+ java.net.http.HttpRequest req = java.net.http.HttpRequest.newBuilder()
+ .uri(URI.create(trimSlash(this.baseUrl) + "/v1/chat/completions"))
+ .timeout(Duration.ofMinutes(3))
+ .header("Content-Type", "application/json")
+ .header("Accept", "text/event-stream")
+ .header("Authorization", "Bearer " + apiKey)
+ .POST(java.net.http.HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(body)))
+ .build();
+
+ java.net.http.HttpResponse<Stream<String>> resp =
+ http.send(req, java.net.http.HttpResponse.BodyHandlers.ofLines());
+ int status = resp.statusCode();
+ if (status != 200) {
+ throw new RuntimeException("LLM HTTP " + status + " (check llm.base.url / llm.api.key / llm.model)");
+ }
+ try (Stream<String> lines = resp.body()) {
+ lines.filter(l -> l.startsWith("data:")).forEach(line -> {
+ String payload = line.substring(5).trim();
+ if (payload.isEmpty() || "[DONE]".equals(payload)) {
+ return;
+ }
+ try {
+ JsonNode node = MAPPER.readTree(payload);
+ JsonNode choices = node.get("choices");
+ if (choices != null && choices.isArray() && choices.size() > 0) {
+ JsonNode delta = choices.get(0).get("delta");
+ if (delta != null && delta.has("content")) {
+ String content = delta.get("content").asText("");
+ if (!content.isEmpty()) {
+ chunkCb.accept(content);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.debug("skip unparseable SSE line: {}", payload);
+ }
+ });
+ }
+ }
+
+ private static String trimSlash(String url) {
+ return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
+ }
+}
diff --git a/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/ConversationStoreTest.java b/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/ConversationStoreTest.java
new file mode 100644
index 000000000..dabb5604d
--- /dev/null
+++ b/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/ConversationStoreTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+/** Covers ConversationStore get/append/trim + the null-convId single-turn case. */
+class ConversationStoreTest {
+
+ @Test
+ void nullConversationIsSingleTurn() {
+ ConversationStore store = new ConversationStore(20);
+ assertThat(store.get(null)).isEmpty();
+ store.appendTurn(null, "p", "a"); // no-op
+ assertThat(store.get(null)).isEmpty();
+ }
+
+ @Test
+ void appendsUserAssistantPairs() {
+ ConversationStore store = new ConversationStore(20);
+ store.appendTurn("c1", "hello", "hi");
+ store.appendTurn("c1", "how are you", "fine");
+ List<Map<String, String>> msgs = store.get("c1");
+ assertThat(msgs).hasSize(4);
+ assertThat(msgs).extracting(m -> m.get("role")).containsExactly("user", "assistant", "user", "assistant");
+ assertThat(msgs.get(0).get("content")).isEqualTo("hello");
+ assertThat(msgs.get(3).get("content")).isEqualTo("fine");
+ }
+
+ @Test
+ void getReturnsIndependentSnapshot() {
+ ConversationStore store = new ConversationStore(20);
+ store.appendTurn("c1", "p", "a");
+ List<Map<String, String>> snap = store.get("c1");
+ snap.clear(); // mutate the snapshot
+ assertThat(store.get("c1")).hasSize(2); // store unaffected
+ }
+
+ @Test
+ void trimsToSlidingWindow() {
+ ConversationStore store = new ConversationStore(4); // 2 turns kept
+ store.appendTurn("c1", "p1", "a1");
+ store.appendTurn("c1", "p2", "a2");
+ store.appendTurn("c1", "p3", "a3"); // 3 turns = 6 msgs → trim oldest
+ List<Map<String, String>> msgs = store.get("c1");
+ assertThat(msgs).hasSize(4); // trimmed to maxMessages
+ // oldest turn (p1/a1) dropped; only a2-turn and a3-turn kept
+ assertThat(msgs).extracting(m -> m.get("content"))
+ .containsExactly("p2", "a2", "p3", "a3");
+ }
+
+ @Test
+ void conversationsAreIsolated() {
+ ConversationStore store = new ConversationStore(20);
+ store.appendTurn("c1", "p", "a");
+ store.appendTurn("c2", "x", "y");
+ assertThat(store.get("c1")).hasSize(2);
+ assertThat(store.get("c2")).hasSize(2);
+ assertThat(store.get("c1").get(0).get("content")).isEqualTo("p");
+ assertThat(store.get("c2").get(0).get("content")).isEqualTo("x");
+ }
+}
diff --git a/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/llm/OpenAiLlmClientTest.java b/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/llm/OpenAiLlmClientTest.java
new file mode 100644
index 000000000..9655e1fd9
--- /dev/null
+++ b/eventmesh-agent/src/test/java/org/apache/eventmesh/agent/llm/OpenAiLlmClientTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.agent.llm;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+
+/**
+ * Drives {@link OpenAiLlmClient} against an in-process mock that speaks the OpenAI streaming SSE
+ * wire format (data: {choices:[{delta:{content}}]} ... data: [DONE]). Asserts ordered token
+ * delivery. Hermetic — no external LLM/key.
+ */
+class OpenAiLlmClientTest {
+
+ private HttpServer server;
+ private OpenAiLlmClient client;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.start();
+ String base = "http://127.0.0.1:" + server.getAddress().getPort();
+ client = new OpenAiLlmClient(base, "test-key", "test-model");
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void streamsOrderedDeltasUntilDone() throws Exception {
+ server.createContext("/v1/chat/completions", this::handleOk);
+
+ List<String> chunks = new ArrayList<>();
+ client.stream("hello", null, chunks::add);
+
+ assertThat(chunks).containsExactly("Hel", "lo", " ", "world");
+ }
+
+ @Test
+ void streamsWithMessageListSendsAllMessages() throws Exception {
+ List<JsonNode> captured = new ArrayList<>();
+ server.createContext("/v1/chat/completions", ex -> {
+ captured.add(new ObjectMapper().readTree(ex.getRequestBody().readAllBytes()));
+ handleOk(ex);
+ });
+
+ // multi-turn messages list (user/assistant/user)
+ List<Map<String, String>> messages = new ArrayList<>();
+ messages.add(message("user", "hi"));
+ messages.add(message("assistant", "hello"));
+ messages.add(message("user", "how are you"));
+ List<String> chunks = new ArrayList<>();
+ client.stream(messages, null, chunks::add);
+
+ // the full messages list was serialized into the request body, in order
+ assertThat(captured).hasSize(1);
+ JsonNode sent = captured.get(0).get("messages");
+ assertThat(sent).hasSize(3);
+ assertThat(sent.get(0).get("role").asText()).isEqualTo("user");
+ assertThat(sent.get(1).get("role").asText()).isEqualTo("assistant");
+ assertThat(sent.get(2).get("content").asText()).isEqualTo("how are you");
+ // and chunks still stream back
+ assertThat(chunks).containsExactly("Hel", "lo", " ", "world");
+ }
+
+ private static Map<String, String> message(String role, String content) {
+ Map<String, String> m = new HashMap<>();
+ m.put("role", role);
+ m.put("content", content);
+ return m;
+ }
+
+ @Test
+ void throwsOnNon200() {
+ server.createContext("/v1/chat/completions", this::handle500);
+
+ List<String> chunks = new ArrayList<>();
+ try {
+ client.stream("hello", null, chunks::add);
+ } catch (Exception e) {
+ assertThat(e).hasMessageContaining("LLM HTTP 500");
+ }
+ assertThat(chunks).isEmpty();
+ }
+
+ private void handleOk(HttpExchange exchange) throws IOException {
+ String body = String.join("\n",
+ "data: {\"choices\":[{\"delta\":{\"content\":\"Hel\"}}]}",
+ "",
+ "data: {\"choices\":[{\"delta\":{\"content\":\"lo\"}}]}",
+ "",
+ "data: {\"choices\":[{\"delta\":{\"content\":\" \"}}]}",
+ "",
+ "data: {\"choices\":[{\"delta\":{\"content\":\"world\"}}]}",
+ "",
+ "data: {\"choices\":[{\"delta\":{}}]}",
+ "",
+ "data: [DONE]",
+ "",
+ "");
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "text/event-stream");
+ exchange.sendResponseHeaders(200, bytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
+
+ private void handle500(HttpExchange exchange) throws IOException {
+ byte[] bytes = "{\"error\":\"upstream\"}".getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(500, bytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/ByteTransport.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/ByteTransport.java
new file mode 100644
index 000000000..c7e081825
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/ByteTransport.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.protocol;
+
+import java.util.Arrays;
+
+/**
+ * A {@link ProtocolTransportObject} wrapping a raw {@code byte[]} — used by protocol adaptors whose
+ * wire format is a byte blob (e.g. CloudEvents structured-JSON). This lets {@code FrameAdaptor}
+ * implementations stay typed against {@code ProtocolTransportObject} without inventing a new class
+ * per byte-carrying protocol.
+ */
+public final class ByteTransport implements ProtocolTransportObject {
+
+ private static final long serialVersionUID = 1L;
+
+ private final byte[] bytes;
+
+ public ByteTransport(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ @Override
+ public String toString() {
+ return "ByteTransport[" + (bytes == null ? 0 : bytes.length) + " bytes]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ByteTransport)) {
+ return false;
+ }
+ return Arrays.equals(bytes, ((ByteTransport) o).bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamChunk.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamChunk.java
new file mode 100644
index 000000000..8492e6f54
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamChunk.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.stream;
+
+import java.util.Map;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * One fragment in a streaming-call response (agent → runtime over the reply lite). {@link #seq} is
+ * monotonic and strictly ordered within a single-queue lite parent; the terminal chunk carries
+ * {@code done=true} ({@code error} non-null on failure).
+ *
+ * <p>{@link #eventType} and {@link #meta} are optional extensions for AgentScope-style
+ * multi-event streaming (thought, tool-call, structured output). The default OpenAI-compatible
+ * path only emits {@code eventType=text} (or {@code null} treated as text) with {@code chunk}
+ * as the text delta. The agentscope-harness path maps {@code AgentEvent} subtypes to distinct
+ * {@code eventType} values and serializes the full event JSON into {@code meta}.</p>
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StreamChunk {
+
+ /** Which session this chunk belongs to (matches the request's sessionId). */
+ private String sessionId;
+ /** Monotonic sequence (0,1,2,...). */
+ private int seq;
+ /** The token/text fragment. Empty on the terminal done chunk. */
+ private String chunk;
+ /** True on the terminal chunk (normal end or error). */
+ private boolean done;
+ /** Non-null only on a terminal error chunk. */
+ private String error;
+ /**
+ * Optional event type for multi-event streaming: {@code null|text|thought|tool|structured}.
+ * {@code null} is treated as {@code text}. Only the agentscope-harness path emits non-text
+ * types; the default OpenAI-compatible path only sets {@code chunk} (text delta).
+ */
+ private String eventType;
+ /**
+ * Optional passthrough metadata for AgentScope events. The agentscope-harness path serializes
+ * the full {@code AgentEvent} JSON here (thought content, tool name + arguments, structured
+ * result). The default OpenAI-compatible path leaves this null.
+ */
+ private Map<String, Object> meta;
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamEventCodec.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamEventCodec.java
new file mode 100644
index 000000000..509545514
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamEventCodec.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.stream;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * CloudEvent ↔ stream-DTO codec. Extension names are lowercase-alnum (CloudEvents spec allows a-z0-9
+ * only — no hyphens), matching the {@code emcorrelationid} / {@code emtenantid} convention. Both
+ * request and chunk travel as structured CloudEvents (JSON on the wire) over lite topics; the
+ * variable payload rides in {@code data} as UTF-8 text, the structured fields as extensions.
+ */
+public final class StreamEventCodec {
+
+ public static final String CE_TYPE_REQUEST = "stream.request";
+ public static final String CE_TYPE_CHUNK = "stream.chunk";
+
+ public static final String EXT_SESSION_ID = "emsessionid";
+ public static final String EXT_REPLY_TO = "emreplyto";
+ public static final String EXT_SEQ = "emseq";
+ public static final String EXT_DONE = "emdone";
+ public static final String EXT_ERROR = "emerror";
+ public static final String EXT_MODEL = "emmodel";
+ public static final String EXT_CONVERSATION_ID = "emconvid";
+ /** Optional: {@code null|text|thought|tool|structured} (null treated as text). */
+ public static final String EXT_EVENT_TYPE = "emeventtype";
+ /** Optional: passthrough metadata, serialized as a JSON string. */
+ public static final String EXT_META = "emmeta";
+
+ /** Reused for {@link #EXT_META} JSON-string serialization (meta is a free-form map). */
+ private static final ObjectMapper META_MAPPER = new ObjectMapper();
+ private static final TypeReference<Map<String, Object>> META_TYPE = new TypeReference<Map<String, Object>>() {
+ };
+
+ private StreamEventCodec() {
+ }
+
+ // -------------------- encode (all overloads grouped) --------------------
+
+ /** Encode a streaming-call request → CloudEvent (data = prompt, text/plain). */
+ public static CloudEvent toEvent(StreamRequest req) {
+ CloudEventBuilder b = CloudEventBuilder.v1()
+ .withId(UUID.randomUUID().toString())
+ .withSource(URI.create("em://stream/" + req.getSessionId()))
+ .withType(CE_TYPE_REQUEST)
+ .withDataContentType("text/plain")
+ .withData((req.getPrompt() == null ? "" : req.getPrompt()).getBytes(StandardCharsets.UTF_8))
+ .withExtension(EXT_SESSION_ID, req.getSessionId())
+ .withExtension(EXT_REPLY_TO, req.getReplyTo());
+ if (req.getModel() != null) {
+ b.withExtension(EXT_MODEL, req.getModel());
+ }
+ if (req.getConversationId() != null) {
+ b.withExtension(EXT_CONVERSATION_ID, req.getConversationId());
+ }
+ return b.build();
+ }
+
+ /** Encode a response chunk → CloudEvent (data = chunk text, text/plain). */
+ public static CloudEvent toEvent(StreamChunk chunk) {
+ CloudEventBuilder b = CloudEventBuilder.v1()
+ .withId(chunk.getSessionId() + "-" + chunk.getSeq())
+ .withSource(URI.create("em://stream/" + chunk.getSessionId()))
+ .withType(CE_TYPE_CHUNK)
+ .withDataContentType("text/plain")
+ .withData((chunk.getChunk() == null ? "" : chunk.getChunk()).getBytes(StandardCharsets.UTF_8))
+ .withExtension(EXT_SESSION_ID, chunk.getSessionId())
+ .withExtension(EXT_SEQ, chunk.getSeq())
+ .withExtension(EXT_DONE, chunk.isDone());
+ if (chunk.getError() != null) {
+ b.withExtension(EXT_ERROR, chunk.getError());
+ }
+ if (chunk.getEventType() != null) {
+ b.withExtension(EXT_EVENT_TYPE, chunk.getEventType());
+ }
+ if (chunk.getMeta() != null && !chunk.getMeta().isEmpty()) {
+ try {
+ b.withExtension(EXT_META, META_MAPPER.writeValueAsString(chunk.getMeta()));
+ } catch (JsonProcessingException ignored) {
+ // best-effort: skip meta if it can't be serialized
+ }
+ }
+ return b.build();
+ }
+
+ // -------------------- decode --------------------
+
+ public static StreamRequest requestFromEvent(CloudEvent event) {
+ return StreamRequest.builder()
+ .sessionId(str(event, EXT_SESSION_ID))
+ .replyTo(str(event, EXT_REPLY_TO))
+ .model(str(event, EXT_MODEL))
+ .conversationId(str(event, EXT_CONVERSATION_ID))
+ .prompt(textData(event))
+ .build();
+ }
+
+ public static StreamChunk chunkFromEvent(CloudEvent event) {
+ Object seqVal = event.getExtension(EXT_SEQ);
+ return StreamChunk.builder()
+ .sessionId(str(event, EXT_SESSION_ID))
+ .seq(seqVal == null ? 0 : ((Number) seqVal).intValue())
+ .chunk(textData(event))
+ .done(parseBool(event.getExtension(EXT_DONE)))
+ .error(str(event, EXT_ERROR))
+ .eventType(str(event, EXT_EVENT_TYPE))
+ .meta(parseMeta(event.getExtension(EXT_META)))
+ .build();
+ }
+
+ // -------------------- helpers --------------------
+
+ private static String textData(CloudEvent event) {
+ return event.getData() != null ? new String(event.getData().toBytes(), StandardCharsets.UTF_8) : "";
+ }
+
+ private static String str(CloudEvent event, String ext) {
+ Object v = event.getExtension(ext);
+ return v == null ? null : v.toString();
+ }
+
+ private static boolean parseBool(Object v) {
+ if (v == null) {
+ return false;
+ }
+ if (v instanceof Boolean) {
+ return (Boolean) v;
+ }
+ return Boolean.parseBoolean(v.toString());
+ }
+
+ /** Parse the {@code emmeta} extension (a JSON string) back into a map; null/invalid → null. */
+ private static Map<String, Object> parseMeta(Object v) {
+ if (v == null) {
+ return null;
+ }
+ String json = v.toString();
+ if (json.isEmpty()) {
+ return null;
+ }
+ try {
+ Map<String, Object> parsed = META_MAPPER.readValue(json, META_TYPE);
+ return parsed == null || parsed.isEmpty() ? null : parsed;
+ } catch (JsonProcessingException ignored) {
+ // best-effort: leave meta null if it can't be parsed
+ return null;
+ }
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamRequest.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamRequest.java
new file mode 100644
index 000000000..b32efc3e0
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/stream/StreamRequest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.stream;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * A streaming-call request (runtime → agent over the agent's lite channel). The agent consumes this,
+ * calls the LLM, and publishes {@link StreamChunk}s back to {@link #replyTo}.
+ *
+ * <p>v2: {@link #sessionId} is {@code <agentId>:<uuid>} — the routing key (runtime parses the
+ * {@code :} prefix for zero-lookup routing) AND the agent's conversation-context key. {@link #replyTo}
+ * is the reply address ({@code parent#lite}). {@link #conversationId} is v1 legacy (v2 keys multi-turn
+ * by sessionId) — kept temporarily for the v1 path.</p>
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StreamRequest {
+
+ /** Session id ({@code <agentId>:<uuid>} in v2); routing + conversation key. */
+ private String sessionId;
+ /** Reply address the agent publishes response chunks to (v2: {@code parent#lite}; v1 legacy: lite name). */
+ private String replyTo;
+ /** The user prompt. */
+ private String prompt;
+ /** The LLM model id (optional; agent default applies if null). */
+ private String model;
+ /**
+ * Conversation id for multi-turn chat (v1 legacy, optional). v2 keys multi-turn by sessionId;
+ * this field is retained only until the v1 path is removed. Null/absent = single-turn.
+ */
+ private String conversationId;
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrame.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrame.java
new file mode 100644
index 000000000..8770c7d6f
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrame.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import org.apache.eventmesh.common.stream.StreamChunk;
+import org.apache.eventmesh.common.stream.StreamRequest;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+
+/**
+ * The single internal wire format for EventMesh — one frame, two message families. Used on every
+ * internal path: streaming session/subscribe (runtime↔agent over lite topics), normal pub/sub
+ * (runtime↔storage MQ), and cross-instance forwarding. The PUBLIC surface (HTTP/SSE/TCP/gRPC/A2A)
+ * stays CloudEvents; the runtime converts at the edge (CE → EventMeshFrame on ingress,
+ * EventMeshFrame → CE on egress). See {@code docs/eventmesh-architecture-refinement.md}.
+ *
+ * <p>This unifies the former {@code PrivateFrame} (streaming-only, fixed sessionId/seq fields) and
+ * {@code CloudEventEnvelope} (event-only, all-KV) into one format whose fixed header serves the
+ * streaming hot path while a generic KV section carries arbitrary named attributes for events.
+ *
+ * <h3>Wire layout</h3>
+ * <pre>
+ * fixed header (14 bytes, big-endian):
+ * [magic:1] = 0xEF ('E'+'F' sentinel)
+ * [ver:1] = 1
+ * [msgType:1] = 1 STREAM_REQ | 2 STREAM_CHUNK | 3 EVENT
+ * [flags:1] = bit0 done | bit1 hasError | bit2 hasMeta | bit3..7 reserved
+ * [seq:4] = stream sequence (STREAM_CHUNK only; 0 otherwise)
+ * [keyCount:2] = number of KV attribute entries
+ * [dataLen:4] = data byte length
+ *
+ * KV attributes (keyCount × ):
+ * [nameLen:2][name] UTF-8
+ * [valLen:4][value] UTF-8 (toString of the source value)
+ *
+ * data payload:
+ * [data: dataLen] raw bytes (chunk text / prompt / CloudEvent data)
+ * </pre>
+ *
+ * <p><b>STREAM_REQ</b> uses KV {@code replyTo/model/conversationId} + data=prompt.
+ * <b>STREAM_CHUNK</b> uses {@code seq} (fixed) + {@code done} (flag) + KV
+ * {@code sessionId/eventType/error/meta} + data=chunk text. <b>EVENT</b> uses KV
+ * {@code id/type/subject/time/emttl/emcorrelationid/...} (whatever the CloudEvent carried) + data.
+ * On decode, EVENT KV names that match standard CE attributes are rebuilt via the right builder
+ * methods; everything else becomes an extension.</p>
+ */
+public final class EventMeshFrame {
+
+ public static final int MAGIC = 0xEF;
+ public static final int VERSION = 1;
+ public static final int TYPE_STREAM_REQ = 1;
+ public static final int TYPE_STREAM_CHUNK = 2;
+ public static final int TYPE_EVENT = 3;
+
+ // flag bits
+ static final int FLAG_DONE = 0x01;
+ static final int FLAG_HAS_ERROR = 0x02;
+ static final int FLAG_HAS_META = 0x04;
+
+ private static final int HEADER_LEN = 14; // magic(1)+ver(1)+msgType(1)+flags(1)+seq(4)+keyCount(2)+dataLen(4)
+
+ // well-known KV keys (kept short to save bytes on the wire)
+ static final String K_SESSION_ID = "sid";
+ static final String K_REPLY_TO = "replyTo";
+ static final String K_MODEL = "model";
+ static final String K_CONVERSATION_ID = "conv";
+ static final String K_EVENT_TYPE = "etype";
+ static final String K_ERROR = "err";
+ static final String K_META = "meta";
+
+ private final int msgType;
+ private final int flags;
+ private final int seq;
+ private final Map<String, String> attrs;
+ private final byte[] data;
+
+ EventMeshFrame(int msgType, int flags, int seq, Map<String, String> attrs, byte[] data) {
+ this.msgType = msgType;
+ this.flags = flags;
+ this.seq = seq;
+ this.attrs = attrs;
+ this.data = data;
+ }
+
+ // -------------------- factories: streaming --------------------
+
+ public static EventMeshFrame fromRequest(StreamRequest req) {
+ Map<String, String> a = new LinkedHashMap<>();
+ String sid = req.getSessionId() == null ? "" : req.getSessionId();
+ a.put(K_SESSION_ID, sid);
+ if (req.getReplyTo() != null && !req.getReplyTo().isEmpty()) {
+ a.put(K_REPLY_TO, req.getReplyTo());
+ }
+ if (req.getModel() != null && !req.getModel().isEmpty()) {
+ a.put(K_MODEL, req.getModel());
+ }
+ if (req.getConversationId() != null && !req.getConversationId().isEmpty()) {
+ a.put(K_CONVERSATION_ID, req.getConversationId());
+ }
+ byte[] data = utf8(req.getPrompt() == null ? "" : req.getPrompt());
+ return new EventMeshFrame(TYPE_STREAM_REQ, 0, 0, a, data);
+ }
+
+ public static EventMeshFrame fromChunk(StreamChunk c) {
+ Map<String, String> a = new LinkedHashMap<>();
+ a.put(K_SESSION_ID, c.getSessionId() == null ? "" : c.getSessionId());
+ int flags = c.isDone() ? FLAG_DONE : 0;
+ if (c.getError() != null && !c.getError().isEmpty()) {
+ flags |= FLAG_HAS_ERROR;
+ a.put(K_ERROR, c.getError());
+ }
+ if (c.getEventType() != null && !c.getEventType().isEmpty()) {
+ a.put(K_EVENT_TYPE, c.getEventType());
+ }
+ if (c.getMeta() != null && !c.getMeta().isEmpty()) {
+ flags |= FLAG_HAS_META;
+ a.put(K_META, MetaJson.stringify(c.getMeta()));
+ }
+ byte[] data = utf8(c.getChunk() == null ? "" : c.getChunk());
+ return new EventMeshFrame(TYPE_STREAM_CHUNK, flags, c.getSeq(), a, data);
+ }
+
+ // -------------------- factories: CloudEvent (normal pub/sub) --------------------
+
+ /** Build an EVENT frame from pre-mapped attributes + raw data (used by protocol adaptors that
+ * map a non-CloudEvents protocol directly, e.g. MeshMessage). */
+ public static EventMeshFrame event(Map<String, String> attrs, byte[] data) {
+ return new EventMeshFrame(TYPE_EVENT, 0, 0, attrs, data);
+ }
+
+ /** Capture an arbitrary {@link CloudEvent} as an EVENT frame (all attributes + extensions + data). */
+ public static EventMeshFrame fromCloudEvent(CloudEvent event) {
+ Map<String, String> a = new LinkedHashMap<>();
+ for (String name : event.getAttributeNames()) {
+ Object v = event.getAttribute(name);
+ if (v != null) {
+ a.put(name, v.toString());
+ }
+ }
+ for (String name : event.getExtensionNames()) {
+ Object v = event.getExtension(name);
+ if (v != null) {
+ a.put(name, v.toString());
+ }
+ }
+ byte[] data = event.getData() != null ? event.getData().toBytes() : new byte[0];
+ return new EventMeshFrame(TYPE_EVENT, 0, 0, a, data);
+ }
+
+ /** Reconstruct the {@link CloudEvent} (EVENT frame → attributes + extensions + data). */
+ public CloudEvent toCloudEvent() {
+ if (msgType != TYPE_EVENT) {
+ throw new IllegalStateException("not an EVENT frame");
+ }
+ CloudEventBuilder b = CloudEventBuilder.v1();
+ for (Map.Entry<String, String> e : attrs.entrySet()) {
+ applyCe(b, e.getKey(), e.getValue());
+ }
+ if (data.length > 0) {
+ b.withData(data);
+ }
+ return b.build();
+ }
+
+ private static void applyCe(CloudEventBuilder b, String name, String value) {
+ // specversion is implied (v1 builder → V1); datacontentencoding was removed in CE 3.0.
+ switch (name) {
+ case "specversion":
+ return; // builder defaults to V1.0
+ case "id":
+ b.withId(value);
+ return;
+ case "source":
+ b.withSource(URI.create(value));
+ return;
+ case "type":
+ b.withType(value);
+ return;
+ case "datacontenttype":
+ b.withDataContentType(value);
+ return;
+ case "subject":
+ b.withSubject(value);
+ return;
+ case "time":
+ b.withTime(OffsetDateTime.parse(value));
+ return;
+ case "dataschema":
+ b.withDataSchema(URI.create(value));
+ return;
+ case "datacontentencoding":
+ return; // removed in CE 3.0; drop silently
+ default:
+ b.withExtension(name, value);
+ }
+ }
+
+ // -------------------- decode back to streaming DTOs --------------------
+
+ public StreamRequest toStreamRequest() {
+ if (msgType != TYPE_STREAM_REQ) {
+ throw new IllegalStateException("not a STREAM_REQ frame");
+ }
+ return StreamRequest.builder()
+ .sessionId(attrs.get(K_SESSION_ID))
+ .replyTo(attrs.get(K_REPLY_TO))
+ .model(attrs.get(K_MODEL))
+ .conversationId(attrs.get(K_CONVERSATION_ID))
+ .prompt(str(data))
+ .build();
+ }
+
+ public StreamChunk toChunk() {
+ if (msgType != TYPE_STREAM_CHUNK) {
+ throw new IllegalStateException("not a STREAM_CHUNK frame");
+ }
+ return StreamChunk.builder()
+ .sessionId(attrs.get(K_SESSION_ID))
+ .seq(seq)
+ .chunk(str(data))
+ .done((flags & FLAG_DONE) != 0)
+ .error(attrs.get(K_ERROR))
+ .eventType(attrs.get(K_EVENT_TYPE))
+ .meta(attrs.containsKey(K_META) ? MetaJson.parse(attrs.get(K_META)) : null)
+ .build();
+ }
+
+ // -------------------- encode / decode --------------------
+
+ public byte[] encode() {
+ byte[][] nameBytes = new byte[attrs.size()][];
+ byte[][] valBytes = new byte[attrs.size()][];
+ int kvLen = 0;
+ int i = 0;
+ for (Map.Entry<String, String> e : attrs.entrySet()) {
+ nameBytes[i] = utf8(e.getKey());
+ valBytes[i] = utf8(e.getValue());
+ kvLen += 2 + nameBytes[i].length + 4 + valBytes[i].length;
+ i++;
+ }
+ ByteBuffer buf = ByteBuffer.allocate(HEADER_LEN + kvLen + data.length);
+ buf.put((byte) MAGIC);
+ buf.put((byte) VERSION);
+ buf.put((byte) msgType);
+ buf.put((byte) flags);
+ buf.putInt(seq);
+ buf.putShort((short) attrs.size());
+ buf.putInt(data.length);
+ for (int j = 0; j < nameBytes.length; j++) {
+ buf.putShort((short) nameBytes[j].length);
+ buf.put(nameBytes[j]);
+ buf.putInt(valBytes[j].length);
+ buf.put(valBytes[j]);
+ }
+ buf.put(data);
+ return buf.array();
+ }
+
+ public static EventMeshFrame decode(byte[] bytes) {
+ return decode(bytes, 0, bytes.length);
+ }
+
+ public static EventMeshFrame decode(byte[] bytes, int offset, int length) {
+ if (length < HEADER_LEN) {
+ throw new IllegalArgumentException("frame too short: " + length);
+ }
+ ByteBuffer buf = ByteBuffer.wrap(bytes, offset, length);
+ int magic = buf.get() & 0xFF;
+ if (magic != MAGIC) {
+ throw new IllegalArgumentException("bad frame magic: 0x" + Integer.toHexString(magic));
+ }
+ int ver = buf.get() & 0xFF;
+ if (ver != VERSION) {
+ throw new IllegalArgumentException("unsupported frame version: " + ver);
+ }
+ int msgType = buf.get() & 0xFF;
+ int flags = buf.get() & 0xFF;
+ int seq = buf.getInt();
+ int keyCount = buf.getShort() & 0xFFFF;
+ int dataLen = buf.getInt();
+ Map<String, String> attrs = new LinkedHashMap<>(keyCount);
+ for (int i = 0; i < keyCount; i++) {
+ String name = getString(buf, buf.getShort() & 0xFFFF);
+ String value = getString(buf, buf.getInt());
+ attrs.put(name, value);
+ }
+ byte[] data = new byte[dataLen];
+ buf.get(data);
+ return new EventMeshFrame(msgType, flags, seq, attrs, data);
+ }
+
+ // -------------------- accessors --------------------
+
+ public int msgType() {
+ return msgType;
+ }
+
+ public boolean isStreamRequest() {
+ return msgType == TYPE_STREAM_REQ;
+ }
+
+ public boolean isStreamChunk() {
+ return msgType == TYPE_STREAM_CHUNK;
+ }
+
+ public boolean isEvent() {
+ return msgType == TYPE_EVENT;
+ }
+
+ public boolean isDone() {
+ return (flags & FLAG_DONE) != 0;
+ }
+
+ public Map<String, String> attributes() {
+ return attrs;
+ }
+
+ public byte[] data() {
+ return data;
+ }
+
+ // -------------------- helpers --------------------
+
+ private static byte[] utf8(String s) {
+ return s == null ? new byte[0] : s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String str(byte[] b) {
+ return new String(b, StandardCharsets.UTF_8);
+ }
+
+ private static String getString(ByteBuffer buf, int len) {
+ byte[] b = new byte[len];
+ buf.get(b);
+ return new String(b, StandardCharsets.UTF_8);
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrameCodec.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrameCodec.java
new file mode 100644
index 000000000..2b352fa02
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/EventMeshFrameCodec.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import org.apache.eventmesh.common.stream.StreamChunk;
+import org.apache.eventmesh.common.stream.StreamRequest;
+
+import io.cloudevents.CloudEvent;
+
+/**
+ * Default {@link WireCodec} — delegates to the compact {@link EventMeshFrame} binary format (the
+ * single internal wire format since the architecture refinement). This is the codec
+ * {@link WireCodecs#get()} loads when no override is configured.
+ *
+ * <p>Alternative codecs (CE-protobuf, binary CE) implement {@link WireCodec} and are selected via
+ * {@code -Deventmesh.wire.codec=<fqcn>}.</p>
+ */
+public class EventMeshFrameCodec implements WireCodec {
+
+ @Override
+ public byte[] encode(StreamRequest request) {
+ return EventMeshFrame.fromRequest(request).encode();
+ }
+
+ @Override
+ public byte[] encode(StreamChunk chunk) {
+ return EventMeshFrame.fromChunk(chunk).encode();
+ }
+
+ @Override
+ public byte[] encode(CloudEvent event) {
+ return EventMeshFrame.fromCloudEvent(event).encode();
+ }
+
+ @Override
+ public StreamRequest decodeRequest(byte[] bytes) {
+ return EventMeshFrame.decode(bytes).toStreamRequest();
+ }
+
+ @Override
+ public StreamChunk decodeChunk(byte[] bytes) {
+ return EventMeshFrame.decode(bytes).toChunk();
+ }
+
+ @Override
+ public CloudEvent decodeEvent(byte[] bytes) {
+ return EventMeshFrame.decode(bytes).toCloudEvent();
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MeshMessageFrameCodec.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MeshMessageFrameCodec.java
new file mode 100644
index 000000000..3e1b736c7
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MeshMessageFrameCodec.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import org.apache.eventmesh.common.Constants;
+import org.apache.eventmesh.common.protocol.tcp.EventMeshMessage;
+import org.apache.eventmesh.common.protocol.tcp.Header;
+
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Direct {@link EventMeshFrame} ↔ {@link EventMeshMessage} conversion — the legacy TCP
+ * {@code MeshMessage} protocol maps straight to the internal frame, without going through
+ * CloudEvent as an intermediary (CloudEvent and MeshMessage are peer external protocols; neither
+ * converts to the other). Used by the legacy TCP ingress/egress ({@code MeshMessagePackageRouter},
+ * {@code NettyTcpPushChannel}) so a MeshMessage client's path is MeshMessage → Frame (ingress) and
+ * Frame → MeshMessage (egress), mirroring how the CloudEvents path is CloudEvents → Frame directly.
+ *
+ * <p>Field mapping:<ul>
+ * <li>{@code message.topic} ↔ frame attribute {@code subject} (the routing key both protocols use)</li>
+ * <li>{@code message.body} (content string) ↔ frame {@code data} (UTF-8 bytes)</li>
+ * <li>{@code message.properties} + {@code message.headers} ↔ frame KV attributes (merged)</li>
+ * <li>{@code header.seq} ↔ frame attribute {@code id} (the message id)</li>
+ * <li>frame {@code type} = {@code eventmeshmessage} (marks the origin protocol)</li>
+ * </ul></p>
+ */
+public final class MeshMessageFrameCodec {
+
+ /** Attribute marking a frame built from a MeshMessage (so egress knows to convert back to MeshMessage). */
+ public static final String ATTR_ORIGIN_PROTOCOL = "emorigin";
+ public static final String ORIGIN_MESH_MESSAGE = "meshmessage";
+
+ private MeshMessageFrameCodec() {
+ }
+
+ /** Build an EventMeshFrame from a legacy TCP MeshMessage + its header. */
+ public static EventMeshFrame fromMeshMessage(Header header, EventMeshMessage message) {
+ Map<String, String> attrs = new LinkedHashMap<>();
+ attrs.put("specversion", "1.0");
+ attrs.put("type", "eventmeshmessage");
+ attrs.put("source", "/"); // required CE field (CloudEventBuilder needs it if anyone calls toCloudEvent)
+ attrs.put(ATTR_ORIGIN_PROTOCOL, ORIGIN_MESH_MESSAGE);
+ if (header != null && header.getSeq() != null) {
+ attrs.put("id", header.getSeq());
+ }
+ if (message.getTopic() != null) {
+ attrs.put("subject", message.getTopic());
+ }
+ // message-level properties + headers merge into the frame's KV section.
+ if (message.getProperties() != null) {
+ attrs.putAll(message.getProperties());
+ }
+ if (message.getHeaders() != null) {
+ attrs.putAll(message.getHeaders());
+ }
+ byte[] data = message.getBody() == null ? new byte[0]
+ : message.getBody().getBytes(Constants.DEFAULT_CHARSET);
+ return new EventMeshFrame(EventMeshFrame.TYPE_EVENT, 0, 0, attrs, data);
+ }
+
+ /** Reconstruct a legacy TCP MeshMessage (in a {@link org.apache.eventmesh.common.protocol.tcp.Package})
+ * from an EventMeshFrame that originated as a MeshMessage. */
+ public static org.apache.eventmesh.common.protocol.tcp.Package toMeshMessagePackage(EventMeshFrame frame) {
+ Map<String, String> attrs = frame.attributes();
+ EventMeshMessage message = new EventMeshMessage();
+ message.setTopic(attrs.get("subject"));
+ message.setBody(frame.data() == null ? "" : new String(frame.data(), StandardCharsets.UTF_8));
+
+ Map<String, String> props = new LinkedHashMap<>();
+ // Carry the remaining attributes as message properties (skip the CE-shaped + origin markers).
+ for (Map.Entry<String, String> e : attrs.entrySet()) {
+ String k = e.getKey();
+ if ("type".equals(k) || ATTR_ORIGIN_PROTOCOL.equals(k) || "id".equals(k) || "subject".equals(k)
+ || "source".equals(k) || "specversion".equals(k) || "time".equals(k) || "dataschema".equals(k)
+ || "datacontenttype".equals(k)) {
+ continue;
+ }
+ props.put(k, e.getValue());
+ }
+ message.setProperties(props);
+
+ org.apache.eventmesh.common.protocol.tcp.Package pkg = new org.apache.eventmesh.common.protocol.tcp.Package();
+ pkg.setBody(message);
+ return pkg;
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MetaJson.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MetaJson.java
new file mode 100644
index 000000000..11365eb61
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/MetaJson.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Minimal JSON codec for the streaming {@code meta} map (string/number/bool/null + nested values),
+ * kept dependency-free so {@code eventmesh-common}'s wire package doesn't pull Jackson. Used by
+ * {@link EventMeshFrame} for the {@code meta} attribute on stream chunks.
+ */
+final class MetaJson {
+
+ private MetaJson() {
+ }
+
+ static String stringify(Map<String, Object> meta) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ boolean first = true;
+ for (Map.Entry<String, Object> e : meta.entrySet()) {
+ if (!first) {
+ sb.append(',');
+ }
+ first = false;
+ quote(sb, e.getKey());
+ sb.append(':');
+ value(sb, e.getValue());
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ static Map<String, Object> parse(String json) {
+ if (json == null || json.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ try {
+ return new Parser(json).parseObject();
+ } catch (Exception ex) {
+ return Collections.emptyMap();
+ }
+ }
+
+ private static void quote(StringBuilder sb, String s) {
+ sb.append('"').append(escape(s)).append('"');
+ }
+
+ private static String escape(String s) {
+ StringBuilder out = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '"':
+ out.append("\\\"");
+ break;
+ case '\\':
+ out.append("\\\\");
+ break;
+ case '\n':
+ out.append("\\n");
+ break;
+ case '\r':
+ out.append("\\r");
+ break;
+ case '\t':
+ out.append("\\t");
+ break;
+ default:
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ private static void value(StringBuilder sb, Object v) {
+ if (v == null) {
+ sb.append("null");
+ } else if (v instanceof Number || v instanceof Boolean) {
+ sb.append(v);
+ } else {
+ quote(sb, v.toString());
+ }
+ }
+
+ private static final class Parser {
+ private final String src;
+ private int idx;
+
+ Parser(String src) {
+ this.src = src;
+ }
+
+ private void skipWs() {
+ while (idx < src.length() && Character.isWhitespace(src.charAt(idx))) {
+ idx++;
+ }
+ }
+
+ Map<String, Object> parseObject() {
+ Map<String, Object> map = new LinkedHashMap<>();
+ skipWs();
+ if (idx >= src.length() || src.charAt(idx) != '{') {
+ return map;
+ }
+ idx++;
+ skipWs();
+ if (idx < src.length() && src.charAt(idx) == '}') {
+ return map;
+ }
+ while (idx < src.length()) {
+ skipWs();
+ String key = parseString();
+ skipWs();
+ if (idx < src.length() && src.charAt(idx) == ':') {
+ idx++;
+ }
+ Object val = parseValue();
+ map.put(key, val);
+ skipWs();
+ if (idx < src.length() && src.charAt(idx) == ',') {
+ idx++;
+ } else {
+ break;
+ }
+ }
+ return map;
+ }
+
+ private Object parseValue() {
+ skipWs();
+ if (idx >= src.length()) {
+ return null;
+ }
+ char c = src.charAt(idx);
+ if (c == '"') {
+ return parseString();
+ }
+ if (c == '{') {
+ return parseObject();
+ }
+ if (c == 't' || c == 'f' || c == 'n' || c == '-' || (c >= '0' && c <= '9')) {
+ return parseLiteral();
+ }
+ return null;
+ }
+
+ private String parseString() {
+ StringBuilder out = new StringBuilder();
+ if (idx >= src.length() || src.charAt(idx) != '"') {
+ return "";
+ }
+ idx++;
+ while (idx < src.length()) {
+ char c = src.charAt(idx++);
+ if (c == '"') {
+ break;
+ }
+ if (c == '\\' && idx < src.length()) {
+ char e = src.charAt(idx++);
+ switch (e) {
+ case '"':
+ out.append('"');
+ break;
+ case '\\':
+ out.append('\\');
+ break;
+ case 'n':
+ out.append('\n');
+ break;
+ case 'r':
+ out.append('\r');
+ break;
+ case 't':
+ out.append('\t');
+ break;
+ default:
+ out.append(e);
+ }
+ } else {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ private Object parseLiteral() {
+ int start = idx;
+ while (idx < src.length()) {
+ char c = src.charAt(idx);
+ if (c == ',' || c == '}' || c == ']' || Character.isWhitespace(c)) {
+ break;
+ }
+ idx++;
+ }
+ String tok = src.substring(start, idx);
+ if ("true".equals(tok)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equals(tok)) {
+ return Boolean.FALSE;
+ }
+ if ("null".equals(tok)) {
+ return null;
+ }
+ try {
+ if (tok.contains(".") || tok.contains("e") || tok.contains("E")) {
+ return Double.parseDouble(tok);
+ }
+ return Long.parseLong(tok);
+ } catch (NumberFormatException ex) {
+ return tok;
+ }
+ }
+ }
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodec.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodec.java
new file mode 100644
index 000000000..004238d69
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodec.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import org.apache.eventmesh.common.stream.StreamChunk;
+import org.apache.eventmesh.common.stream.StreamRequest;
+
+import io.cloudevents.CloudEvent;
+
+/**
+ * Internal-wire codec SPI — the binary format messages travel in between the runtime, the storage
+ * MQ, and the agent (everywhere EventMesh talks to itself). The PUBLIC surface (HTTP/SSE/TCP/gRPC/
+ * A2A) stays CloudEvents; this codec only shapes the internal bytes, so the format can be swapped
+ * (compact {@link EventMeshFrame}, CE-protobuf, binary CE, …) without touching call sites. See
+ * {@code docs/eventmesh-architecture-refinement.md} §1.3.
+ *
+ * <p>Lives in {@code eventmesh-common} (not the {@code eventmesh-spi} module) so every consumer
+ * (runtime, agent, storage plugins) can reach it without a circular dependency. Pluggability is via
+ * {@link WireCodecs#get()}, which reflects a system property
+ * ({@code -Deventmesh.wire.codec=<fqcn>}) and falls back to {@link EventMeshFrameCodec}.</p>
+ *
+ * <p>Three message families share one codec, mirroring {@link EventMeshFrame}'s msgTypes:
+ * {@code StreamRequest}, {@code StreamChunk}, and {@code CloudEvent} (normal pub/sub).</p>
+ *
+ * @since 1.11.0
+ */
+public interface WireCodec {
+
+ /** Encode a streaming-call request → internal wire bytes. */
+ byte[] encode(StreamRequest request);
+
+ /** Encode a streaming response chunk → internal wire bytes. */
+ byte[] encode(StreamChunk chunk);
+
+ /** Encode a normal pub/sub CloudEvent → internal wire bytes. */
+ byte[] encode(CloudEvent event);
+
+ /** Decode internal wire bytes (a STREAM_REQ) back to a streaming-call request. */
+ StreamRequest decodeRequest(byte[] bytes);
+
+ /** Decode internal wire bytes (a STREAM_CHUNK) back to a streaming response chunk. */
+ StreamChunk decodeChunk(byte[] bytes);
+
+ /** Decode internal wire bytes (an EVENT) back to a CloudEvent. */
+ CloudEvent decodeEvent(byte[] bytes);
+}
diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodecs.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodecs.java
new file mode 100644
index 000000000..7990242a5
--- /dev/null
+++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/wire/WireCodecs.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Entry point for the internal-wire codec. Returns the configured {@link WireCodec}: by default
+ * {@link EventMeshFrameCodec}; override with {@code -Deventmesh.wire.codec=<fqcn>} (the class must
+ * have a public no-arg constructor and implement {@link WireCodec}). Falls back to
+ * {@link EventMeshFrameCodec} if the property is unset or the named class can't be loaded.
+ *
+ * <p>Callers use {@link #get()} instead of touching {@link EventMeshFrame} directly, so the wire
+ * format stays pluggable from a single place.</p>
+ */
+@Slf4j
+public final class WireCodecs {
+
+ /** System property name overriding the wire codec implementation class. */
+ public static final String CODEC_PROPERTY = "eventmesh.wire.codec";
+
+ private static volatile WireCodec cached;
+
+ private WireCodecs() {
+ }
+
+ /**
+ * @return the configured {@link WireCodec} (default EventMeshFrameCodec, override via property).
+ */
+ public static WireCodec get() {
+ WireCodec c = cached;
+ if (c != null) {
+ return c;
+ }
+ synchronized (WireCodecs.class) {
+ if (cached == null) {
+ cached = load();
+ }
+ return cached;
+ }
+ }
+
+ private static WireCodec load() {
+ String fqcn = System.getProperty(CODEC_PROPERTY, "").trim();
+ if (!fqcn.isEmpty()) {
+ try {
+ Class<?> cls = Class.forName(fqcn, true, Thread.currentThread().getContextClassLoader());
+ return (WireCodec) cls.getDeclaredConstructor().newInstance();
+ } catch (Throwable t) {
+ log.warn("failed to load WireCodec '{}' ({}); falling back to EventMeshFrameCodec", fqcn, t.toString());
+ }
+ }
+ return new EventMeshFrameCodec();
+ }
+}
diff --git a/eventmesh-common/src/test/java/org/apache/eventmesh/common/stream/StreamEventCodecTest.java b/eventmesh-common/src/test/java/org/apache/eventmesh/common/stream/StreamEventCodecTest.java
new file mode 100644
index 000000000..72a4b98d0
--- /dev/null
+++ b/eventmesh-common/src/test/java/org/apache/eventmesh/common/stream/StreamEventCodecTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.format.EventFormat;
+import io.cloudevents.core.provider.EventFormatProvider;
+import io.cloudevents.jackson.JsonFormat;
+
+/**
+ * Round-trips {@link StreamRequest}/{@link StreamChunk} through the codec AND
+ * the structured-JSON wire format (encode → JsonFormat serialize → deserialize → decode). Covers the
+ * v2 framing: sessionId (incl. the {@code <agentId>:<uuid>} colon delimiter), replyTo, and
+ * spec-legal extension names.
+ */
+class StreamEventCodecTest {
+
+ private final EventFormat format = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE);
+
+ private CloudEvent roundTrip(CloudEvent event) {
+ return format.deserialize(format.serialize(event));
+ }
+
+ @Test
+ void requestRoundTripPreservesAllFields() {
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("agentX:abc123").replyTo("em-agent#resp-abc123").prompt("hello world").model("gpt-4o-mini").build();
+
+ StreamRequest decoded = StreamEventCodec.requestFromEvent(roundTrip(StreamEventCodec.toEvent(req)));
+
+ assertThat(decoded.getSessionId()).isEqualTo("agentX:abc123");
+ assertThat(decoded.getReplyTo()).isEqualTo("em-agent#resp-abc123");
+ assertThat(decoded.getModel()).isEqualTo("gpt-4o-mini");
+ assertThat(decoded.getPrompt()).isEqualTo("hello world");
+ }
+
+ @Test
+ void sessionIdWithColonRoundTripsIntact() {
+ // v2 sessionId = <agentId>:<uuid>; the ':' is the routing delimiter and must survive the wire.
+ // agentId may itself contain '-' (e.g. auto "agent-<uuid>").
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("agent-1:b2c3d4").replyTo("p#l").prompt("p").build();
+
+ StreamRequest decoded = StreamEventCodec.requestFromEvent(roundTrip(StreamEventCodec.toEvent(req)));
+
+ assertThat(decoded.getSessionId()).isEqualTo("agent-1:b2c3d4");
+ }
+
+ @Test
+ void requestWithNullModelOmitsExtension() {
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("s1").replyTo("p#l").prompt("p").model(null).build();
+
+ StreamRequest decoded = StreamEventCodec.requestFromEvent(roundTrip(StreamEventCodec.toEvent(req)));
+
+ assertThat(decoded.getModel()).isNull();
+ assertThat(decoded.getPrompt()).isEqualTo("p");
+ }
+
+ @Test
+ void requestConversationIdRoundTrips() {
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("s1").replyTo("p#l").prompt("p").conversationId("conv-abc").build();
+
+ StreamRequest decoded = StreamEventCodec.requestFromEvent(roundTrip(StreamEventCodec.toEvent(req)));
+
+ assertThat(decoded.getConversationId()).isEqualTo("conv-abc");
+ }
+
+ @Test
+ void requestWithNullConversationIdOmitsExtension() {
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("s1").replyTo("p#l").prompt("p").conversationId(null).build();
+
+ StreamRequest decoded = StreamEventCodec.requestFromEvent(roundTrip(StreamEventCodec.toEvent(req)));
+
+ assertThat(decoded.getConversationId()).isNull();
+ }
+
+ @Test
+ void chunkRoundTripPreservesSeqAndText() {
+ StreamChunk chunk = StreamChunk.builder()
+ .sessionId("s1").seq(3).chunk("token-3").done(false).build();
+
+ StreamChunk decoded = StreamEventCodec.chunkFromEvent(roundTrip(StreamEventCodec.toEvent(chunk)));
+
+ assertThat(decoded.getSessionId()).isEqualTo("s1");
+ assertThat(decoded.getSeq()).isEqualTo(3);
+ assertThat(decoded.getChunk()).isEqualTo("token-3");
+ assertThat(decoded.isDone()).isFalse();
+ assertThat(decoded.getError()).isNull();
+ }
+
+ @Test
+ void terminalErrorChunkRoundTrips() {
+ StreamChunk chunk = StreamChunk.builder()
+ .sessionId("s1").seq(9).chunk("").done(true).error("llm 503").build();
+
+ StreamChunk decoded = StreamEventCodec.chunkFromEvent(roundTrip(StreamEventCodec.toEvent(chunk)));
+
+ assertThat(decoded.isDone()).isTrue();
+ assertThat(decoded.getError()).isEqualTo("llm 503");
+ assertThat(decoded.getChunk()).isEmpty();
+ assertThat(decoded.getSeq()).isEqualTo(9);
+ }
+
+ @Test
+ void chunkRoundTripWithEventTypeAndMeta() {
+ // agentscope-harness path: a tool-call event carries eventType=tool + meta (tool name/args).
+ Map<String, Object> meta = new LinkedHashMap<>();
+ meta.put("toolName", "search");
+ meta.put("args", "apache eventmesh");
+ StreamChunk chunk = StreamChunk.builder()
+ .sessionId("s1").seq(4).chunk("").done(false).eventType("tool").meta(meta).build();
+
+ StreamChunk decoded = StreamEventCodec.chunkFromEvent(roundTrip(StreamEventCodec.toEvent(chunk)));
+
+ assertThat(decoded.getEventType()).isEqualTo("tool");
+ assertThat(decoded.getMeta()).containsEntry("toolName", "search").containsEntry("args", "apache eventmesh");
+ }
+
+ @Test
+ void chunkWithNullEventTypeAndMetaOmitsExtensions() {
+ // default OpenAI path: eventType/meta are null → no extensions emitted → decode back to null.
+ StreamChunk chunk = StreamChunk.builder()
+ .sessionId("s1").seq(0).chunk("Hello").done(false).build();
+
+ StreamChunk decoded = StreamEventCodec.chunkFromEvent(roundTrip(StreamEventCodec.toEvent(chunk)));
+
+ assertThat(decoded.getEventType()).isNull();
+ assertThat(decoded.getMeta()).isNull();
+ assertThat(decoded.getChunk()).isEqualTo("Hello");
+ }
+
+ @Test
+ void chunkWithEmptyMetaOmitsExtension() {
+ // empty map is treated like null (no extension emitted).
+ StreamChunk chunk = StreamChunk.builder()
+ .sessionId("s1").seq(1).chunk("x").done(false).meta(new LinkedHashMap<>()).build();
+
+ StreamChunk decoded = StreamEventCodec.chunkFromEvent(roundTrip(StreamEventCodec.toEvent(chunk)));
+
+ assertThat(decoded.getMeta()).isNull();
+ }
+
+ @Test
+ void extensionNamesAreSpecLegalLowercaseAlnum() {
+ // CloudEvents spec: extension names must be lowercase a-z0-9 (no hyphens).
+ assertThat(StreamEventCodec.EXT_SESSION_ID).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_REPLY_TO).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_SEQ).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_DONE).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_ERROR).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_MODEL).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_EVENT_TYPE).matches("[a-z0-9]+");
+ assertThat(StreamEventCodec.EXT_META).matches("[a-z0-9]+");
+ }
+}
diff --git a/eventmesh-common/src/test/java/org/apache/eventmesh/common/wire/EventMeshFrameTest.java b/eventmesh-common/src/test/java/org/apache/eventmesh/common/wire/EventMeshFrameTest.java
new file mode 100644
index 000000000..2ccc2d699
--- /dev/null
+++ b/eventmesh-common/src/test/java/org/apache/eventmesh/common/wire/EventMeshFrameTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.common.wire;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.eventmesh.common.stream.StreamChunk;
+import org.apache.eventmesh.common.stream.StreamRequest;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+
+/**
+ * Round-trip tests for {@link EventMeshFrame}: all three message families (STREAM_REQ, STREAM_CHUNK,
+ * EVENT) encode→decode equality, plus footprint vs CloudEvents-JSON.
+ */
+class EventMeshFrameTest {
+
+ @Test
+ void streamRequestRoundTrip() {
+ StreamRequest req = StreamRequest.builder()
+ .sessionId("agent-1:uuid").replyTo("client-parent#client.c1")
+ .prompt("Introduce EventMesh").model("qwen-max").conversationId("conv-9").build();
+ StreamRequest back = EventMeshFrame.decode(EventMeshFrame.fromRequest(req).encode()).toStreamRequest();
+ assertEquals(req.getSessionId(), back.getSessionId());
+ assertEquals(req.getReplyTo(), back.getReplyTo());
+ assertEquals(req.getPrompt(), back.getPrompt());
+ assertEquals(req.getModel(), back.getModel());
+ assertEquals(req.getConversationId(), back.getConversationId());
+ }
+
+ @Test
+ void streamChunkRoundTripMinimal() {
+ StreamChunk c = StreamChunk.builder().sessionId("s1").seq(7).chunk("Hello").done(false).build();
+ StreamChunk back = EventMeshFrame.decode(EventMeshFrame.fromChunk(c).encode()).toChunk();
+ assertEquals(c.getSessionId(), back.getSessionId());
+ assertEquals(c.getSeq(), back.getSeq());
+ assertEquals(c.getChunk(), back.getChunk());
+ assertEquals(c.isDone(), back.isDone());
+ }
+
+ @Test
+ void streamChunkTerminalWithError() {
+ StreamChunk c = StreamChunk.builder().sessionId("s1").seq(3).chunk("").done(true).error("LLM 500").build();
+ StreamChunk back = EventMeshFrame.decode(EventMeshFrame.fromChunk(c).encode()).toChunk();
+ assertEquals("LLM 500", back.getError());
+ assertTrue(back.isDone());
+ }
+
+ @Test
+ void streamChunkWithExtensions() {
+ Map<String, Object> meta = new LinkedHashMap<>();
+ meta.put("tool", "search");
+ meta.put("count", 3);
+ StreamChunk c = StreamChunk.builder().sessionId("s2").seq(1).chunk("delta")
+ .eventType("tool").meta(meta).build();
+ StreamChunk back = EventMeshFrame.decode(EventMeshFrame.fromChunk(c).encode()).toChunk();
+ assertEquals("tool", back.getEventType());
+ assertEquals("search", back.getMeta().get("tool"));
+ assertEquals(3, ((Number) back.getMeta().get("count")).intValue());
+ }
+
+ @Test
+ void eventRoundTripPreservesAttributesAndExtensions() {
+ CloudEvent ce = CloudEventBuilder.v1()
+ .withId("order-123")
+ .withSource(URI.create("/checkout"))
+ .withType("com.example.order.created")
+ .withSubject("orders")
+ .withDataContentType("application/json")
+ .withData("{\"amount\":99}".getBytes())
+ .withExtension("emttl", "60000")
+ .withExtension("emcorrelationid", "corr-1")
+ .build();
+ CloudEvent back = EventMeshFrame.decode(EventMeshFrame.fromCloudEvent(ce).encode()).toCloudEvent();
+ assertEquals("order-123", back.getId());
+ assertEquals("com.example.order.created", back.getType());
+ assertEquals("orders", back.getSubject());
+ assertEquals("/checkout", back.getSource().toString());
+ assertEquals("60000", back.getExtension("emttl"));
+ assertEquals("corr-1", back.getExtension("emcorrelationid"));
+ assertEquals("{\"amount\":99}", new String(back.getData().toBytes()));
+ }
+
+ @Test
+ void eventRoundTripNoData() {
+ CloudEvent ce = CloudEventBuilder.v1()
+ .withId("e1").withSource(URI.create("/x")).withType("t").build();
+ CloudEvent back = EventMeshFrame.decode(EventMeshFrame.fromCloudEvent(ce).encode()).toCloudEvent();
+ assertEquals("e1", back.getId());
+ assertEquals("t", back.getType());
+ }
+
+ @Test
+ void streamChunkSmallerThanCloudEventsJson() {
+ StreamChunk tiny = StreamChunk.builder().sessionId("agent-1:abc").seq(0).chunk("He").build();
+ int frameBytes = EventMeshFrame.fromChunk(tiny).encode().length;
+ CloudEvent ce = org.apache.eventmesh.common.stream.StreamEventCodec.toEvent(tiny);
+ int ceBytes = io.cloudevents.core.provider.EventFormatProvider.getInstance()
+ .resolveFormat(io.cloudevents.jackson.JsonFormat.CONTENT_TYPE).serialize(ce).length;
+ assertTrue(frameBytes * 3 < ceBytes,
+ "frame (" + frameBytes + "B) not smaller enough than CE (" + ceBytes + "B)");
+ }
+
+ @Test
+ void badMagicRejected() {
+ StreamChunk c = StreamChunk.builder().sessionId("s").seq(0).chunk("x").build();
+ byte[] wire = EventMeshFrame.fromChunk(c).encode();
+ wire[0] = 0x00;
+ try {
+ EventMeshFrame.decode(wire);
+ } catch (IllegalArgumentException expected) {
+ return;
+ }
+ throw new AssertionError("expected IllegalArgumentException for bad magic");
+ }
+
+ @Test
+ void unicodePayloadRoundTrips() {
+ // CJK payload built from char codes to keep this source file pure ASCII.
+ String cjk = new String(new char[] {0x4f60, 0x597d, 0x4e16, 0x754c});
+ String sessionId = "s-" + (char) 0x4e2d;
+ StreamChunk c = StreamChunk.builder().sessionId(sessionId).seq(2).chunk(cjk).build();
+ StreamChunk back = EventMeshFrame.decode(EventMeshFrame.fromChunk(c).encode()).toChunk();
+ assertEquals(cjk, back.getChunk());
+ assertEquals(sessionId, back.getSessionId());
+ }
+
+ @Test
+ void decodeWithOffsetHandlesSlice() {
+ StreamChunk c = StreamChunk.builder().sessionId("s").seq(1).chunk("hi").build();
+ byte[] wire = EventMeshFrame.fromChunk(c).encode();
+ byte[] padded = new byte[wire.length + 5];
+ System.arraycopy(wire, 0, padded, 5, wire.length);
+ StreamChunk back = EventMeshFrame.decode(padded, 5, wire.length).toChunk();
+ assertEquals("hi", back.getChunk());
+ }
+
+ @Test
+ void metaOrderAndTypesPreserved() {
+ Map<String, Object> meta = new LinkedHashMap<>();
+ meta.put("a", "1");
+ meta.put("b", 2L);
+ meta.put("c", true);
+ StreamChunk c = StreamChunk.builder().sessionId("s").seq(0).chunk("").meta(meta).build();
+ Map<String, Object> back = EventMeshFrame.decode(EventMeshFrame.fromChunk(c).encode()).toChunk().getMeta();
+ assertArrayEquals(meta.keySet().toArray(), back.keySet().toArray());
+ assertEquals("1", back.get("a"));
+ assertEquals(2L, back.get("b"));
+ assertEquals(true, back.get("c"));
+ }
+
+ @Test
+ void msgTypeDistinguishesFamilies() {
+ EventMeshFrame req = EventMeshFrame.fromRequest(StreamRequest.builder().sessionId("s").prompt("p").build());
+ EventMeshFrame chunk = EventMeshFrame.fromChunk(StreamChunk.builder().sessionId("s").seq(0).chunk("c").build());
+ EventMeshFrame evt = EventMeshFrame.fromCloudEvent(
+ CloudEventBuilder.v1().withId("i").withSource(URI.create("/x")).withType("t").build());
+ assertTrue(req.isStreamRequest());
+ assertTrue(chunk.isStreamChunk());
+ assertTrue(evt.isEvent());
+ }
+}
diff --git a/eventmesh-examples/src/main/java/org/apache/eventmesh/cloudevents/demo/stream/StreamingCallDemo.java b/eventmesh-examples/src/main/java/org/apache/eventmesh/cloudevents/demo/stream/StreamingCallDemo.java
new file mode 100644
index 000000000..fe35846c8
--- /dev/null
+++ b/eventmesh-examples/src/main/java/org/apache/eventmesh/cloudevents/demo/stream/StreamingCallDemo.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.cloudevents.demo.stream;
+
+import org.apache.eventmesh.client.cloudevents.CloudEventsClient;
+import org.apache.eventmesh.client.cloudevents.stream.OpenSession;
+import org.apache.eventmesh.client.cloudevents.stream.StreamRequest;
+import org.apache.eventmesh.client.cloudevents.stream.StreamingResponse;
+import org.apache.eventmesh.client.cloudevents.stream.StreamingSession;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Streaming-call client demo for the uni-architecture (mode 1: client↔agent, runtime-mediated).
+ *
+ * <p>This is a pure HTTP client demo that connects to a running EventMesh Runtime + at least one
+ * registered streaming Agent. It does NOT depend on any runtime server-side classes.
+ *
+ * <p>Prerequisites:
+ * <ol>
+ * <li>A running EventMesh Runtime on {@code runtimeUrl} (default {@code http://localhost:8080}),
+ * wired with a {@code SessionRouter} (see docs/sdk-streaming-call-guide.md §9).</li>
+ * <li>At least one streaming Agent registered + heartbeating against that runtime.</li>
+ * </ol>
+ *
+ * <p>Run:
+ * <pre>
+ * java org.apache.eventmesh.cloudevents.demo.stream.StreamingCallDemo [runtimeUrl] [prompt]
+ * # defaults: runtimeUrl=http://localhost:8080 prompt="Introduce Apache EventMesh in three sentences."
+ * </pre>
+ *
+ * <p>The demo exercises {@code forEach}-only consumption (the sole posture) on a single-turn call,
+ * then a multi-turn session that proves context is carried across turns.
+ * See docs/sdk-streaming-call-guide.md for the full API reference.
+ */
+@Slf4j
+public class StreamingCallDemo {
+
+ private static final String DEFAULT_RUNTIME_URL = "http://localhost:8080";
+ private static final String DEFAULT_PROMPT = "Introduce Apache EventMesh in three sentences.";
+
+ public static void main(String[] args) throws Exception {
+ String runtimeUrl = args.length > 0 ? args[0] : DEFAULT_RUNTIME_URL;
+ String prompt = args.length > 1 ? args[1] : DEFAULT_PROMPT;
+
+ log.info("=== Streaming Call Demo ===");
+ log.info("Runtime: {}", runtimeUrl);
+
+ // Each session uses a FRESH clientId. The matchmaker binds clientId→agent, so a fresh clientId
+ // per session lets it load-balance across all registered agents (random within the
+ // least-loaded tier). The multi-turn session keeps a single client on purpose: sticky binding
+ // is what carries context across turns, so it stays on whichever agent it first binds to.
+
+ // 1) Single-turn session: openSession → call → forEach → close
+ withClient(runtimeUrl, c -> singleTurnForEach(c, prompt));
+
+ // 2) Multi-turn session: context is carried across turns (single client → sticky to one agent)
+ withClient(runtimeUrl, StreamingCallDemo::multiTurnSession);
+
+ log.info("=== Demo Complete ===");
+ }
+
+ /** Build a client with a unique clientId, run an action, always shut it down. */
+ private static void withClient(String runtimeUrl, ThrowingClientAction action) throws Exception {
+ CloudEventsClient client = CloudEventsClient.builder()
+ .runtimeUrl(runtimeUrl)
+ .clientId("streaming-demo-" + System.nanoTime())
+ .build();
+ try {
+ action.run(client);
+ } finally {
+ client.shutdown();
+ }
+ }
+
+ @FunctionalInterface
+ private interface ThrowingClientAction {
+ void run(CloudEventsClient client) throws Exception;
+ }
+
+ // ------------------------------------------------------------------
+ // Single-turn (openSession → call → forEach)
+ // ------------------------------------------------------------------
+
+ /** Open a session, make one call, print tokens as they arrive, close. */
+ private static void singleTurnForEach(CloudEventsClient client, String prompt) throws Exception {
+ log.info("");
+ log.info("--- [1] single-turn / forEach --- prompt=\"{}\"", prompt);
+ StreamingSession session = client.streaming()
+ .openSession(OpenSession.builder().clientId(client.clientId()).build());
+ log.info("sessionId={}, agentId={}", session.sessionId(), session.agentId());
+ try {
+ try (StreamingResponse r = session.call(
+ StreamRequest.builder().prompt(prompt).timeout(Duration.ofMinutes(2)).build())) {
+ StringBuilder sb = new StringBuilder();
+ r.forEach(chunk -> {
+ sb.append(chunk.getChunk());
+ System.out.print(chunk.getChunk()); // stream tokens as they arrive
+ })
+ .orTimeout(2, TimeUnit.MINUTES)
+ .join();
+ log.info("");
+ log.info("full reply: {}", sb);
+ }
+ } finally {
+ session.close();
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Multi-turn session
+ // ------------------------------------------------------------------
+
+ /**
+ * Open one session and run two turns. Closing a turn's {@link StreamingResponse} cancels only
+ * that turn's read; the same {@code sessionId} is reused so the agent accumulates context.
+ */
+ private static void multiTurnSession(CloudEventsClient client) throws Exception {
+ log.info("");
+ log.info("--- [2] multi-turn session (context carried across turns) ---");
+ StreamingSession session = client.streaming()
+ .openSession(OpenSession.builder().clientId(client.clientId()).build());
+ log.info("sessionId={}", session.sessionId());
+ try {
+ // Turn 1
+ try (StreamingResponse r = session.call("My name is Zhang San and I am a Java engineer.")) {
+ log.info("[turn 1] prompt=\"My name is Zhang San and I am a Java engineer.\"");
+ r.forEach(chunk -> System.out.print(chunk.getChunk())).join();
+ log.info("");
+ }
+
+ // Turn 2 — same session: the agent remembers turn 1
+ try (StreamingResponse r = session.call("What is my name and what do I do?")) {
+ log.info("[turn 2] prompt=\"What is my name and what do I do?\"");
+ r.forEach(chunk -> System.out.print(chunk.getChunk())).join();
+ log.info("");
+ }
+ } finally {
+ session.close(); // POST /session/close/{sessionId} — idempotent
+ }
+ }
+}
\ No newline at end of file
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/java/org/apache/eventmesh/protocol/a2a/A2AFrameAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/java/org/apache/eventmesh/protocol/a2a/A2AFrameAdaptor.java
new file mode 100644
index 000000000..b62a60486
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/java/org/apache/eventmesh/protocol/a2a/A2AFrameAdaptor.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.protocol.a2a;
+
+import org.apache.eventmesh.common.protocol.ByteTransport;
+import org.apache.eventmesh.common.protocol.ProtocolTransportObject;
+import org.apache.eventmesh.common.wire.EventMeshFrame;
+import org.apache.eventmesh.protocol.api.FrameAdaptor;
+import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * {@link FrameAdaptor} for the A2A (Agent-to-Agent) JSON-RPC 2.0 protocol. Converts A2A JSON-RPC
+ * messages directly to/from {@link EventMeshFrame} — no CloudEvent intermediary.
+ *
+ * <p>Ingress: A2A JSON-RPC bytes → parse JSON-RPC → extract routing (method, params._topic,
+ * agent-card identity) into frame attributes + raw JSON as data.
+ * Egress: frame attributes + data → reconstruct A2A JSON-RPC bytes.</p>
+ *
+ * <p>Registered under the name {@code a2a}; load via
+ * {@code FrameAdaptors.get("a2a")}.</p>
+ */
+public class A2AFrameAdaptor implements FrameAdaptor {
+
+ public static final String PROTOCOL_TYPE = "a2a";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Override
+ public EventMeshFrame toFrame(ProtocolTransportObject proto) throws ProtocolHandleException {
+ byte[] jsonBytes;
+ if (proto instanceof ByteTransport) {
+ jsonBytes = ((ByteTransport) proto).getBytes();
+ } else {
+ jsonBytes = proto.toString().getBytes(StandardCharsets.UTF_8);
+ }
+ if (jsonBytes == null || jsonBytes.length == 0) {
+ throw new ProtocolHandleException("empty A2A body");
+ }
+ try {
+ String content = new String(jsonBytes, StandardCharsets.UTF_8);
+ JsonNode node = MAPPER.readTree(content);
+
+ Map<String, String> attrs = new LinkedHashMap<>();
+ attrs.put("specversion", "1.0");
+ attrs.put("type", "a2a");
+ attrs.put("source", "eventmesh-a2a");
+ attrs.put("id", node.has("id") ? node.get("id").asText() : java.util.UUID.randomUUID().toString());
+ attrs.put(A2AProtocolConstants.CE_EXTENSION_PROTOCOL, PROTOCOL_TYPE);
+
+ if (node.has("jsonrpc")) {
+ attrs.put("ema2aversion", node.get("jsonrpc").asText());
+ }
+ if (node.has("method")) {
+ String method = node.get("method").asText();
+ attrs.put(A2AProtocolConstants.CE_EXTENSION_A2A_METHOD, method);
+ attrs.put("type", A2AProtocolConstants.CE_TYPE_PREFIX + method.replace("/", ".") + ".req");
+ } else if (node.has("result") || node.has("error")) {
+ attrs.put("type", A2AProtocolConstants.CE_TYPE_PREFIX + "common.response");
+ }
+
+ // Extract routing from params
+ if (node.has("params")) {
+ JsonNode params = node.get("params");
+ if (params.has("_topic")) {
+ attrs.put("subject", params.get("_topic").asText());
+ } else if (params.has("org_id") && params.has("unit_id") && params.has("agent_id")) {
+ AgentIdentity identity = new AgentIdentity(
+ params.get("org_id").asText(),
+ params.get("unit_id").asText(),
+ params.get("agent_id").asText());
+ attrs.put("subject", identity.discoveryTopic());
+ }
+ if (params.has("_seq")) {
+ attrs.put(A2AProtocolConstants.CE_EXTENSION_SEQ, params.get("_seq").asText());
+ }
+ if (params.has("_agentId")) {
+ attrs.put(A2AProtocolConstants.CE_EXTENSION_TARGET_AGENT, params.get("_agentId").asText());
+ }
+ }
+
+ return EventMeshFrame.event(attrs, jsonBytes);
+ } catch (Exception e) {
+ throw new ProtocolHandleException("A2A → EventMeshFrame conversion failed", e);
+ }
+ }
+
+ @Override
+ public ProtocolTransportObject fromFrame(EventMeshFrame frame) throws ProtocolHandleException {
+ // A2A egress: the frame's data IS the original A2A JSON-RPC bytes (preserved verbatim on
+ // ingress). Return as-is — the A2A client speaks JSON-RPC.
+ return new ByteTransport(frame.data());
+ }
+
+ @Override
+ public String getProtocolType() {
+ return PROTOCOL_TYPE;
+ }
+}
diff --git a/eventmesh-runtime/conf/a2a-protocol-config.yaml b/eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
similarity index 58%
rename from eventmesh-runtime/conf/a2a-protocol-config.yaml
rename to eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
index 4ce81f502..32de77f5f 100644
--- a/eventmesh-runtime/conf/a2a-protocol-config.yaml
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-a2a/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
@@ -13,23 +13,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-eventMesh:
- protocol:
- plugin:
- a2a:
- # Whether the A2A (Agent-to-Agent) protocol adaptor is enabled
- enabled: true
-
- # Protocol version
- version: "2.0"
-
- # Features configuration
- features:
- # Enable delegation to existing CloudEvents/HTTP protocols
- delegation: true
-
- # Enable MCP (Model Context Protocol) JSON-RPC 2.0 support
- mcp-support: true
-
- # Enable batch processing support
- batch-processing: true
+a2a=org.apache.eventmesh.protocol.a2a.A2AFrameAdaptor
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptor.java
new file mode 100644
index 000000000..d00f58835
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptor.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.protocol.api;
+
+import org.apache.eventmesh.common.protocol.ProtocolTransportObject;
+import org.apache.eventmesh.common.wire.EventMeshFrame;
+import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException;
+import org.apache.eventmesh.spi.EventMeshExtensionType;
+import org.apache.eventmesh.spi.EventMeshSPI;
+
+/**
+ * SPI for converting between an <b>external protocol</b> (CloudEvents JSON, MeshMessage TCP Package,
+ * gRPC, …) and EventMesh's internal {@link EventMeshFrame}. Each protocol plugin implements this so
+ * the runtime calls a single SPI boundary — it never hardcodes {@code fromCloudEvent} /
+ * {@code MeshMessageFrameCodec} directly.
+ *
+ * <p>This is the protocol-layer counterpart to the internal {@code WireCodec}: {@code WireCodec}
+ * shapes the <i>internal MQ bytes</i> (EventMeshFrame ↔ byte[]); {@code FrameAdaptor} shapes the
+ * <i>external protocol representation</i> (EventMeshFrame ↔ the client's wire format). The two
+ * never overlap: CloudEvent and MeshMessage are peer external protocols, neither converts to the
+ * other — both convert directly to/from EventMeshFrame.</p>
+ *
+ * <h3>Ingress (client → runtime)</h3>
+ * <pre>{@code
+ * ProtocolTransportObject proto = ...; // what arrived on the wire (Package, byte[], HttpCommand)
+ * EventMeshFrame frame = adaptor.toFrame(proto);
+ * ingress.publish(topic, frame);
+ * }</pre>
+ *
+ * <h3>Egress (runtime → client)</h3>
+ * <pre>{@code
+ * EventMeshFrame frame = ...; // what the dispatcher is delivering
+ * ProtocolTransportObject proto = adaptor.fromFrame(frame);
+ * // write proto to the client's transport
+ * }</pre>
+ *
+ * @since 1.11.0
+ */
+@EventMeshSPI(eventMeshExtensionType = EventMeshExtensionType.PROTOCOL)
+public interface FrameAdaptor {
+
+ /**
+ * Convert an external-protocol transport object to the internal EventMeshFrame (ingress).
+ *
+ * @param proto the wire-format object ({@link org.apache.eventmesh.common.protocol.tcp.Package}
+ * for MeshMessage TCP, {@code byte[]} / {@link org.apache.eventmesh.common.protocol.http.HttpCommand}
+ * for CloudEvents HTTP, etc.)
+ * @return the internal EventMeshFrame
+ */
+ EventMeshFrame toFrame(ProtocolTransportObject proto) throws ProtocolHandleException;
+
+ /**
+ * Convert an internal EventMeshFrame back to the external-protocol transport object (egress).
+ *
+ * @param frame the internal EventMeshFrame being delivered to a client
+ * @return the wire-format object to write to the client's transport
+ */
+ ProtocolTransportObject fromFrame(EventMeshFrame frame) throws ProtocolHandleException;
+
+ /**
+ * The protocol type this adaptor handles (e.g. {@code "cloudevents"}, {@code "meshmessage"}).
+ * Used by the runtime to select the right adaptor per client connection.
+ */
+ String getProtocolType();
+
+ /**
+ * Ingress convenience: convert without throwing — wraps {@link ProtocolHandleException} in a
+ * {@link RuntimeException} so call sites that can't propagate checked exceptions stay terse.
+ */
+ default EventMeshFrame toFrameSilent(ProtocolTransportObject proto) {
+ try {
+ return toFrame(proto);
+ } catch (ProtocolHandleException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Egress convenience: convert without throwing — wraps {@link ProtocolHandleException} in a
+ * {@link RuntimeException}.
+ */
+ default ProtocolTransportObject fromFrameSilent(EventMeshFrame frame) {
+ try {
+ return fromFrame(frame);
+ } catch (ProtocolHandleException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptors.java b/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptors.java
new file mode 100644
index 000000000..8d6b14515
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-api/src/main/java/org/apache/eventmesh/protocol/api/FrameAdaptors.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.protocol.api;
+
+import org.apache.eventmesh.common.protocol.ByteTransport;
+import org.apache.eventmesh.common.protocol.ProtocolTransportObject;
+import org.apache.eventmesh.common.wire.EventMeshFrame;
+import org.apache.eventmesh.spi.EventMeshExtensionFactory;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Entry point for protocol-adaptor SPI loading. Caches {@link FrameAdaptor} instances by protocol
+ * name ({@code "cloudevents"}, {@code "meshmessage"}, …); falls back to the CloudEvents adaptor
+ * when a named adaptor can't be loaded. Callers use {@link #get(String)} instead of touching
+ * {@code EventMeshFrame.fromCloudEvent} / {@code MeshMessageFrameCodec} directly, so all protocol
+ * conversion logic lives in the adaptors.
+ */
+@Slf4j
+public final class FrameAdaptors {
+
+ /** Default protocol name when the caller doesn't specify (HTTP CloudEvents path). */
+ public static final String DEFAULT = "cloudevents";
+
+ private static final ConcurrentHashMap<String, FrameAdaptor> CACHE = new ConcurrentHashMap<>();
+
+ private FrameAdaptors() {
+ }
+
+ /**
+ * @return the {@link FrameAdaptor} for {@code protocolName} (cached; CloudEvents fallback).
+ */
+ public static FrameAdaptor get(String protocolName) {
+ String name = (protocolName == null || protocolName.isEmpty()) ? DEFAULT : protocolName;
+ return CACHE.computeIfAbsent(name, FrameAdaptors::load);
+ }
+
+ /** Convenience: the default (CloudEvents) adaptor. */
+ public static FrameAdaptor cloudevents() {
+ return get(DEFAULT);
+ }
+
+ private static FrameAdaptor load(String name) {
+ try {
+ FrameAdaptor adaptor = EventMeshExtensionFactory.getExtension(FrameAdaptor.class, name);
+ if (adaptor != null) {
+ return adaptor;
+ }
+ } catch (Throwable t) {
+ log.warn("FrameAdaptor '{}' load failed ({}); trying default", name, t.toString());
+ }
+ // If the named adaptor isn't found, try the default (cloudevents) — it's registered in the
+ // separate eventmesh-protocol-cloudevents plugin on the classpath.
+ if (!DEFAULT.equals(name)) {
+ try {
+ FrameAdaptor fallback = EventMeshExtensionFactory.getExtension(FrameAdaptor.class, DEFAULT);
+ if (fallback != null) {
+ log.warn("FrameAdaptor '{}' not found; using default ({})", name, DEFAULT);
+ return fallback;
+ }
+ } catch (Throwable ignored) {
+ // best-effort
+ }
+ }
+ throw new IllegalStateException("FrameAdaptor '" + name + "' not found (is the protocol plugin on the classpath?)");
+ }
+
+ // ---- convenience wrappers for the common CloudEvents path ----
+
+ /** Ingress: CloudEvents-JSON bytes → EventMeshFrame (via the default CloudEvents adaptor). */
+ public static EventMeshFrame toFrame(byte[] cloudEventsJson) {
+ return cloudevents().toFrameSilent(new ByteTransport(cloudEventsJson));
+ }
+
+ /** Egress: EventMeshFrame → CloudEvents-JSON bytes (via the default CloudEvents adaptor). */
+ public static byte[] toCloudEventsJson(EventMeshFrame frame) {
+ ProtocolTransportObject proto = cloudevents().fromFrameSilent(frame);
+ if (proto instanceof ByteTransport) {
+ return ((ByteTransport) proto).getBytes();
+ }
+ return new byte[0];
+ }
+}
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/build.gradle b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/build.gradle
new file mode 100644
index 000000000..dc81e8f4c
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/build.gradle
@@ -0,0 +1,7 @@
+// CloudEvents protocol plugin — CloudEvents-JSON ↔ EventMeshFrame FrameAdaptor.
+// The default external protocol for HTTP/SSE/WS clients speaking CloudEvents 1.0.
+dependencies {
+ api project(":eventmesh-protocol-plugin:eventmesh-protocol-api")
+ implementation "io.cloudevents:cloudevents-core"
+ implementation "io.cloudevents:cloudevents-json-jackson"
+}
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/java/org/apache/eventmesh/protocol/cloudevents/CloudEventsFrameAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/java/org/apache/eventmesh/protocol/cloudevents/CloudEventsFrameAdaptor.java
new file mode 100644
index 000000000..1414856f7
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/java/org/apache/eventmesh/protocol/cloudevents/CloudEventsFrameAdaptor.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.protocol.cloudevents;
+
+import org.apache.eventmesh.common.protocol.ByteTransport;
+import org.apache.eventmesh.common.protocol.ProtocolTransportObject;
+import org.apache.eventmesh.common.wire.EventMeshFrame;
+import org.apache.eventmesh.protocol.api.FrameAdaptor;
+import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException;
+import org.apache.eventmesh.spi.EventMeshExtensionType;
+import org.apache.eventmesh.spi.EventMeshSPI;
+
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.provider.EventFormatProvider;
+import io.cloudevents.jackson.JsonFormat;
+
+/**
+ * {@link FrameAdaptor} for the CloudEvents 1.0 protocol (structured-JSON on the wire). Converts
+ * directly between CloudEvents-JSON bytes and {@link EventMeshFrame}.
+ *
+ * <p>Registered under the name {@code cloudevents}; the default external protocol for HTTP/SSE/WS
+ * clients. Load via {@code FrameAdaptors.get("cloudevents")}.</p>
+ */
+@EventMeshSPI(eventMeshExtensionType = EventMeshExtensionType.PROTOCOL)
+public class CloudEventsFrameAdaptor implements FrameAdaptor {
+
+ public static final String PROTOCOL_TYPE = "cloudevents";
+
+ private static io.cloudevents.core.format.EventFormat jsonFormat() {
+ return EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE);
+ }
+
+ @Override
+ public EventMeshFrame toFrame(ProtocolTransportObject proto) throws ProtocolHandleException {
+ if (!(proto instanceof ByteTransport)) {
+ throw new ProtocolHandleException("expected ByteTransport (CloudEvents JSON bytes), got "
+ + (proto == null ? "null" : proto.getClass()));
+ }
+ byte[] ceJsonBytes = ((ByteTransport) proto).getBytes();
+ if (ceJsonBytes == null || ceJsonBytes.length == 0) {
+ throw new ProtocolHandleException("empty CloudEvents body");
+ }
+ try {
+ CloudEvent ce = jsonFormat().deserialize(ceJsonBytes);
+ if (ce == null) {
+ throw new ProtocolHandleException("failed to deserialize CloudEvents JSON");
+ }
+ return EventMeshFrame.fromCloudEvent(ce);
+ } catch (ProtocolHandleException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ProtocolHandleException("CloudEvents → EventMeshFrame conversion failed", e);
+ }
+ }
+
+ @Override
+ public ProtocolTransportObject fromFrame(EventMeshFrame frame) throws ProtocolHandleException {
+ try {
+ CloudEvent ce = frame.toCloudEvent();
+ byte[] json = jsonFormat().serialize(ce);
+ return new ByteTransport(json);
+ } catch (Exception e) {
+ throw new ProtocolHandleException("EventMeshFrame → CloudEvents conversion failed", e);
+ }
+ }
+
+ @Override
+ public String getProtocolType() {
+ return PROTOCOL_TYPE;
+ }
+}
diff --git a/gradle.properties b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
similarity index 62%
copy from gradle.properties
copy to eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
index d752fa384..38c007f09 100644
--- a/gradle.properties
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-cloudevents/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
@@ -1,4 +1,3 @@
-#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
@@ -13,24 +12,5 @@
# 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.
-#
-
-jdk=1.8
-snapshot=false
-group=org.apache.eventmesh
-version=1.12.0-release
-#last eight bits of public key
-signing.keyId=
-#passphrase for key pairs
-signing.password=
-#path of exporting private key
-signing.secretKeyRingFile=
-#apache id
-apacheUserName=
-#apache password
-apachePassWord=
-signEnabled=false
-org.gradle.warning.mode=none
-org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
-org.gradle.caching=true
+cloudevents=org.apache.eventmesh.protocol.cloudevents.CloudEventsFrameAdaptor
diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/java/org/apache/eventmesh/protocol/meshmessage/MeshMessageFrameAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/java/org/apache/eventmesh/protocol/meshmessage/MeshMessageFrameAdaptor.java
new file mode 100644
index 000000000..d529a37e4
--- /dev/null
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/java/org/apache/eventmesh/protocol/meshmessage/MeshMessageFrameAdaptor.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.eventmesh.protocol.meshmessage;
+
+import org.apache.eventmesh.common.Constants;
+import org.apache.eventmesh.common.protocol.ProtocolTransportObject;
+import org.apache.eventmesh.common.protocol.tcp.EventMeshMessage;
+import org.apache.eventmesh.common.protocol.tcp.Header;
+import org.apache.eventmesh.common.protocol.tcp.Package;
+import org.apache.eventmesh.common.utils.JsonUtils;
+import org.apache.eventmesh.common.wire.EventMeshFrame;
+import org.apache.eventmesh.protocol.api.FrameAdaptor;
+import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * {@link FrameAdaptor} for the legacy MeshMessage TCP protocol — converts a {@link Package}
+ * (carrying an {@link EventMeshMessage}) directly to/from an {@link EventMeshFrame}, without going
+ * through CloudEvent. This replaces the old {@code MeshMessageProtocolAdaptor.toCloudEvent} path
+ * that used CloudEvent as an intermediary.
+ *
+ * <p>Registered under the name {@code meshmessage}; load via
+ * {@code EventMeshExtensionFactory.getExtension(FrameAdaptor.class, "meshmessage")}.</p>
+ */
+public class MeshMessageFrameAdaptor implements FrameAdaptor {
+
+ public static final String PROTOCOL_TYPE = "meshmessage";
+ static final String ATTR_ORIGIN_PROTOCOL = "emorigin";
+
+ @Override
+ public EventMeshFrame toFrame(ProtocolTransportObject proto) throws ProtocolHandleException {
+ if (!(proto instanceof Package)) {
+ throw new ProtocolHandleException("expected Package, got " + (proto == null ? "null" : proto.getClass()));
+ }
+ Package pkg = (Package) proto;
+ Object body = pkg.getBody();
+ EventMeshMessage message;
+ if (body instanceof EventMeshMessage) {
+ message = (EventMeshMessage) body;
+ } else if (body instanceof String) {
+ message = JsonUtils.parseObject((String) body, EventMeshMessage.class);
+ } else {
+ throw new ProtocolHandleException("Package body is not an EventMeshMessage: " + (body == null ? "null" : body.getClass()));
+ }
+ if (message == null) {
+ throw new ProtocolHandleException("failed to parse EventMeshMessage from Package body");
+ }
+
+ Map<String, String> attrs = new LinkedHashMap<>();
+ attrs.put("specversion", "1.0");
+ attrs.put("type", "eventmeshmessage");
+ attrs.put("source", "/");
+ attrs.put(ATTR_ORIGIN_PROTOCOL, PROTOCOL_TYPE);
+ Header header = pkg.getHeader();
+ if (header != null && header.getSeq() != null) {
+ attrs.put("id", header.getSeq());
+ }
+ if (message.getTopic() != null) {
+ attrs.put("subject", message.getTopic());
+ }
+ if (message.getProperties() != null) {
+ attrs.putAll(message.getProperties());
+ }
+ if (message.getHeaders() != null) {
+ attrs.putAll(message.getHeaders());
+ }
+ byte[] data = message.getBody() == null ? new byte[0]
+ : message.getBody().getBytes(Constants.DEFAULT_CHARSET);
+ return EventMeshFrame.event(attrs, data);
+ }
+
+ @Override
+ public ProtocolTransportObject fromFrame(EventMeshFrame frame) throws ProtocolHandleException {
+ Map<String, String> attrs = frame.attributes();
+ EventMeshMessage message = new EventMeshMessage();
+ message.setTopic(attrs.get("subject"));
+ message.setBody(frame.data() == null ? "" : new String(frame.data(), StandardCharsets.UTF_8));
+
+ Map<String, String> props = new LinkedHashMap<>();
+ for (Map.Entry<String, String> e : attrs.entrySet()) {
+ String k = e.getKey();
+ if ("type".equals(k) || ATTR_ORIGIN_PROTOCOL.equals(k) || "id".equals(k) || "subject".equals(k)
+ || "source".equals(k) || "specversion".equals(k) || "time".equals(k) || "dataschema".equals(k)
+ || "datacontenttype".equals(k)) {
+ continue;
+ }
+ props.put(k, e.getValue());
+ }
+ message.setProperties(props);
+
+ Package pkg = new Package();
+ pkg.setBody(message);
+ return pkg;
+ }
+
+ @Override
+ public String getProtocolType() {
+ return PROTOCOL_TYPE;
+ }
+}
diff --git a/gradle.properties b/eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
similarity index 62%
copy from gradle.properties
copy to eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
index d752fa384..0a2191628 100644
--- a/gradle.properties
+++ b/eventmesh-protocol-plugin/eventmesh-protocol-meshmessage/src/main/resources/META-INF/eventmesh/org.apache.eventmesh.protocol.api.FrameAdaptor
@@ -1,4 +1,3 @@
-#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
@@ -13,24 +12,5 @@
# 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.
-#
-
-jdk=1.8
-snapshot=false
-group=org.apache.eventmesh
-version=1.12.0-release
-#last eight bits of public key
-signing.keyId=
-#passphrase for key pairs
-signing.password=
-#path of exporting private key
-signing.secretKeyRingFile=
-#apache id
-apacheUserName=
-#apache password
-apachePassWord=
-signEnabled=false
-org.gradle.warning.mode=none
-org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
-org.gradle.caching=true
+meshmessage=org.apache.eventmesh.protocol.meshmessage.MeshMessageFrameAdaptor
diff --git a/eventmesh-runtime/build.gradle b/eventmesh-runtime/build.gradle
index ab9b525a9..4b8e0d49e 100644
--- a/eventmesh-runtime/build.gradle
+++ b/eventmesh-runtime/build.gradle
@@ -46,6 +46,7 @@ dependencies {
// 协议适配 (TCP/HTTP 桥 + A2A Agent 通信)
implementation project(':eventmesh-protocol-plugin:eventmesh-protocol-api')
+ implementation project(':eventmesh-protocol-plugin:eventmesh-protocol-cloudevents')
implementation project(':eventmesh-protocol-plugin:eventmesh-protocol-meshmessage')
implementation project(':eventmesh-protocol-plugin:eventmesh-protocol-a2a')
@@ -62,10 +63,12 @@ dependencies {
testImplementation project(':eventmesh-storage-plugin:eventmesh-storage-kafka')
// Topic creation in RealBrokerIntegrationTest (broker may have autoCreateTopicEnable=false).
testImplementation 'org.apache.rocketmq:rocketmq-tools:4.9.5'
- // A2AClientServerIntegrationTest drives the A2A Gateway over HTTP.
- testImplementation 'org.apache.httpcomponents:httpclient:4.5.14'
// ClientBrokerIntegrationTest drives the runtime via the real CloudEventsClient SDK over HTTP.
testImplementation project(':eventmesh-sdks:eventmesh-sdk-java')
+ // LiteStreamCallIntegrationTest boots the agent (StreamingAgent + OpenAiLlmClient) in-process.
+ testImplementation project(':eventmesh-agent')
+ // StreamingCallDemoE2ETest drives the demo's main() against a real booted runtime + agent.
+ testImplementation project(':eventmesh-examples')
}
// Resolve the lz4-java capability conflict when kafka-clients and rocketmq are both on the test
@@ -77,16 +80,162 @@ configurations.all {
exclude group: 'org.lz4', module: 'lz4-java'
}
+// Dev runner: boots EventMeshApplication with the rocketmq5 storage plugin on the classpath (so the
+// SPI loads it without a dist/plugin dir). Forwards eventmesh*/eventMesh*/llm.*/it.* -D flags.
+task runRuntime(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.runtime.boot.EventMeshApplication'
+ classpath = sourceSets.main.runtimeClasspath
+ classpath += project(':eventmesh-storage-plugin:eventmesh-storage-rocketmq5').sourceSets.main.output
+ classpath += project(':eventmesh-storage-plugin:eventmesh-storage-rocketmq5').configurations.runtimeClasspath
+ classpath += files('conf') // so conf/log4j2.xml is auto-discovered (INFO console)
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('llm.')
+ || k.startsWith('it.') || k.startsWith('log4j2')
+ }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+}
+
+// Broker integration tests need real brokers (rocketmq4/5/kafka/nacos) and are flaky from a dev
+// machine, so they run ON-DEMAND via `gradle :eventmesh-runtime:e2e-test` - NOT in the default
+// `test` task (which stays fast + green: unit tests + in-memory ITs only). They auto-detect infra
+// via E2EConfig (assumeTrue) - no -D needed.
+//
+// rocketmq 4.9.5 and 5.5.0 cannot coexist on one classpath (5.x moved common.protocol.* ->
+// remoting.protocol.*), so the 4.9 broker ITs (which reference 4.9.5 classes directly) run in a
+// separate task with storage-rocketmq5 excluded; the 5.x/kafka/nacos ITs run in another task with
+// the full classpath. `e2e-test` is a lifecycle task that runs both (two JVMs).
+final List<String> BROKER_IT_5X_CLASSES = [
+ 'org.apache.eventmesh.runtime.it.LiteStreamCallIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.StreamingSdkE2ETest',
+ 'org.apache.eventmesh.runtime.it.RocketMQ5BrokerIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.RocketMQ5LiteHttpIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.KafkaClientE2EIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.NacosClusterForwardIntegrationTest'
+]
+final List<String> BROKER_IT_4_CLASSES = [
+ 'org.apache.eventmesh.runtime.it.RealBrokerIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.AckTimeoutRedeliveryIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.BrokerListTest',
+ 'org.apache.eventmesh.runtime.it.ClientBrokerIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.LoadThroughputIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.LegacyTcpClusterBrokerIntegrationTest',
+ 'org.apache.eventmesh.runtime.it.MultiInstanceRocketMqIntegrationTest'
+]
+
test {
- // Forward -Dit.* (set on the gradle CLI) to the test JVM so RealBrokerIntegrationTest can be
- // enabled + pointed at a broker. Only forwarded when actually set, so the test's own defaults
- // (it.topic=em-it-orders, it.namesrv=localhost:9092) still apply otherwise.
- // Enable + run: -Dit.storage=rocketmq -Dit.namesrv=host:9876 [-Dit.topic=...]
jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
- ['it.storage', 'it.topic', 'it.namesrv', 'it.nacos', 'it.storage5', 'it.namesrv5'].each { k ->
+ ['it.storage', 'it.topic', 'it.namesrv', 'it.nacos', 'it.storage5', 'it.namesrv5',
+ 'llm.base.url', 'llm.api.key', 'llm.model'].each { k ->
def v = System.getProperty(k)
if (v != null) {
systemProperty k, v
}
}
+ // Exclude all broker ITs - they run on-demand via `e2e-test`.
+ (BROKER_IT_5X_CLASSES + BROKER_IT_4_CLASSES).each { cls -> filter { excludeTestsMatching cls } }
+}
+
+// --- isolated classpath for the 4.9 broker ITs (excludes storage-rocketmq5 so 4.9.5 wins) ---
+configurations {
+ rocketmq4TestRuntimeClasspath {
+ canBeResolved = true
+ canBeConsumed = false
+ extendsFrom configurations.implementation, configurations.runtimeOnly,
+ configurations.testImplementation, configurations.testRuntimeOnly
+ }
+}
+configurations.rocketmq4TestRuntimeClasspath.exclude(
+ group: 'org.apache.eventmesh', module: 'eventmesh-storage-rocketmq5')
+
+// 5.x/kafka/nacos broker ITs: full test classpath (storage-rocketmq5 present -> 5.5.0 wins).
+task e2eTest5x(type: Test) {
+ testClassesDirs = sourceSets.test.output.classesDirs
+ classpath = sourceSets.test.runtimeClasspath
+ useJUnitPlatform()
+ BROKER_IT_5X_CLASSES.each { cls -> filter { includeTestsMatching cls } }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+ ['it.storage', 'it.topic', 'it.namesrv', 'it.nacos', 'it.storage5', 'it.namesrv5',
+ 'it.kafka.bootstrap', 'it.kafka.user', 'it.kafka.password',
+ 'llm.base.url', 'llm.api.key', 'llm.model'].each { k ->
+ def v = System.getProperty(k)
+ if (v != null) { systemProperty k, v }
+ }
+}
+
+// 4.9 broker ITs: isolated classpath (storage-rocketmq5 excluded -> 4.9.5 wins).
+task e2eTest4(type: Test) {
+ testClassesDirs = sourceSets.test.output.classesDirs
+ classpath = configurations.rocketmq4TestRuntimeClasspath + sourceSets.main.output + sourceSets.test.output
+ useJUnitPlatform()
+ BROKER_IT_4_CLASSES.each { cls -> filter { includeTestsMatching cls } }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+ ['it.storage', 'it.topic', 'it.namesrv', 'it.nacos'].each { k ->
+ def v = System.getProperty(k)
+ if (v != null) { systemProperty k, v }
+ }
+}
+
+// On-demand: run ALL broker ITs (5.x + 4.9, in two separate JVMs).
+// `gradle :eventmesh-runtime:e2e-test`
+task('e2e-test') {
+ group = 'verification'
+ description = 'Runs all broker integration tests (auto-detect real brokers via E2EConfig).'
+ dependsOn 'e2eTest5x', 'e2eTest4'
+}
+
+// One-command demo runner: boots an in-process EventMeshApplication + StreamingAgent (mock LLM)
+// against a real RocketMQ 5.x broker, then drives the StreamingCallDemo client (eventmesh-examples).
+// Override the broker: -Dit.namesrv5=host:9876 (default = E2EConfig.ROCKETMQ5_NAMESRV).
+task runDemo(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.runtime.it.StreamingDemoRunner'
+ classpath = sourceSets.test.runtimeClasspath
+ classpath += project(':eventmesh-storage-plugin:eventmesh-storage-rocketmq5').sourceSets.main.output
+ classpath += project(':eventmesh-examples').sourceSets.main.output
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('it.') || k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('llm.')
+ }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+}
+
+// --- standalone launchers for the streaming demo (real-LLM, multi-process deployment) ---
+// Start in 3 separate terminals: startRuntime → startAgent (×N) → demo client.
+
+// Terminal 1: the EventMesh Runtime (session layer, dual channel mode). Blocks.
+task startRuntime(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.runtime.it.RuntimeLauncher'
+ classpath = sourceSets.test.runtimeClasspath
+ classpath += project(':eventmesh-storage-plugin:eventmesh-storage-rocketmq5').sourceSets.main.output
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('it.') || k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('em.')
+ }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+}
+
+// Terminal 2..N: one streaming Agent per invocation, connected to the real LLM gateway.
+// gradle :eventmesh-runtime:startAgent --args="agent1" -Dllm.api.key=sk-...
+task startAgent(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.runtime.it.AgentLauncher'
+ classpath = sourceSets.test.runtimeClasspath
+ classpath += project(':eventmesh-storage-plugin:eventmesh-storage-rocketmq5').sourceSets.main.output
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('it.') || k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('llm.') || k.startsWith('em.')
+ }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
+}
+
+// Demo client (pure HTTP). Requires startRuntime + at least one startAgent already running.
+// gradle :eventmesh-runtime:runDemoClient
+task runDemoClient(type: JavaExec) {
+ group = 'application'
+ mainClass = 'org.apache.eventmesh.cloudevents.demo.stream.StreamingCallDemo'
+ classpath = sourceSets.test.runtimeClasspath
+ classpath += project(':eventmesh-examples').sourceSets.main.output
+ systemProperties = System.properties.findAll { k, v ->
+ k.startsWith('it.') || k.startsWith('eventmesh') || k.startsWith('eventMesh') || k.startsWith('llm.') || k.startsWith('em.')
+ }
+ jvmArgs('-XX:MaxDirectMemorySize=512m', '-Xmx1024m', '-Dio.netty.noPreferDirect=true')
}
diff --git a/eventmesh-runtime/conf/eventmesh.properties b/eventmesh-runtime/conf/eventmesh.properties
index cdcd34e45..19e3a8522 100644
--- a/eventmesh-runtime/conf/eventmesh.properties
+++ b/eventmesh-runtime/conf/eventmesh.properties
@@ -19,6 +19,10 @@
# ===== RocketMQ 5.x (EVENTMESH_STORAGE_TYPE=rocketmq5) =====
#eventMesh.server.rocketmq5.namesrvAddr=127.0.0.1:9876
#eventMesh.server.rocketmq5.cluster=DefaultCluster
+# Lite-topic pull offset checkpoint interval (ms). Offsets are persisted to disk periodically so a
+# JVM crash replays at most this interval's worth of messages. Shutdown always persists regardless.
+# Set to 0 or negative to disable periodic checkpointing (only persist on shutdown).
+#eventmesh.rocketmq5.lite.checkpoint.interval.ms=5000
# ===== Kafka (EVENTMESH_STORAGE_TYPE=kafka) =====
eventMesh.server.kafka.namesrvAddr=localhost:9092
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2ACardHttpHandler.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2ACardHttpHandler.java
deleted file mode 100644
index 1e9f262c8..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2ACardHttpHandler.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.eventmesh.runtime.a2a;
-
-import org.apache.eventmesh.protocol.a2a.AgentIdentity;
-import org.apache.eventmesh.protocol.a2a.model.AgentCard;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpMethod;
-import io.netty.handler.codec.http.HttpRequest;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.codec.http.HttpVersion;
-import io.netty.handler.codec.http.QueryStringDecoder;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * HTTP handler for A2A Agent Card Registry API.
- * Matches EMQX's API pattern:
- * - GET /a2a/cards/list - list agent cards
- * - GET /a2a/cards/card/{org_id}/{unit_id}/{agent_id} - get specific card
- * - POST /a2a/cards/card/{org_id}/{unit_id}/{agent_id} - register card
- * - DELETE /a2a/cards/card/{org_id}/{unit_id}/{agent_id} - delete card
- */
-@Slf4j
-public class A2ACardHttpHandler {
-
- private static final ObjectMapper objectMapper = new ObjectMapper();
- private static final String PATH_LIST = "/a2a/cards/list";
- private static final String PATH_CARD_PREFIX = "/a2a/cards/card/";
-
- private final A2APublishSubscribeService a2aService;
-
- public A2ACardHttpHandler(A2APublishSubscribeService a2aService) {
- this.a2aService = a2aService;
- }
-
- public FullHttpResponse handle(HttpRequest httpRequest, ChannelHandlerContext ctx) throws Exception {
- String uri = httpRequest.uri();
- QueryStringDecoder decoder = new QueryStringDecoder(uri);
- String path = decoder.path();
- HttpMethod method = httpRequest.method();
-
- try {
- // Handle CORS preflight
- if (method == HttpMethod.OPTIONS) {
- return corsResponse();
- }
-
- if (path.equals(PATH_LIST)) {
- return handleList(decoder);
- }
-
- if (path.startsWith(PATH_CARD_PREFIX)) {
- String[] segments = path.substring(PATH_CARD_PREFIX.length()).split("/");
- if (segments.length != 3) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST,
- errorBody("Invalid path. Expected /a2a/cards/card/{org_id}/{unit_id}/{agent_id}"));
- }
-
- String orgId = segments[0];
- String unitId = segments[1];
- String agentId = segments[2];
-
- if (method == HttpMethod.GET) {
- return handleGet(orgId, unitId, agentId);
- } else if (method == HttpMethod.POST) {
- return handleRegister(orgId, unitId, agentId, httpRequest);
- } else if (method == HttpMethod.DELETE) {
- return handleDelete(orgId, unitId, agentId);
- }
- }
-
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Not found: " + path));
- } catch (Exception e) {
- log.error("A2A card handler error: {}", e.getMessage(), e);
- return jsonResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, errorBody(e.getMessage()));
- }
- }
-
- private FullHttpResponse handleList(QueryStringDecoder decoder) throws Exception {
- String orgId = getQueryParam(decoder, "org_id");
- String unitId = getQueryParam(decoder, "unit_id");
- String agentId = getQueryParam(decoder, "agent_id");
-
- List<A2APublishSubscribeService.CardEntry> cards = a2aService.listCards(orgId, unitId, agentId);
- String body = objectMapper.writeValueAsString(cards);
- return jsonResponse(HttpResponseStatus.OK, body);
- }
-
- private FullHttpResponse handleGet(String orgId, String unitId, String agentId) throws Exception {
- AgentIdentity identity = AgentIdentity.builder()
- .orgId(orgId).unitId(unitId).agentId(agentId).build();
- A2APublishSubscribeService.CardEntry card = a2aService.getCard(identity);
-
- if (card == null) {
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Card not found"));
- }
-
- String body = objectMapper.writeValueAsString(formatCardOut(card));
- return jsonResponse(HttpResponseStatus.OK, body);
- }
-
- private FullHttpResponse handleRegister(String orgId, String unitId, String agentId, HttpRequest request) throws Exception {
- // Read body from request
- String requestBody = "";
- if (request instanceof io.netty.handler.codec.http.FullHttpRequest) {
- ByteBuf content = ((io.netty.handler.codec.http.FullHttpRequest) request).content();
- requestBody = content.toString(StandardCharsets.UTF_8);
- }
-
- AgentCard card;
- try {
- card = objectMapper.readValue(requestBody, AgentCard.class);
- } catch (Exception e) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody("Invalid card JSON: " + e.getMessage()));
- }
-
- AgentIdentity identity = AgentIdentity.builder()
- .orgId(orgId).unitId(unitId).agentId(agentId).build();
- A2APublishSubscribeService.RegistrationResult result = a2aService.registerCard(identity, card);
-
- if (result.isSuccess()) {
- return jsonResponse(HttpResponseStatus.NO_CONTENT, "");
- } else {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody(result.getErrorMessage()));
- }
- }
-
- private FullHttpResponse handleDelete(String orgId, String unitId, String agentId) throws Exception {
- AgentIdentity identity = AgentIdentity.builder()
- .orgId(orgId).unitId(unitId).agentId(agentId).build();
- boolean deleted = a2aService.deleteCard(identity);
-
- if (deleted) {
- return jsonResponse(HttpResponseStatus.NO_CONTENT, "");
- } else {
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Card not found"));
- }
- }
-
- private Map<String, Object> formatCardOut(A2APublishSubscribeService.CardEntry entry) {
- Map<String, Object> out = new LinkedHashMap<>();
- out.put("namespace", entry.getNamespace());
- out.put("id", entry.getId());
- out.put("name", entry.getName());
- out.put("version", entry.getVersion());
- out.put("description", entry.getDescription());
- out.put("status", entry.getStatus());
- return out;
- }
-
- private String getQueryParam(QueryStringDecoder decoder, String key) {
- List<String> values = decoder.parameters().get(key);
- return (values != null && !values.isEmpty()) ? values.get(0) : null;
- }
-
- private String errorBody(String message) {
- try {
- return objectMapper.writeValueAsString(Collections.singletonMap("error", message));
- } catch (Exception e) {
- return "{\"error\":\"" + message + "\"}";
- }
- }
-
- private FullHttpResponse jsonResponse(HttpResponseStatus status, String body) {
- ByteBuf content = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
- DefaultFullHttpResponse response = new DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1, status, content);
- response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
- setCorsHeaders(response);
- return response;
- }
-
- private FullHttpResponse corsResponse() {
- DefaultFullHttpResponse response = new DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
- setCorsHeaders(response);
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
- return response;
- }
-
- private void setCorsHeaders(FullHttpResponse response) {
- response.headers().set("Access-Control-Allow-Origin", "*");
- response.headers().set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
- response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization");
- response.headers().set("Access-Control-Max-Age", "3600");
- }
-}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayHttpHandler.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayHttpHandler.java
deleted file mode 100644
index 1fdcef901..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayHttpHandler.java
+++ /dev/null
@@ -1,648 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.eventmesh.runtime.a2a;
-
-import java.nio.charset.StandardCharsets;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.DefaultHttpContent;
-import io.netty.handler.codec.http.DefaultHttpResponse;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpHeaderValues;
-import io.netty.handler.codec.http.HttpMethod;
-import io.netty.handler.codec.http.HttpRequest;
-import io.netty.handler.codec.http.HttpResponse;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.codec.http.HttpVersion;
-import io.netty.handler.codec.http.LastHttpContent;
-import io.netty.handler.codec.http.QueryStringDecoder;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * HTTP handler for A2A Gateway API.
- *
- * <p>Endpoints:
- * <ul>
- * <li>POST /a2a/tasks - submit a new task (sync or async)</li>
- * <li>GET /a2a/tasks - list tasks (optional ?state=COMPLETED&limit=100&offset=0)</li>
- * <li>GET /a2a/tasks/{taskId} - get task status</li>
- * <li>DELETE /a2a/tasks/{taskId} - cancel a task</li>
- * <li>GET /a2a/tasks/{taskId}/wait - wait for task result (long-poll)</li>
- * <li>GET /a2a/tasks/{taskId}/stream - SSE stream of task status updates</li>
- * <li>GET /a2a/agents - list registered agents</li>
- * <li>POST /a2a/heartbeat - agent heartbeat</li>
- * <li>GET /a2a/health - health check (for liveness/readiness probes)</li>
- * <li>OPTIONS * - CORS preflight</li>
- * </ul>
- */
-@Slf4j
-public class A2AGatewayHttpHandler {
-
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- private static final String PATH_TASKS = "/a2a/tasks";
- private static final String PATH_AGENTS = "/a2a/agents";
- private static final String PATH_HEARTBEAT = "/a2a/heartbeat";
- private static final String PATH_HEALTH = "/a2a/health";
-
- private static final long DEFAULT_WAIT_TIMEOUT_MS = 30_000L;
-
- private final A2AGatewayService gatewayService;
- private final A2APublishSubscribeService a2aService;
-
- public A2AGatewayHttpHandler(A2AGatewayService gatewayService,
- A2APublishSubscribeService a2aService) {
- this.gatewayService = gatewayService;
- this.a2aService = a2aService;
- }
-
- /**
- * Handles an HTTP request. Returns a FullHttpResponse, or null if the response was written
- * directly to the channel (e.g. SSE streaming).
- */
- public FullHttpResponse handle(HttpRequest httpRequest, ChannelHandlerContext ctx) throws Exception {
- String uri = httpRequest.uri();
- QueryStringDecoder decoder = new QueryStringDecoder(uri);
- String path = decoder.path();
- HttpMethod method = httpRequest.method();
-
- try {
- // Handle CORS preflight
- if (method == HttpMethod.OPTIONS) {
- return corsResponse(HttpResponseStatus.OK);
- }
-
- // POST /a2a/tasks
- if (PATH_TASKS.equals(path) && method == HttpMethod.POST) {
- return handleSubmitTask(httpRequest, decoder);
- }
-
- // GET /a2a/tasks (list tasks with optional pagination)
- if (PATH_TASKS.equals(path) && method == HttpMethod.GET) {
- return handleListTasks(decoder);
- }
-
- // /a2a/tasks/{taskId} or /a2a/tasks/{taskId}/wait
- if (path.startsWith(PATH_TASKS + "/")) {
- String suffix = path.substring(PATH_TASKS.length() + 1);
- String[] parts = suffix.split("/", 2);
- String taskId = parts[0];
-
- if (taskId.isEmpty()) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody("Missing taskId"));
- }
-
- // GET /a2a/tasks/{taskId}
- if (method == HttpMethod.GET && parts.length == 1) {
- return handleGetTask(taskId);
- }
-
- // DELETE /a2a/tasks/{taskId}
- if (method == HttpMethod.DELETE && parts.length == 1) {
- return handleCancelTask(taskId);
- }
-
- // GET /a2a/tasks/{taskId}/wait
- if (method == HttpMethod.GET && parts.length == 2 && "wait".equals(parts[1])) {
- return handleWaitTask(taskId, decoder);
- }
-
- // GET /a2a/tasks/{taskId}/stream (SSE)
- if (method == HttpMethod.GET && parts.length == 2 && "stream".equals(parts[1])) {
- return handleStreamTask(taskId, ctx);
- }
- }
-
- // GET /a2a/health
- if (PATH_HEALTH.equals(path) && method == HttpMethod.GET) {
- return handleHealth();
- }
-
- // GET /a2a/agents
- if (PATH_AGENTS.equals(path) && method == HttpMethod.GET) {
- return handleListAgents();
- }
-
- // POST /a2a/heartbeat
- if (PATH_HEARTBEAT.equals(path) && method == HttpMethod.POST) {
- return handleHeartbeat(httpRequest);
- }
-
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Not found: " + path));
- } catch (Exception e) {
- log.error("A2A gateway handler error: {}", e.getMessage(), e);
- return jsonResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, errorBody(e.getMessage()));
- }
- }
-
- // =========================================================================
- // POST /a2a/tasks
- // =========================================================================
-
- private FullHttpResponse handleSubmitTask(HttpRequest request, QueryStringDecoder decoder) throws Exception {
- String body = readBody(request);
- TaskRequest taskRequest;
- try {
- taskRequest = objectMapper.readValue(body, TaskRequest.class);
- } catch (Exception e) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody("Invalid JSON: " + e.getMessage()));
- }
-
- if (taskRequest.getTargetAgent() == null || taskRequest.getTargetAgent().isEmpty()) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody("targetAgent is required"));
- }
- if (taskRequest.getMessage() == null) {
- taskRequest.setMessage("");
- }
-
- String mode = getQueryParam(decoder, "mode");
- String parentTaskId = taskRequest.getParentTaskId();
-
- // Generate taskId first so we can include it in the response
- String taskId = "task-" + java.util.UUID.randomUUID().toString().substring(0, 8);
-
- // Submit the task with the explicit taskId
- java.util.concurrent.CompletableFuture<A2AGatewayService.TaskResult> future =
- gatewayService.submitTask(taskId, taskRequest.getTargetAgent(), taskRequest.getMessage(), parentTaskId);
-
- // For sync mode, wait for result
- if ("sync".equalsIgnoreCase(mode) || mode == null) {
- try {
- A2AGatewayService.TaskResult result = future.get(DEFAULT_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", taskId);
- response.put("state", result.getState().name());
- response.put("data", result.getData());
- if (result.getErrorMessage() != null) {
- response.put("error", result.getErrorMessage());
- }
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- } catch (java.util.concurrent.TimeoutException e) {
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", taskId);
- response.put("state", "TIMEOUT");
- response.put("error", "Task accepted but timed out waiting for response");
- return jsonResponse(HttpResponseStatus.ACCEPTED, objectMapper.writeValueAsString(response));
- } catch (java.util.concurrent.ExecutionException e) {
- // Unwrap cause to determine correct status code
- Throwable cause = e.getCause() != null ? e.getCause() : e;
- if (cause instanceof IllegalArgumentException || cause instanceof IllegalStateException) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST,
- errorBody(cause.getMessage()));
- }
- return jsonResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR,
- errorBody("Task failed: " + cause.getMessage()));
- } catch (Exception e) {
- return jsonResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR,
- errorBody("Task failed: " + e.getMessage()));
- }
- } else {
- // Async mode: return task ID immediately
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", taskId);
- response.put("status", "accepted");
- response.put("message", "Task submitted. Use GET /a2a/tasks/" + taskId + " to check status.");
- return jsonResponse(HttpResponseStatus.ACCEPTED, objectMapper.writeValueAsString(response));
- }
- }
-
- // =========================================================================
- // GET /a2a/tasks (list with optional pagination)
- // =========================================================================
-
- private FullHttpResponse handleListTasks(QueryStringDecoder decoder) throws Exception {
- String stateFilter = getQueryParam(decoder, "state");
- int limit = parseIntQueryParam(decoder, "limit", 100, 1, 1_000);
- int offset = parseIntQueryParam(decoder, "offset", 0, 0, Integer.MAX_VALUE);
-
- List<TaskRegistry.TaskEntry> allTasks = gatewayService.getTaskRegistry().listTasks();
- List<Map<String, Object>> filteredTasks = new java.util.ArrayList<>();
- for (TaskRegistry.TaskEntry entry : allTasks) {
- if (stateFilter != null && !stateFilter.equalsIgnoreCase(entry.getState().name())) {
- continue;
- }
- Map<String, Object> task = new LinkedHashMap<>();
- task.put("taskId", entry.getTaskId());
- task.put("state", entry.getState().name());
- task.put("targetAgent", entry.getTargetAgent());
- task.put("parentTaskId", entry.getParentTaskId());
- task.put("createdAt", entry.getCreatedAt());
- task.put("updatedAt", entry.getUpdatedAt());
- filteredTasks.add(task);
- }
-
- int total = filteredTasks.size();
- int fromIndex = Math.min(offset, total);
- int toIndex = Math.min(fromIndex + limit, total);
- List<Map<String, Object>> page = filteredTasks.subList(fromIndex, toIndex);
-
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("total", total);
- response.put("count", page.size());
- response.put("limit", limit);
- response.put("offset", offset);
- response.put("hasMore", toIndex < total);
- response.put("tasks", page);
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- }
-
- // =========================================================================
- // GET /a2a/tasks/{taskId}
- // =========================================================================
-
- private FullHttpResponse handleGetTask(String taskId) throws Exception {
- TaskRegistry.TaskEntry entry = gatewayService.getTaskStatus(taskId);
- if (entry == null) {
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Task not found: " + taskId));
- }
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", entry.getTaskId());
- response.put("state", entry.getState().name());
- response.put("targetAgent", entry.getTargetAgent());
- response.put("gatewayId", entry.getGatewayId());
- response.put("parentTaskId", entry.getParentTaskId());
- response.put("createdAt", entry.getCreatedAt());
- response.put("updatedAt", entry.getUpdatedAt());
- if (entry.getResult() != null) {
- response.put("result", entry.getResult());
- }
- if (entry.getErrorMessage() != null) {
- response.put("error", entry.getErrorMessage());
- }
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- }
-
- // =========================================================================
- // DELETE /a2a/tasks/{taskId}
- // =========================================================================
-
- private FullHttpResponse handleCancelTask(String taskId) throws Exception {
- boolean cancelled = gatewayService.cancelTask(taskId);
- if (cancelled) {
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", taskId);
- response.put("state", "CANCELLED");
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- }
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Task not found or cannot be cancelled: " + taskId));
- }
-
- // =========================================================================
- // GET /a2a/tasks/{taskId}/wait
- // =========================================================================
-
- private FullHttpResponse handleWaitTask(String taskId, QueryStringDecoder decoder) throws Exception {
- String timeoutStr = getQueryParam(decoder, "timeout");
- long timeout = timeoutStr != null ? Long.parseLong(timeoutStr) : DEFAULT_WAIT_TIMEOUT_MS;
-
- TaskRegistry.TaskEntry entry = gatewayService.getTaskStatus(taskId);
- if (entry == null) {
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Task not found: " + taskId));
- }
-
- // If already in a terminal state, return immediately
- if (isTerminalState(entry.getState())) {
- return handleGetTask(taskId);
- }
-
- // Register a status subscriber and wait
- java.util.concurrent.CompletableFuture<TaskRegistry.TaskEntry> waitFuture = new java.util.concurrent.CompletableFuture<>();
- A2AGatewayService.StatusSubscriber subscriber = (tid, state, data) -> {
- if ("completed".equals(state) || "failed".equals(state) || "cancelled".equals(state)) {
- waitFuture.complete(gatewayService.getTaskStatus(tid));
- }
- };
- gatewayService.registerStatusSubscriber(taskId, subscriber);
-
- try {
- TaskRegistry.TaskEntry result = waitFuture.get(timeout, TimeUnit.MILLISECONDS);
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("taskId", result.getTaskId());
- response.put("state", result.getState().name());
- if (result.getResult() != null) {
- response.put("result", result.getResult());
- }
- if (result.getErrorMessage() != null) {
- response.put("error", result.getErrorMessage());
- }
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- } catch (java.util.concurrent.TimeoutException e) {
- return jsonResponse(HttpResponseStatus.OK,
- errorBody("Task still in progress. State: " + entry.getState()));
- } catch (Exception e) {
- return jsonResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR,
- errorBody("Wait failed: " + e.getMessage()));
- } finally {
- gatewayService.unregisterStatusSubscriber(taskId, subscriber);
- }
- }
-
- // =========================================================================
- // GET /a2a/tasks/{taskId}/stream (SSE)
- // =========================================================================
-
- /**
- * Streams task status updates via Server-Sent Events.
- * Writes directly to the channel and returns null (no FullHttpResponse).
- */
- private FullHttpResponse handleStreamTask(String taskId, ChannelHandlerContext ctx) throws Exception {
- TaskRegistry.TaskEntry entry = gatewayService.getTaskStatus(taskId);
- if (entry == null) {
- return jsonResponse(HttpResponseStatus.NOT_FOUND, errorBody("Task not found: " + taskId));
- }
-
- // Write SSE response headers
- HttpResponse headers = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
- headers.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/event-stream; charset=utf-8");
- headers.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache");
- headers.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
- headers.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
- headers.headers().set("Access-Control-Allow-Origin", "*");
- ctx.writeAndFlush(headers);
-
- // Send initial state
- sendSseEvent(ctx, taskId, entry.getState().name(),
- entry.getResult() != null ? entry.getResult() : entry.getErrorMessage());
-
- // If already terminal, close stream
- if (isTerminalState(entry.getState())) {
- ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
- ctx.close();
- return null;
- }
-
- // Schedule periodic heartbeat to keep connection alive through proxies/load balancers
- java.util.concurrent.ScheduledExecutorService heartbeatExecutor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor(r -> {
- Thread t = new Thread(r, "a2a-sse-heartbeat");
- t.setDaemon(true);
- return t;
- });
- heartbeatExecutor.scheduleAtFixedRate(() -> {
- try {
- ByteBuf hb = Unpooled.copiedBuffer(": heartbeat\n\n", StandardCharsets.UTF_8);
- ctx.writeAndFlush(new DefaultHttpContent(hb));
- } catch (Exception e) {
- // Channel may have been closed; ignore
- }
- }, 15, 15, TimeUnit.SECONDS);
-
- // Register subscriber for ongoing updates
- A2AGatewayService.StatusSubscriber subscriber = (tid, state, data) -> {
- sendSseEvent(ctx, tid, state, data);
- if ("completed".equals(state) || "failed".equals(state) || "cancelled".equals(state)) {
- heartbeatExecutor.shutdownNow();
- ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
- .addListener(ChannelFutureListener.CLOSE);
- }
- };
- gatewayService.registerStatusSubscriber(taskId, subscriber);
-
- // Remove subscriber and stop heartbeat when channel closes
- ctx.channel().closeFuture().addListener(future -> {
- gatewayService.unregisterStatusSubscriber(taskId, subscriber);
- heartbeatExecutor.shutdownNow();
- });
-
- return null; // Response already written to channel
- }
-
- private void sendSseEvent(ChannelHandlerContext ctx, String taskId, String state, String data) {
- Map<String, Object> event = new LinkedHashMap<>();
- event.put("taskId", taskId);
- event.put("state", state);
- if (data != null) {
- event.put("data", data);
- }
- String json;
- try {
- json = objectMapper.writeValueAsString(event);
- } catch (Exception e) {
- json = "{\"error\":\"" + e.getMessage() + "\"}";
- }
- String sse = "data: " + json + "\n\n";
- ByteBuf buf = Unpooled.copiedBuffer(sse, StandardCharsets.UTF_8);
- ctx.writeAndFlush(new DefaultHttpContent(buf));
- }
-
- // =========================================================================
- // GET /a2a/health
- // =========================================================================
-
- private FullHttpResponse handleHealth() throws Exception {
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("status", "UP");
- response.put("gatewayId", gatewayService.getGatewayId());
- response.put("namespace", gatewayService.getNamespace());
- response.put("taskCount", gatewayService.getTaskRegistry().size());
- response.put("agentCount", a2aService.listAllCards().size());
- response.put("timestamp", System.currentTimeMillis());
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- }
-
- // =========================================================================
- // GET /a2a/agents
- // =========================================================================
-
- private FullHttpResponse handleListAgents() throws Exception {
- return jsonResponse(HttpResponseStatus.OK,
- objectMapper.writeValueAsString(a2aService.listAllCards()));
- }
-
- // =========================================================================
- // POST /a2a/heartbeat
- // =========================================================================
-
- private FullHttpResponse handleHeartbeat(HttpRequest request) throws Exception {
- String body = readBody(request);
- HeartbeatRequest hb;
- try {
- hb = objectMapper.readValue(body, HeartbeatRequest.class);
- } catch (Exception e) {
- return jsonResponse(HttpResponseStatus.BAD_REQUEST, errorBody("Invalid JSON: " + e.getMessage()));
- }
-
- org.apache.eventmesh.protocol.a2a.AgentIdentity identity =
- org.apache.eventmesh.protocol.a2a.AgentIdentity.builder()
- .orgId(hb.getOrgId())
- .unitId(hb.getUnitId())
- .agentId(hb.getAgentId())
- .build();
-
- boolean refreshed = a2aService.heartbeat(identity);
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("ok", refreshed);
- if (!refreshed) {
- response.put("error", "Agent not registered. POST /a2a/cards/card/{org}/{unit}/{agent} first.");
- }
- return jsonResponse(HttpResponseStatus.OK, objectMapper.writeValueAsString(response));
- }
-
- // =========================================================================
- // Helpers
- // =========================================================================
-
- private boolean isTerminalState(TaskRegistry.TaskState state) {
- return state == TaskRegistry.TaskState.COMPLETED
- || state == TaskRegistry.TaskState.FAILED
- || state == TaskRegistry.TaskState.CANCELLED;
- }
-
- private String readBody(HttpRequest request) {
- if (request instanceof io.netty.handler.codec.http.FullHttpRequest) {
- ByteBuf content = ((io.netty.handler.codec.http.FullHttpRequest) request).content();
- return content.toString(StandardCharsets.UTF_8);
- }
- return "";
- }
-
- private String getQueryParam(QueryStringDecoder decoder, String key) {
- java.util.List<String> values = decoder.parameters().get(key);
- return (values != null && !values.isEmpty()) ? values.get(0) : null;
- }
-
- private int parseIntQueryParam(QueryStringDecoder decoder, String key,
- int defaultValue, int minValue, int maxValue) {
- String value = getQueryParam(decoder, key);
- if (value == null || value.isEmpty()) {
- return defaultValue;
- }
- try {
- int parsed = Integer.parseInt(value);
- if (parsed < minValue) {
- return minValue;
- }
- if (parsed > maxValue) {
- return maxValue;
- }
- return parsed;
- } catch (NumberFormatException e) {
- return defaultValue;
- }
- }
-
- private String errorBody(String message) {
- try {
- return objectMapper.writeValueAsString(java.util.Collections.singletonMap("error", message));
- } catch (Exception e) {
- return "{\"error\":\"" + message + "\"}";
- }
- }
-
- private FullHttpResponse jsonResponse(HttpResponseStatus status, String body) {
- ByteBuf content = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
- DefaultFullHttpResponse response = new DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1, status, content);
- response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
- setCorsHeaders(response);
- return response;
- }
-
- private FullHttpResponse corsResponse(HttpResponseStatus status) {
- DefaultFullHttpResponse response = new DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1, status);
- setCorsHeaders(response);
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
- return response;
- }
-
- private void setCorsHeaders(HttpResponse response) {
- response.headers().set("Access-Control-Allow-Origin", "*");
- response.headers().set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
- response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization");
- response.headers().set("Access-Control-Max-Age", "3600");
- }
-
- // =========================================================================
- // Request / Response DTOs
- // =========================================================================
-
- public static class TaskRequest {
-
- private String targetAgent;
- private String message;
- private String parentTaskId;
-
- public String getTargetAgent() {
- return targetAgent;
- }
-
- public void setTargetAgent(String targetAgent) {
- this.targetAgent = targetAgent;
- }
-
- public String getMessage() {
- return message;
- }
-
- public void setMessage(String message) {
- this.message = message;
- }
-
- public String getParentTaskId() {
- return parentTaskId;
- }
-
- public void setParentTaskId(String parentTaskId) {
- this.parentTaskId = parentTaskId;
- }
- }
-
- public static class HeartbeatRequest {
-
- private String orgId;
- private String unitId;
- private String agentId;
-
- public String getOrgId() {
- return orgId;
- }
-
- public void setOrgId(String orgId) {
- this.orgId = orgId;
- }
-
- public String getUnitId() {
- return unitId;
- }
-
- public void setUnitId(String unitId) {
- this.unitId = unitId;
- }
-
- public String getAgentId() {
- return agentId;
- }
-
- public void setAgentId(String agentId) {
- this.agentId = agentId;
- }
- }
-}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayServer.java
deleted file mode 100644
index 9a2410614..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayServer.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.eventmesh.runtime.a2a;
-
-import org.apache.eventmesh.protocol.a2a.A2AProtocolConstants;
-import org.apache.eventmesh.protocol.a2a.A2ATopicFactory;
-import org.apache.eventmesh.protocol.a2a.AgentIdentity;
-import org.apache.eventmesh.protocol.a2a.model.AgentCard;
-import org.apache.eventmesh.protocol.a2a.model.AgentSkill;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.UUID;
-
-import io.cloudevents.CloudEvent;
-import io.cloudevents.core.builder.CloudEventBuilder;
-import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInitializer;
-import io.netty.channel.ChannelOption;
-import io.netty.channel.EventLoopGroup;
-import io.netty.channel.nio.NioEventLoopGroup;
-import io.netty.channel.socket.SocketChannel;
-import io.netty.channel.socket.nio.NioServerSocketChannel;
-import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.FullHttpRequest;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpObjectAggregator;
-import io.netty.handler.codec.http.HttpRequestDecoder;
-import io.netty.handler.codec.http.HttpResponseEncoder;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.codec.http.HttpVersion;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Standalone A2A Gateway Server.
- *
- * <p>Starts an embedded Netty HTTP server that exposes the A2A Gateway REST API.
- * Pre-registers a mock "weather-agent" that auto-responds to task requests.
- *
- * <p>Usage:
- * <pre>
- * java org.apache.eventmesh.runtime.a2a.A2AGatewayServer [port]
- * </pre>
- *
- * <p>Then run the client demo (A2AGatewayDemo) to submit tasks via HTTP.
- */
-@Slf4j
-public class A2AGatewayServer {
-
- private static final String NAMESPACE = "global";
- private static final String GATEWAY_ID = "demo-gateway";
- private static final int DEFAULT_PORT = 10105;
-
- private final int port;
- private EventLoopGroup bossGroup;
- private EventLoopGroup workerGroup;
- private ChannelFuture serverChannel;
-
- private InMemoryA2AMessageTransport transport;
- private TaskRegistry taskRegistry;
- private A2APublishSubscribeService a2aService;
- private A2AGatewayService gatewayService;
- private A2AGatewayHttpHandler gatewayHandler;
- private A2ACardHttpHandler cardHandler;
-
- public A2AGatewayServer(int port) {
- this.port = port;
- }
-
- public void start() throws Exception {
- // 1. Initialize components
- transport = new InMemoryA2AMessageTransport();
- taskRegistry = new TaskRegistry();
- a2aService = new A2APublishSubscribeService();
- a2aService.init();
- a2aService.start();
-
- gatewayService = new A2AGatewayService(NAMESPACE, GATEWAY_ID, transport, taskRegistry, a2aService);
- gatewayService.start();
-
- gatewayHandler = new A2AGatewayHttpHandler(gatewayService, a2aService);
- cardHandler = new A2ACardHttpHandler(a2aService);
-
- // 2. Pre-register a mock weather agent
- registerMockWeatherAgent();
-
- // 3. Start Netty HTTP server
- bossGroup = new NioEventLoopGroup(1);
- workerGroup = new NioEventLoopGroup();
-
- ServerBootstrap bootstrap = new ServerBootstrap();
- bootstrap.group(bossGroup, workerGroup)
- .channel(NioServerSocketChannel.class)
- .childHandler(new ChannelInitializer<SocketChannel>() {
-
- @Override
- protected void initChannel(SocketChannel ch) {
- ch.pipeline()
- .addLast(new HttpRequestDecoder())
- .addLast(new HttpObjectAggregator(65536))
- .addLast(new HttpResponseEncoder())
- .addLast(new A2AGatewayRequestHandler(gatewayHandler, cardHandler));
- }
- })
- .option(ChannelOption.SO_BACKLOG, 128)
- .childOption(ChannelOption.SO_KEEPALIVE, true);
-
- serverChannel = bootstrap.bind(port).sync();
- log.info("=== A2A Gateway Server started on port {} ===", port);
- log.info("Gateway ID: {}, Namespace: {}", GATEWAY_ID, NAMESPACE);
- log.info("Endpoints:");
- log.info(" POST /a2a/tasks - submit task (sync/async)");
- log.info(" GET /a2a/tasks - list tasks (?state=COMPLETED&limit=100&offset=0)");
- log.info(" GET /a2a/tasks/{{taskId}} - get task status");
- log.info(" DELETE /a2a/tasks/{{taskId}} - cancel task");
- log.info(" GET /a2a/tasks/{{taskId}}/wait - wait for result (long-poll)");
- log.info(" GET /a2a/tasks/{{taskId}}/stream - SSE stream of task status updates");
- log.info(" GET /a2a/agents - list registered agents");
- log.info(" POST /a2a/heartbeat - agent heartbeat");
- log.info(" GET /a2a/health - health check");
- log.info(" GET /a2a/cards/list - list agent cards");
- log.info(" POST /a2a/cards/card/{{org}}/{{unit}}/{{agent}} - register card");
- }
-
- /**
- * Registers a mock weather agent that auto-responds to task requests.
- */
- private void registerMockWeatherAgent() throws Exception {
- AgentCard weatherCard = AgentCard.builder()
- .name("weather-agent")
- .description("A weather agent that can tell you the weather")
- .version("1.0.0")
- .supportedInterfaces(Arrays.asList(
- org.apache.eventmesh.protocol.a2a.model.AgentInterface.builder()
- .url("http://localhost:" + port + "/a2a")
- .protocolBinding("JSONRPC")
- .protocolVersion("0.3")
- .build()))
- .capabilities(org.apache.eventmesh.protocol.a2a.model.AgentCapabilities.builder()
- .streaming(false)
- .pushNotifications(false)
- .build())
- .skills(Arrays.asList(
- AgentSkill.builder()
- .id("get-weather")
- .name("Get Weather")
- .description("Get the current weather for a city")
- .tags(Arrays.asList("weather", "test"))
- .build()))
- .defaultInputModes(Arrays.asList("text/plain"))
- .defaultOutputModes(Arrays.asList("text/plain"))
- .build();
-
- AgentIdentity identity = AgentIdentity.builder()
- .orgId("default").unitId("default").agentId("weather-agent").build();
- A2APublishSubscribeService.RegistrationResult regResult =
- a2aService.registerCard(identity, weatherCard);
- if (!regResult.isSuccess()) {
- log.warn("Failed to register weather-agent card: {}", regResult.getErrorMessage());
- }
-
- // Subscribe to agent request topic and auto-respond
- String requestTopic = A2ATopicFactory.agentRequestTopic(NAMESPACE, "weather-agent");
- transport.subscribe(requestTopic, (topic, event) -> {
- String taskId = event.getId();
- String message = event.getData() != null
- ? new String(event.getData().toBytes(), StandardCharsets.UTF_8)
- : "";
-
- log.info("[Weather Agent] Received request: taskId={}, message={}", taskId, message);
-
- // Simulate processing delay
- try {
- Thread.sleep(300);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- // Publish response
- String gwId = extractGatewayId(event);
- String responseTopic = A2ATopicFactory.gatewayResponseTopic(NAMESPACE, gwId, taskId);
- String result = "The weather in " + message + " is sunny, 25°C";
- CloudEvent responseEvent = CloudEventBuilder.v1()
- .withId(UUID.randomUUID().toString())
- .withType(A2AProtocolConstants.CE_TYPE_PREFIX + "task.response")
- .withSource(java.net.URI.create("agent/weather-agent"))
- .withDataContentType("application/json")
- .withData(result.getBytes(StandardCharsets.UTF_8))
- .withExtension(A2AProtocolConstants.CE_EXTENSION_A2A_METHOD, A2AProtocolConstants.OP_SEND_MESSAGE)
- .build();
-
- try {
- transport.publish(responseTopic, responseEvent);
- log.info("[Weather Agent] Response published: taskId={}", taskId);
- } catch (Exception e) {
- log.error("[Weather Agent] Failed to publish response", e);
- }
- });
-
- log.info("Mock weather-agent registered and listening for requests.");
- }
-
- private String extractGatewayId(CloudEvent event) {
- if (event.getSource() == null) {
- return "default-gateway";
- }
- String source = event.getSource().toString();
- if (source.startsWith("gateway/")) {
- return source.substring("gateway/".length());
- }
- return "default-gateway";
- }
-
- /**
- * Returns the actual bound port. Useful when port 0 was specified to let the OS pick a port.
- */
- public int getBoundPort() {
- if (serverChannel != null) {
- return ((io.netty.channel.socket.ServerSocketChannel) serverChannel.channel()).localAddress().getPort();
- }
- return port;
- }
-
- public void shutdown() throws Exception {
- if (serverChannel != null) {
- serverChannel.channel().close().sync();
- }
- if (workerGroup != null) {
- workerGroup.shutdownGracefully();
- }
- if (bossGroup != null) {
- bossGroup.shutdownGracefully();
- }
- if (gatewayService != null) {
- gatewayService.shutdown();
- }
- if (a2aService != null) {
- a2aService.shutdown();
- }
- log.info("A2A Gateway Server shut down.");
- }
-
- public static void main(String[] args) throws Exception {
- int port = args.length > 0 ? Integer.parseInt(args[0]) : DEFAULT_PORT;
-
- A2AGatewayServer server = new A2AGatewayServer(port);
-
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- try {
- server.shutdown();
- } catch (Exception e) {
- log.error("Error during shutdown", e);
- }
- }));
-
- server.start();
-
- // Keep running until interrupted
- Thread.currentThread().join();
- }
-
- // =========================================================================
- // Netty Channel Handler
- // =========================================================================
-
- private static class A2AGatewayRequestHandler extends io.netty.channel.SimpleChannelInboundHandler<FullHttpRequest> {
-
- private final A2AGatewayHttpHandler gatewayHandler;
- private final A2ACardHttpHandler cardHandler;
-
- A2AGatewayRequestHandler(A2AGatewayHttpHandler gatewayHandler, A2ACardHttpHandler cardHandler) {
- this.gatewayHandler = gatewayHandler;
- this.cardHandler = cardHandler;
- }
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
- String uri = request.uri();
- try {
- FullHttpResponse response;
- if (uri.startsWith("/a2a/cards")) {
- response = cardHandler.handle(request, ctx);
- } else if (uri.startsWith("/a2a/")) {
- response = gatewayHandler.handle(request, ctx);
- } else {
- response = jsonError(HttpResponseStatus.NOT_FOUND, "Not found: " + uri);
- }
-
- // null means the handler wrote the response directly (e.g. SSE streaming)
- if (response != null) {
- ctx.writeAndFlush(response);
- }
- } catch (Exception e) {
- log.error("Error handling request: {} - {}", uri, e.getMessage(), e);
- FullHttpResponse errorResponse = jsonError(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage());
- ctx.writeAndFlush(errorResponse);
- }
- }
-
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
- log.error("Channel exception: {}", cause.getMessage(), cause);
- ctx.close();
- }
-
- private FullHttpResponse jsonError(HttpResponseStatus status, String message) {
- String body = "{\"error\":\"" + message + "\"}";
- DefaultFullHttpResponse response = new DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(body, StandardCharsets.UTF_8));
- response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
- response.headers().set("Access-Control-Allow-Origin", "*");
- response.headers().set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
- response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization");
- return response;
- }
- }
-}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayService.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayService.java
deleted file mode 100644
index e70db7107..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2AGatewayService.java
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.eventmesh.runtime.a2a;
-
-import org.apache.eventmesh.protocol.a2a.A2AMessageTransport;
-import org.apache.eventmesh.protocol.a2a.A2AProtocolConstants;
-import org.apache.eventmesh.protocol.a2a.A2ATopicFactory;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import io.cloudevents.CloudEvent;
-import io.cloudevents.core.builder.CloudEventBuilder;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * A2A Gateway Service: orchestrates task submission, response handling, and SSE streaming.
- *
- * <p>This is the core component that ties together:
- * <ul>
- * <li>{@link A2AMessageTransport} for pub/sub</li>
- * <li>{@link TaskRegistry} for task lifecycle</li>
- * <li>{@link A2APublishSubscribeService} for agent discovery</li>
- * <li>{@link A2ATopicFactory} for topic routing</li>
- * </ul>
- */
-@Slf4j
-public class A2AGatewayService {
-
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- private final String namespace;
- private final String gatewayId;
- private final A2AMessageTransport transport;
- private final TaskRegistry taskRegistry;
- private final A2APublishSubscribeService a2aService;
-
- // Pending tasks waiting for response
- private final ConcurrentHashMap<String, CompletableFuture<TaskResult>> pendingTasks = new ConcurrentHashMap<>();
- // SSE subscribers for status updates
- private final ConcurrentHashMap<String, List<StatusSubscriber>> statusSubscribers = new ConcurrentHashMap<>();
-
- private volatile boolean started = false;
- private String responseSubscriptionId;
- private String statusSubscriptionId;
-
- // Task timeout: tasks that don't receive a response within this duration are auto-failed
- private static final long DEFAULT_TASK_TIMEOUT_MS = 120_000L; // 2 minutes
- private final long taskTimeoutMs;
- private ScheduledExecutorService taskTimeoutScheduler;
-
- public A2AGatewayService(String namespace, String gatewayId,
- A2AMessageTransport transport,
- TaskRegistry taskRegistry,
- A2APublishSubscribeService a2aService) {
- this(namespace, gatewayId, transport, taskRegistry, a2aService, DEFAULT_TASK_TIMEOUT_MS);
- }
-
- public A2AGatewayService(String namespace, String gatewayId,
- A2AMessageTransport transport,
- TaskRegistry taskRegistry,
- A2APublishSubscribeService a2aService,
- long taskTimeoutMs) {
- this.namespace = namespace;
- this.gatewayId = gatewayId;
- this.transport = transport;
- this.taskRegistry = taskRegistry;
- this.a2aService = a2aService;
- this.taskTimeoutMs = taskTimeoutMs;
- }
-
- public String getGatewayId() {
- return gatewayId;
- }
-
- public String getNamespace() {
- return namespace;
- }
-
- public A2APublishSubscribeService getA2aService() {
- return a2aService;
- }
-
- public TaskRegistry getTaskRegistry() {
- return taskRegistry;
- }
-
- /**
- * Starts the gateway service: subscribes to gateway response/status topics.
- */
- public synchronized void start() throws Exception {
- if (started) {
- return;
- }
- // Start task registry TTL cleanup
- taskRegistry.start();
-
- // Start task timeout scheduler
- taskTimeoutScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
- Thread t = new Thread(r, "a2a-task-timeout");
- t.setDaemon(true);
- return t;
- });
-
- // Subscribe to all responses for this gateway
- String responseTopic = A2ATopicFactory.gatewayResponseWildcardTopic(namespace, gatewayId);
- responseSubscriptionId = transport.subscribe(responseTopic, this::handleResponse);
-
- // Subscribe to all status updates for this gateway
- String statusTopic = A2ATopicFactory.gatewayStatusWildcardTopic(namespace, gatewayId);
- statusSubscriptionId = transport.subscribe(statusTopic, this::handleStatus);
-
- started = true;
- log.info("A2AGatewayService started: gatewayId={}, namespace={}", gatewayId, namespace);
- }
-
- public synchronized void shutdown() throws Exception {
- if (!started) {
- return;
- }
- if (responseSubscriptionId != null) {
- transport.unsubscribe(responseSubscriptionId);
- }
- if (statusSubscriptionId != null) {
- transport.unsubscribe(statusSubscriptionId);
- }
- if (taskTimeoutScheduler != null) {
- taskTimeoutScheduler.shutdownNow();
- taskTimeoutScheduler = null;
- }
- pendingTasks.clear();
- statusSubscribers.clear();
- taskRegistry.shutdown();
- started = false;
- log.info("A2AGatewayService shutdown.");
- }
-
- // =========================================================================
- // Task Submission
- // =========================================================================
-
- /**
- * Submits an A2A task to a target agent.
- *
- * @param targetAgent target agent name
- * @param message the message content (JSON string)
- * @param parentTaskId parent task ID (null for root tasks from external clients)
- * @return CompletableFuture that will be completed with the task result
- */
- public CompletableFuture<TaskResult> submitTask(String targetAgent, String message, String parentTaskId) {
- String taskId = generateTaskId();
- return submitTask(taskId, targetAgent, message, parentTaskId);
- }
-
- /**
- * Submits an A2A task with a specific task ID.
- */
- public CompletableFuture<TaskResult> submitTask(String taskId, String targetAgent,
- String message, String parentTaskId) {
- if (!started) {
- CompletableFuture<TaskResult> future = new CompletableFuture<>();
- future.completeExceptionally(new IllegalStateException("Gateway not started"));
- return future;
- }
-
- // Validate that the target agent is registered
- if (!a2aService.isAgentRegistered(targetAgent)) {
- CompletableFuture<TaskResult> future = new CompletableFuture<>();
- future.completeExceptionally(new IllegalArgumentException(
- "Target agent not registered: " + targetAgent));
- return future;
- }
-
- // Create task entry
- TaskRegistry.TaskEntry taskEntry = taskRegistry.createTask(taskId, parentTaskId, targetAgent, gatewayId);
-
- // Build A2A CloudEvent
- CloudEvent event = buildTaskRequestEvent(taskId, targetAgent, message, parentTaskId);
-
- // Register pending future BEFORE publishing to avoid race condition:
- // InMemoryTransport delivers synchronously, so if we publish first,
- // handleResponse() could run before put() and the future would never complete.
- CompletableFuture<TaskResult> future = new CompletableFuture<>();
- pendingTasks.put(taskId, future);
-
- // Schedule timeout: if no response within taskTimeoutMs, auto-fail the task
- if (taskTimeoutScheduler != null) {
- taskTimeoutScheduler.schedule(() -> {
- CompletableFuture<TaskResult> pending = pendingTasks.get(taskId);
- if (pending != null && !pending.isDone()) {
- taskRegistry.markFailed(taskId, "Task timed out after " + taskTimeoutMs + "ms with no response");
- pendingTasks.remove(taskId);
- pending.completeExceptionally(new java.util.concurrent.TimeoutException(
- "Task " + taskId + " timed out after " + taskTimeoutMs + "ms"));
- notifyStatusSubscribers(taskId, "failed", "Task timed out");
- log.warn("Task timed out: taskId={}, targetAgent={}", taskId, targetAgent);
- }
- }, taskTimeoutMs, TimeUnit.MILLISECONDS);
- }
-
- // Publish to agent request topic
- String requestTopic = A2ATopicFactory.agentRequestTopic(namespace, targetAgent);
- try {
- transport.publish(requestTopic, event);
- log.info("Task submitted: taskId={}, targetAgent={}, topic={}", taskId, targetAgent, requestTopic);
- } catch (Exception e) {
- log.error("Failed to publish task: taskId={}", taskId, e);
- taskRegistry.markFailed(taskId, "Publish failed: " + e.getMessage());
- pendingTasks.remove(taskId);
- future.completeExceptionally(e);
- }
-
- return future;
- }
-
- /**
- * Cancels a task.
- */
- public boolean cancelTask(String taskId) {
- boolean cancelled = taskRegistry.markCancelled(taskId);
- if (cancelled) {
- CompletableFuture<TaskResult> future = pendingTasks.remove(taskId);
- if (future != null) {
- future.complete(new TaskResult(TaskRegistry.TaskState.CANCELLED, null, "Task cancelled"));
- }
- notifyStatusSubscribers(taskId, "cancelled", "Task cancelled");
- return true;
- }
- return false;
- }
-
- /**
- * Gets task status.
- */
- public TaskRegistry.TaskEntry getTaskStatus(String taskId) {
- return taskRegistry.getTask(taskId);
- }
-
- // =========================================================================
- // SSE Status Subscription
- // =========================================================================
-
- /**
- * Registers a subscriber for status updates on a specific task.
- */
- public void registerStatusSubscriber(String taskId, StatusSubscriber subscriber) {
- statusSubscribers.computeIfAbsent(taskId, k -> new CopyOnWriteArrayList<>()).add(subscriber);
- }
-
- /**
- * Removes a status subscriber.
- */
- public void unregisterStatusSubscriber(String taskId, StatusSubscriber subscriber) {
- List<StatusSubscriber> subs = statusSubscribers.get(taskId);
- if (subs != null) {
- subs.remove(subscriber);
- if (subs.isEmpty()) {
- statusSubscribers.remove(taskId);
- }
- }
- }
-
- // =========================================================================
- // Response / Status Handling
- // =========================================================================
-
- private void handleResponse(String topic, CloudEvent event) {
- A2ATopicFactory.ParsedTopic parsed = A2ATopicFactory.parse(topic);
- if (parsed == null || !parsed.isResponse() || parsed.getTaskId() == null) {
- return;
- }
- String taskId = parsed.getTaskId();
- log.info("Received response for task: {}", taskId);
-
- TaskRegistry.TaskEntry entry = taskRegistry.getTask(taskId);
- if (entry == null) {
- log.warn("Response received for unknown task: {}", taskId);
- return;
- }
-
- String resultData = extractEventData(event);
- taskRegistry.markCompleted(taskId, resultData);
-
- CompletableFuture<TaskResult> future = pendingTasks.remove(taskId);
- if (future != null) {
- future.complete(new TaskResult(TaskRegistry.TaskState.COMPLETED, resultData, null));
- }
-
- // Notify SSE subscribers
- notifyStatusSubscribers(taskId, "completed", resultData);
- }
-
- private void handleStatus(String topic, CloudEvent event) {
- A2ATopicFactory.ParsedTopic parsed = A2ATopicFactory.parse(topic);
- if (parsed == null || !parsed.isStatus() || parsed.getTaskId() == null) {
- return;
- }
- String taskId = parsed.getTaskId();
- String statusData = extractEventData(event);
- log.debug("Received status for task: {} -> {}", taskId, statusData);
-
- // Mark as working if currently submitted
- TaskRegistry.TaskEntry entry = taskRegistry.getTask(taskId);
- if (entry != null && entry.getState() == TaskRegistry.TaskState.SUBMITTED) {
- taskRegistry.markWorking(taskId);
- }
-
- // Notify SSE subscribers
- notifyStatusSubscribers(taskId, "working", statusData);
- }
-
- private void notifyStatusSubscribers(String taskId, String state, String data) {
- List<StatusSubscriber> subs = statusSubscribers.get(taskId);
- if (subs != null) {
- for (StatusSubscriber sub : subs) {
- try {
- sub.onStatus(taskId, state, data);
- } catch (Exception e) {
- log.warn("Status subscriber error for task {}: {}", taskId, e.getMessage());
- }
- }
- }
- }
-
- // =========================================================================
- // CloudEvent Building
- // =========================================================================
-
- private CloudEvent buildTaskRequestEvent(String taskId, String targetAgent,
- String message, String parentTaskId) {
- CloudEventBuilder builder = CloudEventBuilder.v1()
- .withId(taskId)
- .withType(A2AProtocolConstants.CE_TYPE_PREFIX + "task.request")
- .withSource(java.net.URI.create("gateway/" + gatewayId))
- .withDataContentType("application/json")
- .withData(message.getBytes(StandardCharsets.UTF_8))
- .withExtension(A2AProtocolConstants.CE_EXTENSION_A2A_METHOD, A2AProtocolConstants.OP_SEND_MESSAGE)
- .withExtension(A2AProtocolConstants.CE_EXTENSION_TARGET_AGENT, targetAgent)
- .withExtension(A2AProtocolConstants.CE_EXTENSION_PROTOCOL, "A2A")
- .withExtension(A2AProtocolConstants.CE_EXTENSION_PROTOCOL_VERSION, A2AProtocolConstants.PROTOCOL_VERSION);
-
- if (parentTaskId != null) {
- builder.withExtension(A2AProtocolConstants.CE_EXTENSION_COLLABORATION_ID, parentTaskId);
- }
-
- return builder.build();
- }
-
- private String extractEventData(CloudEvent event) {
- if (event.getData() == null) {
- return null;
- }
- return new String(event.getData().toBytes(), StandardCharsets.UTF_8);
- }
-
- private String generateTaskId() {
- return "task-" + UUID.randomUUID().toString().substring(0, 8);
- }
-
- // =========================================================================
- // Result Types
- // =========================================================================
-
- public static class TaskResult {
-
- private final TaskRegistry.TaskState state;
- private final String data;
- private final String errorMessage;
-
- public TaskResult(TaskRegistry.TaskState state, String data, String errorMessage) {
- this.state = state;
- this.data = data;
- this.errorMessage = errorMessage;
- }
-
- public TaskRegistry.TaskState getState() {
- return state;
- }
-
- public String getData() {
- return data;
- }
-
- public String getErrorMessage() {
- return errorMessage;
- }
- }
-
- /**
- * Callback interface for task status change notifications.
- */
- @FunctionalInterface
- public interface StatusSubscriber {
-
- void onStatus(String taskId, String state, String data);
- }
-}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2APublishSubscribeService.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2APublishSubscribeService.java
deleted file mode 100644
index 10734d2d3..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/A2APublishSubscribeService.java
+++ /dev/null
@@ -1,449 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.eventmesh.runtime.a2a;
-
-import org.apache.eventmesh.protocol.a2a.A2AProtocolConstants;
-import org.apache.eventmesh.protocol.a2a.AgentCardValidator;
-import org.apache.eventmesh.protocol.a2a.AgentIdentity;
-import org.apache.eventmesh.protocol.a2a.model.AgentCard;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import io.cloudevents.CloudEvent;
-import io.cloudevents.core.builder.CloudEventBuilder;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * A2APublishSubscribeService: Manages A2A Agent Card Registry and processes A2A events.
- *
- * <p>Features:
- * - Agent Card CRUD (register, delete, get, list)
- * - Agent status tracking (online/offline)
- * - Event processing with status metadata augmentation
- * - Hierarchical identity (org_id/unit_id/agent_id) with wildcard queries
- */
-@Slf4j
-public class A2APublishSubscribeService {
-
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- private volatile boolean isStarted = false;
-
- private final ConcurrentHashMap<AgentIdentity, RegisteredCard> cardRegistry = new ConcurrentHashMap<>();
- private final ConcurrentHashMap<String, AgentStatus> agentStatusMap = new ConcurrentHashMap<>();
-
- private AgentCardValidator cardValidator;
-
- // TTL heartbeat configuration
- private static final long DEFAULT_CARD_TTL_MS = 60_000L; // 60 seconds
- private static final long DEFAULT_CLEANUP_INTERVAL_MS = 15_000L; // 15 seconds
- private final long cardTtlMs;
- private final long cleanupIntervalMs;
- private ScheduledExecutorService cleanupExecutor;
-
- public A2APublishSubscribeService() {
- this(DEFAULT_CARD_TTL_MS, DEFAULT_CLEANUP_INTERVAL_MS);
- }
-
- public A2APublishSubscribeService(long cardTtlMs, long cleanupIntervalMs) {
- this.cardTtlMs = cardTtlMs;
- this.cleanupIntervalMs = cleanupIntervalMs;
- }
-
- public void init() throws Exception {
- this.cardValidator = new AgentCardValidator(true);
- log.info("A2APublishSubscribeService initialized with Agent Card Registry (TTL={}ms, cleanup={}ms).",
- cardTtlMs, cleanupIntervalMs);
- }
-
- public void start() throws Exception {
- isStarted = true;
- startCleanupScheduler();
- log.info("A2APublishSubscribeService started.");
- }
-
- public void shutdown() throws Exception {
- isStarted = false;
- if (cleanupExecutor != null) {
- cleanupExecutor.shutdownNow();
- cleanupExecutor = null;
- }
- cardRegistry.clear();
- agentStatusMap.clear();
- log.info("A2APublishSubscribeService shutdown.");
- }
-
- // -------------------------------------------------------------------------
- // TTL Heartbeat Cleanup
- // -------------------------------------------------------------------------
-
- private void startCleanupScheduler() {
- cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
- Thread t = new Thread(r, "a2a-card-ttl-cleanup");
- t.setDaemon(true);
- return t;
- });
- cleanupExecutor.scheduleAtFixedRate(this::cleanupExpiredCards,
- cleanupIntervalMs, cleanupIntervalMs, TimeUnit.MILLISECONDS);
- }
-
- /**
- * Removes AgentCards that have not been refreshed within the TTL window.
- */
- void cleanupExpiredCards() {
- if (!isStarted) {
- return;
- }
- long now = System.currentTimeMillis();
- int removed = 0;
- Iterator<Map.Entry<AgentIdentity, RegisteredCard>> it = cardRegistry.entrySet().iterator();
- while (it.hasNext()) {
- Map.Entry<AgentIdentity, RegisteredCard> entry = it.next();
- RegisteredCard rc = entry.getValue();
- if (now - rc.lastHeartbeat > cardTtlMs) {
- it.remove();
- agentStatusMap.remove(entry.getKey().clientId());
- removed++;
- log.info("Agent card expired and removed: {} (lastHeartbeat={}ms ago)",
- entry.getKey().clientId(), now - rc.lastHeartbeat);
- }
- }
- if (removed > 0) {
- log.info("TTL cleanup removed {} expired agent card(s).", removed);
- }
- }
-
- /**
- * Refreshes the heartbeat timestamp for an agent card (agent calls this periodically).
- */
- public boolean heartbeat(AgentIdentity identity) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
- RegisteredCard rc = cardRegistry.get(identity);
- if (rc == null) {
- return false;
- }
- rc.lastHeartbeat = System.currentTimeMillis();
- agentStatusMap.put(identity.clientId(), AgentStatus.ONLINE);
- return true;
- }
-
- // =========================================================================
- // Agent Card Registry Operations
- // =========================================================================
-
- /**
- * Registers an Agent Card. Validates the card and identity before storing.
- *
- * @param identity the agent identity (org_id/unit_id/agent_id)
- * @param card the agent card
- * @return RegistrationResult with success/failure
- */
- public RegistrationResult registerCard(AgentIdentity identity, AgentCard card) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
-
- // Validate identity
- if (!identity.isValid()) {
- String msg = String.format("Invalid agent identity: orgId=%s, unitId=%s, agentId=%s",
- identity.getOrgId(), identity.getUnitId(), identity.getAgentId());
- log.warn(msg);
- return RegistrationResult.failure(msg);
- }
-
- // Validate card
- try {
- String cardJson = objectMapper.writeValueAsString(card);
- AgentCardValidator.ValidationResult result = cardValidator.validate(cardJson);
- if (!result.isValid()) {
- log.warn("Agent card schema validation failed for {}: {}", identity.clientId(), result.getErrorMessage());
- return RegistrationResult.failure(result.getErrorMessage());
- }
- } catch (Exception e) {
- log.warn("Failed to serialize/validate agent card for {}: {}", identity.clientId(), e.getMessage());
- return RegistrationResult.failure("Card validation error: " + e.getMessage());
- }
-
- RegisteredCard existing = cardRegistry.put(identity, new RegisteredCard(card, System.currentTimeMillis()));
- if (existing != null) {
- log.info("Updated agent card for {}", identity.clientId());
- } else {
- log.info("Registered new agent card for {}", identity.clientId());
- }
-
- // Set agent online
- agentStatusMap.put(identity.clientId(), AgentStatus.ONLINE);
-
- return RegistrationResult.success();
- }
-
- /**
- * Deletes an Agent Card from the registry.
- */
- public boolean deleteCard(AgentIdentity identity) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
- RegisteredCard removed = cardRegistry.remove(identity);
- if (removed != null) {
- agentStatusMap.remove(identity.clientId());
- log.info("Deleted agent card for {}", identity.clientId());
- return true;
- }
- return false;
- }
-
- /**
- * Gets a specific Agent Card.
- */
- public CardEntry getCard(AgentIdentity identity) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
- RegisteredCard rc = cardRegistry.get(identity);
- if (rc == null) {
- return null;
- }
- return new CardEntry(identity, rc.card, lookupAgentStatus(identity));
- }
-
- /**
- * Lists Agent Cards matching the given filters. Use null or "+" for wildcard matching.
- */
- public List<CardEntry> listCards(String orgId, String unitId, String agentId) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
- List<CardEntry> results = new ArrayList<>();
- for (Map.Entry<AgentIdentity, RegisteredCard> entry : cardRegistry.entrySet()) {
- AgentIdentity id = entry.getKey();
- if (id.matchesFilter(orgId, unitId, agentId)) {
- results.add(new CardEntry(id, entry.getValue().card, lookupAgentStatus(id)));
- }
- }
- return results;
- }
-
- /**
- * Lists all registered Agent Cards.
- */
- public List<CardEntry> listAllCards() {
- return listCards(null, null, null);
- }
-
- /**
- * Checks whether an agent with the given name is registered.
- *
- * @param agentName the agent name (or agentId segment) to look up
- * @return true if a registered card's name matches
- */
- public boolean isAgentRegistered(String agentName) {
- if (!isStarted || agentName == null) {
- return false;
- }
- for (RegisteredCard rc : cardRegistry.values()) {
- if (agentName.equals(rc.card.getName())) {
- return true;
- }
- }
- return false;
- }
-
- // =========================================================================
- // Agent Status
- // =========================================================================
-
- /**
- * Looks up the status of an agent (online/offline).
- */
- public String lookupAgentStatus(AgentIdentity identity) {
- AgentStatus status = agentStatusMap.get(identity.clientId());
- return status != null ? status.value : A2AProtocolConstants.STATUS_OFFLINE;
- }
-
- /**
- * Sets the status of an agent.
- */
- public void setAgentStatus(AgentIdentity identity, String status) {
- if (A2AProtocolConstants.STATUS_ONLINE.equals(status)) {
- agentStatusMap.put(identity.clientId(), AgentStatus.ONLINE);
- } else {
- agentStatusMap.put(identity.clientId(), AgentStatus.OFFLINE);
- }
- }
-
- // =========================================================================
- // Event Processing
- // =========================================================================
-
- /**
- * Processes an A2A CloudEvent. Augments events with agent status metadata.
- *
- * @param event The CloudEvent to process.
- * @return The processed (potentially modified) CloudEvent.
- */
- public CloudEvent process(CloudEvent event) {
- if (!isStarted) {
- throw new IllegalStateException("A2APublishSubscribeService is not started");
- }
-
- log.debug("Processing A2A event: {}", event.getId());
-
- // Check if this is an A2A discovery topic event
- String subject = event.getSubject();
- if (subject != null && subject.startsWith(A2AProtocolConstants.TOPIC_NAMESPACE + "/")) {
- AgentIdentity identity = AgentIdentity.fromDiscoveryTopic(subject);
- if (identity != null) {
- return augmentWithStatusMetadata(event, identity);
- }
- }
-
- return event;
- }
-
- /**
- * Augments a CloudEvent with agent status metadata (a2astatus, a2astatussource extensions).
- * Matches EMQX's on_message_delivered hook behavior.
- */
- private CloudEvent augmentWithStatusMetadata(CloudEvent event, AgentIdentity identity) {
- String status = lookupAgentStatus(identity);
- CloudEventBuilder builder = CloudEventBuilder.from(event);
- builder.withExtension(A2AProtocolConstants.CE_EXTENSION_A2A_STATUS, status);
- builder.withExtension(A2AProtocolConstants.CE_EXTENSION_A2A_STATUS_SOURCE, "eventmesh");
- return builder.build();
- }
-
- // =========================================================================
- // Inner Types
- // =========================================================================
-
- private enum AgentStatus {
-
- ONLINE(A2AProtocolConstants.STATUS_ONLINE),
- OFFLINE(A2AProtocolConstants.STATUS_OFFLINE);
-
- final String value;
-
- AgentStatus(String value) {
- this.value = value;
- }
- }
-
- private static class RegisteredCard {
-
- final AgentCard card;
- final long registeredAt;
- volatile long lastHeartbeat;
-
- RegisteredCard(AgentCard card, long registeredAt) {
- this.card = card;
- this.registeredAt = registeredAt;
- this.lastHeartbeat = registeredAt;
- }
- }
-
- /**
- * Represents a registered agent card entry with identity, card data, and status.
- */
- public static class CardEntry {
-
- private final AgentIdentity identity;
- private final AgentCard card;
- private final String status;
-
- public CardEntry(AgentIdentity identity, AgentCard card, String status) {
- this.identity = identity;
- this.card = card;
- this.status = status;
- }
-
- public AgentIdentity getIdentity() {
- return identity;
- }
-
- public AgentCard getCard() {
- return card;
- }
-
- public String getStatus() {
- return status;
- }
-
- public String getNamespace() {
- return identity.getNamespace();
- }
-
- public String getId() {
- return identity.clientId();
- }
-
- public String getName() {
- return card.getName();
- }
-
- public String getVersion() {
- return card.getVersion();
- }
-
- public String getDescription() {
- return card.getDescription();
- }
- }
-
- /**
- * Result of a card registration attempt.
- */
- public static class RegistrationResult {
-
- private final boolean success;
- private final String errorMessage;
-
- private RegistrationResult(boolean success, String errorMessage) {
- this.success = success;
- this.errorMessage = errorMessage;
- }
-
- public static RegistrationResult success() {
- return new RegistrationResult(true, null);
- }
-
- public static RegistrationResult failure(String errorMessage) {
- return new RegistrationResult(false, errorMessage);
- }
-
- public boolean isSuccess() {
- return success;
- }
-
- public String getErrorMessage() {
- return errorMessage;
- }
- }
-}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/EventMeshA2ATransport.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/EventMeshA2ATransport.java
index 23da28bb4..1353b7fc3 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/EventMeshA2ATransport.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/EventMeshA2ATransport.java
@@ -101,7 +101,8 @@ public class EventMeshA2ATransport implements A2AMessageTransport {
try {
List<BufferedEvent> batch = ingress.poll(clientId, 100, 500L);
for (BufferedEvent be : batch) {
- CloudEvent event = be.getEvent();
+ // Egress boundary: the push buffer carries Frame; convert to CloudEvent for the A2A callback.
+ CloudEvent event = be.getEvent().toCloudEvent();
String topic = event.getSubject() != null ? event.getSubject() : "default";
MessageCallback cb = callbacks.get(topic);
if (cb != null) {
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/InMemoryA2AMessageTransport.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/InMemoryA2AMessageTransport.java
deleted file mode 100644
index d7f2f2ed4..000000000
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/a2a/InMemoryA2AMessageTransport.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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,
... 18926 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@eventmesh.apache.org
For additional commands, e-mail: commits-help@eventmesh.apache.org