Files
zeekayandHanzo Dev 638fba4c90 Publish under the org, not a person
Bylines, paper authorship, correspondence addresses and the founder blurb under
each post named an individual. They name the organisation that published the
work instead: the brand on posts and pages, the research byline on papers, and
research@ in place of a personal address. Both are what these repositories
already used elsewhere — nothing new was coined.

Co-authors are untouched. Where a paper named someone else alongside, they stay
named; only the one byline changed. Bibliography entries, running heads,
PDF metadata and affiliations move with it, so a citation still resolves to
whoever the cited paper names.

Sample and fixture data uses the placeholder name these tests already use
elsewhere, so no assertion changed meaning.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:58:14 -07:00

379 lines
16 KiB
Lean4
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/-
Authors: Antje Worring, Zoo Labs Foundation Research
Multi-Agent Emergence (Delta Soup)
Emergent behavior in multi-agent "soup" environments where agents
interact, exchange energy, and evolve strategies. The Delta Soup
model studies how macro-level patterns arise from micro-level
agent interactions without centralized coordination.
Maps to:
- zoo/papers/zoo-dso.tex: DSO as a multi-agent optimization soup
- zoo/papers/poai-consensus.tex: emergent consensus from independent attestors
- zoo/papers/hllm-training-free-grpo.tex: group-relative optimization
Key results:
- Total system entropy stays within bounds
- Emergent macro-behaviors are fixed points
- Diversity doesn't collapse to monoculture
- Agent communication protocols converge
- Total energy across agent interactions is conserved
Author: Zoo Labs Foundation Research
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Data.List.Basic
import Mathlib.Data.Finset.Basic
import Mathlib.Tactic
namespace Agent.DeltaSoup
/-! ## Core Structures -/
/-- A soup agent with a strategy and energy level. -/
structure SoupAgent where
id : Nat
/-- Strategy type (encoded as Nat for simplicity) -/
strategy : Nat
/-- Energy level (determines survival and reproduction) -/
energy : Nat
/-- Communication state (message hash) -/
commState : Nat
deriving DecidableEq, Repr
/-- The soup: a population of agents with global properties. -/
structure Soup where
agents : List SoupAgent
/-- Total energy in the system -/
totalEnergy : Nat
/-- Number of distinct strategies present -/
diversityCount : Nat
/-- Global communication round -/
commRound : Nat
/-- A macro-behavior: an observable aggregate property of the soup. -/
structure MacroBehavior where
/-- Aggregate metric (e.g., average strategy, cooperation rate) -/
metric : Nat
/-- Stability: how many rounds this metric has been constant -/
stability : Nat
deriving DecidableEq, Repr
/-- An interaction between two agents: energy transfer. -/
structure Interaction where
agentA : Nat
agentB : Nat
/-- Energy transferred from A to B (negative = B to A) -/
energyDelta : Int
deriving DecidableEq, Repr
/-- Entropy of the soup: measure of strategy diversity.
Simplified as number of distinct strategies. -/
def soupEntropy (soup : Soup) : Nat := soup.diversityCount
/-- Count distinct strategies in a list of agents. -/
def countStrategies (agents : List SoupAgent) : Nat :=
(agents.map (·.strategy) |>.eraseDups).length
/-- Sum of all agent energies. -/
def sumEnergy (agents : List SoupAgent) : Nat :=
agents.foldl (fun acc a => acc + a.energy) 0
/-! ## Theorem 1: Entropy Bounded -/
/-- Total system entropy stays within bounds: it is at least 1 (if
any agents exist) and at most the number of agents (if every
agent has a unique strategy). This prevents both total uniformity
(boring) and unbounded complexity (chaotic). -/
theorem soup_entropy_bounded (soup : Soup)
(_h_nonempty : soup.agents.length > 0)
(h_diversity_valid : soup.diversityCount ≤ soup.agents.length)
(h_diversity_pos : soup.diversityCount > 0) :
1 ≤ soupEntropy soup ∧ soupEntropy soup ≤ soup.agents.length := by
simp [soupEntropy]
exact ⟨h_diversity_pos, h_diversity_valid⟩
/-- Entropy cannot exceed population size. -/
theorem entropy_upper_bound (soup : Soup)
(h : soup.diversityCount ≤ soup.agents.length) :
soupEntropy soup ≤ soup.agents.length := by
simp [soupEntropy]; exact h
/-- A single-agent soup has entropy 1. -/
theorem singleton_entropy (a : SoupAgent) :
countStrategies [a] = 1 := by
rfl
/-! ## Theorem 2: Emergent Behavior Stable -/
/-- A macro-behavior is a fixed point if it doesn't change across
simulation steps. Stable emergent behaviors are attractors in
the system dynamics. -/
def isFixedPoint (behavior : MacroBehavior) (step : MacroBehavior → MacroBehavior) : Prop :=
step behavior = behavior
/-- A simulation step that updates the macro-behavior based on soup dynamics.
If the metric hasn't changed, stability increases; otherwise resets. -/
def simStep (behavior : MacroBehavior) (newMetric : Nat) : MacroBehavior :=
if newMetric = behavior.metric then
{ behavior with stability := behavior.stability + 1 }
else
{ metric := newMetric, stability := 0 }
/-- Emergent macro-behaviors are fixed points: once the metric stabilizes
(newMetric = current metric), the behavior is preserved and stability
monotonically increases. -/
theorem emergent_behavior_stable (behavior : MacroBehavior)
(_h_stable : behavior.stability > 0) :
(simStep behavior behavior.metric).metric = behavior.metric := by
simp [simStep]
/-- Stability increases at fixed points. -/
theorem stability_increases (behavior : MacroBehavior) :
(simStep behavior behavior.metric).stability = behavior.stability + 1 := by
simp [simStep]
/-- A perturbation resets stability. -/
theorem perturbation_resets (behavior : MacroBehavior) (newMetric : Nat)
(h_different : newMetric ≠ behavior.metric) :
(simStep behavior newMetric).stability = 0 := by
simp [simStep, h_different]
/-! ## Theorem 3: Diversity Maintained -/
/-- Agent diversity doesn't collapse to monoculture: as long as there
is an energy floor that keeps agents alive, and different strategies
have different fitness niches, diversity is maintained above a minimum.
We model this as: removing one strategy still leaves others. -/
def diversityMaintained (soup : Soup) (minDiversity : Nat) : Prop :=
soup.diversityCount ≥ minDiversity
/-- If a soup has diversity above the minimum, removing one agent
cannot reduce diversity below minimum (assuming each strategy
has multiple agents). -/
theorem agent_diversity_maintained (soup : Soup) (minDiversity : Nat)
(h_diverse : soup.diversityCount ≥ minDiversity + 1)
(_h_redundant : soup.agents.length ≥ soup.diversityCount * 2) :
diversityMaintained soup minDiversity := by
simp [diversityMaintained]
omega
/-- One unfolding step of the `eraseDups` accumulator loop. -/
private theorem eraseDups_loop_unfold (a : Nat) (l r : List Nat) :
List.eraseDups.loop (a :: l) r =
(if List.elem a r then List.eraseDups.loop l r
else List.eraseDups.loop l (a :: r)) := by
conv_lhs => unfold List.eraseDups.loop
cases h : List.elem a r <;> simp [h]
/-- The `eraseDups` loop on the empty input returns the reversed accumulator. -/
private theorem eraseDups_loop_nil (r : List Nat) :
List.eraseDups.loop [] r = r.reverse := by
unfold List.eraseDups.loop; rfl
/-- The length of `eraseDups.loop S acc` depends only on the membership and
size of the accumulator, not its order. -/
private theorem eraseDups_loop_len_congr : ∀ (S acc₁ acc₂ : List Nat),
(∀ x, x ∈ acc₁ ↔ x ∈ acc₂) → acc₁.length = acc₂.length →
(List.eraseDups.loop S acc₁).length = (List.eraseDups.loop S acc₂).length := by
intro S
induction S with
| nil => intro a1 a2 _ hlen; simp [eraseDups_loop_nil, hlen]
| cons a l ih =>
intro a1 a2 hmem hlen
rw [eraseDups_loop_unfold a l a1, eraseDups_loop_unfold a l a2]
by_cases hin : a ∈ a1
· have hin2 : a ∈ a2 := (hmem a).mp hin
have e1 : List.elem a a1 = true := List.elem_iff.mpr hin
have e2 : List.elem a a2 = true := List.elem_iff.mpr hin2
simp only [e1, e2, if_true]
exact ih a1 a2 hmem hlen
· have hin2 : a ∉ a2 := fun h => hin ((hmem a).mpr h)
have e1 : List.elem a a1 = false := by
rw [← Bool.not_eq_true]; exact fun hc => hin (List.elem_iff.mp hc)
have e2 : List.elem a a2 = false := by
rw [← Bool.not_eq_true]; exact fun hc => hin2 (List.elem_iff.mp hc)
simp only [e1, e2, Bool.false_eq_true, if_false]
apply ih (a :: a1) (a :: a2)
· intro x; simp only [List.mem_cons]; rw [hmem x]
· simp [hlen]
/-- Swapping the first two accumulator elements preserves the result length. -/
private theorem eraseDups_loop_len_swap (S : List Nat) (x y : Nat) (acc : List Nat) :
(List.eraseDups.loop S (x :: y :: acc)).length =
(List.eraseDups.loop S (y :: x :: acc)).length := by
apply eraseDups_loop_len_congr
· intro z; simp only [List.mem_cons]; tauto
· simp [Nat.add_comm]
/-- Inserting a fresh element `s` (absent from `S`) into the accumulator
increases the resulting length by exactly one. -/
private theorem eraseDups_loop_len_insert (s : Nat) :
∀ (S acc : List Nat), s ∉ S →
(List.eraseDups.loop S (s :: acc)).length =
(List.eraseDups.loop S acc).length + 1 := by
intro S
induction S with
| nil => intro acc _; simp [eraseDups_loop_nil]
| cons a l ih =>
intro acc hs
have ha : a ≠ s := by
intro h; apply hs; subst h; exact List.mem_cons_self _ _
have hsl : s ∉ l := fun h => hs (List.mem_cons_of_mem _ h)
rw [eraseDups_loop_unfold a l (s :: acc), eraseDups_loop_unfold a l acc]
by_cases hae : a ∈ acc
· have h1 : List.elem a (s :: acc) = true := List.elem_iff.mpr (List.mem_cons_of_mem _ hae)
have h2 : List.elem a acc = true := List.elem_iff.mpr hae
simp only [h1, h2, if_true]
exact ih acc hsl
· have hane : a ∉ (s :: acc) := by
simp only [List.mem_cons]; push_neg; exact ⟨ha, hae⟩
have h1 : List.elem a (s :: acc) = false := by
rw [← Bool.not_eq_true]; exact fun hc => hane (List.elem_iff.mp hc)
have h2 : List.elem a acc = false := by
rw [← Bool.not_eq_true]; exact fun hc => hae (List.elem_iff.mp hc)
simp only [h1, h2, Bool.false_eq_true, if_false]
rw [eraseDups_loop_len_swap l a s acc]
exact ih (a :: acc) hsl
/-- Prepending a fresh element to a list increases its `eraseDups` length by one. -/
private theorem eraseDups_cons_len (s : Nat) (S : List Nat) (h : s ∉ S) :
(s :: S).eraseDups.length = S.eraseDups.length + 1 := by
show (List.eraseDups.loop (s :: S) []).length = (List.eraseDups.loop S []).length + 1
rw [eraseDups_loop_unfold s S []]
have he : List.elem s ([] : List Nat) = false := rfl
simp only [he, Bool.false_eq_true, if_false]
exact eraseDups_loop_len_insert s S [] h
/-- Adding a new strategy type increases diversity: if the new agent's
strategy differs from every existing agent's strategy, the count of
distinct strategies grows by exactly one. -/
theorem new_strategy_increases_diversity (soup : Soup) (newAgent : SoupAgent)
(h_novel : ∀ a ∈ soup.agents, a.strategy ≠ newAgent.strategy) :
countStrategies (newAgent :: soup.agents) = countStrategies soup.agents + 1 := by
unfold countStrategies
rw [List.map_cons]
apply eraseDups_cons_len
intro hmem
rw [List.mem_map] at hmem
obtain ⟨a, ha, hstrat⟩ := hmem
exact h_novel a ha hstrat
/-- Zero agents means zero diversity. -/
theorem empty_no_diversity : countStrategies ([] : List SoupAgent) = 0 := by
rfl
/-! ## Theorem 4: Communication Convergence -/
/-- Communication protocol state across the soup. -/
structure CommState where
/-- Messages seen by each agent: agentId → set of message hashes -/
seen : Nat → Finset Nat
/-- Total messages in the system -/
totalMessages : Nat
/-- Round number -/
round : Nat
/-- Broadcast a message: add it to all agents' seen sets. -/
def broadcast (cs : CommState) (msg : Nat) (recipients : Finset Nat) : CommState :=
{ seen := fun agentId =>
if agentId ∈ recipients then cs.seen agentId ∪ {msg}
else cs.seen agentId
totalMessages := cs.totalMessages + 1
round := cs.round + 1 }
/-- Communication convergence: eventually all agents see all messages.
In each round, at least one new agent sees a message it hasn't seen.
After enough rounds, all agents have seen all messages.
We encode this as: the seen set grows monotonically per agent. -/
theorem communication_convergence (cs : CommState) (msg : Nat)
(recipients : Finset Nat) (agentId : Nat) (h : agentId ∈ recipients) :
cs.seen agentId ⊆ (broadcast cs msg recipients).seen agentId := by
simp [broadcast, h]
/-- New messages are received by recipients. -/
theorem message_received (cs : CommState) (msg : Nat)
(recipients : Finset Nat) (agentId : Nat) (h : agentId ∈ recipients) :
msg ∈ (broadcast cs msg recipients).seen agentId := by
simp [broadcast, h, Finset.mem_union, Finset.mem_singleton]
/-- Non-recipients are unaffected. -/
theorem non_recipient_unchanged (cs : CommState) (msg : Nat)
(recipients : Finset Nat) (agentId : Nat) (h : agentId ∉ recipients) :
(broadcast cs msg recipients).seen agentId = cs.seen agentId := by
simp [broadcast, h]
/-- Round counter increases. -/
theorem round_increases (cs : CommState) (msg : Nat) (recipients : Finset Nat) :
(broadcast cs msg recipients).round = cs.round + 1 := by
simp [broadcast]
/-! ## Theorem 5: Energy Conservation -/
/-- Total energy across agent interactions is conserved: interactions
transfer energy between agents but do not create or destroy it.
This is the first law of thermodynamics for the agent soup. -/
def applyInteraction (agents : List SoupAgent) (inter : Interaction) : List SoupAgent :=
agents.map fun a =>
if a.id = inter.agentA then
{ a with energy := a.energy - (inter.energyDelta.toNat) }
else if a.id = inter.agentB then
{ a with energy := a.energy + (inter.energyDelta.toNat) }
else a
/-- `sumEnergy` accumulated by `foldl` equals the plain sum of the mapped
energies (shifted by the initial accumulator). -/
private theorem sumEnergy_foldl {α} (l : List α) (f : α → Nat) (init : Nat) :
l.foldl (fun acc a => acc + f a) init = init + (l.map f).sum := by
induction l generalizing init with
| nil => simp
| cons x xs ih => simp only [List.foldl_cons, List.map_cons, List.sum_cons, ih]; ring
/-- `sumEnergy` is the sum of the agents' energies. -/
theorem sumEnergy_eq (agents : List SoupAgent) :
sumEnergy agents = (agents.map (·.energy)).sum := by
unfold sumEnergy; rw [sumEnergy_foldl]; simp
/-- Energy is conserved across a pairwise interaction: when energy is
transferred between two distinct agents A and B, what A loses, B gains,
so the total energy of the pair is unchanged. This is the first law of
thermodynamics for the agent soup, stated for the canonical two-body
transfer (the meaningful conserved unit of interaction).
Note: the previous formulation over an arbitrary agent list was not a
theorem — e.g. if several agents share `agentA`'s id, or `agentB`'s id is
absent, the transferred deltas do not cancel. The honest invariant
requires the two endpoints to be distinct and unique, captured here by the
two-element list `[a, b]` with `a.id = agentA`, `b.id = agentB`,
`agentA ≠ agentB`. -/
theorem soup_energy_conservation (a b : SoupAgent) (inter : Interaction)
(hA : a.id = inter.agentA) (hB : b.id = inter.agentB)
(h_distinct : inter.agentA ≠ inter.agentB)
(_h_symmetric : inter.energyDelta ≥ 0)
(h_sufficient : a.energy ≥ inter.energyDelta.toNat) :
sumEnergy (applyInteraction [a, b] inter) = sumEnergy [a, b] := by
rw [sumEnergy_eq, sumEnergy_eq]
unfold applyInteraction
simp only [List.map_cons, List.map_nil, List.sum_cons, List.sum_nil]
have hbA : ¬ (b.id = inter.agentA) := by rw [hB]; exact fun h => h_distinct h.symm
rw [if_pos hA, if_neg hbA, if_pos hB]
set d := inter.energyDelta.toNat
simp only
omega
/-- A no-op interaction (zero delta) preserves all energies exactly. -/
theorem zero_interaction_noop (agents : List SoupAgent) :
applyInteraction agents ⟨0, 1, 0⟩ = agents.map fun a =>
if a.id = 0 then { a with energy := a.energy }
else if a.id = 1 then { a with energy := a.energy }
else a := by
simp [applyInteraction]
/-- Sum energy of empty soup is zero. -/
theorem empty_soup_zero_energy : sumEnergy ([] : List SoupAgent) = 0 := by
simp [sumEnergy]
end Agent.DeltaSoup