Rust-based automation tool for AI-assisted task execution. Daemon/client architecture: long-running daemon executes scenarios (action chains) via Unix socket IPC.
- Daemon: Unix socket server on
$HOME/rhd.sock(default), acceptsRunScenariorequests - Client: Connects to daemon, sends scenario name, receives output
- Scenario executor: Runs action chains sequentially with placeholder resolution
- IPC protocol: rkyv serialization with version-prefixed framing
cargo build --releaseBinary: target/release/rhd
Save as ~/.local/bin/rhd (or any directory in $PATH):
#!/bin/bash
cargo build --release --manifest-path ~/projects/rhd/Cargo.toml > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Build failed" >&2
exit 1
fi
exec ~/projects/rhd/target/release/rhd "$@"Make executable:
chmod +x ~/.local/bin/rhdrhd daemon [OPTIONS]| Flag | Default | Description |
|---|---|---|
--config PATH |
rhd.yaml |
Path to configuration file |
--models-dir PATH |
models |
Directory with model YAML files (overrides config) |
--scenarios-dir PATH |
scenarios |
Directory with scenario folders (overrides config) |
--mcp-dir PATH |
mcp |
Directory with MCP server configurations (overrides config) |
--default-model NAME |
— | Fallback model for aiChat steps without model field (overrides config) |
--logs PATH |
— | Directory for execution logs (overrides config) |
--socket PATH |
$HOME/rhd.sock |
Unix socket path |
--ws-port PORT |
— | WebSocket server port for Web UI (optional) |
--db-dir DIR |
rhd_db |
Directory for SQLite database (overrides config) |
The daemon can be configured via a YAML file (default: rhd.yaml in current directory). CLI arguments override config file values.
Config file format (rhd.yaml):
modelsDir: models
scenariosDir: scenarios
mcpDir: mcp
defaultModel: null
logs: null
wsPort: null
dbDir: rhd_db
credentialsConfig: null| Field | Default | Description |
|---|---|---|
modelsDir |
models |
Directory containing model YAML files |
scenariosDir |
scenarios |
Directory containing scenario folders |
mcpDir |
mcp |
Directory containing MCP server configurations |
defaultModel |
null |
Fallback model for aiChat steps without model field |
logs |
null |
Directory for execution logs (no logging if null) |
wsPort |
null |
WebSocket server port (disabled if null) |
dbDir |
rhd_db |
Directory for SQLite database |
credentialsConfig |
null |
Path to credentials file (relative to config file) |
When logs is configured, each scenario execution creates a timestamped log directory:
- Format:
<logs>/<scenarioName>-YYYY-MM-DD-HH-MM-SS/ - Collision handling: If directory exists, appends
-2,-3, etc. - Log file:
log.txtinside the directory
rhd run <scenario_name> [--socket PATH] [--modelAlias ALIAS=TARGET]| Flag | Description |
|---|---|
--socket PATH |
Unix socket path (default: $HOME/rhd.sock) |
--modelAlias ALIAS=TARGET |
Override model alias at runtime (can be repeated) |
Client captures current working directory and sends it to daemon. Commands execute in client's cwd unless scenario overrides with cwd field.
Model alias override example:
rhd run example --modelAlias medium=gpt4 --modelAlias small=qwen3Model configs: models/<name>.yaml. Filename (without extension) becomes model name.
| Field | Required | Description |
|---|---|---|
baseUrl |
yes | OpenAI-compatible API endpoint. Supports $ENV_VAR. |
apiKey |
yes | API key. Plain string, $ENV_VAR, or { cred: keyName }. |
model |
yes | Model identifier passed to API. Supports $ENV_VAR. |
inputTokenPrice |
no | Price per 1M input tokens |
outputTokenPrice |
no | Price per 1M output tokens |
priceTiers |
no | Tiered pricing (see below) |
Example models/deepseek.yaml:
baseUrl: "https://api.deepseek.com/v1"
apiKey: "$DEEPSEEK_API_KEY"
model: "deepseek-chat"Optional fields for cost tracking:
inputTokenPrice: 5.0 # Price per 1M input tokens
outputTokenPrice: 15.0 # Price per 1M output tokens
priceTiers: # Optional tiered pricing
- afterTokens: 250000 # Apply after this many tokens
inputTokenPrice: 10.0
outputTokenPrice: 30.0A model config can reference another model:
# models/medium.yaml
alias: gpt4Filename becomes alias name. Resolves to target model at runtime. Aliases can chain.
Separate API keys from config for safe sharing:
credentials.yaml:
openaiKey: "sk-..."
anthropicKey: "sk-ant-..."rhd.yaml:
credentialsConfig: ./credentials.yamlmodels/gpt4.yaml:
baseUrl: "https://api.openai.com/v1"
apiKey:
cred: openaiKey
model: "gpt-4"Add credentials.yaml to .gitignore.
Optional WebSocket server for Web UI integration. Enable via --ws-port or wsPort config.
Default disabled. Used by frontend for real-time scenario monitoring.
Scenarios: scenarios/<name>/scenario.yaml. Directory name is scenario identifier.
| Field | Required | Description |
|---|---|---|
description |
no | Free text description |
actions |
yes | Ordered list of actions |
Execute shell command. Never fails scenario on non-zero exit.
| Field | Required | Description |
|---|---|---|
type |
yes | runCommand |
name |
no | Step name for placeholder references |
cmd |
yes | Executable path or command. Supports $ENV_VAR. |
args |
no | Argument list. Default []. Each arg supports $ENV_VAR. |
cwd |
no | Working directory. Default: client's cwd. Supports $ENV_VAR. |
Send message to AI model, receive response. Supports tool calling via MCP (Model Context Protocol).
| Field | Required | Description |
|---|---|---|
type |
yes | aiChat |
name |
no | Step name for placeholder references |
model |
no | Model name (from models/*.yaml). Falls back to --default-model. Supports $ENV_VAR. |
systemPrompt |
no | System prompt. Supports $ENV_VAR and %placeholder%. |
message |
yes | User message. Supports $ENV_VAR and %placeholder%. |
mcp |
no | List of MCP server references for tool calling. See MCP Tools. |
maxToolIterations |
no | Max tool call loop iterations. Default 20, set "inf" for unlimited. |
skip |
no | Skip condition based on flag. See Skip Conditions. |
Return final text. Resolves placeholders in template.
| Field | Required | Description |
|---|---|---|
type |
yes | output |
name |
no | Step name |
output |
yes | Text template with %placeholder% references |
Reference previous step results: %stepName.field%.
| Field | Source | Description |
|---|---|---|
%step.exitCode% |
runCommand |
Exit code (integer) |
%step.stdout% |
runCommand |
Standard output |
%step.stderr% |
runCommand |
Standard error |
%step.stdoutStderr% |
runCommand |
Concatenated stdout+stderr on success; empty on failure |
%step.success% |
runCommand |
true if exit code 0, false otherwise |
%step.cwd% |
runCommand |
Resolved working directory |
%step.message% |
aiChat |
Assistant reply text |
%step.flag_name% |
aiChat |
Flag value set by rhd_set_flag tool (true or false) |
Missing placeholders resolve to empty string (no error).
The aiChat action supports Model Context Protocol (MCP) for tool usage. When mcp field is present, the model can call tools in a loop until it responds without tool calls.
- type: aiChat
name: ai_step
model: model_name
mcp:
- name: fs # Reference to mcp/fs/mcp.yaml
args: ["--extra-arg"] # Optional override
env:
AVAILABLE_ROOT: /tmp # Optional env vars
- name: flags # Built-in tools
maxToolIterations: 20 # Optional, default 20, "inf" for unlimited
systemPrompt: "Optional system prompt"
message: "User message"Behavior:
- Without
mcpfield: Single-shot mode (current behavior) - With
mcpfield: Tool loop mode - model can call tools, results fed back, loop untilfinish_reason: "stop" - Max iterations guard prevents infinite loops (configurable per step)
Built-in Tools:
rhd_set_flag: Sets a flag that can be used for conditional step execution{"name": "flag_name", "value": true}
MCP Configuration (mcp/<name>/mcp.yaml):
cmd: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "$AVAILABLE_ROOT"]
cwd: nullMCP Server Lifecycle:
- Spawned on first use per (mcp_name, scenario_id, step_id)
- Cached at daemon level, reused across scenario executions
- Killed only on daemon shutdown
Steps can be conditionally skipped based on flags set by rhd_set_flag:
- type: runCommand
name: build
cmd: make
skip: ai_step.flag_skip_build # Skip if flag is trueFlag Format: <aiChatStepName>.flag_<flagName>
- Flags are stored in
ExecutionContext - Accessible to all subsequent steps
- If flag is
true→ skip step, iffalse/absent → execute - Applies to all step types except
output
All string fields support $VAR_NAME syntax (alphanumeric + underscore). Replaced at load time with environment variable value. If variable not set, original $VAR_NAME string kept as-is.
description: Just a test
actions:
- type: runCommand
name: cmd
cmd: ./echo.sh
args:
- '21'
- type: aiChat
name: ai
model: deepseek_v4_flash
systemPrompt: >-
You will receive number. Multiply it by 2 and respond only with result,
append short funny description to this number.
For example: "4 — looks definitely random!"
message: "%cmd.stdout%"
- type: aiChat
name: ai2
model: deepseek_v4_flash
systemPrompt: >-
You will receive number and short description of it. Translate it in Russian,
then add one paragraph of text about it.
message: "%ai.message%"
- type: output
name: out
output: "Result: %ai2.message%"Run:
rhd run example- runCommand never fails scenario: Non-zero exit codes captured in
%step.exitCode%and%step.success%, but execution continues. - Missing placeholders: Resolve to empty string, not errors.
- CWD propagation:
rhd runsends its cwd to daemon. Commands execute in client's cwd unless scenariocwdfield overrides. - Socket cleanup: Daemon removes stale socket file on startup.
- Graceful shutdown: Daemon handles SIGTERM/SIGINT for clean shutdown.
- Model validation: Daemon validates all models at startup, exits if invalid.