* docs: add global AI chat context-optimization plan for raw apps Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai-evals): add global raw-app debugging cases on a large fixture Adds a ~20-file analytics_dashboard raw-app fixture (incl. a 5k-line data module and a planted wrong-totals bug), two global cases (read-heavy debug + small-edit baseline), app-seed support in the mock backend, directory-fixture loading, and a decorateHelpers seam so read-dedupe is measurable. Records tokenUsage for before/ after comparison of the read-tool optimization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai-chat): cap and dedupe read_app_file to bound context in large apps read_app_file now defaults to a head slice (1500 lines / 50k chars) with offset/ limit to page further, and skips resending a file whose earlier read is still in context (per-conversation ledger keyed off the originating tool-call id, so it self-heals after compaction). Bounds the file-content portion of global-chat context when working in large raw apps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai-evals): add read-heavy raw-app debug case (large data module) global-test31 induces the model to inspect the 5k-line seedData module, exercising the read_app_file cap/offset path. Baseline ~262k tokens vs ~200k with the cap+dedupe change (-24%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: record A+B benchmark results and fixed-overhead finding Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai-chat): clearer read_app_file past-EOF message + unit tests for cap/dedupe Addresses local-review nits: out-of-range offset now reports 'offset N is past the end of the file' instead of a backwards 'lines 11-10' label; adds unit coverage for the slicing (line cap, offset/limit window, char budget, past-EOF) and re-read dedupe (hit + miss-when-not-retained). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai-chat): char-level paging + per-range dedupe for read_app_file Adds char_offset/char_limit so minified/long-line files can be paged within a line window, keys the re-read ledger by range (so reading different ranges no longer collides), and dedupes on the full-file hash (a cached range stub is invalidated when any byte of the file changes, not just the returned range). Tests updated for the char-slice behavior plus single-line capping, char paging, and out-of-window change detection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai-chat): add read_app_file context micro-benchmark + re-read eval case Adds a deterministic micro-benchmark (no LLM) that drives read_app_file through a realistic big-project read pattern (large file, re-read, minified bundle, paging) and asserts the cap+dedupe cut returned context >50% vs the old whole-file behavior — isolating the feature's effect from model nondeterminism and guarding against silent weakening. Adds global-test32, a cross-file consistency investigation that revisits overlapping files so re-read dedupe is exercised in a real run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai-evals): clarify test32 measures the read cap, not dedupe Verified: sonnet and haiku both read each file once per conversation and retain it, so test32 never triggers read_app_file re-read dedupe. Dedupe is measured deterministically by the micro-benchmark instead. Comment corrected to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ai-chat): drop read_app_file re-read dedupe, ship the cap only Benchmarking showed the per-conversation re-read dedupe never fires in practice: across sonnet/opus/gpt-5.5/haiku, every model reads each file once per conversation and keeps it in context (0 within-conversation re-reads). It was a correct but unused guard, so this removes the ledger, full-file hash, retention predicate, the AIChatManager wiring, and the eval decorateHelpers seam — keeping the read cap + offset/limit/char paging (A), which is the lever that actually bounds context. The micro-benchmark is now cap-only; test32 is kept as a multi-file read-load case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai-chat): add search_app grep tool for global raw-app chat (experimental) Client-side grep over a raw app's frontend files and inline runnables (literal, case-insensitive, optional file_glob/context_lines/max_matches, head-capped). Completes the list -> search -> ranged-read triad. Includes the eval A/B gate (WMILL_AI_EVAL_DISABLE_SEARCH_APP), unit tests + micro-benchmark, and a find-all-usages eval case (global-test33). Experimental: A/B benchmarking shows it is not an unconditional win — it helps on find-all-usages but adds agentic iterations on navigable apps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai-evals): accept search_app as a valid file-inspection tool in raw-app cases Add requiredToolsAnyOf alternatives-group to ToolValidationSpec and switch global-test29..32 to it so a model that locates files via search_app instead of read_app_file no longer false-fails the tool assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: remove stale ai-chat context-optimization planning doc Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ai-chat): drop read_app_file char paging for a hard char cap The char_offset/char_limit params guarded minified files (a single line over the char budget) but were effectively unused in benchmarks. Remove them and the in-window char paging; keep the hard 50k-char budget and, when a read hits it, tell the model to narrow the line limit (or treat the file as unreadable if a single line exceeds the budget). Proper long-line handling is left as a TODO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ai-chat): bake search_app context to 1 line, clarify query is literal Drop the context_lines param (models varied it to little effect) for a fixed SEARCH_APP_CONTEXT_LINES=1, and cap on matching lines instead of pushed rows so max_matches stays accurate with context always on. Sharpen the query description to state it is a literal (non-regex) substring and to suggest the call form (e.g. formatCurrency() to hit call sites and skip formatCurrencyPrecise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ai-chat): widen baked search_app context to 2 lines Models that set the old context_lines param leaned to 2; match the lean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai-chat): count every file with a match in search_app header Move fileHadMatch ahead of the render cap so files whose matches fall past max_matches are still counted (with a regression test). Also swap the raw NUL globstar sentinel for a printable escape (the NUL bytes made core.ts read as binary to grep) and reword two comments to describe current constraints instead of drafting history. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai-chat): drop redundant input echoes from app tool results read_app_file and search_app no longer prefix results with the tool name or echo back the caller's own inputs (file path, query, file_glob) — the model already has them from the call args, and the unbounded query echo could push the search result past its output budget. Keeps the useful signals (line range, match/file counts, truncation) and the actionable advice. Also reword max_matches to 'matching lines' since it caps lines (each expands to context rows). Unit tests updated to the new format. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AI Evals
Small benchmark runner for the Windmill AI generation modes:
cliflowscriptappglobal
The benchmark always tests the current production prompts, tools, and guidance in this checkout.
Each attempt runs:
- the real production path
- deterministic validation
- LLM judging
Install
cd ai_evals
bun install
Frontend modes also require frontend dependencies:
cd frontend
bun install
Commands
List model aliases:
cd ai_evals
bun run cli -- models
List cases:
cd ai_evals
bun run cli -- cases
bun run cli -- cases flow
Run benchmarks:
cd ai_evals
bun run cli -- run flow
bun run cli -- run flow flow-test4-order-processing-loop --model opus
bun run cli -- run flow flow-test0-sum-two-numbers --models haiku,opus,4o
bun run cli -- run flow flow-test0-sum-two-numbers --runs 3 --verbose
bun run cli -- run flow --record
GEMINI_API_KEY=... bun run cli -- run app app-test1-counter-create --model gemini-3-flash-preview
WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000 bun run cli -- run flow --backend-validation preview
bun run cli -- run global global-test1-script-create
bun run cli -- run cli bun-hello-script
Public CLI surface:
modelscases [mode]run <mode> [caseIds...]
run options:
--runs <n>: repeat each casentimes--output <path>: custom result JSON path--model <alias>: choose the model under test--models <a,b,c>: run the same cases sequentially against several model aliases--verbose: stream assistant output for frontend runs--record: append a compact tracked summary line toai_evals/history/<mode>.jsonlfor full-suite runs only--backend-validation <mode>: optional backend smoke validation (offorpreview) forscriptandflowevals
Models
Use bun run cli -- models to see the current aliases.
Today:
haikusonnetopus4ogpt-5.5gemini-3-flash-previewgemini-3.1-pro-previewdeepseek-v4-flashdeepseek-v4-pro
Notes:
- the command also prints accepted alias spellings such as
gpt-4o,gpt-55,claude-opus-4.6, andclaude-haiku-4.5 - frontend modes (
flow,script,app,global) can use Anthropic, OpenAI, Gemini, and DeepSeek-backed aliases climode always uses the Anthropic agent SDK, so only Anthropic aliases are valid there- the judge model is separate and currently defaults to
claude-sonnet-4-6
Case Format
Cases live in one YAML file per mode under ai_evals/cases/.
Minimal shape:
- id: flow-test0-sum-two-numbers
prompt: |-
Create a flow that takes two numbers, `a` and `b`, and returns their sum.
initial: ai_evals/fixtures/...
expected: ai_evals/fixtures/...
Optional fields:
initial: starting state fixtureexpected: expected artifact fixturevalidate: extra deterministic validation rulesruntime.backendPreview: optional real backend preview config for smoke validation
For flow mode, validate can express requirements such as:
- accepted input schema shapes
- required
results.*reference validity - required module/code/input characteristics
For app mode, validate can express narrow hard requirements such as:
- required frontend file paths or backend runnable keys
- minimum backend runnable counts
- required backend runnable types
- minimum datatable / datatable-table counts
- specific required datatable tables
For global mode, validate can express draft-level requirements such as:
- required draft type/path/language
- required or forbidden snippets in draft values
- required or forbidden draft counts
- forbidden draft paths
Global initial fixtures can also seed liveEditorDrafts with type,
storagePath, effectivePath, and value fields. These drafts emulate the
currently open script, flow, or raw app editor so cases can test prompts that
refer to "this" or the "current" item.
Global (and flow) initial fixtures can seed workspace.datatables so the
list_datatables, get_datatable_table_schema, and exec_datatable_sql tools
return seeded data during evals. Each entry is
{ datatable_name, schemas: { <schema>: { <table>: { columns, rows? } } } }.
SQL runs through a small in-memory engine (datatableSqlEngine.ts), not a real
database. Writes are stateful within a case: CREATE/DROP/INSERT/UPDATE/
DELETE mutate the seeded datatable in place, so a later list_datatables,
get_datatable_table_schema, SELECT, or information_schema query reflects them
— this is what stops a model from looping when it re-queries to verify a write.
The engine is best-effort: SELECT returns all rows of the referenced (or first)
table with no WHERE filtering/projection/joins, WHERE on UPDATE/DELETE supports
col = value predicates joined by AND, and anything unparseable is a no-op
success. So validate datatable cases through tool-use and SQL-argument assertions
(requiredToolsUsed, stringIncludesAnyOf) — not through exact returned row
values. An empty/absent datatables seed makes list_datatables return [],
which is what the "no datatable configured" blocking cases rely on.
Set WMILL_AI_EVAL_DISABLE_ACTIVE_EDITOR_CONTEXT=1 to run those cases with
the old behavior where the live editor is only discoverable through
list_workspace_items.
App fixtures can also include an optional datatables.json file at the fixture root.
For flow mode, an initial fixture can also include a benchmark workspace catalog of
existing scripts and flows. That lets the real search_workspace and
get_runnable_details tools discover reusable workspace runnables during evals.
If --backend-validation preview is enabled:
scriptevals run a real backend script preview in an isolated temp workspaceflowevals run a real backend flow preview only for cases that defineruntime.backendPreviewflowcases withinitial.workspacefixtures seed those scripts and flows into the preview workspace before preview- when
WMILL_AI_EVAL_BACKEND_WORKSPACEis set,ai_evalscreates or reuses that workspace as a dedicated test workspace, clears managed eval assets underf/evals/*before each preview run, and then reseeds the current case fixtures
Supported backend env vars:
WMILL_AI_EVAL_BACKEND_VALIDATION=previewWMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000WMILL_AI_EVAL_BACKEND_EMAIL=admin@windmill.devWMILL_AI_EVAL_BACKEND_PASSWORD=changemeWMILL_AI_EVAL_BACKEND_WORKSPACE=integration-teststo reuse an existing workspace on CE installs with low workspace limits
Frontend modes require a reachable Windmill backend and send model requests through the workspace AI proxy at /api/w/{workspace}/ai/proxy. At startup, ai_evals checks the resolved backend URL and fails early with setup guidance if the backend cannot be reached or login fails.
For frontend modes:
ai_evalscreates a temporary backend workspace, or creates/reusesWMILL_AI_EVAL_BACKEND_WORKSPACEwhen it is set- it upserts a provider resource under
f/evals/ai/<provider> - frontend requests go through
/api/w/{workspace}/ai/proxy
Results And Artifacts
Every run writes:
- a summary JSON under
ai_evals/results/ - generated artifacts in a sibling directory
If --record is used, the CLI also appends one compact JSON line to:
ai_evals/history/flow.jsonlai_evals/history/script.jsonlai_evals/history/app.jsonlai_evals/history/global.jsonlai_evals/history/cli.jsonl
Each recorded line contains:
- run metadata (
createdAt,gitSha,mode,runModel,judgeModel) - suite totals (
caseCount,attemptCount,passedAttempts,passRate,averageDurationMs,averagePassedDurationMs,averageJudgeScore) - average token usage (
averageTokenUsagePerAttempt,averageTokenUsagePerPassedAttempt) - per-case metrics under
cases[](averageDurationMs,averagePassedDurationMs,averageJudgeScore,averageTokenUsagePerAttempt,averageTokenUsagePerPassedAttempt, pass rate) failedCaseIds
The CLI headline duration and token averages use passed attempts only. All-attempt averages are still recorded to make failures auditable without letting failed attempts skew success cost comparisons.
Example:
- summary:
ai_evals/results/2026-04-09T09-40-33.051Z__flow.json - artifacts:
ai_evals/results/2026-04-09T09-40-33.051Z__flow/
Typical artifacts by mode:
flow:flow.jsonscript:script.jsonplus the generated script fileapp:app.jsonplus frontend/backend filesglobal:global-drafts.jsoncli:assistant-output.txt,trace.json,wmill-invocations.jsonl, plus generated workspace files- backend-validated attempts also include
backend-preview.json
Layout
cases/: one YAML file per modefixtures/: initial and expected fixturescore/: shared loading, model resolution, validation, judging, and result writingmodes/: one runner per modehistory/: optional tracked pass-rate history written byrun --record, one JSONL file per moderesults/: local benchmark output and artifacts
Notes
- Frontend modes reuse the production frontend chat code through the Vitest bridge.
- Global mode evaluates the production global AI tools and validates the resulting AI draft store.
- CLI mode creates an isolated workspace, writes the current checkout guidance into it, and benchmarks the real skills /
AGENTS.mdflow. - CLI mode now also records a structured trace of invoked skills, tool calls, proposed
wmillcommands, and any attemptedwmillexecutions. - Frontend progress streams live while the benchmark is running.
- Deterministic validators should stay focused on real correctness constraints, not one exact implementation shape.