agent-ix 开源 Sapho:用 System One 模型和自写规则做复合决策的 Rust 库与 CLI
agent-ix/sapho
agent-ix 发布 Sapho,一个用 System One 模型(如 Jev)和自写规则做复合决策的 Rust 库与命令行工具,把决策拆成多个简单问题、逐个询问模型,再用 and、or、min、max 等逻辑算子组合答案。
Make compound decisions with System One models and rules you write.
Sapho is a Rust library and command-line tool for making compound decisions with System One models such as Jev. System One models cut inference time and cost significantly, and they are fast enough to run on an M5 laptop.
Sapho splits a decision into many simple questions, asks the model each one, and combines the answers using rules you write. Rules use logic operators such as and, or, min and max, and you can build larger rules out of smaller ones.
Write your questions and rules as a decision graph in YAML or JSON. Run them from the command line, or use Sapho in a Rust application. Keep the answers and intermediate results so you can see how a decision was made, measure it against labelled examples, and improve the rule.
CLI setup · Rust integration · Claude Code · Documentation
How it works
Suppose you want to decide whether a code change needs closer review. Break that decision into questions:
- Did the public API change?
- Could the change break an existing contract?
- Are important cases missing from the tests?
Your code can answer the first question. A model can help answer the other two. Then a rule combines those answers. For example, you could use this policy:
needs_review = public_api_changed AND
(contract_risk >= 0.7 OR test_gap >= 0.8)
You choose the questions, the meaning of each answer, and the thresholds. Sapho runs the steps and returns the decision with a trace of the work that produced it.
Here is that rule running on a real change, one that shortens a default timeout from 30 seconds to 5:
- Ask small questions. Sapho sends the change and both questions to Jev in one request. Jev does not write an explanation. For each question it returns how likely each answer is: 0.79 that the change could break a caller, and 0.61 that its tests miss new behavior.
- Apply your rule. Each probability is compared with its threshold.
0.79 clears 0.7; 0.61 misses 0.8.
orandandjoin those results with the fact your code supplied. - Get the decision and its trace.
needs_reviewis true. The trace keeps the request, Jev's answers and every intermediate value, so you can see which question decided it.
Because the answers are numbers, changing a threshold changes the decision without asking the model again. That is what lets you measure and tune a rule against labelled examples cheaply. How Sapho works covers answer types, combining strengths, layered questions, guards and collections.
Build decisions in layers
A later question can use an earlier answer, and an expensive check can run only when it is needed. This requirement check makes up to three model calls for each statement:
Show the whole diagram
- Layer 1 asks Jev which pattern the requirement follows. For "When the operator requests a report, the system shall produce it promptly." Jev answers event with probability 1.00, so the statement has a condition.
- Layer 2 sees that answer next to the statement and asks whether the condition and the response are both stated. Jev answers 0.78: likely, but not certain.
- Layer 3 runs because 0.78 falls in the unsure band, 0.2 to 0.8. The expert, here Jev with a more detailed question, answers 0.60, so the requirement does not pass.
Four of the five recorded statements were settled at layer 2 and never reached the expert. Ask in layers explains the nodes that connect the layers.
Install the CLI
You need Rust 1.98 or later and access to this repository. The checkout pins Rust 1.98.1.
git clone https://github.com/agent-ix/sapho.git
cd sapho
cargo install --path crates/sapho-cli --lockedTry it offline
The repository includes Jev's recorded answers for the change above. Replay them; no key or network is needed:
sapho replay examples/graphs/code-review.yaml \ --input examples/data/code-review-input.json \ --recording examples/recordings/code-review.json
The JSON report's outputs hold the decision and both probabilities, and its
trace holds every step. To see just the outputs, pipe the report through
jq '.outputs | map_values(.value.value)':
{"contract_risk": 0.79, "needs_review": true, "test_gap": 0.61}Now try a more lenient policy. code-review-relaxed.yaml
asks the same questions but raises both thresholds to 0.9:
sapho replay examples/graphs/code-review-relaxed.yaml \ --input examples/data/code-review-input.json \ --recording examples/recordings/code-review.json
needs_review is now false. The questions did not change, so the same
recorded answers were reused. Only your rule changed.
The layered requirement check replays the same way:
sapho replay examples/graphs/requirement-check.yaml \ --input examples/data/requirements/report.json \ --recording examples/recordings/requirement-check.json
It reports asked_expert true and complete 0.6. With
examples/data/requirements/brake-lamp.json instead, asked_expert is false
and the trace shows the expert node as skipped.
To use a decision in CI, add --fail-on needs_review. The command exits 1
when review is needed and 0 when it is not. Configuration or execution
failures exit 2. Reports go to stdout and diagnostics to stderr.
Ask Jev yourself
Install the CLI with Jev support:
cargo install --path crates/sapho-cli --locked --features jev
A graph names its model backend (judge here); a bindings file says which
model that is. Save this as bindings.yaml:
judge: provider: jev model: jev-1.13.0 expected_model: jev-1.13.0 distribution_policy: {kind: strict}
Set TYPESAFE_API_KEY in your environment. Put your own diff in input.json
as {"change": "…", "public_api_changed": true}, then run:
sapho run examples/graphs/code-review.yaml --input input.json --bindings bindings.yaml
Each run makes one request to Jev. To save the answers for replay, use
sapho record with the same arguments plus --recording saved.json. Improve a rule shows how to
measure a rule against labelled cases and compare candidates offline. To use
an existing CLM service instead of Jev, see CLM setup.
Measure and tune a rule
Record a model's answers for cases you have labelled, then compare rules against them offline. A threshold or combination change reuses the recorded answers, so trying a rule costs no model calls:
Show the whole diagram
sapho tune \ --candidate examples/graphs/code-review.yaml \ --candidate examples/graphs/code-review-relaxed.yaml \ --dataset examples/data/code-review-dataset.json \ --output-name needs_review --metric agreement \ --replay examples/recordings/code-review.json
On the six development cases, the original rule agrees with every label (1.00) and the relaxed rule with four of six (0.67). Improve a rule covers the held-out check and training export.
What Sapho gives you
- Questions and rules in YAML or JSON. Yes/no, choice and score questions; logic, thresholds and strength combinations; reusable subgraphs.
- Models and code in one decision. Use the Jev or CLM adapter, or register your own Rust functions and model backends.
- Layers, guards and collections. Feed one answer into the next question, ask an expensive question only when needed, and apply a rule to every item.
- Evidence you can reuse. Record answers, replay them offline, measure a rule against labels, compare candidates and export training data.
- Checks before a run and limits during it. Types and connections are checked before evaluation. Model calls, items, concurrency, data and time are bounded.
Use Sapho in a Rust project
Use the library when your application needs custom extraction, Rust functions or model backends. The stock CLI runs graphs with data operations and its configured Jev or CLM adapter; your registered Rust functions belong in an application host.
Add these dependencies to your application's Cargo.toml:
[dependencies] sapho = { git = "https://github.com/agent-ix/sapho.git" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
For local development, use sapho = { path = "../sapho" } instead, adjusting
the path to your checkout. Add features = ["jev"] to the Sapho dependency
when you need the included Jev adapter.
Copy examples/graphs/review.yaml to your
application's root. It flags a statement for review when its support is below
0.8. Put this code in src/main.rs:
//! A minimal Sapho decision in a Rust application. use sapho::{ core::{BackendRegistry, Datum, PrimitiveRegistry, Probability, Value}, graph::{GraphSpec, compile}, runtime::{Engine, RunLimits}, }; use std::collections::BTreeMap; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let spec = GraphSpec::parse(include_str!("../review.yaml"))?; let graph = compile(&spec, &PrimitiveRegistry::default())?; let engine = Engine::new(graph, BackendRegistry::default())?; let inputs = BTreeMap::from([( "support".into(), Datum::new("statement-1", Value::Probability(Probability::new(0.7)?))?, )]); let run = engine.run(&inputs, RunLimits::default()).await?; for (name, datum) in &run.outputs { println!("{name}: {:?}", datum.value); } Ok(()) }
Run cargo run. It prints needs_review: Boolean(true) without calling a
model. For a model-based rule, register its backends before constructing the
engine. For custom Rust steps, register functions before compiling the graph.
The Rust user guide covers
Jev integration,
Rust functions and
other model backends.
Use the skills in Claude Code
The bundled Sapho plugin provides three skills:
| Skill | What it helps you do |
|---|---|
/sapho:create |
Turn a decision into questions and rules, then build and validate a graph. |
/sapho:tune |
Measure a graph and compare candidates against development labels. |
/sapho:record |
Save evaluation evidence and check exact offline replay. |
Install the Sapho CLI first. From the Sapho checkout, start Claude Code with:
claude --plugin-dir ./plugins/sapho
When working in another project, pass the absolute path to that same plugin
directory. This loads the skills for that session; include --plugin-dir when
starting a new session. See Claude Code's
local plugin loading instructions.
For example, ask Claude:
/sapho:create Build a rule that checks whether a requirement names an actor,
an observable action and a target. Combine the three strengths with min and
flag statements below 0.8. Include an input example and validate the graph.
The skills use the same CLI and graphs as your application. Model calls need the bindings described in Ask Jev yourself.
Documentation
- How Sapho works: model answers, rules, layers, guards, collections and traces, with real recorded examples.
- Improve a rule: record answers once, then measure and tune against labelled cases offline.
- CLI guide: commands, input gathering, model bindings, recording, replay, measurement, tuning and training export.
- Rust user guide: embedding, Rust extensions, model adapters and integration choices.
- All documentation: graph, CLI and API references, runnable examples and feature coverage.
- Behavior specifications: the contracts for values, graphs, execution, logic, evidence and extensions. Use these when implementing an adapter or checking a boundary condition.
License and contributions
Sapho is licensed under AGPL-3.0-or-later. Read CONTRIBUTING.md, content rights and the CLA before contributing. Join us on Discord.
The name comes from the juice Mentats drink to aid their mental calculations.
来源:GitHub · github.com
