molecule-ai-workspace-runtime 0.1.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.
- molecule_ai_workspace_runtime-0.1.0/PKG-INFO +84 -0
- molecule_ai_workspace_runtime-0.1.0/README.md +65 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/PKG-INFO +84 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/SOURCES.txt +60 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/dependency_links.txt +1 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/entry_points.txt +2 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/requires.txt +11 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/top_level.txt +1 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/__init__.py +6 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/a2a_cli.py +245 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/a2a_client.py +111 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/a2a_executor.py +419 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/a2a_mcp_server.py +293 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/a2a_tools.py +269 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/adapters/__init__.py +86 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/adapters/base.py +309 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/adapters/shared_runtime.py +190 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/agent.py +133 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/__init__.py +0 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/a2a_tools.py +85 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/approval.py +320 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/audit.py +274 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/awareness_client.py +122 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/compliance.py +359 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/delegation.py +366 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/governance.py +403 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/hitl.py +531 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/medo.py +106 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/memory.py +468 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/sandbox.py +281 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/security_scan.py +344 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/telemetry.py +418 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/builtin_tools/temporal_workflow.py +515 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/claude_sdk_executor.py +449 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/cli_executor.py +456 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/config.py +349 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/consolidation.py +131 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/coordinator.py +136 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/events.py +96 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/executor_helpers.py +389 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/heartbeat.py +291 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/initial_prompt.py +51 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/main.py +556 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/molecule_ai_status.py +72 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/platform_auth.py +105 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/plugins.py +154 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/plugins_registry/__init__.py +135 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/plugins_registry/builtins.py +327 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/plugins_registry/protocol.py +104 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/plugins_registry/raw_drop.py +71 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/policies/__init__.py +11 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/policies/namespaces.py +18 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/policies/routing.py +98 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/preflight.py +143 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/prompt.py +132 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/skill_loader/__init__.py +0 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/skill_loader/loader.py +191 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/skill_loader/watcher.py +227 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/transcript_auth.py +30 -0
- molecule_ai_workspace_runtime-0.1.0/molecule_runtime/watcher.py +120 -0
- molecule_ai_workspace_runtime-0.1.0/pyproject.toml +35 -0
- molecule_ai_workspace_runtime-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: molecule-ai-workspace-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Molecule AI workspace runtime — shared infrastructure for all agent adapters
|
|
5
|
+
License: BSL-1.1
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: a2a-sdk[http-server]>=0.3.25
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
11
|
+
Requires-Dist: starlette>=0.38.0
|
|
12
|
+
Requires-Dist: websockets>=12.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.24.0
|
|
16
|
+
Requires-Dist: opentelemetry-sdk>=1.24.0
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24.0
|
|
18
|
+
Requires-Dist: temporalio>=1.7.0
|
|
19
|
+
|
|
20
|
+
# molecule-ai-workspace-runtime
|
|
21
|
+
|
|
22
|
+
Shared Python runtime infrastructure for all Molecule AI agent adapters.
|
|
23
|
+
|
|
24
|
+
This package provides the core machinery that every Molecule AI workspace container needs:
|
|
25
|
+
|
|
26
|
+
- **A2A server** — Registers with the platform, heartbeats, serves A2A JSON-RPC
|
|
27
|
+
- **Adapter interface** — `BaseAdapter` / `AdapterConfig` / `SetupResult`
|
|
28
|
+
- **Built-in tools** — delegation, memory, approvals, sandbox, telemetry
|
|
29
|
+
- **Skill loader** — loads and hot-reloads skill modules from `/configs/skills/`
|
|
30
|
+
- **Plugin system** — per-workspace + shared plugin discovery and install
|
|
31
|
+
- **Config / preflight** — YAML config loading with validation
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install molecule-ai-workspace-runtime
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Adapter Discovery
|
|
40
|
+
|
|
41
|
+
The runtime discovers adapters in two ways:
|
|
42
|
+
|
|
43
|
+
1. **`ADAPTER_MODULE` env var** (standalone adapter repos):
|
|
44
|
+
```bash
|
|
45
|
+
ADAPTER_MODULE=my_adapter molecule-runtime
|
|
46
|
+
```
|
|
47
|
+
The module must export an `Adapter` class extending `BaseAdapter`.
|
|
48
|
+
|
|
49
|
+
2. **Built-in subdirectory scan** (monorepo local dev):
|
|
50
|
+
Scans `molecule_runtime/adapters/` subdirectories for `Adapter` classes.
|
|
51
|
+
|
|
52
|
+
## Writing an Adapter
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig
|
|
56
|
+
from a2a.server.agent_execution import AgentExecutor
|
|
57
|
+
|
|
58
|
+
class Adapter(BaseAdapter):
|
|
59
|
+
@staticmethod
|
|
60
|
+
def name() -> str:
|
|
61
|
+
return "my-runtime"
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def display_name() -> str:
|
|
65
|
+
return "My Runtime"
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def description() -> str:
|
|
69
|
+
return "My custom agent runtime"
|
|
70
|
+
|
|
71
|
+
async def setup(self, config: AdapterConfig) -> None:
|
|
72
|
+
result = await self._common_setup(config)
|
|
73
|
+
# Store result attributes for create_executor
|
|
74
|
+
|
|
75
|
+
async def create_executor(self, config: AdapterConfig) -> AgentExecutor:
|
|
76
|
+
# Return an AgentExecutor instance
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Set `ADAPTER_MODULE=my_package.adapter` and run `molecule-runtime`.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
BSL-1.1 — see LICENSE for details.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# molecule-ai-workspace-runtime
|
|
2
|
+
|
|
3
|
+
Shared Python runtime infrastructure for all Molecule AI agent adapters.
|
|
4
|
+
|
|
5
|
+
This package provides the core machinery that every Molecule AI workspace container needs:
|
|
6
|
+
|
|
7
|
+
- **A2A server** — Registers with the platform, heartbeats, serves A2A JSON-RPC
|
|
8
|
+
- **Adapter interface** — `BaseAdapter` / `AdapterConfig` / `SetupResult`
|
|
9
|
+
- **Built-in tools** — delegation, memory, approvals, sandbox, telemetry
|
|
10
|
+
- **Skill loader** — loads and hot-reloads skill modules from `/configs/skills/`
|
|
11
|
+
- **Plugin system** — per-workspace + shared plugin discovery and install
|
|
12
|
+
- **Config / preflight** — YAML config loading with validation
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install molecule-ai-workspace-runtime
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Adapter Discovery
|
|
21
|
+
|
|
22
|
+
The runtime discovers adapters in two ways:
|
|
23
|
+
|
|
24
|
+
1. **`ADAPTER_MODULE` env var** (standalone adapter repos):
|
|
25
|
+
```bash
|
|
26
|
+
ADAPTER_MODULE=my_adapter molecule-runtime
|
|
27
|
+
```
|
|
28
|
+
The module must export an `Adapter` class extending `BaseAdapter`.
|
|
29
|
+
|
|
30
|
+
2. **Built-in subdirectory scan** (monorepo local dev):
|
|
31
|
+
Scans `molecule_runtime/adapters/` subdirectories for `Adapter` classes.
|
|
32
|
+
|
|
33
|
+
## Writing an Adapter
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig
|
|
37
|
+
from a2a.server.agent_execution import AgentExecutor
|
|
38
|
+
|
|
39
|
+
class Adapter(BaseAdapter):
|
|
40
|
+
@staticmethod
|
|
41
|
+
def name() -> str:
|
|
42
|
+
return "my-runtime"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def display_name() -> str:
|
|
46
|
+
return "My Runtime"
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def description() -> str:
|
|
50
|
+
return "My custom agent runtime"
|
|
51
|
+
|
|
52
|
+
async def setup(self, config: AdapterConfig) -> None:
|
|
53
|
+
result = await self._common_setup(config)
|
|
54
|
+
# Store result attributes for create_executor
|
|
55
|
+
|
|
56
|
+
async def create_executor(self, config: AdapterConfig) -> AgentExecutor:
|
|
57
|
+
# Return an AgentExecutor instance
|
|
58
|
+
...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Set `ADAPTER_MODULE=my_package.adapter` and run `molecule-runtime`.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
BSL-1.1 — see LICENSE for details.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: molecule-ai-workspace-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Molecule AI workspace runtime — shared infrastructure for all agent adapters
|
|
5
|
+
License: BSL-1.1
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: a2a-sdk[http-server]>=0.3.25
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
11
|
+
Requires-Dist: starlette>=0.38.0
|
|
12
|
+
Requires-Dist: websockets>=12.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.24.0
|
|
16
|
+
Requires-Dist: opentelemetry-sdk>=1.24.0
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24.0
|
|
18
|
+
Requires-Dist: temporalio>=1.7.0
|
|
19
|
+
|
|
20
|
+
# molecule-ai-workspace-runtime
|
|
21
|
+
|
|
22
|
+
Shared Python runtime infrastructure for all Molecule AI agent adapters.
|
|
23
|
+
|
|
24
|
+
This package provides the core machinery that every Molecule AI workspace container needs:
|
|
25
|
+
|
|
26
|
+
- **A2A server** — Registers with the platform, heartbeats, serves A2A JSON-RPC
|
|
27
|
+
- **Adapter interface** — `BaseAdapter` / `AdapterConfig` / `SetupResult`
|
|
28
|
+
- **Built-in tools** — delegation, memory, approvals, sandbox, telemetry
|
|
29
|
+
- **Skill loader** — loads and hot-reloads skill modules from `/configs/skills/`
|
|
30
|
+
- **Plugin system** — per-workspace + shared plugin discovery and install
|
|
31
|
+
- **Config / preflight** — YAML config loading with validation
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install molecule-ai-workspace-runtime
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Adapter Discovery
|
|
40
|
+
|
|
41
|
+
The runtime discovers adapters in two ways:
|
|
42
|
+
|
|
43
|
+
1. **`ADAPTER_MODULE` env var** (standalone adapter repos):
|
|
44
|
+
```bash
|
|
45
|
+
ADAPTER_MODULE=my_adapter molecule-runtime
|
|
46
|
+
```
|
|
47
|
+
The module must export an `Adapter` class extending `BaseAdapter`.
|
|
48
|
+
|
|
49
|
+
2. **Built-in subdirectory scan** (monorepo local dev):
|
|
50
|
+
Scans `molecule_runtime/adapters/` subdirectories for `Adapter` classes.
|
|
51
|
+
|
|
52
|
+
## Writing an Adapter
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig
|
|
56
|
+
from a2a.server.agent_execution import AgentExecutor
|
|
57
|
+
|
|
58
|
+
class Adapter(BaseAdapter):
|
|
59
|
+
@staticmethod
|
|
60
|
+
def name() -> str:
|
|
61
|
+
return "my-runtime"
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def display_name() -> str:
|
|
65
|
+
return "My Runtime"
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def description() -> str:
|
|
69
|
+
return "My custom agent runtime"
|
|
70
|
+
|
|
71
|
+
async def setup(self, config: AdapterConfig) -> None:
|
|
72
|
+
result = await self._common_setup(config)
|
|
73
|
+
# Store result attributes for create_executor
|
|
74
|
+
|
|
75
|
+
async def create_executor(self, config: AdapterConfig) -> AgentExecutor:
|
|
76
|
+
# Return an AgentExecutor instance
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Set `ADAPTER_MODULE=my_package.adapter` and run `molecule-runtime`.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
BSL-1.1 — see LICENSE for details.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
molecule_ai_workspace_runtime.egg-info/PKG-INFO
|
|
4
|
+
molecule_ai_workspace_runtime.egg-info/SOURCES.txt
|
|
5
|
+
molecule_ai_workspace_runtime.egg-info/dependency_links.txt
|
|
6
|
+
molecule_ai_workspace_runtime.egg-info/entry_points.txt
|
|
7
|
+
molecule_ai_workspace_runtime.egg-info/requires.txt
|
|
8
|
+
molecule_ai_workspace_runtime.egg-info/top_level.txt
|
|
9
|
+
molecule_runtime/__init__.py
|
|
10
|
+
molecule_runtime/a2a_cli.py
|
|
11
|
+
molecule_runtime/a2a_client.py
|
|
12
|
+
molecule_runtime/a2a_executor.py
|
|
13
|
+
molecule_runtime/a2a_mcp_server.py
|
|
14
|
+
molecule_runtime/a2a_tools.py
|
|
15
|
+
molecule_runtime/agent.py
|
|
16
|
+
molecule_runtime/claude_sdk_executor.py
|
|
17
|
+
molecule_runtime/cli_executor.py
|
|
18
|
+
molecule_runtime/config.py
|
|
19
|
+
molecule_runtime/consolidation.py
|
|
20
|
+
molecule_runtime/coordinator.py
|
|
21
|
+
molecule_runtime/events.py
|
|
22
|
+
molecule_runtime/executor_helpers.py
|
|
23
|
+
molecule_runtime/heartbeat.py
|
|
24
|
+
molecule_runtime/initial_prompt.py
|
|
25
|
+
molecule_runtime/main.py
|
|
26
|
+
molecule_runtime/molecule_ai_status.py
|
|
27
|
+
molecule_runtime/platform_auth.py
|
|
28
|
+
molecule_runtime/plugins.py
|
|
29
|
+
molecule_runtime/preflight.py
|
|
30
|
+
molecule_runtime/prompt.py
|
|
31
|
+
molecule_runtime/transcript_auth.py
|
|
32
|
+
molecule_runtime/watcher.py
|
|
33
|
+
molecule_runtime/adapters/__init__.py
|
|
34
|
+
molecule_runtime/adapters/base.py
|
|
35
|
+
molecule_runtime/adapters/shared_runtime.py
|
|
36
|
+
molecule_runtime/builtin_tools/__init__.py
|
|
37
|
+
molecule_runtime/builtin_tools/a2a_tools.py
|
|
38
|
+
molecule_runtime/builtin_tools/approval.py
|
|
39
|
+
molecule_runtime/builtin_tools/audit.py
|
|
40
|
+
molecule_runtime/builtin_tools/awareness_client.py
|
|
41
|
+
molecule_runtime/builtin_tools/compliance.py
|
|
42
|
+
molecule_runtime/builtin_tools/delegation.py
|
|
43
|
+
molecule_runtime/builtin_tools/governance.py
|
|
44
|
+
molecule_runtime/builtin_tools/hitl.py
|
|
45
|
+
molecule_runtime/builtin_tools/medo.py
|
|
46
|
+
molecule_runtime/builtin_tools/memory.py
|
|
47
|
+
molecule_runtime/builtin_tools/sandbox.py
|
|
48
|
+
molecule_runtime/builtin_tools/security_scan.py
|
|
49
|
+
molecule_runtime/builtin_tools/telemetry.py
|
|
50
|
+
molecule_runtime/builtin_tools/temporal_workflow.py
|
|
51
|
+
molecule_runtime/plugins_registry/__init__.py
|
|
52
|
+
molecule_runtime/plugins_registry/builtins.py
|
|
53
|
+
molecule_runtime/plugins_registry/protocol.py
|
|
54
|
+
molecule_runtime/plugins_registry/raw_drop.py
|
|
55
|
+
molecule_runtime/policies/__init__.py
|
|
56
|
+
molecule_runtime/policies/namespaces.py
|
|
57
|
+
molecule_runtime/policies/routing.py
|
|
58
|
+
molecule_runtime/skill_loader/__init__.py
|
|
59
|
+
molecule_runtime/skill_loader/loader.py
|
|
60
|
+
molecule_runtime/skill_loader/watcher.py
|
molecule_ai_workspace_runtime-0.1.0/molecule_ai_workspace_runtime.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
a2a-sdk[http-server]>=0.3.25
|
|
2
|
+
httpx>=0.27.0
|
|
3
|
+
uvicorn>=0.30.0
|
|
4
|
+
starlette>=0.38.0
|
|
5
|
+
websockets>=12.0
|
|
6
|
+
pyyaml>=6.0
|
|
7
|
+
langchain-core>=0.3.0
|
|
8
|
+
opentelemetry-api>=1.24.0
|
|
9
|
+
opentelemetry-sdk>=1.24.0
|
|
10
|
+
opentelemetry-exporter-otlp-proto-http>=1.24.0
|
|
11
|
+
temporalio>=1.7.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
molecule_runtime
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A2A CLI — command-line tools for inter-workspace communication.
|
|
3
|
+
|
|
4
|
+
Supports both synchronous and asynchronous delegation:
|
|
5
|
+
a2a delegate <id> <task> — Send task, wait for response (sync)
|
|
6
|
+
a2a delegate --async <id> <task> — Send task, return task ID immediately
|
|
7
|
+
a2a status <task_id> — Check task status / get result
|
|
8
|
+
a2a peers — List available peers
|
|
9
|
+
a2a info — Show this workspace's info
|
|
10
|
+
|
|
11
|
+
Environment variables:
|
|
12
|
+
WORKSPACE_ID — this workspace's ID
|
|
13
|
+
PLATFORM_URL — platform API base URL
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "")
|
|
25
|
+
PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def discover(target_id: str) -> dict | None:
|
|
29
|
+
"""Discover a peer workspace's URL."""
|
|
30
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
31
|
+
resp = await client.get(
|
|
32
|
+
f"{PLATFORM_URL}/registry/discover/{target_id}",
|
|
33
|
+
headers={"X-Workspace-ID": WORKSPACE_ID},
|
|
34
|
+
)
|
|
35
|
+
if resp.status_code == 200:
|
|
36
|
+
return resp.json()
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def delegate(target_id: str, task: str, async_mode: bool = False):
|
|
41
|
+
"""Delegate a task to another workspace."""
|
|
42
|
+
peer = await discover(target_id)
|
|
43
|
+
if not peer:
|
|
44
|
+
print(f"Error: cannot reach workspace {target_id} (access denied or offline)", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
target_url = peer.get("url", "")
|
|
48
|
+
if not target_url:
|
|
49
|
+
print(f"Error: workspace {target_id} has no URL", file=sys.stderr)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
task_id = str(uuid.uuid4())
|
|
53
|
+
|
|
54
|
+
if async_mode:
|
|
55
|
+
# Async: send and return immediately, don't wait for response
|
|
56
|
+
# Use a background task that fires and forgets
|
|
57
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
58
|
+
try:
|
|
59
|
+
# Send with a short timeout — just confirm receipt
|
|
60
|
+
resp = await client.post(
|
|
61
|
+
target_url,
|
|
62
|
+
json={
|
|
63
|
+
"jsonrpc": "2.0",
|
|
64
|
+
"id": task_id,
|
|
65
|
+
"method": "message/send",
|
|
66
|
+
"params": {
|
|
67
|
+
"message": {
|
|
68
|
+
"role": "user",
|
|
69
|
+
"messageId": str(uuid.uuid4()),
|
|
70
|
+
"parts": [{"kind": "text", "text": task}],
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
# Even if we timeout, the task is queued on the target
|
|
76
|
+
print(json.dumps({
|
|
77
|
+
"task_id": task_id,
|
|
78
|
+
"target": target_id,
|
|
79
|
+
"status": "submitted",
|
|
80
|
+
"target_url": target_url,
|
|
81
|
+
}))
|
|
82
|
+
except httpx.TimeoutException:
|
|
83
|
+
# Request was sent but we didn't get confirmation — task may or may not have been received
|
|
84
|
+
print(json.dumps({
|
|
85
|
+
"task_id": task_id,
|
|
86
|
+
"target": target_id,
|
|
87
|
+
"status": "uncertain",
|
|
88
|
+
"note": "Request sent but response timed out — delivery unconfirmed. Use 'a2a status' to check.",
|
|
89
|
+
}), file=sys.stderr)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Sync: wait for full response with retry on rate limit
|
|
93
|
+
max_retries = 3
|
|
94
|
+
for attempt in range(max_retries):
|
|
95
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
96
|
+
try:
|
|
97
|
+
resp = await client.post(
|
|
98
|
+
target_url,
|
|
99
|
+
json={
|
|
100
|
+
"jsonrpc": "2.0",
|
|
101
|
+
"id": task_id,
|
|
102
|
+
"method": "message/send",
|
|
103
|
+
"params": {
|
|
104
|
+
"message": {
|
|
105
|
+
"role": "user",
|
|
106
|
+
"messageId": str(uuid.uuid4()),
|
|
107
|
+
"parts": [{"kind": "text", "text": task}],
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
data = resp.json()
|
|
114
|
+
except Exception:
|
|
115
|
+
print(f"Error: invalid JSON response (status {resp.status_code})", file=sys.stderr)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
if "result" in data:
|
|
118
|
+
parts = data["result"].get("parts", [])
|
|
119
|
+
text = parts[0].get("text", "") if parts else ""
|
|
120
|
+
if text and text != "(no response generated)":
|
|
121
|
+
print(text)
|
|
122
|
+
return
|
|
123
|
+
# Empty or no-response — might be rate limited, retry
|
|
124
|
+
if attempt < max_retries - 1:
|
|
125
|
+
delay = 5 * (2 ** attempt)
|
|
126
|
+
print(f"(empty response, retrying in {delay}s...)", file=sys.stderr)
|
|
127
|
+
await asyncio.sleep(delay)
|
|
128
|
+
continue
|
|
129
|
+
print(text or "(no response after retries)")
|
|
130
|
+
elif "error" in data:
|
|
131
|
+
error_msg = data['error'].get('message', 'unknown')
|
|
132
|
+
if ("rate" in error_msg.lower() or "overloaded" in error_msg.lower()) and attempt < max_retries - 1:
|
|
133
|
+
delay = 5 * (2 ** attempt)
|
|
134
|
+
print(f"(rate limited, retrying in {delay}s...)", file=sys.stderr)
|
|
135
|
+
await asyncio.sleep(delay)
|
|
136
|
+
continue
|
|
137
|
+
print(f"Error: {error_msg}", file=sys.stderr)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
return
|
|
140
|
+
except httpx.TimeoutException:
|
|
141
|
+
if attempt < max_retries - 1:
|
|
142
|
+
delay = 5 * (2 ** attempt)
|
|
143
|
+
print(f"(timeout, retrying in {delay}s...)", file=sys.stderr)
|
|
144
|
+
await asyncio.sleep(delay)
|
|
145
|
+
continue
|
|
146
|
+
print("Error: request timed out after retries", file=sys.stderr)
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def check_status(target_id: str, task_id: str):
|
|
151
|
+
"""Check the status of an async task."""
|
|
152
|
+
peer = await discover(target_id)
|
|
153
|
+
if not peer:
|
|
154
|
+
print(f"Error: cannot reach workspace {target_id}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
target_url = peer.get("url", "")
|
|
158
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
159
|
+
resp = await client.post(
|
|
160
|
+
target_url,
|
|
161
|
+
json={
|
|
162
|
+
"jsonrpc": "2.0",
|
|
163
|
+
"id": str(uuid.uuid4()),
|
|
164
|
+
"method": "tasks/get",
|
|
165
|
+
"params": {"id": task_id},
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
data = resp.json()
|
|
169
|
+
if "result" in data:
|
|
170
|
+
task = data["result"]
|
|
171
|
+
status = task.get("status", {}).get("state", "unknown")
|
|
172
|
+
print(f"Status: {status}")
|
|
173
|
+
if status == "completed":
|
|
174
|
+
artifacts = task.get("artifacts", [])
|
|
175
|
+
for a in artifacts:
|
|
176
|
+
for p in a.get("parts", []):
|
|
177
|
+
if p.get("text"):
|
|
178
|
+
print(p["text"])
|
|
179
|
+
elif "error" in data:
|
|
180
|
+
print(f"Error: {data['error'].get('message', 'unknown')}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def peers():
|
|
184
|
+
"""List available peers."""
|
|
185
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
186
|
+
resp = await client.get(f"{PLATFORM_URL}/registry/{WORKSPACE_ID}/peers")
|
|
187
|
+
if resp.status_code != 200:
|
|
188
|
+
print("Error: could not fetch peers", file=sys.stderr)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
for p in resp.json():
|
|
191
|
+
status = p.get("status", "?")
|
|
192
|
+
role = p.get("role", "")
|
|
193
|
+
print(f"{p['id']} {p['name']:30s} {status:10s} {role}")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def info():
|
|
197
|
+
"""Get this workspace's info."""
|
|
198
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
199
|
+
resp = await client.get(f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}")
|
|
200
|
+
if resp.status_code == 200:
|
|
201
|
+
d = resp.json()
|
|
202
|
+
print(f"ID: {d['id']}")
|
|
203
|
+
print(f"Name: {d['name']}")
|
|
204
|
+
print(f"Role: {d.get('role', '')}")
|
|
205
|
+
print(f"Tier: {d['tier']}")
|
|
206
|
+
print(f"Status: {d['status']}")
|
|
207
|
+
print(f"Parent: {d.get('parent_id', '(root)')}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main():
|
|
211
|
+
if len(sys.argv) < 2:
|
|
212
|
+
print("Usage: a2a <command> [args]")
|
|
213
|
+
print("Commands:")
|
|
214
|
+
print(" delegate <workspace_id> <task> — Send task, wait for response")
|
|
215
|
+
print(" delegate --async <workspace_id> <task> — Send task, return immediately")
|
|
216
|
+
print(" status <workspace_id> <task_id> — Check async task status")
|
|
217
|
+
print(" peers — List available peers")
|
|
218
|
+
print(" info — Show workspace info")
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
cmd = sys.argv[1]
|
|
222
|
+
|
|
223
|
+
if cmd == "delegate":
|
|
224
|
+
async_mode = "--async" in sys.argv
|
|
225
|
+
args = [a for a in sys.argv[2:] if a != "--async"]
|
|
226
|
+
if len(args) < 2:
|
|
227
|
+
print("Usage: a2a delegate [--async] <workspace_id> <task>", file=sys.stderr)
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
asyncio.run(delegate(args[0], " ".join(args[1:]), async_mode))
|
|
230
|
+
elif cmd == "status":
|
|
231
|
+
if len(sys.argv) < 4:
|
|
232
|
+
print("Usage: a2a status <workspace_id> <task_id>", file=sys.stderr)
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
asyncio.run(check_status(sys.argv[2], sys.argv[3]))
|
|
235
|
+
elif cmd == "peers":
|
|
236
|
+
asyncio.run(peers())
|
|
237
|
+
elif cmd == "info":
|
|
238
|
+
asyncio.run(info())
|
|
239
|
+
else:
|
|
240
|
+
print(f"Unknown command: {cmd}", file=sys.stderr)
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__": # pragma: no cover
|
|
245
|
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""A2A protocol client — peer discovery, messaging, and workspace info.
|
|
2
|
+
|
|
3
|
+
Shared constants (WORKSPACE_ID, PLATFORM_URL) live here so that
|
|
4
|
+
a2a_tools and a2a_mcp_server can import them from a single place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from platform_auth import auth_headers
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "")
|
|
18
|
+
PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080")
|
|
19
|
+
|
|
20
|
+
# Cache workspace ID → name mappings (populated by list_peers calls)
|
|
21
|
+
_peer_names: dict[str, str] = {}
|
|
22
|
+
|
|
23
|
+
# Sentinel prefix for errors originating from send_a2a_message / child agents.
|
|
24
|
+
# Used by delegate_task to distinguish real errors from normal response text.
|
|
25
|
+
_A2A_ERROR_PREFIX = "[A2A_ERROR] "
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def discover_peer(target_id: str) -> dict | None:
|
|
29
|
+
"""Discover a peer workspace's URL via the platform registry."""
|
|
30
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
31
|
+
try:
|
|
32
|
+
resp = await client.get(
|
|
33
|
+
f"{PLATFORM_URL}/registry/discover/{target_id}",
|
|
34
|
+
headers={"X-Workspace-ID": WORKSPACE_ID, **auth_headers()},
|
|
35
|
+
)
|
|
36
|
+
if resp.status_code == 200:
|
|
37
|
+
return resp.json()
|
|
38
|
+
return None
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f"Discovery failed for {target_id}: {e}")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def send_a2a_message(target_url: str, message: str) -> str:
|
|
45
|
+
"""Send an A2A message/send to a target workspace."""
|
|
46
|
+
# Fix F (Cycle 5 / H2 — flagged 5 consecutive audits): timeout=None allowed
|
|
47
|
+
# a hung upstream to block the agent indefinitely. Use a generous but bounded
|
|
48
|
+
# timeout: 30s connect + 300s read (long enough for slow LLM responses).
|
|
49
|
+
async with httpx.AsyncClient(
|
|
50
|
+
timeout=httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=30.0)
|
|
51
|
+
) as client:
|
|
52
|
+
try:
|
|
53
|
+
resp = await client.post(
|
|
54
|
+
target_url,
|
|
55
|
+
headers=auth_headers(),
|
|
56
|
+
json={
|
|
57
|
+
"jsonrpc": "2.0",
|
|
58
|
+
"id": str(uuid.uuid4()),
|
|
59
|
+
"method": "message/send",
|
|
60
|
+
"params": {
|
|
61
|
+
"message": {
|
|
62
|
+
"role": "user",
|
|
63
|
+
"messageId": str(uuid.uuid4()),
|
|
64
|
+
"parts": [{"kind": "text", "text": message}],
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
data = resp.json()
|
|
70
|
+
if "result" in data:
|
|
71
|
+
parts = data["result"].get("parts", [])
|
|
72
|
+
text = parts[0].get("text", "") if parts else "(no response)"
|
|
73
|
+
# Tag child-reported errors so the caller can detect them reliably
|
|
74
|
+
if text.startswith("Agent error:"):
|
|
75
|
+
return f"{_A2A_ERROR_PREFIX}{text}"
|
|
76
|
+
return text
|
|
77
|
+
elif "error" in data:
|
|
78
|
+
return f"{_A2A_ERROR_PREFIX}{data['error'].get('message', 'unknown')}"
|
|
79
|
+
return str(data)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return f"{_A2A_ERROR_PREFIX}{e}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def get_peers() -> list[dict]:
|
|
85
|
+
"""Get this workspace's peers from the platform registry."""
|
|
86
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
87
|
+
try:
|
|
88
|
+
resp = await client.get(
|
|
89
|
+
f"{PLATFORM_URL}/registry/{WORKSPACE_ID}/peers",
|
|
90
|
+
headers={"X-Workspace-ID": WORKSPACE_ID, **auth_headers()},
|
|
91
|
+
)
|
|
92
|
+
if resp.status_code == 200:
|
|
93
|
+
return resp.json()
|
|
94
|
+
return []
|
|
95
|
+
except Exception:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def get_workspace_info() -> dict:
|
|
100
|
+
"""Get this workspace's info from the platform."""
|
|
101
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
102
|
+
try:
|
|
103
|
+
resp = await client.get(
|
|
104
|
+
f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}",
|
|
105
|
+
headers=auth_headers(),
|
|
106
|
+
)
|
|
107
|
+
if resp.status_code == 200:
|
|
108
|
+
return resp.json()
|
|
109
|
+
return {"error": "not found"}
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return {"error": str(e)}
|