Cookbook

Patterns plugin authors reach for, ready to copy-paste into plugin.py.

Last updated: 19 مايو 2026

Patterns we have seen plugin authors reach for. Each recipe is copy-pasteable into plugin.py.

1. HITL prompt — pause for human approval

Sometimes an agent must pause until a human approves a side-effect (send email, push to prod, charge a card). Use the host’s HITL channel:

from dryade_plugins_sdk import tool


@tool(
    name="propose_email_send",
    description="Draft an email and pause for human approval before sending.",
)
async def propose_email_send(*, to: str, subject: str, body: str) -> dict:
    from dryade_plugins_sdk.channels import await_human_approval  # host-injected

    decision = await await_human_approval(
        prompt=f"Send '{subject}' to {to}?",
        body=body,
        ttl_seconds=600,
    )
    if decision != "approve":
        return {"status": "rejected", "decision": decision}
    # ... actually send ...
    return {"status": "sent"}

The host renders the prompt in the workbench HITL panel; the function suspends until the user clicks approve / reject / extend. Default TTL is configurable via DRYADE_HITL_TTL_S.

2. MCP tool — expose your plugin’s tool to MCP clients

If your plugin declares an mcp_server block in dryade.json, the host spawns the MCP server subprocess and proxies tool calls:

{
  "mcp_server": {
    "name": "my-mcp-server",
    "command": ["python", "-m", "my_plugin.mcp_server"],
    "args": [],
    "env": {"LOG_LEVEL": "info"}
  }
}

The command runs inside the plugin’s Leash sandbox. Stdin/stdout speak the MCP JSON-RPC protocol. See the official MCP spec for the on-wire shape.

3. Multi-agent collaboration

See examples/multi_agent. The pattern: namespace KV keys by plugin name (<plugin>:<key>), so cross-plugin collisions are impossible.

4. Persistent KV — TTL-aware caching

class CachingPlugin:
    name = "caching"
    version = "0.1.0"
    description = "Caches expensive computation in plugin-local KV."
    core_version_constraint = ">=1.0.0,<2.0.0"

    def __init__(self):
        self._kv = None

    def register(self, registry): pass

    def startup(self, **kwargs):
        self._kv = kwargs.get("kv")

    @tool(name="expensive_op", description="Cached compute.")
    def expensive_op(self, key: str) -> dict:
        cached = self._kv.get(f"caching:result:{key}")
        if cached is not None:
            return cached
        result = self._compute(key)
        self._kv.set(f"caching:result:{key}", result, ttl_seconds=3600)
        return result

5. UI component — react to backend state

The UI half of a plugin (has_ui: true) is a React bundle the workbench mounts. The bundle fetches from /api/plugins/<your_plugin>/... to talk to your backend. Authentication / theming / toasts come from a peer @dryade/workbench-sdk package — your bundle does not bundle React.

See examples/with_ui.

6. Error boundary — fail-closed inside a tool

The Plugin Protocol does not give you a try/except wrapper at the tool layer — the host catches AgentExecutionError. If your tool can fail in recoverable ways, swallow at the source:

@tool(name="fetch_url", description="GET a URL, surface errors as data.")
def fetch_url(url: str) -> dict:
    import requests
    try:
        resp = requests.get(url, timeout=5)
        resp.raise_for_status()
        return {"ok": True, "data": resp.text}
    except requests.RequestException as e:
        return {"ok": False, "error": str(e)}

Returning {"ok": False, ...} lets the orchestrator make a decision; a raised exception triggers retry/abort policy at the host.

7. Health check — surface plugin-internal liveness

from dryade_plugins_sdk import HealthCheck


def _db_ping() -> dict:
    # ... your DB ping ...
    return {"latency_ms": 12}


class WithHealth:
    name = "with_health"
    # ... required Plugin fields ...
    def get_health_checks(self):
        return {
            "db_reachable": HealthCheck(
                name="db_reachable",
                category="critical",
                check_fn=_db_ping,
                description="Plugin DB ping",
                timeout_seconds=2.0,
            ),
        }

The host polls these on its own schedule, surfaces results in the workbench, and routes to alerting if category=critical fails.