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 bothworkflow.tasksandworkflow.groups[].tasksshapes used in the cookbook).osmo_workflow_render- Jinja-renders a templated YAML so you can preview what--set k=vwill 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": {...}}}
],
}
textis for the LLM to read directly.jsonis structured data the LLM can reference and pass to other tools.- On error,
textstarts 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.