Skip to content

Saving & loading

An optimized program carries a small, explicit set of prompt parameters. ProgramPersistence writes those parameters to JSON and applies them to a freshly constructed program, so optimization can run once and the result can be loaded at startup.

Save and load

def save(program: DynamicPredict, path: String): Either[DspyError, Unit] =
  ProgramPersistence.save(program, path)

def load(fresh: DynamicPredict, path: String): Either[DspyError, DynamicPredict] =
  ProgramPersistence.load(fresh, path)

load does not reconstruct an executable program. It takes a fresh program whose architecture and runtime bindings were created normally in Scala, then returns a new immutable value with the saved optimizable parameters applied.

The optimizable-parameter contract

Every optimizer-addressable leaf exposes the same writable OptimizableParameters:

Field Meaning
instructions: Option[String] The signature-level prompt instructions.
demos: Vector[Example] Few-shot examples rendered by the adapter.
config: DynamicValue.Record Module-level LM option defaults; per-call options still win.

This contract is shared by Predict, ChainOfThought, and DynamicPredict. Framework composites expose the same parameters for each stable leaf—for example, ProgramOfThought exposes its generator, regenerator, and answerer. The executable predictor is not used as a parameter carrier.

OptimizableLeaf[P] is a lens onto these parameters. Its instances obey:

  • Get-Put: writing the parameters just read is a no-op.
  • Put-Get: reading after a write returns the written parameters.
  • Put-Put: only the last parameters written matter.
  • Frame: writing parameters does not change predictor metadata.

At the composite level, OptimizableStructure.replace(program, OptimizableStructure.read(program)) returns the original program, and an arity-matched replacement reads back as the same parameter vector.

Algebraically, OptimizableLeaf[P] is a lawful lens focused on one OptimizableParameters, while OptimizableStructure[P] describes a finite, ordered collection of such leaves. Composition concatenates the child structures, parameter-free structure contributes the empty vector, and Vector[OptimizableParameters] forms the parameter monoid under concatenation. Named inspection is checked against the canonical structure so labels cannot silently reorder or substitute parameters.

Metadata and execution resources

OptimizableMetadata is inspectable but not writable through optimizer replacement. It contains the signature field structure (with instructions removed) and module name. Loading preserves that metadata from the fresh target program.

The following are also not persisted:

  • program code and composite structure;
  • runtime/model resolution and adapter configuration;
  • output JSON schemas, bound LMs, and tools;
  • callbacks, traces, history, and prior predictions.

This boundary prevents a saved prompt artifact from replacing architecture or live execution resources. Recreate and configure those normally, then load state into it.

OptimizableLeaf IDs and compatibility

The JSON object keys are predictor-0, predictor-1, and so on. They are ordinal IDs derived from the root OptimizableStructure leaf order:

  • JSON object order does not matter.
  • Missing and unknown ordinals are rejected.
  • An equal-size reorder cannot be detected, because the IDs are not semantic names.

Load only into a compatible program with the same optimizable-leaf count and leaf order. Structural display names are useful diagnostics, but they are not persisted identity.

Each entry has this shape:

{
  "optimizableParameters": {
    "optimizable-0": {
      "instructions": "Answer concisely.",
      "demos": [],
      "config": { "temperature": 0.2 }
    }
  }
}

The former entry format containing a full signature layout is unsupported. There is no legacy migration path: regenerate the artifact with the current code. The field structure now belongs exclusively to the fresh program's metadata.

State-only versus whole-program persistence

dspy4s supports state-only JSON. It does not implement Python DSPy's save_program=True, pickle/cloudpickle artifacts, or modules_to_serialize.

Next: Streaming.