Files
Dumitru Picalau 26df88216f feat(ai): chat round, node negatives, pack builder quality fixes (#69)
Chat round (explain on selection, page starters, reactions, streamed notes, thinking), negatives on node fixed values and combines, Pack builder quality fixes from 31 real-model eval rounds, and small-model support (scoped tools, checked tool arguments on local servers, two-phase steps, fixed prefix shown in Test connection).
2026-10-17 01:23:58 +02:00

62 lines
2.4 KiB
Python

"""Shared seed-derivation helper for module handlers.
Every kind that needs reproducible randomness (wildcard pick, combine
{a|b|c} resolution, fixed_values value alternations) routes through
this same function so the same `(seed, key)` pair always yields the
same RNG state. Lifting it out of wildcard_handler.py prevents drift
when more kinds gain seed-lock support.
Independence: every roll draws from its own stream. A handler keys its
module stream by the INSTANCE (`instance_key`), not by its variable or
library id, so two instances of one module, or two modules writing the
same variable, never roll in lockstep. Inside the module, each value,
rule action, option and `{…}` / `@{…}` site gets a child stream
(`stream_key` + `engine.syntax.streams`), so adding, editing or firing
one pick never moves another.
"""
from __future__ import annotations
import hashlib
import random
from typing import Any
def derive_module_rng(seed: int, key: str) -> random.Random:
"""Per-module RNG derived from (seed, key).
`key` is a stable per-instance string (see `instance_key`). Different
keys under the same seed yield independent streams.
Used for BOTH locked and unlocked picks — applying the same
derivation either way is what lets lock capture the visible
roll: locking with `locked_seed = chain_seed` reproduces the
unlocked pick exactly because both paths derive from
`sha256(chain_seed:key)`.
"""
digest = hashlib.sha256(f"{int(seed)}:{key}".encode()).hexdigest()
return random.Random(int(digest[:16], 16))
def instance_key(ctx: Any, fallback: str) -> str:
"""The stream key of the module instance running now.
The pipeline sets `__wp_instance_key__` (bundle copy + per-instance
uid) before each handler runs. Handlers called outside a pipeline run
(unit tests, previews) have none and fall back to `fallback`, their
old key, so those callers keep their seeds.
"""
if not isinstance(ctx, dict):
return fallback
# Without the pipeline's key, the instance uid still keeps two
# instances apart.
for k in ("__wp_instance_key__", "__wp_current_module_uid__"):
key = ctx.get(k)
if isinstance(key, str) and key:
return key
return fallback
def stream_key(seed: int, key: str) -> str:
"""The resolver stream key for text resolved under `(seed, key)`."""
return f"{int(seed)}:{key}"