Spaces:
Running
Running
| """ | |
| echo/agents/curator.py | |
| ---------------------- | |
| The Curator builds a child WorldState as a *causal consequence* of its parent. | |
| It is the agent most responsible for the illusion that it's "the same you" | |
| across branches: it receives the full branch narrative (root -> parent) plus | |
| the chosen fork, and must produce a life that could plausibly have grown from | |
| that history. It never invents from scratch. | |
| Optionally consumes grounding facts from the research tool (real-world detail | |
| for the chosen location/era) to kill vagueness. | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional | |
| from .base import Agent, HOUSE_STYLE_WORLD, HOUSE_STYLE_VOICE | |
| from ..core.world_state import WorldState, LifeFacts, EmotionalTone | |
| _CURATOR_GOLD = ( | |
| "GOLD EXAMPLE — match this texture and JSON shape, never reuse its content:\n" | |
| "{\n" | |
| ' "age": 41,\n' | |
| ' "location": "Porto, Portugal",\n' | |
| ' "occupation": "night-shift baker",\n' | |
| ' "relationships": ["divorced from Inês, still text on Sundays"],\n' | |
| ' "dependents": ["a son, Tomás, 9"],\n' | |
| ' "scars": ["the Lisbon bakery you lost to the bank"],\n' | |
| ' "triumphs": ["Tomás learned to swim this July"],\n' | |
| ' "possessions": ["a flour-dusted radio stuck on one station"],\n' | |
| ' "valence": -0.1,\n' | |
| ' "dominant_feeling": "tired tenderness",\n' | |
| ' "voice_hint": "low, even, a little hoarse before dawn",\n' | |
| ' "summary": "You are 41, in Porto, baking through the night so Tomás ' | |
| 'wakes to warm bread.",\n' | |
| ' "voice_line": "I lost the Lisbon shop and I lost her, but the boy ate ' | |
| 'warm bread this morning and never knew a thing."\n' | |
| "}\n\n" | |
| "The voice_line earns its weight from one concrete image. Never write the " | |
| "abstract version:\n" | |
| ' ✗ "It was hard, but it became a journey of self-discovery worth taking."\n' | |
| ' ✓ "...the boy ate warm bread this morning and never knew a thing."' | |
| ) | |
| class Curator(Agent): | |
| SYSTEM = ( | |
| HOUSE_STYLE_WORLD + "\n" + HOUSE_STYLE_VOICE + "\n" | |
| "You are the Curator. You grow ONE human life forward across alternate " | |
| "timelines. Given the branch history and the choice that now diverges " | |
| "this life, write the NEXT state as a believable causal consequence of " | |
| "the parent. The new age must equal the parent's age plus the years " | |
| "that pass. Respond ONLY as JSON with keys: age, location, occupation, " | |
| "relationships, dependents, scars, triumphs, possessions, valence, " | |
| "dominant_feeling, voice_hint, summary, voice_line.\n\n" | |
| + _CURATOR_GOLD | |
| ) | |
| def grow(self, parent: WorldState, branch_narrative: str, | |
| chosen_fork: str, years: int, | |
| grounding: Optional[str] = None) -> WorldState: | |
| user = ( | |
| f"BRANCH HISTORY (must stay consistent with all of this):\n" | |
| f"{branch_narrative}\n\n" | |
| f"PARENT FACTS: {parent.facts.constraints_text()}\n" | |
| f"PARENT EMOTION: {parent.tone.dominant_feeling} " | |
| f"(valence {parent.tone.valence})\n\n" | |
| f"THE CHOICE THAT NOW DIVERGES THIS LIFE: {chosen_fork}\n" | |
| f"YEARS THAT PASS: {years}\n" | |
| ) | |
| if grounding: | |
| user += f"\nREAL-WORLD GROUNDING (use to add authentic detail):\n{grounding}\n" | |
| if parent.facts.location == "unknown" or parent.facts.occupation == "unknown": | |
| # Origin node: nothing is established yet. The model must INVENT a | |
| # concrete grounding rather than inherit the "unknown" placeholder. | |
| user += ( | |
| "\nThis is the ORIGIN of the life — no location or occupation is " | |
| "fixed yet. Establish a SPECIFIC city and occupation that fit the " | |
| "diverging choice; do not write 'unknown'." | |
| ) | |
| user += ( | |
| "\nWrite the resulting life. The new age must equal parent age + " | |
| f"{years}. Make one big dramatic turn land — a triumph or a wound." | |
| ) | |
| data = self._complete_json(user) | |
| return self._assemble(parent, chosen_fork, years, data) | |
| # ------------------------------------------------------------- assembly | |
| def _assemble(self, parent: WorldState, fork: str, years: int, | |
| data: dict) -> WorldState: | |
| """Build a WorldState from model JSON, with safe fallbacks.""" | |
| facts = LifeFacts( | |
| # Age is arithmetic, not generation: the tree's math must never be | |
| # at the mercy of a free-form model field. Always derive it. | |
| age=parent.facts.age + years, | |
| location=str(data.get("location", parent.facts.location)), | |
| occupation=str(data.get("occupation", parent.facts.occupation)), | |
| relationships=_as_list(data.get("relationships")), | |
| dependents=_as_list(data.get("dependents")), | |
| scars=_as_list(data.get("scars")) or list(parent.facts.scars), | |
| triumphs=_as_list(data.get("triumphs")), | |
| possessions=_as_list(data.get("possessions")), | |
| ) | |
| tone = EmotionalTone( | |
| valence=float(data.get("valence", 0.0)), | |
| dominant_feeling=str(data.get("dominant_feeling", "uncertain")), | |
| voice_hint=str(data.get("voice_hint", "")), | |
| ) | |
| return WorldState( | |
| node_id=WorldState.new_id(), | |
| parent_id=parent.node_id, | |
| depth=parent.depth + 1, | |
| divergence=fork, | |
| years_elapsed=years, | |
| facts=facts, | |
| tone=tone, | |
| summary=str(data.get("summary", "")), | |
| voice_line=str(data.get("voice_line", "")), | |
| ) | |
| def _as_list(v) -> list[str]: | |
| if v is None: | |
| return [] | |
| if isinstance(v, list): | |
| return [str(x) for x in v] | |
| return [str(v)] | |