roar-sdk 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: roar-sdk
3
+ Version: 0.2.0
4
+ Summary: ROAR Protocol — Python SDK. Standalone implementation of the 5-layer agent communication standard.
5
+ Project-URL: Homepage, https://github.com/ProwlrBot/roar-protocol
6
+ Project-URL: Repository, https://github.com/ProwlrBot/roar-protocol
7
+ Project-URL: Specification, https://github.com/ProwlrBot/roar-protocol/blob/main/ROAR-SPEC.md
8
+ Author-email: kdairatchi <kdairatchi@users.noreply.github.com>
9
+ License: Apache-2.0
10
+ Keywords: a2a,agents,ai,did,mcp,protocol,roar
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: pydantic>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: cryptography>=41.0; extra == 'dev'
25
+ Requires-Dist: httpx>=0.25; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Requires-Dist: websockets>=12.0; extra == 'dev'
29
+ Provides-Extra: ed25519
30
+ Requires-Dist: cryptography>=41.0; extra == 'ed25519'
31
+ Provides-Extra: http
32
+ Requires-Dist: httpx>=0.25; extra == 'http'
33
+ Provides-Extra: server
34
+ Requires-Dist: fastapi>=0.104; extra == 'server'
35
+ Requires-Dist: httpx>=0.25; extra == 'server'
36
+ Requires-Dist: uvicorn[standard]>=0.24; extra == 'server'
37
+ Provides-Extra: websocket
38
+ Requires-Dist: websockets>=12.0; extra == 'websocket'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # roar-sdk
42
+
43
+ **ROAR Protocol** — standalone Python SDK. 5-layer agent communication standard.
44
+
45
+ Design by [@kdairatchi](https://github.com/kdairatchi) — [ProwlrBot/roar-protocol](https://github.com/ProwlrBot/roar-protocol)
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ git clone https://github.com/ProwlrBot/roar-protocol.git
51
+ pip install -e ./python # types + client + server (pydantic only)
52
+ pip install -e './python[http]' # + httpx for HTTP transport
53
+ pip install -e './python[server]' # + fastapi + uvicorn for serving
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ from roar_sdk import AgentIdentity, ROARMessage, MessageIntent, ROARClient, ROARServer
60
+
61
+ # Layer 1: identity
62
+ agent = AgentIdentity(display_name="my-agent", capabilities=["code"])
63
+
64
+ # Layer 4: message
65
+ msg = ROARMessage(
66
+ **{"from": agent, "to": other},
67
+ intent=MessageIntent.DELEGATE,
68
+ payload={"task": "review"},
69
+ )
70
+ msg.sign("shared-secret")
71
+ ```
72
+
73
+ See [examples/python/](https://github.com/ProwlrBot/roar-protocol/tree/main/examples/python) for runnable server + client.
74
+ See [ROAR-SPEC.md](https://github.com/ProwlrBot/roar-protocol/blob/main/ROAR-SPEC.md) for the full protocol specification.
@@ -0,0 +1,34 @@
1
+ # roar-sdk
2
+
3
+ **ROAR Protocol** — standalone Python SDK. 5-layer agent communication standard.
4
+
5
+ Design by [@kdairatchi](https://github.com/kdairatchi) — [ProwlrBot/roar-protocol](https://github.com/ProwlrBot/roar-protocol)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ git clone https://github.com/ProwlrBot/roar-protocol.git
11
+ pip install -e ./python # types + client + server (pydantic only)
12
+ pip install -e './python[http]' # + httpx for HTTP transport
13
+ pip install -e './python[server]' # + fastapi + uvicorn for serving
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from roar_sdk import AgentIdentity, ROARMessage, MessageIntent, ROARClient, ROARServer
20
+
21
+ # Layer 1: identity
22
+ agent = AgentIdentity(display_name="my-agent", capabilities=["code"])
23
+
24
+ # Layer 4: message
25
+ msg = ROARMessage(
26
+ **{"from": agent, "to": other},
27
+ intent=MessageIntent.DELEGATE,
28
+ payload={"task": "review"},
29
+ )
30
+ msg.sign("shared-secret")
31
+ ```
32
+
33
+ See [examples/python/](https://github.com/ProwlrBot/roar-protocol/tree/main/examples/python) for runnable server + client.
34
+ See [ROAR-SPEC.md](https://github.com/ProwlrBot/roar-protocol/blob/main/ROAR-SPEC.md) for the full protocol specification.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "roar-sdk"
7
+ version = "0.2.0"
8
+ description = "ROAR Protocol — Python SDK. Standalone implementation of the 5-layer agent communication standard."
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ authors = [{ name = "kdairatchi", email = "kdairatchi@users.noreply.github.com" }]
12
+ keywords = ["agents", "ai", "protocol", "roar", "did", "a2a", "mcp"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ requires-python = ">=3.10"
26
+ dependencies = ["pydantic>=2.0"]
27
+
28
+ [project.optional-dependencies]
29
+ http = ["httpx>=0.25"]
30
+ websocket = ["websockets>=12.0"]
31
+ ed25519 = ["cryptography>=41.0"]
32
+ server = ["fastapi>=0.104", "uvicorn[standard]>=0.24", "httpx>=0.25"]
33
+ dev = ["pytest>=7.0", "pytest-asyncio>=0.23", "httpx>=0.25", "websockets>=12.0", "cryptography>=41.0"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/ProwlrBot/roar-protocol"
37
+ Repository = "https://github.com/ProwlrBot/roar-protocol"
38
+ Specification = "https://github.com/ProwlrBot/roar-protocol/blob/main/ROAR-SPEC.md"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/roar_sdk"]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+ testpaths = ["tests"]
@@ -0,0 +1,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ROAR Protocol — Python SDK.
3
+
4
+ Standalone implementation of the 5-layer agent communication standard.
5
+ Design: @kdairatchi — https://github.com/ProwlrBot/roar-protocol
6
+
7
+ Quick start::
8
+
9
+ from roar_sdk import AgentIdentity, ROARMessage, MessageIntent, ROARClient, ROARServer
10
+
11
+ # Layer 1: Identity
12
+ identity = AgentIdentity(display_name="my-agent", capabilities=["code"])
13
+ print(identity.did) # did:roar:agent:my-agent-a1b2c3d4...
14
+
15
+ # Layer 4: Exchange — build and sign a message
16
+ msg = ROARMessage(
17
+ **{"from": identity, "to": other_identity},
18
+ intent=MessageIntent.DELEGATE,
19
+ payload={"task": "review"},
20
+ )
21
+ msg.sign("shared-secret")
22
+
23
+ # Layer 3: Connect — send over HTTP
24
+ client = ROARClient(identity, signing_secret="shared-secret")
25
+ response = await client.send_remote(
26
+ to_agent_id=other_identity.did,
27
+ intent=MessageIntent.DELEGATE,
28
+ content={"task": "review"},
29
+ )
30
+ """
31
+
32
+ __version__ = "0.2.0"
33
+ __author__ = "kdairatchi"
34
+ __spec_version__ = "0.2.0"
35
+
36
+ from .types import (
37
+ # Layer 1
38
+ AgentIdentity,
39
+ AgentCapability,
40
+ AgentCard,
41
+ # Layer 2
42
+ DiscoveryEntry,
43
+ AgentDirectory,
44
+ # Layer 3
45
+ TransportType,
46
+ ConnectionConfig,
47
+ # Layer 4
48
+ MessageIntent,
49
+ ROARMessage,
50
+ # Layer 5
51
+ StreamEventType,
52
+ StreamEvent,
53
+ # Adapters
54
+ MCPAdapter,
55
+ A2AAdapter,
56
+ )
57
+ from .client import ROARClient
58
+ from .server import ROARServer
59
+ from .streaming import EventBus, StreamFilter, Subscription
60
+ from .signing import generate_keypair, sign_ed25519, verify_ed25519
61
+ from .delegation import DelegationToken, issue_token, verify_token
62
+ from .adapters import ACPAdapter
63
+ from .adapters.detect import detect_protocol, ProtocolType
64
+ from .hub import ROARHub
65
+ from .did_document import DIDDocument, VerificationMethod, ServiceEndpoint
66
+ from .did_key import DIDKeyMethod, DIDKeyIdentity
67
+ from .did_web import DIDWebMethod, DIDWebIdentity
68
+ from .sqlite_directory import SQLiteAgentDirectory
69
+ from .discovery_cache import DiscoveryCache
70
+ from .dedup import IdempotencyGuard
71
+ from .autonomy import AutonomyLevel, CapabilityDelegation, RuntimeToken
72
+
73
+ __all__ = [
74
+ # Layer 1
75
+ "AgentIdentity",
76
+ "AgentCapability",
77
+ "AgentCard",
78
+ # Layer 2
79
+ "DiscoveryEntry",
80
+ "AgentDirectory",
81
+ # Layer 3
82
+ "TransportType",
83
+ "ConnectionConfig",
84
+ # Layer 4
85
+ "MessageIntent",
86
+ "ROARMessage",
87
+ # Layer 5
88
+ "StreamEventType",
89
+ "StreamEvent",
90
+ # Adapters
91
+ "MCPAdapter",
92
+ "A2AAdapter",
93
+ "ACPAdapter",
94
+ "detect_protocol",
95
+ "ProtocolType",
96
+ # Client / Server / Hub
97
+ "ROARClient",
98
+ "ROARServer",
99
+ "ROARHub",
100
+ # Streaming
101
+ "EventBus",
102
+ "StreamFilter",
103
+ "Subscription",
104
+ # Ed25519 signing
105
+ "generate_keypair",
106
+ "sign_ed25519",
107
+ "verify_ed25519",
108
+ # Delegation tokens (cryptographic, portable)
109
+ "DelegationToken",
110
+ "issue_token",
111
+ "verify_token",
112
+ # DID methods
113
+ "DIDDocument",
114
+ "VerificationMethod",
115
+ "ServiceEndpoint",
116
+ "DIDKeyMethod",
117
+ "DIDKeyIdentity",
118
+ "DIDWebMethod",
119
+ "DIDWebIdentity",
120
+ # Persistent discovery
121
+ "SQLiteAgentDirectory",
122
+ "DiscoveryCache",
123
+ # Autonomy model (runtime policy enforcement)
124
+ "AutonomyLevel",
125
+ "CapabilityDelegation",
126
+ "RuntimeToken",
127
+ # Deduplication
128
+ "IdempotencyGuard",
129
+ ]
@@ -0,0 +1,11 @@
1
+ # Python 3.10 compatibility shim for StrEnum (added in 3.11)
2
+ try:
3
+ from enum import StrEnum
4
+ except ImportError:
5
+ from enum import Enum
6
+
7
+ class StrEnum(str, Enum): # type: ignore[no-redef]
8
+ """Backport of Python 3.11 StrEnum for Python 3.10."""
9
+
10
+ def __str__(self) -> str:
11
+ return self.value
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ROAR Protocol — protocol adapters for backward compatibility.
3
+
4
+ Available adapters:
5
+ MCPAdapter — translate MCP tool calls ↔ ROARMessage (in roar_sdk.types)
6
+ A2AAdapter — translate A2A tasks ↔ ROARMessage (in roar_sdk.types)
7
+ ACPAdapter — translate ACP sessions ↔ ROARMessage (in roar_sdk.adapters.acp)
8
+ """
9
+
10
+ from .acp import ACPAdapter
11
+
12
+ __all__ = ["ACPAdapter"]
@@ -0,0 +1,192 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ROAR ACP Adapter — translate between ACP (Agent Communication Protocol) and ROAR.
3
+
4
+ ACP (IBM/BeeAI) is a session-based HTTP protocol for IDE-to-agent communication.
5
+ It defines sessions, messages, and responses but has no identity, signing, or
6
+ federation layer.
7
+
8
+ Mapping:
9
+ ACP session start → ROARMessage(intent=NOTIFY, payload={"event": "session.start"})
10
+ ACP message → ROARMessage(intent=ASK) if awaiting human input
11
+ → ROARMessage(intent=UPDATE) if reporting progress
12
+ ACP response → ROARMessage(intent=RESPOND)
13
+ ACP session end → ROARMessage(intent=NOTIFY, payload={"event": "session.end"})
14
+
15
+ ACP wire format (simplified):
16
+ POST /sessions → create session, returns session_id
17
+ POST /sessions/{id}/runs → send message, returns run_id + response stream
18
+ GET /sessions/{id} → get session state
19
+
20
+ Usage::
21
+
22
+ from roar_sdk.adapters import ACPAdapter
23
+ from roar_sdk import AgentIdentity, MessageIntent
24
+
25
+ ide = AgentIdentity(display_name="vscode", agent_type="ide")
26
+ agent = AgentIdentity(display_name="my-agent")
27
+
28
+ # Translate an incoming ACP message to a ROARMessage
29
+ acp_msg = {"content": "Explain this function", "role": "user"}
30
+ roar_msg = ACPAdapter.acp_to_roar(acp_msg, from_agent=ide, to_agent=agent)
31
+
32
+ # Translate a ROARMessage back to ACP response format
33
+ acp_response = ACPAdapter.roar_to_acp(roar_msg)
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from typing import Any, Dict, List, Optional
39
+
40
+ from ..types import AgentIdentity, MessageIntent, ROARMessage
41
+
42
+
43
+ class ACPAdapter:
44
+ """Translate between ACP sessions/messages and ROAR messages."""
45
+
46
+ # ── ACP → ROAR ──────────────────────────────────────────────────────────
47
+
48
+ @staticmethod
49
+ def acp_message_to_roar(
50
+ acp_message: Dict[str, Any],
51
+ from_agent: AgentIdentity,
52
+ to_agent: AgentIdentity,
53
+ session_id: str = "",
54
+ ) -> ROARMessage:
55
+ """Translate an ACP message dict to a ROARMessage.
56
+
57
+ ACP message format::
58
+
59
+ {
60
+ "role": "user" | "assistant",
61
+ "content": str | list,
62
+ "attachments": [...] # optional
63
+ }
64
+
65
+ The intent is derived from role:
66
+ - "user" → ASK (user is requesting something from the agent)
67
+ - "assistant" → RESPOND (agent is replying)
68
+ """
69
+ role = acp_message.get("role", "user")
70
+ content = acp_message.get("content", "")
71
+ attachments = acp_message.get("attachments", [])
72
+
73
+ intent = MessageIntent.ASK if role == "user" else MessageIntent.RESPOND
74
+
75
+ payload: Dict[str, Any] = {"content": content}
76
+ if attachments:
77
+ payload["attachments"] = attachments
78
+
79
+ context: Dict[str, Any] = {"protocol": "acp"}
80
+ if session_id:
81
+ context["session_id"] = session_id
82
+
83
+ return ROARMessage(
84
+ **{"from": from_agent, "to": to_agent},
85
+ intent=intent,
86
+ payload=payload,
87
+ context=context,
88
+ )
89
+
90
+ @staticmethod
91
+ def acp_session_event_to_roar(
92
+ event: str, # "start" | "end"
93
+ from_agent: AgentIdentity,
94
+ to_agent: AgentIdentity,
95
+ session_id: str = "",
96
+ metadata: Optional[Dict[str, Any]] = None,
97
+ ) -> ROARMessage:
98
+ """Translate an ACP session lifecycle event to a ROARMessage."""
99
+ return ROARMessage(
100
+ **{"from": from_agent, "to": to_agent},
101
+ intent=MessageIntent.NOTIFY,
102
+ payload={"event": f"session.{event}", **(metadata or {})},
103
+ context={"protocol": "acp", "session_id": session_id},
104
+ )
105
+
106
+ # ── ROAR → ACP ──────────────────────────────────────────────────────────
107
+
108
+ @staticmethod
109
+ def roar_to_acp_message(msg: ROARMessage) -> Dict[str, Any]:
110
+ """Translate a ROARMessage to an ACP message dict.
111
+
112
+ Maps intent to ACP role:
113
+ RESPOND → "assistant"
114
+ ASK → "user" (agent asking human for input)
115
+ UPDATE → "assistant" with status metadata
116
+ NOTIFY → "assistant" with event metadata
117
+ * → "assistant"
118
+ """
119
+ intent_to_role = {
120
+ MessageIntent.RESPOND: "assistant",
121
+ MessageIntent.ASK: "user",
122
+ MessageIntent.UPDATE: "assistant",
123
+ MessageIntent.NOTIFY: "assistant",
124
+ }
125
+ role = intent_to_role.get(msg.intent, "assistant")
126
+
127
+ # Prefer a "content" field, fall back to full payload as string
128
+ content = msg.payload.get("content") or msg.payload.get("result") or msg.payload
129
+
130
+ acp: Dict[str, Any] = {"role": role, "content": content}
131
+
132
+ # Carry attachments through if present
133
+ if "attachments" in msg.payload:
134
+ acp["attachments"] = msg.payload["attachments"]
135
+
136
+ return acp
137
+
138
+ @staticmethod
139
+ def roar_to_acp_run(msg: ROARMessage, run_id: str = "") -> Dict[str, Any]:
140
+ """Translate a ROARMessage to an ACP run response (richer format)."""
141
+ import time
142
+
143
+ return {
144
+ "run_id": run_id or msg.id,
145
+ "session_id": msg.context.get("session_id", ""),
146
+ "status": "completed" if msg.intent == MessageIntent.RESPOND else "in_progress",
147
+ "output": ACPAdapter.roar_to_acp_message(msg),
148
+ "metadata": {
149
+ "roar_intent": msg.intent,
150
+ "roar_message_id": msg.id,
151
+ "from_did": msg.from_identity.did,
152
+ "timestamp": msg.timestamp,
153
+ },
154
+ "created_at": time.time(),
155
+ }
156
+
157
+ # ── Agent Card ↔ ACP Agent ───────────────────────────────────────────────
158
+
159
+ @staticmethod
160
+ def well_known_agent_to_card(
161
+ well_known: Dict[str, Any],
162
+ endpoint: str = "",
163
+ ) -> Dict[str, Any]:
164
+ """Convert an A2A/ACP /.well-known/agent.json to a ROAR AgentCard dict.
165
+
166
+ Returns a dict suitable for constructing an AgentCard + AgentIdentity.
167
+ """
168
+ name = well_known.get("name", "unknown-agent")
169
+ description = well_known.get("description", "")
170
+ skills: List[str] = [
171
+ s.get("name", "") for s in well_known.get("skills", []) if s.get("name")
172
+ ]
173
+
174
+ return {
175
+ "identity": {
176
+ "did": "", # will be auto-generated
177
+ "display_name": name,
178
+ "agent_type": "agent",
179
+ "capabilities": skills,
180
+ "version": well_known.get("version", "1.0"),
181
+ "public_key": None,
182
+ },
183
+ "description": description,
184
+ "skills": skills,
185
+ "channels": well_known.get("supportedModes", []),
186
+ "endpoints": {"http": endpoint or well_known.get("url", "")},
187
+ "declared_capabilities": [],
188
+ "metadata": {
189
+ "protocol": "acp",
190
+ "original": well_known,
191
+ },
192
+ }
@@ -0,0 +1,96 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Protocol auto-detection — sniff incoming messages to identify their format.
3
+
4
+ Examines the structure of an incoming JSON message to determine whether
5
+ it's ROAR native, MCP (JSON-RPC 2.0), A2A, or ACP protocol format, then
6
+ routes to the appropriate adapter.
7
+
8
+ Usage::
9
+
10
+ from roar_sdk.adapters.detect import detect_protocol, ProtocolType
11
+
12
+ msg = json.loads(raw_body)
13
+ protocol = detect_protocol(msg)
14
+
15
+ if protocol == ProtocolType.ROAR:
16
+ roar_msg = ROARMessage.model_validate(msg)
17
+ elif protocol == ProtocolType.MCP:
18
+ roar_msg = MCPAdapter.mcp_to_roar(...)
19
+ elif protocol == ProtocolType.A2A:
20
+ roar_msg = A2AAdapter.a2a_task_to_roar(...)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from enum import Enum
26
+ from typing import Any, Dict
27
+
28
+
29
+ class ProtocolType(str, Enum):
30
+ """Detected protocol type."""
31
+
32
+ ROAR = "roar"
33
+ MCP = "mcp"
34
+ A2A = "a2a"
35
+ ACP = "acp"
36
+ UNKNOWN = "unknown"
37
+
38
+
39
+ _MCP_METHOD_PREFIXES = (
40
+ "tools/",
41
+ "resources/",
42
+ "prompts/",
43
+ "completion/",
44
+ "initialize",
45
+ "notifications/",
46
+ )
47
+
48
+ _A2A_METHOD_PREFIXES = ("tasks/", "agent/")
49
+
50
+
51
+ def detect_protocol(message: Dict[str, Any]) -> ProtocolType:
52
+ """Detect the protocol of an incoming message.
53
+
54
+ Detection heuristics (in priority order):
55
+ 1. ROAR: Has "roar" version field and "intent" field
56
+ 2. ACP: Has "role" field and "content" field (ACP message body)
57
+ 3. MCP: JSON-RPC 2.0 with MCP method prefix
58
+ 4. A2A: JSON-RPC 2.0 with tasks/ or agent/ method prefix, or task envelope
59
+
60
+ Args:
61
+ message: The raw JSON message dict.
62
+
63
+ Returns:
64
+ The detected ProtocolType.
65
+ """
66
+ # ROAR native
67
+ if "roar" in message and "intent" in message:
68
+ return ProtocolType.ROAR
69
+
70
+ # ACP message (session-based, role/content structure)
71
+ if "role" in message and "content" in message:
72
+ return ProtocolType.ACP
73
+
74
+ # JSON-RPC based protocols
75
+ if message.get("jsonrpc") == "2.0":
76
+ method = message.get("method", "")
77
+
78
+ if any(method.startswith(p) for p in _A2A_METHOD_PREFIXES):
79
+ return ProtocolType.A2A
80
+
81
+ if any(method.startswith(p) for p in _MCP_METHOD_PREFIXES):
82
+ return ProtocolType.MCP
83
+
84
+ # Infer from result structure
85
+ result = message.get("result", {})
86
+ if isinstance(result, dict):
87
+ if "status" in result and "id" in result:
88
+ return ProtocolType.A2A
89
+ if "tools" in result or "resources" in result:
90
+ return ProtocolType.MCP
91
+
92
+ # A2A task envelope (no jsonrpc wrapper)
93
+ if "status" in message and "id" in message and "artifacts" in message:
94
+ return ProtocolType.A2A
95
+
96
+ return ProtocolType.UNKNOWN