npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scenesystems/effect-search

v0.8.1

Published

Effect-native black-box optimization for TypeScript

Readme

@scenesystems/effect-search

@scenesystems/effect-search is black-box optimization for programs built with Effect. Use it when you can evaluate a configuration but cannot express its quality as a closed-form or differentiable function: benchmark scores, model quality, operating cost, or the outcome of an experiment.

A SearchSpace describes the valid configurations and infers their TypeScript type. An Optimization asks a Sampler for a configuration, runs your Effect objective, records the resulting Trial, and returns that history to the sampler before its next suggestion. Because the optimization owns trial states, sampler checkpoints, and search-space identity, it can be inspected, snapshotted, and resumed.

The samplers compute with @scenesystems/effect-math. Cached objective inputs and search artifacts get stable content identities from @scenesystems/digest. @scenesystems/effect-dsp builds its prompt optimizers on this package.

Reusable trial schemas, history, stop controls, event streams, generic schema-parameterized storage, and artifact persistence live in @scenesystems/effect-study. effect-search depends on that lower-level package and specializes it with optimization schemas, checkpoints, and replay policy; effect-study does not depend on effect-search. Use effect-study directly for fixed-input evaluation or non-numeric observations. Sampling, ranking, pruning, and optimization recovery remain effect-search responsibilities.

Installation

bun add @scenesystems/effect-search effect

Effect ^4.0.0 is the only required peer dependency. Platform implementations such as @effect/platform-bun or @effect/platform-node are needed only when an application uses filesystem-backed persistence. Install an Effect SQL implementation only when using the SQL-backed cache layer.

Basic use

The optimization below minimizes a two-dimensional function. SearchSpace.make validates the definition and carries the inferred configuration type through the objective and the result.

import { Effect, Match, Number as Num } from "effect"
import * as Numeric from "@scenesystems/effect-math/Numeric"
import { Optimization, Sampler, SearchSpace } from "@scenesystems/effect-search"

export const program = Effect.gen(function* () {
  const space = yield* SearchSpace.make({
    x: SearchSpace.float(-5, 5),
    y: SearchSpace.float(-5, 5)
  })

  const result = yield* Optimization.minimize(
    new Optimization.FlatOptions({
      space,
      sampler: Sampler.tpe(new Sampler.TpeOptions({ seed: 42 })),
      objective: ({ x, y }) =>
        Effect.succeed(Num.sum(Numeric.pow(Num.subtract(x, 2), 2), Numeric.pow(Num.sum(y, 1), 2))),
      trials: 50
    })
  )

  yield* Match.value(result).pipe(
    Match.tag("SingleObjective", ({ bestTrial }) =>
      Effect.log("Best trial", { value: bestTrial.state.value, config: bestTrial.config })
    ),
    Match.tag("MultiObjective", () => Effect.void),
    Match.exhaustive
  )
})

The objective is an ordinary Effect, so it can use services, fail with typed errors, and run concurrently. Each trial records its configuration and a lifecycle state: running, completed, failed, pruned, or cancelled. The result is a tagged union because the same optimization machinery returns a Pareto front when there are several objectives.

Search spaces

A space is a record of dimensions. SearchSpace.float takes bounds, an optional step, and an optional scale: "log" for parameters that vary over orders of magnitude. SearchSpace.int takes bounds and an optional step. SearchSpace.categorical takes a readonly array of literals, SearchSpace.boolean is a two-value shortcut, and SearchSpace.fidelity marks the budget dimension that HyperBand and BOHB schedule over.

Conditional spaces branch on a categorical value. SearchSpace.makeConditional combines shared dimensions with a SearchSpace.switchOn over a non-empty Chunk of SearchSpace.when branches, and the inferred type is a discriminated union that Match can exhaust.

import { Chunk, Effect, Match, Number as Num, Tuple } from "effect"
import * as Numeric from "@scenesystems/effect-math/Numeric"
import { Optimization, Sampler, SearchSpace } from "@scenesystems/effect-search"

export const program = Effect.gen(function* () {
  const linear = yield* SearchSpace.make({
    learningRate: SearchSpace.float(1e-4, 1e-1, { scale: "log" }),
    regularization: SearchSpace.float(0, 1)
  })
  const tree = yield* SearchSpace.make({
    maxDepth: SearchSpace.int(2, 12),
    minSamplesLeaf: SearchSpace.int(1, 6)
  })
  const space = yield* SearchSpace.makeConditional(
    { model: SearchSpace.categorical(Tuple.make("linear", "tree")) },
    SearchSpace.switchOn("model", Chunk.make(SearchSpace.when("linear", linear), SearchSpace.when("tree", tree)))
  )

  return yield* Optimization.minimize(
    new Optimization.FlatOptions({
      space,
      sampler: Sampler.tpe(new Sampler.TpeOptions({ seed: 17 })),
      trials: 45,
      objective: (config: SearchSpace.Type<typeof space>) =>
        Match.value(config).pipe(
          Match.when({ model: "linear" }, ({ learningRate }) =>
            Effect.succeed(Numeric.pow(Numeric.log10(learningRate), 2))
          ),
          Match.when({ model: "tree" }, ({ maxDepth }) =>
            Effect.succeed(Numeric.pow(Num.divideUnsafe(Num.subtract(maxDepth, 7), 7), 2))
          ),
          Match.orElseAbsurd
        )
    })
  )
})

SearchSpace.Type<typeof space> names the configuration type when you need it outside the optimization. SearchSpace.extend composes an existing space with additional dimensions.

Samplers and schedulers

| Constructor | Suitable space | Use it for | | ----------------------- | ---------------------------------------- | -------------------------------------------------------- | | Sampler.random() | Any, including conditional | Baselines, broad exploration, and cheap objectives | | Sampler.grid() | Small finite spaces | Exhaustive enumeration | | Sampler.tpe() | Mixed, categorical, or conditional | Sequential model-guided search; also multi-objective TPE | | Sampler.cmaEs() | Continuous and integer, single objective | Evolutionary search and local refinement | | Sampler.gpBo() | Continuous and integer, single objective | Gaussian-process Bayesian optimization | | Scheduler.hyperband() | Spaces with a fidelity dimension | Successive halving across budgets | | Scheduler.bohb() | Spaces with a fidelity dimension | HyperBand allocation with TPE suggestions |

Start with TPE for mixed spaces and compare it against random search on the same objective and budget. Use grid search only when the finite product is small enough to enumerate. CMA-ES and GP-BO reject categorical dimensions. HyperBand and BOHB require a SearchSpace.fidelity dimension and are passed to Optimization.run as the scheduler option in place of a sampler.

Joint categorical TPE supports at most 65,536 combinations across its dimensions. Larger products fail with checked InvalidSamplerConfig before the joint domain is allocated. nEiCandidates limits candidate draws, not domain size. This limit applies when model-driven joint categorical sampling begins; random startup does not enumerate the domain.

A seeded sampler reproduces its suggestions when it sees the same ordered trial history and a compatible checkpoint. The optimization as a whole is reproducible only if the objective, clock, external services, and observation order are too. Concurrent evaluation can change completion order, so a seed alone does not guarantee identical results under every concurrency setting.

Effect 4 changed the seeded random sequence used by the samplers, so the same numeric seed does not reproduce the Effect 3 suggestion sequence. Start a new run after upgrading rather than expecting an Effect 3 snapshot or RNG trace to replay under Effect 4; snapshot resumption requires a checkpoint produced by the compatible implementation. The checked-in numerical fixtures themselves were not changed as part of this migration.

TPE accepts the built-in acquisition names "ei", "pi", and "thompson", or a custom Acquisition.Acquisition created with Acquisition.make. Use Acquisition.isAcquisition when narrowing unknown extension values.

Running optimizations

Optimization.minimize and Optimization.maximize run a single-objective optimization to completion. Optimization.run takes an explicit direction or a directions array and accepts a scheduler. All three share the same options:

  • Stopping: trials, maxDuration, maxCost, targetValue, or noImprovementWindow, combined by stopMode.
  • Concurrency: concurrency runs trials in parallel while the sampler keeps suggesting from imputed pending results.
  • Robustness: trialTimeout, a retrySchedule, and a pruningPolicy.
  • Warm starts: priorTrials and priorWeight seed the history; evaluationsPerTrial averages noisy objectives.

With several directions, the objective returns a vector and the result is MultiObjective with a paretoFront. The Pareto module provides dominance checks, front extraction, and two-dimensional hypervolume for comparing runs.

Pruning.threshold creates the built-in threshold policy. Objective callbacks receive Pruning.Runtime as their second argument, with report, heartbeat, requestStop, and scheduled resource controls.

import { Array as Arr, Effect, Iterable, Match, Number as Num, Tuple } from "effect"
import { type Direction, Optimization, Sampler, SearchSpace } from "@scenesystems/effect-search"

export const program = Effect.gen(function* () {
  const space = yield* SearchSpace.make({
    replicas: SearchSpace.int(1, 8),
    cacheMb: SearchSpace.int(64, 1024, { step: 64 })
  })

  const result = yield* Optimization.run(
    new Optimization.FlatOptions({
      space,
      sampler: Sampler.tpe(new Sampler.TpeOptions({ seed: 919 })),
      directions: Arr.replicate<Direction.Direction>("minimize", 2),
      trials: 40,
      objective: ({ replicas, cacheMb }) => {
        const latency = Num.sum(Num.divideUnsafe(100, replicas), Num.divideUnsafe(2000, cacheMb))
        const cost = Num.sum(Num.multiply(replicas, 1.5), Num.divideUnsafe(cacheMb, 256))
        return Effect.succeed(Tuple.make(latency, cost))
      }
    })
  )

  return Match.value(result).pipe(
    Match.tag("MultiObjective", ({ paretoFront }) => Iterable.size(paretoFront)),
    Match.tag("SingleObjective", () => 1),
    Match.exhaustive
  )
})

Optimization.stream runs the same optimization but emits typed OptimizationEvent.OptimizationEvent values for every trial and optimization lifecycle change. Fold, filter, or publish the stream with ordinary Stream operators; Progress.tap() adds the ready-made terminal progress reporter without changing stream values.

Persistence and resumption

Optimization.snapshot captures the trials, the next trial number, the sampler checkpoint, and compatibility metadata from a result or an open handle. OptimizationSnapshot.OptimizationSnapshot is the schema, so encode it for storage and decode it later. Optimization.resume validates the space and settings against the snapshot before continuing.

Completed results capture the sampler's final state. Interruption cancels outstanding reservations before saving a checkpoint; restoring an open handle's snapshot likewise cancels reservations whose workers no longer exist. Completed observations and trial numbers are retained, and new trials receive new numbers. Restore rejects duplicate or invalid identities, inconsistent counters, and invalid configurations before invoking sampler restoration. OptimizationSnapshot.decodeUnknown can normalize stale derived diagnostics, but never repairs trial identities.

import { Effect, Number as Num, Schema } from "effect"
import { Optimization, OptimizationSnapshot, Sampler, SearchSpace } from "@scenesystems/effect-search"

export const program = Effect.gen(function* () {
  const space = yield* SearchSpace.make({ x: SearchSpace.float(-5, 5) })
  const objective = ({ x }: SearchSpace.Type<typeof space>) => {
    const distance = Num.subtract(x, 1.25)
    return Effect.succeed(Num.multiply(distance, distance))
  }

  const firstLeg = yield* Optimization.minimize(
    new Optimization.FlatOptions({
      space,
      sampler: Sampler.tpe(new Sampler.TpeOptions({ seed: 404 })),
      trials: 20,
      objective
    })
  )
  const stored = yield* Schema.encodeEffect(OptimizationSnapshot.OptimizationSnapshot)(
    yield* Optimization.snapshot(firstLeg)
  )

  const snapshot = yield* Schema.decodeEffect(OptimizationSnapshot.OptimizationSnapshot)(stored)
  return yield* Optimization.resume(
    new Optimization.ResumeOptions({
      space,
      sampler: Sampler.tpe(new Sampler.TpeOptions({ seed: 404 })),
      snapshot,
      direction: "minimize",
      trials: 20,
      objective
    })
  )
})

For long-running work, import StudyStorage from @scenesystems/effect-study/StudyStorage and install OptimizationStorage.layerFileSystem(StudyStorage.fileSystemOptions(directory)). Generic storage owns the run-bound recording file and defaults to study-storage.jsonl. OptimizationStorage opens the optimization run with Search-owned trial and snapshot codecs, storing trials as events and snapshots as cursor-bound checkpoints. One location belongs to one optimization; trial numbers supply stable record identities, so identical append retries are deduplicated and conflicting contents fail. Optimization.resumeFromStorage or Optimization.resumeFromStorageStream continue from that state. OptimizationStorage.makeFileSystem is the effectful filesystem constructor. OptimizationStorage.make opens an ambient StudyStorage, while OptimizationStorage.layer provides that specialization as a Layer; both retain typed persistence failures during acquisition. Filesystem-backed storage also needs the platform FileSystem and Path services, which @effect/platform-bun or @effect/platform-node provide. Optimization persistence does not require an artifact sink or artifact context.

Recovery filters the complete append-only trial log against the exact snapshot it loaded, so a concurrently appended snapshot cannot advance the replay boundary and hide intervening trials. Storage must retain that log; this does not provide distributed ownership of an optimization, so callers must still coordinate which process resumes execution.

Artifacts are independent from checkpoints. Import ArtifactContext and ArtifactSink from @scenesystems/effect-study/ArtifactContext and @scenesystems/effect-study/ArtifactSink. Construct new ArtifactContext.Options({ packageVersion, runId })—the fields do not include an optimization or study ID. Emit with sink.emit(schema, artifact). For a filesystem sink use ArtifactSink.layerFileSystem(directory, fileName?); ArtifactSink.makeFileSystem remains the effectful constructor, while ArtifactSink.layer installs an already-created generic sink.

Objective caching is a separate concern. A cache avoids re-running the objective for an input that was already evaluated, keyed by a content digest of that input, while storage preserves the optimization lifecycle. Construct new ObjectiveCache.Options({ scope }) and install ObjectiveCache.layerMemory, ObjectiveCache.layerFileSystem, or ObjectiveCache.layerSql. The lower-level Cache module owns schema-keyed cache descriptors and backend services; it fingerprints schema-encoded keys with the canonical identity implementation from @scenesystems/digest.

When evaluationsPerTrial is greater than one, optimization bypasses the objective cache for every sample, including repeated configurations across trials. These independent evaluations produce the reported mean and variance; cached configuration values cannot estimate noise.

Ask and tell

When another process owns evaluation, such as a job queue or a remote worker, the optimization can hand out configurations instead of running the objective itself. Optimization.open creates a scoped handle and acquires its sampler; closing the scope releases it. Optimization.ask reserves the next typed configuration, and Optimization.tell or Optimization.fail completes that reservation. Optimization.cancel closes the handle and cancels every pending reservation. Prior observations do not consume the fresh trial budget. The handle remains the authority for trial numbers, sampler observations, events, snapshots, and the final Optimization.result.

import { Effect, Number as Num } from "effect"
import { Optimization, Sampler, SearchSpace } from "@scenesystems/effect-search"

export const program = Effect.scoped(
  Effect.gen(function* () {
    const space = yield* SearchSpace.make({ x: SearchSpace.float(-4, 4) })
    const evaluateRemotely = (config: SearchSpace.Type<typeof space>) =>
      Effect.succeed(Num.multiply(config.x, config.x))
    const handle = yield* Optimization.open(
      new Optimization.FlatOptions({
        space,
        sampler: Sampler.random({ seed: 25 }),
        direction: "minimize",
        trials: 4,
        objective: evaluateRemotely
      })
    )

    yield* Effect.gen(function* () {
      const asked = yield* Optimization.ask(handle)
      const value = yield* evaluateRemotely(asked.config)
      yield* Optimization.tell(handle, asked.trialNumber, value)
    }).pipe(Effect.repeat({ times: 3 }))

    return yield* Optimization.result(handle)
  })
)

Public surface

Every module is available as a namespace from the package root and as a subpath such as @scenesystems/effect-search/Optimization.

| Module | Scope | | ------------------------------------------------------- | ------------------------------------------------------------------------------- | | Acquisition | Built-in and custom acquisition scoring strategies | | Artifact | Search artifact provenance, payloads, and envelopes | | Cache | Schema-keyed cache descriptors, results, observers, and backend layers | | Direction | Objective comparison polarity | | Distribution | Sampling distributions and schema annotations | | Objective | Objective specifications, scalar/vector values, and normalization | | ObjectiveCache | Objective caching and memory, filesystem, and SQL layers | | Optimization | Execution, streaming, ask/tell coordination, resumption, and results | | OptimizationEvent | Optimization lifecycle schema, constructors, guards, and matching | | OptimizationSnapshot | Snapshot schema, compatibility validation, and recovery | | OptimizationStorage | Optimization schemas, checkpoints, and replay over generic study storage | | Pareto | Dominance, fronts, weights, and two-dimensional hypervolume | | Progress | Terminal event formatting, sinks, and stream tapping | | Pruning | Intermediate reports, objective runtime controls, and pruning policies | | Sampler | Suggestion strategies, options, extension contract, and checkpoints | | Scheduler | HyperBand and BOHB plans and summaries | | SearchError | Typed expected failures for spaces, optimization, samplers, storage, and trials | | SearchSpace | Dimensions, conditional branches, composition, and inferred configuration types | | Trial | Search trial states, records, guards, and matching |

Paths under internal are not exported.

Errors and boundaries

Failures surface in the Effect error channel as Schema.TaggedError values, so Effect.catchTag and Effect.catchTags work on them directly. InvalidSearchSpace and InvalidOptimizationConfig reject definitions before any trial runs. InvalidSamplerConfig, SamplerSearchSpaceUnsupported, and SamplerObjectiveUnsupported report a sampler that cannot serve the space or the objective shape. TrialError wraps an objective failure with its trial number, NoSuccessfulTrials means a completed optimization has no best trial to report, and SamplerExhausted means a finite sampler has nothing left to suggest.

The package owns the search loop and its state. It does not own the objective's resources, retries beyond the schedule you pass, or the durability of the directory or database behind storage and caches. Reproducibility of the objective itself remains your responsibility.

Examples

The examples directory contains one runnable program per capability. Start with the quick start, then follow the topic you need: conditional spaces and space composition; multi-objective optimization, constrained optimization, and HyperBand and BOHB; snapshot resume, storage resume, and trial caching; ask and tell, streaming events, and parallel evaluation; sampler comparison and acquisition strategies.

Status

This package is pre-1.0. Minor releases may change public APIs; pin a compatible version and review the changelog when upgrading.

Contributing and support

Read the repository contributing guide before opening a pull request. Report defects and request changes through GitHub issues. For security concerns, follow the security policy.

Attribution

The sampler behavior and numerical fixtures draw on ideas and reference results from Optuna, including TPE, multi-objective TPE, and study orchestration. Optuna is distributed under the MIT License.

License

MIT. Copyright 2026 Scene Systems.