"""GODSEED tool DSL — the single source of truth for the 11-tool surface. Spec is data (TOOLS); validation is `validate_call`. Semantics per the architecture contract: * out-of-range NUMBERS are CLAMPED, never rejected; * unknown tools / unknown enum values are REJECTED with a terse observation string the model can react to (e.g. "rejected: unknown kind 'cactus'"); * missing or non-numeric/malformed args are rejected (they cannot be clamped); * extra args not in the spec are silently dropped. `place_water` constraints: kind "stream" needs 2..5 path waypoints and takes no radius; kind "pool" uses path[0] as its center and requires radius_deg. `place_road` takes a path of 2..6 [lat,lon] waypoints (a paved surface spline). Pure stdlib. Other engine modules (and the mind's prompt builder) import this. """ from __future__ import annotations import math from dataclasses import dataclass from typing import Any, Optional LAT_RANGE = (-85.0, 85.0) LON_RANGE = (-180.0, 180.0) @dataclass(frozen=True) class NumberSpec: lo: float hi: float integer: bool = False @dataclass(frozen=True) class EnumSpec: values: tuple[str, ...] @dataclass(frozen=True) class TextSpec: max_len: int @dataclass(frozen=True) class PathSpec: max_points: int = 5 FLORA_KINDS = ("trees", "glowgrass", "mushrooms", "vines", "flowers", "reeds") STRUCTURE_KINDS = ( "lighthouse", "monolith", "shrine", "village", "beacon", "arch", "tower", "warehouse", "cafe", "bank", "market", "house", ) # The god (a 9B model, no grammar constraint on the live ZeroGPU path) invents # building names freely — "market_square", "skyscraper", "townhall". REJECTING # them makes a town wish build nothing and the model gives up (June 12 live: # "it says it created the town... there is no town"). So place_structure is # FORGIVING: synonyms map to a real kind, and any unknown kind falls back to a # house rather than failing. The god always builds something. STRUCTURE_SYNONYMS: dict[str, str] = { "market_square": "market", "marketsquare": "market", "square": "market", "plaza": "market", "bazaar": "market", "marketplace": "market", "stall": "market", "skyscraper": "tower", "highrise": "tower", "high_rise": "tower", "office": "tower", "spire": "tower", "obelisk": "monolith", "pillar": "monolith", "stone": "monolith", "depot": "warehouse", "storehouse": "warehouse", "storage": "warehouse", "factory": "warehouse", "barn": "warehouse", "silo": "warehouse", "coffee": "cafe", "coffeehouse": "cafe", "restaurant": "cafe", "diner": "cafe", "cafeteria": "cafe", "inn": "cafe", "tavern": "cafe", "pub": "cafe", "bakery": "cafe", "home": "house", "cottage": "house", "cabin": "house", "hut": "house", "lodge": "house", "dwelling": "house", "residence": "house", "bungalow": "house", "manor": "house", "church": "shrine", "temple": "shrine", "chapel": "shrine", "cathedral": "shrine", "townhall": "bank", "town_hall": "bank", "hall": "bank", "courthouse": "bank", "library": "bank", "museum": "bank", "school": "bank", "guildhall": "bank", "harbor": "lighthouse", "harbour": "lighthouse", "port": "lighthouse", "dock": "lighthouse", "gate": "arch", "gateway": "arch", "bridge": "arch", "tower_gate": "arch", "fire": "beacon", "torch": "beacon", "lantern": "beacon", "lamp": "beacon", "hamlet": "village", "settlement": "village", "houses": "village", } # Kinds that really mean "a whole neighborhood" -> the engine routes them to a # dense build_district instead of a single building, so the town reads dense. DISTRICT_SYNONYMS = frozenset(( "district", "neighborhood", "neighbourhood", "block", "quarter", "town", "city", "downtown", "suburb", "metropolis", "borough", "ward", )) def resolve_structure_kind(value: Any) -> str | None: """Map a model-supplied kind to a real STRUCTURE_KINDS value. None means 'this is really a district' (caller should route to build_district). Unknown-but-buildingish names fall back to 'house' — never a rejection.""" if not isinstance(value, str): return "house" v = value.strip().lower().replace(" ", "_").replace("-", "_") if v in STRUCTURE_KINDS: return v if v in DISTRICT_SYNONYMS: return None if v in STRUCTURE_SYNONYMS: return STRUCTURE_SYNONYMS[v] # substring rescue (e.g. "grand_market_hall" -> market) before defaulting for token, kind in STRUCTURE_SYNONYMS.items(): if token in v: return kind for kind in STRUCTURE_KINDS: if kind in v: return kind return "house" # any other word becomes a humble house, never a rejection WATER_KINDS = ("stream", "pool") WEATHER_KINDS = ("clear", "rain", "snow", "embers", "mist", "storm") SKY_PALETTES = ("dawn", "dusk", "night", "aurora", "ember", "void", "gold") INSCRIBE_STYLES = ("orbit", "stone") LIFE_KINDS = ("carts", "birds", "fireflies") TOOLS: dict[str, dict[str, Any]] = { "raise_terrain": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "radius_deg": NumberSpec(2, 45), "height": NumberSpec(0.01, 0.12), "roughness": NumberSpec(0, 1), }, "lower_terrain": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "radius_deg": NumberSpec(2, 45), "depth": NumberSpec(0.01, 0.10), "roughness": NumberSpec(0, 1), }, "spawn_flora": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "radius_deg": NumberSpec(1, 30), "kind": EnumSpec(FLORA_KINDS), "density": NumberSpec(0, 1), "hue": NumberSpec(0, 360), }, "place_structure": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "kind": EnumSpec(STRUCTURE_KINDS), "scale": NumberSpec(0.5, 2.0), "hue": NumberSpec(0, 360), }, "place_water": { "kind": EnumSpec(WATER_KINDS), "path": PathSpec(5), "radius_deg": NumberSpec(1, 10), # pool only "hue": NumberSpec(160, 260), }, "set_weather": { "kind": EnumSpec(WEATHER_KINDS), "intensity": NumberSpec(0, 1), }, "set_sky": { "palette": EnumSpec(SKY_PALETTES), "star_density": NumberSpec(0, 1), "moons": NumberSpec(0, 3, integer=True), }, "inscribe_wish": { "text": TextSpec(90), "style": EnumSpec(INSCRIBE_STYLES), }, "spawn_life": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "radius_deg": NumberSpec(1, 20), "kind": EnumSpec(LIFE_KINDS), "count": NumberSpec(1, 12, integer=True), "hue": NumberSpec(0, 360), }, "build_district": { "lat": NumberSpec(*LAT_RANGE), "lon": NumberSpec(*LON_RANGE), "radius_deg": NumberSpec(2, 15), "density": NumberSpec(0, 1), "hue": NumberSpec(0, 360), }, "place_road": { "path": PathSpec(6), # 2..6 [lat,lon] waypoints }, } TOOL_NAMES: tuple[str, ...] = tuple(TOOLS.keys()) def _snip(value: Any) -> str: """Terse, single-line render of a bad value for observation strings.""" s = str(value) s = "".join(ch if ch.isprintable() else "?" for ch in s) return s[:32] def _coerce_number(name: str, value: Any, spec: NumberSpec): """Returns (clamped_value, None) or (None, rejection).""" if isinstance(value, bool): return None, f"rejected: bad arg '{name}'" if isinstance(value, str): try: value = float(value.strip()) except (ValueError, AttributeError): return None, f"rejected: bad arg '{name}'" if not isinstance(value, (int, float)): return None, f"rejected: bad arg '{name}'" v = float(value) if math.isnan(v): return None, f"rejected: bad arg '{name}'" v = min(max(v, float(spec.lo)), float(spec.hi)) if spec.integer: return int(round(v)), None return round(v, 4), None def _coerce_enum(name: str, value: Any, spec: EnumSpec): if not isinstance(value, str): return None, f"rejected: unknown {name} '{_snip(value)}'" v = value.strip().lower() if v not in spec.values: return None, f"rejected: unknown {name} '{_snip(value)}'" return v, None def _coerce_text(name: str, value: Any, spec: TextSpec): if not isinstance(value, str): return None, f"rejected: bad arg '{name}'" v = "".join(ch for ch in value if ch.isprintable()).strip() if not v: return None, f"rejected: empty {name}" return v[: spec.max_len], None def _coerce_point(point: Any): """A waypoint is [lat, lon] (or {'lat':..,'lon':..}); returns [lat, lon] or None.""" if isinstance(point, dict): raw = (point.get("lat"), point.get("lon")) elif isinstance(point, (list, tuple)) and len(point) >= 2: raw = (point[0], point[1]) else: return None lat, err = _coerce_number("lat", raw[0], NumberSpec(*LAT_RANGE)) if err: return None lon, err = _coerce_number("lon", raw[1], NumberSpec(*LON_RANGE)) if err: return None return [lat, lon] DEFAULT_WATER_HUE = 190.0 DEFAULT_POOL_RADIUS = 5.0 def _validate_place_water(args: dict): """Models fumble water more than any other tool (June 12 live traces), so this one is deliberately forgiving: lat/lon synthesize a path, a flat [lat, lon] pair is wrapped, and hue / pool radius take engine defaults.""" spec = TOOLS["place_water"] kind, err = _coerce_enum("kind", args.get("kind"), spec["kind"]) if err: return None, err raw_path = args.get("path") # lat/lon supplied like every other tool -> a one-point path if not isinstance(raw_path, (list, tuple)) or not raw_path: if args.get("lat") is not None and args.get("lon") is not None: raw_path = [[args.get("lat"), args.get("lon")]] else: return None, "rejected: water needs path [[lat,lon],...] or lat+lon" # a flat [lat, lon] pair instead of [[lat, lon]] if raw_path and not isinstance(raw_path[0], (list, tuple)): raw_path = [raw_path] points = [] for p in list(raw_path)[: spec["path"].max_points]: # extra waypoints clamped off pt = _coerce_point(p) if pt is None: return None, "rejected: bad waypoint in path (each is [lat, lon])" points.append(pt) hue_raw = args.get("hue") if hue_raw is None: hue = DEFAULT_WATER_HUE else: hue, err = _coerce_number("hue", hue_raw, spec["hue"]) if err: return None, err if kind == "stream": if len(points) < 2: return None, "rejected: stream needs 2..5 waypoints" return {"kind": "stream", "path": points, "hue": hue}, None # pool: path[0] is the center; radius defaults rather than dies radius_raw = args.get("radius_deg") if radius_raw is None: radius = DEFAULT_POOL_RADIUS else: radius, err = _coerce_number("radius_deg", radius_raw, spec["radius_deg"]) if err: return None, err return {"kind": "pool", "path": points[:1], "radius_deg": radius, "hue": hue}, None def _validate_place_road(args: dict): """A road is a paved surface spline through 2..6 [lat,lon] waypoints. Path handling mirrors place_water (a flat [lat, lon] pair is wrapped; extra waypoints past the max are clamped off) but a road always needs at least two points — there is no single-point form. """ spec = TOOLS["place_road"] raw_path = args.get("path") if not isinstance(raw_path, (list, tuple)) or not raw_path: return None, "rejected: road needs path [[lat,lon],...] (2..6 points)" # a flat [lat, lon] pair instead of [[lat, lon]] if not isinstance(raw_path[0], (list, tuple, dict)): raw_path = [raw_path] points = [] for p in list(raw_path)[: spec["path"].max_points]: # extra waypoints clamped off pt = _coerce_point(p) if pt is None: return None, "rejected: bad waypoint in path (each is [lat, lon])" points.append(pt) if len(points) < 2: return None, "rejected: road needs 2..6 waypoints" return {"path": points}, None def validate_call(tool: Any, args: Any) -> tuple[Optional[dict], Optional[str]]: """Validate one tool call. Returns (canonical_args, None) or (None, rejection). Canonical args contain exactly the spec'd keys (in spec order), with numbers clamped + rounded, enums lowercased, text sanitized/truncated. """ if not isinstance(tool, str) or tool not in TOOLS: return None, f"rejected: unknown tool '{_snip(tool)}'" if args is None: args = {} if not isinstance(args, dict): return None, "rejected: args must be an object" if tool == "place_water": return _validate_place_water(args) if tool == "place_road": return _validate_place_road(args) # place_structure: forgive the kind. A "district"-ish name becomes a dense # build_district; any other unknown becomes the nearest real building (or a # house) — the god never builds nothing because it picked a word we lack. if tool == "place_structure": resolved = resolve_structure_kind(args.get("kind")) if resolved is None: district_args = { "lat": args.get("lat"), "lon": args.get("lon"), "radius_deg": args.get("radius_deg", 6), "density": args.get("density", 0.7), "hue": args.get("hue", 40), } return validate_call("build_district", district_args) args = {**args, "kind": resolved} out: dict[str, Any] = {} for name, spec in TOOLS[tool].items(): if name not in args or args.get(name) is None: return None, f"rejected: missing arg '{name}'" value = args[name] if isinstance(spec, NumberSpec): v, err = _coerce_number(name, value, spec) elif isinstance(spec, EnumSpec): v, err = _coerce_enum(name, value, spec) elif isinstance(spec, TextSpec): v, err = _coerce_text(name, value, spec) else: # pragma: no cover — only place_water has PathSpec, handled above v, err = None, f"rejected: bad arg '{name}'" if err: return None, err out[name] = v return out, None # Per-tool minimum path length (place_water's pool form accepts a single # center point; place_road always needs at least two waypoints). _PATH_MIN_POINTS: dict[str, int] = {"place_water": 1, "place_road": 2} def as_dict() -> dict: """Plain-JSON view of the tool spec (handy for prompts / grammar builders).""" out: dict[str, dict] = {} for tool, argspecs in TOOLS.items(): spec_out: dict[str, dict] = {} for name, spec in argspecs.items(): if isinstance(spec, NumberSpec): spec_out[name] = { "type": "number", "min": spec.lo, "max": spec.hi, "integer": spec.integer, } elif isinstance(spec, EnumSpec): spec_out[name] = {"type": "enum", "values": list(spec.values)} elif isinstance(spec, TextSpec): spec_out[name] = {"type": "string", "max_len": spec.max_len} elif isinstance(spec, PathSpec): spec_out[name] = { "type": "path", "min_points": _PATH_MIN_POINTS.get(tool, 1), "max_points": spec.max_points, "point": "[lat, lon]", } out[tool] = spec_out out["place_water"]["radius_deg"]["note"] = "pool only" out["place_water"]["path"]["note"] = "stream: 2..5 waypoints; pool: path[0] is center" out["place_road"]["path"]["note"] = "2..6 waypoints; a paved road laid on the surface" return out