原始内容
weir
weir is a single-binary CLI agent orchestrator that drives a fleet of local CLI agents through one unified command. It spawns CLIs — it is neither an HTTP client nor an HTTP server, and it handles no API keys.
weir ──spawns──▶ hermes (local / OpenRouter)
──▶ claude (Claude Code CLI)
──▶ gemini (Gemini CLI)
──▶ ollama run (local models)
You drive it from the shell (weir chat, weir workflow run) or from Claude
Code via the bundled /weir skill, which calls the same CLI.
Why weir?
| weir | Python wrappers | Go gateways | |
|---|---|---|---|
| Distribution | single binary, zero deps | virtualenv / uv | binary + config |
| Binary size | ~1.6 MB | ~50–200 MB | ~10–20 MB |
| LLM-native workflows | fan-out, pipeline, eval-loop, fusion | none | none |
| API keys in config | none ever (CLI agents own auth) | often inline | varies |
| Interface | single CLI (scriptable, skill-friendly) | varies | varies |
Every command reads the same weir.toml and supports all backends and workflows.
Install
Pre-built binary (coming soon)
curl -fsSL https://github.com/typangaa/otterbridge/releases/latest/download/weir-linux-x86_64 \
-o ~/.local/bin/weir && chmod +x ~/.local/bin/weir
Build from source
git clone https://github.com/typangaa/otterbridge
cd otterbridge
cargo build --release
cp target/release/weir ~/.local/bin/weir # or any directory in $PATH
Requires Rust 1.75+. No other system dependencies.
Quick start
1. Create a config:
mkdir -p ~/.config/weir
cp weir.example.toml ~/.config/weir/weir.toml
2. Make sure any CLI agents you reference are installed and logged in.
weir handles no API keys and makes no network calls of its own. Every backend
is a CLI agent (hermes, claude, agy, gemini, ollama run, …) that you have
already installed and logged in — each owns its own auth and network access.
3. Validate:
weir validate --config ~/.config/weir/weir.toml --json
# {"backend_count":3,"status":"ok","workflow_count":2}
4. Use as a Claude Code skill (optional):
Copy the skill file:
mkdir -p ~/.claude/skills/weir
cp skill/SKILL.md ~/.claude/skills/weir/SKILL.md
Then invoke with /weir in Claude Code, or Claude will use it automatically
when you ask to "chat with agy", "ask hermes", "fan-out to all backends", etc.
The skill simply calls the weir CLI from the shell.
CLI usage
The quickest way to call a backend or run a workflow:
# Chat with any backend
weir --config ~/.config/weir/weir.toml chat agy "Explain Rust's borrow checker"
weir --config ~/.config/weir/weir.toml chat hermes "Summarise: $(cat notes.txt)"
# Pipe from stdin
cat long_doc.txt | weir --config ~/.config/weir/weir.toml chat agy -
# With system message
weir --config ~/.config/weir/weir.toml chat agy \
--system "You are a terse code reviewer." \
"Review: $(cat src/main.rs)"
# Machine-readable JSON
weir --config ~/.config/weir/weir.toml --json chat agy "What is 2+2?"
# → {"backend":"agy","content":"4\n"}
# Run a fan-out workflow (parallel responses from multiple backends)
weir --config ~/.config/weir/weir.toml workflow run dual-review \
"What are the trade-offs of async Rust?"
# Run a pipeline workflow (sequential, each step refines the previous)
weir --config ~/.config/weir/weir.toml workflow run draft-then-polish \
"Write a README for a CLI tool"
# Run an eval-loop workflow (iterate until criteria met)
weir --config ~/.config/weir/weir.toml workflow run quality-loop \
--criteria "Must be under 50 words and cite a specific Rust feature" \
"Describe what makes Rust unique"
# JSON output for any workflow
weir --config ~/.config/weir/weir.toml --json workflow run dual-review "PROMPT"
# Override which backends a workflow uses, at call time (no flag = weir.toml default)
weir --config ~/.config/weir/weir.toml workflow run dual-review \
--backend agy --backend hermes-local "..." # replace the fan-out list
weir --config ~/.config/weir/weir.toml workflow run deep-review \
--judge claude-code --synthesizer agy "..." # swap fusion roles
Per-pattern override flags on workflow run (each flag fully replaces that slot;
referenced backends must exist in weir.toml):
| Flag | Overrides | Pattern(s) |
|---|---|---|
--backend NAME (repeat) |
backend list | fan-out / router / fusion panel |
--step BACKEND[:TEMPLATE] (repeat) |
pipeline steps | pipeline |
--generator / --evaluator |
loop roles | eval-loop |
--judge / --synthesizer |
fusion roles | fusion |
Fan-out JSON output:
{
"workflow": "dual-review",
"pattern": "fan-out",
"results": [
{"backend": "agy", "content": "..."},
{"backend": "hermes", "content": "..."}
]
}
Configuration (weir.toml)
TOML is the single source of truth. See weir.example.toml for a
fully annotated example.
[[backend]] — stdio CLI (the only backend type)
[[backend]]
name = "hermes"
type = "stdio-cli"
command = "hermes"
args = ["-z", "{prompt}"] # {prompt} is replaced at call time
timeout_secs = 180
The process is spawned, stdout captured as the response, then exits. Works with
any CLI agent that supports a oneshot / headless mode (hermes -z, agy -p,
llamafile --oneshot, etc.).
[[workflow]] — fan-out
[[workflow]]
name = "multi-review"
pattern = "fan-out"
backends = ["ollama", "hermes"]
aggregation = "all"
All backends called in parallel. Returns an array of responses.
[[workflow]] — pipeline
[[workflow]]
name = "draft-then-polish"
pattern = "pipeline"
[[workflow.steps]]
backend = "ollama"
role = "drafter"
[[workflow.steps]]
backend = "hermes"
role = "polisher"
prompt_template = "Refine this draft:\n\n{{step.output}}"
Each step receives the previous step's output. Use {{step.output}} in
prompt_template to inject it.
[[workflow]] — eval-loop
[[workflow]]
name = "quality-loop"
pattern = "eval-loop"
generator = "ollama"
evaluator = "hermes"
max_iterations = 5
Generator produces a response; evaluator scores it against caller-supplied
criteria. Loops until evaluator says PASS or max_iterations is reached.
[[workflow]] — router
[[workflow]]
name = "fast-path"
pattern = "router"
backends = ["ollama"]
Explicit single-backend dispatch. Useful for aliasing backends by role.
Full CLI reference
All commands accept --json for machine-readable output and
--config PATH to override the default weir.toml.
weir [--config PATH] [--json] [--log-level LEVEL] [--log-format pretty|json] <COMMAND>
Inference commands:
chat <BACKEND> <PROMPT> Call a backend directly, print response
chat <BACKEND> - Read prompt from stdin
chat <BACKEND> [--system MSG] Prepend a system message
chat <BACKEND> [--max-tokens N] Cap generation length
workflow run <NAME> <PROMPT> Run any workflow (fan-out/pipeline/router/eval-loop/fusion)
workflow run <NAME> --criteria C Criteria for eval-loop workflows
workflow run <NAME> --backend B Override the backend list (repeat); also
--step/--generator/--evaluator/--judge/--synthesizer
Config management:
validate Validate weir.toml and exit
backend list List configured backends
backend test <NAME> Check backend connectivity
backend add cli <NAME> \
--command CMD [--arg ARG]... Add a stdio-CLI backend
backend remove <NAME> Remove a backend
workflow list List configured workflows
workflow add fanout <NAME> \
--backend B... [--aggregation all]
workflow add pipeline <NAME> \
--step B[:TEMPLATE]...
workflow remove <NAME>
status Print config summary
version Print version info
schema Print JSON Schema for weir.toml
Observability
Logging — structured logs to stderr (stdout stays clean for command output). Pretty format by default; JSON for machine parsing.
weir --log-format json --log-level debug chat agy "..." # JSON logs
RUST_LOG=weir=debug weir chat agy "..." # filter to weir only
Metrics — per-backend counters persisted across invocations:
weir status --json
Security
- weir handles no API keys. There is no key/auth field of any kind in
weir.toml. Every backend is astdio-cliagent that owns its own credentials. weir never reads, stores, or forwards a secret. - No network surface. weir is not an HTTP client (it spawns CLIs, it does not
call
/v1/chat/completions) and not an HTTP server (it opens no port and listens on no socket).
Architecture
weir.toml (TOML source of truth)
│
└─── CLI (clap) ──▶ weir chat / weir workflow run / weir backend / …
│
▼ one-shot Config::load → validate (syntactic → semantic → resilience)
│
▼
Engine ──▶ Backend::chat() (wrapped by ResilientBackend:
└── StdioCliBackend retry → rate-limit → breaker)
(tokio::process oneshot, stdin=Stdio::null())
Engines:
fan_out → tokio JoinSet (parallel)
pipeline → sequential chain with {{step.output}} template substitution
router → explicit single backend
eval_loop → generator ↔ evaluator loop until PASS / max_iterations
fusion → panel fan-out → judge JSON analysis → synthesizer
Codebase layout
src/
├── main.rs # clap CLI, dispatch, run_chat, run_workflow
├── error.rs # WeirError + Result<T>
├── config/
│ ├── mod.rs # Config, BackendConfig, WorkflowConfig (serde)
│ └── validate.rs # 3-layer validation (syntactic → semantic → resilience)
├── backends/
│ ├── mod.rs # Backend trait, ChatRequest/Response
│ └── stdio_cli.rs # tokio::process oneshot (stdin=Stdio::null()) — only backend
├── engine/
│ ├── fan_out.rs # parallel JoinSet
│ ├── pipeline.rs # sequential + template substitution
│ ├── router.rs # explicit dispatch
│ ├── eval_loop.rs # gen ↔ eval loop
│ └── fusion.rs # panel → judge → synthesizer
├── resilience/
│ ├── circuit_breaker.rs # half-open state machine (wired v0.2)
│ ├── retry.rs # exp backoff + deterministic jitter (wired v0.2)
│ ├── rate_limit.rs # token bucket (wired v0.2)
│ └── resilient_backend.rs # decorator wrapping every backend call
├── cli/
│ ├── backend.rs # backend subcommands (toml_edit write-back)
│ ├── workflow.rs # workflow subcommands
│ ├── validate.rs # `weir validate`
│ └── status.rs # version, schema
└── observability/
├── metrics.rs # per-backend AtomicU64 counters (wired v0.2)
├── persist.rs # metrics snapshot → ~/.local/state/weir/metrics.json
└── tracing_setup.rs # tracing-subscriber init
Development
cargo test # run all 71 tests
cargo fmt --all --check # formatting (default rustfmt)
cargo clippy --all-targets -- -D warnings # lint (zero-warning policy)
cargo build --release # ~1.6 MB binary
./target/release/weir validate --config weir.example.toml --json
All four gates (fmt / clippy / test / release build) run in CI on every push
and PR — see .github/workflows/ci.yml.
Roadmap
- v0.1 — Core backends + fan-out / pipeline / router / eval-loop;
weir chat/weir workflow run;backend/workflowwrite-back; Claude Code skill - v0.2 — Resilience (retry + circuit breaker + rate limiter via
ResilientBackend); per-backend metrics persisted to disk +weir status - v0.3 — Narrowed to a pure stdio-cli orchestrator: removed the openai-compat HTTP
client (and the
reqwest/TLS deps → ~2.5 MB binary) and all API-key handling - v0.4 — Removed the MCP server and the hot-reload config layer (dropped rmcp/schemars/arc-swap/notify → ~1.6 MB binary); weir is now a focused single-binary CLI agent orchestrator (engine unit tests + CI added)
- v0.4.1 — Hardening: end-to-end CLI integration tests (
tests/cli.rs), pipeline template token aligned on{{step.output}}, and stale openai-compat/MCP references and dead code removed - v0.5.0 — Call-time backend overrides for
workflow run(--backend/--step/--generator/--evaluator/--judge/--synthesizer): choose which CLIs a workflow uses per call without editingweir.toml - v1.0 — Stable config schema; backwards-compatibility guarantee
Non-goals: weir will not become an HTTP client (/v1/chat/completions), an
HTTP server, or an MCP server. It spawns local agent CLIs and nothing else. Wrap
HTTP-only model servers in a CLI (e.g. ollama run) instead.
Legacy Python v1
The original Python FastMCP server is preserved in legacy/.
License
Apache-2.0. See LICENSE.
Contributing
Issues and pull requests welcome at github.com/typangaa/otterbridge.
One feature or fix per PR. All new code must include unit tests.
Run cargo test and cargo clippy before opening a PR.