跳到正文
GitHub·· 7 天前AI 评分58

agent-ix 开源 Sapho:用 System One 模型和自写规则做复合决策的 Rust 库与 CLI

agent-ix/sapho

AI 导读

agent-ix 发布 Sapho,一个用 System One 模型(如 Jev)和自写规则做复合决策的 Rust 库与命令行工具,把决策拆成多个简单问题、逐个询问模型,再用 and、or、min、max 等逻辑算子组合答案。

正文

Sapho logo

License: AGPL-3.0-or-later Discord

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:

A code change and two yes/no questions go to Jev in one request. Jev answers 0.79 for contract risk and 0.61 for a test gap. 0.79 clears its 0.7 threshold and 0.61 misses its 0.8 threshold, so OR gives true. AND with the public_api_changed fact from your code gives needs_review = true.
  1. 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.
  2. Apply your rule. Each probability is compared with its threshold. 0.79 clears 0.7; 0.61 misses 0.8. or and and join those results with the fact your code supplied.
  3. Get the decision and its trace. needs_review is 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:

Three model layers for one requirement. Layer 1 asks Jev for its pattern: event 1.00, so has_condition is 1.00. Layer 2 sees that answer next to the statement and asks whether the condition and response are stated: 0.78. That is in the unsure band from 0.2 to 0.8, so layer 3 asks the expert, which answers 0.60, below the 0.8 needed to pass. 4 of 5 recorded statements never reach layer 3. The animation then replays a statement layer 2 is sure about (0.95), which skips the expert.

Show the whole diagram

Three model layers for one requirement. Layer 1 asks Jev for its pattern: event 1.00, so has_condition is 1.00. Layer 2 sees that answer next to the statement and asks whether the condition and response are stated: 0.78. That is in the unsure band from 0.2 to 0.8, so layer 3 asks the expert, which answers 0.60, below the 0.8 needed to pass. 4 of 5 recorded statements never reach layer 3.
  1. 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.
  2. 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.
  3. 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 --locked

Try 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:

A graph and a labelled dataset of 8 cases, 6 for development and 2 held out. sapho record runs once per case against Jev and the 8 answers are saved in one recording. Tune and measure then run offline: tuning on development cases scores the 0.7 / 0.8 rule 1.00 and the 0.9 / 0.9 rule 0.67. The chosen rule is measured once on held-out cases, and development cases can be exported for a trainer outside Sapho.

Show the whole diagram

A graph and a labelled dataset of 8 cases, 6 for development and 2 held out. sapho record runs once per case against Jev and the 8 answers are saved in one recording. Tune and measure then run offline: tuning on development cases scores the 0.7 / 0.8 rule 1.00 and the 0.9 / 0.9 rule 0.67. The chosen rule is measured once on held-out cases, and development cases can be exported for a trainer outside Sapho.
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