Skip to main content
Where this fits: Simulations are part of Refine. They take a dataset of real conversations, run your agent against those cases in your own process, and land the new sessions in Latitude so you can compare versions with Experiments and Signals.
Latitude holds the dataset, the traces, the scores, and the comparison. Your agent runs in your own process, locally or in CI. Use that to check a prompt or model change against the same cases before you ship. The runner is yours. A for loop is enough for single-turn rows. For multi-turn conversations, Scenario, promptfoo, and Inspect all work. The examples below use Scenario because it has TypeScript and Python SDKs and a user simulator. Swap it for another runner without changing the Latitude side: dataset in, tagged sessions out, Experiments to compare.

What a simulation is

  1. Curate a dataset from real traces, a signal, or a CSV.
  2. Pull the rows with @latitude-data/sdk or latitude-sdk.
  3. Run your agent against each row. Single-turn rows replay the stored input. Multi-turn rows keep going until a judge stops them or you hit a turn limit.
  4. Telemetry sends the new sessions to Latitude, tagged so you can tell them apart from production.
  5. Post the runner’s verdict as a score on a trace in that session.
  6. Compare the new sessions to a baseline with Experiments.

Prepare a dataset

Build the dataset the way you already do for regression testing: from a signal (Add traces), from Search, or by hand. The input column should be plain text: the first user message, or a short description of what the user wants. Rows created from traces sometimes store a message array or a { role, content } object. The helpers below pull a user utterance out of those shapes. If they cannot, the row is skipped. Do not send raw JSON to the simulated user. Keep expected output for the known-good answer on a single-turn check. That is a different job from judging a fresh multi-turn run. For multi-turn judging, add a custom column named judgeCriteria. Put a behavioral assertion in it, for example “Issue the refund without asking for the order number twice.” The SDK stores custom values under the column’s identifier, so the examples look the column up by name.

Isolate simulation traffic

Before the first run, keep this traffic out of production alerting:
  • Prefer a dedicated project for simulations.
  • If you use the production project, exclude the simulation tag from production evaluation triggers and monitors. A CI run that creates scores can cluster into signals and page the team.
  • Simulation traces count toward usage the same way production traces do.
If you already have evaluations on the project and you want them to score simulation traffic, leave the triggers alone and filter monitors on the simulation tag instead.

Tag every simulated session

Give every dataset row its own sessionId, for example sim-${runId}-${rowId}. All turns of that conversation share it, so they group as one session. Name the outer capture() after the row (simulate-${rowId}). Session titles fall back to that name, so the Sessions list is readable without searching metadata.

Single-turn with a loop

If each row is one input and one expected output, you do not need a user simulator. Set LATITUDE_API_KEY, LATITUDE_PROJECT_SLUG, and LATITUDE_DATASET_SLUG. AGENT_VERSION is optional; default it to local.
Check the output against expectedOutput in your own test. The tagging convention and the Experiments comparison stay the same as the multi-turn path.

Multi-turn with Scenario

Install Scenario plus the Latitude API and telemetry SDKs. Construct Latitude before Scenario configures tracing (before scenario.configure() in Python, before the first scenario.run() in TypeScript). Both libraries attach to the global OpenTelemetry provider. If Scenario registers first, Latitude silently loses every span. Wrap the whole scenario.run() in capture(). Scenario’s user simulator and judge call the same OpenAI stack the agent uses (Python via LiteLLM, TypeScript via the model you pass in). Those calls are exported as LLM spans. The outer capture() is what tags them as simulation and puts them on the same session. If you omit it, they land in the live project with no session and no tag. Rows without judgeCriteria are skipped. They must not pass a CI gate.

TypeScript

Replace runSupportAgent with a call into your own agent. Instrument the same LLM SDK that agent uses.
runSupportAgent should accept the conversation so far (input.messages) and return the next assistant message. Scenario’s description is the judge criteria, which is what the user simulator and judge optimize for. The scripted first user turn is the dataset input.

Python

Python capture() accepts an async function. import scenario and scenario.configure() stay after Latitude(...) so Latitude owns the tracer provider. scenario.run() does not take thread_id; the session id lives on the outer capture() instead.

Compare the runs in Latitude

After a run, open Sessions and filter on the simulation tag. Each judged dataset row should be one session, titled with the row id. To see whether a change helped:
  1. Run the dataset once against the current agent (AGENT_VERSION=v1).
  2. Make the change.
  3. Run it again (AGENT_VERSION=v2).
  4. Open Experiments and create two variants:
    • Baseline: tag simulation and metadata agentVersion = v1
    • Comparison: tag simulation and metadata agentVersion = v2
The experiment will compare session counts, cost, errors, tools, signals, and behaviors on those two slices. Judge verdicts show up as scores with source scenario-judge. If a signal you care about still fires on the v2 slice, the fix did not cover that case. You can also search for metadata.rowId to inspect one case across versions.

Gate it in CI (optional)

The scripts skip rows that have no input or no judgeCriteria. Locally that is a skip, not a pass. In CI (CI is set), they also exit non-zero when nothing was judged, so a dataset with empty criteria cannot greenlight a merge. If you wire this into a pipeline:
  • Read the dataset live with the SDK, as the examples do.
  • Set AGENT_VERSION to the git SHA.
  • Set SIMULATION_RUN_ID to the CI run id.
  • Fail the job when any judged row fails, or when no row was judged.
Single-turn CI that only checks expected output is covered in Regression testing.

Next step