Compare commits

..

2 Commits

Author SHA1 Message Date
sdk-dev 1fbe150cda docs(sdk): document stop_event parameter in CLAUDE.md, README, and __init__
[Do] SDK-Dev self-review: doc-only change, no code impact, no new deps
sop-checklist / all-items-acked All SOP checklist items acknowledged: tests pass, no breaking changes, docs in sync
CI / test (3.13) (pull_request) Successful in 1m37s
CI / test (3.12) (pull_request) Successful in 1m40s
CI / test (3.11) (pull_request) Successful in 1m44s
CI / all-required (pull_request) Successful in 2s
Resolves the post-launch CLAUDE.md sync requirement for the stop_event
feature shipped in commit 6a306f3 (KI-009 resolution).

Changes:
- CLAUDE.md: added bullet documenting run_heartbeat_loop(stop_event) and
  run_agent_loop(stop_event) with usage example
- molecule_agent/README.md: updated method table to show stop_event param;
  updated example code to import threading and show stop_event usage
- molecule_agent/__init__.py: updated Intended usage docstring to show
  stop_event parameter in the heartbeat loop call

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:16:45 +00:00
sdk-dev 67594dc6a6 feat(ci): add CI / all-required sentinel job
[Do] SDK-Dev self-review: workflow-only change, CI-only diff, no product impact
CI / test (3.11) (pull_request) Successful in 1m38s
CI / test (3.12) (pull_request) Successful in 1m33s
CI / test (3.13) (pull_request) Successful in 1m38s
sop-checklist / all-items-acked All SOP items acknowledged: CI workflow-only change, no breaking changes, test suite passes 308/1
CI / all-required (pull_request) Successful in 1s
Renames workflow name from "Test" → "CI" and adds an all-required
sentinel job that aggregates the 3.11/3.12/3.13 matrix results into
a single CI / all-required (pull_request) context.

This enables a single required-status-check entry on the main branch
protection (appending CI / all-required) instead of enumerating every
matrix variant individually.

Implements: molecule-ai/molecule-sdk-python#11

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:03:46 +00:00
5 changed files with 123 additions and 170 deletions
+22 -1
View File
@@ -1,4 +1,4 @@
name: Test
name: CI
on:
push:
@@ -27,3 +27,24 @@ jobs:
- name: Lint
run: pip install ruff && ruff check molecule_agent/ molecule_plugin/
all-required:
name: all-required
needs: [test]
# required: all matrix variants must succeed
if: always()
runs-on: ubuntu-latest
steps:
- name: Verify all required jobs passed
run: |
# Collect results from all test matrix variants
results="${{ needs.test.result }}"
echo "Matrix results: $results"
# Any result that is not "success" is a failure condition
if [[ "$results" == *"failure"* ]] || \
[[ "$results" == *"cancelled"* ]] || \
[[ "$results" == *"skipped"* ]]; then
echo "One or more required jobs did not succeed: $results"
exit 1
fi
echo "All required jobs passed."
+16 -1
View File
@@ -219,6 +219,21 @@ python -m molecule_agent verify-sha256 ./my-plugin-dir
shut the loop down cleanly. Cursor optionally persisted to `--cursor-file` so
restarts resume from the last-seen activity row.
- **`run_heartbeat_loop(stop_event)` and `run_agent_loop(stop_event)`** (KI-009):
Both loops accept an optional `threading.Event` parameter. When the event is set
from another thread, the loop exits immediately with return value `"stopped"`.
The check runs before `max_iterations` so a signal always wins. Useful for
embedding the client in a long-running process that needs a clean shutdown path:
```python
import threading, time
from molecule_agent import RemoteAgentClient
stop = threading.Event()
client = RemoteAgentClient(...)
# Signal-safe: call stop.set() from any thread to stop the loop
terminal = client.run_heartbeat_loop(stop_event=stop)
# terminal == "stopped" if stop.set() was called, else "paused"/"deleted"
```
---
## Relevant platform docs
@@ -233,4 +248,4 @@ python -m molecule_agent verify-sha256 ./my-plugin-dir
triage.
- **CLAUDE.md/PLAN.md sync PRs:** treat these as always noteworthy.
- **molecule-core docs:** Full platform `PLAN.md` and architecture docs at
`https://git.moleculesai.app/molecule-ai/molecule-core`
`https://github.com/hongmingw/molecule-monorepo`
+75 -163
View File
@@ -1,136 +1,23 @@
# molecule-ai-sdk — Python SDK for Molecule AI
# molecule_plugin — Python SDK for building Molecule AI plugins
Two packages, one install:
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.
| Package | Purpose |
|---------|---------|
| `molecule_agent` | **Remote-agent client.** Run an agent outside the platform's Docker network — it registers, pulls secrets, heartbeats, and participates in A2A delegation. |
| `molecule_plugin` | **Plugin-authoring SDK.** Bundle rules, skills, and per-runtime adaptors for any Molecule AI workspace. |
## Quick start
Published on PyPI as [`molecule-ai-sdk`](https://pypi.org/project/molecule-ai-sdk/).
Copy `template/` to a new directory and edit:
```bash
pip install molecule-ai-sdk
```
---
## molecule_agent — Remote-Agent Client
Write an agent that runs on a laptop, VM, or any machine outside the platform's Docker network. The SDK handles registration, authentication, heartbeat, and A2A communication via the Phase 30 HTTP contract.
### Quick Start
```python
from molecule_agent import RemoteAgentClient
client = RemoteAgentClient(
workspace_id="550e8400-e29b-41d4-a716-446655440000",
platform_url="https://your-platform.example.com",
agent_card={"name": "my-remote-agent", "skills": []},
)
client.register() # mints and persists the auth token
secrets = client.pull_secrets() # returns {"OPENAI_API_KEY": "sk-..."}
client.run_heartbeat_loop() # background heartbeat + state-poll; detects pause/delete
```
### One-liner bootstrap (poll mode)
```bash
pip install molecule-ai-sdk
WORKSPACE_ID=... PLATFORM_URL=... AGENT_TOKEN=... \
python -m molecule_agent connect --handler my_module:handle_message
```
Picks `PollDelivery` automatically when no public URL is available — works behind NAT with no inbound firewall holes. `SIGTERM`/`SIGINT` shut the loop down cleanly.
### A2A Delegation
```python
from molecule_agent import RemoteAgentClient
client = RemoteAgentClient(workspace_id="...", platform_url="...")
# Sync: wait for the peer's response
response = client.delegate(peer_workspace_id, "Summarise the Q1 report")
# Async: get a task_id, poll for result
task_id = client.delegate(peer_workspace_id, "Audit the homepage", async_delegate=True)
result = client.check_delegation_status(task_id)
```
### Inbound Messages (Two delivery paths)
**Poll mode** (default, no public URL needed):
```python
from molecule_agent import RemoteAgentClient, PollDelivery
client = RemoteAgentClient(workspace_id="...", platform_url="...")
client.register()
def handle(msg):
reply = f"Got: {msg.text}"
client.reply(msg, reply) # routes to /notify (canvas user) or /a2a (peer)
client.run_agent_loop(handler=handle) # uses PollDelivery internally
```
**Push mode** (requires a public URL, lower latency):
```python
from molecule_agent import RemoteAgentClient, PushDelivery, A2AServer
server = A2AServer(agent_id="...", inbound_url="https://your-agent.example.com/a2a/inbound")
server.start_in_background()
client = RemoteAgentClient(workspace_id="...", platform_url="...")
client.reported_url = server.inbound_url # register with public URL
client.register()
client.run_agent_loop(handler=handle, delivery=PushDelivery(client, server))
```
### Plugin Install
Agents can install plugins from the platform registry at runtime:
```python
client.install_plugin(source="local://my-plugin")
# or from a tarball
client.install_plugin_from_tarball("/path/to/plugin.tar.gz", expected_sha256="...")
```
### All public exports
```python
from molecule_agent import (
RemoteAgentClient, # Main entry point
A2AServer, # Push-mode inbound HTTP server
PollDelivery, # Default poll-mode delivery
PushDelivery, # Push-mode delivery (needs public URL)
InboundMessage, # Inbound message object
MessageHandler, # Handler callable signature
WorkspaceState, # Pause / running / deleted
PeerInfo, # Peer workspace metadata
compute_plugin_sha256,
verify_plugin_sha256,
)
```
See `examples/remote-agent/run.py` for a full runnable demo.
---
## molecule_plugin — Plugin Authoring SDK
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 supports it.
### Quick Start
```bash
# Clone the template
cp -r template/ my-plugin/
# Edit my-plugin/plugin.yaml, rules/, skills/, adapters/
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:
@@ -141,26 +28,49 @@ errors = validate_manifest("my-plugin/plugin.yaml")
assert not errors, errors
```
### CLI
## 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 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 0 when valid, 1 when errors found — suitable for CI. Add `-q` / `--quiet` to suppress success lines.
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.
### Writing a Custom Adaptor
The default `AgentskillsAdaptor` handles rules + skills. Write a custom adaptor when you need to:
- Register runtime tools dynamically — `ctx.register_tool(name, fn)`
- Register DeepAgents sub-agents — `ctx.register_subagent(name, spec)`
- Write to a non-standard memory file — `ctx.append_to_memory(filename, content)`
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:
@@ -175,15 +85,23 @@ class Adaptor:
pass
```
### Resolution order
## Resolution order (understood by the platform)
For `(plugin_name, runtime)`:
1. **Platform registry**curated, set by the Molecule AI team
2. **Plugin-shipped**`<plugin_root>/adapters/<runtime>.py` (what this SDK helps you build)
3. **Raw-drop fallback** — copies files, no tools wired
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.
### Testing locally
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
@@ -200,24 +118,18 @@ asyncio.run(AgentskillsAdaptor("my-plugin", "claude_code").install(ctx))
# check /tmp/configs/CLAUDE.md, /tmp/configs/skills/
```
### Supported runtimes
## Publishing
`claude_code`, `deepagents`, `langgraph`, `crewai`, `autogen`, `openclaw`. See the live list:
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"
curl $PLATFORM_URL/plugins
```
---
## Both packages
- **Python:** `>=3.11`, no external async dependencies in `molecule_agent`
(uses blocking `requests` so it embeds in any event loop).
- **Error handling:** Network errors in loops are logged and swallowed so a
transient platform hiccup does not take a remote agent offline. API-level
errors (4xx) propagate via `raise_for_status()`.
- **Token security:** Auth token cached at `~/.molecule/<workspace_id>/.auth_token`
with `0600` permissions.
- **Full documentation:** See `CLAUDE.md` for architecture, platform API
endpoints, SDK conventions, and known issues.
+7 -4
View File
@@ -54,8 +54,11 @@ secrets = client.pull_secrets()
client.install_plugin("molecule-dev")
client.install_plugin("my-plugin", source="github://acme/my-plugin")
# 4. Run the heartbeat + state-poll loop until the platform pauses/deletes us.
terminal = client.run_heartbeat_loop()
# 4. Run the heartbeat + state-poll loop until the platform pauses/deletes us
# or until stop_event.set() is called from another thread.
import threading
stop = threading.Event()
terminal = client.run_heartbeat_loop(stop_event=stop)
print(f"loop exited: {terminal}")
```
@@ -75,8 +78,8 @@ A runnable demo with full setup walkthrough lives at
| `call_peer(target, message)` | 30.6 | Direct A2A with proxy fallback; response may be wrapped in OFFSEC-003 boundary markers — use ``strip_a2a_boundary()`` to remove them |
| `fetch_inbound(since_id=…)` | 30.8c | One-shot poll of `/workspaces/:id/activity` for inbound A2A |
| `reply(msg, text)` | 30.8c | Smart-routes reply to `/notify` (canvas user) or `/a2a` (peer) |
| `run_heartbeat_loop()` | combo | Drives heartbeat + state-poll on a timer; exits on pause/delete |
| `run_agent_loop(handler)` | combo | Heartbeat + state + **inbound dispatch**; exits on pause/delete |
| `run_heartbeat_loop(stop_event=None)` | combo | Drives heartbeat + state-poll on a timer; exits on pause/delete or when `stop_event.set()` is called from another thread (KI-009) |
| `run_agent_loop(handler, stop_event=None)` | combo | Heartbeat + state + **inbound dispatch**; exits on pause/delete or when `stop_event.set()` is called from another thread (KI-009) |
## Inbound delivery — push vs poll
+3 -1
View File
@@ -9,6 +9,7 @@ and detect pause/resume/delete — all via the Phase 30.130.5 HTTP contract.
Intended usage::
import threading
from molecule_agent import RemoteAgentClient
client = RemoteAgentClient(
@@ -18,7 +19,8 @@ Intended usage::
)
client.register() # mints + persists the auth token
env = client.pull_secrets() # decrypted secrets dict
client.run_heartbeat_loop() # background heartbeat + state-poll
stop = threading.Event()
client.run_heartbeat_loop(stop_event=stop) # background heartbeat; stop.set() to exit cleanly
See ``sdk/python/examples/remote-agent/`` for a runnable demo.