Vally 0.7.0
Released on 2026-06-30.
Features
vally evalcollects OpenTelemetry traces from the copilot-sdk executor and writes them tootel-spans.jsonlin the output directory. Specify--otlp-endpoint <url>to instead export traces to an OTLP/HTTP collector; the endpoint must be anhttp(s)URL without embedded credentials.- CLI:
vally exportnow accepts repeatable-e, --eval-specflags to export multiple eval specs in one invocation. When more than one spec is given, each spec’s output is written into its own subdirectory of the output directory, named after the spec’snamefield. - CLI:
vally evalnow copies per-stimulus configured workspace files into the run output directory wheneverartifactsis present on that stimulus. Include/exclude patterns are configured viaartifacts.includeandartifacts.exclude; when a path matches both, exclude wins. Copied files are placed in anartifacts/subdirectory of that trial’s session-log directory (<eval>/<stimulus>/<model>/<trial>/artifacts/), alongside itsevents.jsonl/metadata.json. - CLI:
vally experiment runnow also copies each stimulus’s configured workspace files wheneverartifactsis present, co-located under that trial’s per-variant session-log directory (<variant>/<eval>/<stimulus>/<model>/<trial>/artifacts/), matchingvally evalbehavior. run-commandgrader:stdout_contains,stderr_contains,stdout_matches, andstdout_not_matchesnow accept either a single non-empty string or a non-empty array of non-empty strings. For positive fields all listed entries must match; forstdout_not_matchesnone of the listed regexes may match. Each array entry produces its ownGraderResultdetail (namedstdout-contains[0],stdout-contains[1], …) so failure evidence still points at the specific check that failed.vally experiment runnow also writes two run-level artifacts —plan-snapshot.jsonandshard-manifest.json— alongside the per-variant directories. The snapshot captures the planned eval/stimulus metadata and grader configs that the per-trial JSONL omits; the manifest records run identity (a machine-independentplanDigest, variant names, varied axes) and shard identity (an unsharded run is recorded as shard 1/1). These are the artifact contract that the forthcomingvally experiment mergewill consume to reconcile sharded runs. New public helperscomputeShardKey,computePlanDigest,assertUniqueShardKeys,assemblePlanSnapshot,assembleShardManifest, andsingleShardIdentityare exported from@microsoft/vally.vally experiment runis now shardable across N runners via--shard i/n,--run-id <id>(required with--shard), and--shard-strategy <specifier>(defaultround-robin). A shard runs only its slice of the planned trials and writes outputs under<output-dir>/<run-id>/shard-NN-of-NN/; the experiment-level markdown report is skipped in shard mode (a per-shard report is incomplete by definition). Theexperimentmanifest block andplanDigeststay byte-identical across every shard of a run, andshard-manifest.jsonnow records the real shard block (index/total, chosen strategy, selected/completed shard keys). Non-sharded runs keep today’s timestamped layout.- New public
ShardStrategyinterface in@microsoft/vallyplus two built-in strategies —round-robin(spreads trials evenly) andby-stimulus(keeps every trial of a stimulus on one shard). Strategies receive inertShardableItem[]descriptors (never live trial closures) and return the shard keys to run. Custom strategies load from a module specifier (--shard-strategy ./my-strategy.jsor a package name) via the shared plugin resolver and must exportcreateShardStrategy(): ShardStrategy(trusted code, no sandbox). Exports:ShardStrategy,ShardableItem,ShardSelectContext,roundRobinStrategy,byStimulusStrategy,resolveShardStrategy,validateSelectedShardKeys,BUILT_IN_SHARD_STRATEGIES,DEFAULT_SHARD_STRATEGY. - Each
trial-resultJSONL line now carries an optionalshardKeyfield (the stable per-trial identity fromcomputeShardKey), making the merge step’s results→shardKey mapping direct rather than re-derived. - Multi-turn stimuli via a new
turns: [...]field — each prompt is sent sequentially to the same agent session so the agent retains full conversation context. Any grader can be scoped to a specific turn withturn: <N>(0-based, matching the gh-eval YAML convention) on its config; the pipeline slices the trajectory to that turn before grading, so scoping works for every built-in and plugin grader. Executors opt in viasupportsMultiTurn; unsupported executors fail fast with a clear error. New exports:stimulusPrompts,eventTurn,sliceTrajectoryToTurn,stampConversationTurns. (#481)
Breaking
vally-server: theoutcomestable gained a nullableshard_keycolumn (populated from the new optionalshardKeyfield on eachtrial-resultJSONL line).SCHEMA_VERSIONbumped to3. There is no migration path — pre-existing SQLite databases must be deleted and re-ingested. Note: raw per-shard outputs (under<run-id>/shard-NN-of-NN/) are not meant for direct ingestion; ingest the merged output instead.scoring.weightsnow requires a normalized distribution: all weight values must sum to 1.0 (±0.01 tolerance for two-decimal-place rounding). Previously,scoring.weightswas accepted and validated but silently ignored — all graders contributed equally regardless. Now the aggregate score is a weighted sum keyed by grader type —Σ(weight_t × avg_score_t): when multiple graders share the same type, their scores are averaged first and the type weight is applied once. Graders absent from the map receive weight 0 and are excluded from scoring. Comparative graders (e.g.pairwise) are always excluded from the weighted score. The linter (vally lint) reports ascoring-weight-sumerror when weights do not sum to approximately 1.0. (#416)- When
scoring.thresholdis configured, per-trial pass — and thereforepass@k/pass^k, the per-trial glyphs/counts, the markdown and JUnit verdicts, and thevally gradeexit code — is now determined byscore >= thresholdrather than the binary all-graders-passed flag. This applies to any threshold eval, including equal-weight ones: existing threshold evals where some graders pass and others fail will see their pass@k/pass^k numbers and verdicts shift to reflect the aggregate score. (#416) ScoringConfig.thresholdandScoringConfig.weightsare now typed as optional (threshold: number→number?,weights: Record<string, number>→Record<string, number>?). This is a type-level change that aligns the interface with existing runtime behavior — validation already accepted ascoringblock with either field absent, and an omitted threshold already defaulted to the binary all-graders-pass verdict. No spec-authoring behavior changes; but TypeScript consumers that readconfig.threshold/config.weightsmust now handleundefined. (#416)- Removed the exported
resolveThreshold()helper. It returned an implicit default threshold of1.0when none was configured, which contradicted the new verdict semantics where an omitted threshold falls back to the binary all-graders-pass flag (seeresolveGradePass). It was unused internally; consumers should readscoring.thresholddirectly (treatingundefinedas “no threshold”). (#416) run-commandgrader: empty values forstdout_contains,stderr_contains,stdout_matches, andstdout_not_matchesare now rejected at validation time (empty scalar strings, empty arrays, and empty-string array entries). Previously an empty scalar was silently treated as a no-op, so a check could vanish without warning. Remove or fill in any such entries before upgrading.vally serve: the analytics server now validates theHostheader to defend against DNS-rebinding attacks (which bypass CORS) — a malicious page can rebind its hostname to127.0.0.1and read eval data otherwise. Requests are rejected with403 forbidden_hostunless theHostis a loopback name (localhost/127.0.0.1/::1), the bound--host, an IP literal (IP-literal Hosts are inherently safe from rebinding), or a name passed via the new repeatable--allowed-host <host>flag./api/healthis exempt so liveness probes can address the server by a Service DNS name. Upgrade note: if you frontvally servewith a reverse proxy or ingress that forwards a customHost(e.g. nginxproxy_set_header Host $host;), add that hostname with--allowed-hostor requests will be rejected. The effective allow-list is logged at startup.vally-server: theoutcomestable gained astimulus_turnscolumn (JSON-serializedStimulus.turnsfor multi-turn stimuli).SCHEMA_VERSIONbumped to3. There is no migration path — pre-existing SQLite databases must be deleted and re-ingested.vally-serverschema bump 3 → 5 (breaking): theoutcomestable gained anend_reason TEXTcolumn (trajectory end reason:completedoragent_timeout). Action required: delete your analytics database and re-ingest — the server will refuse to open a pre-version-5 database.- Removed the
orphan-filesskill linter and its exportedOrphanFilesGrader.vally lintno longer reports orphanedreferences/files — it had a high false-positive rate and no rigorous definition. (#599) - Removed the grader portability tier: the
PortabilityTiertype, theGraderMetadata.portabilityfield, andGraderRegistry.getByPortability()are gone. Custom graders must drop theportabilityproperty. max_durationis now enforced (behavior change): previouslymax_durationon a stimulus was parsed but not reliably applied as a hard abort. It now is. Any eval with amax_durationshorter than the agent typically needs will be aborted and fail where before it ran to the default 120 s timeout. Review allmax_durationvalues before upgrading.- Removed the
pairwisegrader type; comparison is now a mode of thepromptjudge, run viavally compare. Eval specs that declaretype: pairwisefail validation with a migration error. - Public API (
@microsoft/vally): the pairwise grading surface was removed and renamed. Removed:PairwiseGrader,GraderExecution,GraderBehavior.execution, andGraderInput.trajectoryB. Renamed:PAIRWISE_MAGNITUDE_SCORES→COMPARISON_MAGNITUDE_SCORES,PairwiseJudgeResponseSchema→ComparisonJudgeResponseSchema,PairwiseJudgeResponse→ComparisonJudgeResponse,PairwiseRubricResult→ComparisonRubricResult,PairwiseMagnitude→JudgeMagnitude. Also removedPairwiseGraderConfig(the comparison judge reusesPromptGraderConfig). New comparison contract types:GraderComparison,GraderComparisonInput,GraderComparisonResult,ComparisonEntry,ComparisonWinner,ComparisonMagnitude. A judge advertises comparison support by implementing the optionalGrader.compare()method (replacing the oldbehavior.execution === "comparative"flag). vally compare:--run-a/--run-bare now--baseline/--treatment, plus an experiment-directory form (vally compare <experiment-dir>).--eval-specis no longer required.vally-server: theoutcomestable gained abaselinecolumn (the experiment’s baseline variant, recorded pertrial-resultrecord). Part of the schema bump above; delete the analytics database and re-ingest.
Fixed
vally eval--workspaceplacement failures now report where the original workspace was preserved (or that a backup directory was orphaned) instead of failing silently.