Skip to content

Cash-Codes/Agent-regression-lab

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

78 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ§ͺ Agent Regression Lab

Deterministic testing and regression infrastructure for LLM agent systems. Records every LLM call, tool call and runtime observation so an agent run can be replayed identically, evaluated against typed assertions and diffed event-by-event against any prior run on the same scenario.

LLM agents have logs and traces, but no CI/CD-grade regression testing. Agent Regression Lab fills that gap - think git diff for agents, not another observability dashboard.

The interesting layer is everything around the LLM call: canonical-JSON-hashed event content (contentHash per event, replayHash per run), a seeded PRNG and frozen logical clock so randomness and time replay deterministically, a snapshot tool executor keyed on (toolName, sha256(canonicalJSON(input))), an assertion DSL that runs over the in-memory event stream before flush and an overlap-only regression detector that compares each new run against its immediate predecessor.

TypeScript Next.js React Tailwind CSS Prisma PostgreSQL Zod Anthropic Vitest Docker Google Cloud Run

🌐 Live Demo

πŸ”— https://agent-regression-lab-768962821576.europe-west2.run.app

The deployed instance will run without an ANTHROPIC_API_KEY, so the agent loop will fall back to MockLLMClient and the seeded Refund flow scenario (/scenarios) will demonstrate replay, evaluations and side-by-side compare end-to-end without an API key. Live mode (real Claude calls) is intentionally not exercised in the public deploy.

πŸŽ₯ Demo

Demo

Live Flow

Scenario seeded in DB (inputs + fixtures + assertions, tag=seed-demo)
  β†’ POST /scenarios/{id}/run (runScenarioAction) β†’ runScenario()
    β†’ EventCapture + SeededPRNG + FrozenClock constructed against the new run row
      β†’ withCapture(MockLLMClient) wraps the LLM so every request/response is recorded
        β†’ runAgentLoop iterates: complete β†’ tool.call β†’ SnapshotToolExecutor.execute β†’ tool.result
          β†’ llm.request / llm.response / tool.call / tool.result / runtime.* events captured in order
            β†’ evaluateRun(pendingEvents, assertions) runs the 6-type assertion DSL pre-flush
              β†’ emit('evaluation.result') for each assertion (PASS/FAIL with message)
                β†’ detectRegression(current, prior) compares against immediate predecessor
                  β†’ capture.flush(prisma) - atomic transaction: createMany(events) + run.update(replayHash, status, eval counts, regression)
                    β†’ /runs/{id} renders RunHeader + EvaluationsList + Timeline
                      β†’ Pick 2 runs on the scenario β†’ /compare/{runA}/{runB}
                        β†’ diffRuns(refsA, refsB) by contentHash β†’ ComparePanes (sync-scrolled, sticky divergence marker)
                          β†’ VerdictPill: identical Β· βœ“ byte-deterministic / diverged @ #N / length-mismatch

The runner is byte-deterministic across two runs with the same seed. The seeded Refund flow demo proves it: baseline and replay-verification runs produce identical replayHash values; the broken-prompt run diverges on the first wrong tool call and three assertions flip from PASS to FAIL.

✨ Features

  • Eight typed event types (Zod-discriminated): llm.request, llm.response, tool.call, tool.result, runtime.random, runtime.time, branch.created, evaluation.result
  • Canonical JSON + sha256 - recursive key sort, no whitespace, Date β†’ ISO, BigInt β†’ string, NaN/Infinity rejected. contentHash per event, replayHash per run
  • Snapshot replay - a seeded PRNG (mulberry32), a frozen logical clock and a fixture-keyed tool executor make the same run produce a byte-identical replayHash across executions
  • Assertion DSL - six assertion types: tool_called, tool_not_called, response_contains, response_matches, event_count_equals, replay_hash_equals. Stable per-assertion ids so regression detection can compare across runs
  • Regression detection - overlap-only: an assertion that passed in the prior run and now fails is a regression. New assertions and removed assertions don't trigger it
  • Side-by-side compare - /compare/{runA}/{runB} aligns events by sequence, classifies each pair as match / diverge / onlyA / onlyB, scrolls in lock-step, jumps to the first divergence
  • Inline run viewer - /runs/{id} shows status + assertion pass-rate + regression chip + token totals, a card-stack timeline with expandable payloads and an Evaluations panel with per-assertion PASS/FAIL rows
  • Mock LLM by default - MockLLMClient matches the last user message against canned (substring | RegExp) β†’ response rules. The agent loop is identical for mock and live; just swap the client
  • Live Anthropic adapter - AnthropicLLMClient wraps @anthropic-ai/sdk against Claude Haiku 4.5 by default
  • Seeded refund demo - npx prisma db seed creates a 5-assertion scenario and three runs (baseline pass / replay-verification byte-identical / broken-prompt regression) so the UI is fully usable in seconds

πŸ–ΌοΈ Screenshots

Scenario detail with comparable runs Side-by-side compare with sticky divergence marker Run viewer with evaluations + timeline

🧠 Tech Stack

App

Technology Version Role
Next.js 16.2.6 App Router, server components, server actions
React 19.2 UI framework (uses useActionState, async params/searchParams)
TypeScript 5 Strict mode, noEmit, target: ES2022 (BigInt literals)
Tailwind CSS 4 @theme inline tokens in app/globals.css, postcss plugin variant
Zod 4 Event-payload schemas, assertion DSL, server-action form parsing

Persistence

Technology Role
Prisma 7.8 - ESM client generated to src/generated/prisma/, schema datasource has no url (lives in prisma.config.ts)
@prisma/adapter-pg Required runtime adapter - new PrismaClient({ adapter: new PrismaPg({ connectionString }) })
PostgreSQL 16 (Alpine) via docker-compose.yml (arl-postgres on port 5432)

Agent runtime

Component Role
EventCapture Buffers typed events with monotonic sequenceNumber; computes contentHash per event and replayHash per run; atomic transactional flush
canonicalJSON / sha256 Stable serialiser (recursive key sort, rejects NaN/Infinity, Date β†’ ISO, BigInt β†’ string) + node:crypto sha256
SeededPRNG mulberry32 - emits a runtime.random event on every draw
FrozenClock Logical-clock substitute for Date.now() - advance(ms) ticks the clock and emits runtime.time
SnapshotToolExecutor fixtures[fixtureKey(toolName, input)] lookup; throws SnapshotMiss on absent key
LLMClient interface One method: complete(LLMRequest) β†’ Promise<LLMResponse> with stopReason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error'
MockLLMClient Scripted responses; matches last user message by substring | RegExp
AnthropicLLMClient Real Claude via @anthropic-ai/sdk, default model claude-haiku-4-5
withCapture(inner) Higher-order wrapper that records llm.request / llm.response events around any LLMClient
runAgentLoop Anthropic-style tool-use loop with MAX_ITERATIONS = 10 default; appends TOOL_RESULT[<callId>]: <canonicalJSON> user messages
runScenario Public entry point: builds the capture, runs the loop, runs evaluateRun, runs detectRegression, flushes atomically

Evaluations

Component Role
AssertionSchema Zod discriminated union over six assertion types
evaluateRun(events, [a]) Runs each assertion against the pre-flush event stream; returns { assertionId, passed, message? }[]
detectRegression Overlap-only: regression iff prior.passed === true && current.passed === false for the same assertionId
loadAssertionResultsForRun Reloads prior evaluation.result events from Postgres for the regression comparison

Tooling

Technology Role
Vitest 4 Two-project config (node for *.test.ts, jsdom for *.test.tsx)
@testing-library/react Component tests against jsdom
@vitejs/plugin-react JSX/TSX transform for tests
Husky pre-commit (lint-staged) + pre-push (npm run test && npm run build)
lint-staged + Prettier + ESLint Format + lint the staged set on every commit
Anthropic SDK @anthropic-ai/sdk ^0.97.0
tsx Runs prisma/seed.ts for npx prisma db seed

Infrastructure

Technology Role
Docker App container (Dockerfile) + Postgres service (docker-compose.yml)
Google Cloud Run Production hosting (europe-west2, see Cloud Run Deployment)
Artifact Registry Container image storage

Architecture

agent-regression-lab/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ page.tsx                          Redirects β†’ /scenarios
β”‚   β”œβ”€β”€ layout.tsx                        Root layout (Inter + Geist Mono, header nav)
β”‚   β”œβ”€β”€ globals.css                       Tailwind 4 @theme inline tokens
β”‚   β”œβ”€β”€ scenarios/
β”‚   β”‚   β”œβ”€β”€ page.tsx                      ScenarioList + NewScenarioForm
β”‚   β”‚   β”œβ”€β”€ actions.ts                    "use server" - createScenarioAction, runScenarioAction
β”‚   β”‚   └── [id]/page.tsx                 Scenario detail: inputs / fixtures / assertions / ComparableRunsShell
β”‚   β”œβ”€β”€ runs/[id]/page.tsx                RunHeader + EvaluationsList + Timeline
β”‚   └── compare/[runA]/[runB]/page.tsx    CompareHeader + ComparePanes (diffRuns by contentHash)
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server/
β”‚   β”‚   β”œβ”€β”€ db/client.ts                  Lazy Prisma proxy + PrismaPg adapter
β”‚   β”‚   β”œβ”€β”€ events/
β”‚   β”‚   β”‚   β”œβ”€β”€ types.ts                  Zod schemas for all 8 event types
β”‚   β”‚   β”‚   β”œβ”€β”€ canonical.ts              canonicalJSON + sha256 (rejects NaN/Infinity)
β”‚   β”‚   β”‚   └── capture.ts                EventCapture - buffer + contentHash + atomic flush
β”‚   β”‚   β”œβ”€β”€ runner/
β”‚   β”‚   β”‚   β”œβ”€β”€ runner.ts                 runScenario - public entry point
β”‚   β”‚   β”‚   β”œβ”€β”€ agent-loop.ts             Tool-use loop (Anthropic-style)
β”‚   β”‚   β”‚   β”œβ”€β”€ prng.ts                   SeededPRNG (mulberry32)
β”‚   β”‚   β”‚   β”œβ”€β”€ clock.ts                  FrozenClock (logical-clock substitute)
β”‚   β”‚   β”‚   β”œβ”€β”€ tool-executor.ts          SnapshotToolExecutor + fixtureKey()
β”‚   β”‚   β”‚   └── errors.ts                 LiveModeNotImplemented, MaxIterationsExceeded
β”‚   β”‚   β”œβ”€β”€ llm/
β”‚   β”‚   β”‚   β”œβ”€β”€ types.ts                  LLMClient / LLMRequest / LLMResponse interfaces
β”‚   β”‚   β”‚   β”œβ”€β”€ mock.ts                   MockLLMClient - canned responses
β”‚   β”‚   β”‚   β”œβ”€β”€ anthropic.ts              AnthropicLLMClient - claude-haiku-4-5
β”‚   β”‚   β”‚   β”œβ”€β”€ capture.ts                withCapture(inner) - records llm.request/response
β”‚   β”‚   β”‚   └── factory.ts                makeLLMClient({ mode: 'mock' | 'anthropic' })
β”‚   β”‚   └── evals/
β”‚   β”‚       β”œβ”€β”€ types.ts                  AssertionSchema (Zod discriminated union, 6 types)
β”‚   β”‚       β”œβ”€β”€ engine.ts                 evaluateRun(events, assertions)
β”‚   β”‚       β”œβ”€β”€ regression.ts             detectRegression(current, prior)
β”‚   β”‚       └── load.ts                   loadAssertionResultsForRun(prisma, runId)
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ scenario/                     ScenarioList, NewScenarioForm, RunButton, ComparableRunsShell, RunsList
β”‚   β”‚   β”œβ”€β”€ run/                          RunHeader, EvaluationsList, EvalRow, Timeline, TimelineEvent, ExpandablePayload
β”‚   β”‚   β”œβ”€β”€ compare/                      CompareHeader, ComparePanes, DiffCell, DivergenceMarker, SyncScroll, JumpToDivergenceButton, VerdictPill
β”‚   β”‚   └── ui/                           Button, Card, Pill, CopyButton, EmptyState, ErrorBanner
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”œβ”€β”€ diff.ts                       diffRuns(a, b) - pair-by-pair contentHash diff
β”‚   β”‚   β”œβ”€β”€ format.ts                     shortHash, formatDuration, formatTokens, formatRelativeTime
β”‚   β”‚   β”œβ”€β”€ validation.ts                 parseTags, parseInputsJSON, parseFixturesJSON
β”‚   β”‚   └── assertion-validation.ts       parseAssertionsJSON
β”‚   └── types/                            run.ts (RunStatus), compare.ts (EventRef, DiffPair, Verdict)
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma                     Scenario / Run / Event + RunMode/RunStatus enums
β”‚   β”œβ”€β”€ migrations/                       init_event_model + eval_columns_on_run
β”‚   β”œβ”€β”€ seed.ts                           Orchestrates the 3-run refund demo
β”‚   └── seed/refund-data.ts               GOOD_INPUTS / BAD_INPUTS / REFUND_FIXTURES / ASSERTIONS
β”œβ”€β”€ prisma.config.ts                      datasource.url + migrations.seed (Prisma 7 location)
β”œβ”€β”€ docker-compose.yml                    Postgres 16 alpine (arl/arl/arl on :5432)
β”œβ”€β”€ Dockerfile                            Three-stage build: deps β†’ builder β†’ runner (node server.js)
β”œβ”€β”€ vitest.config.ts                      Two projects: node (.test.ts) + jsdom (.test.tsx)
└── package.json                          Next 16, React 19, TS 5, Prisma 7, Zod 4, Vitest 4

Request pipeline (view scenarios):

Browser β†’ GET /scenarios
  β†’ app/scenarios/page.tsx (server component)
    β†’ prisma.scenario.findMany({ include: { runs (last), _count: runs } })
    β†’ <ScenarioList rows={...} /> + <NewScenarioForm /> (client, useActionState)

Request pipeline (run a scenario):

RunButton form POSTs to runScenarioAction
  β†’ "use server"
    β†’ runScenario({ scenarioId })
      β†’ prisma.scenario.findUniqueOrThrow β†’ reads inputs, fixtures, assertions
      β†’ prisma.run.create({ status: RUNNING, mode: SNAPSHOT, model: 'mock' })
        β†’ new EventCapture(run.id) + new FrozenClock + new SeededPRNG(seed ?? hashSeed(run.id))
          β†’ withCapture(MockLLMClient(inputs.cannedResponses))
            β†’ runAgentLoop({ llm, executor, capture, initialMessages, maxIterations: 10 })
              β”œβ”€β”€ llm.request emitted
              β”œβ”€β”€ llm.response emitted
              β”œβ”€β”€ tool.call emitted (per toolCall)
              β”œβ”€β”€ executor.execute(toolName, input) β†’ fixtures[fixtureKey(...)]
              β”œβ”€β”€ tool.result emitted
              └── messages.push({ role: 'user', content: 'TOOL_RESULT[<callId>]: <canonicalJSON>' })
          β†’ evaluateRun(capture.pendingEvents, assertions)
          β†’ emit('evaluation.result') for each result
          β†’ detectRegression(current, loadAssertionResultsForRun(prior))
          β†’ capture.flush(prisma)  - TRANSACTION:
              β”œβ”€β”€ createMany(events) with payload + contentHash + sequenceNumber + logicalClock
              └── run.update({ replayHash, status: COMPLETE, finishedAt })
          β†’ run.update({ totalTokensIn/Out, durationMs, passedAssertions, totalAssertions, regression, regressedAssertionIds })
    β†’ revalidatePath(/scenarios/{id}) β†’ redirect(/runs/{newRunId})

Request pipeline (compare two runs):

ComparableRunsShell (client) β†’ router.push(/compare/{runA}/{runB})
  β†’ app/compare/[runA]/[runB]/page.tsx (server component)
    β†’ Promise.all([ prisma.run.findUnique(A), prisma.run.findUnique(B) ])
    β†’ Guard: same scenario? both COMPLETE/FAILED? else redirect(?error=...)
    β†’ Promise.all([ prisma.event.findMany(A, asc), prisma.event.findMany(B, asc) ])
    β†’ diffRuns(refsA, refsB) - pair-by-pair contentHash comparison
        β†’ kind: 'match' | 'diverge' | 'onlyA' | 'onlyB'
        β†’ firstDivergence: index of first non-match
        β†’ verdict: 'identical' | 'diverged' | 'length-mismatch'
    β†’ <CompareHeader> (verdict pill, sync-scroll toggle, jump-to-divergence, per-run stats, regression line)
    β†’ <ComparePanes> (two sync-scrolled lists, sticky divergence marker, colored DiffCells)

Setup

Prerequisites

  • Node.js 22+
  • Docker (for the local Postgres instance via docker-compose.yml)
  • Git
  • An Anthropic API key (only required for mode: 'anthropic'; the seeded demo uses MockLLMClient)

1. Clone and install

git clone https://github.com/Cash-Codes/Agent-Regression-Lab.git
cd Agent-Regression-Lab
npm install

npm install runs prisma generate via Husky's prepare script; the generated ESM client lands at src/generated/prisma/ (gitignored).

2. Configure environment

cp .env-example .env

Edit .env:

DATABASE_URL="postgresql://arl:arl@localhost:5432/arl?schema=public"
# Optional. If unset, the demo uses MockLLMClient.
# ANTHROPIC_API_KEY=sk-ant-...

See Environment Variables for the full reference.

3. Start Postgres + apply migrations + seed

docker compose up -d postgres
npx prisma migrate deploy
npx prisma db seed

The seed (prisma/seed.ts) is idempotent - it deletes prior scenarios tagged seed-demo and recreates the Refund flow scenario plus three runs.

4. Start the dev server

npm run dev

Open http://localhost:3000 (auto-redirects to /scenarios). You should see the seeded Refund flow scenario with three runs.

5. Try the demo flow

  1. Open the Refund flow scenario.
  2. Tick baseline + replay verification β†’ click Compare 2 runs. The VerdictPill reads identical Β· βœ“ byte-deterministic - proof of byte-deterministic replay across two independent runs with the same seed.
  3. Back to the scenario, tick replay verification + broken prompt β†’ Compare. The verdict flips to diverged @ #N; the sticky DivergenceMarker shows the first non-matching pair and the header's regression line lists B:calls-eligibility, B:no-unilateral-refund, B:asks-confirmation.
  4. Open the broken prompt run directly. The EvaluationsList shows three FAIL rows with the engine's exact failure messages; the timeline includes red evaluation.result pills inline with the events.

Docker (local app container)

The Dockerfile is a three-stage build (deps β†’ builder β†’ runner) that produces a minimal standalone image for Cloud Run cold-starts:

# Build
docker build -t agent-regression-lab:local .

# Run against the host Postgres (Linux: --network host; macOS/Windows: use host.docker.internal in DATABASE_URL)
docker run --rm -p 3000:3000 \
  -e DATABASE_URL="postgresql://arl:arl@host.docker.internal:5432/arl?schema=public" \
  agent-regression-lab:local

Open http://localhost:3000. The container does not auto-run migrations or seeds - run those against the host DB before starting the container.

Cloud Run Deployment

1. Authenticate with Google Cloud

gcloud auth login
gcloud config set project agent-regression-lab

2. Enable APIs + create the Artifact Registry repo (once)

export REGION=europe-west2
export PROJECT_ID=agent-regression-lab

gcloud services enable run.googleapis.com artifactregistry.googleapis.com

gcloud artifacts repositories create agent-regression-lab \
  --repository-format=docker \
  --location=${REGION}

gcloud auth configure-docker ${REGION}-docker.pkg.dev

3. Provision Neon Postgres (once)

  1. Create a project at https://neon.tech
  2. Copy the DATABASE_URL from the dashboard (with ?sslmode=require)
  3. From your laptop: run the schema migrations and seed the demo scenario
DATABASE_URL="postgresql://<neon-url>?sslmode=require" npx prisma migrate deploy
DATABASE_URL="postgresql://<neon-url>?sslmode=require" npx prisma db seed

4. Build and push the image

docker build \
  --platform linux/amd64 \
  -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/agent-regression-lab/app:latest \
  .

docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/agent-regression-lab/app:latest

5. Deploy

gcloud run deploy agent-regression-lab \
  --image ${REGION}-docker.pkg.dev/${PROJECT_ID}/agent-regression-lab/app:latest \
  --region ${REGION} \
  --platform managed \
  --allow-unauthenticated \
  --port 3000 \
  --memory 512Mi \
  --cpu 1 \
  --min-instances 0 \
  --max-instances 3 \
  --timeout 60s \
  --set-env-vars DATABASE_URL="postgresql://<neon-url>?sslmode=require"

The deploy prints a service URL. The public deploy intentionally does not set ANTHROPIC_API_KEY β€” the runner falls back to MockLLMClient and the header shows a demo Β· mock LLM pill.

6. Verify

SERVICE_URL=$(gcloud run services describe agent-regression-lab --region ${REGION} --format='value(status.url)')
curl -s "$SERVICE_URL/api/health" | jq .
curl -s -o /dev/null -w "/scenarios β†’ %{http_code}\n" "$SERVICE_URL/scenarios"

Testing

# All tests (both projects)
npm test

# Watch mode
npm run test:watch

# Lint
npm run lint

# Format check
npm run format:check

Vitest runs two projects (vitest.config.ts):

  • node - src/**/*.test.ts, app/**/*.test.ts, prisma/seed/**/*.test.ts - runs in Node, used for the server runtime (events, runner, evals, LLM clients, server actions, seed data validation)
  • jsdom - src/**/*.test.tsx, app/**/*.test.tsx - runs in jsdom with @testing-library/react for component tests

A Husky pre-commit hook runs lint-staged (eslint + prettier on the staged set). pre-push runs npm run test && npm run build - the full test suite plus a production build.

Demo Mode

The seed (prisma/seed.ts) wipes prior scenarios tagged seed-demo and creates Refund flow with three runs:

Run Seed Inputs Expected
baseline 42 GOOD_INPUTS 5/5 assertions pass; replayHash is the determinism reference
replay verification 42 GOOD_INPUTS byte-identical to baseline; Compare reads identical Β· βœ“ byte-deterministic
broken prompt 99 BAD_INPUTS 2/5 pass, 3 regressions (calls-eligibility, no-unilateral-refund, asks-confirmation)

The five assertions:

id Type Check
calls-lookup tool_called agent looks up the order before deciding
calls-eligibility tool_called agent checks eligibility before refunding
no-unilateral-refund tool_not_called agent must NOT refund without confirmation
asks-confirmation response_contains response contains 'confirm'
two-tool-calls event_count_equals exactly 2 tool.call events (good path)

To reset between demos:

npx prisma db seed

Environment Variables

Variable Required Default Description
DATABASE_URL Yes - Postgres connection string. Read by prisma.config.ts and by src/server/db/client.ts at runtime
ANTHROPIC_API_KEY For mode: anthropic only - Anthropic API key for AnthropicLLMClient. If unset, scenarios run with MockLLMClient
NODE_ENV No development Runtime environment. Prisma client is module-scoped (not globalThis-cached) in production
PORT No 3000 Server listen port (Cloud Run sets this)
HOSTNAME No 0.0.0.0 Bind address (Docker/Cloud Run)

Troubleshooting

DATABASE_URL is not set src/server/db/client.ts throws this from buildPrismaClient() if the env var is missing. Confirm .env exists at the repo root and is loaded - dotenv/config is imported at the top of prisma.config.ts and prisma/seed.ts, so npx prisma commands work, but a bare node invocation needs the env loaded separately.

To seed your database, add a seed property to the migrations section in your Prisma config file. Prisma 7 moved the seed hook from package.json (prisma.seed) to prisma.config.ts (migrations.seed). Confirm prisma.config.ts has migrations: { seed: 'tsx prisma/seed.ts' }.

Prisma Client could not be generated. The required engine type "library" / adapter is not configured. Prisma 7 ESM client requires the runtime adapter. Confirm src/server/db/client.ts constructs the client as new PrismaClient({ adapter: new PrismaPg({ connectionString }) }) (not new PrismaClient({ datasourceUrl: ... })).

Error: connect ECONNREFUSED 127.0.0.1:5432 The local Postgres container isn't running or another container is holding port 5432. Check docker ps; if another service holds the port, either stop it or change the port mapping in docker-compose.yml and update DATABASE_URL accordingly.

MockResponseMissing thrown mid-run MockLLMClient matched no canned response to the last user message. The runner emits TOOL_RESULT[<callId>]: <canonicalJSON> as the user content for tool results - make sure your cannedResponses use 'TOOL_RESULT[c-1]' (callId-specific) rather than 'TOOL_RESULT[' (which matches the first call ambiguously).

SnapshotMiss: No fixture for <toolName>:<sha256> The agent called a tool with arguments not present in scenario.fixtures. The key is ${toolName}:${sha256(canonicalJSON(input))} - recompute it via fixtureKey(toolName, input) from src/server/runner/tool-executor.ts and add the entry.

Compare shows length-mismatch for runs you expected to be identical The verdict is length-mismatch when one run's event stream is a strict prefix of the other (every diverging pair is onlyA or onlyB). Most often: a regression in the loop produced a different evaluation.result count or runtime.random/runtime.time drifted. Compare replayHash values directly; if they differ, the runs are not byte-identical.

Cannot find module '@/...' / '@server/...' / '@actions/...' Path aliases are declared in both tsconfig.json (compilerOptions.paths) and vitest.config.ts (resolve.alias). Editing one without the other breaks either the type-check or the test run.


Good luck and feel free to reach out if you need any clarification or would like to contribute further. Always happy to help. Thanks!


Document Version: 1.0 Last Updated: June, 2026 Maintainer: Cashley cashley.dps@gmail.com

About

Git diff for LLM agents. Records every tool call and LLM response as a canonical hashed event stream, replays runs byte identically and compares side-by-side so you can catch behavioral regressions before they ship.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages