Skip to content

Architecture

strands-osmo is intentionally minimal. Every tool follows the same recipe:

sequenceDiagram
    participant A as Strands Agent
    participant T as @tool wrapper
    participant C as _common.osmo_run
    participant O as osmo CLI
    participant S as OSMO Server

    A->>T: osmo_pool_list(mode="free")
    T->>C: osmo_run("pool", "list", "--mode", "free", parse_json=True)
    C->>O: subprocess: osmo pool list ...
    O->>S: HTTPS + JWT
    S-->>O: JSON response
    O-->>C: stdout (JSON)
    C-->>T: {"ok": true, "data": {...}}
    T-->>A: ToolResult{status: success, content: [text, json]}

Layers

1. _common.py

The only file that calls subprocess.run. Provides:

  • osmo_run(*args, ...) - shells out, captures, parses, returns a normalized dict.
  • proc_result(proc, ...) - converts that dict into a Strands ToolResult.
  • ok(...) / err(...) - direct ToolResult builders for tools that don't shell out (e.g. osmo_workflow_validate).
  • osmo_available() - fast existence check used by the doctor.

Environment knobs:

Variable Default Purpose
STRANDS_OSMO_BIN osmo Override the binary name / path.
OSMO_HOST - Forwarded to osmo for non-default servers
OSMO_PROFILE - Forwarded for profile selection.
OSMO_TOKEN - Long-lived access token for headless agents

2. Per-CLI-command tool files

Each file is 30–80 lines and matches one OSMO subcommand. Pattern:

@tool
def osmo_pool_list(mode: str = "used") -> dict:
    args = ["pool", "list"]
    if mode != "used":
        args += ["--mode", mode]
    args += ["--format-type", "json"]
    proc = osmo_run(*args, parse_json=True, timeout_s=30)
    return proc_result(proc, success_text=f"pool list (mode={mode})")

The @tool decorator (from strands) registers the function and infers the schema from the type hints + docstring. There's no business logic - that's deliberately the agent's job.

3. Local helpers

Three tools don't talk to OSMO at all:

  • osmo_workflow_validate - parses YAML, checks required keys (handles both workflow.tasks and workflow.groups[].tasks shapes used in the cookbook).
  • osmo_workflow_render - Jinja-renders a templated YAML so you can preview what --set k=v will produce.
  • osmo_cookbook_fetch - pulls a recipe from a local OSMO checkout or GitHub raw.

These let an agent prep a workflow before spending a quota slot.

ToolResult shape

Every tool returns:

{
    "status": "success" | "error",
    "content": [
        {"text": "human-readable summary"},
        {"json": {"cmd": "...", "returncode": 0, "parsed": {...}}}
    ],
}
  • text is for the LLM to read directly.
  • json is structured data the LLM can reference and pass to other tools.
  • On error, text starts with and includes a stderr tail.

Why no OsmoModel class?

OSMO orchestrates compute; it doesn't ship a model. Compare with strands-cosmos, which provides CosmosVisionModel because Cosmos does ship a VLM. Adding a model class to strands-osmo would be incoherent - you'd be modeling "the OSMO control plane is a generative LLM," which it isn't.

Why CLI instead of OSMO's Python SDK?

OSMO has internal Python modules (src/lib/utils/, src/cli/) but they aren't published, aren't versioned for external use, and change between releases. The osmo binary is the public, stable contract. Wrapping it keeps strands-osmo version-tolerant and lets operators run the same commands by hand.