uclone-x 0.1.0__py3-none-any.whl

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 (134) hide show
  1. uclone_x/__init__.py +5 -0
  2. uclone_x/a2a/__init__.py +31 -0
  3. uclone_x/a2a/discovery.py +71 -0
  4. uclone_x/a2a/http_transport.py +204 -0
  5. uclone_x/a2a/in_memory.py +100 -0
  6. uclone_x/a2a/models.py +76 -0
  7. uclone_x/a2a/protocols.py +62 -0
  8. uclone_x/a2a/server.py +643 -0
  9. uclone_x/a2a/wire.py +113 -0
  10. uclone_x/adapters/__init__.py +14 -0
  11. uclone_x/adapters/uclone2/__init__.py +33 -0
  12. uclone_x/adapters/uclone2/adk_content.py +437 -0
  13. uclone_x/agent/__init__.py +79 -0
  14. uclone_x/agent/base.py +2534 -0
  15. uclone_x/agent/bootstrap.py +27 -0
  16. uclone_x/agent/hooks/__init__.py +23 -0
  17. uclone_x/agent/hooks/models.py +126 -0
  18. uclone_x/agent/hooks/permission.py +67 -0
  19. uclone_x/agent/hooks/protocols.py +71 -0
  20. uclone_x/agent/hooks/runner.py +149 -0
  21. uclone_x/agent/hooks/script_runner.py +192 -0
  22. uclone_x/agent/models.py +351 -0
  23. uclone_x/agent/planner.py +174 -0
  24. uclone_x/agent/protocols.py +190 -0
  25. uclone_x/agent/session.py +1360 -0
  26. uclone_x/cli/__init__.py +1 -0
  27. uclone_x/cli/commands/__init__.py +1 -0
  28. uclone_x/cli/commands/a2a.py +121 -0
  29. uclone_x/cli/commands/dev.py +900 -0
  30. uclone_x/cli/commands/eval.py +392 -0
  31. uclone_x/cli/commands/llm.py +116 -0
  32. uclone_x/cli/commands/ontology.py +585 -0
  33. uclone_x/cli/commands/run.py +403 -0
  34. uclone_x/cli/commands/session_dev.py +460 -0
  35. uclone_x/cli/commands/skill.py +537 -0
  36. uclone_x/cli/main.py +721 -0
  37. uclone_x/cli/quality_gate.py +623 -0
  38. uclone_x/code_intel/__init__.py +59 -0
  39. uclone_x/code_intel/ast_parser.py +2097 -0
  40. uclone_x/code_intel/models.py +205 -0
  41. uclone_x/code_intel/protocols.py +106 -0
  42. uclone_x/code_intel/scip.py +482 -0
  43. uclone_x/code_intel/symbol_graph.py +405 -0
  44. uclone_x/core/__init__.py +78 -0
  45. uclone_x/core/capability.py +35 -0
  46. uclone_x/core/host.py +111 -0
  47. uclone_x/core/immutable.py +109 -0
  48. uclone_x/core/log_inspector.py +190 -0
  49. uclone_x/core/log_offset.py +147 -0
  50. uclone_x/core/logging_setup.py +105 -0
  51. uclone_x/core/provenance.py +245 -0
  52. uclone_x/core/secrets.py +227 -0
  53. uclone_x/core/session_diagnostics.py +441 -0
  54. uclone_x/core/session_store.py +105 -0
  55. uclone_x/core/workspace.py +40 -0
  56. uclone_x/engine/__init__.py +57 -0
  57. uclone_x/engine/event_bus.py +1508 -0
  58. uclone_x/engine/mattermost_bridge.py +138 -0
  59. uclone_x/engine/protocols.py +362 -0
  60. uclone_x/errors.py +768 -0
  61. uclone_x/evaluation/__init__.py +39 -0
  62. uclone_x/evaluation/loader.py +123 -0
  63. uclone_x/evaluation/protocols.py +98 -0
  64. uclone_x/llm/__init__.py +69 -0
  65. uclone_x/llm/budget.py +318 -0
  66. uclone_x/llm/compactor.py +583 -0
  67. uclone_x/llm/connectors/__init__.py +27 -0
  68. uclone_x/llm/connectors/anthropic.py +415 -0
  69. uclone_x/llm/connectors/base.py +109 -0
  70. uclone_x/llm/connectors/factory.py +133 -0
  71. uclone_x/llm/connectors/gemini.py +432 -0
  72. uclone_x/llm/connectors/mock.py +131 -0
  73. uclone_x/llm/connectors/ollama.py +437 -0
  74. uclone_x/llm/connectors/openai.py +387 -0
  75. uclone_x/llm/cost.py +80 -0
  76. uclone_x/llm/models.py +291 -0
  77. uclone_x/llm/protocols.py +169 -0
  78. uclone_x/llm/router.py +46 -0
  79. uclone_x/log/__init__.py +5 -0
  80. uclone_x/log/file_allocator.py +412 -0
  81. uclone_x/ontology/__init__.py +79 -0
  82. uclone_x/ontology/engine.py +1960 -0
  83. uclone_x/ontology/justification.py +453 -0
  84. uclone_x/ontology/models.py +383 -0
  85. uclone_x/ontology/protocols.py +94 -0
  86. uclone_x/ontology/reasoner.py +313 -0
  87. uclone_x/ontology/rules.py +1269 -0
  88. uclone_x/sandbox/__init__.py +45 -0
  89. uclone_x/sandbox/models.py +254 -0
  90. uclone_x/sandbox/path_validator.py +42 -0
  91. uclone_x/sandbox/protocols.py +56 -0
  92. uclone_x/sandbox/workspace_runner.py +159 -0
  93. uclone_x/skills/__init__.py +51 -0
  94. uclone_x/skills/auditor.py +777 -0
  95. uclone_x/skills/models.py +167 -0
  96. uclone_x/skills/protocols.py +131 -0
  97. uclone_x/skills/synthesizer.py +459 -0
  98. uclone_x/telemetry/__init__.py +93 -0
  99. uclone_x/telemetry/exporter.py +454 -0
  100. uclone_x/telemetry/langfuse.py +299 -0
  101. uclone_x/telemetry/metrics.py +169 -0
  102. uclone_x/telemetry/models.py +65 -0
  103. uclone_x/telemetry/otlp.py +415 -0
  104. uclone_x/telemetry/protocols.py +125 -0
  105. uclone_x/telemetry/tracer.py +646 -0
  106. uclone_x/tools/__init__.py +112 -0
  107. uclone_x/tools/base.py +202 -0
  108. uclone_x/tools/builtin/__init__.py +81 -0
  109. uclone_x/tools/builtin/comfy_client.py +355 -0
  110. uclone_x/tools/builtin/comfy_image_tool.py +272 -0
  111. uclone_x/tools/builtin/filesystem.py +463 -0
  112. uclone_x/tools/builtin/mcp_loader.py +404 -0
  113. uclone_x/tools/builtin/plan.py +49 -0
  114. uclone_x/tools/builtin/shell.py +286 -0
  115. uclone_x/tools/builtin/skill_loader.py +49 -0
  116. uclone_x/tools/builtin/subagent.py +188 -0
  117. uclone_x/tools/builtin/web.py +732 -0
  118. uclone_x/tools/client.py +524 -0
  119. uclone_x/tools/models.py +206 -0
  120. uclone_x/tools/protocols.py +88 -0
  121. uclone_x/tools/registry.py +176 -0
  122. uclone_x/tools/semantic_filter.py +37 -0
  123. uclone_x/ui/__init__.py +6 -0
  124. uclone_x/ui/app.py +2519 -0
  125. uclone_x/ui/server.py +226 -0
  126. uclone_x/ui_static/assets/index-Cyoj7xep.css +1 -0
  127. uclone_x/ui_static/assets/index-DcnoPPnb.js +410 -0
  128. uclone_x/ui_static/index.html +13 -0
  129. uclone_x-0.1.0.dist-info/METADATA +206 -0
  130. uclone_x-0.1.0.dist-info/RECORD +134 -0
  131. uclone_x-0.1.0.dist-info/WHEEL +4 -0
  132. uclone_x-0.1.0.dist-info/entry_points.txt +2 -0
  133. uclone_x-0.1.0.dist-info/licenses/LICENSE +176 -0
  134. uclone_x-0.1.0.dist-info/licenses/NOTICE +17 -0
uclone_x/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """UClone-X: Next-Generation Event-Driven AI Agent Core & Collaboration Framework."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Kenny Lim <kennylim@uclone.net>"
5
+ __all__ = ["__version__"]
@@ -0,0 +1,31 @@
1
+ """Google A2A v1.0.1 subsystem: Discovery, local fastpath, and remote REST/SSE wire protocol."""
2
+
3
+ from uclone_x.a2a.discovery import A2ADiscoveryService
4
+ from uclone_x.a2a.http_transport import A2AHttpTransport
5
+ from uclone_x.a2a.in_memory import A2AInMemoryTransport
6
+ from uclone_x.a2a.models import (
7
+ AgentCard,
8
+ TaskMessage,
9
+ TaskResult,
10
+ TaskStatus,
11
+ WireProtocolType,
12
+ )
13
+ from uclone_x.a2a.protocols import (
14
+ A2ADiscoveryProtocol,
15
+ A2ATransportProtocol,
16
+ )
17
+ from uclone_x.a2a.server import A2AServer
18
+
19
+ __all__ = [
20
+ "A2ADiscoveryProtocol",
21
+ "A2ADiscoveryService",
22
+ "A2AHttpTransport",
23
+ "A2AInMemoryTransport",
24
+ "A2AServer",
25
+ "A2ATransportProtocol",
26
+ "AgentCard",
27
+ "TaskMessage",
28
+ "TaskResult",
29
+ "TaskStatus",
30
+ "WireProtocolType",
31
+ ]
@@ -0,0 +1,71 @@
1
+ """A2A discovery service for publishing and discovering Agent Cards per RFC 8615."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from uclone_x.a2a.models import AgentCard
8
+ from uclone_x.errors import (
9
+ InvalidAgentResponseError,
10
+ TaskNotFoundError,
11
+ )
12
+
13
+
14
+ class A2ADiscoveryService:
15
+ """Service for discovering and publishing Agent Cards per RFC 8615."""
16
+
17
+ def __init__(
18
+ self,
19
+ local_agent_card: AgentCard | None = None,
20
+ http_client: httpx.AsyncClient | None = None,
21
+ ) -> None:
22
+ self._local_card = local_agent_card or AgentCard(
23
+ name="uclone-x-agent",
24
+ description="UClone-X Default Agent",
25
+ version="1.0.1",
26
+ )
27
+ self._http_client = http_client
28
+
29
+ def get_local_agent_card(self) -> AgentCard:
30
+ """Export local agent capabilities."""
31
+ return self._local_card
32
+
33
+ def set_local_agent_card(self, card: AgentCard) -> None:
34
+ """Update the local agent card."""
35
+ self._local_card = card
36
+
37
+ async def fetch_remote_agent_card(self, endpoint_url: str) -> AgentCard:
38
+ """Fetch /.well-known/agent-card.json from remote peer per RFC 8615."""
39
+ url = endpoint_url.strip()
40
+ if not url.endswith("/.well-known/agent-card.json"):
41
+ url = f"{url.rstrip('/')}/.well-known/agent-card.json"
42
+
43
+ client = self._http_client or httpx.AsyncClient()
44
+ should_close = self._http_client is None
45
+ try:
46
+ resp = await client.get(
47
+ url,
48
+ headers={"A2A-Version": "1.0", "Accept": "application/json"},
49
+ timeout=10.0,
50
+ )
51
+ if resp.status_code == 404:
52
+ raise TaskNotFoundError(f"Agent card not found at {url}")
53
+ if resp.status_code != 200:
54
+ raise InvalidAgentResponseError(
55
+ f"Failed to fetch agent card from {url}: HTTP {resp.status_code}"
56
+ )
57
+ try:
58
+ return AgentCard.model_validate_json(resp.text)
59
+ except Exception as exc:
60
+ raise InvalidAgentResponseError(
61
+ f"Invalid AgentCard payload from {url}: {exc}"
62
+ ) from exc
63
+ except (TaskNotFoundError, InvalidAgentResponseError):
64
+ raise
65
+ except Exception as exc:
66
+ raise InvalidAgentResponseError(
67
+ f"Network error fetching agent card from {url}: {exc}"
68
+ ) from exc
69
+ finally:
70
+ if should_close:
71
+ await client.aclose()
@@ -0,0 +1,204 @@
1
+ """A2A remote HTTP REST/SSE wire transport conforming to A2ATransportProtocol and A2ADiscoveryProtocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any, cast
7
+
8
+ import httpx
9
+
10
+ from uclone_x.a2a.discovery import A2ADiscoveryService
11
+ from uclone_x.a2a.models import (
12
+ AgentCard,
13
+ TaskMessage,
14
+ TaskResult,
15
+ WireProtocolType,
16
+ )
17
+ from uclone_x.a2a.wire import task_message_to_wire, task_result_from_wire_json
18
+ from uclone_x.errors import (
19
+ A2AError,
20
+ InvalidAgentResponseError,
21
+ MissingProvenanceError,
22
+ TaskNotFoundError,
23
+ UnsupportedOperationError,
24
+ VersionNotSupportedError,
25
+ )
26
+
27
+
28
+ class A2AHttpTransport:
29
+ """Remote REST/SSE transport for distributed A2A communication."""
30
+
31
+ def __init__(
32
+ self,
33
+ http_client: httpx.AsyncClient | None = None,
34
+ default_agent_card: AgentCard | None = None,
35
+ discovery_service: A2ADiscoveryService | None = None,
36
+ ) -> None:
37
+ self._http_client = http_client
38
+ self._discovery = discovery_service or A2ADiscoveryService(
39
+ local_agent_card=default_agent_card,
40
+ http_client=http_client,
41
+ )
42
+
43
+ @property
44
+ def transport_type(self) -> WireProtocolType:
45
+ """Transport mechanism identifier."""
46
+ return WireProtocolType.REST_SSE
47
+
48
+ def get_local_agent_card(self) -> AgentCard:
49
+ """Export local agent capabilities."""
50
+ return self._discovery.get_local_agent_card()
51
+
52
+ def set_local_agent_card(self, card: AgentCard) -> None:
53
+ """Update local agent capabilities."""
54
+ self._discovery.set_local_agent_card(card)
55
+
56
+ async def fetch_remote_agent_card(self, endpoint_url: str) -> AgentCard:
57
+ """Fetch remote /.well-known/agent-card.json per RFC 8615."""
58
+ return await self._discovery.fetch_remote_agent_card(endpoint_url)
59
+
60
+ async def send_task(self, target_endpoint: str, message: TaskMessage) -> TaskResult:
61
+ """Dispatch task to remote agent over HTTP REST."""
62
+ url = target_endpoint.strip()
63
+ if not (
64
+ url.endswith("/message:send") or url.endswith("/tasks/send") or url.endswith("/tasks")
65
+ ):
66
+ url = f"{url.rstrip('/')}/message:send"
67
+
68
+ client = self._http_client or httpx.AsyncClient()
69
+ should_close = self._http_client is None
70
+ try:
71
+ resp = await client.post(
72
+ url,
73
+ headers={
74
+ "A2A-Version": "1.0",
75
+ "Content-Type": "application/json",
76
+ "Accept": "application/json",
77
+ },
78
+ json=task_message_to_wire(message),
79
+ timeout=30.0,
80
+ )
81
+ if resp.status_code == 404:
82
+ err_msg = f"Endpoint or task not found at {url}"
83
+ try:
84
+ raw_err = resp.json()
85
+ if isinstance(raw_err, dict):
86
+ raw_dict: dict[str, Any] = cast(dict[str, Any], raw_err)
87
+ detail = raw_dict.get("detail")
88
+ if isinstance(detail, dict) and "message" in detail:
89
+ detail_dict: dict[str, Any] = cast(dict[str, Any], detail)
90
+ err_msg = str(detail_dict["message"])
91
+ elif isinstance(detail, str):
92
+ err_msg = detail
93
+ elif "message" in raw_dict:
94
+ err_msg = str(raw_dict["message"])
95
+ except Exception:
96
+ pass
97
+ raise TaskNotFoundError(err_msg)
98
+ if resp.status_code == 400:
99
+ try:
100
+ err_obj = resp.json()
101
+ if isinstance(err_obj, dict):
102
+ err_dict: dict[str, Any] = cast(dict[str, Any], err_obj)
103
+ detail_obj = err_dict.get("detail")
104
+ detail_dict: dict[str, Any] = (
105
+ cast(dict[str, Any], detail_obj) if isinstance(detail_obj, dict) else {}
106
+ )
107
+ err_name = err_dict.get("error") or detail_dict.get("error")
108
+ err_type = str(err_name or "")
109
+ if err_type == "VersionNotSupportedError":
110
+ raise VersionNotSupportedError(
111
+ f"A2A version not supported by {url}: {resp.text}"
112
+ )
113
+ except (VersionNotSupportedError, TaskNotFoundError):
114
+ raise
115
+ except Exception:
116
+ pass
117
+ raise UnsupportedOperationError(f"Bad request sent to {url}: HTTP 400 {resp.text}")
118
+ if resp.status_code != 200:
119
+ raise InvalidAgentResponseError(
120
+ f"Remote agent returned error {resp.status_code}: {resp.text}"
121
+ )
122
+
123
+ result = task_result_from_wire_json(resp.text)
124
+ if result.provenance is None:
125
+ raise MissingProvenanceError(
126
+ f"Remote agent response from {url} is missing provenance (P6)"
127
+ )
128
+ return result
129
+ except (A2AError, MissingProvenanceError):
130
+ raise
131
+ except Exception as exc:
132
+ raise InvalidAgentResponseError(
133
+ f"Network or protocol error communicating with {url}: {exc}"
134
+ ) from exc
135
+ finally:
136
+ if should_close:
137
+ await client.aclose()
138
+
139
+ def stream_task(
140
+ self,
141
+ target_endpoint: str,
142
+ message: TaskMessage,
143
+ ) -> AsyncIterator[str]:
144
+ """Stream task results via remote SSE stream."""
145
+ url = target_endpoint.strip()
146
+ if not (url.endswith("/message:stream") or url.endswith("/tasks/stream")):
147
+ url = f"{url.rstrip('/')}/message:stream"
148
+
149
+ async def _stream_generator() -> AsyncIterator[str]:
150
+ client = self._http_client or httpx.AsyncClient()
151
+ should_close = self._http_client is None
152
+ try:
153
+ async with client.stream(
154
+ "POST",
155
+ url,
156
+ headers={
157
+ "A2A-Version": "1.0",
158
+ "Content-Type": "application/json",
159
+ "Accept": "text/event-stream",
160
+ },
161
+ json=task_message_to_wire(message),
162
+ timeout=30.0,
163
+ ) as resp:
164
+ if resp.status_code == 404:
165
+ err_msg = f"Endpoint or task not found at {url}"
166
+ try:
167
+ body = await resp.aread()
168
+ import json
169
+
170
+ raw_err: object = json.loads(body)
171
+ if isinstance(raw_err, dict):
172
+ err_dict: dict[str, object] = cast(dict[str, object], raw_err)
173
+ detail_val = err_dict.get("detail")
174
+ if isinstance(detail_val, dict):
175
+ detail_d: dict[str, object] = cast(
176
+ dict[str, object], detail_val
177
+ )
178
+ if "message" in detail_d:
179
+ err_msg = str(detail_d["message"])
180
+ elif isinstance(detail_val, str):
181
+ err_msg = detail_val
182
+ elif "message" in err_dict:
183
+ err_msg = str(err_dict["message"])
184
+ except Exception:
185
+ pass
186
+ raise TaskNotFoundError(err_msg)
187
+ if resp.status_code != 200:
188
+ raise InvalidAgentResponseError(
189
+ f"Remote agent returned streaming error {resp.status_code}"
190
+ )
191
+ async for line in resp.aiter_lines():
192
+ if line:
193
+ yield line
194
+ except (A2AError, MissingProvenanceError):
195
+ raise
196
+ except Exception as exc:
197
+ raise InvalidAgentResponseError(
198
+ f"Network error during stream from {url}: {exc}"
199
+ ) from exc
200
+ finally:
201
+ if should_close:
202
+ await client.aclose()
203
+
204
+ return _stream_generator()
@@ -0,0 +1,100 @@
1
+ """In-memory zero-copy fastpath transport for co-located A2A agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator, Awaitable, Callable
6
+
7
+ from uclone_x.a2a.models import TaskMessage, TaskResult, WireProtocolType
8
+ from uclone_x.a2a.wire import task_result_to_wire_json
9
+ from uclone_x.errors import (
10
+ MissingProvenanceError,
11
+ TaskNotFoundError,
12
+ )
13
+
14
+ TaskHandler = Callable[[TaskMessage], Awaitable[TaskResult]]
15
+ StreamHandler = Callable[[TaskMessage], AsyncIterator[str]]
16
+
17
+
18
+ class A2AInMemoryTransport:
19
+ """Zero-copy in-memory transport for co-located agents within the same process."""
20
+
21
+ def __init__(self) -> None:
22
+ self._handlers: dict[str, TaskHandler] = {}
23
+ self._stream_handlers: dict[str, StreamHandler] = {}
24
+
25
+ @property
26
+ def transport_type(self) -> WireProtocolType:
27
+ """Transport mechanism identifier."""
28
+ return WireProtocolType.LOCAL_IN_MEMORY
29
+
30
+ def register_handler(
31
+ self,
32
+ endpoint_or_agent_id: str,
33
+ handler: TaskHandler,
34
+ ) -> None:
35
+ """Register a direct async task handler for an endpoint or agent id."""
36
+ self._handlers[endpoint_or_agent_id] = handler
37
+
38
+ def register_stream_handler(
39
+ self,
40
+ endpoint_or_agent_id: str,
41
+ handler: StreamHandler,
42
+ ) -> None:
43
+ """Register a streaming task handler for an endpoint or agent id."""
44
+ self._stream_handlers[endpoint_or_agent_id] = handler
45
+
46
+ def unregister_handler(self, endpoint_or_agent_id: str) -> None:
47
+ """Unregister handlers associated with an endpoint or agent id."""
48
+ self._handlers.pop(endpoint_or_agent_id, None)
49
+ self._stream_handlers.pop(endpoint_or_agent_id, None)
50
+
51
+ async def send_task(self, target_endpoint: str, message: TaskMessage) -> TaskResult:
52
+ """Dispatch task directly to local in-process agent without network or serialization."""
53
+ handler = self._handlers.get(target_endpoint) or self._handlers.get(message.target_agent_id)
54
+ if handler is None:
55
+ raise TaskNotFoundError(
56
+ f"No in-memory handler registered for endpoint '{target_endpoint}' "
57
+ f"or agent '{message.target_agent_id}'"
58
+ )
59
+ result = await handler(message)
60
+ if result.provenance is None:
61
+ raise MissingProvenanceError(
62
+ "TaskResult returned by in-memory handler must contain in-band provenance (P6)"
63
+ )
64
+ return result
65
+
66
+ def stream_task(
67
+ self,
68
+ target_endpoint: str,
69
+ message: TaskMessage,
70
+ ) -> AsyncIterator[str]:
71
+ """Stream task results via in-memory generator."""
72
+ stream_handler = self._stream_handlers.get(target_endpoint) or self._stream_handlers.get(
73
+ message.target_agent_id
74
+ )
75
+ if stream_handler is not None:
76
+ return stream_handler(message)
77
+
78
+ handler = self._handlers.get(target_endpoint) or self._handlers.get(message.target_agent_id)
79
+ if handler is not None:
80
+
81
+ async def _fallback_stream() -> AsyncIterator[str]:
82
+ res = await handler(message)
83
+ if res.provenance is None:
84
+ raise MissingProvenanceError(
85
+ "TaskResult returned by in-memory handler must contain in-band provenance (P6)"
86
+ )
87
+ yield task_result_to_wire_json(res)
88
+
89
+ return _fallback_stream()
90
+
91
+ async def _not_found_stream() -> AsyncIterator[str]:
92
+ raise TaskNotFoundError(
93
+ f"No in-memory handler registered for endpoint '{target_endpoint}' "
94
+ f"or agent '{message.target_agent_id}'"
95
+ )
96
+ # Make this an async generator function for type checkers
97
+ if False: # pragma: no cover
98
+ yield ""
99
+
100
+ return _not_found_stream()
uclone_x/a2a/models.py ADDED
@@ -0,0 +1,76 @@
1
+ """Data models for Google Agent-to-Agent (A2A) protocol v1.0.1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from uclone_x.core.immutable import ImmutableJsonMapping, ImmutableStrMapping
10
+ from uclone_x.core.provenance import Provenance
11
+
12
+
13
+ class WireProtocolType(StrEnum):
14
+ """Transport protocol type."""
15
+
16
+ LOCAL_IN_MEMORY = "local_in_memory"
17
+ REST_SSE = "rest_sse"
18
+
19
+
20
+ class TaskStatus(StrEnum):
21
+ """Lifecycle and execution status of an A2A task per A2A v1.0.1 specification."""
22
+
23
+ SUBMITTED = "submitted"
24
+ WORKING = "working"
25
+ INPUT_REQUIRED = "input_required"
26
+ AUTH_REQUIRED = "auth_required"
27
+ COMPLETED = "completed"
28
+ FAILED = "failed"
29
+ CANCELED = "canceled"
30
+ REJECTED = "rejected"
31
+
32
+
33
+ class AgentCard(BaseModel):
34
+ """Agent discovery metadata exposed at /.well-known/agent-card.json per RFC 8615."""
35
+
36
+ model_config = ConfigDict(frozen=True, extra="forbid", strict=True)
37
+
38
+ name: str
39
+ description: str
40
+ version: str = "1.0.1"
41
+ endpoints: ImmutableStrMapping = Field(default_factory=dict)
42
+ skills: tuple[str, ...] = Field(
43
+ default_factory=tuple,
44
+ description="Declared skills and capabilities of the agent per A2A v1.0.1.",
45
+ )
46
+ input_schema: ImmutableJsonMapping = Field(default_factory=dict)
47
+ output_schema: ImmutableJsonMapping = Field(default_factory=dict)
48
+
49
+
50
+ class TaskMessage(BaseModel):
51
+ """A2A task dispatch envelope."""
52
+
53
+ model_config = ConfigDict(frozen=True, extra="forbid", strict=True)
54
+
55
+ task_id: str
56
+ session_id: str
57
+ input_data: ImmutableJsonMapping = Field(default_factory=dict)
58
+ sender_agent_id: str
59
+ target_agent_id: str
60
+ metadata: ImmutableStrMapping = Field(default_factory=dict)
61
+
62
+
63
+ class TaskResult(BaseModel):
64
+ """A2A task response envelope."""
65
+
66
+ model_config = ConfigDict(frozen=True, extra="forbid", strict=True)
67
+
68
+ task_id: str
69
+ status: TaskStatus = TaskStatus.COMPLETED
70
+ output_data: ImmutableJsonMapping = Field(default_factory=dict)
71
+ error: str | None = None
72
+ provenance: Provenance | None = Field(
73
+ description="In-band attribution required by Principle 6. Explicit with no "
74
+ "default: `None` is representable so a non-conformant value can be rejected by "
75
+ "`require_provenance`, but it is never inherited silently.",
76
+ )
@@ -0,0 +1,62 @@
1
+ """Protocols for A2A dual-transport and discovery.
2
+
3
+ `@runtime_checkable` is applied only where a runtime `isinstance` check is actually
4
+ performed. On a protocol with a `@property`, `issubclass()` raises `TypeError` and
5
+ `isinstance()` calls the object's getters as a side effect of the type test, and neither
6
+ form checks a signature — which is what actually drifted in issue 2026-09-02-035.
7
+ Conformance is enforced statically instead, by the bindings in
8
+ `tests/unit/test_protocol_conformance.py`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import AsyncIterator
14
+ from typing import Protocol, runtime_checkable
15
+
16
+ from uclone_x.a2a.models import (
17
+ AgentCard,
18
+ TaskMessage,
19
+ TaskResult,
20
+ WireProtocolType,
21
+ )
22
+
23
+
24
+ @runtime_checkable
25
+ class A2ADiscoveryProtocol(Protocol):
26
+ """Protocol for discovering and publishing Agent Cards."""
27
+
28
+ def get_local_agent_card(self) -> AgentCard:
29
+ """Export local agent capabilities."""
30
+ ...
31
+
32
+ async def fetch_remote_agent_card(self, endpoint_url: str) -> AgentCard:
33
+ """Fetch /.well-known/agent-card.json from remote peer."""
34
+ ...
35
+
36
+
37
+ class A2ATransportProtocol(Protocol):
38
+ """Protocol for sending tasks across local zero-copy or remote REST/SSE transport."""
39
+
40
+ @property
41
+ def transport_type(self) -> WireProtocolType:
42
+ """Transport mechanism (local vs remote)."""
43
+ ...
44
+
45
+ async def send_task(self, target_endpoint: str, message: TaskMessage) -> TaskResult:
46
+ """Dispatch task to local or remote agent."""
47
+ ...
48
+
49
+ def stream_task(
50
+ self,
51
+ target_endpoint: str,
52
+ message: TaskMessage,
53
+ ) -> AsyncIterator[str]:
54
+ """Stream task results via SSE or in-memory generator.
55
+
56
+ Declared `def`, not `async def`: an async generator function *is* a plain
57
+ function returning an `AsyncIterator`. As `async def ... -> AsyncIterator` the
58
+ caller had to await the call to obtain the iterator and then iterate it, which
59
+ no async generator can satisfy — caught by the conformance stub in
60
+ `tests/unit/test_protocol_conformance.py` (issue 2026-09-02-035).
61
+ """
62
+ ...