External agents that can't expose a public HTTP endpoint (laptops behind NAT, ephemeral CI runners, hermes self-hosted, codex et al) had to reverse- engineer the activity-poll loop from molecule-mcp-claude-channel/server.ts because the SDK only shipped the push-mode `A2AServer` (Phase 30.8b). This adds the complementary path: - `RemoteAgentClient.fetch_inbound(since_id=…)` — one-shot GET against `/workspaces/:id/activity?type=a2a_receive&since_id=…`. Cursor-loss (410) surfaces as `CursorLostError`; caller resets and re-polls. - `RemoteAgentClient.reply(msg, text)` — smart-routes to `/notify` for canvas users, `/a2a` (JSON-RPC envelope + X-Source-Workspace-Id) for peer agents. Hides the reply-path bifurcation from connector authors. - `PollDelivery` / `PushDelivery` / `InboundDelivery` protocol — same `MessageHandler` callback works for both transports. - `RemoteAgentClient.run_agent_loop(handler, delivery=None)` — combined heartbeat + state-poll + inbound dispatch. Defaults to `PollDelivery`. Async handlers detected and `asyncio.run`'d (matches A2AServer pattern). Sleep cadence = min(heartbeat_interval, delivery.interval). - `python -m molecule_agent connect` CLI — one-line bootstrap. Loads a user's `module:function` via importlib, registers, runs the loop until pause/delete or SIGTERM. All flags also read from environment variables. Tests: 50 new (test_inbound.py, test_cli_connect.py) covering every prod branch — source normalization, cursor advancement, 410 reset, async/sync handler dispatch, handler exception → log+continue+advance, smart-reply routing for canvas vs peer vs unknown sources, run_agent_loop terminal states, sleep-interval selection, CLI handler resolution failures. Resolves #17.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""Molecule AI remote-agent SDK — build agents that run outside the platform
|
||
network and register as first-class workspaces.
|
||
|
||
This is the Phase 30.8 companion to ``molecule_plugin`` (for plugin authors).
|
||
Where ``molecule_plugin`` helps you ship installable behavior for workspaces
|
||
that already exist, ``molecule_agent`` helps you *be* a workspace from the
|
||
other side of the wire: register, authenticate, pull secrets, heartbeat,
|
||
and detect pause/resume/delete — all via the Phase 30.1–30.5 HTTP contract.
|
||
|
||
Intended usage::
|
||
|
||
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 + persists the auth token
|
||
env = client.pull_secrets() # decrypted secrets dict
|
||
client.run_heartbeat_loop() # background heartbeat + state-poll
|
||
|
||
See ``sdk/python/examples/remote-agent/`` for a runnable demo.
|
||
|
||
Design notes:
|
||
* **No async.** The SDK uses blocking ``requests`` so a remote agent author
|
||
can embed it in any event loop / thread / script without forcing anyio.
|
||
* **Token cached on disk** at ``~/.molecule/<workspace_id>/.auth_token``
|
||
with 0600 permissions, so a restart of the agent doesn't re-issue a
|
||
token (the platform refuses to issue a second token when one is on file).
|
||
* **Pause/delete detection is polling-based** because remote agents usually
|
||
can't expose an inbound WebSocket reachable from the platform.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from .a2a_server import A2AServer
|
||
from .client import (
|
||
PeerInfo,
|
||
RemoteAgentClient,
|
||
WorkspaceState,
|
||
verify_plugin_sha256,
|
||
)
|
||
from .inbound import (
|
||
CursorLostError,
|
||
DEFAULT_POLL_INTERVAL,
|
||
InboundDelivery,
|
||
InboundMessage,
|
||
InboundSource,
|
||
MessageHandler,
|
||
PollDelivery,
|
||
PushDelivery,
|
||
)
|
||
|
||
# compute_plugin_sha256 lives in __main__ (the CLI entry point).
|
||
# Import it here so `from molecule_agent import compute_plugin_sha256` works.
|
||
from .__main__ import compute_plugin_sha256
|
||
|
||
__all__ = [
|
||
"A2AServer",
|
||
"RemoteAgentClient",
|
||
"WorkspaceState",
|
||
"PeerInfo",
|
||
"InboundMessage",
|
||
"InboundSource",
|
||
"InboundDelivery",
|
||
"PollDelivery",
|
||
"PushDelivery",
|
||
"MessageHandler",
|
||
"CursorLostError",
|
||
"DEFAULT_POLL_INTERVAL",
|
||
"compute_plugin_sha256",
|
||
"verify_plugin_sha256",
|
||
"__version__",
|
||
]
|
||
__version__ = "0.1.0"
|