Skip to content

[AI-795] Add vision-agents agent simulate with table and report output - #656

Open
darkoatanasovski wants to merge 4 commits into
mainfrom
AI-795
Open

darkoatanasovski wants to merge 4 commits into
mainfrom
AI-795

Conversation

@darkoatanasovski

Copy link
Copy Markdown

Why

Developers can already run and serve an agent from the CLI, but there was no way to put an agent through a set of conversations and get a pass/fail answer, locally or in CI. The hosted dashboard trigger for this exists in Go on the accelerate branch; this is its local and CI counterpart.

Linear: AI-795

Changes

  • vision-agents agent simulate <dir-or-file> (also uv run agent.py simulate ...), added next to run and serve in Runner.cli(). It loads every *.toml scenario file, resolves the agent from pyproject.toml, and runs each scenario in text mode: a caller model follows the scenario brief, the agent replies turn by turn through TestSession, and a judge model rules on every criterion once the conversation ends. Every conversation gets a fresh agent so no history leaks between cases.
  • Options: --repeat N, --variations N (LLM-generated rephrasings of the brief; the brief as written is always variation 0), --judge MODEL (provider/model such as gemini/gemini-2.5-flash, or module:attribute naming a callable that returns an LLM, which is also how the tests inject a scripted judge), --report DIR, --filter NAME, plus --log-level for symmetry with run.
  • Output: a terminal table with scenario, variations, pass@k, turns, P50 turn latency and failed criteria, a progress line per conversation, and report.json / report.md in the report directory with full transcripts (including tool calls) and per-criterion verdicts. The JSON shape follows the acceleration SimulationRun / SimulationCase / SimulationLine schema (state, cases, passed, failed, conversations[] with variation, scenario, transcript[{caller, text, at}], turns, passed, verdict, ended, error, timestamps), with attempt, criteria[] and latency_ms added for the local use case.
  • Exit codes: 0 when every scenario passed, 1 when any failed, 2 when a judge or provider error meant a case never reached a verdict (bad --judge spec, missing scenarios and malformed scenario files also exit 2).
  • Scenario format (TOML): name (defaults to the file stem), scenario (the caller brief), criteria (list), optional max_turns (default 12) and variations (default 1). Documented in the root README together with the rest of the CLI, and referenced from the scaffold README.
  • Engine in vision_agents.testing: Scenario, load_scenario, find_scenarios, Simulator, SimulationReport / SimulationRun / SimulationCase / TranscriptLine / CriterionVerdict / ToolCall. LLMJudge's verdict parsing is extracted into a shared parse_verdict so both judges accept the same JSON format; parse failures now surface as a ValueError that LLMJudge.evaluate still turns into a failed verdict.
  • Dispatcher fix: vision-agents agent now sets allow_interspersed_args = False, so options after the first positional (e.g. agent simulate --help, agent run --help) reach the Runner CLI instead of being parsed by the dispatcher. --entrypoint keeps working when given before the subcommand.
  • Tests: tests/test_cli covers --help, exit codes 0/1/2, every option being reflected in the run, bad --judge specs, missing and malformed scenarios, and the contents of report.json / report.md, all against a stub project with a scripted LLM (no real provider). tests/test_testing/test_simulation.py covers scenario loading, the conversation loop (end token, max_turns cutoff, tool-call capture), variations and repeat semantics, error propagation and the report shape.

Realtime (audio) LLMs cannot be simulated in text mode; such agents get a clear per-case error and exit code 2. This is called out in the README.

Let developers run text-mode simulations from the terminal and in CI,
next to `run` and `serve` in `Runner.cli()`.

`vision-agents agent simulate <dir-or-file>` loads TOML scenario files
(a brief for the caller plus the criteria the agent must meet), plays
each one against a fresh agent from the project's `pyproject.toml`
entrypoint, and has a judge model rule on every criterion. It prints a
table (scenario, variations, pass@k, turns, P50 turn latency, failed
criteria), writes `report.json` and `report.md` with the full
transcripts and verdicts, and exits 0/1/2 for passed/failed/errored so
it can gate CI. `--repeat`, `--variations`, `--judge`, `--report` and
`--filter` shape the run; `--judge` takes `provider/model` or a
`module:attribute` callable that returns an LLM.

The engine lives in `vision_agents.testing` (`Simulator`, `Scenario`,
`load_scenario`, `find_scenarios`) and its report shape follows the
hosted `SimulationRun` schema. `LLMJudge`'s verdict parsing is extracted
into a shared `parse_verdict` so both judges agree on the format.

`vision-agents agent` now stops parsing its own options after the first
positional, so `agent simulate --help` (and `agent run --help`) reach
the Runner CLI instead of printing the dispatcher's help.
- A caller model that returns an empty message, or ends before saying
  anything, now errors the case (exit 2) instead of judging an empty
  conversation as a failed scenario.
- The `--judge` LLM is probed inside the event loop and closed, so a bad
  spec still fails fast without leaking a provider client.
- Report writing failures and the empty-table edge case raise cleanly.
- Scenario discovery only looks at `*.toml` files directly in the given
  directory, so `simulate .` no longer picks up `pyproject.toml`.
- Drop a `getattr` in favour of attribute access.
- Tests: silent-caller and bare `[END]` cases, errored-over-failed
  precedence, fresh-interpreter import order for the core/testing
  cycle, and `agent run --help` reaching the Runner CLI.
- Scaffold README no longer advertises `simulate` against the realtime
  template agent without explaining the text-LLM requirement.
Plugin LLMs report provider failures through `LLMErrorEvent` and return
an empty reply instead of raising, so the simulator now subscribes to the
agent's events during a conversation and turns such an error into an
errored case (exit 2) rather than letting the judge fail the scenario.

Also export `parse_verdict` from `vision_agents.testing`, dedupe repeated
errors in the table's failed-criteria cell, note that `--entrypoint` must
precede the subcommand, and document what `Runner.simulate()` raises.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

The change adds TOML scenario loading and a text-mode simulation harness. The simulator runs caller and agent conversations, applies repeats and variations, evaluates criteria with a judge LLM, and produces structured reports. The Runner exposes agent simulate with judge, filter, repeat, variation, report, and log-level options. The CLI forwards arguments after the subcommand. Documentation and tests cover the new behavior.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 58ddb

The feature remains usable, but some CI error classification, model-response parsing, and programmatic configuration behavior should be corrected or explicitly accepted before merging.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e03a28f3-4957-478e-9c87-b80617bed026

📥 Commits

Reviewing files that changed from the base of the PR and between ba2fbf1 and 58ddb3f.

📒 Files selected for processing (12)
  • README.md
  • agents-core/vision_agents/cli/agent/command.py
  • agents-core/vision_agents/cli/init/templates/README.md.j2
  • agents-core/vision_agents/core/runner/runner.py
  • agents-core/vision_agents/core/runner/simulate.py
  • agents-core/vision_agents/testing/__init__.py
  • agents-core/vision_agents/testing/_judge.py
  • agents-core/vision_agents/testing/_scenario.py
  • agents-core/vision_agents/testing/_simulation.py
  • agents-core/vision_agents/testing/_utils.py
  • tests/test_cli/test_cli.py
  • tests/test_testing/test_simulation.py

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Returns:
The process exit code: 0 all passed, 1 a scenario failed, 2 an error.
"""
configure_sdk_logger(level=getattr(logging, log_level.upper(), logging.WARNING))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '45,95p' agents-core/vision_agents/core/runner/simulate.py
sed -n '220,275p' agents-core/vision_agents/core/runner/runner.py
sed -n '445,535p' agents-core/vision_agents/core/runner/runner.py
rg -n 'run_simulation\(|\.simulate\(' agents-core tests | head -80

Repository: GetStream/Vision-Agents

Length of output: 7134


🏁 Script executed:

#!/bin/bash
rg -n -C 4 'def configure_sdk_logger|configure_sdk_logger\(|def run_simulation|def simulate\(|run_simulation\(' agents-core/vision_agents agents-core/tests tests 2>/dev/null | head -240
printf '\n--- candidate tests/usages ---\n'
rg -n -C 3 'log_level|--log-level|run_simulation|\.simulate\(' agents-core/tests tests agents-core/vision_agents 2>/dev/null | head -260
printf '\n--- package exports ---\n'
rg -n -C 3 'run_simulation|Runner|simulate' agents-core/vision_agents/core/__init__.py agents-core/vision_agents/core/runner/__init__.py agents-core/vision_agents/__init__.py 2>/dev/null

Repository: GetStream/Vision-Agents

Length of output: 21862


Reject unsupported log levels.

Runner.simulate calls run_simulation directly, so programmatic callers bypass Click's Choice validation. An unsupported value silently selects WARNING, which hides the caller's configuration error. Raise ValueError for invalid values.

Proposed fix
-    configure_sdk_logger(level=getattr(logging, log_level.upper(), logging.WARNING))
+    levels = {
+        "DEBUG": logging.DEBUG,
+        "INFO": logging.INFO,
+        "WARNING": logging.WARNING,
+        "ERROR": logging.ERROR,
+        "CRITICAL": logging.CRITICAL,
+    }
+    try:
+        numeric_level = levels[log_level.upper()]
+    except KeyError as err:
+        raise ValueError(f"unsupported log level: {log_level}") from err
+    configure_sdk_logger(level=numeric_level)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
configure_sdk_logger(level=getattr(logging, log_level.upper(), logging.WARNING))
levels = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
try:
numeric_level = levels[log_level.upper()]
except KeyError as err:
raise ValueError(f"unsupported log level: {log_level}") from err
configure_sdk_logger(level=numeric_level)

raise SimulateError(
f"--judge {spec!r}: expected an LLM instance, got {type(llm).__name__}"
)
await llm.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '120,138p' agents-core/vision_agents/core/runner/simulate.py
sed -n '485,535p' agents-core/vision_agents/core/runner/runner.py
rg -n 'exit code 2|exit_code = 2|provider error|probe' README.md agents-core tests/test_cli/test_cli.py

Repository: GetStream/Vision-Agents

Length of output: 2882


Convert probe cleanup failures to exit code 2.

If llm.close() raises, the exception escapes probe_llm_factory as an unclassified nonzero failure. This prevents CI from distinguishing the provider error from a scenario failure, even though the command still terminates.

Wrap the close failure in SimulateError.

Proposed fix
-    await llm.close()
+    try:
+        await llm.close()
+    except Exception as err:
+        raise SimulateError(
+            f"--judge {spec!r}: failed to close the probe LLM: {err}"
+        ) from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await llm.close()
try:
await llm.close()
except Exception as err:
raise SimulateError(
f"--judge {spec!r}: failed to close the probe LLM: {err}"
) from err

Comment on lines +31 to +32
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Single-line code fences are not stripped.

Line 32 removes the opening fence by splitting on the first newline. A one-line fenced response has no newline, so split("\n", 1)[-1] returns the whole string, including the leading ```. Only the trailing fence is removed.

Input ```{"verdict": "pass"}``` returns ```{"verdict": "pass"}. parse_verdict then fails json.loads and raises ValueError, and _parse_string_list raises SimulationError. The case is reported as errored instead of judged.

Strip the opening fence directly:

Proposed fix
 def strip_code_fences(text: str) -> str:
     """Return ``text`` without a surrounding markdown code fence, if any."""
     cleaned = text.strip()
     if cleaned.startswith("```"):
-        cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
+        cleaned = cleaned[3:]
+        head, newline, rest = cleaned.partition("\n")
+        # Drop an info string such as ``json`` on the opening fence line.
+        cleaned = rest if newline and "`" not in head else cleaned
+        cleaned = cleaned.rsplit("```", 1)[0].strip()
     return cleaned
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
if cleaned.startswith("```"):
cleaned = cleaned[3:]
head, newline, rest = cleaned.partition("\n")
# Drop an info string such as ``json`` on the opening fence line.
cleaned = rest if newline and "`" not in head else cleaned
cleaned = cleaned.rsplit("```", 1)[0].strip()

Comment thread README.md
model follows the scenario brief, your agent replies turn by turn, and a judge model rules on each criterion
once the conversation ends. It prints a table with the scenario, variations, pass@k, turns, P50 turn latency
and failed criteria, writes `report.json` and `report.md` with the full transcripts and verdicts, and exits
`0` when every scenario passed, `1` when any failed and `2` on a judge or provider error, so it can gate CI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document all exit code 2 conditions.

Scenario, configuration, and execution errors also return exit code 2. The current text limits this code to judge or provider errors. This can cause incorrect CI error handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant