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.5.0

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. A Study 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 study owns trial states, sampler checkpoints, and search-space identity, an optimization can be inspected, snapshotted, and resumed.

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

Installation

npm install @scenesystems/effect-search effect @effect/platform @effect/experimental

Effect ^3.22.1 is a required peer dependency, together with @effect/platform and @effect/experimental. @effect/sql is an optional peer that is needed only for the SQL-backed cache layers.

Basic use

The study 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 } from "effect"
import { Sampler, SearchSpace, Study } 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* Study.minimize({
    space,
    sampler: Sampler.tpe({ seed: 42 }),
    objective: ({ x, y }) => Effect.succeed((x - 2) ** 2 + (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 study 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 list 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.switch over SearchSpace.when branches, and the inferred type is a discriminated union that Match can exhaust.

import { Effect, Match } from "effect"
import { Sampler, SearchSpace, Study } 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(["linear", "tree"]) },
    SearchSpace.switch("model", [SearchSpace.when("linear", linear), SearchSpace.when("tree", tree)])
  )

  return yield* Study.minimize({
    space,
    sampler: Sampler.tpe({ seed: 17 }),
    trials: 45,
    objective: (config) =>
      Match.value(config).pipe(
        Match.when({ model: "linear" }, ({ learningRate }) => Effect.succeed(Math.log10(learningRate) ** 2)),
        Match.when({ model: "tree" }, ({ maxDepth }) => Effect.succeed(((maxDepth - 7) / 7) ** 2)),
        Match.exhaustive
      )
  })
})

SearchSpace.Type<typeof space> names the configuration type when you need it outside the study. 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 Study.optimize as the scheduler option in place of a sampler.

A seeded sampler reproduces its suggestions when it sees the same ordered trial history and a compatible checkpoint. The study 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.

Running studies

Study.minimize and Study.maximize run a single-objective study to completion. Study.optimize 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.

import { Effect, Match } from "effect"
import { Sampler, SearchSpace, Study } 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* Study.optimize({
    space,
    sampler: Sampler.tpe({ seed: 919 }),
    directions: ["minimize", "minimize"],
    trials: 40,
    objective: ({ replicas, cacheMb }) => {
      const latency = 100 / replicas + 2000 / cacheMb
      const cost = replicas * 1.5 + cacheMb / 256
      return Effect.succeed([latency, cost])
    }
  })

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

Study.optimizeStream runs the same study but emits typed StudyEvent values for every trial and study lifecycle change. Fold, filter, or publish the stream with ordinary Stream operators; Study.tapTerminalProgress() is a ready-made progress sink.

Persistence and resumption

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

import { Effect, Schema } from "effect"
import { Sampler, SearchSpace, Study } 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>) => Effect.succeed((x - 1.25) ** 2)

  const firstLeg = yield* Study.minimize({ space, sampler: Sampler.tpe({ seed: 404 }), trials: 20, objective })
  const stored = yield* Schema.encode(Study.StudySnapshot)(yield* Study.snapshot(firstLeg))

  const snapshot = yield* Schema.decode(Study.StudySnapshot)(stored)
  return yield* Study.resume({
    space,
    sampler: Sampler.tpe({ seed: 404 }),
    snapshot,
    direction: "minimize",
    trials: 20,
    objective
  })
})

For long-running work, Study.StudyStorageLive keeps an append-only trial log with atomic snapshots in a directory you choose, and Study.resumeFromStorage or Study.resumeFromStorageStream continue from it. Storage needs the platform FileSystem service, which @effect/platform-bun or @effect/platform-node provide.

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 study lifecycle. Cache.SchemaCacheMemory, Cache.SchemaCacheFileSystem, and Cache.SchemaCacheSql provide the backends; Study.StudyObjectiveCacheMemory, Study.StudyObjectiveCacheFileSystem, and Study.StudyObjectiveCacheSql wire them to studies.

Ask and tell

When another process owns evaluation, such as a job queue or a remote worker, the study can hand out configurations instead of running the objective itself. Study.open creates a scoped handle. Study.ask reserves the next typed configuration, and Study.tell, Study.fail, or Study.cancel completes that reservation. The handle remains the authority for trial numbers, sampler observations, events, snapshots, and the final Study.result.

import { Effect } from "effect"
import { Sampler, SearchSpace, Study } from "@scenesystems/effect-search"

const evaluateRemotely = (config: { readonly x: number }) => Effect.succeed(config.x ** 2)

export const program = Effect.scoped(
  Effect.gen(function* () {
    const space = yield* SearchSpace.make({ x: SearchSpace.float(-4, 4) })
    const handle = yield* Study.open({
      space,
      sampler: Sampler.random({ seed: 25 }),
      direction: "minimize",
      trials: 4,
      objective: evaluateRemotely
    })

    const asked = yield* Study.ask(handle)
    const value = yield* evaluateRemotely(asked.config)
    yield* Study.tell(handle, asked.trialNumber, value)

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

Public surface

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

| Module | Scope | | --------------------------------------------- | --------------------------------------------------------------------------------------------- | | SearchSpace | Dimensions, conditional branches, composition, validation, and inferred configuration types | | Study | Optimization, ask/tell handles, snapshots, storage, objective caching, streaming, and results | | Sampler | Sampler constructors, options, checkpoints, and the sampler extension contract | | Scheduler | HyperBand and BOHB plans | | Trial | Trial records and lifecycle states | | StudyEvent | Typed lifecycle events emitted by streaming studies | | Pareto | Dominance, fronts, weights, and two-dimensional hypervolume | | Cache | Schema-aware cache descriptors with memory, file-system, and SQL layers | | Contracts | Shared schemas, identities, and objective contracts | | Errors | Typed errors for spaces, studies, samplers, and trials | | Experimental | Unstable APIs that may change outside semver guarantees |

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 InvalidStudyConfig 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 study 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. The Experimental module may change or be removed with less migration support than the other modules.

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.