theprotocol-sdk 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/PKG-INFO +53 -21
  2. theprotocol_sdk-0.2.0/README.md +95 -0
  3. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/pyproject.toml +1 -1
  4. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/router.py +41 -15
  5. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/task_store.py +9 -7
  6. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/acp.py +6 -6
  7. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/google_a2a.py +8 -6
  8. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/client/a2a_client.py +19 -8
  9. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/models/a2a_protocol.py +11 -9
  10. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/models/agent_card.py +2 -1
  11. theprotocol_sdk-0.2.0/src/theprotocol/models/v1_compat.py +230 -0
  12. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_bridge_anp.py +3 -3
  13. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_bridge_google_a2a.py +1 -1
  14. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_models.py +2 -2
  15. theprotocol_sdk-0.1.1/README.md +0 -63
  16. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/.gitignore +0 -0
  17. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/LICENSE +0 -0
  18. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/examples/simple_agent.py +0 -0
  19. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/__init__.py +0 -0
  20. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/__init__.py +0 -0
  21. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/base.py +0 -0
  22. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/decorators.py +0 -0
  23. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/__init__.py +0 -0
  24. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/__init__.py +0 -0
  25. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/auth.py +0 -0
  26. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/bridge.py +0 -0
  27. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/did_wba.py +0 -0
  28. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/translation.py +0 -0
  29. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/base.py +0 -0
  30. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/mcp.py +0 -0
  31. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/client/__init__.py +0 -0
  32. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/client/credentials.py +0 -0
  33. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/exceptions.py +0 -0
  34. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/src/theprotocol/models/__init__.py +0 -0
  35. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_agent.py +0 -0
  36. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_bridge_acp.py +0 -0
  37. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_bridge_mcp.py +0 -0
  38. {theprotocol_sdk-0.1.1 → theprotocol_sdk-0.2.0}/tests/test_credentials.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: theprotocol-sdk
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more.
5
5
  Project-URL: Homepage, https://theprotocol.cloud
6
6
  Project-URL: Documentation, https://api.theprotocol.cloud/docs
@@ -41,37 +41,36 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  # theprotocol-sdk
43
43
 
44
- Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
44
+ **A2A v1.0** — Build and call AI agents on [TheProtocol](https://theprotocol.cloud). Native protocol bridges for Google A2A, ANP, and MCP.
45
45
 
46
46
  ## Install
47
47
 
48
48
  ```bash
49
- pip install theprotocol-sdk # Client only (call agents)
50
- pip install theprotocol-sdk[server] # + FastAPI router (build agents)
51
- pip install theprotocol-sdk[all] # Everything
49
+ pip install theprotocol-sdk # Client only (call agents)
50
+ pip install theprotocol-sdk[server] # + FastAPI router (build agents)
51
+ pip install theprotocol-sdk[anp] # + Ed25519 crypto for ANP DID:WBA
52
+ pip install theprotocol-sdk[all] # Everything
52
53
  ```
53
54
 
54
- ## Build an Agent (10 lines)
55
+ ## Build an Agent
55
56
 
56
57
  ```python
57
58
  from theprotocol.agent import BaseA2AAgent, create_a2a_router
58
- from theprotocol.models import Message, TextPart
59
59
  from fastapi import FastAPI
60
60
 
61
61
  class MyAgent(BaseA2AAgent):
62
62
  async def handle_task_send(self, task_id, message):
63
- return "task-1" # Return task ID
64
- async def handle_task_get(self, task_id):
65
- ... # Return Task object
66
- async def handle_task_cancel(self, task_id):
67
- return True
68
- async def handle_subscribe_request(self, task_id):
69
- yield # SSE events
63
+ return "task-1"
64
+ async def handle_task_get(self, task_id): ...
65
+ async def handle_task_cancel(self, task_id): return True
66
+ async def handle_subscribe_request(self, task_id): yield
70
67
 
71
68
  app = FastAPI()
72
69
  app.include_router(create_a2a_router(MyAgent()))
73
70
  ```
74
71
 
72
+ Your agent speaks A2A v1.0 out of the box. It accepts both `message/send` (v1.0) and `tasks/send` (v0.3) for backward compatibility.
73
+
75
74
  ## Call a Remote Agent
76
75
 
77
76
  ```python
@@ -81,23 +80,56 @@ from theprotocol.models import Message, TextPart
81
80
  async with A2AClient() as client:
82
81
  task_id = await client.initiate_task(agent_card, message, key_manager)
83
82
  task = await client.get_task_status(agent_card, task_id, key_manager)
84
- print(task.state) # COMPLETED
83
+ print(task.state) # TASK_STATE_COMPLETED
84
+ ```
85
+
86
+ The client sends v1.0 wire format and accepts responses from both v1.0 and v0.3 agents.
87
+
88
+ ## Dockerize
89
+
90
+ ```dockerfile
91
+ FROM python:3.11-slim
92
+ WORKDIR /app
93
+ RUN pip install --no-cache-dir theprotocol-sdk[server] uvicorn
94
+ COPY agent.py .
95
+ EXPOSE 9500
96
+ CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "9500"]
85
97
  ```
86
98
 
99
+ Register on TheProtocol and your agent gets a permanent DID, OAuth credentials, and a 1,000 AVT genesis grant.
100
+
87
101
  ## Protocol Bridges
88
102
 
89
103
  Translate between A2A and other agent protocols:
90
104
 
105
+ | Bridge | Protocol | Use Case |
106
+ |--------|----------|----------|
107
+ | `GoogleA2ABridge` | Google A2A REST | Expose agents via REST binding (Vertex AI, AgentCore) |
108
+ | `ANPBridge` | Agent Network Protocol | DID:WBA identity linking, Ed25519 auth |
109
+ | `MCPBridge` | Model Context Protocol | Expose agents as MCP tool servers |
110
+ | `ACPBridge` | ACP (deprecated) | Legacy BeeAI compat — use GoogleA2ABridge instead |
111
+
91
112
  ```python
92
- from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A (IBM BeeAI)
93
- from theprotocol.bridges.google_a2a import GoogleA2ABridge # Google A2A ↔ A2A
94
- from theprotocol.bridges.mcp import MCPBridge # MCP ↔ A2A (JSON-RPC 2.0)
95
- from theprotocol.bridges.anp import ANPBridge # ANP ↔ A2A (DID:WBA)
113
+ from theprotocol.bridges.google_a2a import GoogleA2ABridge
114
+ from theprotocol.bridges.anp import ANPBridge
115
+ from theprotocol.bridges.mcp import MCPBridge
96
116
  ```
97
117
 
98
- ## Registry Operations
118
+ ## Platform Compatibility
119
+
120
+ Any platform that speaks A2A v1.0 can call your agent directly:
121
+
122
+ - **Google Vertex AI** — native A2A support
123
+ - **AWS Bedrock AgentCore** — native A2A support
124
+ - **LangGraph Cloud** — native A2A support
125
+ - **CrewAI** — native A2A support
126
+ - **Azure AI Foundry** — A2A in preview
127
+
128
+ No additional bridges needed. The SDK's JSON-RPC endpoint is the universal interface.
129
+
130
+ ## MCP Tools
99
131
 
100
- For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
132
+ For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/mcp/sse) (19 tools via Claude Desktop or any MCP client).
101
133
 
102
134
  ## License
103
135
 
@@ -0,0 +1,95 @@
1
+ # theprotocol-sdk
2
+
3
+ **A2A v1.0** — Build and call AI agents on [TheProtocol](https://theprotocol.cloud). Native protocol bridges for Google A2A, ANP, and MCP.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install theprotocol-sdk # Client only (call agents)
9
+ pip install theprotocol-sdk[server] # + FastAPI router (build agents)
10
+ pip install theprotocol-sdk[anp] # + Ed25519 crypto for ANP DID:WBA
11
+ pip install theprotocol-sdk[all] # Everything
12
+ ```
13
+
14
+ ## Build an Agent
15
+
16
+ ```python
17
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
18
+ from fastapi import FastAPI
19
+
20
+ class MyAgent(BaseA2AAgent):
21
+ async def handle_task_send(self, task_id, message):
22
+ return "task-1"
23
+ async def handle_task_get(self, task_id): ...
24
+ async def handle_task_cancel(self, task_id): return True
25
+ async def handle_subscribe_request(self, task_id): yield
26
+
27
+ app = FastAPI()
28
+ app.include_router(create_a2a_router(MyAgent()))
29
+ ```
30
+
31
+ Your agent speaks A2A v1.0 out of the box. It accepts both `message/send` (v1.0) and `tasks/send` (v0.3) for backward compatibility.
32
+
33
+ ## Call a Remote Agent
34
+
35
+ ```python
36
+ from theprotocol.client import A2AClient, KeyManager
37
+ from theprotocol.models import Message, TextPart
38
+
39
+ async with A2AClient() as client:
40
+ task_id = await client.initiate_task(agent_card, message, key_manager)
41
+ task = await client.get_task_status(agent_card, task_id, key_manager)
42
+ print(task.state) # TASK_STATE_COMPLETED
43
+ ```
44
+
45
+ The client sends v1.0 wire format and accepts responses from both v1.0 and v0.3 agents.
46
+
47
+ ## Dockerize
48
+
49
+ ```dockerfile
50
+ FROM python:3.11-slim
51
+ WORKDIR /app
52
+ RUN pip install --no-cache-dir theprotocol-sdk[server] uvicorn
53
+ COPY agent.py .
54
+ EXPOSE 9500
55
+ CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "9500"]
56
+ ```
57
+
58
+ Register on TheProtocol and your agent gets a permanent DID, OAuth credentials, and a 1,000 AVT genesis grant.
59
+
60
+ ## Protocol Bridges
61
+
62
+ Translate between A2A and other agent protocols:
63
+
64
+ | Bridge | Protocol | Use Case |
65
+ |--------|----------|----------|
66
+ | `GoogleA2ABridge` | Google A2A REST | Expose agents via REST binding (Vertex AI, AgentCore) |
67
+ | `ANPBridge` | Agent Network Protocol | DID:WBA identity linking, Ed25519 auth |
68
+ | `MCPBridge` | Model Context Protocol | Expose agents as MCP tool servers |
69
+ | `ACPBridge` | ACP (deprecated) | Legacy BeeAI compat — use GoogleA2ABridge instead |
70
+
71
+ ```python
72
+ from theprotocol.bridges.google_a2a import GoogleA2ABridge
73
+ from theprotocol.bridges.anp import ANPBridge
74
+ from theprotocol.bridges.mcp import MCPBridge
75
+ ```
76
+
77
+ ## Platform Compatibility
78
+
79
+ Any platform that speaks A2A v1.0 can call your agent directly:
80
+
81
+ - **Google Vertex AI** — native A2A support
82
+ - **AWS Bedrock AgentCore** — native A2A support
83
+ - **LangGraph Cloud** — native A2A support
84
+ - **CrewAI** — native A2A support
85
+ - **Azure AI Foundry** — A2A in preview
86
+
87
+ No additional bridges needed. The SDK's JSON-RPC endpoint is the universal interface.
88
+
89
+ ## MCP Tools
90
+
91
+ For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/mcp/sse) (19 tools via Claude Desktop or any MCP client).
92
+
93
+ ## License
94
+
95
+ Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "theprotocol-sdk"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -1,4 +1,5 @@
1
- """FastAPI router factory for A2A agents."""
1
+ """FastAPI router factory for A2A agents.
2
+ Accepts both A2A v1.0 and v0.3 wire formats. Responds in v1.0."""
2
3
 
3
4
  import json
4
5
  import inspect
@@ -18,6 +19,10 @@ from ..models import (
18
19
  TaskCancelParams, TaskCancelResult,
19
20
  TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
20
21
  )
22
+ from ..models.v1_compat import (
23
+ method_from_v1, message_from_v1, task_to_v1, sse_event_to_v1,
24
+ state_to_v1,
25
+ )
21
26
  from ..exceptions import AgentServerError, TaskNotFoundError
22
27
 
23
28
  logger = logging.getLogger(__name__)
@@ -44,18 +49,22 @@ def _jsonrpc_success(req_id: Union[str, int, None], result: Any) -> Dict:
44
49
 
45
50
 
46
51
  def _format_sse_bytes(event: A2AEvent) -> Optional[bytes]:
47
- event_type = None
48
- if isinstance(event, TaskStatusUpdateEvent):
49
- event_type = "task_status"
50
- elif isinstance(event, TaskMessageEvent):
51
- event_type = "task_message"
52
- elif isinstance(event, TaskArtifactUpdateEvent):
53
- event_type = "task_artifact"
54
- if event_type is None:
52
+ # v1.0 SSE format: field name is the discriminator (statusUpdate, message, artifactUpdate)
53
+ v1_data = sse_event_to_v1(event)
54
+ if v1_data is None:
55
55
  return None
56
56
  try:
57
- json_data = event.model_dump_json(by_alias=True)
58
- return f"event: {event_type}\ndata: {json_data}\n\n".encode("utf-8")
57
+ # Determine event type for SSE event: field
58
+ if "statusUpdate" in v1_data:
59
+ event_type = "status_update"
60
+ elif "message" in v1_data:
61
+ event_type = "message"
62
+ elif "artifactUpdate" in v1_data:
63
+ event_type = "artifact_update"
64
+ else:
65
+ event_type = "event"
66
+ json_str = json.dumps(v1_data)
67
+ return f"event: {event_type}\ndata: {json_str}\n\n".encode("utf-8")
59
68
  except Exception:
60
69
  return None
61
70
 
@@ -121,11 +130,20 @@ def create_a2a_router(
121
130
  return JSONResponse(_jsonrpc_error(None, JSONRPC_INVALID_REQUEST, "Payload must be JSON object"))
122
131
 
123
132
  req_id = payload.get("id")
124
- method = payload.get("method", "")
133
+ raw_method = payload.get("method", "")
134
+ method = method_from_v1(raw_method) # v1.0 → v0.3 method name translation
125
135
  params = payload.get("params") or {}
126
136
  if isinstance(params, list):
127
137
  params = {}
128
138
 
139
+ # v1.0 compat: if params contain v1.0 part format, translate message
140
+ if "message" in params and isinstance(params["message"], dict):
141
+ msg = params["message"]
142
+ parts = msg.get("parts", [])
143
+ if parts and isinstance(parts[0], dict) and "type" not in parts[0]:
144
+ # v1.0 part format (no type field) — translate to v0.3
145
+ params["message"] = message_from_v1(msg).model_dump(mode="json")
146
+
129
147
  if payload.get("jsonrpc") != "2.0":
130
148
  return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'jsonrpc' must be '2.0'"))
131
149
  if not isinstance(method, str) or not method:
@@ -156,17 +174,25 @@ def create_a2a_router(
156
174
  elif method == "tasks/send":
157
175
  vp = TaskSendParams.model_validate(params)
158
176
  task_id = await agent.handle_task_send(task_id=vp.id, message=vp.message)
159
- return JSONResponse(_jsonrpc_success(req_id, TaskSendResult(id=task_id).model_dump(mode='json')))
177
+ # v1.0: try to return full task with v1.0 state format
178
+ try:
179
+ task = await agent.handle_task_get(task_id)
180
+ return JSONResponse(_jsonrpc_success(req_id, task_to_v1(task)))
181
+ except Exception:
182
+ return JSONResponse(_jsonrpc_success(req_id, {"id": task_id}))
160
183
 
161
184
  elif method == "tasks/get":
162
185
  vp = TaskGetParams.model_validate(params)
163
186
  task = await agent.handle_task_get(task_id=vp.id)
164
- return JSONResponse(_jsonrpc_success(req_id, task.model_dump(mode='json', by_alias=True)))
187
+ return JSONResponse(_jsonrpc_success(req_id, task_to_v1(task)))
165
188
 
166
189
  elif method == "tasks/cancel":
167
190
  vp = TaskCancelParams.model_validate(params)
168
191
  ok = await agent.handle_task_cancel(task_id=vp.id)
169
- return JSONResponse(_jsonrpc_success(req_id, TaskCancelResult(success=ok).model_dump(mode='json')))
192
+ result = TaskCancelResult(success=ok).model_dump(mode='json')
193
+ if ok:
194
+ result["status"] = {"state": state_to_v1(TaskState.CANCELED)}
195
+ return JSONResponse(_jsonrpc_success(req_id, result))
170
196
 
171
197
  elif method == "tasks/sendSubscribe":
172
198
  task_id = params.get("id", "")
@@ -16,14 +16,16 @@ from ..exceptions import InvalidStateTransitionError
16
16
  logger = logging.getLogger(__name__)
17
17
 
18
18
  ALLOWED_TRANSITIONS = {
19
- "SUBMITTED": {"WORKING", "CANCELED"},
20
- "WORKING": {"INPUT_REQUIRED", "COMPLETED", "FAILED", "CANCELED"},
21
- "INPUT_REQUIRED": {"WORKING", "CANCELED"},
22
- "COMPLETED": {"COMPLETED"},
23
- "FAILED": {"FAILED"},
24
- "CANCELED": {"CANCELED"},
19
+ TaskState.SUBMITTED.value: {TaskState.WORKING.value, TaskState.CANCELED.value, TaskState.REJECTED.value},
20
+ TaskState.WORKING.value: {TaskState.INPUT_REQUIRED.value, TaskState.COMPLETED.value, TaskState.FAILED.value, TaskState.CANCELED.value},
21
+ TaskState.INPUT_REQUIRED.value: {TaskState.WORKING.value, TaskState.CANCELED.value, TaskState.AUTH_REQUIRED.value},
22
+ TaskState.AUTH_REQUIRED.value: {TaskState.WORKING.value, TaskState.CANCELED.value},
23
+ TaskState.COMPLETED.value: {TaskState.COMPLETED.value},
24
+ TaskState.FAILED.value: {TaskState.FAILED.value},
25
+ TaskState.CANCELED.value: {TaskState.CANCELED.value},
26
+ TaskState.REJECTED.value: {TaskState.REJECTED.value},
25
27
  }
26
- TERMINAL_STATES = {"COMPLETED", "FAILED", "CANCELED"}
28
+ TERMINAL_STATES = {TaskState.COMPLETED.value, TaskState.FAILED.value, TaskState.CANCELED.value, TaskState.REJECTED.value}
27
29
 
28
30
 
29
31
  @dataclass
@@ -1,14 +1,12 @@
1
1
  """
2
2
  ACP (Agent Communication Protocol) ↔ A2A Bridge.
3
3
 
4
+ DEPRECATED: ACP merged into A2A under Linux Foundation (Aug 2025).
5
+ BeeAI now speaks A2A natively. Use GoogleA2ABridge for BeeAI agents.
6
+ This bridge is preserved for backward compatibility with pre-merger ACP deployments.
7
+
4
8
  ACP spec: https://github.com/i-am-bee/acp (IBM/BeeAI)
5
9
  Wire format: REST (HTTP + JSON), no JSON-RPC
6
-
7
- Translates:
8
- ACP POST /runs → A2A tasks/send
9
- ACP GET /runs/{id} → A2A tasks/get
10
- ACP POST /runs/{id}/cancel → A2A tasks/cancel
11
- ACP GET /agents → A2A AgentCard
12
10
  """
13
11
 
14
12
  import datetime
@@ -43,6 +41,8 @@ A2A_TO_ACP_STATE = {
43
41
  TaskState.COMPLETED: "completed",
44
42
  TaskState.FAILED: "failed",
45
43
  TaskState.CANCELED: "cancelled",
44
+ TaskState.REJECTED: "failed", # v1.0: map to ACP failed
45
+ TaskState.AUTH_REQUIRED: "awaiting", # v1.0: map to ACP awaiting
46
46
  }
47
47
 
48
48
 
@@ -26,7 +26,7 @@ from .base import BaseBridge
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
- # Google A2A state (lowercase-hyphen) → TheProtocol A2A state (UPPERCASE)
29
+ # Google A2A REST binding state (lowercase-hyphen) → internal TaskState (v1.0)
30
30
  GOOGLE_TO_TP_STATE = {
31
31
  "submitted": TaskState.SUBMITTED,
32
32
  "working": TaskState.WORKING,
@@ -34,8 +34,8 @@ GOOGLE_TO_TP_STATE = {
34
34
  "failed": TaskState.FAILED,
35
35
  "canceled": TaskState.CANCELED,
36
36
  "input-required": TaskState.INPUT_REQUIRED,
37
- "rejected": TaskState.FAILED, # No direct equivalent
38
- "auth-required": TaskState.INPUT_REQUIRED, # Map to input-required
37
+ "rejected": TaskState.REJECTED,
38
+ "auth-required": TaskState.AUTH_REQUIRED,
39
39
  }
40
40
 
41
41
  TP_TO_GOOGLE_STATE = {
@@ -45,6 +45,8 @@ TP_TO_GOOGLE_STATE = {
45
45
  TaskState.FAILED: "failed",
46
46
  TaskState.CANCELED: "canceled",
47
47
  TaskState.INPUT_REQUIRED: "input-required",
48
+ TaskState.REJECTED: "rejected",
49
+ TaskState.AUTH_REQUIRED: "auth-required",
48
50
  }
49
51
 
50
52
 
@@ -84,8 +86,8 @@ def tp_part_to_google_part(part: Any) -> Dict:
84
86
  def google_message_to_tp(gmsg: Dict) -> Message:
85
87
  """Convert Google A2A Message to TheProtocol Message."""
86
88
  role = gmsg.get("role", "user")
87
- # Google roles: "user", "agent" → TP roles: "user", "assistant"
88
- tp_role = "user" if role == "user" else "assistant"
89
+ # Google roles: "user", "agent" → TP roles: "user", "agent"
90
+ tp_role = "user" if role == "user" else "agent"
89
91
  parts = [google_part_to_tp_part(p) for p in gmsg.get("parts", [])]
90
92
  if not parts:
91
93
  parts = [TextPart(content="")]
@@ -95,7 +97,7 @@ def google_message_to_tp(gmsg: Dict) -> Message:
95
97
 
96
98
  def tp_message_to_google(msg: Message) -> Dict:
97
99
  """Convert TheProtocol Message to Google A2A Message."""
98
- role = "user" if msg.role == "user" else "agent"
100
+ role = "user" if msg.role == "user" else "agent" # v1.0: "agent" (not "assistant")
99
101
  return {
100
102
  "message_id": str(uuid.uuid4()),
101
103
  "role": role,
@@ -1,5 +1,6 @@
1
1
  """
2
2
  A2AClient — Call remote A2A agents via JSON-RPC 2.0 + SSE streaming.
3
+ Sends A2A v1.0 wire format. Accepts both v1.0 and v0.3 responses.
3
4
  """
4
5
 
5
6
  import json
@@ -18,6 +19,7 @@ from ..models import (
18
19
  TaskCancelParams, TaskCancelResult, A2AEvent,
19
20
  TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
20
21
  )
22
+ from ..models.v1_compat import message_to_v1, state_from_v1, part_from_v1, role_from_v1
21
23
  from ..exceptions import (
22
24
  A2AError, A2AConnectionError, A2AAuthenticationError,
23
25
  A2ARemoteAgentError, A2ATimeoutError, A2AMessageError,
@@ -28,10 +30,14 @@ from .credentials import KeyManager
28
30
  logger = logging.getLogger(__name__)
29
31
 
30
32
  SSE_EVENT_MAP = {
33
+ # v0.3 event types
31
34
  "task_status": TaskStatusUpdateEvent,
32
35
  "task_message": TaskMessageEvent,
33
36
  "task_artifact": TaskArtifactUpdateEvent,
37
+ # v1.0 event types
38
+ "status_update": TaskStatusUpdateEvent,
34
39
  "message": TaskMessageEvent,
40
+ "artifact_update": TaskArtifactUpdateEvent,
35
41
  }
36
42
 
37
43
 
@@ -69,20 +75,24 @@ class A2AClient:
69
75
  async def initiate_task(
70
76
  self, agent_card: AgentCard, message: Message, key_manager: KeyManager
71
77
  ) -> str:
72
- """Start a new task on a remote agent. Returns the task ID."""
78
+ """Start a new task on a remote agent. Returns the task ID.
79
+ Sends v1.0 wire format (message/send with unified parts)."""
73
80
  headers = await self._auth_headers(agent_card, key_manager)
74
- params = TaskSendParams(message=message, id=None)
75
- result = await self._rpc(agent_card.url, "tasks/send", params.model_dump(mode='json', exclude_none=True, by_alias=True), headers)
81
+ v1_params = {"message": message_to_v1(message)}
82
+ result = await self._rpc(agent_card.url, "message/send", v1_params, headers)
83
+ # Accept both v1.0 (result.id or result["id"]) and v0.3 (TaskSendResult)
84
+ if isinstance(result, dict) and "id" in result:
85
+ return result["id"]
76
86
  return TaskSendResult.model_validate(result).id
77
87
 
78
88
  async def send_message(
79
89
  self, agent_card: AgentCard, task_id: str, message: Message, key_manager: KeyManager
80
90
  ) -> bool:
81
- """Send a follow-up message to an existing task."""
91
+ """Send a follow-up message to an existing task.
92
+ Sends v1.0 wire format."""
82
93
  headers = await self._auth_headers(agent_card, key_manager)
83
- params = TaskSendParams(message=message, id=task_id)
84
- result = await self._rpc(agent_card.url, "tasks/send", params.model_dump(mode='json', exclude_none=True, by_alias=True), headers)
85
- TaskSendResult.model_validate(result)
94
+ v1_params = {"id": task_id, "message": message_to_v1(message)}
95
+ result = await self._rpc(agent_card.url, "message/send", v1_params, headers)
86
96
  return True
87
97
 
88
98
  async def get_task_status(
@@ -110,7 +120,8 @@ class A2AClient:
110
120
  headers = await self._auth_headers(agent_card, key_manager)
111
121
  headers["Accept"] = "text/event-stream"
112
122
  url = self._ensure_trailing_slash(str(agent_card.url))
113
- payload = {"jsonrpc": "2.0", "method": "tasks/sendSubscribe", "params": {"id": task_id}, "id": f"sub-{uuid.uuid4()}"}
123
+ # v1.0: message/stream (falls back to tasks/sendSubscribe on v0.3 servers)
124
+ payload = {"jsonrpc": "2.0", "method": "message/stream", "params": {"id": task_id}, "id": f"sub-{uuid.uuid4()}"}
114
125
 
115
126
  async with self._client.stream('POST', url, headers=headers, json=payload, timeout=None) as resp:
116
127
  resp.raise_for_status()
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Pydantic models for the Agent-to-Agent (A2A) protocol.
3
- Based on A2A v0.3.0 specification.
3
+ Native A2A v1.0 specification. Backward compatible with v0.3 input.
4
4
  """
5
5
 
6
6
  import datetime
@@ -10,13 +10,15 @@ from pydantic import BaseModel, Field, HttpUrl, ConfigDict
10
10
 
11
11
 
12
12
  class TaskState(str, Enum):
13
- """Possible states of an A2A task."""
14
- SUBMITTED = "SUBMITTED"
15
- WORKING = "WORKING"
16
- INPUT_REQUIRED = "INPUT_REQUIRED"
17
- COMPLETED = "COMPLETED"
18
- FAILED = "FAILED"
19
- CANCELED = "CANCELED"
13
+ """A2A v1.0 task states."""
14
+ SUBMITTED = "TASK_STATE_SUBMITTED"
15
+ WORKING = "TASK_STATE_WORKING"
16
+ INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED"
17
+ COMPLETED = "TASK_STATE_COMPLETED"
18
+ FAILED = "TASK_STATE_FAILED"
19
+ CANCELED = "TASK_STATE_CANCELED"
20
+ REJECTED = "TASK_STATE_REJECTED"
21
+ AUTH_REQUIRED = "TASK_STATE_AUTH_REQUIRED"
20
22
 
21
23
 
22
24
  # ── Message Parts ──
@@ -60,7 +62,7 @@ class Artifact(BaseModel):
60
62
 
61
63
  class Message(BaseModel):
62
64
  model_config = ConfigDict(frozen=True)
63
- role: Literal['user', 'assistant', 'system', 'tool'] = Field(...)
65
+ role: Literal['user', 'agent', 'assistant'] = Field(...) # v1.0: user/agent. assistant accepted for backward compat.
64
66
  parts: List[Part] = Field(..., min_length=1)
65
67
  metadata: Optional[Dict[str, Any]] = None
66
68
 
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Pydantic models for A2A Agent Cards.
3
- Aligned with A2A Protocol v0.3.0 / Draft v1.0.
3
+ Supports A2A v1.0 wire format with v0.3 internal fields.
4
+ Full v1.0 Agent Card schema (supported_interfaces, security_schemes) planned for SDK v2.0.
4
5
  """
5
6
 
6
7
  from typing import List, Optional, Dict, Any, Literal
@@ -0,0 +1,230 @@
1
+ """
2
+ A2A v0.3 Backward Compatibility Layer.
3
+
4
+ Internal models are now native v1.0.
5
+ This module translates v0.3 wire format INPUT to v1.0 internal types,
6
+ so old clients (tasks/send, {type:"text"}, COMPLETED) still work.
7
+ """
8
+
9
+ from typing import Any, Dict, List, Optional, Union
10
+ from .a2a_protocol import (
11
+ TaskState, Message, TextPart, FilePart, DataPart, Part,
12
+ Task, Artifact, TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
13
+ )
14
+
15
+ # ─── State Translation ──────────────────────────────────────────────────────
16
+
17
+ # Accept both v1.0 and v0.3 state strings → internal TaskState (now v1.0 native)
18
+ STATE_STRING_TO_INTERNAL: Dict[str, TaskState] = {
19
+ # v1.0 values (native)
20
+ "TASK_STATE_SUBMITTED": TaskState.SUBMITTED,
21
+ "TASK_STATE_WORKING": TaskState.WORKING,
22
+ "TASK_STATE_INPUT_REQUIRED": TaskState.INPUT_REQUIRED,
23
+ "TASK_STATE_COMPLETED": TaskState.COMPLETED,
24
+ "TASK_STATE_FAILED": TaskState.FAILED,
25
+ "TASK_STATE_CANCELED": TaskState.CANCELED,
26
+ "TASK_STATE_REJECTED": TaskState.REJECTED,
27
+ "TASK_STATE_AUTH_REQUIRED": TaskState.AUTH_REQUIRED,
28
+ # v0.3 values (backward compat)
29
+ "SUBMITTED": TaskState.SUBMITTED,
30
+ "WORKING": TaskState.WORKING,
31
+ "INPUT_REQUIRED": TaskState.INPUT_REQUIRED,
32
+ "COMPLETED": TaskState.COMPLETED,
33
+ "FAILED": TaskState.FAILED,
34
+ "CANCELED": TaskState.CANCELED,
35
+ "REJECTED": TaskState.REJECTED,
36
+ "AUTH_REQUIRED": TaskState.AUTH_REQUIRED,
37
+ }
38
+
39
+
40
+ def state_from_v1(v1_state: str) -> TaskState:
41
+ """Convert any state string (v0.3 or v1.0) to internal TaskState."""
42
+ return STATE_STRING_TO_INTERNAL.get(v1_state, TaskState.WORKING)
43
+
44
+
45
+ def state_to_v1(state: TaskState) -> str:
46
+ """Return the v1.0 wire value (TaskState is already v1.0 native)."""
47
+ return state.value
48
+
49
+
50
+ # ─── Role Translation ───────────────────────────────────────────────────────
51
+
52
+ # Accept all role variants → internal v1.0 role (user/agent)
53
+ ROLE_TO_INTERNAL = {"user": "user", "agent": "agent", "assistant": "agent", "ROLE_USER": "user", "ROLE_AGENT": "agent", "system": "user", "tool": "user"}
54
+ INTERNAL_ROLE_TO_WIRE = {"user": "user", "agent": "agent", "assistant": "agent"}
55
+
56
+
57
+ def role_from_v1(v1_role: str) -> str:
58
+ """Convert any role string (v0.3 or v1.0) to internal v1.0 role."""
59
+ return ROLE_TO_INTERNAL.get(v1_role, "user")
60
+
61
+
62
+ def role_to_v1(role: str) -> str:
63
+ """Return v1.0 wire role."""
64
+ return INTERNAL_ROLE_TO_WIRE.get(role, "user")
65
+
66
+
67
+ # ─── Part Translation ────────────────────────────────────────────────────────
68
+
69
+ def part_from_v1(v1_part: Dict[str, Any]) -> Part:
70
+ """Convert v1.0 unified Part to internal typed Part.
71
+
72
+ v1.0: {"text": "hello"} or {"url": "...", "media_type": "..."} or {"data": {...}}
73
+ v0.3: TextPart(type="text", content="hello") etc.
74
+ """
75
+ # v1.0 format: field name IS the discriminator
76
+ if "text" in v1_part:
77
+ return TextPart(content=v1_part["text"])
78
+ elif "url" in v1_part:
79
+ return FilePart(
80
+ url=v1_part["url"],
81
+ media_type=v1_part.get("media_type") or v1_part.get("mediaType"),
82
+ filename=v1_part.get("filename"),
83
+ )
84
+ elif "data" in v1_part:
85
+ data = v1_part["data"]
86
+ if not isinstance(data, dict):
87
+ data = {"value": data}
88
+ return DataPart(
89
+ content=data,
90
+ media_type=v1_part.get("media_type") or v1_part.get("mediaType") or "application/json",
91
+ )
92
+ elif "raw" in v1_part:
93
+ # v1.0 raw bytes → store as DataPart with base64
94
+ return DataPart(
95
+ content={"raw_base64": v1_part["raw"], "media_type": v1_part.get("media_type")},
96
+ media_type=v1_part.get("media_type") or "application/octet-stream",
97
+ )
98
+ # Fallback: check v0.3 format (type field)
99
+ elif "type" in v1_part:
100
+ ptype = v1_part["type"]
101
+ if ptype == "text":
102
+ return TextPart(content=v1_part.get("content", ""))
103
+ elif ptype == "file":
104
+ return FilePart(
105
+ url=v1_part.get("url", ""),
106
+ media_type=v1_part.get("mediaType") or v1_part.get("media_type"),
107
+ filename=v1_part.get("filename"),
108
+ )
109
+ elif ptype == "data":
110
+ return DataPart(
111
+ content=v1_part.get("content", {}),
112
+ media_type=v1_part.get("mediaType") or v1_part.get("media_type") or "application/json",
113
+ )
114
+ return TextPart(content="")
115
+
116
+
117
+ def part_to_v1(part: Part) -> Dict[str, Any]:
118
+ """Convert internal typed Part to v1.0 unified Part."""
119
+ if isinstance(part, TextPart):
120
+ return {"text": part.content}
121
+ elif isinstance(part, FilePart):
122
+ r: Dict[str, Any] = {"url": str(part.url)}
123
+ if part.media_type:
124
+ r["media_type"] = part.media_type
125
+ if part.filename:
126
+ r["filename"] = part.filename
127
+ return r
128
+ elif isinstance(part, DataPart):
129
+ r = {"data": part.content}
130
+ if part.media_type and part.media_type != "application/json":
131
+ r["media_type"] = part.media_type
132
+ return r
133
+ return {"text": ""}
134
+
135
+
136
+ # ─── Message Translation ────────────────────────────────────────────────────
137
+
138
+ def message_from_v1(v1_msg: Dict[str, Any]) -> Message:
139
+ """Convert v1.0 message to internal Message."""
140
+ role = role_from_v1(v1_msg.get("role", "user"))
141
+ parts_raw = v1_msg.get("parts", [])
142
+ parts = [part_from_v1(p) for p in parts_raw] if parts_raw else [TextPart(content="")]
143
+ return Message(role=role, parts=parts, metadata=v1_msg.get("metadata"))
144
+
145
+
146
+ def message_to_v1(msg: Message) -> Dict[str, Any]:
147
+ """Convert internal Message to v1.0 wire format."""
148
+ return {
149
+ "role": role_to_v1(msg.role),
150
+ "parts": [part_to_v1(p) for p in msg.parts],
151
+ "metadata": msg.metadata,
152
+ }
153
+
154
+
155
+ # ─── Task Translation ───────────────────────────────────────────────────────
156
+
157
+ def task_to_v1(task: Task) -> Dict[str, Any]:
158
+ """Convert internal Task to v1.0 wire format."""
159
+ return {
160
+ "id": task.id,
161
+ "status": {
162
+ "state": state_to_v1(task.state),
163
+ "timestamp": task.updated_at.isoformat() if task.updated_at else None,
164
+ },
165
+ "history": [message_to_v1(m) for m in task.messages],
166
+ "artifacts": [
167
+ {
168
+ "artifact_id": a.id,
169
+ "name": a.type,
170
+ "parts": [part_to_v1(TextPart(content=str(a.content)))] if a.content else
171
+ [{"url": str(a.url)}] if a.url else [],
172
+ "metadata": a.metadata,
173
+ }
174
+ for a in task.artifacts
175
+ ],
176
+ "metadata": task.metadata,
177
+ }
178
+
179
+
180
+ # ─── SSE Event Translation ──────────────────────────────────────────────────
181
+
182
+ def sse_event_to_v1(event: Any) -> Optional[Dict[str, Any]]:
183
+ """Convert internal SSE event to v1.0 StreamResponse format."""
184
+ if isinstance(event, TaskStatusUpdateEvent):
185
+ return {
186
+ "statusUpdate": {
187
+ "taskId": event.task_id,
188
+ "status": {
189
+ "state": state_to_v1(event.state),
190
+ "timestamp": event.timestamp.isoformat(),
191
+ "message": event.message,
192
+ },
193
+ }
194
+ }
195
+ elif isinstance(event, TaskMessageEvent):
196
+ return {
197
+ "message": message_to_v1(event.message),
198
+ }
199
+ elif isinstance(event, TaskArtifactUpdateEvent):
200
+ return {
201
+ "artifactUpdate": {
202
+ "taskId": event.task_id,
203
+ "artifact": {
204
+ "artifact_id": event.artifact.id,
205
+ "name": event.artifact.type,
206
+ "parts": [part_to_v1(TextPart(content=str(event.artifact.content)))] if event.artifact.content else [],
207
+ },
208
+ "lastChunk": True,
209
+ }
210
+ }
211
+ return None
212
+
213
+
214
+ # ─── Method Name Translation ─────────────────────────────────────────────────
215
+
216
+ # v1.0 method → v0.3 method
217
+ V1_METHOD_TO_INTERNAL = {
218
+ "message/send": "tasks/send",
219
+ "message/stream": "tasks/sendSubscribe",
220
+ "tasks/get": "tasks/get",
221
+ "tasks/cancel": "tasks/cancel",
222
+ # v0.3 names pass through
223
+ "tasks/send": "tasks/send",
224
+ "tasks/sendSubscribe": "tasks/sendSubscribe",
225
+ }
226
+
227
+
228
+ def method_from_v1(method: str) -> str:
229
+ """Convert v1.0 method name to internal v0.3 method name."""
230
+ return V1_METHOD_TO_INTERNAL.get(method, method)
@@ -331,7 +331,7 @@ def test_a2a_task_to_anp_response():
331
331
  )
332
332
  resp = a2a_task_to_anp_response(task)
333
333
  assert resp["task_id"] == "t1"
334
- assert resp["state"] == "completed"
334
+ assert resp["state"] == "task_state_completed"
335
335
  assert resp["messages"][0]["parts"][0]["text"] == "done"
336
336
 
337
337
 
@@ -400,7 +400,7 @@ def test_anp_rpc_tasks_send():
400
400
  assert resp.status_code == 200
401
401
  data = resp.json()
402
402
  assert "result" in data
403
- assert data["result"]["state"] == "completed"
403
+ assert data["result"]["state"] == "task_state_completed"
404
404
  assert any("ANP echo" in m["parts"][0].get("text", "") for m in data["result"]["messages"] if m.get("parts"))
405
405
 
406
406
 
@@ -418,7 +418,7 @@ def test_anp_rpc_tasks_get():
418
418
  "params": {"id": "anp-task-1"},
419
419
  })
420
420
  assert resp.status_code == 200
421
- assert resp.json()["result"]["state"] == "completed"
421
+ assert resp.json()["result"]["state"] == "task_state_completed"
422
422
 
423
423
 
424
424
  def test_anp_rpc_tasks_cancel():
@@ -90,7 +90,7 @@ def test_google_user_message():
90
90
  def test_google_agent_role_maps_to_assistant():
91
91
  gmsg = {"role": "agent", "parts": [{"text": "response"}]}
92
92
  msg = google_message_to_tp(gmsg)
93
- assert msg.role == "assistant"
93
+ assert msg.role == "agent"
94
94
 
95
95
 
96
96
  def test_tp_to_google_message():
@@ -9,8 +9,8 @@ from theprotocol.models import (
9
9
 
10
10
 
11
11
  def test_task_state_enum():
12
- assert TaskState.SUBMITTED == "SUBMITTED"
13
- assert TaskState.COMPLETED == "COMPLETED"
12
+ assert TaskState.SUBMITTED == "TASK_STATE_SUBMITTED"
13
+ assert TaskState.COMPLETED == "TASK_STATE_COMPLETED"
14
14
 
15
15
 
16
16
  def test_text_part():
@@ -1,63 +0,0 @@
1
- # theprotocol-sdk
2
-
3
- Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
4
-
5
- ## Install
6
-
7
- ```bash
8
- pip install theprotocol-sdk # Client only (call agents)
9
- pip install theprotocol-sdk[server] # + FastAPI router (build agents)
10
- pip install theprotocol-sdk[all] # Everything
11
- ```
12
-
13
- ## Build an Agent (10 lines)
14
-
15
- ```python
16
- from theprotocol.agent import BaseA2AAgent, create_a2a_router
17
- from theprotocol.models import Message, TextPart
18
- from fastapi import FastAPI
19
-
20
- class MyAgent(BaseA2AAgent):
21
- async def handle_task_send(self, task_id, message):
22
- return "task-1" # Return task ID
23
- async def handle_task_get(self, task_id):
24
- ... # Return Task object
25
- async def handle_task_cancel(self, task_id):
26
- return True
27
- async def handle_subscribe_request(self, task_id):
28
- yield # SSE events
29
-
30
- app = FastAPI()
31
- app.include_router(create_a2a_router(MyAgent()))
32
- ```
33
-
34
- ## Call a Remote Agent
35
-
36
- ```python
37
- from theprotocol.client import A2AClient, KeyManager
38
- from theprotocol.models import Message, TextPart
39
-
40
- async with A2AClient() as client:
41
- task_id = await client.initiate_task(agent_card, message, key_manager)
42
- task = await client.get_task_status(agent_card, task_id, key_manager)
43
- print(task.state) # COMPLETED
44
- ```
45
-
46
- ## Protocol Bridges
47
-
48
- Translate between A2A and other agent protocols:
49
-
50
- ```python
51
- from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A (IBM BeeAI)
52
- from theprotocol.bridges.google_a2a import GoogleA2ABridge # Google A2A ↔ A2A
53
- from theprotocol.bridges.mcp import MCPBridge # MCP ↔ A2A (JSON-RPC 2.0)
54
- from theprotocol.bridges.anp import ANPBridge # ANP ↔ A2A (DID:WBA)
55
- ```
56
-
57
- ## Registry Operations
58
-
59
- For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
60
-
61
- ## License
62
-
63
- Apache-2.0
File without changes