fix: address self-review findings — lint + comment + missing test
- Drop unused `import time` from inbound.py and `import call` from test_inbound.py (caught by ruff in CI; would have caught locally if I'd run it before pushing). - Rewrite the misleading comment in PollDelivery.run_once: the cursor DOES advance past handler exceptions (poison-pill resilience). The previous comment claimed otherwise, which would have confused future readers. - Drop `_parse_activity_row` from inbound.py's `__all__`. The leading underscore signals "private helper"; exposing it via `__all__` contradicted the convention. Tests still import it directly via the module path. - Add `test_fetch_inbound_429_retries_via_get_with_retry` — the PR description claimed branch-coverage of the 429 path but no test exercised it. Closes the gap.
This commit is contained in:
parent
70d66cd814
commit
87a4cfcc55
BIN
molecule_agent/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
molecule_agent/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_agent/__pycache__/__main__.cpython-313.pyc
Normal file
BIN
molecule_agent/__pycache__/__main__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_agent/__pycache__/a2a_server.cpython-313.pyc
Normal file
BIN
molecule_agent/__pycache__/a2a_server.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_agent/__pycache__/client.cpython-313.pyc
Normal file
BIN
molecule_agent/__pycache__/client.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_agent/__pycache__/inbound.cpython-313.pyc
Normal file
BIN
molecule_agent/__pycache__/inbound.cpython-313.pyc
Normal file
Binary file not shown.
@ -29,7 +29,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
@ -253,10 +252,13 @@ class PollDelivery:
|
|||||||
def run_once(self, handler: MessageHandler) -> int:
|
def run_once(self, handler: MessageHandler) -> int:
|
||||||
"""Fetch one batch and dispatch each message to ``handler``.
|
"""Fetch one batch and dispatch each message to ``handler``.
|
||||||
|
|
||||||
Returns the number of messages dispatched. A handler exception is
|
Returns the number of messages dispatched. The cursor advances past
|
||||||
logged but does not abort the batch — at-least-once semantics, the
|
every dispatched row, including ones whose handler raised — a
|
||||||
same row may be re-delivered on the next iteration if its cursor
|
poison-pill input shouldn't block the queue forever. The handler
|
||||||
wasn't advanced.
|
is responsible for surfacing its own errors via logging or its own
|
||||||
|
observability. This matches Slack Events delivery and SQS DLQ
|
||||||
|
semantics; the platform makes no exactly-once guarantees on
|
||||||
|
activity poll, so handlers must be idempotent regardless.
|
||||||
"""
|
"""
|
||||||
if self._stopped:
|
if self._stopped:
|
||||||
return 0
|
return 0
|
||||||
@ -366,5 +368,4 @@ __all__ = [
|
|||||||
"MessageHandler",
|
"MessageHandler",
|
||||||
"PollDelivery",
|
"PollDelivery",
|
||||||
"PushDelivery",
|
"PushDelivery",
|
||||||
"_parse_activity_row",
|
|
||||||
]
|
]
|
||||||
|
|||||||
159
molecule_ai_sdk.egg-info/PKG-INFO
Normal file
159
molecule_ai_sdk.egg-info/PKG-INFO
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: molecule-ai-sdk
|
||||||
|
Version: 0.2.0
|
||||||
|
Summary: Molecule AI SDK — build plugins (molecule_plugin) AND remote agents that join a Molecule AI org from another machine (molecule_agent).
|
||||||
|
Author: Molecule AI
|
||||||
|
License: MIT
|
||||||
|
Project-URL: Homepage, https://github.com/Molecule-AI/molecule-sdk-python
|
||||||
|
Project-URL: Repository, https://github.com/Molecule-AI/molecule-sdk-python
|
||||||
|
Project-URL: Documentation, https://github.com/Molecule-AI/molecule-sdk-python#readme
|
||||||
|
Keywords: agents,ai,multi-agent,a2a,plugins,saas,remote-agent
|
||||||
|
Classifier: Development Status :: 4 - Beta
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Requires-Python: >=3.11
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
Requires-Dist: pyyaml>=6.0
|
||||||
|
Requires-Dist: requests>=2.31
|
||||||
|
Provides-Extra: test
|
||||||
|
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
||||||
|
|
||||||
|
# molecule_plugin — Python SDK for building Molecule AI plugins
|
||||||
|
|
||||||
|
A Molecule AI plugin is a directory that bundles rules, skills, and per-runtime
|
||||||
|
install adaptors. Any plugin that conforms to this contract is installable
|
||||||
|
on any Molecule AI workspace whose runtime the plugin supports.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Copy `template/` to a new directory and edit:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-plugin/
|
||||||
|
├── plugin.yaml # name, version, runtimes, description
|
||||||
|
├── rules/my-rule.md # optional — appended to CLAUDE.md at install
|
||||||
|
├── skills/my-skill/
|
||||||
|
│ ├── SKILL.md # instructions injected into the system prompt
|
||||||
|
│ └── tools/do_thing.py # optional LangChain @tool functions
|
||||||
|
└── adapters/
|
||||||
|
├── claude_code.py # one-liner: `from molecule_plugin import AgentskillsAdaptor as Adaptor`
|
||||||
|
└── deepagents.py # same
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from molecule_plugin import validate_manifest
|
||||||
|
errors = validate_manifest("my-plugin/plugin.yaml")
|
||||||
|
assert not errors, errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
The SDK ships a CLI for validating Molecule AI artifacts before publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m molecule_plugin validate plugin my-plugin/
|
||||||
|
python -m molecule_plugin validate workspace workspace-configs-templates/claude-code-default/
|
||||||
|
python -m molecule_plugin validate org org-templates/molecule-dev/
|
||||||
|
python -m molecule_plugin validate channel channels.yaml
|
||||||
|
python -m molecule_plugin validate my-plugin/ # kind defaults to 'plugin'
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code is 0 when valid, 1 when any errors are found — suitable for CI.
|
||||||
|
Add `-q` / `--quiet` to suppress success lines and emit only errors.
|
||||||
|
|
||||||
|
Programmatic equivalents:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from molecule_plugin import (
|
||||||
|
validate_plugin,
|
||||||
|
validate_workspace_template,
|
||||||
|
validate_org_template,
|
||||||
|
validate_channel_file,
|
||||||
|
validate_channel_config,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-runtime adaptors — when to write a custom one
|
||||||
|
|
||||||
|
The default `AgentskillsAdaptor` handles the common shape: rules go into
|
||||||
|
the runtime's memory file (CLAUDE.md), skill dirs go into `/configs/skills/`.
|
||||||
|
That covers most plugins.
|
||||||
|
|
||||||
|
Write a custom adaptor when you need to:
|
||||||
|
|
||||||
|
- **Register runtime tools dynamically** — call `ctx.register_tool(name, fn)`.
|
||||||
|
- **Register DeepAgents sub-agents** — call `ctx.register_subagent(name, spec)`.
|
||||||
|
- **Write to a non-standard memory file** — call `ctx.append_to_memory(filename, content)`.
|
||||||
|
|
||||||
|
Minimum custom adaptor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# adapters/deepagents.py
|
||||||
|
from molecule_plugin import InstallContext, InstallResult
|
||||||
|
|
||||||
|
class Adaptor:
|
||||||
|
def __init__(self, plugin_name: str, runtime: str):
|
||||||
|
self.plugin_name, self.runtime = plugin_name, runtime
|
||||||
|
|
||||||
|
async def install(self, ctx: InstallContext) -> InstallResult:
|
||||||
|
ctx.register_subagent("my-agent", {"prompt": "...", "tools": [...]})
|
||||||
|
return InstallResult(plugin_name=self.plugin_name, runtime=self.runtime, source="plugin")
|
||||||
|
|
||||||
|
async def uninstall(self, ctx: InstallContext) -> None:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolution order (understood by the platform)
|
||||||
|
|
||||||
|
For `(plugin_name, runtime)`:
|
||||||
|
|
||||||
|
1. **Platform registry** — `workspace-template/plugins_registry/<plugin>/<runtime>.py`
|
||||||
|
(curated; set by the Molecule AI team for quality-assured plugins).
|
||||||
|
2. **Plugin-shipped** — `<plugin_root>/adapters/<runtime>.py` (what this SDK helps you build).
|
||||||
|
3. **Raw-drop fallback** — copies plugin files into `/configs/plugins/<name>/`
|
||||||
|
and surfaces a warning; no tools are wired.
|
||||||
|
|
||||||
|
You generally ship for path #2. If your plugin becomes popular enough to be
|
||||||
|
promoted to "default," the Molecule AI team PRs a copy of your adaptor into
|
||||||
|
the platform registry (path #1) so it survives upstream breakage.
|
||||||
|
|
||||||
|
## Testing locally
|
||||||
|
|
||||||
|
The SDK ships `AgentskillsAdaptor` as a standalone, unit-testable class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from molecule_plugin import AgentskillsAdaptor, InstallContext
|
||||||
|
|
||||||
|
ctx = InstallContext(
|
||||||
|
configs_dir=Path("/tmp/configs"),
|
||||||
|
workspace_id="local",
|
||||||
|
runtime="claude_code",
|
||||||
|
plugin_root=Path("./my-plugin"),
|
||||||
|
)
|
||||||
|
asyncio.run(AgentskillsAdaptor("my-plugin", "claude_code").install(ctx))
|
||||||
|
# check /tmp/configs/CLAUDE.md, /tmp/configs/skills/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
A plugin is just a directory. Push it to any Git host. Installation via
|
||||||
|
`POST /plugins/install {git_url}` is on the roadmap — see the platform's
|
||||||
|
`PLAN.md` under "Install-from-GitHub-URL flow." Until then, plugins are
|
||||||
|
bundled into the platform by dropping them into `plugins/` at deploy time.
|
||||||
|
|
||||||
|
## Supported runtimes
|
||||||
|
|
||||||
|
As of 2026-Q2: `claude_code`, `deepagents`, `langgraph`, `crewai`, `autogen`,
|
||||||
|
`openclaw`. See the live list with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl $PLATFORM_URL/plugins
|
||||||
|
```
|
||||||
30
molecule_ai_sdk.egg-info/SOURCES.txt
Normal file
30
molecule_ai_sdk.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
molecule_agent/__init__.py
|
||||||
|
molecule_agent/__main__.py
|
||||||
|
molecule_agent/a2a_server.py
|
||||||
|
molecule_agent/client.py
|
||||||
|
molecule_agent/inbound.py
|
||||||
|
molecule_ai_sdk.egg-info/PKG-INFO
|
||||||
|
molecule_ai_sdk.egg-info/SOURCES.txt
|
||||||
|
molecule_ai_sdk.egg-info/dependency_links.txt
|
||||||
|
molecule_ai_sdk.egg-info/requires.txt
|
||||||
|
molecule_ai_sdk.egg-info/top_level.txt
|
||||||
|
molecule_plugin/__init__.py
|
||||||
|
molecule_plugin/__main__.py
|
||||||
|
molecule_plugin/builtins.py
|
||||||
|
molecule_plugin/channel.py
|
||||||
|
molecule_plugin/manifest.py
|
||||||
|
molecule_plugin/org.py
|
||||||
|
molecule_plugin/protocol.py
|
||||||
|
molecule_plugin/workspace.py
|
||||||
|
tests/test_a2a_server.py
|
||||||
|
tests/test_call_peer_errors.py
|
||||||
|
tests/test_cli_connect.py
|
||||||
|
tests/test_inbound.py
|
||||||
|
tests/test_remote_agent.py
|
||||||
|
tests/test_retry_backoff.py
|
||||||
|
tests/test_safe_extract.py
|
||||||
|
tests/test_sdk.py
|
||||||
|
tests/test_sha256_verification.py
|
||||||
|
tests/test_validators.py
|
||||||
1
molecule_ai_sdk.egg-info/dependency_links.txt
Normal file
1
molecule_ai_sdk.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
5
molecule_ai_sdk.egg-info/requires.txt
Normal file
5
molecule_ai_sdk.egg-info/requires.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pyyaml>=6.0
|
||||||
|
requests>=2.31
|
||||||
|
|
||||||
|
[test]
|
||||||
|
pytest-asyncio>=0.24
|
||||||
2
molecule_ai_sdk.egg-info/top_level.txt
Normal file
2
molecule_ai_sdk.egg-info/top_level.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
molecule_agent
|
||||||
|
molecule_plugin
|
||||||
BIN
molecule_plugin/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/__main__.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/__main__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/builtins.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/builtins.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/channel.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/channel.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/manifest.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/manifest.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/org.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/org.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/protocol.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/protocol.cpython-313.pyc
Normal file
Binary file not shown.
BIN
molecule_plugin/__pycache__/workspace.cpython-313.pyc
Normal file
BIN
molecule_plugin/__pycache__/workspace.cpython-313.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_a2a_server.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_a2a_server.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
tests/__pycache__/test_cli_connect.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_cli_connect.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_inbound.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_inbound.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_remote_agent.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_remote_agent.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
tests/__pycache__/test_safe_extract.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_safe_extract.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_sdk.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_sdk.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
tests/__pycache__/test_validators.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_validators.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
@ -19,7 +19,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, call
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
@ -223,6 +223,33 @@ def test_fetch_inbound_empty_returns_empty(client: RemoteAgentClient):
|
|||||||
assert client.fetch_inbound() == []
|
assert client.fetch_inbound() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_inbound_429_retries_via_get_with_retry(
|
||||||
|
client: RemoteAgentClient, monkeypatch
|
||||||
|
):
|
||||||
|
"""A 429 on the first GET should route through _get_with_retry, which
|
||||||
|
honours Retry-After / jittered backoff and eventually returns a 2xx.
|
||||||
|
"""
|
||||||
|
# Don't actually sleep during the retry — keeps the test fast.
|
||||||
|
monkeypatch.setattr("time.sleep", lambda _s: None)
|
||||||
|
|
||||||
|
rows = [{"id": "act-after-retry", "data": {"source": "canvas_user", "text": "ok"}}]
|
||||||
|
|
||||||
|
# First call: 429. Second call (the retry): 200 + rows. _get_with_retry
|
||||||
|
# will see 429 and call session.get again with the rebuilt URL — both
|
||||||
|
# responses come from the same mocked session.get, so we use side_effect.
|
||||||
|
first_429 = FakeResponse(429)
|
||||||
|
first_429.headers = {"Retry-After": "0"}
|
||||||
|
second_200 = FakeResponse(200, rows)
|
||||||
|
client._session.get.side_effect = [first_429, second_200]
|
||||||
|
|
||||||
|
out = client.fetch_inbound(since_id="act-prev")
|
||||||
|
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0].activity_id == "act-after-retry"
|
||||||
|
# Two GETs total: one 429, one 200.
|
||||||
|
assert client._session.get.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# reply()
|
# reply()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user