Skip to main content

Test framework for multi-step AI agents

Project description

SALVO

Test your AI agents like you test your code.

Define scenarios in YAML. Run them N times. Get pass rates, costs, and failure breakdowns. When runs fail, replay recorded traces without spending another cent.

PyPI version Tests Python License


pip install salvo-ai[openai]

Your agent passes once. Does it pass 50 times in a row?


Why Salvo · Quick Start · How It Works · Assertions · Commands · Adapters


Why Salvo

You changed a system prompt. You tweaked the tools. You swapped models. Does your agent still work?

Single prompt-response evals don't catch multi-step failures. Your agent might call tools in the wrong order, hallucinate arguments, or bail out after one turn. You won't know until a user hits it.

Salvo fixes that. Write a YAML scenario, give it tools with mock responses, define what "correct" looks like with assertions, and run it 50 times. Get a pass rate. Sleep at night.

$ salvo run scenarios/ -n 10

  coding-assistant   gpt-4o   10/10 passed   score: 1.00   $0.12   avg 2.3s
  search-agent       gpt-4o    8/10 passed   score: 0.85   $0.08   avg 1.8s
  data-pipeline      gpt-4o    9/10 passed   score: 0.94   $0.15   avg 3.1s

  3 scenarios | 27/30 passed | avg score: 0.93

When something breaks, don't re-run and re-pay. Replay the recorded trace and re-evaluate with updated assertions. Zero API calls. Zero cost.


Quick Start

1. Install

pip install salvo-ai[openai]       # For OpenAI models
pip install salvo-ai[anthropic]    # For Anthropic models
pip install salvo-ai[all]          # Both

2. Initialize

salvo init

Creates a starter project:

my-project/
  salvo.yaml              # Project config
  scenarios/
    example.yaml          # Example scenario with tools and assertions
  tools/
    example_tool.yaml     # Reusable tool definition

3. Write a Scenario

description: "Agent fixes a bug by reading source, finding the issue, and applying a fix"

adapter: openai
model: gpt-4o

system_prompt: |
  You are a senior software engineer. Read the failing test,
  inspect the source, write a fix, then run tests to verify.

prompt: |
  The test in tests/test_math.py is failing. Please investigate and fix the bug.

tools:
  - name: file_read
    description: "Read the contents of a file"
    parameters:
      type: object
      properties:
        path:
          type: string
      required: [path]
    mock_response: |
      def add(a, b):
          return a - b  # BUG: should be a + b

  - name: file_write
    description: "Write contents to a file"
    parameters:
      type: object
      properties:
        path: { type: string }
        content: { type: string }
      required: [path, content]
    mock_response: "File written successfully"

  - name: run_tests
    description: "Run the test suite"
    parameters:
      type: object
      properties:
        test_path: { type: string }
    mock_response: "1 passed, 0 failed"

assertions:
  - type: tool_called
    tool: file_read
    required: true

  - type: tool_sequence
    mode: in_order
    sequence: [file_read, file_write, run_tests]
    weight: 2.0

  - type: output_contains
    value: "+"

threshold: 0.8

4. Run

# Run once
salvo run scenarios/example.yaml

# Run 10 times for reliability stats
salvo run scenarios/ -n 10

# Record traces for replay
salvo run scenarios/ -n 10 --record

How It Works

 YAML Scenario          Adapter            N-Trial Runner
┌──────────────┐    ┌──────────────┐    ┌──────────────────┐
│ system_prompt │───>│   OpenAI /   │───>│  Run scenario N  │
│ prompt        │    │  Anthropic   │    │  times, collect  │
│ tools + mocks │    │              │    │  traces + usage  │
│ assertions    │    └──────────────┘    └────────┬─────────┘
└──────────────┘                                  │
                                                  v
                    ┌──────────────┐    ┌──────────────────┐
                    │   Results    │<───│  Assertion Engine │
                    │              │    │                   │
                    │ pass rates   │    │ tool_sequence     │
                    │ scores       │    │ jmespath queries  │
                    │ cost / time  │    │ LLM judge (k=3)  │
                    │ breakdowns   │    │ cost/latency caps │
                    └──────────────┘    └──────────────────┘

Scenarios define what to test: a system prompt, user prompt, available tools with mock responses, and assertions that define "correct."

Adapters send the conversation to your model. Tools are presented to the model, and mock responses are returned when the model calls them. Multi-turn loops continue until the model stops calling tools or hits the safety limit.

Assertions evaluate the trace: Did it call the right tools? In the right order? Did the output contain what we expected? Did it stay under budget?

Traces capture everything: every message, every tool call, token counts, latency, cost. Record them to replay later without API calls.


Assertions

Salvo ships with 7 assertion types that cover the full evaluation spectrum.

Quick Assertions

The simplest way to check common things:

# Did the agent call a specific tool?
- type: tool_called
  tool: file_read
  required: true          # Hard fail if not called

# Does the output contain a string?
- type: output_contains
  value: "fixed"

Tool Sequence

Verify your agent follows the correct workflow:

- type: tool_sequence
  mode: in_order          # exact | in_order | any_order
  sequence:
    - file_read
    - file_write
    - run_tests
  weight: 2.0             # Count this assertion double
Mode Behavior
exact Actual calls must match expected exactly
in_order Expected tools appear in order (extra calls OK)
any_order All expected tools called (any order, extras OK)

JMESPath Queries

Query the full trace data structure with JMESPath:

# Check a tool was called with specific arguments
- type: jmespath
  expression: "tool_calls[?name=='file_write'].arguments.path | [0]"
  operator: exists

# Check token usage
- type: jmespath
  expression: "metadata.total_tokens"
  operator: lt
  value: 5000

# Check the model's final response
- type: jmespath
  expression: "response.content"
  operator: contains
  value: "successfully"
Available operators
Operator Description
eq Equal
ne Not equal
gt Greater than
gte Greater than or equal
lt Less than
lte Less than or equal
contains String/list contains value
regex Matches regular expression
exists Value is not null/missing
Queryable trace structure
{
  "response": {
    "content": "final assistant message",
    "finish_reason": "stop"
  },
  "turns": [
    { "role": "assistant", "content": "...", "tool_calls": [...] },
    { "role": "tool_result", "content": "...", "tool_name": "file_read" }
  ],
  "tool_calls": [
    { "name": "file_read", "arguments": { "path": "test.py" } },
    { "name": "file_write", "arguments": { "path": "src.py", "content": "..." } }
  ],
  "metadata": {
    "model": "gpt-4o",
    "provider": "openai",
    "cost_usd": 0.012,
    "latency_seconds": 2.3,
    "input_tokens": 1500,
    "output_tokens": 500,
    "total_tokens": 2000,
    "turn_count": 3,
    "finish_reason": "stop"
  }
}

Cost & Latency Limits

Hard caps on spend and time:

- type: cost_limit
  max_usd: 0.50

- type: latency_limit
  max_seconds: 30.0

LLM Judge

When assertions can't be expressed as rules, use an LLM judge with k-vote consensus:

- type: judge
  criteria:
    - name: correctness
      description: "Did the agent fix the actual bug?"
      weight: 2.0
    - name: methodology
      description: "Did the agent read before writing?"
      weight: 1.0
  judge_model: gpt-4o-mini   # Cheap model for judging
  k: 3                        # 3 votes, majority wins
  threshold: 0.7

Commands

Command What it does
salvo init Create a starter project with example scenario
salvo validate <path> Validate scenario files (catches typos, bad schemas)
salvo run <path> [-n N] Run scenarios N times, show pass rates and scores
salvo report <run-id> Show detailed breakdown of a completed run
salvo replay [trace-id] Replay a recorded trace without API calls
salvo reeval [trace-id] Re-evaluate a trace with updated assertions

Key Flags

# Run 20 trials with trace recording
salvo run scenarios/ -n 20 --record

# Run with a specific model
salvo run scenarios/ --model gpt-4o-mini

# Validate before running
salvo validate scenarios/

# Replay the latest recorded trace
salvo replay

# Re-evaluate with a modified scenario
salvo reeval --scenario scenarios/updated.yaml

Adapters

Salvo supports OpenAI and Anthropic out of the box, with a plugin system for custom adapters.

Adapter Models Install
openai gpt-4o, gpt-4o-mini, o1, o3, etc. pip install salvo-ai[openai]
anthropic claude-4, claude-sonnet, etc. pip install salvo-ai[anthropic]
Custom Any model Subclass BaseAdapter

Custom Adapters

# In your scenario
adapter: my_package.adapters.LlamaAdapter
model: llama-3.1-70b
# my_package/adapters.py
from salvo.adapters.base import BaseAdapter

class LlamaAdapter(BaseAdapter):
    async def run(self, scenario, resolved_extras):
        # Your implementation here
        ...

Configuration

salvo.yaml

Project-level defaults that scenarios can override:

# Default model settings
default_adapter: openai
default_model: gpt-4o

# Where to find scenarios
scenarios_dir: scenarios

# Where to store run data
storage_dir: .salvo

# Judge defaults
judge:
  adapter: openai
  model: gpt-4o-mini
  k: 3
  temperature: 0.0
  default_threshold: 0.8

Reusable Tools

Extract tool definitions into YAML files and include them across scenarios:

# tools/file_tools.yaml
name: file_read
description: "Read file contents"
parameters:
  type: object
  properties:
    path: { type: string }
  required: [path]
mock_response: "file contents here"
# In your scenario
tools:
  - !include tools/file_tools.yaml
  - name: inline_tool
    description: "Defined right here"
    mock_response: "ok"

Trace Recording & Replay

Record complete traces during runs, then replay and re-evaluate without making API calls.

# Record traces
salvo run scenarios/ --record

# Replay the last recorded trace (no API calls)
salvo replay

# Replay a specific trace
salvo replay abc123

# Re-evaluate with updated assertions (no API calls)
salvo reeval abc123 --scenario scenarios/stricter.yaml

This is the money feature for iteration. Change your assertions, re-evaluate the same trace, see if your new scoring logic works. No API costs. Instant feedback.


Validation

Salvo catches problems before you spend money on API calls:

$ salvo validate scenarios/bad.yaml

  scenarios/bad.yaml
    [error] Line 1: Unknown field 'modle'. Did you mean 'model'?
    [error] Assertion 0: Unknown type 'tool_calld'. Did you mean 'tool_called'?

  0/1 scenarios valid

Validates schema, field names (with typo suggestions), assertion types, tool definitions, and threshold ranges.


Architecture

src/salvo/
  adapters/          # OpenAI, Anthropic, custom adapter registry
  cli/               # Typer CLI: init, validate, run, report, replay, reeval
  evaluation/        # Assertion engine: normalizer, scorer, 5 evaluators
  execution/         # Multi-turn runner, N-trial runner, trace capture
  loader/            # YAML loading, !include resolution, validation
  models/            # Pydantic v2 models: Scenario, RunResult, RunTrace
  recording/         # Trace recorder, replayer, redaction
  scaffold/          # Project templates for salvo init
  storage/           # JSON store for runs, traces, manifests

License

MIT License. See LICENSE for details.


Your agent works once. Salvo tells you if it works every time.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

salvo_ai-0.1.1.tar.gz (453.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

salvo_ai-0.1.1-py3-none-any.whl (90.2 kB view details)

Uploaded Python 3

File details

Details for the file salvo_ai-0.1.1.tar.gz.

File metadata

  • Download URL: salvo_ai-0.1.1.tar.gz
  • Upload date:
  • Size: 453.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for salvo_ai-0.1.1.tar.gz
Algorithm Hash digest
SHA256 950254526e5ccb9baf176e42d904f11ad6f8d794258f030b1fa72d7207a6f956
MD5 d7c8c132aaaa47973a276d9585891835
BLAKE2b-256 f2c632a61c009d69868af04ae13c47e8a42265dfd996a00d36e746cf6d8909e2

See more details on using hashes here.

File details

Details for the file salvo_ai-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: salvo_ai-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 90.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for salvo_ai-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2c5ef6e788a7cdc441d8baaa127dc4500c7057454e6583fa89ad1a59a8144017
MD5 912063d0583471f23aba157c3c796e96
BLAKE2b-256 d4f3d52f10e89383c67062a2fae5712668fc13561e02a4ca58e84e9cad1ae976

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page