# E2A (Everything-to-Agent) Protocol > **Implementation**: `jiuwenswarm/common/e2a/` (`models.py`, `adapters.py`, `constants.py`, `__init__.py`). **Version**: `E2A_PROTOCOL_VERSION` (default `1.0`). **On conflict**: treat the dataclass fields in `models.py` as the source of truth and update this document accordingly. Requests: `E2AEnvelope`; responses: `E2AResponse`. > **中文版**:[../zh/E2A-protocol.md](../zh/E2A-protocol.md) --- ## 0. Document layout and single source of truth | Location | Role | |----------|------| | **docs/zh/E2A-protocol.md** | Same specification in Chinese | | **docs/en/E2A-protocol.md** (this file) | Normative description, pitfalls, examples (English) | | `jiuwenswarm/common/e2a/models.py` | Request `E2AEnvelope`, response `E2AResponse`, nested types; `from_dict` / `to_dict` | | `jiuwenswarm/common/e2a/constants.py` | **`E2A_SOURCE_PROTOCOL_*`**, **`E2A_RESPONSE_KINDS`**, **`E2A_RESPONSE_STATUS_*`**, ACP method names and SessionUpdate strings (runtime: use tuples in code) | | `jiuwenswarm/common/e2a/adapters.py` | ACP / A2A → E2A; E2A → ACP JSON-RPC; **`E2AResponse` → ACP / A2A projections** (§8) | | `jiuwenswarm/common/e2a/__init__.py` | Public exports | --- ## 1. Scope and boundaries - Payloads sent to AgentServer **after Gateway normalization** are expressed as E2A. Raw channel fields are **mapped at the Gateway edge** into normalized fields (`user_id`, `message_id`, `params`, etc.); the full channel JSON is **not** required to be the E2A primary path. - **`channel_context`**: optional; transitional or residual keys that do not fit normalized fields; `from_dict` may migrate legacy `metadata` for compatibility. - E2A **does not** specify the transport (WebSocket, HTTP, etc.). --- ## 2. Pitfall: two meanings of `method` (required reading) The same field **`method`** means different things by origin. It is **not** two different fields; do not confuse it with `request_id` / `message_id`. | Origin | Typical `method` values | Notes | |--------|-------------------------|-------| | **Gateway → AgentServer** | `chat.send`, `history.get`, `chat.interrupt`, … | In-project RPC names; **not** enumerated in `constants.ACP_CLIENT_TO_AGENT_METHODS` | | **ACP via `envelope_from_acp_jsonrpc`** | `session/prompt`, `initialize`, … | Same as JSON-RPC `method`; full list: **`constants.ACP_CLIENT_TO_AGENT_METHODS`** | | **Internal special** | `null` | e.g. heartbeat with no RPC-style operation name | **Export to ACP**: `envelope_to_acp_jsonrpc_call` only uses `merge_params_to_acp_prompt` to fill `prompt` from **`params`** when `method == "session/prompt"`. If `method` is `chat.send`, you must **map explicitly** in an adapter to ACP’s `session/prompt` and `params`; otherwise the JSON-RPC is invalid for a pure ACP peer. --- ## 3. Relationship to ACP and A2A (summary) | Aspect | ACP | A2A | E2A (internal) | |--------|-----|-----|----------------| | Role | JSON-RPC session | Task / Message / Card | Unified envelope; externals **adapt in** | | `method` | JSON-RPC method | Abstract op (binding maps) | See **§2** | | Extension | `_meta` | `metadata` | Gateway uses normalized fields; `a2a_metadata` / `acp_meta` for interop only | --- ## 4. `E2AEnvelope` field reference ### 4.1 Core and correlation | Field | Type | Description | |-------|------|-------------| | `protocol_version` | string | Default `1.0` | | `request_id` | string / null | Primary Gateway↔AgentServer id (stream chunks align on this) | | `jsonrpc_id` | string / number / null | JSON-RPC `id` when ingested from ACP | | `correlation_id` | string / null | Tracing; distinct from `request_id` | | `task_id` / `context_id` | string / null | A2A alignment | | `session_id` | string / null | Session | | `message_id` | string / null | Platform or A2A message id; do not conflate with `request_id` | | `is_stream` | boolean | Streamed response or not | ### 4.2 `provenance` (`E2AProvenance`) | Field | Description | |-------|-------------| | `source_protocol` | `e2a` / `acp` / `a2a` (see `E2A_SOURCE_PROTOCOL_*`) | | `converter` / `converted_at` / `details` | Converter id, time, hints (e.g. `jsonrpc_method`) | Hand-built defaults use `source_protocol` = `e2a`. Legacy `binding` migrates to `provenance.details.migrated_from_binding`. ### 4.3 Time, identity, ingress | Field | Description | |-------|-------------| | `timestamp` | **Normative**: RFC 3339 UTC string; `from_dict` may accept float/int epoch seconds | | `identity_origin` | `system` / `user` / `agent` / `service` | | `channel` | Ingress name; **use `channel` only**. `from_dict` may read `channel_id` | | `user_id` / `source_agent_id` | User or upstream agent | `timestamp` (business time) ≠ `provenance.converted_at` (E2A conversion time). ### 4.4 `method`, `params`, bridging | Field | Description | |-------|-------------| | `method` | See **§2**; `from_dict` may read `req_method` | | **`params`** | **Single business-parameter dict** (aligned with JSON-RPC `params`): options, user text, multimodal blocks, attachments **all** go here; **do not** use a top-level `payload` (see table below) | | `ext_method` | Real name when `method == "ext"` | | `session_update_kind` | For ACP `session/update` bridge; values match `constants.ACP_SESSION_UPDATE_KINDS` | | `expected_output_modes` | A2A `acceptedOutputModes` | **Common `params` keys** (extend as needed; names below cooperate with ACP bridging): | Key | Purpose | |-----|---------| | `prompt` | Use as-is when already an ACP ContentBlock array (`merge_params_to_acp_prompt` does not overwrite) | | `content_blocks` | JSON array isomorphic to ACP `Vec`; when `method==session/prompt` and no `prompt`, can map to `prompt` | | `text` / `content` / `query` | Plain user text; if no `prompt`/`content_blocks`, first non-empty string becomes a single `text` block | | `files` / `attachments` | Attachment lists; elements follow **`E2AFileRef`** (`uri` required) | | Others | e.g. `mode`, `page_idx`, heartbeat text—extend per Gateway RPC | ### 4.5 `auth` and extension slots | Field | Description | |-------|-------------| | `auth` | `E2AAuth`: credential references, etc. | | `channel_context` | Optional overflow / legacy `metadata`; see **§1** | | `a2a_metadata` | **A2A interop only** (adapter arg `metadata` maps here—not Gateway `metadata`) | | `acp_meta` | Merged into `params._meta` by `merge_params_to_acp_prompt` (§7) | --- ## 5. Nested types (summary) - **`E2AFileRef`**: recommended attachment element (`uri` required; `name`, `mime_type`, `size`, `_meta` optional); usually in **`params.files`** or **`params.attachments`**. - **`E2AAuth`**: `method_id`, `bearer_token`, `api_key_ref`, `credential_ref`, `extra_headers`, `_meta` --- ## 6. ACP strings in `constants.py` (bridge reference) For ACP bridging and doc cross-check only; **at runtime** use tuples in `jiuwenswarm.common.e2a.constants`: - **`ACP_CLIENT_TO_AGENT_METHODS`**: client → Agent JSON-RPC method names - **`ACP_AGENT_TO_CLIENT_METHODS`**, **`ACP_NOTIFICATION_NAMES`**: downstream / notifications - **`ACP_SESSION_UPDATE_KINDS`**: values for `session_update_kind` - **`E2A_RESPONSE_KINDS`**, **`E2A_RESPONSE_STATUS_*`**, **`E2A_A2A_STREAM_BRANCHES`**: response `response_kind` / `status` / `a2a.stream_event.branch` (see **§12**) **Gateway RPC** names (`chat.send`, etc.) are project-defined and **not** in those tuples. --- ## 7. `merge_params_to_acp_prompt` Python: `jiuwenswarm.common.e2a.merge_params_to_acp_prompt(envelope)`. If and only if **`method == "session/prompt"`**: 1. If `params` already has `prompt`, do not overwrite. 2. Else if `params.content_blocks` is a non-empty array, use as `prompt`. 3. Else if any of `params.text`, `params.content`, `params.query` is a non-empty string, build `{ "type": "text", "text": "..." }` as `prompt`. 4. If the envelope has `session_id` and `params` lacks `session_id`, set it. 5. Merge `envelope.acp_meta` into `params._meta`. For any other `method`, return a shallow copy of `params`. --- ## 8. Adapters (`adapters.py`) | Function | Role | |----------|------| | `envelope_from_acp_jsonrpc` | ACP JSON-RPC → E2A; `provenance.source_protocol=acp`; `method` is ACP method | | `envelope_from_a2a_send_message` | A2A SendMessage semantics → E2A; adapter **`metadata`** → **`a2a_metadata`** (A2A-only, not Gateway `metadata`) | | `envelope_to_acp_jsonrpc_call` | E2A → JSON-RPC call shape; does not change `provenance` | | `e2a_response_to_acp_jsonrpc_response` | **`E2AResponse`** → one JSON-RPC **response** object (`result` or `error`); returns `None` if not applicable | | `e2a_response_to_a2a_stream_payload` | **`E2AResponse`** (`a2a.stream_event`) → A2A **`StreamResponse`**-shaped JSON (exactly one branch); returns `None` if not applicable | --- ## 9. Serialization and compatibility keys - `E2AEnvelope.to_dict()` / `from_dict(data)` - **Read compatibility**: `channel_id`→`channel`, `req_method`→`method`, `metadata`→`channel_context` (only if `channel_context` empty), `binding`→`provenance` (above) - **Optional**: if top-level legacy **`payload`** (object) exists, keys are **merged into `params`** without overwriting existing `params` keys; **new protocol must not** send top-level `payload`. --- ## 10. Mapping from `AgentRequest` | AgentRequest | E2AEnvelope | |--------------|-------------| | `request_id` | `request_id` | | `channel_id` | `channel` | | `session_id` | `session_id` | | `req_method` | `method` | | `params` | `params` | | `is_stream` | `is_stream` | | `timestamp` (float) | `timestamp` (string, auto-normalized) | | `metadata` | Target: map to normalized fields; compat: `from_dict`→`channel_context` | --- ## 11. Examples (JSON shapes) ### 11.1 After Gateway normalization → AgentServer (chat, streaming) ```json { "protocol_version": "1.0", "request_id": "req_abc_01", "session_id": "sess_xyz", "channel": "web", "method": "chat.send", "is_stream": true, "timestamp": "2026-03-28T12:00:00+00:00", "identity_origin": "user", "user_id": "u_001", "params": { "content": "Hello", "mode": "plan", "query": "Hello" }, "provenance": { "source_protocol": "e2a" } } ``` `channel_context` may be omitted (or `{}` when serialized empty). ### 11.2 Legacy `AgentRequest` shape (readable by `from_dict`) ```json { "request_id": "req_legacy_1", "channel_id": "feishu", "session_id": "sess_feishu_1", "req_method": "chat.send", "is_stream": true, "timestamp": 1774524781.15, "params": { "content": "List desktop files", "mode": "plan", "query": "List desktop files" }, "metadata": { "feishu_open_id": "ou_xxx", "message_id": "om_xxx" } } ``` After parse: `channel`=`feishu`, `method`=`chat.send`, `timestamp` normalized to RFC 3339; if not mapped elsewhere, `metadata` lands in `channel_context` (migration). **Target state**: Gateway should map `feishu_open_id` into `user_id` / `message_id`, etc., and minimize `channel_context`. ### 11.3 ACP ingress (illustrative; built by `envelope_from_acp_jsonrpc`) - `method` = `"session/prompt"` - `jsonrpc_id` = `42` - `params` = `{ "session_id": "s1", "prompt": [...] }` - `provenance.source_protocol` = `"acp"`, `details` includes `jsonrpc_method`, etc. ### 11.4 Exported ACP JSON-RPC (`envelope_to_acp_jsonrpc_call` shape) ```json { "jsonrpc": "2.0", "id": 42, "method": "session/prompt", "params": { "session_id": "s1", "prompt": [{"type": "text", "text": "hi"}] } } ``` `merge_params_to_acp_prompt` only fills `prompt` from `content_blocks` or `text`/`content`/`query` when envelope `method == "session/prompt"` and `params` has no `prompt` yet. ### 11.5 `session/prompt` + multimodal (all in `params`) ```json { "method": "session/prompt", "session_id": "s1", "jsonrpc_id": 1, "params": { "content_blocks": [ {"type": "text", "text": "Describe this image"}, {"type": "image", "mime_type": "image/png", "data": ""} ] } } ``` After `merge_params_to_acp_prompt`, `params` gains a `prompt` equivalent to `content_blocks` if there was no `prompt` before. ### 11.6 `session/prompt` + per-request `workspace_dir` External clients (IDE plugins, headless evaluators, MCP drivers) can scope one prompt's filesystem root to a per-invocation directory by passing `workspace_dir` in `params`. The AgentServer resolves the path, creates it on demand (`mkdir -p`), and threads the absolute path through to `init_cwd(cwd=workspace_dir, workspace=workspace_dir)` so openjiuwen's per-task `CwdState` ContextVar is reseeded for the duration of the prompt. Both layers get the override: - **`get_cwd()`** returns the scratch dir, so tools resolving relative paths land there. - **`get_workspace()`** also returns the scratch dir, so `fs_operation`'s sandbox enforcement (which gates absolute-path writes by workspace membership) accepts writes anywhere under it. Memory files, `.workspace`, and other artifact-root consumers therefore land under the scratch dir for that request — useful for hermetic evaluators that want one fresh slate per task. Long-lived agents whose memory must persist should *not* pass `workspace_dir`; they fall back to the agent's instance-level workspace as before. Precedence and behavior: - **Precedence:** when `workspace_dir` is set and `mkdir` succeeds, it wins over `params.cwd`. When `workspace_dir` is set but `mkdir` fails (e.g. permission denied), the request degrades to `params.cwd` (if any), then to the agent's global default; a warning is logged. - **Path form:** absolute paths are recommended. Relative paths are resolved against the AgentServer process cwd, not the client's. ```json { "method": "session/prompt", "session_id": "s1", "jsonrpc_id": 1, "params": { "content": "Create the project under datautils-project/", "workspace_dir": "/tmp/eval-task-42" } } ``` `jiuwenswarm-tui` (the ACP stdio bridge) exposes this as `--workspace-dir `. --- ## 12. E2A response protocol (`E2AResponse`) Every outbound record from Agent → Gateway → client (including each streaming frame) is expressed as **`E2AResponse`**, symmetric with **`E2AEnvelope`** (`snake_case`, RFC 3339 `timestamp`, reuse **`E2AProvenance`**). ### 12.1 Three layers | Layer | Name | Role | |-------|------|------| | **L1** | Response envelope | Correlation ids, ordering, stream end, status, provenance | | **L2** | Discriminated payload | **`response_kind`** + **`body`** (shape constrained by kind) | | **L3** | `projections` (optional) | Under `acp` / `a2a`, pre-built wire JSON for interop—**auxiliary only**; consumers **must not** treat L3 as the sole source of truth | ### 12.2 `is_final` and `status` - **`is_final`**: `true` means **no further** `E2AResponse` records for the same **`request_id`**. This is **not** “success”; a failed final frame may still have `is_final: true`. - **`status`**: one of **`constants.E2A_RESPONSE_STATUS_*`** (`succeeded` / `failed` / `in_progress`). When **`status == failed`**, **`response_kind`** should be **`e2a.error`** (see **§12.6** if `acp.jsonrpc_error` is also used). ### 12.3 Invariants - For one **`request_id`**, **`sequence`** starts at **0** and strictly increases (0, 1, 2, …). - Normal completion: **exactly one** record has **`is_final: true`**. - Abnormal cases (disconnect, crash): an `is_final` frame may be missing; use timeouts and connection state—this spec does not require a synthetic final frame. ### 12.4 L1: envelope fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `protocol_version` | string | yes | Same as request; default `1.0` | | `response_id` | string | yes | Unique id for this record (UUID recommended) | | `request_id` | string | yes | Echo request; stream chunk alignment | | `sequence` | integer | yes | Per-`request_id`, starting at 0 | | `is_final` | boolean | yes | `true` on the last frame | | `status` | string | yes | `succeeded` / `failed` / `in_progress` | | `response_kind` | string | yes | See **§12.5**; runtime: **`constants.E2A_RESPONSE_KINDS`** | | `timestamp` | string | yes | RFC 3339 UTC; when this record was produced | | `provenance` | object | yes | **`E2AProvenance`** (same shape as requests) | | `body` | object | yes | L2 payload | | `jsonrpc_id` | string / number / null | no | Echo ACP JSON-RPC `id` | | `correlation_id` | string | no | Tracing | | `task_id` / `context_id` / `session_id` / `message_id` | string | no | Same meaning as on the request envelope | | `is_stream` | boolean | no | Whether the originating request was streaming | | `identity_origin` | string | no | Often **`agent`** for agent-originated responses | | `channel` / `user_id` / `source_agent_id` | same as request | no | Echo | | `method` | string / null | no | Echo request `method` (including heartbeat `null`) | | `metadata` | object | no | **Channel/business** key-value map; aligns with legacy **`AgentResponse.metadata`**; on **protocol conversion failure**, may hold fallback data. **AgentServer→Gateway WebSocket** uses **`constants.E2A_WIRE_LEGACY_AGENT_RESPONSE_KEY`** / **`E2A_WIRE_LEGACY_AGENT_CHUNK_KEY`** for full legacy JSON blobs; use channel-prefixed keys for app metadata | | `projections` | object | no | L3: optional `acp`, `a2a` keys | | `channel_context` / `a2a_metadata` / `acp_meta` | object | no | Same spillover slots as request; **distinct from `metadata`**—`a2a_metadata` / `acp_meta` are ACP/A2A interop only; `metadata` is general business and fallback | ### 12.5 L2: `response_kind` and `body` Runtime list: **`constants.E2A_RESPONSE_KINDS`**; **`body`** is a **dict**. Normative summary: | `response_kind` | `body` highlights | |-----------------|-------------------| | `e2a.complete` | **`result`**: business outcome (mirror the “single `params` dict” philosophy for requests) | | `e2a.chunk` | **`delta_kind`**: `text` / `reasoning` / `tool` / `custom`; **`delta`**: string or structured object; optional **`mime_type`** | | `e2a.error` | **`code`**, **`message`**, **`details`**; optional **`external`**: `{ "protocol": "acp"|"a2a", "type": string, "payload": object }` | | `acp.session_update` | ACP `session/update` notification payload; **`sessionUpdate`** aligns with **`ACP_SESSION_UPDATE_KINDS`** | | `acp.prompt_result` | JSON-RPC **`result`** for `session/prompt` (e.g. `stop_reason`, optional `usage`, `_meta`) | | `acp.jsonrpc_error` | JSON-RPC **`error`** semantics: `code`, `message`, optional `data` (prefer **`e2a.error`** as the canonical failure kind; use this for ACP-edge projection if needed) | | `a2a.task` | A2A **`Task`** JSON (**snake_case** keys aligned with `spec/a2a.proto`) | | `a2a.message` | A2A **`Message`** JSON (same) | | `a2a.stream_event` | **`branch`**: `task` / `message` / `status_update` / `artifact_update` (**`E2A_A2A_STREAM_BRANCHES`**); **`payload`**: branch object | | `ext` | **`ext_method`** + **`params`** (symmetric to request `ext`) | **Recommendation**: internally prefer **`e2a.complete`** / **`e2a.chunk`** / **`e2a.error`**; build **`acp.*`** / **`a2a.*`** or **`projections`** at the edge. ### 12.6 Mapping to ACP / A2A (summary) - **ACP**: JSON-RPC response from **`jsonrpc_id`** + **`acp.prompt_result`** (`result`) or **`e2a.error`** / **`acp.jsonrpc_error`** (`error`); **`acp.session_update`** maps to **`session/update`** **`params`**. See **`e2a_response_to_acp_jsonrpc_response`** (§8). - **A2A**: **`a2a.stream_event`** matches **`StreamResponse`** branches; **`e2a_response_to_a2a_stream_payload`** emits one event. **`a2a.task`** / **`a2a.message`** match SendMessage outcomes. ### 12.7 Logging (e.g. `E:\logs`) - One line per **`E2AResponse`** JSON; sort/replay by **`sequence`**. - Minimum keys: **`request_id`**, **`response_id`**, **`sequence`**, **`is_final`**, **`response_kind`**, **`timestamp`**. - Do not log secrets (`bearer_token`, sensitive model output). Large **`body`** may later use **`body_ref`** (not defined in this version). ### 12.8 Example: two chunks + final ```json {"protocol_version":"1.0","response_id":"r1","request_id":"req_1","sequence":0,"is_final":false,"status":"in_progress","response_kind":"e2a.chunk","timestamp":"2026-03-29T12:00:00+00:00","provenance":{"source_protocol":"e2a"},"body":{"delta_kind":"text","delta":"Hello"}} ``` ```json {"protocol_version":"1.0","response_id":"r2","request_id":"req_1","sequence":1,"is_final":false,"status":"in_progress","response_kind":"e2a.chunk","timestamp":"2026-03-29T12:00:01+00:00","provenance":{"source_protocol":"e2a"},"body":{"delta_kind":"text","delta":" world"}} ``` ```json {"protocol_version":"1.0","response_id":"r3","request_id":"req_1","sequence":2,"is_final":true,"status":"succeeded","response_kind":"e2a.complete","timestamp":"2026-03-29T12:00:02+00:00","provenance":{"source_protocol":"e2a"},"body":{"result":{"content":"Hello world"}}} ``` Cross-reference: request log normalization in **`E2A-AgentRequest-log-migration.md`**; response logging can adopt the same single-line JSON convention later. ### 12.9 Streaming `chat.error` carries `error_type` When the inner agent loop raises, the streaming aggregator emits a `chat.error` event whose payload carries the originating exception class. Consumers can group failures structurally instead of regexing the message string. ```json {"event_type":"chat.error","error":"rate limited (429)","error_type":"RateLimitError"} ``` The same `error_type` field appears at the top level of the persisted `history.json` record. Cancellation (`asyncio.CancelledError`) propagates as cancellation rather than being classified as a `chat.error`. --- ## 13. Security notes - Prefer `credential_ref` / `api_key_ref` and ticket exchange at the Gateway; do not log full `bearer_token`. - Use TLS and least privilege across trust boundaries. --- *Maintained in sync with `jiuwenswarm/common/e2a/models.py` (`E2AEnvelope`, `E2AResponse`).*