otter-bridge

内容来源:README.md(说明文档) · 原始地址 · 查看安装指南

原始内容

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 a stdio-cli agent 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/workflow write-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 editing weir.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.