Skip to content

Terrarium in soul-protocol: reproduction with lineage - #313

Open
prakashUXtech wants to merge 4 commits into
devfrom
integration/terrarium
Open

prakashUXtech wants to merge 4 commits into
devfrom
integration/terrarium

Conversation

@prakashUXtech

@prakashUXtech prakashUXtech commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Integration branch for Terrarium's soul-protocol work. Do not merge without review — this is the gate.

Terrarium is a watchable agent civilization: citizens are Souls that live in a world with scarce credits, build things, remember, and have children. Design and contract live in qbtrix/paw-workspace#207. Soul Protocol's part is reproduction.

What has landed here

Slice PR What
T1 #312 Soul.fork() — a soul produces a child soul with lineage
T1a #314 birth(evolution=...) — so a lineage can actually diverge

The change

Soul.fork() mints a new DID for the child, records parent_did, bumps generation, deep-copies the DNA, and drifts each OCEAN trait by an independent uniform delta clamped to 0..1. Memory inheritance defaults to core plus procedural, because how-tos passing down is how culture spreads; episodic is always empty on a child. Lineage is a separate axis from reincarnation, and both new fields default so existing souls load unchanged. A soul fork CLI command exposes it with --json.

Three things to look at

A bug this surfaced. reincarnate() built its Identity without the lineage fields, so reincarnating a forked soul orphaned it at generation 1 and cut the family tree on every rebirth. Fixed with a test that forks, reincarnates the child, and checks both axes.

Forking was genetically inert by default. immutable_traits defaults to ["personality", "core_values"], and the personality category gates all five OCEAN traits, so a default-born soul forked into an exact OCEAN clone whatever drift was asked for. Measured: a parent at openness 0.6 produced a child at exactly 0.6.

So birth() gained an evolution parameter. It had no way to set EvolutionConfig at all, which meant a consumer had to reach into soul._evolution.config from another repo. With the parameter, the same parent born with {"immutable_traits": ["core_values"], "mutation_rate": 0.08} produces a child at 0.5893. The clone case is pinned by a test so it cannot regress unnoticed.

Whether drift-at-conception should have its own switch rather than inheriting the in-life mutation freeze is a real protocol question, and it wants an RFC rather than a decision buried here.

Verification

Full suite locally: 3258 to 3284 passed, the delta being exactly the 26 new tests in tests/test_fork.py. Zero new failures. The six errors are pre-existing and environmental — tests/cross_runtime/conftest.py calls asyncio.get_event_loop(), which raises on Python 3.14 while CI runs 3.11 to 3.13. I checked independently that this diff touches neither that directory nor any conftest. test_schemas.py clean, so no schema regeneration was needed for the second change. Ruff check and format clean.

Note on checks: ci.yml triggers on PRs into main and dev, so the feature PRs into this integration branch ran only the quality gate. This PR is where the 3.11/3.12/3.13 matrix actually runs. Read the checks here, not there.

Known downstream constraint

pocketpaw depends on the published soul-protocol[engine]>=0.3.1, so birth(evolution=...) will not reach it until a release. Because birth() only warns on unknown keyword arguments rather than raising, a consumer on an older version that passes it would get silently frozen traits. Terrarium's citizen seeding therefore verifies the resulting config rather than assuming the argument landed.

Follow-up: seedable drift

Soul.fork() now takes rng: random.Random | None. When a caller passes a seeded generator, every OCEAN drift draw comes from it, so the same parent and seed give the same child traits; the DID is still minted fresh per call. With no rng the behaviour is exactly what it was. The CLI exposes it as soul fork --seed N. The header comments that tagged this work by the downstream project name now say (lineage), which is the protocol-side name for the feature. Suite: 3294 passed, 3 skipped on Python 3.12.

A soul can now produce a child soul. `Soul.fork()` mints a new DID, records
`parent_did` and bumps `generation`, deep-copies the DNA, and drifts each of
the five OCEAN traits by an independent uniform delta clamped to 0..1.

Lineage is a separate axis from reincarnation. `incarnation` and
`previous_lives` track the same soul living again; `parent_did` and
`generation` track a new soul descended from another. Both fields default,
so souls written before this change load unchanged.

Memory inheritance is opt-in per tier and defaults to core + procedural,
because how-tos passing down is how culture spreads. Episodic is always
empty on the child even when the caller asks for it. Bonds, role, the trust
chain and the parent's mutation log stay with the parent.

`EvolutionConfig.mutation_rate` has been serialized in every soul since the
evolution system shipped and nothing read it. It is now the default drift
width.

Known consequence: `immutable_traits` defaults to `["personality",
"core_values"]` and fork honours it, so a default-configured parent forks an
OCEAN clone whatever the drift. `soul fork` warns and reports the frozen
traits in `--json`; drop "personality" from the parent's `immutable_traits`
to let children diverge.

Adds `soul fork <parent> --child <name>` with --drift, --inherit, --charter,
--output and --json. `soul inspect` and `soul status` show Parent and
Generation for forked souls only. `public_profile()` carries the lineage.
reincarnate() built the new Identity without parent_did or generation, so a
reborn child came back orphaned at generation 1. Lineage and rebirth are
independent axes: a rebirth is a new life for the same soul, which is still
its parent's child and still at the same depth in the family tree. Only the
rebirth axis advances.

Adds the case the fork tests missed — reincarnating the gen-1 parent made the
assertions trivially true either way, so the drop went unnoticed. The new test
forks, then reincarnates the child, and checks both axes.

Also names origin_story and prime_directive in fork()'s does-not-inherit list;
they were already excluded, just undocumented.
@github-actions

github-actions Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Issues (must fix)

  • PR title does not follow Conventional Commits format (e.g. feat: add recall API, fix(memory): handle empty tiers).
  • No evidence of local testing found. Please include terminal output or screenshots.

Heads up

  • Large PR detected (1029 lines across 10 files). Consider splitting into smaller PRs.

Please update your PR to address these points.

Soul.birth() had no way to set EvolutionConfig, so a caller who wanted a soul
whose children could diverge had to reach into soul._evolution.config after the
fact. That matters because immutable_traits defaults to ["personality",
"core_values"], and the personality category gates all five OCEAN traits — so a
default-born soul forks into an exact OCEAN clone however large a drift is
asked for.

birth(evolution=...) accepts an EvolutionConfig or a plain dict and wires it
into SoulConfig. Passing immutable_traits without "personality" is now the
supported way to let a lineage diverge.

Three tests: a default-born parent forks to a clone (the trap, pinned so it
cannot regress silently), a dict-configured parent produces a child that drifts
within mutation_rate, and an EvolutionConfig object is accepted as-is.
fork() and _drift_ocean() take rng: random.Random | None; a seeded instance
makes the child's OCEAN drift reproducible while the DID stays fresh. The CLI
exposes it as soul fork --seed N. Header and banner comments that tagged this
work by the downstream project now say (lineage), the protocol-side name.
@github-actions

Copy link
Copy Markdown

This PR has been automatically marked as stale because it has not had activity in the last 14 days. It will be closed in 7 days if no further activity occurs. If you're still working on this, please push an update or leave a comment.

@github-actions github-actions Bot added the stale label Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant