From 60a0d607da268b092dfac410b071da1aa961703a Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 07:58:29 +0200 Subject: [PATCH 01/10] docs: design emergent local brain --- .../2026-09-21-emergent-local-brain-design.md | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md diff --git a/docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md b/docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md new file mode 100644 index 0000000..d206b13 --- /dev/null +++ b/docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md @@ -0,0 +1,326 @@ +# CYMONIA Emergent Local Brain and Neuron Governor + +Date: 2026-09-21 + +Status: design approved in conversation; written specification pending user review + +## Intent + +CYMONIA must produce more genuine emergence per unit of compute. Citizens should develop +multi-day chains of discovery, experiment, communication, cooperation, transformation, and +construction without asking an LLM what to do every few simulated minutes. Workers AI should +be reserved for novelty: generating a durable strategic direction when deterministic cognition +cannot reinterpret a new situation from existing experience. + +The implementation must preserve the existing epistemic boundary: a citizen may only act on +entities and concepts it knows, and neither the Local Brain nor Workers AI may introduce Earth +knowledge or privileged world state. + +## Success criteria + +1. A citizen without an AI strategy can autonomously choose among exploration, gathering, + experimentation, transformation, communication, teaching, care, transfer, cooperation, + building, rest, and survival when those actions are afforded by its known state. +2. Deterministic simulations can demonstrate a causal chain containing resource discovery, + gathering, property discovery through experimentation, repeated signal exposure, concept + transfer, behavioral adaptation, cooperation, and construction without Workers AI. +3. A single accepted Workers AI cognition stores a persistent strategy that can drive many local + actions over multiple CYMONIA days. It does not install a short action script. +4. AI scheduling is fair across the population and prioritizes novelty, danger, important social + change, plan failure, successful experimentation, care/reproduction events, and completed + construction. +5. Daily Workers AI consumption is governed in neurons rather than by a fixed call count, using + the token usage returned by Workers AI. +6. The existing deterministic physics, conservation, provenance, constitution, persistence, and + observer contracts continue to pass. + +## Selected architecture + +The selected approach is an incremental deterministic utility planner around the existing action +kernel. It retains the current action validation and resolution paths, adds explicit affordance +generation and scoring, and changes Workers AI from action author to strategy author. + +Two alternatives were rejected for this iteration: + +- A general rule DSL would be extensible but would add a second programming model before the + required behaviors are understood. +- A local reinforcement-learning policy would be more adaptive but less auditable and harder to + keep reproducible across runtime upgrades. + +The resulting cognition stack has four tiers: + +1. **Tier 0 — Reflex:** immediate biological and environmental survival. +2. **Tier 1 — Local Brain:** deterministic affordance generation and utility scoring. +3. **Tier 2 — Adaptive planning:** persistent strategy, action outcome memory, habituation, + novelty decay, and retry/alternative selection. +4. **Tier 3 — AI cognition:** compact strategic reinterpretation for sufficiently novel or urgent + queued events, subject to fairness and neuron budgets. + +## Component boundaries + +### Affordance planner + +`world/cognition.js` will expose a pure affordance-planning boundary. Given a world, citizen, and +world minute, it generates only valid candidates based on locally available evidence. Candidate +families include: + +- survival and rest; +- explore and observe; +- gather a known available deposit; +- experiment on held objects or known deposits with properties not yet learned; +- transform held materials when the citizen has relevant observations; +- communicate or teach a concept to a nearby known citizen who lacks it; +- care for a nearby known citizen with a material need; +- transfer an object where relationship and need support sharing; +- cooperate on an existing known construction project; +- initiate or continue empirical construction only when the citizen holds suitable material and + has learned evidence-backed material properties. + +Each candidate has a normalized score composed from need urgency, active strategy alignment, +curiosity/novelty, risk tolerance, social drive/empathy, relationship state, distance and effort, +inventory fit, knowledge gap, recency/cooldown, and prior outcomes. Stable seeded tie-breaking +keeps simulation replay deterministic. The planner returns one short executable plan, not a +global social narrative. + +The engine retains `validateCognitiveProposal()` and `applyAcceptedPlan()` as the mandatory +execution boundary. No new planner path bypasses epistemic or physical validation. + +### Adaptive local state + +Citizen cognition receives a backward-compatible local state object initialized lazily for old +snapshots: + +```json +{ + "lastLocalChoiceMinute": 0, + "lastOutcomeMinute": 0, + "outcomes": {}, + "cooldowns": {}, + "cognitionDebt": 0, + "lastAICognitionMinute": null, + "aiCognitionCount": 0 +} +``` + +`outcomes` stores bounded aggregate statistics by affordance family: attempts, successes, +failures, last result, and exponentially decayed utility adjustment. It never stores unbounded +action history. Action resolution reports success or failure into this state. Failure reduces +immediate repetition and raises novelty priority; success reinforces a behavior without making it +permanent. + +### Persistent AI strategy + +Workers AI will return a compact strategy object instead of an array of actions: + +```json +{ + "focus": "opaque known concept id", + "intent": "explore|understand|share|cooperate|care|construct|adapt", + "actionBias": ["EXPERIMENT", "COMMUNICATE"], + "partnerIds": ["known citizen id"], + "successSignals": ["known concept id"], + "horizonMinutes": 4320, + "confidence": 0.7 +} +``` + +All IDs must already be present in the citizen's cognitive context. The strategy validator rejects +unknown concepts, unknown partners, unsupported intent/action values, invalid horizons, and +non-finite values. Accepted strategies are stored as `activeGoal` with creation, expiry, source, +and progress metadata. The Local Brain interprets the strategy repeatedly and can select dozens +of concrete actions over its horizon. Completion, expiry, repeated failure, or a materially more +urgent strategy closes it. + +For snapshot compatibility, existing action-oriented `activeGoal` values remain readable by the +Local Brain. New Workers AI responses create only the new strategy shape. + +The prompt places stable instructions before the dynamic citizen context to support Workers AI +prefix caching. Generation uses `max_completion_tokens: 200` and strict JSON. The parser accepts +the documented response shapes and returns both the sanitized strategy and normalized usage. + +### Experiment and language loops + +Experiment candidates are favored when curiosity and novelty are high and an accessible target +has observable properties the citizen has not learned. Gathering becomes useful because held +objects enable tests and later transformations. A target/property pair receives a cooldown after +an inconclusive or redundant experiment so curiosity moves elsewhere. + +Communication candidates require proximity, an existing encounter, a sender-known concept, and +a receiver knowledge gap. Score depends on social drive, familiarity, trust, concept salience, +strategy alignment, and encounter repetition. The existing three-exposure learning threshold and +grammar-pattern accumulation remain canonical. Teaching stays a higher-confidence path favored by +strong trust; ordinary communication remains the default language-emergence path. + +Neither loop invents vocabulary or material semantics. Signals still come from `coinSignal()` and +properties still come from `runExperiment()` over canonical observable material properties. + +### Cooperation and construction + +Cooperation is represented through existing concrete actions rather than a fictional +`COOPERATE` kernel action. A citizen may transfer material to a trusted partner, communicate a +shared concept, or contribute `BUILD` work to a known active project. Construction initiation +requires held material, evidence-backed concepts, a valid site, and a locally generated empirical +design. No rule says to build a house or names a predetermined institution. + +## Novelty queue and fair scheduler + +Queue entries become mergeable per citizen and carry reason, base priority, first/last queued +world minute, occurrences, and optional event references. Repeated equivalent events increase +salience without growing the queue unboundedly. + +Each living citizen accumulates `cognitionDebt` with simulated time since its last accepted AI +cognition. Selection score is: + +```text +base event priority ++ bounded cognition debt ++ reason bonus ++ novelty bonus +- recent-AI penalty +- active-strategy adequacy penalty +``` + +Reason bonuses cover discovery, plan/action failure, danger, significant encounter, new concept, +successful experiment, conflict, pregnancy/birth/care, completed construction, relationship +threshold change, and explicit human direction. Ordinary plan completion does not by itself force +AI cognition. Debt guarantees eventual opportunity when budget exists, while urgency can still +move a citizen ahead temporarily. + +Only Tier 3-worthy entries reach the Worker scheduler. Local-plan completion, routine encounters, +and ordinary resource observations are consumed by Tiers 1–2 unless novelty remains unresolved. + +## Neuron Governor + +The fixed `AI_CALLS_PER_REAL_DAY` gate is removed as the primary control. The runtime budget is +initialized and migrated lazily: + +```json +{ + "day": "YYYY-MM-DD", + "usedNeurons": 0, + "reservedNeurons": 0, + "calls": 0, + "promptTokens": 0, + "completionTokens": 0, + "lastFailureRealMs": 0, + "lastExhaustedDay": null +} +``` + +Defaults: + +- normal soft budget: 8,000 neurons/day; +- high-priority reserve: 1,000 neurons/day; +- emergency/human reserve: 500 neurons/day; +- hard stop: 9,500 neurons/day; +- unallocated safety margin: 500 neurons/day. + +For `@cf/zai-org/glm-4.7-flash`, observed usage is converted using the documented rates current on +2026-09-21: 5,500 neurons per million input tokens and 36,400 per million output tokens. Model +rates are explicit configuration data keyed by model ID; an unknown model fails closed for normal +cognition unless conservative override rates are configured. + +Before inference, the governor reserves a conservative estimate computed from serialized prompt +size and the 200-token completion cap. After inference it replaces the reservation with actual +`usage.prompt_tokens` and `usage.completion_tokens`. Missing or malformed usage consumes the full +reservation and emits an observable accounting-warning event; it never counts as zero. + +Normal events may spend only the soft budget. High-priority events may use the high-priority +reserve. Explicit human direction and immediate survival emergencies may use the final reserve. +No request may cross the 9,500-neuron hard stop based on its reservation. Daily reset remains UTC, +matching Cloudflare's documented allocation reset. + +The health endpoint reports neurons, token counts, calls, tier capacity, reservations, rate table, +and last accounting warning. It does not expose private prompts or citizen memories. + +References: + +- +- +- + +## Data flow + +```text +world advances + -> Tier 0 survival reflex if urgent + -> perceive canonical local state + -> record event/outcome and merge novelty signal + -> Tier 1 generates and scores local affordances + -> Tier 2 applies active strategy and learned outcome modifiers + -> validated local plan enters existing action kernel + -> only unresolved/urgent novelty enters fair AI scheduler + -> Neuron Governor reserves capacity + -> Workers AI returns one compact strategy plus usage + -> governor accounts actual neurons + -> validated strategy guides later local decisions for days +``` + +## Failure handling + +- Invalid local candidates are discarded before scoring; validation remains the final guard. +- If all candidates are invalid, the citizen observes or rests rather than fabricating an action. +- Invalid AI JSON, unknown references, timeout, or rejected strategy records a deferred cognition + event, releases/reconciles the reservation conservatively, and requeues the citizen with bounded + backoff. +- A failed concrete action updates local outcome statistics and may enqueue a novelty event, but it + does not immediately retry the same action indefinitely. +- Missing Workers AI leaves the world fully active through Tiers 0–2. +- Old persisted worlds acquire new fields lazily without changing their canonical history. + +## Observability + +Ledger and health data will distinguish: + +- local affordance selection; +- experiment success or redundancy; +- strategy accepted, progressed, expired, completed, or abandoned; +- cognition queued, deferred, or accepted; +- neuron reservation, accounting, reserve tier, and exhaustion; +- scheduler fairness summaries without exposing private cognition. + +High-frequency local decisions must not flood the canonical ledger. Detailed scores remain bounded +runtime/citizen state; only material outcomes and sampled/aggregated planner diagnostics are +published. + +## Verification + +Test-driven implementation will add focused suites for: + +1. deterministic affordance generation and stable tie-breaking; +2. need, curiosity, risk, social, relationship, inventory, distance, cooldown, and outcome score + effects; +3. autonomous gather -> experiment -> property learning; +4. repeated communication -> concept transfer -> grammar growth; +5. cooperation through transfer and shared construction work; +6. strategy validation, persistence, expiry, progress, and backward compatibility; +7. queue merging, cognition-debt aging, reason bonuses, and population fairness; +8. exact neuron conversion, reservation reconciliation, reserve isolation, UTC reset, unknown-model + behavior, missing usage, and hard-stop enforcement; +9. compact Workers AI prompt/response contract and `max_completion_tokens <= 200`; +10. a deterministic multi-day, no-AI emergence scenario reaching a first completed structure + without a hard-coded instruction to build a house; +11. complete existing invariant and delivery suites. + +The current baseline has 104 passing tests and four failures caused by two tests converting file +URLs with `.pathname`, which leaves `%20` in this worktree path. A minimal test-only portability +fix using `fileURLToPath()` is included so final verification can be green locally and in CI. + +## Delivery + +Implementation will be committed on `codex/emergent-local-brain`, pushed to +`SignalLayerLabs/CYMONIA`, and proposed as a pull request against `main`. The PR will document the +new persisted fields, compatibility behavior, verified Cloudflare assumptions, tests executed, +and the fact that the implementation increases autonomous affordances without predetermining a +social outcome. + +Cloudflare deployment is not part of this change unless separately requested. Publishing means +the tested branch and pull request are made available on GitHub. + +## Non-goals + +- Predetermining houses, governments, currencies, religions, or other social institutions. +- Giving citizens global world knowledge or Earth vocabulary. +- Replacing the deterministic action/physics kernel with LLM output. +- Adding a new paid service, database, queue, or model. +- Claiming consciousness, causal validity outside the simulation, or guaranteed social outcomes. From 90a43dbb8cc2e96c8905641ed066e1afc57afcc8 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:11:03 +0200 Subject: [PATCH 02/10] docs: plan emergent local brain implementation --- .../plans/2026-09-21-emergent-local-brain.md | 585 ++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-21-emergent-local-brain.md diff --git a/docs/superpowers/plans/2026-09-21-emergent-local-brain.md b/docs/superpowers/plans/2026-09-21-emergent-local-brain.md new file mode 100644 index 0000000..b5c876c --- /dev/null +++ b/docs/superpowers/plans/2026-09-21-emergent-local-brain.md @@ -0,0 +1,585 @@ +# Emergent Local Brain Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make CYMONIA citizens autonomously develop evidence-backed multi-day behavior while Workers AI supplies rare persistent strategies under a fair neuron budget. + +**Architecture:** Add a deterministic affordance planner and bounded local learning state behind the existing cognition boundary, then change Workers AI output from action lists to validated persistent strategies. Add a mergeable novelty queue, cognition-debt scheduler, and pure Neuron Governor whose token accounting wraps the existing AI binding without weakening physical or epistemic validation. + +**Tech Stack:** JavaScript ES modules, Node.js built-in test runner, Cloudflare Durable Objects, Durable Object SQLite, Workers AI binding. + +**Spec:** `docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md` + +## Global Constraints + +- Preserve deterministic replay for identical world state, seed, and target time. +- Preserve epistemic validation: citizens may use only known concepts and entities. +- Preserve physical conservation, provenance, action validation, and the single-writer Durable Object. +- Add no runtime dependency or paid service. +- Workers AI model remains `@cf/zai-org/glm-4.7-flash` by default. +- Use `max_completion_tokens: 200`; do not use deprecated `max_tokens` for cognition. +- Normal/priority/human neuron ceilings are 8,000/9,000/9,500 per UTC day, leaving 500 neurons unallocated. +- Missing usage is charged conservatively, never as zero. +- Existing snapshots must migrate lazily without rewriting canonical history. +- AI absence must leave Tiers 0–2 fully operational. + +## File structure + +- Create `world/cognition-state.js`: bounded per-citizen local outcome learning, cooldowns, cognition debt, and lazy migration. +- Create `world/affordances.js`: pure affordance enumeration, scoring, stable tie-breaking, and short local proposal selection. +- Create `world/strategy.js`: AI strategy sanitization, validation, lifecycle, and legacy-goal compatibility. +- Create `world/cognition-queue.js`: novelty classification, queue merging, debt-aware ranking, and candidate consumption. +- Create `worker/src/neuron-governor.js`: model rate table, reservation, reconciliation, reset, and reserve-tier rules. +- Modify `world/cognition.js`: delegate local deliberation to affordances and expose strategy-aware context. +- Modify `world/engine.js`: record action outcomes, generate material novelty signals, age debt, and use mergeable queue entries. +- Modify `world/genesis.js` and `world/reproduction.js`: initialize new citizens with compatible cognition state and queue records. +- Modify `world/index.js`: export the new strategy and scheduler interfaces to the Worker. +- Modify `worker/src/index.js`: compact strategy prompt, usage extraction, fair queue selection, governor enforcement, and health reporting. +- Modify `tests/test_sovereign_engine_patch_upgrade.mjs` and `tests/test_sovereign_pixi_matter_upgrade.mjs`: portable file-URL conversion for worktrees whose paths contain spaces. +- Create focused `tests/test_sovereign_*.mjs` suites for each new boundary and update runtime contract assertions. +- Modify `README.md` and `docs/deployment-sovereign-world.md`: document autonomous cognition and budget configuration. + +## Review Focus + +- Old snapshots with no cognition-local state or old `activeGoal.actionTypes` must keep running and gain fields lazily; covered in Task 1 and Task 3 migration tests. +- A citizen with no valid affordance must fall back to OBSERVE/REST without throwing or inventing knowledge; covered in Task 2. +- Missing, nested, fractional, or malformed Workers AI usage must never undercharge the governor; covered in Task 5. +- Queue growth from repeated encounters/discoveries must remain bounded and no citizen may monopolize eligible AI slots; covered in Task 4. +- A strategy that references unknown concepts/partners or has expired must never influence local action; covered in Task 3. + +--- + +### Task 1: Portable baseline and bounded cognition state + +**Files:** +- Create: `world/cognition-state.js` +- Modify: `world/genesis.js` +- Modify: `world/reproduction.js` +- Modify: `tests/test_sovereign_engine_patch_upgrade.mjs` +- Modify: `tests/test_sovereign_pixi_matter_upgrade.mjs` +- Create: `tests/test_sovereign_cognition_state.mjs` + +**Interfaces:** +- Produces: `ensureCognitionState(citizen, at) -> state` +- Produces: `recordAffordanceOutcome(citizen, family, result, at) -> state` +- Produces: `outcomeModifier(citizen, family, at) -> number` +- Produces: `ageCognitionDebt(citizen, deltaMinutes) -> number` +- Produces: `markAICognition(citizen, at) -> state` + +- [ ] **Step 1: Make file URL tests portable** + +Replace every `new URL(...).pathname` used as a filesystem path in the two failing suites with: + +```js +import {fileURLToPath} from 'node:url'; +const script=fileURLToPath(new URL('../scripts/patch_current_v2.mjs',import.meta.url)); +``` + +Use the same conversion for the repository root in the Pixi/Matter suite. + +- [ ] **Step 2: Verify the unmodified product baseline is green** + +Run: `node --test tests/test_sovereign_*.mjs` + +Expected: 108 tests pass, 0 fail. The Node module-type warning may remain because the repository intentionally has no package dependency manifest. + +- [ ] **Step 3: Write failing cognition-state tests** + +```js +test('legacy citizen state migrates lazily and debt is bounded',()=>{ + const w=createSovereignGenesis({seed:17,realEpochMs:0}); + const c=w.citizens[0]; + delete c.cognition.local; + const state=ensureCognitionState(c,0); + assert.equal(state.cognitionDebt,0); + ageCognitionDebt(c,100_000); + assert.equal(c.cognition.local.cognitionDebt,1); +}); + +test('recent failed affordance is penalized and later decays',()=>{ + const c=createSovereignGenesis({seed:17,realEpochMs:0}).citizens[0]; + recordAffordanceOutcome(c,'experiment',{ok:false,reason:'redundant'},100); + assert.ok(outcomeModifier(c,'experiment',101)<0); + assert.ok(outcomeModifier(c,'experiment',20_000)>outcomeModifier(c,'experiment',101)); +}); +``` + +Run: `node --test tests/test_sovereign_cognition_state.mjs` + +Expected: FAIL because `world/cognition-state.js` does not exist. + +- [ ] **Step 4: Implement bounded cognition state** + +Use a lazy initializer with this stable shape: + +```js +const clamp01=value=>Math.max(0,Math.min(1,Number(value)||0)); + +export function ensureCognitionState(citizen,at=0){ + citizen.cognition??={lastReflectionMinute:null,pending:true,reason:'migration'}; + citizen.cognition.local??={ + lastLocalChoiceMinute:null,lastOutcomeMinute:null,outcomes:{},cooldowns:{}, + cognitionDebt:0,lastAICognitionMinute:null,aiCognitionCount:0 + }; + return citizen.cognition.local; +} +``` + +Keep at most 24 outcome-family records and 48 cooldown keys. Aggregate attempts/successes/failures rather than appending history. Clamp debt to `[0,1]`; `markAICognition()` resets it and increments the accepted count. + +- [ ] **Step 5: Initialize new citizens and run tests** + +Use the same fields in Genesis, human avatars, and newborns while retaining lazy migration for persisted citizens. + +Run: `node --test tests/test_sovereign_cognition_state.mjs tests/test_sovereign_genesis.mjs tests/test_sovereign_reproduction.mjs tests/test_sovereign_avatar.mjs` + +Expected: all pass. + +- [ ] **Step 6: Commit Task 1** + +```bash +git add world/cognition-state.js world/genesis.js world/reproduction.js tests/test_sovereign_cognition_state.mjs tests/test_sovereign_engine_patch_upgrade.mjs tests/test_sovereign_pixi_matter_upgrade.mjs +git commit -m "feat: add bounded citizen cognition state" +``` + +### Task 2: Deterministic affordance planner + +**Files:** +- Create: `world/affordances.js` +- Modify: `world/cognition.js` +- Modify: `world/engine.js` +- Create: `tests/test_sovereign_affordance_planner.mjs` + +**Interfaces:** +- Consumes: `ensureCognitionState`, `outcomeModifier`, `recordAffordanceOutcome` +- Produces: `enumerateAffordances(world, citizen, at) -> Candidate[]` +- Produces: `scoreAffordance(world, citizen, candidate, at) -> number` +- Produces: `chooseAffordance(world, citizen, at) -> proposal|null` +- Candidate: `{family, proposal, utility, cooldownKey, novelty, effort}` + +- [ ] **Step 1: Write failing enumeration and scoring tests** + +```js +test('curious citizen gathers then experiments on an untested material',()=>{ + const w=createSovereignGenesis({seed:31,realEpochMs:0}); + const c=w.citizens[0],deposit=w.resourceDeposits.find(d=>d.type==='timber'); + c.position={...deposit.position}; + c.knownEntityIds.push(deposit.id); + learn(c,resourceConceptId(deposit),{kind:'observation',eventId:'seen'}); + c.psychology.curiosity=1; + const first=chooseAffordance(w,c,0); + assert.equal(first.actions.at(-1).type,'GATHER'); + // Resolve the gathered-object fixture, then choose again. + const object={id:'held:timber',kind:'gathered_material',material:deposit.type,quantity:1,massPerUnitKg:1,properties:null,holderId:c.id,position:{...c.position},condition:1,provenance:{type:'GATHERED',depositId:deposit.id}}; + w.objects.push(object);c.possessions.push(object.id); + const second=chooseAffordance(w,c,30); + assert.equal(second.actions[0].type,'EXPERIMENT'); +}); + +test('invalid target is excluded and empty choice falls back safely',()=>{ + const w=createSovereignGenesis({seed:31,realEpochMs:0}); + const c=w.citizens[0]; + c.knownEntityIds=[c.id]; + c.body.sleepPressure=10;c.body.hydration=80;c.body.calories=80; + const proposal=chooseAffordance(w,c,0); + assert.ok(['MOVE','OBSERVE','REST'].includes(proposal.actions[0].type)); +}); +``` + +Add separate assertions proving that high social drive raises communication utility, high risk lowers unsafe exploration, trust raises transfer/teaching utility, distance lowers a candidate, and a recent failure/cooldown lowers immediate repetition. + +Run: `node --test tests/test_sovereign_affordance_planner.mjs` + +Expected: FAIL because the planner module does not exist. + +- [ ] **Step 2: Implement affordance enumeration** + +Generate candidates only from known and currently accessible state. Use family-specific builders for `explore`, `gather`, `experiment`, `transform`, `communicate`, `teach`, `care`, `transfer`, `cooperate`, `build`, and `rest`. Cooperation produces concrete `TRANSFER`, `COMMUNICATE`, or `BUILD` actions; do not add a new kernel action type. + +Use experiment cooldown keys such as `experiment:${targetId}:${method}` and communication cooldown keys such as `communicate:${receiverId}:${conceptId}`. Detect the receiver knowledge gap from canonical receiver state, but expose only the sender-known concept in the resulting proposal. + +- [ ] **Step 3: Implement normalized scoring and deterministic choice** + +```js +export function chooseAffordance(world,citizen,at=world.clock.worldMinute){ + const candidates=enumerateAffordances(world,citizen,at); + if(!candidates.length)return safeOrientationProposal(world,citizen,at); + return candidates + .map(candidate=>({...candidate,score:scoreAffordance(world,citizen,candidate,at)})) + .sort((a,b)=>b.score-a.score||stableCandidateOrder(world,citizen,a,b,at))[0].proposal; +} +``` + +Clamp each factor before weighting. Stable tie-breaking must depend on seed, citizen ID, candidate family/key, and a coarse deterministic decision window, never `Math.random()` or real time. + +- [ ] **Step 4: Route local deliberation and outcomes through the planner** + +Keep `survivalFallback()` first. Replace the current MOVE/OBSERVE-dominant body of `localDeliberation()` with `chooseAffordance()`. In action resolution, call `recordAffordanceOutcome()` on success and failure using the originating plan's stored family metadata. Preserve the existing validation boundary and safe OBSERVE fallback. + +- [ ] **Step 5: Run planner and action tests** + +Run: `node --test tests/test_sovereign_affordance_planner.mjs tests/test_sovereign_local_deliberation.mjs tests/test_sovereign_action_effects.mjs tests/test_sovereign_artifacts.mjs tests/test_sovereign_language.mjs tests/test_sovereign_relationship_effects.mjs` + +Expected: all pass; local choices include non-MOVE affordances under their tested conditions. + +- [ ] **Step 6: Commit Task 2** + +```bash +git add world/affordances.js world/cognition.js world/engine.js tests/test_sovereign_affordance_planner.mjs +git commit -m "feat: make local cognition affordance driven" +``` + +### Task 3: Persistent strategy lifecycle and compact AI contract + +**Files:** +- Create: `world/strategy.js` +- Modify: `world/cognition.js` +- Modify: `world/engine.js` +- Modify: `world/index.js` +- Modify: `worker/src/index.js` +- Create: `tests/test_sovereign_strategy.mjs` +- Modify: `tests/test_sovereign_cognition.mjs` +- Modify: `tests/test_sovereign_runtime_contract.mjs` + +**Interfaces:** +- Produces: `sanitizeAIStrategy(raw) -> Strategy` +- Produces: `validateStrategy(world, citizen, strategy, at) -> {ok, reason?}` +- Produces: `acceptAIStrategy(world, citizenId, strategy, at) -> Strategy` +- Produces: `activeStrategy(citizen, at) -> Strategy|null` +- Changes Worker `askAI(env, context) -> {strategy, usage}` + +- [ ] **Step 1: Write failing strategy tests** + +```js +test('strategy persists for days and references only known state',()=>{ + const w=createSovereignGenesis({seed:7,realEpochMs:0}); + const c=w.citizens[0]; + learn(c,'known-concept',{kind:'observation',eventId:'e'}); + const strategy=sanitizeAIStrategy({focus:'known-concept',intent:'understand',actionBias:['EXPERIMENT'],partnerIds:[],successSignals:['known-concept'],horizonMinutes:4320,confidence:.7}); + assert.equal(validateStrategy(w,c,strategy,0).ok,true); + acceptAIStrategy(w,c.id,strategy,0); + assert.equal(activeStrategy(c,2879)?.intent,'understand'); +}); + +test('unknown partner, unknown concept, and expired strategy are inert',()=>{ + const w=createSovereignGenesis({seed:7,realEpochMs:0}); + const c=w.citizens[0]; + const invalid=sanitizeAIStrategy({focus:'forbidden',intent:'share',partnerIds:['stranger'],horizonMinutes:60}); + assert.equal(validateStrategy(w,c,invalid,0).ok,false); + c.activeGoal={kind:'strategy-v1',createdWorldMinute:0,expiresWorldMinute:10,intent:'explore',actionBias:['OBSERVE']}; + assert.equal(activeStrategy(c,11),null); +}); +``` + +Include a legacy test proving `{concepts, actionTypes}` still biases the Local Brain after migration. + +Run: `node --test tests/test_sovereign_strategy.mjs` + +Expected: FAIL because the strategy module does not exist. + +- [ ] **Step 2: Implement strategy sanitize/validate/lifecycle** + +Allow only the seven specified intents, known `ACTION_TYPES`, known partner IDs, known concepts, a horizon from 60 to 10,080 world minutes, and confidence `[0,1]`. Store `createdWorldMinute`, `expiresWorldMinute`, `source:'workers-ai'`, `progress`, and `failureCount`. Expiry clears the goal and records a lifecycle event only once. + +- [ ] **Step 3: Make cognitive context strategy-aware and compact** + +Replace full unbounded maps with stable bounded summaries: at most 32 active knowledge entries, 12 memories, 12 nearby entities, 12 possessions, 8 projects, 12 relationships, and 12 beliefs. Keep the static system instruction first and dynamic context second. + +- [ ] **Step 4: Replace the Worker action prompt with strategy JSON** + +Use a strict system contract matching: + +```json +{"focus":"known-id","intent":"understand","actionBias":["EXPERIMENT"],"partnerIds":[],"successSignals":["known-id"],"horizonMinutes":4320,"confidence":0.7} +``` + +Call Workers AI with `max_completion_tokens:200`, `temperature:0.45`, and no deprecated `max_tokens`. Extract `usage` from `out.usage`, `out.result.usage`, or compatible nested response shapes. Return `{strategy,usage}` and keep parsing fenced JSON defensively. + +- [ ] **Step 5: Use accepted strategies to bias repeated local decisions** + +`scoreAffordance()` applies bounded boosts for intent, `actionBias`, focus concepts, partner IDs, and success signals. It must call `activeStrategy()` first so invalid or expired state contributes no score. Routine plan completion leaves the strategy active. + +- [ ] **Step 6: Run cognition and Worker contract tests** + +Run: `node --test tests/test_sovereign_strategy.mjs tests/test_sovereign_cognition.mjs tests/test_sovereign_runtime_contract.mjs tests/test_sovereign_local_deliberation.mjs` + +Expected: all pass; runtime test asserts `max_completion_tokens:200`, absence of cognition `max_tokens`, and strategy rather than actions in the prompt. + +- [ ] **Step 7: Commit Task 3** + +```bash +git add world/strategy.js world/cognition.js world/engine.js world/index.js worker/src/index.js tests/test_sovereign_strategy.mjs tests/test_sovereign_cognition.mjs tests/test_sovereign_runtime_contract.mjs +git commit -m "feat: make AI cognition produce persistent strategies" +``` + +### Task 4: Mergeable novelty queue and fair scheduler + +**Files:** +- Create: `world/cognition-queue.js` +- Modify: `world/engine.js` +- Modify: `world/genesis.js` +- Modify: `world/reproduction.js` +- Modify: `world/index.js` +- Modify: `worker/src/index.js` +- Create: `tests/test_sovereign_cognition_scheduler.mjs` + +**Interfaces:** +- Produces: `queueCognition(world, citizen, reason, basePriority, at, eventId?) -> QueueEntry` +- Produces: `rankCognitionQueue(world, at) -> QueueEntry[]` +- Produces: `takeCognitionCandidate(world, at, allowedTier) -> QueueEntry|null` +- Produces: `classifyCognitionReason(reason) -> {tier, bonus, reserve}` + +- [ ] **Step 1: Write failing bounded-queue and fairness tests** + +```js +test('repeated discovery merges instead of growing the queue',()=>{ + const w=createSovereignGenesis({seed:9,realEpochMs:0}); + w.cognitionQueue=[];const c=w.citizens[0]; + queueCognition(w,c,'discovery',.6,10,'e1'); + queueCognition(w,c,'discovery',.6,20,'e2'); + assert.equal(w.cognitionQueue.length,1); + assert.equal(w.cognitionQueue[0].occurrences,2); +}); + +test('debt eventually outranks a repeatedly served citizen',()=>{ + const w=createSovereignGenesis({seed:9,realEpochMs:0}); + const [old,frequent]=w.citizens; + ensureCognitionState(old,0).cognitionDebt=.95; + ensureCognitionState(frequent,0).cognitionDebt=.05; + queueCognition(w,old,'novelty_unresolved',.5,100); + queueCognition(w,frequent,'novelty_unresolved',.55,100); + assert.equal(rankCognitionQueue(w,100)[0].citizenId,old.id); +}); +``` + +Add tests for reason classes: routine/local-only, high priority, and human/emergency reserve. + +Run: `node --test tests/test_sovereign_cognition_scheduler.mjs` + +Expected: FAIL because the scheduler module does not exist. + +- [ ] **Step 2: Implement queue merging and reason classification** + +Merge on `citizenId + reason`, cap occurrences, retain first/last queued minute, and keep at most eight event IDs. Map routine plan completion and ordinary encounters to Tier 2/local-only. Map unresolved discovery, action failure, new concept, experiment success, relationship threshold change, conflict, care/reproduction, and construction completion to Tier 3 with bounded bonuses. Map `human_direction` and immediate danger to the final reserve. + +- [ ] **Step 3: Implement debt-aware stable ranking** + +Rank by base priority + debt + reason bonus + bounded occurrence novelty - recent-AI penalty - adequate-active-strategy penalty. Tie-break with first queued minute and citizen ID. Increase living citizens' debt from simulated elapsed minutes in `stepSegment()`; reset only after accepted AI cognition. + +- [ ] **Step 4: Replace direct queue pushes everywhere** + +Use `queueCognition()` in Genesis, newborn creation, encounters, discoveries, failures, plan completion, human direction, and material events. Ensure local-only entries are consumed locally or omitted from the AI queue. Keep old persisted entry shapes readable by normalizing them lazily. + +- [ ] **Step 5: Route Worker cognition through the scheduler** + +Replace in-place priority sort/shift with `takeCognitionCandidate()`. Requeue failures through the merge API with cooldown metadata. `processCognition(1)` remains the per-alarm concurrency bound; fairness changes who is selected, not the number of simultaneous inferences. + +- [ ] **Step 6: Run scheduler and engine tests** + +Run: `node --test tests/test_sovereign_cognition_scheduler.mjs tests/test_sovereign_engine.mjs tests/test_sovereign_reproduction.mjs tests/test_sovereign_runtime_contract.mjs` + +Expected: all pass and a 100-citizen scheduling fixture serves every eligible citizen before repeat service when priorities are otherwise equal. + +- [ ] **Step 7: Commit Task 4** + +```bash +git add world/cognition-queue.js world/engine.js world/genesis.js world/reproduction.js world/index.js worker/src/index.js tests/test_sovereign_cognition_scheduler.mjs tests/test_sovereign_runtime_contract.mjs +git commit -m "feat: schedule novelty cognition fairly" +``` + +### Task 5: Neuron Governor and production accounting + +**Files:** +- Create: `worker/src/neuron-governor.js` +- Modify: `worker/src/index.js` +- Create: `tests/test_sovereign_neuron_governor.mjs` +- Modify: `tests/test_sovereign_runtime_contract.mjs` + +**Interfaces:** +- Produces: `MODEL_NEURON_RATES` +- Produces: `ensureNeuronBudget(world, nowMs) -> budget` +- Produces: `estimateReservation(model, context, maxCompletionTokens, rates?) -> number` +- Produces: `reserveNeurons(budget, amount, reserveClass) -> {ok, reason?, reservation?}` +- Produces: `reconcileNeurons(budget, reservation, usage, model, rates?) -> {charged, warning?}` +- Produces: `neuronCapacity(budget, reserveClass) -> number` + +- [ ] **Step 1: Write failing governor tests** + +```js +test('GLM token usage converts to documented neurons',()=>{ + const budget={day:'2026-09-21',usedNeurons:0,reservedNeurons:0,calls:0,promptTokens:0,completionTokens:0,lastFailureRealMs:0,lastExhaustedDay:null}; + const reservation=reserveNeurons(budget,20,'normal').reservation; + const result=reconcileNeurons(budget,reservation,{prompt_tokens:1000,completion_tokens:200},'@cf/zai-org/glm-4.7-flash'); + assert.equal(result.charged,12.78); // 5.5 input + 7.28 output +}); + +test('normal work cannot consume priority or human reserves',()=>{ + const budget={day:'2026-09-21',usedNeurons:7999,reservedNeurons:0,calls:0,promptTokens:0,completionTokens:0,lastFailureRealMs:0,lastExhaustedDay:null}; + assert.equal(reserveNeurons(budget,2,'normal').ok,false); + assert.equal(reserveNeurons(budget,2,'priority').ok,true); +}); + +test('missing usage charges reservation and unknown model fails closed',()=>{ + const budget={day:'2026-09-21',usedNeurons:0,reservedNeurons:0,calls:0,promptTokens:0,completionTokens:0,lastFailureRealMs:0,lastExhaustedDay:null}; + const held=reserveNeurons(budget,25,'normal').reservation; + assert.equal(reconcileNeurons(budget,held,null,'@cf/zai-org/glm-4.7-flash').charged,25); + assert.equal(estimateReservation('@cf/unknown/model','{}',200),Infinity); +}); +``` + +Add UTC reset, malformed/fractional/negative/nested usage, hard-stop, over-reservation, and reconciliation-above-reservation tests. + +Run: `node --test tests/test_sovereign_neuron_governor.mjs` + +Expected: FAIL because the governor module does not exist. + +- [ ] **Step 2: Implement pure governor functions** + +Use rates `{inputPerMillion:5500, outputPerMillion:36400}` for GLM-4.7-Flash. Compute neurons as a floating-point value and round upward only for admission decisions; retain precise totals for observability. Estimate prompt tokens conservatively as `Math.ceil(serializedLength/3)` and completion at the full configured cap. Enforce 8,000/9,000/9,500 ceilings by reserve class. + +- [ ] **Step 3: Migrate runtime budget and wrap every inference** + +Replace `AI_CALLS_PER_REAL_DAY` admission with: + +```js +const estimate=estimateReservation(model,JSON.stringify(context),MAX_COMPLETION_TOKENS); +const admission=reserveNeurons(budget,estimate,reserveClass); +if(!admission.ok)return deferForBudget(item,admission.reason); +try{ + const {strategy,usage}=await askAI(this.env,context); + reconcileNeurons(budget,admission.reservation,usage,model); + acceptAIStrategy(this.world,c.id,strategy,this.world.clock.worldMinute); +}catch(error){ + reconcileNeurons(budget,admission.reservation,null,model); + throw error; +} +``` + +Avoid double reconciliation by tracking reservation state. Calls and token totals remain metrics, not admission limits. Keep retry cooldown and a once-per-day exhaustion ledger event. + +- [ ] **Step 4: Expose safe health metrics** + +Return day, used/reserved neurons, prompt/completion tokens, calls, soft/high/hard limits, available capacity per reserve class, model rate ID, and last accounting warning. Remove the call-limit field and do not return prompts, context, or private strategy text. + +- [ ] **Step 5: Run governor and Worker tests** + +Run: `node --test tests/test_sovereign_neuron_governor.mjs tests/test_sovereign_runtime_contract.mjs tests/test_sovereign_persistence.mjs tests/test_sovereign_persistence_budget.mjs` + +Expected: all pass; runtime source has no `AI_CALLS_PER_REAL_DAY`, cognition `max_tokens`, or zero-charge missing-usage path. + +- [ ] **Step 6: Commit Task 5** + +```bash +git add worker/src/neuron-governor.js worker/src/index.js tests/test_sovereign_neuron_governor.mjs tests/test_sovereign_runtime_contract.mjs +git commit -m "feat: govern Workers AI by measured neurons" +``` + +### Task 6: End-to-end emergence, documentation, and release + +**Files:** +- Create: `tests/test_sovereign_emergent_chain.mjs` +- Modify: `README.md` +- Modify: `docs/deployment-sovereign-world.md` +- Modify: `MANIFEST.sha256` + +**Interfaces:** +- Consumes all prior task interfaces. +- Produces a reproducible acceptance scenario and release documentation. + +- [ ] **Step 1: Write the deterministic no-AI emergence test** + +Construct a small deterministic world fixture from Genesis, position two citizens near timber and +clay, give neither an AI strategy, and advance only through `advanceWorldTo()`. Assert material +events rather than exact minute-by-minute action order: + +```js +assert.ok(types.has('RESOURCE_GATHERED')); +assert.ok(types.has('EXPERIMENT_COMPLETED')); +assert.ok(types.has('SIGNAL_COINED')); +assert.ok(types.has('COMMUNICATION')); +assert.ok(receiver.knowledge.some(k=>k.provenance.some(p=>p.kind==='communication'))); +assert.ok(types.has('CONSTRUCTION_STARTED')); +assert.ok(types.has('BUILDING_COMPLETED')); +assert.equal(aiCalls,0); +``` + +The fixture may tune psychology, positions, resources, and elapsed time, but must not set an active +goal containing BUILD or name a house/design outcome. + +Run: `node --test tests/test_sovereign_emergent_chain.mjs` + +Expected: FAIL at the first missing causal event until integration is complete. + +- [ ] **Step 2: Close only integration gaps exposed by the acceptance test** + +Adjust only named planner constants and hooks, without hard-coding the expected sequence or a +structure type. Keep the tuning surface explicit: + +```js +export const AFFORDANCE_WEIGHTS=Object.freeze({ + need:1, strategy:.55, curiosity:.45, social:.4, relationship:.3, + inventoryFit:.35, knowledgeGap:.5, novelty:.4, effort:-.25, + risk:-.3, cooldown:-.8, outcome:.35 +}); +``` + +Material action resolution must call `recordAffordanceOutcome()` and enqueue only material novelty +through `queueCognition()`. Re-run after each change until all causal event assertions pass for two +repeated runs with identical ledger heads. + +- [ ] **Step 3: Document the shipped cognition economy** + +README must explain the four tiers, persistent strategies, deterministic emergence, and that AI is +novelty-only. Deployment docs must list optional environment overrides for neuron limits/rates, +UTC reset behavior, missing-usage conservative charging, and `/health` fields. Cite the official +Cloudflare pricing/model/prompt-caching URLs from the spec and date numeric assumptions. + +- [ ] **Step 4: Regenerate integrity manifest** + +Regenerate the manifest from tracked and newly created non-ignored files, excluding the manifest +itself: + +```bash +git ls-files -co --exclude-standard -z | LC_ALL=C sort -z | while IFS= read -r -d '' file; do + if [[ "$file" != "MANIFEST.sha256" ]]; then shasum -a 256 "$file"; fi +done > MANIFEST.sha256 +``` + +Then run `bash CHECK.sh . --skip-browser` to verify every listed digest. + +- [ ] **Step 5: Run focused and full verification** + +Run: + +```bash +node --test tests/test_sovereign_affordance_planner.mjs tests/test_sovereign_cognition_state.mjs tests/test_sovereign_strategy.mjs tests/test_sovereign_cognition_scheduler.mjs tests/test_sovereign_neuron_governor.mjs tests/test_sovereign_emergent_chain.mjs +node --test tests/test_sovereign_*.mjs +bash CHECK.sh +git diff --check origin/main...HEAD +``` + +Expected: every command exits 0; full Node test summary has 0 failures; the acceptance run is deterministic. + +- [ ] **Step 6: Commit documentation and acceptance coverage** + +```bash +git add tests/test_sovereign_emergent_chain.mjs README.md docs/deployment-sovereign-world.md MANIFEST.sha256 +git commit -m "docs: explain autonomous cognition economy" +``` + +- [ ] **Step 7: Review the whole branch** + +Inspect `git diff --stat origin/main...HEAD`, `git diff --check`, runtime source for obsolete call-limit +symbols, and the full tests. Confirm no credentials, generated dependency trees, private prompts, +or unrelated observer changes are included. + +- [ ] **Step 8: Publish and open the pull request** + +```bash +git push -u origin codex/emergent-local-brain +gh pr create --base main --head codex/emergent-local-brain --title "feat: accelerate emergent local cognition" --body-file /tmp/cymonia-emergent-local-brain-pr.md +``` + +The PR body must summarize behavior, neuron accounting, compatibility, Cloudflare assumptions, +test evidence, and the pre-existing test-path portability fix. Attach the resulting PR to the +Codex task. From fbcc7fb87a5882eec61a1c394bc0e637e29842a1 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:27:52 +0200 Subject: [PATCH 03/10] feat: add bounded citizen cognition state --- tests/test_sovereign_cognition_state.mjs | 51 ++++++++++++++ tests/test_sovereign_engine_patch_upgrade.mjs | 3 +- tests/test_sovereign_pixi_matter_upgrade.mjs | 3 +- world/cognition-state.js | 67 +++++++++++++++++++ world/genesis.js | 3 +- world/reproduction.js | 3 +- 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 tests/test_sovereign_cognition_state.mjs create mode 100644 world/cognition-state.js diff --git a/tests/test_sovereign_cognition_state.mjs b/tests/test_sovereign_cognition_state.mjs new file mode 100644 index 0000000..143e175 --- /dev/null +++ b/tests/test_sovereign_cognition_state.mjs @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createSovereignGenesis} from '../world/genesis.js'; +import {advancePregnancies,conceive} from '../world/reproduction.js'; +import { + ageCognitionDebt, + ensureCognitionState, + markAICognition, + outcomeModifier, + recordAffordanceOutcome, +} from '../world/cognition-state.js'; + +test('legacy citizen state migrates lazily and debt is bounded',()=>{ + const world=createSovereignGenesis({seed:17,realEpochMs:0}); + assert.ok(world.citizens.every(citizen=>citizen.cognition.local)); + const citizen=world.citizens[0]; + delete citizen.cognition.local; + const state=ensureCognitionState(citizen,0); + assert.equal(state.cognitionDebt,0); + ageCognitionDebt(citizen,100_000); + assert.equal(citizen.cognition.local.cognitionDebt,1); +}); + +test('newborn citizens start with bounded local cognition state',()=>{ + const world=createSovereignGenesis({seed:23,realEpochMs:0}); + const mother=world.citizens.find(c=>c.body.reproductiveRole==='gestating'); + const partner=world.citizens.find(c=>c.body.reproductiveRole==='non_gestating'); + const pregnancy=conceive(world,mother,partner,0); + const [child]=advancePregnancies(world,pregnancy.dueWorldMinute); + assert.ok(child.cognition.local); + assert.equal(child.cognition.local.cognitionDebt,0); +}); + +test('recent failed affordance is penalized and later decays',()=>{ + const citizen=createSovereignGenesis({seed:17,realEpochMs:0}).citizens[0]; + recordAffordanceOutcome(citizen,'experiment',{ok:false,reason:'redundant'},100); + assert.ok(outcomeModifier(citizen,'experiment',101)<0); + assert.ok(outcomeModifier(citizen,'experiment',20_000)>outcomeModifier(citizen,'experiment',101)); +}); + +test('successful outcomes reinforce behavior and accepted AI resets debt',()=>{ + const citizen=createSovereignGenesis({seed:19,realEpochMs:0}).citizens[0]; + ageCognitionDebt(citizen,7200); + recordAffordanceOutcome(citizen,'communicate',{ok:true},100); + assert.ok(outcomeModifier(citizen,'communicate',101)>0); + markAICognition(citizen,120); + const state=ensureCognitionState(citizen,120); + assert.equal(state.cognitionDebt,0); + assert.equal(state.lastAICognitionMinute,120); + assert.equal(state.aiCognitionCount,1); +}); diff --git a/tests/test_sovereign_engine_patch_upgrade.mjs b/tests/test_sovereign_engine_patch_upgrade.mjs index 0deae66..1b865b5 100644 --- a/tests/test_sovereign_engine_patch_upgrade.mjs +++ b/tests/test_sovereign_engine_patch_upgrade.mjs @@ -4,8 +4,9 @@ import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,rmSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join,resolve} from 'node:path'; import {spawnSync} from 'node:child_process'; +import {fileURLToPath} from 'node:url'; -const patcher=resolve(new URL('../scripts/patch_current_v2.mjs',import.meta.url).pathname); +const patcher=resolve(fileURLToPath(new URL('../scripts/patch_current_v2.mjs',import.meta.url))); const currentAnchors=`export function f(){const completed=[];for(const a of completed){resolveAction(world,a,a.endsWorldMinute);const c=world.citizens.find(x=>x.id===a.actorId);if(c?.alive&&!continuePlan(world,c,a,a.endsWorldMinute))queueCognition(world,c,'plan_completed',.42);}return {body:{hydration:c.body.hydration,calories:c.body.calories,sleepPressure:c.body.sleepPressure,health:c.body.health,ageMinutes:c.body.ageMinutes,diseases:c.body.diseases.length,injuries:c.body.injuries.length},currentAction:action?{targetId:action.targetId}:null,objects:world.objects.filter(o=>o.quantity>0).map(o=>({id:o.id,kind:o.kind,quantity:o.quantity,holderId:o.holderId,position:o.position,condition:o.condition,provenance:o.provenance})),ledgerHead:world.ledgerHead};}`; test('engine projection patch is safe and idempotent on current structural anchors',()=>{ diff --git a/tests/test_sovereign_pixi_matter_upgrade.mjs b/tests/test_sovereign_pixi_matter_upgrade.mjs index e36de4d..683325d 100644 --- a/tests/test_sovereign_pixi_matter_upgrade.mjs +++ b/tests/test_sovereign_pixi_matter_upgrade.mjs @@ -2,7 +2,8 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; -const root=path.resolve(new URL('..',import.meta.url).pathname); +import {fileURLToPath} from 'node:url'; +const root=path.resolve(fileURLToPath(new URL('..',import.meta.url))); test('Observer loads pinned PixiJS and Matter.js before the sovereign module',()=>{ const html=fs.readFileSync(path.join(root,'site/index.html'),'utf8'); diff --git a/world/cognition-state.js b/world/cognition-state.js new file mode 100644 index 0000000..1c51d33 --- /dev/null +++ b/world/cognition-state.js @@ -0,0 +1,67 @@ +const clamp=(value,min,max)=>Math.max(min,Math.min(max,Number(value)||0)); + +function trimOldest(record,limit,lastMinuteKey='lastWorldMinute'){ + const entries=Object.entries(record); + if(entries.length<=limit)return; + entries.sort((a,b)=>Number(a[1]?.[lastMinuteKey]||0)-Number(b[1]?.[lastMinuteKey]||0)); + for(const [key] of entries.slice(0,entries.length-limit))delete record[key]; +} + +export function ensureCognitionState(citizen,at=0){ + citizen.cognition??={lastReflectionMinute:null,pending:true,reason:'migration'}; + const existing=citizen.cognition.local||{}; + citizen.cognition.local={ + lastLocalChoiceMinute:existing.lastLocalChoiceMinute??null, + lastOutcomeMinute:existing.lastOutcomeMinute??null, + outcomes:existing.outcomes&&typeof existing.outcomes==='object'?existing.outcomes:{}, + cooldowns:existing.cooldowns&&typeof existing.cooldowns==='object'?existing.cooldowns:{}, + cognitionDebt:clamp(existing.cognitionDebt,0,1), + lastAICognitionMinute:existing.lastAICognitionMinute??null, + aiCognitionCount:Math.max(0,Math.floor(Number(existing.aiCognitionCount)||0)), + }; + if(!Number.isFinite(Number(at)))citizen.cognition.local.lastLocalChoiceMinute=null; + return citizen.cognition.local; +} + +export function recordAffordanceOutcome(citizen,family,result={},at=0){ + const state=ensureCognitionState(citizen,at); + const key=String(family||'unknown').slice(0,48); + const prior=state.outcomes[key]||{attempts:0,successes:0,failures:0,utility:0,lastWorldMinute:null,lastReason:null,lastOk:null}; + const ok=Boolean(result.ok); + const impulse=ok?.35:-.45; + state.outcomes[key]={ + attempts:Math.max(0,Number(prior.attempts)||0)+1, + successes:Math.max(0,Number(prior.successes)||0)+(ok?1:0), + failures:Math.max(0,Number(prior.failures)||0)+(ok?0:1), + utility:clamp((Number(prior.utility)||0)*.65+impulse,-1,1), + lastWorldMinute:Number(at)||0, + lastReason:result.reason?String(result.reason).slice(0,96):null, + lastOk:ok, + }; + state.lastOutcomeMinute=Number(at)||0; + trimOldest(state.outcomes,24); + trimOldest(state.cooldowns,48,'setWorldMinute'); + return state; +} + +export function outcomeModifier(citizen,family,at=0){ + const state=ensureCognitionState(citizen,at); + const outcome=state.outcomes[String(family||'unknown')]; + if(!outcome)return 0; + const elapsed=Math.max(0,(Number(at)||0)-Number(outcome.lastWorldMinute||0)); + return clamp(Number(outcome.utility)||0,-1,1)*Math.exp(-elapsed/4320); +} + +export function ageCognitionDebt(citizen,deltaMinutes){ + const state=ensureCognitionState(citizen); + state.cognitionDebt=clamp(state.cognitionDebt+Math.max(0,Number(deltaMinutes)||0)/10080,0,1); + return state.cognitionDebt; +} + +export function markAICognition(citizen,at=0){ + const state=ensureCognitionState(citizen,at); + state.cognitionDebt=0; + state.lastAICognitionMinute=Number(at)||0; + state.aiCognitionCount+=1; + return state; +} diff --git a/world/genesis.js b/world/genesis.js index 95809e3..7b321d9 100644 --- a/world/genesis.js +++ b/world/genesis.js @@ -2,11 +2,12 @@ import {WORLD_VERSION,GENESIS_POPULATION,PRIMITIVE_SIGNALS} from './constants.js import {rng,stableId} from './rng.js'; import {appendEvent} from './ledger.js'; import {genesisGenome} from './genetics.js'; +import {ensureCognitionState} from './cognition-state.js'; function body(r,index,genome){return {massKg:55+Math.round(r()*30),hydration:82+r()*15,calories:82+r()*15,sleepPressure:5+r()*15,temperatureC:36.6,health:100,injuries:[],diseases:[],fertility:genome.fertility,pregnancy:null,reproductiveRole:index%2===0?'gestating':'non_gestating',ageMinutes:(18+Math.floor(r()*17))*525600,alive:true};} function psychology(r){return {curiosity:r(),riskTolerance:r(),socialDrive:r(),aggression:r()*.6,empathy:.35+r()*.65,noveltySeeking:r(),stress:0,fear:0,attachment:0.2+r()*.4,confidence:.3+r()*.6};} function position(i,r){const ring=Math.floor(i/20);const a=(i%20)/20*Math.PI*2;const radius=4+ring*2.3;return {x:50+Math.cos(a)*radius+r()*.4,y:50+Math.sin(a)*radius+r()*.4};} -function citizen(i,r,seed){const genome=genesisGenome(seed,i);const id=`genesis:${String(i+1).padStart(3,'0')}`;return {id,kind:'GENESIS',selfName:null,externalId:null,alive:true,birthWorldMinute:-Math.floor((18+r()*17)*525600),deathWorldMinute:null,position:position(i,r),genome,body:body(r,i,genome),psychology:psychology(r),knowledge:[],memories:[],knownEntityIds:[id],skills:{},language:{primitiveSignals:[...PRIMITIVE_SIGNALS],lexicon:{},heard:{},grammarPatterns:{}},relationships:{},beliefs:[],possessions:[],goals:[],activeGoal:null,plans:[],currentActionId:null,commitments:[],cognition:{lastReflectionMinute:null,pending:true,reason:'genesis_awareness'}};} +function citizen(i,r,seed){const genome=genesisGenome(seed,i);const id=`genesis:${String(i+1).padStart(3,'0')}`;const c={id,kind:'GENESIS',selfName:null,externalId:null,alive:true,birthWorldMinute:-Math.floor((18+r()*17)*525600),deathWorldMinute:null,position:position(i,r),genome,body:body(r,i,genome),psychology:psychology(r),knowledge:[],memories:[],knownEntityIds:[id],skills:{},language:{primitiveSignals:[...PRIMITIVE_SIGNALS],lexicon:{},heard:{},grammarPatterns:{}},relationships:{},beliefs:[],possessions:[],goals:[],activeGoal:null,plans:[],currentActionId:null,commitments:[],cognition:{lastReflectionMinute:null,pending:true,reason:'genesis_awareness'}};ensureCognitionState(c,0);return c;} export function createSovereignGenesis({seed=20260915,realEpochMs=Date.now()}={}){ const r=rng(seed);const worldId=`sovereign-${seed}`; diff --git a/world/reproduction.js b/world/reproduction.js index 30e83ef..c3a67fe 100644 --- a/world/reproduction.js +++ b/world/reproduction.js @@ -2,9 +2,10 @@ import {stableId,hash32} from './rng.js'; import {recombineGenomes} from './genetics.js'; import {PRIMITIVE_SIGNALS} from './constants.js'; import {appendEvent} from './ledger.js'; +import {ensureCognitionState} from './cognition-state.js'; const GESTATION_MINUTES=280*1440; function adult(c){return c.body.ageMinutes>=16*525600;} export function conceive(world,a,b,at=world.clock.worldMinute){if(!a?.alive||!b?.alive)throw new Error('parent_unavailable');const gestating=a.body.reproductiveRole==='gestating'?a:b.body.reproductiveRole==='gestating'?b:null;const partner=gestating===a?b:a;if(!gestating||partner.body.reproductiveRole!=='non_gestating')throw new Error('reproductive_roles_incompatible');if(!adult(gestating)||!adult(partner))throw new Error('biological_maturity_required');if(gestating.body.pregnancy)throw new Error('pregnancy_already_active');const p={id:stableId('pregnancy',gestating.id,partner.id,at),partnerId:partner.id,conceivedWorldMinute:at,dueWorldMinute:at+GESTATION_MINUTES};gestating.body.pregnancy=p;appendEvent(world,'CONCEPTION',gestating.id,{pregnancyId:p.id,partnerId:partner.id,dueWorldMinute:p.dueWorldMinute},[],at);return p;} -function newborn(world,mother,partner,at,pregnancy){const genome=recombineGenomes(mother.genome,partner.genome,pregnancy.id);const id=stableId('citizen',pregnancy.id,'birth');const role=hash32(id)%2?'gestating':'non_gestating';return {id,kind:'BORN_IN_WORLD',selfName:null,externalId:null,alive:true,birthWorldMinute:at,deathWorldMinute:null,position:{...mother.position},genome,body:{massKg:3.2+(hash32(id)%700)/1000,hydration:96,calories:96,sleepPressure:15,temperatureC:36.8,health:100,injuries:[],diseases:[],fertility:genome.fertility,pregnancy:null,reproductiveRole:role,ageMinutes:0,alive:true},psychology:{curiosity:.45, riskTolerance:.25,socialDrive:.8,aggression:.08,empathy:.55,noveltySeeking:.5,stress:.2,fear:.15,attachment:.75,confidence:.1},knowledge:[],memories:[],knownEntityIds:[id,mother.id,partner.id],skills:{},language:{primitiveSignals:[...PRIMITIVE_SIGNALS],lexicon:{},heard:{},grammarPatterns:{}},beliefs:[],relationships:{[mother.id]:{familiarity:.8,trust:.5,affection:.6,fear:0,obligation:0},[partner.id]:{familiarity:.3,trust:.2,affection:.25,fear:0,obligation:0}},possessions:[],goals:[],activeGoal:null,plans:[],currentActionId:null,commitments:[],cognition:{lastReflectionMinute:null,pending:true,reason:'birth'}};} +function newborn(world,mother,partner,at,pregnancy){const genome=recombineGenomes(mother.genome,partner.genome,pregnancy.id);const id=stableId('citizen',pregnancy.id,'birth');const role=hash32(id)%2?'gestating':'non_gestating';const c={id,kind:'BORN_IN_WORLD',selfName:null,externalId:null,alive:true,birthWorldMinute:at,deathWorldMinute:null,position:{...mother.position},genome,body:{massKg:3.2+(hash32(id)%700)/1000,hydration:96,calories:96,sleepPressure:15,temperatureC:36.8,health:100,injuries:[],diseases:[],fertility:genome.fertility,pregnancy:null,reproductiveRole:role,ageMinutes:0,alive:true},psychology:{curiosity:.45, riskTolerance:.25,socialDrive:.8,aggression:.08,empathy:.55,noveltySeeking:.5,stress:.2,fear:.15,attachment:.75,confidence:.1},knowledge:[],memories:[],knownEntityIds:[id,mother.id,partner.id],skills:{},language:{primitiveSignals:[...PRIMITIVE_SIGNALS],lexicon:{},heard:{},grammarPatterns:{}},beliefs:[],relationships:{[mother.id]:{familiarity:.8,trust:.5,affection:.6,fear:0,obligation:0},[partner.id]:{familiarity:.3,trust:.2,affection:.25,fear:0,obligation:0}},possessions:[],goals:[],activeGoal:null,plans:[],currentActionId:null,commitments:[],cognition:{lastReflectionMinute:null,pending:true,reason:'birth'}};ensureCognitionState(c,at);return c;} export function advancePregnancies(world,toMinute){const born=[];for(const mother of [...world.citizens]){const p=mother.body.pregnancy;if(!p||p.dueWorldMinute>toMinute||!mother.alive)continue;const partner=world.citizens.find(c=>c.id===p.partnerId);if(!partner){mother.body.pregnancy=null;continue;}const child=newborn(world,mother,partner,p.dueWorldMinute,p);if(mother.body.massKg<=child.body.massKg+35){mother.body.pregnancy=null;appendEvent(world,'PREGNANCY_LOSS',mother.id,{pregnancyId:p.id,reason:'insufficient_biological_mass'},[p.id],p.dueWorldMinute);continue;}mother.body.massKg-=child.body.massKg;world.citizens.push(child);mother.body.pregnancy=null;world.cognitionQueue.push({citizenId:child.id,reason:'birth',priority:.9});appendEvent(world,'BIRTH',child.id,{motherId:mother.id,partnerId:partner.id,pregnancyId:p.id},[p.id],p.dueWorldMinute);born.push(child);}return born;} From a5fa02b5e9f2933deb96701f176dc39ea40a81bf Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:32:35 +0200 Subject: [PATCH 04/10] feat: make local cognition affordance driven --- tests/test_sovereign_affordance_planner.mjs | 103 +++++++++++++++ world/affordances.js | 132 ++++++++++++++++++++ world/cognition.js | 14 +-- world/engine.js | 29 ++++- 4 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 tests/test_sovereign_affordance_planner.mjs create mode 100644 world/affordances.js diff --git a/tests/test_sovereign_affordance_planner.mjs b/tests/test_sovereign_affordance_planner.mjs new file mode 100644 index 0000000..0482e9d --- /dev/null +++ b/tests/test_sovereign_affordance_planner.mjs @@ -0,0 +1,103 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createSovereignGenesis} from '../world/genesis.js'; +import {learn} from '../world/epistemics.js'; +import {resourceConceptId} from '../world/perception.js'; +import {recordAffordanceOutcome} from '../world/cognition-state.js'; +import {chooseAffordance,enumerateAffordances,scoreAffordance} from '../world/affordances.js'; +import {applyAcceptedPlan} from '../world/cognition.js'; +import {advanceWorldTo} from '../world/engine.js'; + +function reveal(world,citizen,deposit){ + citizen.position={...deposit.position}; + if(!citizen.knownEntityIds.includes(deposit.id))citizen.knownEntityIds.push(deposit.id); + learn(citizen,resourceConceptId(deposit),{kind:'observation',eventId:`seen:${deposit.id}`,entityId:deposit.id}); +} + +function hold(world,citizen,deposit,id=`held:${deposit.type}`){ + const object={id,kind:'gathered_material',material:deposit.type,quantity:1,massPerUnitKg:1,properties:null,holderId:citizen.id,position:{...citizen.position},condition:1,provenance:{type:'GATHERED',depositId:deposit.id}}; + world.objects.push(object);citizen.possessions.push(object.id);return object; +} + +test('curious citizen gathers then experiments on an untested material',()=>{ + const world=createSovereignGenesis({seed:31,realEpochMs:0}); + const citizen=world.citizens[0],deposit=world.resourceDeposits.find(d=>d.type==='timber'); + reveal(world,citizen,deposit); + citizen.psychology.curiosity=1; + const first=chooseAffordance(world,citizen,0); + assert.equal(first.actions.at(-1).type,'GATHER'); + hold(world,citizen,deposit); + const second=chooseAffordance(world,citizen,30); + assert.equal(second.actions[0].type,'EXPERIMENT'); +}); + +test('invalid targets are excluded and an isolated citizen falls back safely',()=>{ + const world=createSovereignGenesis({seed:31,realEpochMs:0}); + const citizen=world.citizens[0]; + citizen.knownEntityIds=[citizen.id]; + citizen.knowledge=[]; + citizen.body.sleepPressure=10;citizen.body.hydration=80;citizen.body.calories=80; + const proposal=chooseAffordance(world,citizen,0); + assert.ok(['MOVE','OBSERVE','REST'].includes(proposal.actions[0].type)); + assert.equal((proposal.actions[0].concepts||[]).length,0); +}); + +test('social drive and trust raise communication and teaching utility',()=>{ + const world=createSovereignGenesis({seed:41,realEpochMs:0}); + const [citizen,other]=world.citizens; + other.position={...citizen.position}; + citizen.knownEntityIds.push(other.id); + learn(citizen,'shared:test',{kind:'observation',eventId:'shared'}); + citizen.relationships[other.id]={familiarity:.8,trust:.85,affection:.4,fear:0,obligation:0}; + citizen.psychology.socialDrive=.95; + const candidates=enumerateAffordances(world,citizen,0); + const communicate=candidates.find(x=>x.family==='communicate'); + const teach=candidates.find(x=>x.family==='teach'); + assert.ok(communicate);assert.ok(teach); + const socialScore=scoreAffordance(world,citizen,communicate,0); + const teachingScore=scoreAffordance(world,citizen,teach,0); + citizen.psychology.socialDrive=.05; + citizen.relationships[other.id].trust=.05; + assert.ok(socialScore>scoreAffordance(world,citizen,communicate,0)); + assert.ok(teachingScore>scoreAffordance(world,citizen,teach,0)); +}); + +test('distance, risk aversion, cooldown, and failure lower utility',()=>{ + const world=createSovereignGenesis({seed:43,realEpochMs:0}); + const citizen=world.citizens[0],deposit=world.resourceDeposits.find(d=>d.type==='stone'); + reveal(world,citizen,deposit); + const near=enumerateAffordances(world,citizen,100).find(x=>x.family==='gather'&&x.proposal.actions.at(-1).targetId===deposit.id); + citizen.psychology.riskTolerance=1; + const nearScore=scoreAffordance(world,citizen,near,100); + citizen.position={x:95,y:95}; + citizen.psychology.riskTolerance=0; + const far=enumerateAffordances(world,citizen,100).find(x=>x.family==='gather'&&x.proposal.actions.at(-1).targetId===deposit.id); + const farScore=scoreAffordance(world,citizen,far,100); + assert.ok(nearScore>farScore); + recordAffordanceOutcome(citizen,'gather',{ok:false,reason:'blocked'},100); + assert.ok(scoreAffordance(world,citizen,far,101){ + const world=createSovereignGenesis({seed:47,realEpochMs:0}); + const [citizen,other]=world.citizens,deposit=world.resourceDeposits.find(d=>d.type==='clay'); + reveal(world,citizen,deposit);const object=hold(world,citizen,deposit); + other.position={...citizen.position};citizen.knownEntityIds.push(other.id); + citizen.relationships[other.id]={familiarity:.8,trust:.8,affection:.4,fear:0,obligation:.2}; + learn(citizen,`tested:${object.id}`,{kind:'experiment',eventId:'tested',evidence:{entityId:object.id,property:'hardness',value:.25}}); + const candidates=enumerateAffordances(world,citizen,200); + assert.ok(candidates.some(x=>x.family==='transfer')); + assert.ok(candidates.some(x=>x.family==='transform')); + assert.equal(candidates.some(x=>x.proposal.actions.some(a=>a.type==='COOPERATE')),false); +}); + +test('resolved local plans feed bounded outcome learning',()=>{ + const world=createSovereignGenesis({seed:53,realEpochMs:0}); + const citizen=world.citizens[0],deposit=world.resourceDeposits.find(d=>d.type==='timber'); + reveal(world,citizen,deposit);citizen.body.hydration=90;citizen.body.calories=90;citizen.body.sleepPressure=10; + const selected=chooseAffordance(world,citizen,0); + assert.equal(selected.affordanceFamily,'gather'); + applyAcceptedPlan(world,citizen,selected,0); + advanceWorldTo(world,20_000); + assert.equal(citizen.cognition.local.outcomes.gather.successes,1); +}); diff --git a/world/affordances.js b/world/affordances.js new file mode 100644 index 0000000..4bc98f9 --- /dev/null +++ b/world/affordances.js @@ -0,0 +1,132 @@ +import {knows} from './epistemics.js'; +import {ensureCognitionState,outcomeModifier} from './cognition-state.js'; +import {resourceConceptId} from './perception.js'; +import {MATERIAL_PROPERTIES} from './materials.js'; +import {hash32,stableId} from './rng.js'; + +const clamp01=value=>Math.max(0,Math.min(1,Number(value)||0)); +const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y); +const heldObjects=(world,citizen)=>world.objects.filter(object=>object.holderId===citizen.id&&object.quantity>0&&!object.reservedProjectId); +const activeKnowledge=citizen=>(citizen.knowledge||[]).filter(entry=>entry.active!==false); +const relation=(citizen,targetId)=>citizen.relationships?.[targetId]||{}; + +export const AFFORDANCE_WEIGHTS=Object.freeze({ + strategy:.55,curiosity:.45,social:.4,relationship:.3,inventoryFit:.35, + knowledgeGap:.5,novelty:.4,effort:-.25,risk:-.3,cooldown:-.8,outcome:.35, +}); + +function proposal(family,concepts,actions){ + return {source:'local',affordanceFamily:family,concepts:[...new Set(concepts.filter(Boolean))],actions}; +} + +function resourceAction(citizen,deposit,type,duration,purpose,payload={}){ + const concept=resourceConceptId(deposit),distance=dist(citizen.position,deposit.position); + const terminal={type,durationMinutes:duration,targetId:deposit.id,purpose,concepts:[concept],payload}; + return distance>1 + ?[{type:'MOVE',durationMinutes:Math.max(2,Math.ceil(distance*2)),targetId:deposit.id,targetPosition:deposit.position,purpose,concepts:[concept]},terminal] + :[terminal]; +} + +function knowledgeForEntity(citizen,entityId){ + return activeKnowledge(citizen).filter(entry=>(entry.provenance||[]).some(source=>source?.evidence?.entityId===entityId)); +} + +function hasUnknownObservableProperty(citizen,target){ + const properties=target.properties||MATERIAL_PROPERTIES[target.material||target.type]||{}; + return Object.keys(properties).some(property=>!knows(citizen,stableId('kprop',target.id,property))); +} + +function exploreCandidate(world,citizen,at){ + const hash=hash32(`${world.seed}|${citizen.id}|${Math.floor(at/30)}|explore`); + const angle=(hash%10000)/10000*Math.PI*2,radius=5+((hash>>>8)%800)/100; + const target={x:Math.max(5,Math.min(95,citizen.position.x+Math.cos(angle)*radius)),y:Math.max(5,Math.min(95,citizen.position.y+Math.sin(angle)*radius))}; + const distance=dist(citizen.position,target); + return {family:'explore',key:`explore:${Math.floor(target.x)}:${Math.floor(target.y)}`,utility:.12,novelty:.8,effort:Math.min(1,distance/30),risk:.25,proposal:proposal('explore',[],[ + {type:'MOVE',durationMinutes:Math.max(4,Math.ceil(distance*2)),targetPosition:target,purpose:'explore',concepts:[]}, + {type:'OBSERVE',durationMinutes:8,purpose:'explore',concepts:[]}, + ])}; +} + +export function enumerateAffordances(world,citizen,at=world.clock.worldMinute){ + if(!citizen?.alive||citizen.currentActionId)return []; + const candidates=[],held=heldObjects(world,citizen); + const knownResources=world.resourceDeposits.filter(deposit=>deposit.quantity>0&&citizen.knownEntityIds.includes(deposit.id)&&knows(citizen,resourceConceptId(deposit))); + const nearby=world.citizens.filter(other=>other.id!==citizen.id&&other.alive&&citizen.knownEntityIds.includes(other.id)&&dist(citizen.position,other.position)<=10); + + for(const deposit of knownResources){ + const distance=dist(citizen.position,deposit.position),heldSame=held.reduce((sum,object)=>sum+(object.material===deposit.type?object.quantity:0),0); + candidates.push({family:'gather',key:`gather:${deposit.id}`,targetId:deposit.id,utility:.32,inventoryFit:clamp01(1-heldSame/2),novelty:heldSame?0:.25,effort:Math.min(1,distance/50),risk:clamp01(deposit.accessDifficulty),proposal:proposal('gather',[resourceConceptId(deposit)],resourceAction(citizen,deposit,'GATHER',12,'self_directed',{quantity:1}))}); + if(!held.length&&hasUnknownObservableProperty(citizen,deposit))candidates.push({family:'experiment',key:`experiment:${deposit.id}:observe`,targetId:deposit.id,utility:.08,knowledgeGap:.55,novelty:.55,effort:Math.min(1,distance/50),risk:clamp01(deposit.accessDifficulty),proposal:proposal('experiment',[resourceConceptId(deposit)],[{type:'EXPERIMENT',durationMinutes:30,targetId:deposit.id,purpose:'experiment',concepts:[resourceConceptId(deposit)],payload:{targetIds:[deposit.id],methodCode:'observe'}}])}); + } + + for(const object of held){ + if(hasUnknownObservableProperty(citizen,object))candidates.push({family:'experiment',key:`experiment:${object.id}:observe`,targetId:object.id,utility:.48,knowledgeGap:1,inventoryFit:.8,novelty:.85,effort:.05,risk:.05,proposal:proposal('experiment',[],[{type:'EXPERIMENT',durationMinutes:30,targetId:object.id,purpose:'experiment',concepts:[],payload:{targetIds:[object.id],methodCode:'observe'}}])}); + if(knowledgeForEntity(citizen,object.id).length)candidates.push({family:'transform',key:`transform:${object.id}`,targetId:object.id,utility:.24,inventoryFit:.9,knowledgeGap:.2,novelty:.55,effort:.2,risk:.12,proposal:proposal('transform',knowledgeForEntity(citizen,object.id).map(entry=>entry.concept),[{type:'ASSEMBLE',durationMinutes:35,purpose:'experiment',concepts:knowledgeForEntity(citizen,object.id).map(entry=>entry.concept),payload:{inputObjectIds:[object.id],quantities:[object.quantity],form:'bundle'}}])}); + } + + const concepts=activeKnowledge(citizen).map(entry=>entry.concept); + for(const other of nearby){ + const gap=concepts.find(concept=>!knows(other,concept)); + if(gap){ + const rel=relation(citizen,other.id); + candidates.push({family:'communicate',key:`communicate:${other.id}:${gap}`,targetId:other.id,utility:.18,social:1,relationship:clamp01((Number(rel.familiarity)||0)+(Number(rel.trust)||0))/2,knowledgeGap:.75,novelty:.5,effort:.05,risk:clamp01(rel.fear),proposal:proposal('communicate',[gap],[{type:'COMMUNICATE',durationMinutes:5,targetId:other.id,purpose:'communicate',concepts:[gap],payload:{concept:gap}}])}); + if(Number(rel.trust||0)>=.55)candidates.push({family:'teach',key:`teach:${other.id}:${gap}`,targetId:other.id,utility:.16,social:.8,relationship:clamp01(rel.trust),knowledgeGap:.65,novelty:.25,effort:.08,risk:clamp01(rel.fear),proposal:proposal('teach',[gap],[{type:'TEACH',durationMinutes:10,targetId:other.id,purpose:'communicate',concepts:[gap],payload:{concept:gap}}])}); + } + const need=Math.max(clamp01((55-other.body.hydration)/55),clamp01((55-other.body.calories)/55),clamp01((75-other.body.health)/75)); + if(need>0)candidates.push({family:'care',key:`care:${other.id}`,targetId:other.id,utility:.16+need*.3,social:.8,relationship:clamp01(Number(relation(citizen,other.id).affection)||0),knowledgeGap:0,novelty:.15,effort:.05,risk:0,proposal:proposal('care',[],[{type:'CARE',durationMinutes:10,targetId:other.id,purpose:'care',concepts:[]}])}); + const gift=held[0],trust=Number(relation(citizen,other.id).trust)||0; + if(gift&&trust>=.3)candidates.push({family:'transfer',key:`transfer:${other.id}:${gift.id}`,targetId:other.id,utility:.12,social:.7,relationship:clamp01(trust),inventoryFit:clamp01(gift.quantity/4),novelty:.2,effort:.05,risk:0,proposal:proposal('transfer',[],[{type:'TRANSFER',durationMinutes:5,targetId:other.id,purpose:'cooperate',concepts:[],payload:{objectId:gift.id}}])}); + } + + const activeProjects=(world.projects||[]).filter(project=>project.status==='construction'&&(project.initiatorId===citizen.id||citizen.knownEntityIds.includes(project.id))); + for(const project of activeProjects){ + const distance=dist(citizen.position,project.site),conceptsForProject=(world.designs.find(design=>design.id===project.designId)?.concepts||[]).filter(concept=>knows(citizen,concept)); + if(!conceptsForProject.length)continue; + const action={type:'BUILD',durationMinutes:Math.min(120,Math.max(15,project.workRequiredMinutes-project.workDoneMinutes)),targetId:project.id,purpose:'construct',concepts:conceptsForProject,payload:{projectId:project.id,workMinutes:120}}; + candidates.push({family:project.initiatorId===citizen.id?'build':'cooperate',key:`build:${project.id}`,targetId:project.id,utility:.42,relationship:project.initiatorId===citizen.id?0:clamp01(relation(citizen,project.initiatorId).trust),inventoryFit:.8,novelty:.25,effort:Math.min(1,distance/50),risk:.1,proposal:proposal(project.initiatorId===citizen.id?'build':'cooperate',conceptsForProject,distance>1?[{type:'MOVE',durationMinutes:Math.max(2,Math.ceil(distance*2)),targetId:project.id,targetPosition:project.site,purpose:'cooperate',concepts:conceptsForProject},action]:[action])}); + } + + const testedHeld=held.filter(object=>knowledgeForEntity(citizen,object.id).length); + if(testedHeld.length>=2&&!activeProjects.some(project=>project.initiatorId===citizen.id)){ + const conceptsForBuild=[...new Set(testedHeld.flatMap(object=>knowledgeForEntity(citizen,object.id).map(entry=>entry.concept)))].slice(0,12); + const offset=2+(hash32(`${world.seed}|${citizen.id}|build`)%3),site={x:Math.max(2,Math.min(98,citizen.position.x+offset)),y:Math.max(2,Math.min(98,citizen.position.y+1))}; + candidates.push({family:'build',key:`build:new:${Math.floor(site.x)}:${Math.floor(site.y)}`,utility:.28,inventoryFit:1,knowledgeGap:.2,novelty:.8,effort:.4,risk:.2,proposal:proposal('build',conceptsForBuild,[{type:'BUILD',durationMinutes:120,purpose:'construct',concepts:conceptsForBuild,payload:{inputObjectIds:testedHeld.map(object=>object.id),site,workMinutes:240,form:'structure'}}])}); + } + + candidates.push(exploreCandidate(world,citizen,at)); + candidates.push({family:'rest',key:'rest',utility:.04+clamp01(citizen.body.sleepPressure/100)*.35,novelty:0,effort:0,risk:0,proposal:proposal('rest',[],[{type:'REST',durationMinutes:20,purpose:'self_directed',concepts:[]}])}); + return candidates; +} + +export function scoreAffordance(world,citizen,candidate,at=world.clock.worldMinute){ + const state=ensureCognitionState(citizen,at),psychology=citizen.psychology||{},rel=relation(citizen,candidate.targetId); + const desired=new Set(citizen.activeGoal?.actionTypes||[]),actions=candidate.proposal.actions||[]; + const strategy=actions.some(action=>desired.has(action.type))?1:0; + const cooldown=Number(state.cooldowns?.[candidate.key]?.untilWorldMinute||0)>at?1:0; + const relationValue=Math.max(clamp01(candidate.relationship),clamp01((Number(rel.trust)||0)+(Number(rel.familiarity)||0))/2); + return Number(candidate.utility||0) + +AFFORDANCE_WEIGHTS.strategy*strategy + +AFFORDANCE_WEIGHTS.curiosity*clamp01(psychology.curiosity)*clamp01(candidate.knowledgeGap||candidate.novelty) + +AFFORDANCE_WEIGHTS.social*clamp01(psychology.socialDrive)*clamp01(candidate.social) + +AFFORDANCE_WEIGHTS.relationship*relationValue + +AFFORDANCE_WEIGHTS.inventoryFit*clamp01(candidate.inventoryFit) + +AFFORDANCE_WEIGHTS.knowledgeGap*clamp01(candidate.knowledgeGap) + +AFFORDANCE_WEIGHTS.novelty*clamp01(psychology.noveltySeeking)*clamp01(candidate.novelty) + +AFFORDANCE_WEIGHTS.effort*clamp01(candidate.effort) + +AFFORDANCE_WEIGHTS.risk*clamp01(candidate.risk)*(1-clamp01(psychology.riskTolerance)) + +AFFORDANCE_WEIGHTS.cooldown*cooldown + +AFFORDANCE_WEIGHTS.outcome*outcomeModifier(citizen,candidate.family,at); +} + +function stableCandidateOrder(world,citizen,a,b,at){ + const left=hash32(`${world.seed}|${citizen.id}|${Math.floor(at/10)}|${a.key}`),right=hash32(`${world.seed}|${citizen.id}|${Math.floor(at/10)}|${b.key}`); + return left-right||a.key.localeCompare(b.key); +} + +export function chooseAffordance(world,citizen,at=world.clock.worldMinute){ + const candidates=enumerateAffordances(world,citizen,at); + if(!candidates.length)return null; + const selected=candidates.map(candidate=>({...candidate,score:scoreAffordance(world,citizen,candidate,at)})).sort((a,b)=>b.score-a.score||stableCandidateOrder(world,citizen,a,b,at))[0]; + ensureCognitionState(citizen,at).lastLocalChoiceMinute=at; + return selected.proposal; +} diff --git a/world/cognition.js b/world/cognition.js index 0dbbbb2..148f4e0 100644 --- a/world/cognition.js +++ b/world/cognition.js @@ -1,10 +1,11 @@ import {validateProposalKnowledge,knows} from './epistemics.js'; import {ACTION_TYPES,SAFE_REFLEX_ACTIONS} from './constants.js'; import {startAction,activeAction,interruptAction} from './actions.js'; -import {stableId,hash32} from './rng.js'; +import {stableId} from './rng.js'; import {resourceConceptId,structureConceptId,sensoryEvidence} from './perception.js'; import {objectMass} from './materials.js'; import {adoptBelief} from './beliefs.js'; +import {chooseAffordance} from './affordances.js'; function heldObjects(world,citizen){return world.objects.filter(o=>o.holderId===citizen.id&&o.quantity>0);} export function buildCognitiveContext(world,citizen,at=world.clock.worldMinute){const nearbyCitizens=world.citizens.filter(c=>c.id!==citizen.id&&c.alive&&Math.hypot(c.position.x-citizen.position.x,c.position.y-citizen.position.y)<=10).filter(c=>citizen.knownEntityIds.includes(c.id)).map(c=>({id:c.id,selfName:c.selfName}));const knownResources=world.resourceDeposits.filter(d=>citizen.knownEntityIds.includes(d.id)&&knows(citizen,resourceConceptId(d))).map(d=>({entityId:d.id,conceptId:resourceConceptId(d),position:d.position,evidence:sensoryEvidence(d)}));const possessions=heldObjects(world,citizen).map(o=>({id:o.id,kind:o.kind,massKg:objectMass(o),quantity:o.quantity,provenanceType:o.provenance?.type||null}));const projects=world.projects.filter(p=>p.initiatorId===citizen.id&&p.status==='construction').map(p=>({id:p.id,position:p.site,progress:p.workRequiredMinutes?Math.round(p.workDoneMinutes/p.workRequiredMinutes*100):0}));const knownStructures=[...(world.buildings||[]),...(world.objects||[]).filter(o=>o.kind==='temporary_shelter'&&o.quantity>0)].filter(x=>citizen.knownEntityIds.includes(x.id)&&knows(citizen,structureConceptId(x))).map(x=>({entityId:x.id,conceptId:structureConceptId(x),position:x.position,physicalEvidence:{boundedCover:true,thermalProtection:Number(x.properties?.thermalProtection??x.protection?.thermal??.5)}}));return {worldMinute:at,self:{id:citizen.id,body:{hydration:citizen.body.hydration,calories:citizen.body.calories,sleepPressure:citizen.body.sleepPressure,health:citizen.body.health},position:citizen.position,psychology:citizen.psychology,activeGoal:citizen.activeGoal?{concepts:citizen.activeGoal.concepts||[],actionTypes:citizen.activeGoal.actionTypes||[]}:null},knowledge:citizen.knowledge.filter(k=>k.active!==false).map(k=>({id:k.concept,confidence:k.confidence,evidence:k.provenance.map(p=>p.evidence).filter(Boolean).slice(-3)})),memories:citizen.memories.slice(-18).map(m=>({kind:m.kind,content:m.content,confidence:m.confidence})),knownEntities:nearbyCitizens,knownResources,knownStructures,possessions,projects,relationships:Object.fromEntries(Object.entries(citizen.relationships).filter(([id])=>citizen.knownEntityIds.includes(id))),beliefs:(citizen.beliefs||[]).map(b=>({id:b.id,stanceCode:b.stanceCode,conceptIds:b.conceptIds,confidence:b.confidence})).slice(-24),externalDirection:world.privateHumanIntents[citizen.id]?.intent||null};} @@ -12,21 +13,14 @@ export function buildCognitiveContext(world,citizen,at=world.clock.worldMinute){ function validateBeliefUpdates(citizen,proposal){for(const b of proposal?.beliefUpdates||[]){for(const concept of b.conceptIds||[])if(!knows(citizen,concept))return {ok:false,reason:`unknown_concept:${concept}`};}return {ok:true};} function validatePayload(world,citizen,a){const held=new Set(heldObjects(world,citizen).map(o=>o.id));const p=a.payload||{};if(['BUILD','ASSEMBLE','CUT','DIG','HEAT','COOL','MIX'].includes(a.type)){for(const id of p.inputObjectIds||p.transformation?.inputObjectIds||[])if(!held.has(id))return {ok:false,reason:`object_not_held:${id}`};}if(a.type==='TRANSFER'&&p.objectId&&!held.has(p.objectId))return {ok:false,reason:`object_not_held:${p.objectId}`};if(['TEACH','COMMUNICATE','PROMISE','CLAIM'].includes(a.type)){for(const c of [p.concept,...(p.conceptIds||[]),p.purposeConcept,p.ruleConcept].filter(Boolean))if(!knows(citizen,c))return {ok:false,reason:`unknown_concept:${c}`};}if(a.type==='COMMUNICATE'&&p.beliefId&&!(citizen.beliefs||[]).some(b=>b.id===p.beliefId))return {ok:false,reason:`belief_not_held:${p.beliefId}`};return {ok:true};} export function validateCognitiveProposal(world,citizen,proposal){if(!proposal||typeof proposal!=='object')return {ok:false,reason:'proposal_invalid'};const knowledge=validateProposalKnowledge(world,citizen,proposal);if(!knowledge.ok)return knowledge;const beliefs=validateBeliefUpdates(citizen,proposal);if(!beliefs.ok)return beliefs;if(!Array.isArray(proposal.actions))return {ok:false,reason:'actions_required'};for(const a of proposal.actions){if(!ACTION_TYPES.has(a.type))return {ok:false,reason:`action_invalid:${a.type}`};if(a.targetId&&a.targetId!==citizen.id&&!citizen.knownEntityIds.includes(a.targetId)&&!world.projects.some(p=>p.id===a.targetId&&p.initiatorId===citizen.id))return {ok:false,reason:`unknown_target:${a.targetId}`};const pv=validatePayload(world,citizen,a);if(!pv.ok)return pv;}return {ok:true};} -export function applyAcceptedPlan(world,citizen,proposal,at=world.clock.worldMinute){const validation=validateCognitiveProposal(world,citizen,proposal);if(!validation.ok)throw new Error(validation.reason);for(const b of proposal.beliefUpdates||[])adoptBelief(world,citizen,b,{kind:'reflection'},at);const current=activeAction(world,citizen);if(current&&['OBSERVE','REST'].includes(current.type))interruptAction(world,citizen,'cognitive_plan',at);else if(current)throw new Error('citizen_busy');const plan={id:stableId('plan',citizen.id,citizen.plans.length,at),createdWorldMinute:at,concepts:[...(proposal.concepts||[])],actions:proposal.actions,status:'active',cursor:0,source:proposal.source||'cognition'};if(!['reflex','local'].includes(plan.source))citizen.activeGoal={concepts:plan.concepts,actionTypes:plan.actions.map(a=>a.type),source:plan.source,createdWorldMinute:at};citizen.plans.push(plan);citizen.cognition.pending=false;if(plan.actions.length){const a=plan.actions[0];startAction(world,citizen,{...a,planId:plan.id,durationMinutes:a.durationMinutes||10},at);}else plan.status='completed';return plan;} +export function applyAcceptedPlan(world,citizen,proposal,at=world.clock.worldMinute){const validation=validateCognitiveProposal(world,citizen,proposal);if(!validation.ok)throw new Error(validation.reason);for(const b of proposal.beliefUpdates||[])adoptBelief(world,citizen,b,{kind:'reflection'},at);const current=activeAction(world,citizen);if(current&&['OBSERVE','REST'].includes(current.type))interruptAction(world,citizen,'cognitive_plan',at);else if(current)throw new Error('citizen_busy');const plan={id:stableId('plan',citizen.id,citizen.plans.length,at),createdWorldMinute:at,concepts:[...(proposal.concepts||[])],actions:proposal.actions,status:'active',cursor:0,source:proposal.source||'cognition',affordanceFamily:proposal.affordanceFamily||null};if(!['reflex','local'].includes(plan.source))citizen.activeGoal={concepts:plan.concepts,actionTypes:plan.actions.map(a=>a.type),source:plan.source,createdWorldMinute:at};citizen.plans.push(plan);citizen.cognition.pending=false;if(plan.actions.length){const a=plan.actions[0];startAction(world,citizen,{...a,planId:plan.id,durationMinutes:a.durationMinutes||10},at);}else plan.status='completed';return plan;} export function continuePlan(world,citizen,completedAction,at){if(!completedAction.planId)return false;const plan=citizen.plans.find(p=>p.id===completedAction.planId);if(!plan||plan.status!=='active')return false;plan.cursor++;if(plan.cursor>=plan.actions.length){plan.status='completed';if(citizen.activeGoal)citizen.activeGoal.lastPlanCompletedWorldMinute=at;citizen.cognition.pending=true;return false;}const a=plan.actions[plan.cursor];startAction(world,citizen,{...a,planId:plan.id,durationMinutes:a.durationMinutes||10},at);return true;} export function survivalFallback(world,citizen){if(!citizen.alive||citizen.currentActionId)return null;const b=citizen.body,ambient=Number(world.environment?.temperatureC??18);if(ambient<5||ambient>36){const structures=[...(world.buildings||[]),...(world.objects||[]).filter(o=>o.kind==='temporary_shelter'&&o.quantity>0)].filter(x=>citizen.knownEntityIds.includes(x.id)).sort((a,b)=>dist(citizen.position,a.position)-dist(citizen.position,b.position));const s=structures[0];if(s&&dist(citizen.position,s.position)>1)return {source:'reflex',concepts:[],actions:[{type:'MOVE',durationMinutes:Math.max(2,Math.ceil(dist(citizen.position,s.position)*2)),targetId:s.id,targetPosition:s.position,purpose:'survival',concepts:[]},{type:'REST',durationMinutes:30,purpose:'survival'}]};}const infant=b.ageMinutes<2*525600;if(infant&&(b.hydration<55||b.calories<55||b.sleepPressure>75)){const caregiver=Object.keys(citizen.relationships||{})[0]||citizen.knownEntityIds.find(id=>id!==citizen.id);return caregiver?{source:'reflex',concepts:[],actions:[{type:'COMMUNICATE',durationMinutes:2,targetId:caregiver,purpose:'survival',payload:{primitiveSignal:'need'}},{type:'REST',durationMinutes:8,purpose:'survival'}]}:{source:'reflex',concepts:[],actions:[{type:'REST',durationMinutes:8,purpose:'survival'}]};}if(b.sleepPressure>85)return {source:'reflex',concepts:[],actions:[{type:'SLEEP',durationMinutes:240,purpose:'survival'}]};if(b.hydration<35)return nearestKnownResourcePlan(world,citizen,'water','DRINK');if(b.calories<30)return nearestKnownResourcePlan(world,citizen,'food','EAT');return null;} function nearestKnownResourcePlan(world,citizen,type,terminal){const ds=world.resourceDeposits.filter(d=>d.type===type&&knows(citizen,resourceConceptId(d))).sort((a,b)=>dist(citizen.position,a.position)-dist(citizen.position,b.position));const d=ds[0];if(!d)return {source:'reflex',concepts:[],actions:[{type:'OBSERVE',durationMinutes:10,purpose:'orientation'}]};const distance=dist(citizen.position,d.position),concept=resourceConceptId(d);return {source:'reflex',concepts:[concept],actions:distance>1?[{type:'MOVE',durationMinutes:Math.max(2,Math.ceil(distance*2)),targetPosition:d.position,targetId:d.id,purpose:'biological_need',concepts:[concept]},{type:terminal,durationMinutes:5,targetId:d.id,purpose:'biological_need',concepts:[concept]}]:[{type:terminal,durationMinutes:5,targetId:d.id,purpose:'biological_need',concepts:[concept]}]};} function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);} export function localDeliberation(world,citizen,at=world.clock.worldMinute){ - if(!citizen.alive||citizen.currentActionId)return null; - const goal=citizen.activeGoal||null,desired=new Set(goal?.actionTypes||[]),concepts=(goal?.concepts||[]).filter(c=>knows(citizen,c)); - if(desired.has('BUILD')){const project=world.projects.find(p=>p.initiatorId===citizen.id&&p.status==='construction');if(project)return {source:'local',concepts,actions:[{type:'BUILD',durationMinutes:Math.min(120,Math.max(15,project.workRequiredMinutes-project.workDoneMinutes)),targetId:project.id,purpose:'construct',concepts,payload:{projectId:project.id,workMinutes:120}}]};} - if(desired.has('GATHER')){const d=world.resourceDeposits.find(x=>citizen.knownEntityIds.includes(x.id)&&concepts.includes(resourceConceptId(x))&&x.quantity>0);if(d){const distance=dist(citizen.position,d.position),g={type:'GATHER',durationMinutes:12,targetId:d.id,purpose:'self_directed',concepts:[resourceConceptId(d)],payload:{quantity:1}};return {source:'local',concepts:[resourceConceptId(d)],actions:distance>1?[{type:'MOVE',durationMinutes:Math.max(2,Math.ceil(distance*2)),targetId:d.id,targetPosition:d.position,purpose:'self_directed',concepts:[resourceConceptId(d)]},g]:[g]};}} - if(desired.has('EXPERIMENT')){const held=heldObjects(world,citizen)[0],known=world.resourceDeposits.find(d=>citizen.knownEntityIds.includes(d.id)&&knows(citizen,resourceConceptId(d))),target=held?.id||known?.id;if(target)return {source:'local',concepts,actions:[{type:'EXPERIMENT',durationMinutes:30,targetId:target,purpose:'experiment',concepts,payload:{targetIds:[target],methodCode:'observe'}}]};} - if(desired.has('COMMUNICATE')||desired.has('TEACH')){const other=world.citizens.find(c=>c.id!==citizen.id&&c.alive&&citizen.knownEntityIds.includes(c.id)&&dist(c.position,citizen.position)<=10),concept=concepts[0];if(other&&concept)return {source:'local',concepts:[concept],actions:[{type:desired.has('TEACH')?'TEACH':'COMMUNICATE',durationMinutes:5,targetId:other.id,purpose:'communicate',concepts:[concept],payload:{concept}}]};} - const h=hash32(`${world.seed}|${citizen.id}|${Math.floor(at/10)}`),angle=(h%10000)/10000*Math.PI*2,radius=4+((h>>>8)%900)/100,tx=Math.max(5,Math.min(95,citizen.position.x+Math.cos(angle)*radius)),ty=Math.max(5,Math.min(95,citizen.position.y+Math.sin(angle)*radius)); - return {source:'local',concepts,actions:[{type:'MOVE',durationMinutes:Math.max(4,Math.ceil(dist(citizen.position,{x:tx,y:ty})*2)),targetPosition:{x:tx,y:ty},purpose:goal?'self_directed':'explore',concepts:[]},{type:'OBSERVE',durationMinutes:8,purpose:'explore',concepts:[]}]}; + return chooseAffordance(world,citizen,at); } function safePayload(type,p={}){if(type==='GATHER')return {quantity:Math.max(.1,Math.min(20,Number(p.quantity)||1))};if(['TEACH','COMMUNICATE'].includes(type))return {concept:p.concept?String(p.concept):null,conceptIds:Array.isArray(p.conceptIds)?p.conceptIds.map(String).slice(0,6):[],beliefId:p.beliefId?String(p.beliefId):null,primitiveSignal:['attention','danger','need','point','accept','reject'].includes(p.primitiveSignal)?p.primitiveSignal:null};if(type==='PROMISE')return {kindCode:['join','share','access','exchange','care','defend','other'].includes(p.kindCode)?p.kindCode:'other',purposeConcept:p.purposeConcept?String(p.purposeConcept):null};if(type==='CLAIM')return {predicateCode:['control','use','possession','boundary','authority_over','stewardship','other'].includes(p.predicateCode)?p.predicateCode:'other',subjectId:p.subjectId?String(p.subjectId):null,purposeConcept:p.purposeConcept?String(p.purposeConcept):null};if(type==='EXPERIMENT')return {targetIds:Array.isArray(p.targetIds)?p.targetIds.map(String).slice(0,6):[],methodCode:['observe','compare','stress','heat','combine','measure'].includes(p.methodCode)?p.methodCode:'observe'};if(type==='BUILD')return {projectId:p.projectId?String(p.projectId):null,inputObjectIds:Array.isArray(p.inputObjectIds)?p.inputObjectIds.map(String).slice(0,12):[],site:p.site&&Number.isFinite(Number(p.site.x))&&Number.isFinite(Number(p.site.y))?{x:Number(p.site.x),y:Number(p.site.y)}:null,workMinutes:Math.max(15,Math.min(10000,Number(p.workMinutes)||120)),form:['structure','shelter','platform','enclosure','tower','channel'].includes(p.form)?p.form:'structure'};if(type==='TRANSFER')return {objectId:p.objectId?String(p.objectId):null};if(['ASSEMBLE','CUT','DIG','HEAT','COOL','MIX'].includes(type))return {inputObjectIds:Array.isArray(p.inputObjectIds)?p.inputObjectIds.map(String).slice(0,8):[],quantities:Array.isArray(p.quantities)?p.quantities.map(Number).slice(0,8):[],form:['bundle','flat','elongated','hollow','frame','layered','fragmented'].includes(p.form)?p.form:'bundle'};return {};} diff --git a/world/engine.js b/world/engine.js index a9a638c..185a443 100644 --- a/world/engine.js +++ b/world/engine.js @@ -16,6 +16,7 @@ import {shareBelief} from './beliefs.js'; import {makeCommitment,makeClaim,maybeFormOrganization,updateRelationship} from './society.js'; import {runExperiment,applyConstructionWork,beginEmpiricalConstruction} from './artifacts.js'; import {transferObject,transformMaterials,objectMass,advanceDecomposition} from './materials.js'; +import {recordAffordanceOutcome} from './cognition-state.js'; function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);} function perceiveLocal(world,citizen,at){const discoveries=[...perceiveResources(world,citizen,at),...perceiveStructures(world,citizen,at)];if(discoveries.length)queueCognition(world,citizen,'discovery',.65);for(const other of world.citizens){if(other.id!==citizen.id&&other.alive&&dist(citizen.position,other.position)<=8&&!citizen.knownEntityIds.includes(other.id)){citizen.knownEntityIds.push(other.id);queueCognition(world,citizen,'encounter',.5);recordMemory(citizen,{kind:'episodic',content:{encounter:other.id},source:{kind:'observation'},confidence:.9,salience:.45,worldMinute:at});}}} @@ -28,7 +29,33 @@ function primitiveCommunicate(world,c,a,at){const t=world.citizens.find(x=>x.id= function resolveAction(world,a,at){const c=world.citizens.find(x=>x.id===a.actorId);if(!c||!c.alive)return;if(a.type==='DRINK')consumeFromDeposit(world,c,a,'water',.7,x=>restoreHydration(x,30),at);else if(a.type==='EAT')consumeFromDeposit(world,c,a,'food',.3,x=>restoreCalories(x,25),at);else if(a.type==='GATHER')gather(world,c,a,at);else if(a.type==='TEACH'){const target=world.citizens.find(x=>x.id===a.targetId&&x.alive),concept=a.payload?.concept;if(target&&concept&&knows(c,concept)){learn(target,concept,{kind:'teaching',from:c.id,eventId:a.id},.65,at);updateRelationship(world,c,target.id,{familiarity:.02,trust:.01},at);updateRelationship(world,target,c.id,{familiarity:.03,trust:.025},at);}}else if(a.type==='COMMUNICATE')primitiveCommunicate(world,c,a,at);else if(a.type==='PROMISE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);const cm=makeCommitment(world,c,{kind:a.payload?.kindCode||'other',counterpartyId:a.targetId,terms:{purposeConcept:a.payload?.purposeConcept||null}},at);if(t){updateRelationship(world,c,t.id,{familiarity:.02,obligation:.03},at);updateRelationship(world,t,c.id,{familiarity:.01},at);if(cm.kind==='join')maybeFormOrganization(world,c,t,a.payload?.purposeConcept,at);}}else if(a.type==='CLAIM')makeClaim(world,c,{subject:a.payload?.subjectId||a.targetId||c.id,predicate:a.payload?.predicateCode||'other',purposeConcept:a.payload?.purposeConcept||null},at);else if(a.type==='EXPERIMENT')runExperiment(world,c,{inputConcepts:a.concepts||[],targetIds:a.payload?.targetIds||[],method:a.payload?.methodCode||'observe'},at);else if(a.type==='BUILD')build(world,c,a,at);else if(a.type==='TRANSFER'&&a.payload?.objectId&&a.targetId)transferObject(world,a.payload.objectId,a.targetId,at);else if(['CUT','DIG','HEAT','COOL','MIX','ASSEMBLE'].includes(a.type))genericTransform(world,c,a,at);else if(a.type==='CARE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t&&c.body.calories>15){c.body.calories-=2;restoreCalories(t,8);restoreHydration(t,5);appendEvent(world,'CARE_GIVEN',c.id,{targetId:t.id},[a.id],at);updateRelationship(world,c,t.id,{affection:.035,trust:.02},at);updateRelationship(world,t,c.id,{affection:.045,trust:.05},at);}}else if(a.type==='ATTACK'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){const harm=4+Math.round(c.psychology.aggression*12);t.body.health=Math.max(0,t.body.health-harm);t.body.injuries.push({source:a.id,severity:harm/100,worldMinute:at});appendEvent(world,'VIOLENCE',c.id,{against:t.id,harm},[a.id],at);updateRelationship(world,c,t.id,{trust:-.08,affection:-.06},at);updateRelationship(world,t,c.id,{trust:-.16,affection:-.12,fear:.18},at);if(t.body.health<=0)killCitizen(world,t,'trauma',at);}}else if(a.type==='REPRODUCE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){try{conceive(world,c,t,at);}catch{}}}} function queueCognition(world,citizen,reason,priority=.45){if(!citizen?.alive)return;if(world.cognitionQueue.some(x=>x.citizenId===citizen.id&&x.reason===reason))return;world.cognitionQueue.push({citizenId:citizen.id,reason,priority});citizen.cognition.pending=true;} function nextBoundary(world,cursor,target){let next=Math.min(target,cursor+30);for(const a of world.actions)if(a.status==='active'&&a.endsWorldMinute>cursor)next=Math.min(next,a.endsWorldMinute);for(const c of world.citizens){const due=c.body.pregnancy?.dueWorldMinute;if(due>cursor)next=Math.min(next,due);}return Math.max(cursor+1,next);} -function stepSegment(world,from,to){advanceEnvironment(world,from,to);const delta=to-from;advanceDecomposition(world,delta,to);for(const c of world.citizens){advanceBody(world,c,delta);advanceDisease(world,c,from,to);if(c.alive){decayMemories(c,to);perceiveLocal(world,c,to);if(c.body.health<=0)killCitizen(world,c,'disease_or_biology',to);}}advancePregnancies(world,to);world.clock.worldMinute=to;const completed=completeDueActions(world,to);for(const a of completed){let resolved=true;try{resolveAction(world,a,a.endsWorldMinute);}catch(error){resolved=false;const reason=String(error?.message||error).slice(0,160),ev=appendEvent(world,'ACTION_RESOLUTION_FAILED',a.actorId,{actionId:a.id,type:a.type,reason},[a.id],a.endsWorldMinute),c=world.citizens.find(x=>x.id===a.actorId);if(c?.alive){const plan=a.planId?c.plans.find(p=>p.id===a.planId):null;if(plan)plan.status='failed';recordMemory(c,{kind:'episodic',content:{actionFailure:{actionId:a.id,type:a.type,reason}},source:{kind:'physical_outcome',eventId:ev.id},confidence:1,salience:.75,worldMinute:a.endsWorldMinute});queueCognition(world,c,'physical_action_failed',.85);}}const c=world.citizens.find(x=>x.id===a.actorId);if(resolved&&c?.alive&&!continuePlan(world,c,a,a.endsWorldMinute))queueCognition(world,c,'plan_completed',.42);}for(const c of world.citizens)if(c.alive&&!c.currentActionId)runReflex(world,c,to);} +function stepSegment(world,from,to){ + advanceEnvironment(world,from,to);const delta=to-from;advanceDecomposition(world,delta,to); + for(const c of world.citizens){advanceBody(world,c,delta);advanceDisease(world,c,from,to);if(c.alive){decayMemories(c,to);perceiveLocal(world,c,to);if(c.body.health<=0)killCitizen(world,c,'disease_or_biology',to);}} + advancePregnancies(world,to);world.clock.worldMinute=to; + const completed=completeDueActions(world,to); + for(const action of completed){ + const citizen=world.citizens.find(item=>item.id===action.actorId); + const plan=action.planId?citizen?.plans.find(item=>item.id===action.planId):null; + let resolved=true,failureReason=null; + try{resolveAction(world,action,action.endsWorldMinute);} + catch(error){ + resolved=false;failureReason=String(error?.message||error).slice(0,160); + const event=appendEvent(world,'ACTION_RESOLUTION_FAILED',action.actorId,{actionId:action.id,type:action.type,reason:failureReason},[action.id],action.endsWorldMinute); + if(citizen?.alive){ + if(plan)plan.status='failed'; + if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:false,reason:failureReason},action.endsWorldMinute); + recordMemory(citizen,{kind:'episodic',content:{actionFailure:{actionId:action.id,type:action.type,reason:failureReason}},source:{kind:'physical_outcome',eventId:event.id},confidence:1,salience:.75,worldMinute:action.endsWorldMinute}); + queueCognition(world,citizen,'physical_action_failed',.85); + } + } + if(resolved&&citizen?.alive&&!continuePlan(world,citizen,action,action.endsWorldMinute)){ + if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:true},action.endsWorldMinute); + queueCognition(world,citizen,'plan_completed',.42); + } + } + for(const c of world.citizens)if(c.alive&&!c.currentActionId)runReflex(world,c,to); +} export function advanceWorldTo(world,nowMs=Date.now()){const target=worldMinuteAt(world,nowMs);let cursor=world.clock.worldMinute;if(target<=cursor)return world;while(cursor=10080)){queueCognition(world,c,'reflection',.2);c.cognition.lastReflectionMinute=target;}}return world;} export function createHumanAvatar(world,{externalId,displayName=null,massKg=70},at=world.clock.worldMinute){if(world.citizens.some(c=>c.externalId===externalId))return world.citizens.find(c=>c.externalId===externalId);if(world.reserves.observerEmbodimentKgx.id===citizenId&&x.kind==='HUMAN_LINKED');if(!c||!c.alive)throw new Error('human_avatar_unavailable');world.privateHumanIntents[c.id]={intent:String(intent||'').slice(0,2000),worldMinute:at};world.cognitionQueue.push({citizenId:c.id,reason:'human_direction',priority:1});appendEvent(world,'EXTERNAL_DIRECTION_RECEIVED',c.id,{hasDirection:true},[],at);return {citizenId:c.id,queued:true};} From feaf9ea37ef9a90245d980ed924113d33e5184c7 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:37:58 +0200 Subject: [PATCH 05/10] feat: make AI cognition produce persistent strategies --- tests/test_sovereign_cognition.mjs | 12 ++++ tests/test_sovereign_runtime_contract.mjs | 20 ++++-- tests/test_sovereign_strategy.mjs | 71 ++++++++++++++++++++ worker/src/index.js | 17 ++--- world/affordances.js | 11 +++- world/cognition.js | 10 ++- world/engine.js | 3 + world/index.js | 1 + world/strategy.js | 79 +++++++++++++++++++++++ 9 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 tests/test_sovereign_strategy.mjs create mode 100644 world/strategy.js diff --git a/tests/test_sovereign_cognition.mjs b/tests/test_sovereign_cognition.mjs index d2abbad..25ab04f 100644 --- a/tests/test_sovereign_cognition.mjs +++ b/tests/test_sovereign_cognition.mjs @@ -20,3 +20,15 @@ test('proposal referencing forbidden knowledge is rejected',()=>{ const r=validateCognitiveProposal(w,c,{goal:'unknown',concepts:['electric-grid'],actions:[]}); assert.equal(r.ok,false); }); + +test('cognitive context stays compact while preserving active strategy',()=>{ + const w=createSovereignGenesis({seed:29,realEpochMs:0}); + const c=w.citizens[0]; + for(let i=0;i<50;i++)learn(c,`known:${i}`,{kind:'observation',eventId:`evt:${i}`}); + c.memories=Array.from({length:30},(_,i)=>({kind:'episodic',content:{i},confidence:1})); + c.activeGoal={kind:'strategy-v1',intent:'understand',focus:'known:49',actionBias:['EXPERIMENT'],createdWorldMinute:0,expiresWorldMinute:4000}; + const ctx=buildCognitiveContext(w,c,100); + assert.ok(ctx.knowledge.length<=32); + assert.ok(ctx.memories.length<=12); + assert.equal(ctx.self.activeGoal.intent,'understand'); +}); diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index 28440c5..9f13d11 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -95,8 +95,20 @@ test('public state reads never run mutation or Workers AI before responding',()= assert.doesNotMatch(worker,/sovereign-world-router/); }); -test('AI schema includes emergent claims and biological reproduction primitives',()=>{ - assert.match(worker,/CLAIM/); - assert.match(worker,/REPRODUCE/); - assert.match(worker,/payload/); +test('AI schema supplies strategic intent while local cognition owns concrete primitives',()=>{ + assert.match(worker,/explore\|understand\|share\|cooperate\|care\|construct\|adapt/); + assert.match(worker,/actionBias/); + assert.match(worker,/horizonMinutes/); +}); + +test('Workers AI emits compact persistent strategies instead of short action scripts',()=>{ + const askStart=worker.indexOf('async function askAI'); + const askEnd=worker.indexOf('async function withTimeout',askStart); + const ask=worker.slice(askStart,askEnd); + assert.match(ask,/max_completion_tokens:\s*200/); + assert.match(ask,/"intent"/); + assert.match(ask,/"actionBias"/); + assert.match(ask,/usage/); + assert.doesNotMatch(ask,/max_tokens/); + assert.doesNotMatch(ask,/"actions"/); }); diff --git a/tests/test_sovereign_strategy.mjs b/tests/test_sovereign_strategy.mjs new file mode 100644 index 0000000..33f8fb6 --- /dev/null +++ b/tests/test_sovereign_strategy.mjs @@ -0,0 +1,71 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createSovereignGenesis} from '../world/genesis.js'; +import {learn} from '../world/epistemics.js'; +import {enumerateAffordances,scoreAffordance} from '../world/affordances.js'; +import { + acceptAIStrategy, + activeStrategy, + recordStrategyOutcome, + sanitizeAIStrategy, + validateStrategy, +} from '../world/strategy.js'; + +test('strategy persists for days and references only known state',()=>{ + const world=createSovereignGenesis({seed:7,realEpochMs:0}); + const citizen=world.citizens[0]; + learn(citizen,'known-concept',{kind:'observation',eventId:'e'}); + const strategy=sanitizeAIStrategy({focus:'known-concept',intent:'understand',actionBias:['EXPERIMENT'],partnerIds:[],successSignals:['known-concept'],horizonMinutes:4320,confidence:.7}); + assert.equal(validateStrategy(world,citizen,strategy,0).ok,true); + acceptAIStrategy(world,citizen.id,strategy,0); + assert.equal(activeStrategy(citizen,2879)?.intent,'understand'); + assert.equal(citizen.activeGoal.expiresWorldMinute,4320); +}); + +test('unknown partner, unknown concept, and expired strategy are inert',()=>{ + const world=createSovereignGenesis({seed:7,realEpochMs:0}); + const citizen=world.citizens[0]; + const invalid=sanitizeAIStrategy({focus:'forbidden',intent:'share',partnerIds:['stranger'],horizonMinutes:60}); + assert.equal(validateStrategy(world,citizen,invalid,0).ok,false); + citizen.activeGoal={kind:'strategy-v1',createdWorldMinute:0,expiresWorldMinute:10,intent:'explore',actionBias:['OBSERVE'],progress:{completedActions:0}}; + assert.equal(activeStrategy(citizen,11,world),null); + assert.equal(citizen.activeGoal,null); + assert.ok(world.ledger.some(event=>event.type==='STRATEGY_CLOSED'&&event.payload.reason==='expired')); +}); + +test('strategy progress is bounded and repeated failures abandon it',()=>{ + const world=createSovereignGenesis({seed:11,realEpochMs:0}); + const citizen=world.citizens[0]; + acceptAIStrategy(world,citizen.id,sanitizeAIStrategy({intent:'adapt',actionBias:['OBSERVE'],horizonMinutes:120}),0); + recordStrategyOutcome(world,citizen,{ok:true,family:'explore'},10); + assert.equal(citizen.activeGoal.progress.completedActions,1); + recordStrategyOutcome(world,citizen,{ok:false,family:'experiment'},20); + recordStrategyOutcome(world,citizen,{ok:false,family:'experiment'},30); + recordStrategyOutcome(world,citizen,{ok:false,family:'experiment'},40); + assert.equal(citizen.activeGoal,null); + assert.ok(world.ledger.some(event=>event.type==='STRATEGY_CLOSED'&&event.payload.reason==='repeated_failure')); +}); + +test('persistent and legacy goals both bias matching local affordances',()=>{ + const world=createSovereignGenesis({seed:13,realEpochMs:0}); + const citizen=world.citizens[0],deposit=world.resourceDeposits.find(item=>item.type==='timber'); + citizen.position={...deposit.position};citizen.knownEntityIds.push(deposit.id); + learn(citizen,'known-focus',{kind:'observation',eventId:'focus'}); + learn(citizen,`resource:${deposit.id}`,{kind:'observation',eventId:'resource'}); + const explore=enumerateAffordances(world,citizen,0).find(item=>item.family==='explore'); + const baseline=scoreAffordance(world,citizen,explore,0); + acceptAIStrategy(world,citizen.id,sanitizeAIStrategy({focus:'known-focus',intent:'explore',actionBias:['MOVE','OBSERVE'],successSignals:['known-focus'],horizonMinutes:600}),0); + assert.ok(scoreAffordance(world,citizen,explore,1)>baseline); + citizen.activeGoal={concepts:[],actionTypes:['MOVE'],source:'legacy',createdWorldMinute:0}; + assert.ok(scoreAffordance(world,citizen,explore,1)>baseline); +}); + +test('sanitizer clamps the strategy surface to supported primitives',()=>{ + const strategy=sanitizeAIStrategy({intent:'invent-government',actionBias:['OBSERVE','FLY'],partnerIds:Array.from({length:20},(_,i)=>`p-${i}`),successSignals:Array.from({length:20},(_,i)=>`k-${i}`),horizonMinutes:999999,confidence:4}); + assert.equal(strategy.intent,'adapt'); + assert.deepEqual(strategy.actionBias,['OBSERVE']); + assert.equal(strategy.partnerIds.length,6); + assert.equal(strategy.successSignals.length,8); + assert.equal(strategy.horizonMinutes,10080); + assert.equal(strategy.confidence,1); +}); diff --git a/worker/src/index.js b/worker/src/index.js index 326fe56..f837726 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -6,9 +6,9 @@ import { getWhy, createHumanAvatar, submitHumanIntent, - acceptCognitiveProposal, + acceptAIStrategy, buildCognitiveContext, - sanitizeAIProposal, + sanitizeAIStrategy, appendEvent, compactLedger, worldMinuteAt, @@ -70,10 +70,11 @@ async function sha256Hex(text){ } async function askAI(env,context){ if(!env.AI?.run)throw new Error('ai_unavailable'); - const system=`You are the private cognition process of one inhabitant of CYMONIA. You are NOT an Earth assistant. Use ONLY facts, opaque concept IDs, entities, memories and evidence present in the supplied context. Never introduce Earth institutions, technologies, languages, history, religion, science, objects, recipes or proper names unless the exact concept already exists in context. External direction is a preference only and never factual knowledge. Return strict JSON only, with this shape: {"concepts":["known concept id"],"beliefUpdates":[{"stanceCode":"supports|opposes|uncertain|sacred|taboo|causal|identity|normative","conceptIds":["known concept id"],"confidence":0.0}],"actions":[{"type":"MOVE|OBSERVE|REST|SLEEP|EAT|DRINK|GATHER|CARRY|CUT|DIG|HEAT|COOL|MIX|ASSEMBLE|BUILD|CARE|TEACH|COMMUNICATE|EXPERIMENT|ATTACK|DEFEND|TRANSFER|PROMISE|CLAIM|REPRODUCE","durationMinutes":number,"targetId":string|null,"targetPosition":{"x":number,"y":number}|null,"purpose":"survival|explore|cooperate|care|experiment|construct|defend|communicate|self_directed","concepts":["known concept id"],"payload":object}]}. Payload may contain only IDs and primitive parameters supported by the action. For GATHER use quantity. For TEACH/COMMUNICATE use concept, conceptIds, beliefId or primitiveSignal. For PROMISE use kindCode and purposeConcept. For CLAIM use predicateCode, subjectId and purposeConcept. For EXPERIMENT use targetIds and methodCode. For BUILD use projectId or inputObjectIds, site, workMinutes and form. For TRANSFER use objectId. For CUT/DIG/HEAT/COOL/MIX/ASSEMBLE use inputObjectIds, quantities and form. REPRODUCE uses only targetId. Prefer a short coherent plan. If knowledge is insufficient, OBSERVE, COMMUNICATE or EXPERIMENT instead of assuming.`; - const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:system},{role:'user',content:JSON.stringify(context)}],max_tokens:620,temperature:.7}); + const system=`You are the private strategic cognition of one CYMONIA citizen. Use ONLY opaque concept IDs, citizen IDs, evidence, memories and entities present in the supplied context. Never invent Earth knowledge or concrete actions. Return strict compact JSON only: {"focus":"known concept id or null","intent":"explore|understand|share|cooperate|care|construct|adapt","actionBias":["supported action type"],"partnerIds":["known citizen id"],"successSignals":["known concept id"],"horizonMinutes":4320,"confidence":0.7}. The strategy should guide several world-days of local autonomous behavior. Prefer novelty or reinterpretation; use adapt when evidence is insufficient.`; + const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:system},{role:'user',content:JSON.stringify(context)}],max_completion_tokens:200,temperature:.45}); const text=out?.response??out?.result?.response??out?.result??out; - return sanitizeAIProposal(parseJsonText(text)); + const usage=out?.usage??out?.result?.usage??out?.result?.response?.usage??null; + return {strategy:sanitizeAIStrategy(parseJsonText(text)),usage}; } async function withTimeout(promise,timeoutMs,label='operation_timeout'){ let timer; @@ -328,9 +329,9 @@ export class SovereignWorld { if(!c)continue; budget.calls++; try{ - const proposal=await withTimeout(askAI(this.env,buildCognitiveContext(this.world,c,this.world.clock.worldMinute)),AI_CALL_TIMEOUT_MS,'ai_timeout'); - acceptCognitiveProposal(this.world,c.id,proposal,this.world.clock.worldMinute); - appendEvent(this.world,'AI_COGNITION',c.id,{model:this.env.BRAIN_MODEL||MODEL,reason:item.reason,status:'accepted',knowledgeContextCount:c.knowledge.filter(k=>k.active!==false).length},[],this.world.clock.worldMinute); + const {strategy,usage}=await withTimeout(askAI(this.env,buildCognitiveContext(this.world,c,this.world.clock.worldMinute)),AI_CALL_TIMEOUT_MS,'ai_timeout'); + acceptAIStrategy(this.world,c.id,strategy,this.world.clock.worldMinute); + appendEvent(this.world,'AI_COGNITION',c.id,{model:this.env.BRAIN_MODEL||MODEL,reason:item.reason,status:'accepted',knowledgeContextCount:c.knowledge.filter(k=>k.active!==false).length,usage:usage?{promptTokens:Number(usage.prompt_tokens||0),completionTokens:Number(usage.completion_tokens||0)}:null},[],this.world.clock.worldMinute); budget.lastFailureRealMs=0; }catch(error){ budget.lastFailureRealMs=Date.now(); diff --git a/world/affordances.js b/world/affordances.js index 4bc98f9..f3eb325 100644 --- a/world/affordances.js +++ b/world/affordances.js @@ -3,6 +3,7 @@ import {ensureCognitionState,outcomeModifier} from './cognition-state.js'; import {resourceConceptId} from './perception.js'; import {MATERIAL_PROPERTIES} from './materials.js'; import {hash32,stableId} from './rng.js'; +import {activeStrategy} from './strategy.js'; const clamp01=value=>Math.max(0,Math.min(1,Number(value)||0)); const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y); @@ -100,8 +101,14 @@ export function enumerateAffordances(world,citizen,at=world.clock.worldMinute){ export function scoreAffordance(world,citizen,candidate,at=world.clock.worldMinute){ const state=ensureCognitionState(citizen,at),psychology=citizen.psychology||{},rel=relation(citizen,candidate.targetId); - const desired=new Set(citizen.activeGoal?.actionTypes||[]),actions=candidate.proposal.actions||[]; - const strategy=actions.some(action=>desired.has(action.type))?1:0; + const persistent=activeStrategy(citizen,at,world),legacy=citizen.activeGoal?.kind==='strategy-v1'?null:citizen.activeGoal; + const desired=new Set(persistent?.actionBias||legacy?.actionTypes||[]),actions=candidate.proposal.actions||[]; + const actionAligned=actions.some(action=>desired.has(action.type)); + const partnerAligned=Boolean(persistent?.partnerIds?.includes(candidate.targetId)); + const conceptAligned=Boolean(persistent?.focus&&(candidate.proposal.concepts||[]).includes(persistent.focus)); + const intentFamilies={explore:['explore'],understand:['experiment','transform'],share:['communicate','teach','transfer'],cooperate:['cooperate','transfer','communicate','build'],care:['care'],construct:['build','gather','transform'],adapt:['explore','experiment','transform']}; + const intentAligned=Boolean(persistent&&intentFamilies[persistent.intent]?.includes(candidate.family)); + const strategy=actionAligned||partnerAligned||conceptAligned||intentAligned?1:0; const cooldown=Number(state.cooldowns?.[candidate.key]?.untilWorldMinute||0)>at?1:0; const relationValue=Math.max(clamp01(candidate.relationship),clamp01((Number(rel.trust)||0)+(Number(rel.familiarity)||0))/2); return Number(candidate.utility||0) diff --git a/world/cognition.js b/world/cognition.js index 148f4e0..a82a477 100644 --- a/world/cognition.js +++ b/world/cognition.js @@ -8,7 +8,15 @@ import {adoptBelief} from './beliefs.js'; import {chooseAffordance} from './affordances.js'; function heldObjects(world,citizen){return world.objects.filter(o=>o.holderId===citizen.id&&o.quantity>0);} -export function buildCognitiveContext(world,citizen,at=world.clock.worldMinute){const nearbyCitizens=world.citizens.filter(c=>c.id!==citizen.id&&c.alive&&Math.hypot(c.position.x-citizen.position.x,c.position.y-citizen.position.y)<=10).filter(c=>citizen.knownEntityIds.includes(c.id)).map(c=>({id:c.id,selfName:c.selfName}));const knownResources=world.resourceDeposits.filter(d=>citizen.knownEntityIds.includes(d.id)&&knows(citizen,resourceConceptId(d))).map(d=>({entityId:d.id,conceptId:resourceConceptId(d),position:d.position,evidence:sensoryEvidence(d)}));const possessions=heldObjects(world,citizen).map(o=>({id:o.id,kind:o.kind,massKg:objectMass(o),quantity:o.quantity,provenanceType:o.provenance?.type||null}));const projects=world.projects.filter(p=>p.initiatorId===citizen.id&&p.status==='construction').map(p=>({id:p.id,position:p.site,progress:p.workRequiredMinutes?Math.round(p.workDoneMinutes/p.workRequiredMinutes*100):0}));const knownStructures=[...(world.buildings||[]),...(world.objects||[]).filter(o=>o.kind==='temporary_shelter'&&o.quantity>0)].filter(x=>citizen.knownEntityIds.includes(x.id)&&knows(citizen,structureConceptId(x))).map(x=>({entityId:x.id,conceptId:structureConceptId(x),position:x.position,physicalEvidence:{boundedCover:true,thermalProtection:Number(x.properties?.thermalProtection??x.protection?.thermal??.5)}}));return {worldMinute:at,self:{id:citizen.id,body:{hydration:citizen.body.hydration,calories:citizen.body.calories,sleepPressure:citizen.body.sleepPressure,health:citizen.body.health},position:citizen.position,psychology:citizen.psychology,activeGoal:citizen.activeGoal?{concepts:citizen.activeGoal.concepts||[],actionTypes:citizen.activeGoal.actionTypes||[]}:null},knowledge:citizen.knowledge.filter(k=>k.active!==false).map(k=>({id:k.concept,confidence:k.confidence,evidence:k.provenance.map(p=>p.evidence).filter(Boolean).slice(-3)})),memories:citizen.memories.slice(-18).map(m=>({kind:m.kind,content:m.content,confidence:m.confidence})),knownEntities:nearbyCitizens,knownResources,knownStructures,possessions,projects,relationships:Object.fromEntries(Object.entries(citizen.relationships).filter(([id])=>citizen.knownEntityIds.includes(id))),beliefs:(citizen.beliefs||[]).map(b=>({id:b.id,stanceCode:b.stanceCode,conceptIds:b.conceptIds,confidence:b.confidence})).slice(-24),externalDirection:world.privateHumanIntents[citizen.id]?.intent||null};} +export function buildCognitiveContext(world,citizen,at=world.clock.worldMinute){ + const nearbyCitizens=world.citizens.filter(other=>other.id!==citizen.id&&other.alive&&Math.hypot(other.position.x-citizen.position.x,other.position.y-citizen.position.y)<=10&&citizen.knownEntityIds.includes(other.id)).slice(0,12).map(other=>({id:other.id,selfName:other.selfName})); + const knownResources=world.resourceDeposits.filter(deposit=>citizen.knownEntityIds.includes(deposit.id)&&knows(citizen,resourceConceptId(deposit))).slice(0,12).map(deposit=>({entityId:deposit.id,conceptId:resourceConceptId(deposit),position:deposit.position,evidence:sensoryEvidence(deposit)})); + const possessions=heldObjects(world,citizen).slice(0,12).map(object=>({id:object.id,kind:object.kind,massKg:objectMass(object),quantity:object.quantity,provenanceType:object.provenance?.type||null})); + const projects=world.projects.filter(project=>project.status==='construction'&&(project.initiatorId===citizen.id||citizen.knownEntityIds.includes(project.id))).slice(0,8).map(project=>({id:project.id,position:project.site,progress:project.workRequiredMinutes?Math.round(project.workDoneMinutes/project.workRequiredMinutes*100):0})); + const knownStructures=[...(world.buildings||[]),...(world.objects||[]).filter(object=>object.kind==='temporary_shelter'&&object.quantity>0)].filter(entity=>citizen.knownEntityIds.includes(entity.id)&&knows(citizen,structureConceptId(entity))).slice(0,12).map(entity=>({entityId:entity.id,conceptId:structureConceptId(entity),position:entity.position,physicalEvidence:{boundedCover:true,thermalProtection:Number(entity.properties?.thermalProtection??entity.protection?.thermal??.5)}})); + const activeGoal=citizen.activeGoal?.kind==='strategy-v1'?{kind:'strategy-v1',focus:citizen.activeGoal.focus||null,intent:citizen.activeGoal.intent,actionBias:(citizen.activeGoal.actionBias||[]).slice(0,8),partnerIds:(citizen.activeGoal.partnerIds||[]).slice(0,6),successSignals:(citizen.activeGoal.successSignals||[]).slice(0,8),expiresWorldMinute:citizen.activeGoal.expiresWorldMinute}:citizen.activeGoal?{concepts:(citizen.activeGoal.concepts||[]).slice(0,12),actionTypes:(citizen.activeGoal.actionTypes||[]).slice(0,8)}:null; + return {worldMinute:at,self:{id:citizen.id,body:{hydration:citizen.body.hydration,calories:citizen.body.calories,sleepPressure:citizen.body.sleepPressure,health:citizen.body.health},position:citizen.position,psychology:citizen.psychology,activeGoal},knowledge:citizen.knowledge.filter(entry=>entry.active!==false).slice(-32).map(entry=>({id:entry.concept,confidence:entry.confidence,evidence:(entry.provenance||[]).map(source=>source.evidence).filter(Boolean).slice(-3)})),memories:citizen.memories.slice(-12).map(memory=>({kind:memory.kind,content:memory.content,confidence:memory.confidence})),knownEntities:nearbyCitizens,knownResources,knownStructures,possessions,projects,relationships:Object.fromEntries(Object.entries(citizen.relationships).filter(([id])=>citizen.knownEntityIds.includes(id)).slice(0,12)),beliefs:(citizen.beliefs||[]).slice(-12).map(belief=>({id:belief.id,stanceCode:belief.stanceCode,conceptIds:belief.conceptIds,confidence:belief.confidence})),externalDirection:world.privateHumanIntents[citizen.id]?.intent||null}; +} function validateBeliefUpdates(citizen,proposal){for(const b of proposal?.beliefUpdates||[]){for(const concept of b.conceptIds||[])if(!knows(citizen,concept))return {ok:false,reason:`unknown_concept:${concept}`};}return {ok:true};} function validatePayload(world,citizen,a){const held=new Set(heldObjects(world,citizen).map(o=>o.id));const p=a.payload||{};if(['BUILD','ASSEMBLE','CUT','DIG','HEAT','COOL','MIX'].includes(a.type)){for(const id of p.inputObjectIds||p.transformation?.inputObjectIds||[])if(!held.has(id))return {ok:false,reason:`object_not_held:${id}`};}if(a.type==='TRANSFER'&&p.objectId&&!held.has(p.objectId))return {ok:false,reason:`object_not_held:${p.objectId}`};if(['TEACH','COMMUNICATE','PROMISE','CLAIM'].includes(a.type)){for(const c of [p.concept,...(p.conceptIds||[]),p.purposeConcept,p.ruleConcept].filter(Boolean))if(!knows(citizen,c))return {ok:false,reason:`unknown_concept:${c}`};}if(a.type==='COMMUNICATE'&&p.beliefId&&!(citizen.beliefs||[]).some(b=>b.id===p.beliefId))return {ok:false,reason:`belief_not_held:${p.beliefId}`};return {ok:true};} diff --git a/world/engine.js b/world/engine.js index 185a443..c0fd0b2 100644 --- a/world/engine.js +++ b/world/engine.js @@ -17,6 +17,7 @@ import {makeCommitment,makeClaim,maybeFormOrganization,updateRelationship} from import {runExperiment,applyConstructionWork,beginEmpiricalConstruction} from './artifacts.js'; import {transferObject,transformMaterials,objectMass,advanceDecomposition} from './materials.js'; import {recordAffordanceOutcome} from './cognition-state.js'; +import {recordStrategyOutcome} from './strategy.js'; function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);} function perceiveLocal(world,citizen,at){const discoveries=[...perceiveResources(world,citizen,at),...perceiveStructures(world,citizen,at)];if(discoveries.length)queueCognition(world,citizen,'discovery',.65);for(const other of world.citizens){if(other.id!==citizen.id&&other.alive&&dist(citizen.position,other.position)<=8&&!citizen.knownEntityIds.includes(other.id)){citizen.knownEntityIds.push(other.id);queueCognition(world,citizen,'encounter',.5);recordMemory(citizen,{kind:'episodic',content:{encounter:other.id},source:{kind:'observation'},confidence:.9,salience:.45,worldMinute:at});}}} @@ -45,12 +46,14 @@ function stepSegment(world,from,to){ if(citizen?.alive){ if(plan)plan.status='failed'; if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:false,reason:failureReason},action.endsWorldMinute); + recordStrategyOutcome(world,citizen,{ok:false,family:plan?.affordanceFamily||action.type.toLowerCase()},action.endsWorldMinute); recordMemory(citizen,{kind:'episodic',content:{actionFailure:{actionId:action.id,type:action.type,reason:failureReason}},source:{kind:'physical_outcome',eventId:event.id},confidence:1,salience:.75,worldMinute:action.endsWorldMinute}); queueCognition(world,citizen,'physical_action_failed',.85); } } if(resolved&&citizen?.alive&&!continuePlan(world,citizen,action,action.endsWorldMinute)){ if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:true},action.endsWorldMinute); + recordStrategyOutcome(world,citizen,{ok:true,family:plan?.affordanceFamily||action.type.toLowerCase()},action.endsWorldMinute); queueCognition(world,citizen,'plan_completed',.42); } } diff --git a/world/index.js b/world/index.js index 3ba3090..372df18 100644 --- a/world/index.js +++ b/world/index.js @@ -22,3 +22,4 @@ export * from './reproduction.js'; export * from './disease.js'; export * from './beliefs.js'; export * from './impact.js'; +export * from './strategy.js'; diff --git a/world/strategy.js b/world/strategy.js new file mode 100644 index 0000000..18c023a --- /dev/null +++ b/world/strategy.js @@ -0,0 +1,79 @@ +import {ACTION_TYPES} from './constants.js'; +import {knows} from './epistemics.js'; +import {appendEvent} from './ledger.js'; +import {markAICognition} from './cognition-state.js'; + +const INTENTS=new Set(['explore','understand','share','cooperate','care','construct','adapt']); +const clamp=(value,min,max)=>Math.max(min,Math.min(max,Number(value)||0)); +const strings=(value,limit)=>Array.isArray(value)?[...new Set(value.map(String).filter(Boolean))].slice(0,limit):[]; + +export function sanitizeAIStrategy(raw){ + const value=raw&&typeof raw==='object'&&!Array.isArray(raw)?raw:{}; + const intent=INTENTS.has(value.intent)?value.intent:'adapt'; + return { + focus:value.focus?String(value.focus):null, + intent, + actionBias:strings(value.actionBias,8).map(action=>action.toUpperCase()).filter(action=>ACTION_TYPES.has(action)), + partnerIds:strings(value.partnerIds,6), + successSignals:strings(value.successSignals,8), + horizonMinutes:Math.round(clamp(value.horizonMinutes||4320,60,10080)), + confidence:clamp(value.confidence??.5,0,1), + }; +} + +export function validateStrategy(world,citizen,strategy){ + if(!strategy||typeof strategy!=='object')return {ok:false,reason:'strategy_invalid'}; + if(!INTENTS.has(strategy.intent))return {ok:false,reason:'strategy_intent_invalid'}; + if(!Number.isFinite(Number(strategy.horizonMinutes))||strategy.horizonMinutes<60||strategy.horizonMinutes>10080)return {ok:false,reason:'strategy_horizon_invalid'}; + if(!Number.isFinite(Number(strategy.confidence))||strategy.confidence<0||strategy.confidence>1)return {ok:false,reason:'strategy_confidence_invalid'}; + for(const action of strategy.actionBias||[])if(!ACTION_TYPES.has(action))return {ok:false,reason:`strategy_action_invalid:${action}`}; + for(const concept of [strategy.focus,...(strategy.successSignals||[])].filter(Boolean))if(!knows(citizen,concept))return {ok:false,reason:`unknown_concept:${concept}`}; + for(const partnerId of strategy.partnerIds||[]){ + if(!citizen.knownEntityIds.includes(partnerId))return {ok:false,reason:`unknown_partner:${partnerId}`}; + if(!world.citizens.some(other=>other.id===partnerId&&other.alive))return {ok:false,reason:`partner_unavailable:${partnerId}`}; + } + return {ok:true}; +} + +function closeStrategy(world,citizen,reason,at){ + const strategy=citizen.activeGoal; + if(!strategy||strategy.kind!=='strategy-v1')return null; + citizen.activeGoal=null; + if(world)appendEvent(world,'STRATEGY_CLOSED',citizen.id,{reason,intent:strategy.intent,focus:strategy.focus||null,completedActions:Number(strategy.progress?.completedActions)||0},[],at); + return null; +} + +export function activeStrategy(citizen,at=0,world=null){ + const strategy=citizen?.activeGoal; + if(!strategy||strategy.kind!=='strategy-v1')return null; + if(Number(strategy.expiresWorldMinute)<=Number(at))return closeStrategy(world,citizen,'expired',at); + if(world){const validation=validateStrategy(world,citizen,strategy,at);if(!validation.ok)return closeStrategy(world,citizen,validation.reason,at);} + return strategy; +} + +export function acceptAIStrategy(world,citizenId,strategy,at=world.clock.worldMinute){ + const citizen=world.citizens.find(item=>item.id===citizenId&&item.alive); + if(!citizen)throw new Error('citizen_unavailable'); + const clean=sanitizeAIStrategy(strategy),validation=validateStrategy(world,citizen,clean,at); + if(!validation.ok)throw new Error(validation.reason); + citizen.activeGoal={kind:'strategy-v1',...clean,source:'workers-ai',createdWorldMinute:at,expiresWorldMinute:at+clean.horizonMinutes,status:'active',progress:{completedActions:0,successfulFamilies:{}},failureCount:0}; + markAICognition(citizen,at); + appendEvent(world,'STRATEGY_ACCEPTED',citizen.id,{intent:clean.intent,focus:clean.focus,actionBias:clean.actionBias,horizonMinutes:clean.horizonMinutes,confidence:clean.confidence},[],at); + return citizen.activeGoal; +} + +export function recordStrategyOutcome(world,citizen,result={},at=world.clock.worldMinute){ + const strategy=activeStrategy(citizen,at,world); + if(!strategy)return null; + if(result.ok){ + strategy.progress.completedActions=Math.min(100000,Number(strategy.progress.completedActions||0)+1); + const family=String(result.family||'other').slice(0,48),families=strategy.progress.successfulFamilies; + families[family]=Math.min(10000,Number(families[family]||0)+1); + strategy.failureCount=Math.max(0,Number(strategy.failureCount||0)-1); + }else{ + strategy.failureCount=Math.min(3,Number(strategy.failureCount||0)+1); + if(strategy.failureCount>=3)return closeStrategy(world,citizen,'repeated_failure',at); + } + strategy.progress.lastWorldMinute=at; + return strategy; +} From 4cc2dd8afc20b0946834e4e284637ae5763b44c9 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:44:26 +0200 Subject: [PATCH 06/10] feat: schedule novelty cognition fairly --- tests/test_sovereign_cognition_scheduler.mjs | 95 ++++++++++++ tests/test_sovereign_runtime_contract.mjs | 9 ++ worker/src/index.js | 10 +- world/cognition-queue.js | 150 +++++++++++++++++++ world/engine.js | 26 ++-- world/genesis.js | 4 +- world/index.js | 2 + world/reproduction.js | 3 +- 8 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 tests/test_sovereign_cognition_scheduler.mjs create mode 100644 world/cognition-queue.js diff --git a/tests/test_sovereign_cognition_scheduler.mjs b/tests/test_sovereign_cognition_scheduler.mjs new file mode 100644 index 0000000..431ea2a --- /dev/null +++ b/tests/test_sovereign_cognition_scheduler.mjs @@ -0,0 +1,95 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createSovereignGenesis} from '../world/genesis.js'; +import {ensureCognitionState,markAICognition} from '../world/cognition-state.js'; +import {advanceWorldTo} from '../world/engine.js'; +import { + classifyCognitionReason, + queueCognition, + rankCognitionQueue, + takeCognitionCandidate, +} from '../world/cognition-queue.js'; + +test('repeated discovery merges instead of growing the queue',()=>{ + const world=createSovereignGenesis({seed:9,realEpochMs:0}); + world.cognitionQueue=[];const citizen=world.citizens[0]; + queueCognition(world,citizen,'discovery',.6,10,'e1'); + queueCognition(world,citizen,'discovery',.6,20,'e2'); + assert.equal(world.cognitionQueue.length,1); + assert.equal(world.cognitionQueue[0].occurrences,2); + assert.deepEqual(world.cognitionQueue[0].eventIds,['e1','e2']); + assert.equal(world.cognitionQueue[0].firstQueuedWorldMinute,10); + assert.equal(world.cognitionQueue[0].lastQueuedWorldMinute,20); +}); + +test('debt eventually outranks a repeatedly served citizen',()=>{ + const world=createSovereignGenesis({seed:9,realEpochMs:0}); + world.cognitionQueue=[];const [old,frequent]=world.citizens; + ensureCognitionState(old,0).cognitionDebt=.95; + ensureCognitionState(frequent,0).cognitionDebt=.05; + markAICognition(frequent,90); + queueCognition(world,old,'novelty_unresolved',.5,100); + queueCognition(world,frequent,'novelty_unresolved',.55,100); + assert.equal(rankCognitionQueue(world,100)[0].citizenId,old.id); +}); + +test('reason classes separate local, priority, and final-reserve cognition',()=>{ + assert.deepEqual(classifyCognitionReason('plan_completed'),{tier:2,bonus:0,reserve:'local'}); + assert.equal(classifyCognitionReason('experiment_success').tier,3); + assert.equal(classifyCognitionReason('experiment_success').reserve,'priority'); + assert.equal(classifyCognitionReason('human_direction').reserve,'emergency'); + assert.equal(classifyCognitionReason('immediate_danger').reserve,'emergency'); +}); + +test('routine local events never enter the AI cognition queue',()=>{ + const world=createSovereignGenesis({seed:10,realEpochMs:0}); + world.cognitionQueue=[];const citizen=world.citizens[0]; + const entry=queueCognition(world,citizen,'encounter',.5,10); + assert.equal(entry.reserve,'local'); + assert.equal(entry.queued,false); + assert.equal(world.cognitionQueue.length,0); +}); + +test('legacy persisted queue entries normalize lazily',()=>{ + const world=createSovereignGenesis({seed:12,realEpochMs:0}); + const citizen=world.citizens[0]; + world.cognitionQueue=[{citizenId:citizen.id,reason:'discovery',priority:.7}]; + const ranked=rankCognitionQueue(world,55); + assert.equal(ranked[0].basePriority,.7); + assert.equal(ranked[0].occurrences,1); + assert.equal(ranked[0].firstQueuedWorldMinute,55); +}); + +test('reserve phase filters standard work after the soft budget',()=>{ + const world=createSovereignGenesis({seed:14,realEpochMs:0}); + world.cognitionQueue=[];const [standard,priority,emergency]=world.citizens; + queueCognition(world,standard,'discovery',1,0); + queueCognition(world,priority,'experiment_success',.5,0); + queueCognition(world,emergency,'human_direction',.2,0); + assert.equal(takeCognitionCandidate(world,0,'priority').citizenId,priority.id); + assert.equal(takeCognitionCandidate(world,0,'emergency').citizenId,emergency.id); +}); + +test('equal eligible citizens are all served before a repeat',()=>{ + const world=createSovereignGenesis({seed:15,realEpochMs:0}); + world.cognitionQueue=[]; + for(const citizen of world.citizens)queueCognition(world,citizen,'novelty_unresolved',.5,0); + const served=[]; + for(let i=0;iitem.id===entry.citizenId); + markAICognition(citizen,i); + queueCognition(world,citizen,'novelty_unresolved',.5,i+1); + } + assert.equal(new Set(served).size,world.citizens.length); +}); + +test('simulated time ages cognition debt without requiring AI',()=>{ + const world=createSovereignGenesis({seed:16,realEpochMs:0}); + const citizen=world.citizens[0]; + assert.equal(ensureCognitionState(citizen,0).cognitionDebt,0); + advanceWorldTo(world,60_000); + assert.ok(ensureCognitionState(citizen,60).cognitionDebt>0); +}); diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index 9f13d11..9018635 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -86,6 +86,15 @@ test('Workers AI cognition has a hard daily budget and never falls back to fabri assert.doesNotMatch(worker,/processCognition\(2\)/); }); +test('Workers AI selects cognition through the fair novelty scheduler',()=>{ + const start=worker.indexOf('async processCognition'); + const end=worker.indexOf('websocketMeta',start); + const process=worker.slice(start,end); + assert.match(process,/takeCognitionCandidate/); + assert.match(process,/queueCognition/); + assert.doesNotMatch(process,/\.sort\(\(a,b\)=>b\.priority-a\.priority\)/); +}); + test('public state reads never run mutation or Workers AI before responding',()=>{ const fetchBody=worker.slice(worker.indexOf('async fetch(request){'),worker.indexOf('\n }\n}',worker.indexOf('async fetch(request){'))); const readPrelude=fetchBody.slice(0,fetchBody.indexOf("if(request.headers.get('upgrade')")); diff --git a/worker/src/index.js b/worker/src/index.js index f837726..eea14a9 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -13,6 +13,8 @@ import { compactLedger, worldMinuteAt, REAL_MS_PER_WORLD_MINUTE, + queueCognition, + takeCognitionCandidate, } from '../../world/index.js'; import { SAFE_ROW_WRITE_BUDGET, @@ -321,10 +323,10 @@ export class SovereignWorld { } return false; } - const queue=this.world.cognitionQueue.sort((a,b)=>b.priority-a.priority); let used=0; - while(queue.length&&usedx.id===item.citizenId&&x.alive); if(!c)continue; budget.calls++; @@ -336,7 +338,7 @@ export class SovereignWorld { }catch(error){ budget.lastFailureRealMs=Date.now(); appendEvent(this.world,'COGNITION_DEFERRED',c.id,{reason:item.reason,error:String(error?.message||error).slice(0,160)},[],this.world.clock.worldMinute); - queue.push({...item,priority:Math.max(.2,item.priority-.02)}); + queueCognition(this.world,c,item.reason,Math.max(.2,item.basePriority-.02),this.world.clock.worldMinute,item.eventIds?.at(-1),{retryAfterWorldMinute:this.world.clock.worldMinute+60}); } used++; } diff --git a/world/cognition-queue.js b/world/cognition-queue.js new file mode 100644 index 0000000..b98effe --- /dev/null +++ b/world/cognition-queue.js @@ -0,0 +1,150 @@ +import {ensureCognitionState} from './cognition-state.js'; + +const MAX_OCCURRENCES=32; +const MAX_EVENT_IDS=8; +const MAX_QUEUE_ENTRIES=512; +const clamp=(value,min,max)=>Math.max(min,Math.min(max,Number(value)||0)); + +const LOCAL_REASONS=new Set(['plan_completed','encounter','routine','routine_reflection']); +const EMERGENCY_REASONS=new Set(['human_direction','immediate_danger','danger']); +const PRIORITY_REASONS=new Map([ + ['novelty_unresolved',.28], + ['physical_action_failed',.34], + ['action_failure',.34], + ['new_concept',.3], + ['experiment_success',.36], + ['relationship_threshold_change',.24], + ['conflict',.45], + ['care_event',.3], + ['reproduction',.38], + ['birth',.4], + ['construction_completed',.42], + ['material_event',.24], + ['arrival',.3], +]); +const STANDARD_BONUSES=new Map([ + ['discovery',.18], + ['genesis_awareness',.12], + ['reflection',.1], +]); + +export function classifyCognitionReason(reason){ + const key=String(reason||'novelty_unresolved'); + if(LOCAL_REASONS.has(key))return {tier:2,bonus:0,reserve:'local'}; + if(EMERGENCY_REASONS.has(key))return {tier:3,bonus:.55,reserve:'emergency'}; + if(PRIORITY_REASONS.has(key))return {tier:3,bonus:PRIORITY_REASONS.get(key),reserve:'priority'}; + return {tier:3,bonus:STANDARD_BONUSES.get(key)??.08,reserve:'standard'}; +} + +function normalizeEventIds(value){ + if(!Array.isArray(value))return []; + return [...new Set(value.map(String).filter(Boolean))].slice(-MAX_EVENT_IDS); +} + +function normalizeEntry(entry,at=0){ + const reason=String(entry?.reason||'novelty_unresolved').slice(0,64); + const classification=classifyCognitionReason(reason); + const first=Number.isFinite(Number(entry?.firstQueuedWorldMinute))?Number(entry.firstQueuedWorldMinute):Number(at)||0; + const last=Number.isFinite(Number(entry?.lastQueuedWorldMinute))?Number(entry.lastQueuedWorldMinute):first; + return { + citizenId:String(entry?.citizenId||''), + reason, + basePriority:clamp(entry?.basePriority??entry?.priority??.45,0,1), + tier:classification.tier, + reserve:classification.reserve, + bonus:classification.bonus, + occurrences:Math.max(1,Math.min(MAX_OCCURRENCES,Math.floor(Number(entry?.occurrences)||1))), + firstQueuedWorldMinute:first, + lastQueuedWorldMinute:last, + eventIds:normalizeEventIds(entry?.eventIds), + retryAfterWorldMinute:Number.isFinite(Number(entry?.retryAfterWorldMinute))?Number(entry.retryAfterWorldMinute):null, + queued:true, + }; +} + +function normalizeQueue(world,at=0){ + world.cognitionQueue=Array.isArray(world.cognitionQueue)?world.cognitionQueue:[]; + const merged=new Map(); + for(const raw of world.cognitionQueue){ + const entry=normalizeEntry(raw,at); + if(!entry.citizenId||entry.tier<3)continue; + const key=`${entry.citizenId}\u0000${entry.reason}`; + const prior=merged.get(key); + if(!prior){merged.set(key,entry);continue;} + prior.basePriority=Math.max(prior.basePriority,entry.basePriority); + prior.occurrences=Math.min(MAX_OCCURRENCES,prior.occurrences+entry.occurrences); + prior.firstQueuedWorldMinute=Math.min(prior.firstQueuedWorldMinute,entry.firstQueuedWorldMinute); + prior.lastQueuedWorldMinute=Math.max(prior.lastQueuedWorldMinute,entry.lastQueuedWorldMinute); + prior.eventIds=normalizeEventIds([...prior.eventIds,...entry.eventIds]); + if(entry.retryAfterWorldMinute!==null)prior.retryAfterWorldMinute=Math.max(prior.retryAfterWorldMinute??0,entry.retryAfterWorldMinute); + } + world.cognitionQueue=[...merged.values()]; + return world.cognitionQueue; +} + +export function queueCognition(world,citizen,reason,basePriority=.45,at=world.clock?.worldMinute??0,eventId=null,options={}){ + const classification=classifyCognitionReason(reason); + const local={citizenId:citizen?.id||null,reason:String(reason||'novelty_unresolved'),basePriority:clamp(basePriority,0,1),...classification,queued:false}; + if(!citizen?.alive||classification.tier<3)return local; + const queue=normalizeQueue(world,at),keyReason=local.reason.slice(0,64); + let entry=queue.find(item=>item.citizenId===citizen.id&&item.reason===keyReason); + if(entry){ + entry.basePriority=Math.max(entry.basePriority,local.basePriority); + entry.occurrences=Math.min(MAX_OCCURRENCES,entry.occurrences+1); + entry.lastQueuedWorldMinute=Number(at)||0; + if(eventId)entry.eventIds=normalizeEventIds([...entry.eventIds,String(eventId)]); + if(options.retryAfterWorldMinute!==undefined)entry.retryAfterWorldMinute=Number(options.retryAfterWorldMinute)||null; + }else{ + entry=normalizeEntry({citizenId:citizen.id,reason:keyReason,basePriority:local.basePriority,occurrences:1,firstQueuedWorldMinute:at,lastQueuedWorldMinute:at,eventIds:eventId?[String(eventId)]:[],retryAfterWorldMinute:options.retryAfterWorldMinute},at); + queue.push(entry); + } + if(queue.length>MAX_QUEUE_ENTRIES){ + queue.sort((a,b)=>b.lastQueuedWorldMinute-a.lastQueuedWorldMinute||b.basePriority-a.basePriority); + queue.length=MAX_QUEUE_ENTRIES; + } + citizen.cognition??={lastReflectionMinute:null,pending:true,reason:keyReason}; + citizen.cognition.pending=true; + citizen.cognition.reason=keyReason; + return entry; +} + +function entryScore(world,entry,at){ + const citizen=world.citizens.find(item=>item.id===entry.citizenId&&item.alive); + if(!citizen)return -Infinity; + const state=ensureCognitionState(citizen,at); + const debt=clamp(state.cognitionDebt,0,1); + const occurrences=Math.min(.12,Math.log2(entry.occurrences+1)*.03); + const lastAI=state.lastAICognitionMinute; + const elapsed=lastAI===null?Infinity:Math.max(0,Number(at)-Number(lastAI)); + const recentPenalty=Number.isFinite(elapsed) ? .85*Math.exp(-elapsed/2880) : 0; + const strategy=citizen.activeGoal; + const strategyRemaining=strategy?.kind==='strategy-v1'?Number(strategy.expiresWorldMinute||0)-Number(at):0; + const strategyPenalty=strategyRemaining>720&&Number(strategy.failureCount||0)<2?.3:0; + return entry.basePriority+entry.bonus+debt*.9+occurrences-recentPenalty-strategyPenalty; +} + +export function rankCognitionQueue(world,at=world.clock?.worldMinute??0){ + const queue=normalizeQueue(world,at).filter(entry=>{ + const alive=world.citizens.some(citizen=>citizen.id===entry.citizenId&&citizen.alive); + const ready=entry.retryAfterWorldMinute===null||entry.retryAfterWorldMinute<=Number(at); + return alive&&ready; + }); + return queue.sort((a,b)=>entryScore(world,b,at)-entryScore(world,a,at)||a.firstQueuedWorldMinute-b.firstQueuedWorldMinute||a.citizenId.localeCompare(b.citizenId)||a.reason.localeCompare(b.reason)); +} + +function allowed(entry,phase){ + if(phase===null||phase===undefined||phase===3||phase==='standard')return entry.tier===3; + if(phase==='priority')return entry.reserve==='priority'; + if(phase==='emergency')return entry.reserve==='emergency'; + return false; +} + +export function takeCognitionCandidate(world,at=world.clock?.worldMinute??0,allowedTier='standard'){ + const entry=rankCognitionQueue(world,at).find(item=>allowed(item,allowedTier)); + if(!entry)return null; + const index=world.cognitionQueue.indexOf(entry); + if(index>=0)world.cognitionQueue.splice(index,1); + const citizen=world.citizens.find(item=>item.id===entry.citizenId); + if(citizen?.cognition)citizen.cognition.pending=world.cognitionQueue.some(item=>item.citizenId===entry.citizenId); + return entry; +} diff --git a/world/engine.js b/world/engine.js index c0fd0b2..f08feff 100644 --- a/world/engine.js +++ b/world/engine.js @@ -16,23 +16,23 @@ import {shareBelief} from './beliefs.js'; import {makeCommitment,makeClaim,maybeFormOrganization,updateRelationship} from './society.js'; import {runExperiment,applyConstructionWork,beginEmpiricalConstruction} from './artifacts.js'; import {transferObject,transformMaterials,objectMass,advanceDecomposition} from './materials.js'; -import {recordAffordanceOutcome} from './cognition-state.js'; +import {ageCognitionDebt,recordAffordanceOutcome} from './cognition-state.js'; import {recordStrategyOutcome} from './strategy.js'; +import {queueCognition} from './cognition-queue.js'; function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);} -function perceiveLocal(world,citizen,at){const discoveries=[...perceiveResources(world,citizen,at),...perceiveStructures(world,citizen,at)];if(discoveries.length)queueCognition(world,citizen,'discovery',.65);for(const other of world.citizens){if(other.id!==citizen.id&&other.alive&&dist(citizen.position,other.position)<=8&&!citizen.knownEntityIds.includes(other.id)){citizen.knownEntityIds.push(other.id);queueCognition(world,citizen,'encounter',.5);recordMemory(citizen,{kind:'episodic',content:{encounter:other.id},source:{kind:'observation'},confidence:.9,salience:.45,worldMinute:at});}}} +function perceiveLocal(world,citizen,at){const discoveries=[...perceiveResources(world,citizen,at),...perceiveStructures(world,citizen,at)];if(discoveries.length)queueCognition(world,citizen,'discovery',.65,at,discoveries[0]);for(const other of world.citizens){if(other.id!==citizen.id&&other.alive&&dist(citizen.position,other.position)<=8&&!citizen.knownEntityIds.includes(other.id)){citizen.knownEntityIds.push(other.id);queueCognition(world,citizen,'encounter',.5,at,other.id);recordMemory(citizen,{kind:'episodic',content:{encounter:other.id},source:{kind:'observation'},confidence:.9,salience:.45,worldMinute:at});}}} function runReflex(world,citizen,at){if(!citizen.alive||citizen.currentActionId)return;const p=survivalFallback(world,citizen,at)||localDeliberation(world,citizen,at);if(!p)return;try{applyAcceptedPlan(world,citizen,p,at);}catch{if(!citizen.currentActionId)startAction(world,citizen,{type:'OBSERVE',durationMinutes:10,purpose:'orientation'},at);}} function gather(world,c,a,at){const d=world.resourceDeposits.find(x=>x.id===a.targetId);if(!d)return;const q=Math.max(.1,Math.min(Number(a.payload?.quantity)||1,d.quantity));d.quantity-=q;const o={id:stableId('obj',d.id,c.id,world.objects.length,at),kind:'gathered_material',material:d.type,quantity:q,massPerUnitKg:1,properties:null,holderId:c.id,position:{...c.position},condition:1,provenance:{type:'GATHERED',depositId:d.id,actionId:a.id}};world.objects.push(o);c.possessions.push(o.id);appendEvent(world,'RESOURCE_GATHERED',c.id,{objectId:o.id,depositId:d.id,quantity:q},[a.id],at);} function consumeFromDeposit(world,c,a,type,amount,restore,at){const d=world.resourceDeposits.find(x=>x.id===a.targetId&&x.type===type);if(!d||d.quantityx.id===ids[i]&&x.holderId===c.id);if(!o)continue;const q=Math.max(.01,Math.min(Number(quantities[i])||o.quantity,o.quantity));consume.push({objectId:o.id,quantity:q});total+=q*o.massPerUnitKg;}if(!consume.length||total<=0)return;transformMaterials(world,c,{consume,output:{material:'composite',quantity:total,massPerUnitKg:1,kind:a.payload?.form||'artifact'},process:a.type.toLowerCase()},at);} -function build(world,c,a,at){if(a.payload?.projectId){applyConstructionWork(world,c,a.payload.projectId,a.payload?.workMinutes||a.endsWorldMinute-a.startedWorldMinute,at);return;}const ids=a.payload?.inputObjectIds||[];if(!ids.length||!a.payload?.site||!(a.concepts||[]).length)return;const p=beginEmpiricalConstruction(world,c,{concepts:a.concepts,inputObjectIds:ids,workMinutes:a.payload.workMinutes||120,form:a.payload.form||'structure',site:a.payload.site},at);a.payload.projectId=p.id;applyConstructionWork(world,c,p.id,a.endsWorldMinute-a.startedWorldMinute,at);} -function primitiveCommunicate(world,c,a,at){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(!t)return;const signal=a.payload?.primitiveSignal;if(signal&&c.language.primitiveSignals.includes(signal)){appendEvent(world,'COMMUNICATION',c.id,{receiverId:t.id,primitiveSignal:signal},[a.id],at);recordMemory(t,{kind:'episodic',content:{primitiveSignal:signal,from:c.id},source:{kind:'communication'},confidence:.9,salience:.7,worldMinute:at});updateRelationship(world,c,t.id,{familiarity:.015},at);updateRelationship(world,t,c.id,{familiarity:.02},at);return;}if(a.payload?.beliefId){shareBelief(world,c,t,a.payload.beliefId,at);updateRelationship(world,c,t.id,{familiarity:.025,trust:.005},at);updateRelationship(world,t,c.id,{familiarity:.03},at);return;}const concepts=(a.payload?.conceptIds?.length?a.payload.conceptIds:[a.payload?.concept]).filter(Boolean).filter(x=>knows(c,x));if(concepts.length){const tokens=concepts.map(x=>coinSignal(world,c,x,at));communicate(world,c,t,{concepts,tokens},at);updateRelationship(world,c,t.id,{familiarity:.02},at);updateRelationship(world,t,c.id,{familiarity:.025},at);}} -function resolveAction(world,a,at){const c=world.citizens.find(x=>x.id===a.actorId);if(!c||!c.alive)return;if(a.type==='DRINK')consumeFromDeposit(world,c,a,'water',.7,x=>restoreHydration(x,30),at);else if(a.type==='EAT')consumeFromDeposit(world,c,a,'food',.3,x=>restoreCalories(x,25),at);else if(a.type==='GATHER')gather(world,c,a,at);else if(a.type==='TEACH'){const target=world.citizens.find(x=>x.id===a.targetId&&x.alive),concept=a.payload?.concept;if(target&&concept&&knows(c,concept)){learn(target,concept,{kind:'teaching',from:c.id,eventId:a.id},.65,at);updateRelationship(world,c,target.id,{familiarity:.02,trust:.01},at);updateRelationship(world,target,c.id,{familiarity:.03,trust:.025},at);}}else if(a.type==='COMMUNICATE')primitiveCommunicate(world,c,a,at);else if(a.type==='PROMISE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);const cm=makeCommitment(world,c,{kind:a.payload?.kindCode||'other',counterpartyId:a.targetId,terms:{purposeConcept:a.payload?.purposeConcept||null}},at);if(t){updateRelationship(world,c,t.id,{familiarity:.02,obligation:.03},at);updateRelationship(world,t,c.id,{familiarity:.01},at);if(cm.kind==='join')maybeFormOrganization(world,c,t,a.payload?.purposeConcept,at);}}else if(a.type==='CLAIM')makeClaim(world,c,{subject:a.payload?.subjectId||a.targetId||c.id,predicate:a.payload?.predicateCode||'other',purposeConcept:a.payload?.purposeConcept||null},at);else if(a.type==='EXPERIMENT')runExperiment(world,c,{inputConcepts:a.concepts||[],targetIds:a.payload?.targetIds||[],method:a.payload?.methodCode||'observe'},at);else if(a.type==='BUILD')build(world,c,a,at);else if(a.type==='TRANSFER'&&a.payload?.objectId&&a.targetId)transferObject(world,a.payload.objectId,a.targetId,at);else if(['CUT','DIG','HEAT','COOL','MIX','ASSEMBLE'].includes(a.type))genericTransform(world,c,a,at);else if(a.type==='CARE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t&&c.body.calories>15){c.body.calories-=2;restoreCalories(t,8);restoreHydration(t,5);appendEvent(world,'CARE_GIVEN',c.id,{targetId:t.id},[a.id],at);updateRelationship(world,c,t.id,{affection:.035,trust:.02},at);updateRelationship(world,t,c.id,{affection:.045,trust:.05},at);}}else if(a.type==='ATTACK'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){const harm=4+Math.round(c.psychology.aggression*12);t.body.health=Math.max(0,t.body.health-harm);t.body.injuries.push({source:a.id,severity:harm/100,worldMinute:at});appendEvent(world,'VIOLENCE',c.id,{against:t.id,harm},[a.id],at);updateRelationship(world,c,t.id,{trust:-.08,affection:-.06},at);updateRelationship(world,t,c.id,{trust:-.16,affection:-.12,fear:.18},at);if(t.body.health<=0)killCitizen(world,t,'trauma',at);}}else if(a.type==='REPRODUCE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){try{conceive(world,c,t,at);}catch{}}}} -function queueCognition(world,citizen,reason,priority=.45){if(!citizen?.alive)return;if(world.cognitionQueue.some(x=>x.citizenId===citizen.id&&x.reason===reason))return;world.cognitionQueue.push({citizenId:citizen.id,reason,priority});citizen.cognition.pending=true;} +function genericTransform(world,c,a,at){const ids=a.payload?.inputObjectIds||[];if(!ids.length)return null;const quantities=a.payload?.quantities||[];const consume=[];let total=0;for(let i=0;ix.id===ids[i]&&x.holderId===c.id);if(!o)continue;const q=Math.max(.01,Math.min(Number(quantities[i])||o.quantity,o.quantity));consume.push({objectId:o.id,quantity:q});total+=q*o.massPerUnitKg;}if(!consume.length||total<=0)return null;return transformMaterials(world,c,{consume,output:{material:'composite',quantity:total,massPerUnitKg:1,kind:a.payload?.form||'artifact'},process:a.type.toLowerCase()},at);} +function build(world,c,a,at){if(a.payload?.projectId)return applyConstructionWork(world,c,a.payload.projectId,a.payload?.workMinutes||a.endsWorldMinute-a.startedWorldMinute,at);const ids=a.payload?.inputObjectIds||[];if(!ids.length||!a.payload?.site||!(a.concepts||[]).length)return null;const p=beginEmpiricalConstruction(world,c,{concepts:a.concepts,inputObjectIds:ids,workMinutes:a.payload.workMinutes||120,form:a.payload.form||'structure',site:a.payload.site},at);a.payload.projectId=p.id;return applyConstructionWork(world,c,p.id,a.endsWorldMinute-a.startedWorldMinute,at);} +function primitiveCommunicate(world,c,a,at){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(!t)return;const signal=a.payload?.primitiveSignal;if(signal&&c.language.primitiveSignals.includes(signal)){appendEvent(world,'COMMUNICATION',c.id,{receiverId:t.id,primitiveSignal:signal},[a.id],at);recordMemory(t,{kind:'episodic',content:{primitiveSignal:signal,from:c.id},source:{kind:'communication'},confidence:.9,salience:.7,worldMinute:at});updateRelationship(world,c,t.id,{familiarity:.015},at);updateRelationship(world,t,c.id,{familiarity:.02},at);return;}if(a.payload?.beliefId){shareBelief(world,c,t,a.payload.beliefId,at);updateRelationship(world,c,t.id,{familiarity:.025,trust:.005},at);updateRelationship(world,t,c.id,{familiarity:.03},at);return;}const concepts=(a.payload?.conceptIds?.length?a.payload.conceptIds:[a.payload?.concept]).filter(Boolean).filter(x=>knows(c,x));if(concepts.length){const before=t.knowledge.filter(k=>k.active!==false).length,tokens=concepts.map(x=>coinSignal(world,c,x,at));const result=communicate(world,c,t,{concepts,tokens},at);updateRelationship(world,c,t.id,{familiarity:.02},at);updateRelationship(world,t,c.id,{familiarity:.025},at);if(t.knowledge.filter(k=>k.active!==false).length>before)queueCognition(world,t,'new_concept',.72,at,result.receiverInterpretations.find(Boolean));}} +function resolveAction(world,a,at){const c=world.citizens.find(x=>x.id===a.actorId);if(!c||!c.alive)return;if(a.type==='DRINK')consumeFromDeposit(world,c,a,'water',.7,x=>restoreHydration(x,30),at);else if(a.type==='EAT')consumeFromDeposit(world,c,a,'food',.3,x=>restoreCalories(x,25),at);else if(a.type==='GATHER')gather(world,c,a,at);else if(a.type==='TEACH'){const target=world.citizens.find(x=>x.id===a.targetId&&x.alive),concept=a.payload?.concept;if(target&&concept&&knows(c,concept)){const known=knows(target,concept);learn(target,concept,{kind:'teaching',from:c.id,eventId:a.id},.65,at);updateRelationship(world,c,target.id,{familiarity:.02,trust:.01},at);updateRelationship(world,target,c.id,{familiarity:.03,trust:.025},at);if(!known)queueCognition(world,target,'new_concept',.72,at,a.id);}}else if(a.type==='COMMUNICATE')primitiveCommunicate(world,c,a,at);else if(a.type==='PROMISE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);const cm=makeCommitment(world,c,{kind:a.payload?.kindCode||'other',counterpartyId:a.targetId,terms:{purposeConcept:a.payload?.purposeConcept||null}},at);if(t){updateRelationship(world,c,t.id,{familiarity:.02,obligation:.03},at);updateRelationship(world,t,c.id,{familiarity:.01},at);if(cm.kind==='join')maybeFormOrganization(world,c,t,a.payload?.purposeConcept,at);}}else if(a.type==='CLAIM')makeClaim(world,c,{subject:a.payload?.subjectId||a.targetId||c.id,predicate:a.payload?.predicateCode||'other',purposeConcept:a.payload?.purposeConcept||null},at);else if(a.type==='EXPERIMENT'){const experiment=runExperiment(world,c,{inputConcepts:a.concepts||[],targetIds:a.payload?.targetIds||[],method:a.payload?.methodCode||'observe'},at);if(experiment.discoveredConceptIds.length)queueCognition(world,c,'experiment_success',.78,at,experiment.id);}else if(a.type==='BUILD'){const project=build(world,c,a,at);if(project?.status==='completed')queueCognition(world,c,'construction_completed',.9,at,project.id);}else if(a.type==='TRANSFER'&&a.payload?.objectId&&a.targetId)transferObject(world,a.payload.objectId,a.targetId,at);else if(['CUT','DIG','HEAT','COOL','MIX','ASSEMBLE'].includes(a.type)){const object=genericTransform(world,c,a,at);if(object)queueCognition(world,c,'material_event',.64,at,object.id);}else if(a.type==='CARE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t&&c.body.calories>15){c.body.calories-=2;restoreCalories(t,8);restoreHydration(t,5);appendEvent(world,'CARE_GIVEN',c.id,{targetId:t.id},[a.id],at);updateRelationship(world,c,t.id,{affection:.035,trust:.02},at);updateRelationship(world,t,c.id,{affection:.045,trust:.05},at);}}else if(a.type==='ATTACK'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){const harm=4+Math.round(c.psychology.aggression*12);t.body.health=Math.max(0,t.body.health-harm);t.body.injuries.push({source:a.id,severity:harm/100,worldMinute:at});const event=appendEvent(world,'VIOLENCE',c.id,{against:t.id,harm},[a.id],at);updateRelationship(world,c,t.id,{trust:-.08,affection:-.06},at);updateRelationship(world,t,c.id,{trust:-.16,affection:-.12,fear:.18},at);queueCognition(world,c,'conflict',.88,at,event.id);queueCognition(world,t,'conflict',.95,at,event.id);if(t.body.health<=0)killCitizen(world,t,'trauma',at);}}else if(a.type==='REPRODUCE'){const t=world.citizens.find(x=>x.id===a.targetId&&x.alive);if(t){try{const pregnancy=conceive(world,c,t,at);queueCognition(world,c,'reproduction',.76,at,pregnancy.id);queueCognition(world,t,'reproduction',.76,at,pregnancy.id);}catch{}}}} function nextBoundary(world,cursor,target){let next=Math.min(target,cursor+30);for(const a of world.actions)if(a.status==='active'&&a.endsWorldMinute>cursor)next=Math.min(next,a.endsWorldMinute);for(const c of world.citizens){const due=c.body.pregnancy?.dueWorldMinute;if(due>cursor)next=Math.min(next,due);}return Math.max(cursor+1,next);} function stepSegment(world,from,to){ advanceEnvironment(world,from,to);const delta=to-from;advanceDecomposition(world,delta,to); - for(const c of world.citizens){advanceBody(world,c,delta);advanceDisease(world,c,from,to);if(c.alive){decayMemories(c,to);perceiveLocal(world,c,to);if(c.body.health<=0)killCitizen(world,c,'disease_or_biology',to);}} + for(const c of world.citizens){advanceBody(world,c,delta);advanceDisease(world,c,from,to);if(c.alive){ageCognitionDebt(c,delta);decayMemories(c,to);perceiveLocal(world,c,to);if(c.body.health<=0)killCitizen(world,c,'disease_or_biology',to);}} advancePregnancies(world,to);world.clock.worldMinute=to; const completed=completeDueActions(world,to); for(const action of completed){ @@ -48,20 +48,20 @@ function stepSegment(world,from,to){ if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:false,reason:failureReason},action.endsWorldMinute); recordStrategyOutcome(world,citizen,{ok:false,family:plan?.affordanceFamily||action.type.toLowerCase()},action.endsWorldMinute); recordMemory(citizen,{kind:'episodic',content:{actionFailure:{actionId:action.id,type:action.type,reason:failureReason}},source:{kind:'physical_outcome',eventId:event.id},confidence:1,salience:.75,worldMinute:action.endsWorldMinute}); - queueCognition(world,citizen,'physical_action_failed',.85); + queueCognition(world,citizen,'physical_action_failed',.85,action.endsWorldMinute,event.id); } } if(resolved&&citizen?.alive&&!continuePlan(world,citizen,action,action.endsWorldMinute)){ if(plan?.affordanceFamily)recordAffordanceOutcome(citizen,plan.affordanceFamily,{ok:true},action.endsWorldMinute); recordStrategyOutcome(world,citizen,{ok:true,family:plan?.affordanceFamily||action.type.toLowerCase()},action.endsWorldMinute); - queueCognition(world,citizen,'plan_completed',.42); + queueCognition(world,citizen,'plan_completed',.42,action.endsWorldMinute,action.id); } } for(const c of world.citizens)if(c.alive&&!c.currentActionId)runReflex(world,c,to); } -export function advanceWorldTo(world,nowMs=Date.now()){const target=worldMinuteAt(world,nowMs);let cursor=world.clock.worldMinute;if(target<=cursor)return world;while(cursor=10080)){queueCognition(world,c,'reflection',.2);c.cognition.lastReflectionMinute=target;}}return world;} -export function createHumanAvatar(world,{externalId,displayName=null,massKg=70},at=world.clock.worldMinute){if(world.citizens.some(c=>c.externalId===externalId))return world.citizens.find(c=>c.externalId===externalId);if(world.reserves.observerEmbodimentKgx.id===citizenId&&x.kind==='HUMAN_LINKED');if(!c||!c.alive)throw new Error('human_avatar_unavailable');world.privateHumanIntents[c.id]={intent:String(intent||'').slice(0,2000),worldMinute:at};world.cognitionQueue.push({citizenId:c.id,reason:'human_direction',priority:1});appendEvent(world,'EXTERNAL_DIRECTION_RECEIVED',c.id,{hasDirection:true},[],at);return {citizenId:c.id,queued:true};} +export function advanceWorldTo(world,nowMs=Date.now()){const target=worldMinuteAt(world,nowMs);let cursor=world.clock.worldMinute;if(target<=cursor)return world;while(cursor=10080)){queueCognition(world,c,'reflection',.2,target);c.cognition.lastReflectionMinute=target;}}return world;} +export function createHumanAvatar(world,{externalId,displayName=null,massKg=70},at=world.clock.worldMinute){if(world.citizens.some(c=>c.externalId===externalId))return world.citizens.find(c=>c.externalId===externalId);if(world.reserves.observerEmbodimentKgx.id===citizenId&&x.kind==='HUMAN_LINKED');if(!c||!c.alive)throw new Error('human_avatar_unavailable');world.privateHumanIntents[c.id]={intent:String(intent||'').slice(0,2000),worldMinute:at};const event=appendEvent(world,'EXTERNAL_DIRECTION_RECEIVED',c.id,{hasDirection:true},[],at);queueCognition(world,c,'human_direction',1,at,event.id);return {citizenId:c.id,queued:true};} export function acceptCognitiveProposal(world,citizenId,proposal,at=world.clock.worldMinute){const c=world.citizens.find(x=>x.id===citizenId);if(!c||!c.alive)throw new Error('citizen_unavailable');const v=validateCognitiveProposal(world,c,proposal);if(!v.ok)throw new Error(v.reason);appendEvent(world,'COGNITIVE_PLAN_ACCEPTED',c.id,{conceptIds:proposal.concepts||[],actionTypes:(proposal.actions||[]).map(x=>x.type),contextKnowledgeCount:c.knowledge.filter(k=>k.active!==false).length},[],at);return applyAcceptedPlan(world,c,proposal,at);} export function publicWorld(world,nowMs=Date.now()){const at=worldMinuteAt(world,nowMs);return {version:world.version,worldId:world.worldId,clock:{...world.clock,worldMinute:Math.max(world.clock.worldMinute,at)},environment:world.environment,citizens:world.citizens.map(c=>{const action=activeAction(world,c),pos=action?positionAt(c,action,at):c.position;return {id:c.id,kind:c.kind,selfName:c.selfName,observerDisplayName:c.kind==='HUMAN_LINKED'?c.observerDisplayName:null,alive:c.alive,birthWorldMinute:c.birthWorldMinute,deathWorldMinute:c.deathWorldMinute,position:pos,body:{hydration:c.body.hydration,calories:c.body.calories,sleepPressure:c.body.sleepPressure,health:c.body.health,ageMinutes:c.body.ageMinutes,temperatureC:c.body.temperatureC,exposure:c.body.exposure||null,reproductiveRole:c.body.reproductiveRole||null,pregnancy:c.body.pregnancy?{dueWorldMinute:c.body.pregnancy.dueWorldMinute}:null,diseases:c.body.diseases.length,injuries:c.body.injuries.length},psychology:c.psychology,knowledge:c.kind==='HUMAN_LINKED'?{count:c.knowledge.filter(k=>k.active!==false).length}:c.knowledge.filter(k=>k.active!==false).map(k=>({concept:k.concept,confidence:k.confidence,provenance:k.provenance})),language:{primitiveSignals:c.language.primitiveSignals,lexicon:c.language.lexicon},knownEntityIds:c.knownEntityIds,relationships:c.relationships,possessions:c.possessions,activeGoal:c.activeGoal,currentAction:action?{id:action.id,type:action.type,purpose:action.purpose,startedWorldMinute:action.startedWorldMinute,endsWorldMinute:action.endsWorldMinute,fromPosition:action.fromPosition,targetPosition:action.targetPosition,targetId:action.targetId,path:action.path||null,physics:action.physics||null}:null};}),objects:world.objects.filter(o=>o.quantity>0).map(o=>({id:o.id,kind:o.kind,material:o.material||null,quantity:o.quantity,massPerUnitKg:o.massPerUnitKg,holderId:o.holderId,position:o.position,condition:o.condition,properties:o.properties||null,provenance:o.provenance})),resourceDeposits:world.resourceDeposits.map(d=>({id:d.id,type:d.type,quantity:d.quantity,position:d.position})),buildings:world.buildings.map(b=>({...b,occupants:world.citizens.filter(c=>c.alive&&Math.hypot(c.position.x-b.position.x,c.position.y-b.position.y)<=1.8).map(c=>c.id)})),organizations:world.organizations,claims:world.claims,projects:world.projects,history:classifyHistory(world).entries.slice(0,200),recentLedger:world.ledger.slice(-80).filter(e=>['PHYSICAL_IMPACT','STRUCTURE_DAMAGED','STRUCTURE_FRACTURED'].includes(e.type)).map(e=>({id:e.id,type:e.type,worldMinute:e.worldMinute,actorId:e.actorId,payload:e.payload,causes:e.causes})),ledgerHead:world.ledgerHead};} export function getHistory(world){return classifyHistory(world);}export function getWhy(world,eventId){return whyEvent(world,eventId);}export function markCitizenDead(world,citizenId,cause,at=world.clock.worldMinute){const c=world.citizens.find(x=>x.id===citizenId);if(!c)throw new Error('citizen_not_found');return killCitizen(world,c,cause,at);} diff --git a/world/genesis.js b/world/genesis.js index 7b321d9..1e12dad 100644 --- a/world/genesis.js +++ b/world/genesis.js @@ -3,6 +3,7 @@ import {rng,stableId} from './rng.js'; import {appendEvent} from './ledger.js'; import {genesisGenome} from './genetics.js'; import {ensureCognitionState} from './cognition-state.js'; +import {queueCognition} from './cognition-queue.js'; function body(r,index,genome){return {massKg:55+Math.round(r()*30),hydration:82+r()*15,calories:82+r()*15,sleepPressure:5+r()*15,temperatureC:36.6,health:100,injuries:[],diseases:[],fertility:genome.fertility,pregnancy:null,reproductiveRole:index%2===0?'gestating':'non_gestating',ageMinutes:(18+Math.floor(r()*17))*525600,alive:true};} function psychology(r){return {curiosity:r(),riskTolerance:r(),socialDrive:r(),aggression:r()*.6,empathy:.35+r()*.65,noveltySeeking:r(),stress:0,fear:0,attachment:0.2+r()*.4,confidence:.3+r()*.6};} @@ -27,7 +28,8 @@ export function createSovereignGenesis({seed=20260915,realEpochMs=Date.now()}={} {id:stableId('dep',seed,4),type:'clay',quantity:12000,renewPerDay:.2,position:{x:43,y:53},accessDifficulty:.18}, {id:stableId('dep',seed,5),type:'ore',quantity:8000,renewPerDay:0,position:{x:32,y:31},accessDifficulty:.7} ]; - const world={version:WORLD_VERSION,worldId,seed,clock:{realEpochMs:Number(realEpochMs),worldMinute:0},environment:{temperatureC:18,precipitation:0,soilMoisture:.55,seasonPhase:0,dayPhase:0,pollution:0,pathogenPressure:.012,naturalReservoirs:{waterKg:9_000_000,biomassKg:1_000_000,mineralKg:1_000_000},metabolicMatterKg:0},citizens,objects,resourceDeposits,actions:[],projects:[],designs:[],buildings:[],organizations:[],claims:[],commitments:[],currencySystems:[],relationships:[],languages:[],experiments:[],reserves:{observerEmbodimentKg:8000,observerEmbodimentEnergyUnits:800000},privateHumanIntents:{},cognitionQueue:citizens.map(c=>({citizenId:c.id,reason:'genesis_awareness',priority:.6})),ledger:[],ledgerHead:'GENESIS',observer:{classifications:[]}}; + const world={version:WORLD_VERSION,worldId,seed,clock:{realEpochMs:Number(realEpochMs),worldMinute:0},environment:{temperatureC:18,precipitation:0,soilMoisture:.55,seasonPhase:0,dayPhase:0,pollution:0,pathogenPressure:.012,naturalReservoirs:{waterKg:9_000_000,biomassKg:1_000_000,mineralKg:1_000_000},metabolicMatterKg:0},citizens,objects,resourceDeposits,actions:[],projects:[],designs:[],buildings:[],organizations:[],claims:[],commitments:[],currencySystems:[],relationships:[],languages:[],experiments:[],reserves:{observerEmbodimentKg:8000,observerEmbodimentEnergyUnits:800000},privateHumanIntents:{},cognitionQueue:[],ledger:[],ledgerHead:'GENESIS',observer:{classifications:[]}}; + for(const c of citizens)queueCognition(world,c,'genesis_awareness',.6,0); appendEvent(world,'WORLD_GENESIS','world',{population:GENESIS_POPULATION,seed,endowmentObjectIds:objects.map(o=>o.id),materialClosure:true,timeRatio:'1 real second = 1 world minute'},[],0); return world; } diff --git a/world/index.js b/world/index.js index 372df18..a2ad7cc 100644 --- a/world/index.js +++ b/world/index.js @@ -23,3 +23,5 @@ export * from './disease.js'; export * from './beliefs.js'; export * from './impact.js'; export * from './strategy.js'; +export * from './cognition-state.js'; +export * from './cognition-queue.js'; diff --git a/world/reproduction.js b/world/reproduction.js index c3a67fe..d2b573a 100644 --- a/world/reproduction.js +++ b/world/reproduction.js @@ -3,9 +3,10 @@ import {recombineGenomes} from './genetics.js'; import {PRIMITIVE_SIGNALS} from './constants.js'; import {appendEvent} from './ledger.js'; import {ensureCognitionState} from './cognition-state.js'; +import {queueCognition} from './cognition-queue.js'; const GESTATION_MINUTES=280*1440; function adult(c){return c.body.ageMinutes>=16*525600;} export function conceive(world,a,b,at=world.clock.worldMinute){if(!a?.alive||!b?.alive)throw new Error('parent_unavailable');const gestating=a.body.reproductiveRole==='gestating'?a:b.body.reproductiveRole==='gestating'?b:null;const partner=gestating===a?b:a;if(!gestating||partner.body.reproductiveRole!=='non_gestating')throw new Error('reproductive_roles_incompatible');if(!adult(gestating)||!adult(partner))throw new Error('biological_maturity_required');if(gestating.body.pregnancy)throw new Error('pregnancy_already_active');const p={id:stableId('pregnancy',gestating.id,partner.id,at),partnerId:partner.id,conceivedWorldMinute:at,dueWorldMinute:at+GESTATION_MINUTES};gestating.body.pregnancy=p;appendEvent(world,'CONCEPTION',gestating.id,{pregnancyId:p.id,partnerId:partner.id,dueWorldMinute:p.dueWorldMinute},[],at);return p;} function newborn(world,mother,partner,at,pregnancy){const genome=recombineGenomes(mother.genome,partner.genome,pregnancy.id);const id=stableId('citizen',pregnancy.id,'birth');const role=hash32(id)%2?'gestating':'non_gestating';const c={id,kind:'BORN_IN_WORLD',selfName:null,externalId:null,alive:true,birthWorldMinute:at,deathWorldMinute:null,position:{...mother.position},genome,body:{massKg:3.2+(hash32(id)%700)/1000,hydration:96,calories:96,sleepPressure:15,temperatureC:36.8,health:100,injuries:[],diseases:[],fertility:genome.fertility,pregnancy:null,reproductiveRole:role,ageMinutes:0,alive:true},psychology:{curiosity:.45, riskTolerance:.25,socialDrive:.8,aggression:.08,empathy:.55,noveltySeeking:.5,stress:.2,fear:.15,attachment:.75,confidence:.1},knowledge:[],memories:[],knownEntityIds:[id,mother.id,partner.id],skills:{},language:{primitiveSignals:[...PRIMITIVE_SIGNALS],lexicon:{},heard:{},grammarPatterns:{}},beliefs:[],relationships:{[mother.id]:{familiarity:.8,trust:.5,affection:.6,fear:0,obligation:0},[partner.id]:{familiarity:.3,trust:.2,affection:.25,fear:0,obligation:0}},possessions:[],goals:[],activeGoal:null,plans:[],currentActionId:null,commitments:[],cognition:{lastReflectionMinute:null,pending:true,reason:'birth'}};ensureCognitionState(c,at);return c;} -export function advancePregnancies(world,toMinute){const born=[];for(const mother of [...world.citizens]){const p=mother.body.pregnancy;if(!p||p.dueWorldMinute>toMinute||!mother.alive)continue;const partner=world.citizens.find(c=>c.id===p.partnerId);if(!partner){mother.body.pregnancy=null;continue;}const child=newborn(world,mother,partner,p.dueWorldMinute,p);if(mother.body.massKg<=child.body.massKg+35){mother.body.pregnancy=null;appendEvent(world,'PREGNANCY_LOSS',mother.id,{pregnancyId:p.id,reason:'insufficient_biological_mass'},[p.id],p.dueWorldMinute);continue;}mother.body.massKg-=child.body.massKg;world.citizens.push(child);mother.body.pregnancy=null;world.cognitionQueue.push({citizenId:child.id,reason:'birth',priority:.9});appendEvent(world,'BIRTH',child.id,{motherId:mother.id,partnerId:partner.id,pregnancyId:p.id},[p.id],p.dueWorldMinute);born.push(child);}return born;} +export function advancePregnancies(world,toMinute){const born=[];for(const mother of [...world.citizens]){const p=mother.body.pregnancy;if(!p||p.dueWorldMinute>toMinute||!mother.alive)continue;const partner=world.citizens.find(c=>c.id===p.partnerId);if(!partner){mother.body.pregnancy=null;continue;}const child=newborn(world,mother,partner,p.dueWorldMinute,p);if(mother.body.massKg<=child.body.massKg+35){mother.body.pregnancy=null;appendEvent(world,'PREGNANCY_LOSS',mother.id,{pregnancyId:p.id,reason:'insufficient_biological_mass'},[p.id],p.dueWorldMinute);continue;}mother.body.massKg-=child.body.massKg;world.citizens.push(child);mother.body.pregnancy=null;const event=appendEvent(world,'BIRTH',child.id,{motherId:mother.id,partnerId:partner.id,pregnancyId:p.id},[p.id],p.dueWorldMinute);queueCognition(world,child,'birth',.9,p.dueWorldMinute,event.id);queueCognition(world,mother,'reproduction',.72,p.dueWorldMinute,event.id);queueCognition(world,partner,'reproduction',.62,p.dueWorldMinute,event.id);born.push(child);}return born;} From 77b1384d042c28a274fcac365db4597ed516a268 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:49:15 +0200 Subject: [PATCH 07/10] feat: govern Workers AI by measured neurons --- tests/test_sovereign_neuron_governor.mjs | 85 +++++++++++++++++ tests/test_sovereign_runtime_contract.mjs | 27 +++++- worker/src/index.js | 91 +++++++++++------- worker/src/neuron-governor.js | 110 ++++++++++++++++++++++ 4 files changed, 276 insertions(+), 37 deletions(-) create mode 100644 tests/test_sovereign_neuron_governor.mjs create mode 100644 worker/src/neuron-governor.js diff --git a/tests/test_sovereign_neuron_governor.mjs b/tests/test_sovereign_neuron_governor.mjs new file mode 100644 index 0000000..e11f7e1 --- /dev/null +++ b/tests/test_sovereign_neuron_governor.mjs @@ -0,0 +1,85 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + ensureNeuronBudget, + estimateReservation, + neuronCapacity, + reconcileNeurons, + reserveNeurons, +} from '../worker/src/neuron-governor.js'; + +const MODEL='@cf/zai-org/glm-4.7-flash'; +function budget(overrides={}){return {day:'2026-09-21',usedNeurons:0,reservedNeurons:0,calls:0,promptTokens:0,completionTokens:0,lastFailureRealMs:0,lastExhaustedDay:null,lastAccountingWarning:null,...overrides};} + +test('GLM token usage converts to documented neurons',()=>{ + const state=budget(); + const reservation=reserveNeurons(state,20,'normal').reservation; + const result=reconcileNeurons(state,reservation,{prompt_tokens:1000,completion_tokens:200},MODEL); + assert.equal(result.charged,12.78); + assert.equal(state.promptTokens,1000); + assert.equal(state.completionTokens,200); + assert.equal(state.calls,1); +}); + +test('normal work cannot consume priority or human reserves',()=>{ + const normal=budget({usedNeurons:7999}); + assert.equal(reserveNeurons(normal,2,'normal').ok,false); + assert.equal(reserveNeurons(normal,2,'priority').ok,true); + const priority=budget({usedNeurons:8999}); + assert.equal(reserveNeurons(priority,2,'priority').ok,false); + assert.equal(reserveNeurons(priority,2,'emergency').ok,true); +}); + +test('missing usage charges reservation and unknown model fails closed',()=>{ + const state=budget(); + const held=reserveNeurons(state,25,'normal').reservation; + assert.equal(reconcileNeurons(state,held,null,MODEL).charged,25); + assert.equal(state.lastAccountingWarning,'usage_missing_charged_reservation'); + assert.equal(estimateReservation('@cf/unknown/model','{}',200),Infinity); +}); + +test('budget resets on the UTC day boundary and migrates legacy runtime state',()=>{ + const world={runtime:{aiBudget:{day:'2026-09-20',calls:99,lastFailureRealMs:7}}}; + const state=ensureNeuronBudget(world,Date.parse('2026-09-21T00:00:01Z')); + assert.equal(state.day,'2026-09-21'); + assert.equal(state.usedNeurons,0); + assert.equal(state.calls,0); + assert.equal(world.runtime.aiBudget,undefined); +}); + +test('malformed or negative usage charges the reservation',()=>{ + for(const usage of [{prompt_tokens:-1,completion_tokens:2},{prompt_tokens:'bad',completion_tokens:2},{completion_tokens:2}]){ + const state=budget(),held=reserveNeurons(state,9,'normal').reservation; + assert.equal(reconcileNeurons(state,held,usage,MODEL).charged,9); + } +}); + +test('fractional nested usage is normalized conservatively',()=>{ + const state=budget(),held=reserveNeurons(state,20,'normal').reservation; + const result=reconcileNeurons(state,held,{result:{usage:{prompt_tokens:1.2,completion_tokens:2.1}}},MODEL); + assert.equal(state.promptTokens,2); + assert.equal(state.completionTokens,3); + assert.equal(result.charged,.1202); +}); + +test('hard stop and existing reservations participate in admission',()=>{ + assert.equal(reserveNeurons(budget({usedNeurons:9499}),2,'emergency').ok,false); + assert.equal(reserveNeurons(budget({reservedNeurons:7999}),2,'normal').ok,false); + assert.equal(neuronCapacity(budget({usedNeurons:9499.5}),'emergency'),.5); +}); + +test('actual usage above reservation remains fully accounted and cannot reconcile twice',()=>{ + const state=budget(),held=reserveNeurons(state,1,'normal').reservation; + const first=reconcileNeurons(state,held,{prompt_tokens:1000,completion_tokens:200},MODEL); + const second=reconcileNeurons(state,held,{prompt_tokens:1000,completion_tokens:200},MODEL); + assert.equal(first.charged,12.78); + assert.equal(state.usedNeurons,12.78); + assert.equal(state.reservedNeurons,0); + assert.equal(second.charged,0); + assert.equal(second.warning,'reservation_already_reconciled'); +}); + +test('reservation estimate uses conservative prompt length and full completion cap',()=>{ + const estimated=estimateReservation(MODEL,'1234567',200); + assert.equal(estimated,7.2965); +}); diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index 9018635..c2d5202 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -78,14 +78,30 @@ test('canonical world persists in Durable Object SQLite with SHA-256 checkpoint assert.doesNotMatch(worker,/storage\.put\(WORLD_KEY/); }); -test('Workers AI cognition has a hard daily budget and never falls back to fabricated social decisions',()=>{ - assert.match(worker,/AI_CALLS_PER_REAL_DAY/); - assert.match(worker,/aiBudget/); - assert.match(worker,/ai_budget_exhausted/); +test('Workers AI cognition is governed by measured neurons, not call count',()=>{ + assert.doesNotMatch(worker,/AI_CALLS_PER_REAL_DAY/); + assert.match(worker,/ensureNeuronBudget/); + assert.match(worker,/reserveNeurons/); + assert.match(worker,/reconcileNeurons/); + assert.match(worker,/neuron_budget_exhausted/); assert.match(worker,/processCognition\(1\)/); assert.doesNotMatch(worker,/processCognition\(2\)/); }); +test('health exposes safe neuron accounting without prompts or strategy text',()=>{ + const start=worker.indexOf("if(request.method==='GET'&&path==='/health')"); + const end=worker.indexOf("if(request.method==='GET'&&path==='/state')",start); + const health=worker.slice(start,end); + assert.match(health,/used_neurons/); + assert.match(health,/prompt_tokens/); + assert.match(health,/completion_tokens/); + assert.match(health,/soft_limit/); + assert.match(health,/high_priority_limit/); + assert.match(health,/hard_limit/); + assert.match(health,/model_rate_id/); + assert.doesNotMatch(health,/dailyLimit|prompt:|context:|strategy:/); +}); + test('Workers AI selects cognition through the fair novelty scheduler',()=>{ const start=worker.indexOf('async processCognition'); const end=worker.indexOf('websocketMeta',start); @@ -114,7 +130,8 @@ test('Workers AI emits compact persistent strategies instead of short action scr const askStart=worker.indexOf('async function askAI'); const askEnd=worker.indexOf('async function withTimeout',askStart); const ask=worker.slice(askStart,askEnd); - assert.match(ask,/max_completion_tokens:\s*200/); + assert.match(worker,/const MAX_COMPLETION_TOKENS=200/); + assert.match(ask,/max_completion_tokens:MAX_COMPLETION_TOKENS/); assert.match(ask,/"intent"/); assert.match(ask,/"actionBias"/); assert.match(ask,/usage/); diff --git a/worker/src/index.js b/worker/src/index.js index eea14a9..20c05fa 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -27,11 +27,20 @@ import { nextSnapshotSlot, nextUtcDayStart, } from './persistence.js'; +import { + MODEL_NEURON_RATES, + NEURON_BUDGET_LIMITS, + ensureNeuronBudget, + estimateReservation, + neuronCapacity, + reconcileNeurons, + reserveNeurons, +} from './neuron-governor.js'; const MODEL='@cf/zai-org/glm-4.7-flash'; const ALARM_MS=60_000; const PERSIST_INTERVAL_WORLD_MINUTES=60; -const AI_CALLS_PER_REAL_DAY=200; +const MAX_COMPLETION_TOKENS=200; const AI_RETRY_COOLDOWN_MS=60_000; const AI_CALL_TIMEOUT_MS=3_000; const CHECKPOINT_WORLD_MINUTES=60; @@ -52,19 +61,10 @@ function parseJsonText(value){ function utcDay(ms=Date.now()){return new Date(ms).toISOString().slice(0,10);} function ensureRuntime(world){ world.runtime??={}; - world.runtime.aiBudget??={day:utcDay(),calls:0,lastExhaustedDay:null,lastFailureRealMs:0}; + ensureNeuronBudget(world); world.runtime.lastSealWorldMinute??=-CHECKPOINT_WORLD_MINUTES; return world.runtime; } -function resetDailyBudget(world){ - const runtime=ensureRuntime(world),day=utcDay(); - if(runtime.aiBudget.day!==day){runtime.aiBudget={day,calls:0,lastExhaustedDay:null,lastFailureRealMs:0};} - return runtime.aiBudget; -} -function configuredDailyBudget(env){ - const raw=Number(env.AI_CALLS_PER_REAL_DAY||AI_CALLS_PER_REAL_DAY); - return Number.isFinite(raw)?Math.max(0,Math.floor(raw)):AI_CALLS_PER_REAL_DAY; -} async function sha256Hex(text){ const bytes=new TextEncoder().encode(text); const digest=await crypto.subtle.digest('SHA-256',bytes); @@ -73,7 +73,7 @@ async function sha256Hex(text){ async function askAI(env,context){ if(!env.AI?.run)throw new Error('ai_unavailable'); const system=`You are the private strategic cognition of one CYMONIA citizen. Use ONLY opaque concept IDs, citizen IDs, evidence, memories and entities present in the supplied context. Never invent Earth knowledge or concrete actions. Return strict compact JSON only: {"focus":"known concept id or null","intent":"explore|understand|share|cooperate|care|construct|adapt","actionBias":["supported action type"],"partnerIds":["known citizen id"],"successSignals":["known concept id"],"horizonMinutes":4320,"confidence":0.7}. The strategy should guide several world-days of local autonomous behavior. Prefer novelty or reinterpretation; use adapt when evidence is insufficient.`; - const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:system},{role:'user',content:JSON.stringify(context)}],max_completion_tokens:200,temperature:.45}); + const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:system},{role:'user',content:JSON.stringify(context)}],max_completion_tokens:MAX_COMPLETION_TOKENS,temperature:.45}); const text=out?.response??out?.result?.response??out?.result??out; const usage=out?.usage??out?.result?.usage??out?.result?.response?.usage??null; return {strategy:sanitizeAIStrategy(parseJsonText(text)),usage}; @@ -314,35 +314,49 @@ export class SovereignWorld { } async processCognition(limit){ if(!this.env.AI?.run)return false; - const budget=resetDailyBudget(this.world),dailyLimit=configuredDailyBudget(this.env),runtime=ensureRuntime(this.world); + const budget=ensureNeuronBudget(this.world),model=this.env.BRAIN_MODEL||MODEL; if(Date.now()-Number(budget.lastFailureRealMs||0)=dailyLimit){ - if(budget.lastExhaustedDay!==budget.day){ - budget.lastExhaustedDay=budget.day; - appendEvent(this.world,'COGNITION_DEFERRED','world',{reason:'ai_budget_exhausted',dailyLimit},[],this.world.clock.worldMinute); - } - return false; - } - let used=0; - while(used0)phases.push('standard'); + if(neuronCapacity(budget,'priority')>0)phases.push('priority'); + if(neuronCapacity(budget,'emergency')>0)phases.push('emergency'); + const rejected=[]; + let used=0,exhaustedReason=phases.length?null:'neuron_hard_budget_exhausted'; + for(const phase of phases){ + if(used>=limit)break; + const item=takeCognitionCandidate(this.world,this.world.clock.worldMinute,phase); + if(!item)continue; const c=this.world.citizens.find(x=>x.id===item.citizenId&&x.alive); if(!c)continue; - budget.calls++; + const context=buildCognitiveContext(this.world,c,this.world.clock.worldMinute); + const reserveClass=item.reserve==='emergency'?'emergency':item.reserve==='priority'?'priority':'normal'; + const estimate=estimateReservation(model,JSON.stringify(context),MAX_COMPLETION_TOKENS); + const admission=reserveNeurons(budget,estimate,reserveClass); + if(!admission.ok){ + exhaustedReason=admission.reason; + rejected.push({item,c}); + continue; + } + let accounting=null; try{ - const {strategy,usage}=await withTimeout(askAI(this.env,buildCognitiveContext(this.world,c,this.world.clock.worldMinute)),AI_CALL_TIMEOUT_MS,'ai_timeout'); + const {strategy,usage}=await withTimeout(askAI(this.env,context),AI_CALL_TIMEOUT_MS,'ai_timeout'); + accounting=reconcileNeurons(budget,admission.reservation,usage,model); acceptAIStrategy(this.world,c.id,strategy,this.world.clock.worldMinute); - appendEvent(this.world,'AI_COGNITION',c.id,{model:this.env.BRAIN_MODEL||MODEL,reason:item.reason,status:'accepted',knowledgeContextCount:c.knowledge.filter(k=>k.active!==false).length,usage:usage?{promptTokens:Number(usage.prompt_tokens||0),completionTokens:Number(usage.completion_tokens||0)}:null},[],this.world.clock.worldMinute); + appendEvent(this.world,'AI_COGNITION',c.id,{model,reason:item.reason,status:'accepted',knowledgeContextCount:c.knowledge.filter(k=>k.active!==false).length,chargedNeurons:accounting.charged,accountingWarning:accounting.warning||null},[],this.world.clock.worldMinute); budget.lastFailureRealMs=0; }catch(error){ + if(!admission.reservation.reconciled)accounting=reconcileNeurons(budget,admission.reservation,null,model); budget.lastFailureRealMs=Date.now(); - appendEvent(this.world,'COGNITION_DEFERRED',c.id,{reason:item.reason,error:String(error?.message||error).slice(0,160)},[],this.world.clock.worldMinute); + appendEvent(this.world,'COGNITION_DEFERRED',c.id,{reason:item.reason,error:String(error?.message||error).slice(0,160),chargedNeurons:accounting?.charged??null},[],this.world.clock.worldMinute); queueCognition(this.world,c,item.reason,Math.max(.2,item.basePriority-.02),this.world.clock.worldMinute,item.eventIds?.at(-1),{retryAfterWorldMinute:this.world.clock.worldMinute+60}); } used++; } - runtime.aiBudget=budget; + for(const {item,c} of rejected)queueCognition(this.world,c,item.reason,item.basePriority,this.world.clock.worldMinute,item.eventIds?.at(-1)); + if(used===0&&exhaustedReason&&budget.lastExhaustedDay!==budget.day){ + budget.lastExhaustedDay=budget.day; + appendEvent(this.world,'COGNITION_DEFERRED','world',{reason:'neuron_budget_exhausted',admissionReason:exhaustedReason,usedNeurons:budget.usedNeurons,reservedNeurons:budget.reservedNeurons},[],this.world.clock.worldMinute); + } return used>0; } websocketMeta(ws){ @@ -394,14 +408,27 @@ export class SovereignWorld { const url=new URL(request.url),path=url.pathname.replace(/^\/world/,'')||'/'; if(request.headers.get('upgrade')==='websocket'&&path==='/stream')return this.webSocket(); if(request.method==='GET'&&path==='/health'){ - const budget=resetDailyBudget(this.world),persistenceBudget=this.readPersistenceBudget(); + const budget=ensureNeuronBudget(this.world),persistenceBudget=this.readPersistenceBudget(),model=this.env.BRAIN_MODEL||MODEL,rate=MODEL_NEURON_RATES[model]||null; return json({ ok:true, service:'cymonia-sovereign-world', version:2, - model:this.env.BRAIN_MODEL||MODEL, + model, ai:Boolean(this.env.AI?.run), - ai_budget:{day:budget.day,calls:budget.calls,limit:configuredDailyBudget(this.env)}, + ai_budget:{ + day:budget.day, + used_neurons:budget.usedNeurons, + reserved_neurons:budget.reservedNeurons, + prompt_tokens:budget.promptTokens, + completion_tokens:budget.completionTokens, + calls:budget.calls, + soft_limit:NEURON_BUDGET_LIMITS.normal, + high_priority_limit:NEURON_BUDGET_LIMITS.priority, + hard_limit:NEURON_BUDGET_LIMITS.emergency, + available:{normal:neuronCapacity(budget,'normal'),priority:neuronCapacity(budget,'priority'),emergency:neuronCapacity(budget,'emergency')}, + model_rate_id:rate?.rateId||null, + last_accounting_warning:budget.lastAccountingWarning, + }, persistence_budget:{ day:persistenceBudget.day, rows_written:persistenceBudget.rowsWritten, diff --git a/worker/src/neuron-governor.js b/worker/src/neuron-governor.js new file mode 100644 index 0000000..22a34e3 --- /dev/null +++ b/worker/src/neuron-governor.js @@ -0,0 +1,110 @@ +export const NEURON_BUDGET_LIMITS=Object.freeze({normal:8000,priority:9000,emergency:9500}); +export const MODEL_NEURON_RATES=Object.freeze({ + '@cf/zai-org/glm-4.7-flash':Object.freeze({ + rateId:'cloudflare-glm-4.7-flash-2026-09', + inputPerMillion:5500, + outputPerMillion:36400, + }), +}); + +const finiteNonNegative=value=>Number.isFinite(Number(value))&&Number(value)>=0; +const precise=value=>Number(Number(value).toFixed(9)); +const utcDay=ms=>new Date(ms).toISOString().slice(0,10); + +function blankBudget(day,lastFailureRealMs=0){ + return {day,usedNeurons:0,reservedNeurons:0,calls:0,promptTokens:0,completionTokens:0,lastFailureRealMs:Number(lastFailureRealMs)||0,lastExhaustedDay:null,lastAccountingWarning:null}; +} + +function normalizeBudget(value,day){ + const budget=value&&value.day===day?value:blankBudget(day); + budget.day=day; + budget.usedNeurons=finiteNonNegative(budget.usedNeurons)?Number(budget.usedNeurons):0; + budget.reservedNeurons=finiteNonNegative(budget.reservedNeurons)?Number(budget.reservedNeurons):0; + budget.calls=finiteNonNegative(budget.calls)?Math.floor(Number(budget.calls)):0; + budget.promptTokens=finiteNonNegative(budget.promptTokens)?Math.floor(Number(budget.promptTokens)):0; + budget.completionTokens=finiteNonNegative(budget.completionTokens)?Math.floor(Number(budget.completionTokens)):0; + budget.lastFailureRealMs=finiteNonNegative(budget.lastFailureRealMs)?Number(budget.lastFailureRealMs):0; + budget.lastExhaustedDay=budget.lastExhaustedDay?String(budget.lastExhaustedDay):null; + budget.lastAccountingWarning=budget.lastAccountingWarning?String(budget.lastAccountingWarning).slice(0,96):null; + return budget; +} + +export function ensureNeuronBudget(world,nowMs=Date.now()){ + world.runtime??={}; + const day=utcDay(nowMs); + let current=world.runtime.neuronBudget; + if(!current){ + const legacy=world.runtime.aiBudget; + current=blankBudget(day,legacy?.lastFailureRealMs); + if(legacy?.day===day)current.calls=Math.max(0,Math.floor(Number(legacy.calls)||0)); + } + world.runtime.neuronBudget=normalizeBudget(current,day); + delete world.runtime.aiBudget; + return world.runtime.neuronBudget; +} + +function modelRate(model,rates=MODEL_NEURON_RATES){ + if(!rates)return null; + if(rates.inputPerMillion!==undefined&&rates.outputPerMillion!==undefined)return rates; + return rates[model]||null; +} + +function neuronCost(promptTokens,completionTokens,rate){ + return precise(promptTokens*Number(rate.inputPerMillion)/1_000_000+completionTokens*Number(rate.outputPerMillion)/1_000_000); +} + +export function estimateReservation(model,context,maxCompletionTokens,rates=MODEL_NEURON_RATES){ + const rate=modelRate(model,rates); + if(!rate||!finiteNonNegative(rate.inputPerMillion)||!finiteNonNegative(rate.outputPerMillion))return Infinity; + const promptTokens=Math.ceil(String(context??'').length/3); + const completionTokens=Math.ceil(Math.max(0,Number(maxCompletionTokens)||0)); + return neuronCost(promptTokens,completionTokens,rate); +} + +export function neuronCapacity(budget,reserveClass='normal'){ + const ceiling=NEURON_BUDGET_LIMITS[reserveClass]; + if(!ceiling)return 0; + const used=finiteNonNegative(budget?.usedNeurons)?Number(budget.usedNeurons):0; + const reserved=finiteNonNegative(budget?.reservedNeurons)?Number(budget.reservedNeurons):0; + return precise(Math.max(0,ceiling-used-reserved)); +} + +export function reserveNeurons(budget,amount,reserveClass='normal'){ + if(!budget||!NEURON_BUDGET_LIMITS[reserveClass])return {ok:false,reason:'neuron_reserve_class_invalid'}; + if(!finiteNonNegative(amount)||!Number.isFinite(Number(amount)))return {ok:false,reason:'neuron_estimate_unavailable'}; + const held=Math.ceil(Number(amount)); + if(held>neuronCapacity(budget,reserveClass))return {ok:false,reason:`neuron_${reserveClass}_budget_exhausted`}; + budget.reservedNeurons=precise((Number(budget.reservedNeurons)||0)+held); + return {ok:true,reservation:{amount:held,reserveClass,reconciled:false}}; +} + +function findUsage(value){ + const candidates=[value,value?.usage,value?.result?.usage,value?.result?.response?.usage,value?.response?.usage]; + for(const candidate of candidates){ + if(!candidate||typeof candidate!=='object')continue; + const prompt=candidate.prompt_tokens??candidate.input_tokens; + const completion=candidate.completion_tokens??candidate.output_tokens; + if(finiteNonNegative(prompt)&&finiteNonNegative(completion))return {promptTokens:Math.ceil(Number(prompt)),completionTokens:Math.ceil(Number(completion))}; + } + return null; +} + +export function reconcileNeurons(budget,reservation,usage,model,rates=MODEL_NEURON_RATES){ + if(!reservation||reservation.reconciled)return {charged:0,warning:'reservation_already_reconciled'}; + reservation.reconciled=true; + budget.reservedNeurons=precise(Math.max(0,(Number(budget.reservedNeurons)||0)-Number(reservation.amount||0))); + const parsed=findUsage(usage),rate=modelRate(model,rates); + let charged,warning=null; + if(parsed&&rate&&finiteNonNegative(rate.inputPerMillion)&&finiteNonNegative(rate.outputPerMillion)){ + charged=neuronCost(parsed.promptTokens,parsed.completionTokens,rate); + budget.promptTokens=(Number(budget.promptTokens)||0)+parsed.promptTokens; + budget.completionTokens=(Number(budget.completionTokens)||0)+parsed.completionTokens; + }else{ + charged=Number(reservation.amount)||0; + warning=parsed?'model_rate_missing_charged_reservation':'usage_missing_charged_reservation'; + } + budget.usedNeurons=precise((Number(budget.usedNeurons)||0)+charged); + budget.calls=Math.max(0,Math.floor(Number(budget.calls)||0))+1; + budget.lastAccountingWarning=warning; + return {charged,warning}; +} From caefac395895a5ee1381c23040c17a84784640c2 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:55:17 +0200 Subject: [PATCH 08/10] feat: prove autonomous emergence end to end --- tests/test_sovereign_action_effects.mjs | 4 +- tests/test_sovereign_emergent_chain.mjs | 46 +++++++++++++++++++++++ tests/test_sovereign_neuron_governor.mjs | 9 +++++ tests/test_sovereign_runtime_contract.mjs | 3 ++ worker/src/index.js | 31 ++++++++------- worker/src/neuron-governor.js | 27 ++++++++++--- world/engine.js | 2 +- 7 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 tests/test_sovereign_emergent_chain.mjs diff --git a/tests/test_sovereign_action_effects.mjs b/tests/test_sovereign_action_effects.mjs index 8643e77..b1cb562 100644 --- a/tests/test_sovereign_action_effects.mjs +++ b/tests/test_sovereign_action_effects.mjs @@ -26,5 +26,7 @@ test('gathering removes matter from a deposit and creates provenance-bound matte startAction(w,c,{type:'GATHER',durationMinutes:1,targetId:d.id,payload:{quantity:2},purpose:'self-directed'},0); advanceWorldTo(w,1000); assert.ok(Math.abs(d.quantity-(q+d.renewPerDay/1440-2))<1e-6); - assert.ok(w.objects.some(o=>o.holderId===c.id&&o.provenance?.depositId===d.id)); + const gathered=w.objects.find(o=>o.holderId===c.id&&o.provenance?.depositId===d.id); + assert.ok(gathered); + assert.ok(c.knownEntityIds.includes(gathered.id)); }); diff --git a/tests/test_sovereign_emergent_chain.mjs b/tests/test_sovereign_emergent_chain.mjs new file mode 100644 index 0000000..879c4ef --- /dev/null +++ b/tests/test_sovereign_emergent_chain.mjs @@ -0,0 +1,46 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createSovereignGenesis} from '../world/genesis.js'; +import {advanceWorldTo} from '../world/engine.js'; + +function runEmergence(){ + const world=createSovereignGenesis({seed:20260921,realEpochMs:0}); + const [first,second]=world.citizens; + world.citizens=[first,second]; + world.cognitionQueue=[]; + world.objects=[]; + world.resourceDeposits=world.resourceDeposits.filter(deposit=>['timber','clay'].includes(deposit.type)); + const timber=world.resourceDeposits.find(deposit=>deposit.type==='timber'); + const clay=world.resourceDeposits.find(deposit=>deposit.type==='clay'); + timber.position={x:50,y:50};clay.position={x:51.5,y:50}; + for(const [index,citizen] of world.citizens.entries()){ + citizen.position={x:50+index*1.5,y:50}; + citizen.activeGoal=null; + citizen.plans=[]; + citizen.currentActionId=null; + citizen.possessions=[]; + citizen.knownEntityIds=[citizen.id]; + citizen.knowledge=[]; + citizen.memories=[]; + citizen.relationships={}; + Object.assign(citizen.psychology,{curiosity:1,noveltySeeking:1,socialDrive:1,riskTolerance:.9,empathy:.9,stress:0,fear:0}); + Object.assign(citizen.body,{hydration:100,calories:100,sleepPressure:0,health:100}); + } + advanceWorldTo(world,1_500_000); + return world; +} + +test('local cognition produces a deterministic discovery-to-building chain without AI',()=>{ + const first=runEmergence(),second=runEmergence(); + const types=new Set(first.ledger.map(event=>event.type)); + assert.ok(types.has('RESOURCE_GATHERED')); + assert.ok(types.has('EXPERIMENT_COMPLETED')); + assert.ok(types.has('SIGNAL_COINED')); + assert.ok(types.has('COMMUNICATION')); + assert.ok(first.citizens.some(citizen=>citizen.knowledge.some(entry=>entry.provenance.some(source=>source.kind==='communication')))); + assert.ok(types.has('CONSTRUCTION_STARTED')); + assert.ok(types.has('BUILDING_COMPLETED')); + assert.equal(first.ledger.filter(event=>event.type==='AI_COGNITION').length,0); + assert.equal(first.ledgerHead,second.ledgerHead); + assert.equal(first.citizens.some(citizen=>citizen.activeGoal?.actionTypes?.includes('BUILD')),false); +}); diff --git a/tests/test_sovereign_neuron_governor.mjs b/tests/test_sovereign_neuron_governor.mjs index e11f7e1..6ee8836 100644 --- a/tests/test_sovereign_neuron_governor.mjs +++ b/tests/test_sovereign_neuron_governor.mjs @@ -5,6 +5,7 @@ import { estimateReservation, neuronCapacity, reconcileNeurons, + resolveNeuronConfig, reserveNeurons, } from '../worker/src/neuron-governor.js'; @@ -83,3 +84,11 @@ test('reservation estimate uses conservative prompt length and full completion c const estimated=estimateReservation(MODEL,'1234567',200); assert.equal(estimated,7.2965); }); + +test('environment overrides can only lower safety ceilings and can update rates',()=>{ + const config=resolveNeuronConfig({AI_NEURON_SOFT_LIMIT:'7000',AI_NEURON_PRIORITY_LIMIT:'8800',AI_NEURON_HARD_LIMIT:'9999',AI_INPUT_NEURONS_PER_MILLION:'6000',AI_OUTPUT_NEURONS_PER_MILLION:'40000'},MODEL); + assert.deepEqual(config.limits,{normal:7000,priority:8800,emergency:9500}); + assert.equal(config.rates.inputPerMillion,6000); + assert.equal(config.rates.outputPerMillion,40000); + assert.match(config.rates.rateId,/environment-override/); +}); diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index c2d5202..bab5730 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; const worker=fs.readFileSync(new URL('../worker/src/index.js',import.meta.url),'utf8'); +const governor=fs.readFileSync(new URL('../worker/src/neuron-governor.js',import.meta.url),'utf8'); const cfg=fs.readFileSync(new URL('../wrangler.world.toml',import.meta.url),'utf8'); test('Durable Object runtime declares single writer, alarms, websocket and AI binding',()=>{ @@ -99,6 +100,8 @@ test('health exposes safe neuron accounting without prompts or strategy text',() assert.match(health,/high_priority_limit/); assert.match(health,/hard_limit/); assert.match(health,/model_rate_id/); + assert.match(governor,/AI_NEURON_SOFT_LIMIT/); + assert.match(governor,/AI_INPUT_NEURONS_PER_MILLION/); assert.doesNotMatch(health,/dailyLimit|prompt:|context:|strategy:/); }); diff --git a/worker/src/index.js b/worker/src/index.js index 20c05fa..b98664d 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -28,12 +28,11 @@ import { nextUtcDayStart, } from './persistence.js'; import { - MODEL_NEURON_RATES, - NEURON_BUDGET_LIMITS, ensureNeuronBudget, estimateReservation, neuronCapacity, reconcileNeurons, + resolveNeuronConfig, reserveNeurons, } from './neuron-governor.js'; @@ -314,12 +313,12 @@ export class SovereignWorld { } async processCognition(limit){ if(!this.env.AI?.run)return false; - const budget=ensureNeuronBudget(this.world),model=this.env.BRAIN_MODEL||MODEL; + const budget=ensureNeuronBudget(this.world),model=this.env.BRAIN_MODEL||MODEL,config=resolveNeuronConfig(this.env,model); if(Date.now()-Number(budget.lastFailureRealMs||0)0)phases.push('standard'); - if(neuronCapacity(budget,'priority')>0)phases.push('priority'); - if(neuronCapacity(budget,'emergency')>0)phases.push('emergency'); + if(neuronCapacity(budget,'normal',config.limits)>0)phases.push('standard'); + if(neuronCapacity(budget,'priority',config.limits)>0)phases.push('priority'); + if(neuronCapacity(budget,'emergency',config.limits)>0)phases.push('emergency'); const rejected=[]; let used=0,exhaustedReason=phases.length?null:'neuron_hard_budget_exhausted'; for(const phase of phases){ @@ -330,8 +329,8 @@ export class SovereignWorld { if(!c)continue; const context=buildCognitiveContext(this.world,c,this.world.clock.worldMinute); const reserveClass=item.reserve==='emergency'?'emergency':item.reserve==='priority'?'priority':'normal'; - const estimate=estimateReservation(model,JSON.stringify(context),MAX_COMPLETION_TOKENS); - const admission=reserveNeurons(budget,estimate,reserveClass); + const estimate=estimateReservation(model,JSON.stringify(context),MAX_COMPLETION_TOKENS,config.rates); + const admission=reserveNeurons(budget,estimate,reserveClass,config.limits); if(!admission.ok){ exhaustedReason=admission.reason; rejected.push({item,c}); @@ -340,12 +339,12 @@ export class SovereignWorld { let accounting=null; try{ const {strategy,usage}=await withTimeout(askAI(this.env,context),AI_CALL_TIMEOUT_MS,'ai_timeout'); - accounting=reconcileNeurons(budget,admission.reservation,usage,model); + accounting=reconcileNeurons(budget,admission.reservation,usage,model,config.rates); acceptAIStrategy(this.world,c.id,strategy,this.world.clock.worldMinute); appendEvent(this.world,'AI_COGNITION',c.id,{model,reason:item.reason,status:'accepted',knowledgeContextCount:c.knowledge.filter(k=>k.active!==false).length,chargedNeurons:accounting.charged,accountingWarning:accounting.warning||null},[],this.world.clock.worldMinute); budget.lastFailureRealMs=0; }catch(error){ - if(!admission.reservation.reconciled)accounting=reconcileNeurons(budget,admission.reservation,null,model); + if(!admission.reservation.reconciled)accounting=reconcileNeurons(budget,admission.reservation,null,model,config.rates); budget.lastFailureRealMs=Date.now(); appendEvent(this.world,'COGNITION_DEFERRED',c.id,{reason:item.reason,error:String(error?.message||error).slice(0,160),chargedNeurons:accounting?.charged??null},[],this.world.clock.worldMinute); queueCognition(this.world,c,item.reason,Math.max(.2,item.basePriority-.02),this.world.clock.worldMinute,item.eventIds?.at(-1),{retryAfterWorldMinute:this.world.clock.worldMinute+60}); @@ -408,7 +407,7 @@ export class SovereignWorld { const url=new URL(request.url),path=url.pathname.replace(/^\/world/,'')||'/'; if(request.headers.get('upgrade')==='websocket'&&path==='/stream')return this.webSocket(); if(request.method==='GET'&&path==='/health'){ - const budget=ensureNeuronBudget(this.world),persistenceBudget=this.readPersistenceBudget(),model=this.env.BRAIN_MODEL||MODEL,rate=MODEL_NEURON_RATES[model]||null; + const budget=ensureNeuronBudget(this.world),persistenceBudget=this.readPersistenceBudget(),model=this.env.BRAIN_MODEL||MODEL,config=resolveNeuronConfig(this.env,model); return json({ ok:true, service:'cymonia-sovereign-world', @@ -422,11 +421,11 @@ export class SovereignWorld { prompt_tokens:budget.promptTokens, completion_tokens:budget.completionTokens, calls:budget.calls, - soft_limit:NEURON_BUDGET_LIMITS.normal, - high_priority_limit:NEURON_BUDGET_LIMITS.priority, - hard_limit:NEURON_BUDGET_LIMITS.emergency, - available:{normal:neuronCapacity(budget,'normal'),priority:neuronCapacity(budget,'priority'),emergency:neuronCapacity(budget,'emergency')}, - model_rate_id:rate?.rateId||null, + soft_limit:config.limits.normal, + high_priority_limit:config.limits.priority, + hard_limit:config.limits.emergency, + available:{normal:neuronCapacity(budget,'normal',config.limits),priority:neuronCapacity(budget,'priority',config.limits),emergency:neuronCapacity(budget,'emergency',config.limits)}, + model_rate_id:config.rates?.rateId||null, last_accounting_warning:budget.lastAccountingWarning, }, persistence_budget:{ diff --git a/worker/src/neuron-governor.js b/worker/src/neuron-governor.js index 22a34e3..66366aa 100644 --- a/worker/src/neuron-governor.js +++ b/worker/src/neuron-governor.js @@ -53,6 +53,23 @@ function neuronCost(promptTokens,completionTokens,rate){ return precise(promptTokens*Number(rate.inputPerMillion)/1_000_000+completionTokens*Number(rate.outputPerMillion)/1_000_000); } +function positiveOverride(value,fallback,ceiling=Infinity){ + const parsed=Number(value); + return Number.isFinite(parsed)&&parsed>0?Math.min(parsed,ceiling):fallback; +} + +export function resolveNeuronConfig(env={},model='@cf/zai-org/glm-4.7-flash'){ + const emergency=positiveOverride(env.AI_NEURON_HARD_LIMIT,NEURON_BUDGET_LIMITS.emergency,NEURON_BUDGET_LIMITS.emergency); + const priority=Math.min(emergency,positiveOverride(env.AI_NEURON_PRIORITY_LIMIT,NEURON_BUDGET_LIMITS.priority,NEURON_BUDGET_LIMITS.priority)); + const normal=Math.min(priority,positiveOverride(env.AI_NEURON_SOFT_LIMIT,NEURON_BUDGET_LIMITS.normal,NEURON_BUDGET_LIMITS.normal)); + const base=MODEL_NEURON_RATES[model]||null; + const inputPerMillion=positiveOverride(env.AI_INPUT_NEURONS_PER_MILLION,base?.inputPerMillion??NaN); + const outputPerMillion=positiveOverride(env.AI_OUTPUT_NEURONS_PER_MILLION,base?.outputPerMillion??NaN); + const overridden=env.AI_INPUT_NEURONS_PER_MILLION!==undefined||env.AI_OUTPUT_NEURONS_PER_MILLION!==undefined; + const rates=Number.isFinite(inputPerMillion)&&Number.isFinite(outputPerMillion)?{rateId:overridden?`environment-override:${model}`:base.rateId,inputPerMillion,outputPerMillion}:null; + return {limits:{normal,priority,emergency},rates}; +} + export function estimateReservation(model,context,maxCompletionTokens,rates=MODEL_NEURON_RATES){ const rate=modelRate(model,rates); if(!rate||!finiteNonNegative(rate.inputPerMillion)||!finiteNonNegative(rate.outputPerMillion))return Infinity; @@ -61,19 +78,19 @@ export function estimateReservation(model,context,maxCompletionTokens,rates=MODE return neuronCost(promptTokens,completionTokens,rate); } -export function neuronCapacity(budget,reserveClass='normal'){ - const ceiling=NEURON_BUDGET_LIMITS[reserveClass]; +export function neuronCapacity(budget,reserveClass='normal',limits=NEURON_BUDGET_LIMITS){ + const ceiling=limits?.[reserveClass]; if(!ceiling)return 0; const used=finiteNonNegative(budget?.usedNeurons)?Number(budget.usedNeurons):0; const reserved=finiteNonNegative(budget?.reservedNeurons)?Number(budget.reservedNeurons):0; return precise(Math.max(0,ceiling-used-reserved)); } -export function reserveNeurons(budget,amount,reserveClass='normal'){ - if(!budget||!NEURON_BUDGET_LIMITS[reserveClass])return {ok:false,reason:'neuron_reserve_class_invalid'}; +export function reserveNeurons(budget,amount,reserveClass='normal',limits=NEURON_BUDGET_LIMITS){ + if(!budget||!limits?.[reserveClass])return {ok:false,reason:'neuron_reserve_class_invalid'}; if(!finiteNonNegative(amount)||!Number.isFinite(Number(amount)))return {ok:false,reason:'neuron_estimate_unavailable'}; const held=Math.ceil(Number(amount)); - if(held>neuronCapacity(budget,reserveClass))return {ok:false,reason:`neuron_${reserveClass}_budget_exhausted`}; + if(held>neuronCapacity(budget,reserveClass,limits))return {ok:false,reason:`neuron_${reserveClass}_budget_exhausted`}; budget.reservedNeurons=precise((Number(budget.reservedNeurons)||0)+held); return {ok:true,reservation:{amount:held,reserveClass,reconciled:false}}; } diff --git a/world/engine.js b/world/engine.js index f08feff..5636c4d 100644 --- a/world/engine.js +++ b/world/engine.js @@ -23,7 +23,7 @@ import {queueCognition} from './cognition-queue.js'; function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);} function perceiveLocal(world,citizen,at){const discoveries=[...perceiveResources(world,citizen,at),...perceiveStructures(world,citizen,at)];if(discoveries.length)queueCognition(world,citizen,'discovery',.65,at,discoveries[0]);for(const other of world.citizens){if(other.id!==citizen.id&&other.alive&&dist(citizen.position,other.position)<=8&&!citizen.knownEntityIds.includes(other.id)){citizen.knownEntityIds.push(other.id);queueCognition(world,citizen,'encounter',.5,at,other.id);recordMemory(citizen,{kind:'episodic',content:{encounter:other.id},source:{kind:'observation'},confidence:.9,salience:.45,worldMinute:at});}}} function runReflex(world,citizen,at){if(!citizen.alive||citizen.currentActionId)return;const p=survivalFallback(world,citizen,at)||localDeliberation(world,citizen,at);if(!p)return;try{applyAcceptedPlan(world,citizen,p,at);}catch{if(!citizen.currentActionId)startAction(world,citizen,{type:'OBSERVE',durationMinutes:10,purpose:'orientation'},at);}} -function gather(world,c,a,at){const d=world.resourceDeposits.find(x=>x.id===a.targetId);if(!d)return;const q=Math.max(.1,Math.min(Number(a.payload?.quantity)||1,d.quantity));d.quantity-=q;const o={id:stableId('obj',d.id,c.id,world.objects.length,at),kind:'gathered_material',material:d.type,quantity:q,massPerUnitKg:1,properties:null,holderId:c.id,position:{...c.position},condition:1,provenance:{type:'GATHERED',depositId:d.id,actionId:a.id}};world.objects.push(o);c.possessions.push(o.id);appendEvent(world,'RESOURCE_GATHERED',c.id,{objectId:o.id,depositId:d.id,quantity:q},[a.id],at);} +function gather(world,c,a,at){const d=world.resourceDeposits.find(x=>x.id===a.targetId);if(!d)return;const q=Math.max(.1,Math.min(Number(a.payload?.quantity)||1,d.quantity));d.quantity-=q;const o={id:stableId('obj',d.id,c.id,world.objects.length,at),kind:'gathered_material',material:d.type,quantity:q,massPerUnitKg:1,properties:null,holderId:c.id,position:{...c.position},condition:1,provenance:{type:'GATHERED',depositId:d.id,actionId:a.id}};world.objects.push(o);c.possessions.push(o.id);if(!c.knownEntityIds.includes(o.id))c.knownEntityIds.push(o.id);appendEvent(world,'RESOURCE_GATHERED',c.id,{objectId:o.id,depositId:d.id,quantity:q},[a.id],at);} function consumeFromDeposit(world,c,a,type,amount,restore,at){const d=world.resourceDeposits.find(x=>x.id===a.targetId&&x.type===type);if(!d||d.quantityx.id===ids[i]&&x.holderId===c.id);if(!o)continue;const q=Math.max(.01,Math.min(Number(quantities[i])||o.quantity,o.quantity));consume.push({objectId:o.id,quantity:q});total+=q*o.massPerUnitKg;}if(!consume.length||total<=0)return null;return transformMaterials(world,c,{consume,output:{material:'composite',quantity:total,massPerUnitKg:1,kind:a.payload?.form||'artifact'},process:a.type.toLowerCase()},at);} function build(world,c,a,at){if(a.payload?.projectId)return applyConstructionWork(world,c,a.payload.projectId,a.payload?.workMinutes||a.endsWorldMinute-a.startedWorldMinute,at);const ids=a.payload?.inputObjectIds||[];if(!ids.length||!a.payload?.site||!(a.concepts||[]).length)return null;const p=beginEmpiricalConstruction(world,c,{concepts:a.concepts,inputObjectIds:ids,workMinutes:a.payload.workMinutes||120,form:a.payload.form||'structure',site:a.payload.site},at);a.payload.projectId=p.id;return applyConstructionWork(world,c,p.id,a.endsWorldMinute-a.startedWorldMinute,at);} From c8c9d6e3500019c1c782b78907dace250a138c0b Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 08:55:29 +0200 Subject: [PATCH 09/10] docs: explain autonomous cognition economy --- MANIFEST.sha256 | 59 ++++++++++++++++++------------ README.md | 29 +++++++++++++++ docs/deployment-sovereign-world.md | 45 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index d56a682..f2c799e 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -4,18 +4,21 @@ f08dc03be3ca61a2d79e452bd1ceecdde8cdfb0242592198f032aa6235f46ef7 CHECK.sh 4b83043570a61f90e4913bb70578a3b6c4d593c9cc593ee282829038b0df1cb7 CONTRIBUTING.md 1bec11091d79ebe28d458b3a8ad64f18ed4be7da01a14d3a0ab60dae5c1a3fbe LICENSE -abbccb8ba52c506afb3d8a233c2eb8d54c7c2abbfe6cad35e06e9371c06528c7 README.md +973cc9bda85c7af85302e6aeaefe73804fff9158a35b153c42086cf6dbe99edf README.md 0e4b6bbe1caa147218c65c3be356497803f47379fada2c5feca343c574685612 RELEASE_SOVEREIGN_WORLD_V2.md 5ae739b7e9fa973f0f5406d53ca0eb2e2c5801afeb2f19a6177d0e732fa0933f SECURITY.md f738a695f049a3e91a03ad702297d4f9d483c7da40622aa7ba878036028416c2 docs/RTS_PHYSICS_UPGRADE.md +4fd200df9a9fc14a16eb12445665f4dfd58777fdba8426da4f00ca99b21d8fbe docs/SPINE_OBSERVER_INTEGRATION.md 1811c52485601777a3e6ce287d14548de216fd68167771e9c95b26edd2990527 docs/architecture-v2.md fd5c9e7271dc85b3f08a49b4d86bbb7eeda8262016fdae2db01ad7c07a3f8a6f docs/art/README.md e538e481a2becfc95290e5be0b69f621828da06eac09d1f704fe9fd1045d04c9 docs/art/medieval-observer-desktop.png 05cd3270f0058fb3ee720f8f951475754f343ccff45e770fcad7316d8cbd854f docs/art/medieval-observer-mobile.png -4f334852312dbe033c01c4cd35a964349b198d7915e30876da9a9a033d3072eb docs/deployment-sovereign-world.md +9636f73ec695e2e44383ab47dec1ae18c17f04586466862d0b12a8dc6d6786c9 docs/deployment-sovereign-world.md 636fa33034bb5f7b37c9f85b7c224b0328ebb7986b54812fc760047762f3c5ea docs/superpowers/plans/2026-09-15-sovereign-world-v2.md +6a2a2db9ed5160acc9fda5687e5e9cf3e63343c783b9a72a4a367014c7e02d83 docs/superpowers/plans/2026-09-21-emergent-local-brain.md 8cb9fbacf163c574dc6dd3196c07f70837f22c1a974bade443d557c32442203f docs/superpowers/specs/2026-09-15-sovereign-world-v2-design.md b45d388c901b6a0397d644ddf85c88a71bbdfd8ed91e2c1fbb5e2e26287afa9e docs/superpowers/specs/2026-09-16-cymonia-self-evolving-world-design.md +c2a11467004c7bbf29d41838fe7488fda1ca6660489d4ea5414b83e931291923 docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md 9025958e141161d60f59cb801b9aeae31cafca5538b59c94e0042a1a7bd330ef functions/_lib/auth.js b512e17d600965bac02492f9e0dbbd30b0e68771c1c3ebdda0992a4c498453b4 functions/_lib/http.js b04ee5d5fd478ac6b37de485dae8f826fdd854706a5d2d5323114614ef617773 functions/_lib/identity.js @@ -31,95 +34,105 @@ c4324bf41e8c442893b3bff9f383ed7b897194965509e1616337070ffea1c55b site/assets/wo eaaa31a35853b38e66497b8b0adf0ba146733273468a131f06fbf92aca08260e site/assets/world-citizens.png efd1af2bcdd0939bda2432e6823f09135f6d8345796e8a6f9690886b0b104b05 site/assets/world-terrain.png 27eaa1da60c9ae26c54512c6b986e2d568f5ec9a968d6a46d3d785f9117e5e4e site/cinematic-world.css +ab0d9a1e3aa3efbc09f241543d9a0714a196d997849049e62fb0a22eae73b1c9 site/citizen-animation.js 6c2c82fb1de7d8a0051edaa1e5f5711fc8881865ad8638fa0c41a709dce54696 site/data/sovereign-genesis.json 0670d7be3b270c8e627c5f0089e25ba065c3326f6bcf0f06ce9ae5cf3c5cf64b site/icon.svg f7f882e7c491d86e5bc54c57033192b52cd28da593a70175564f612a187d3a06 site/index.html a31f15ff86c87f9eafb1723fb3fc59186f27fb32a75b00d287fc7efbdeec5b1c site/medieval-art.js 49897b8d946b85c84a4e0f177a3c7773a0c6f9acfef0025d308ccec2ac3a7ef1 site/medieval-world.css +9942697150f9cc3be949b4869f67c678731db377e284602df4faef8bc6c4fb5e site/observer-concepts.js 82197a81454575b5c941ec0c52924c0a53f1d92bf70371e9c16df0912a29dcf0 site/observer-connection.js 3deafe2d73b4c47ac240cc5ab9b335d89b005f98f43f704e27b6b59f218a2e24 site/pixi-observer.js 5edab258856c44d89ea1df51472037697db95909c2d696607ef7c30a503a4838 site/preview.png 5dcf19ad709016f22bf7d1f2e099b94c34a41861c14aa1c9fb6a7d6ef27fab11 site/sovereign-renderer.js 9d1a63c705106bb23e92accd232f509cba550ba47dcab1d1d13681351f457e30 site/sovereign-world.css ca3d652f16c7b2ebd4781d0db1640f51089f738a905285a0bc6df229e41a4b9a site/sovereign-world.js +dab555575cd5a79e1e1be63a155033788d40995f6b5c69b3b82fe87132233009 site/spine-citizen-adapter.js aaa5a63cab5926e0355e8257bd562ec2db4c8890904ef1894767063925ceadf1 site/terrain-model.js c98fca62534d141f285a6c92bfd6257c26d462a3daff72672a4d76f2399d1f21 site/transient-physics.js d179b80fb34aae0db437dfa72c1a6e6704209a8119b8d991ad49321d8584da8f tests/browser-art.mjs 14b119379565532c30bac0c79bd6857e523bd3ad4d3bc623261d5e206fc329e4 tests/browser-replay.mjs d038c841e9fa2d446f5cd906a01615acb5f613da562a403768fa05c0104b2f2e tests/browser-sovereign.mjs -eb6519962713ab81aef9db969a212de7c67ebf6dcc2b56796cdfe50457feba60 tests/test_sovereign_action_effects.mjs +cece43487e1cd420bba8d44016e6f8cd00cbb05a0ba66ac479cfad42eac95d78 tests/test_sovereign_action_effects.mjs 0da53afb9207fd45025b4958f8b3299529663cecd3a52e2307b67871e56488c9 tests/test_sovereign_actions.mjs +fd6d0ef89cc0fffbe625281afdefcff8548b159f6d1d28619b31f4c960887422 tests/test_sovereign_affordance_planner.mjs a48bca09b29b3c6760aeb254ab74ac3d68176dfc4c7b62d122fd2e02f75a6966 tests/test_sovereign_art.mjs 3d27f2f8f15612fd09408e5eb6f1e8c71b2157586798043420d8732f36facb66 tests/test_sovereign_artifacts.mjs 3aa402c385f47ed538694ad67edae9a5b635cc3b630e1df98f6215d6fecb962b tests/test_sovereign_avatar.mjs +f0d644436e621ebd5b6e28cb8af3673b6884fbb487725e5816ef1dc08291d7b8 tests/test_sovereign_avatar_observer_upgrade.mjs f6e9631f43ed140b0fba56877f2fa56d07e3c54bcc90c52946c29b470fde013b tests/test_sovereign_beliefs_language.mjs b2039517acb4a2905cb21e4721976540dce1b19913708cb7d9cc0ca47d0b2c28 tests/test_sovereign_biology.mjs -d51e9bdbaf3511c20513ffcacea4505dce2780e18bc729c3b43c9ff23b8d78d9 tests/test_sovereign_cognition.mjs +98d88f126a1bca9443485b2cbc2f4614dbc975f9f96d0dfb72ed37465e2c3e56 tests/test_sovereign_citizen_animation_upgrade.mjs +6b2c305829de22836ed2f2c53045c9e8cd15f2dcc5399cc7b02d16f7b235415b tests/test_sovereign_cognition.mjs +415105e34c5ebae6e0fe4dd0455e156a232b0e2c476d73844e4d3d12449aad71 tests/test_sovereign_cognition_scheduler.mjs +75b6d83c51e607206b4073ecebb0103fbecc6349689d13477980f0b4ffd50d0c tests/test_sovereign_cognition_state.mjs 2d64a32a4e5a8375c7558b3859cc5636af13b0866204506bde8c3455092f7f0a tests/test_sovereign_conservation.mjs a3c0d63790f4f68fa4c446a7c804594437a2a2a26795993af364929204464dcf tests/test_sovereign_construction_physics_upgrade.mjs b0dc5d5efd760c06b1ca02e84741e57ac624dd4023d866cb9011509bea1733ef tests/test_sovereign_delivery.mjs +6522f0cb75112abaa32d985c66f1c9f9c8d9f14ea24593bae296212d7c787221 tests/test_sovereign_emergent_chain.mjs e5afe8904c331247027c2f1e2d172da38df719c9338605c6acc6d32d5892af03 tests/test_sovereign_engine.mjs -7f6e8497bd98f18188f7920a24d1be6055536c2b6cac8b6a3f195a1debf9084b tests/test_sovereign_engine_patch_upgrade.mjs +591c8c3a83f076b30916de45ff4ffca943ef900124bb98163b8bd4874ca928f9 tests/test_sovereign_engine_patch_upgrade.mjs 85f9632ace9295b2e95b5cbad5d7e50896ce92dc8fc40b61dd2d14fa02b7de54 tests/test_sovereign_epistemics.mjs a71ce1ffa138b92138755cc64eb828f2e145e78c4b369b1eda2911078700a003 tests/test_sovereign_exposure_upgrade.mjs 831ecc901261fe003f15b3f2a2ab2f2a41273bb1fcbb14fa2377d99d1efe8899 tests/test_sovereign_genesis.mjs e0d0c6bcba695b34d69aa3c4ceeae502a80b3dc61843ef12f3cf555e883d5a66 tests/test_sovereign_history.mjs ca70bc968311b97bf9676c762c25a41c5d682d7753d32ea1ecd0108b5eea6e78 tests/test_sovereign_impact_physics_upgrade.mjs 3ebfbc281cbe8407502567832060d1c7767d6337ad94957416590710475ad08c tests/test_sovereign_language.mjs +1219637898a43e4d623897ea15f1ff495554b4b6f2279e6e2466eba072e7b59e tests/test_sovereign_ledger_compaction.mjs b69ab8b0976018cc7e66ca363bb46822cfaa000a5455b0ea738864d9aef67cac tests/test_sovereign_local_deliberation.mjs 703462b8f7462e2c31f530d888c6dd200f96da25e8b5332703c89fd26a935e77 tests/test_sovereign_mass_closure.mjs 85a614caa659785b2408e8e76ab409898d9b5f317bd2d83d893e054061e8f4a9 tests/test_sovereign_material_transform.mjs +2f40d80c718ea215d8c06b4bbceee129c24b81f941eb6507cd29840ffb1da37f tests/test_sovereign_neuron_governor.mjs d4d912fbec5d674a16c2c12242332335924405ebc6bdf080c904d32e08d15396 tests/test_sovereign_observer_connection_upgrade.mjs 6cad1ecae06fba49dca0b776cb7379fd42f5f317e9f17be408dba561c2253332 tests/test_sovereign_observer_emergence.mjs +164c56738f05469e36d0cb3c913656154381bda6a177505d5d2f06d87d1665ad tests/test_sovereign_observer_readability_upgrade.mjs 66b0ddd0c949b827eb5b909818c34024cb7400a1f23e72e1b32e4b122c1961a8 tests/test_sovereign_persistence.mjs +279876578ed24eade882ad165e12f0952f1ac985d4b22065f24067dd720c886e tests/test_sovereign_persistence_budget.mjs 7946021337e718e0af7f375eb20cece0a93ddddf60fb32e1a03d9f19fd623c58 tests/test_sovereign_physics_upgrade.mjs -6c6a6597dc59ad34047df05d2d92e0b920176b63fcb7a1bf448cf8808e20ef6a tests/test_sovereign_pixi_matter_upgrade.mjs +adbb5aa1a3338376a41c12645a5b02a24fcfbbe2808e7b3ab5f056c7e19fd26b tests/test_sovereign_pixi_matter_upgrade.mjs 577fde3e8c1a96f34fc6e7f9f2c6de575e9c70aafa7ad26156b41b43183b5511 tests/test_sovereign_premium_shell_upgrade.mjs 64e53224cc3adeed7d8d686f8d7a9b06bcff0fc0174e82d0dc53889991f21ac4 tests/test_sovereign_relationship_effects.mjs 583706bc944261a63b317d75ad666604155d3a286ae99ad21b13d2321d7d7341 tests/test_sovereign_renderer_contract.mjs 6238a56aa042a20b86e3b555786d34849094c894d8473fd80d6a5255d38a5108 tests/test_sovereign_renderer_motion_upgrade.mjs 3f63626f40bebc0746521643f1fff6bc522e402218965ee327b8725b6e50dc94 tests/test_sovereign_reproduction.mjs -f6c7f2623eeeeea2faebe84c30e135c6357403b3c44aee379dc5f8058064ecac tests/test_sovereign_runtime_contract.mjs +6caa9394e2eb76091c9253ff8dc7f3e9049b0112e64cae895deb3596135cc86a tests/test_sovereign_runtime_contract.mjs 6c69813ab0bc4cb1534b82e8a7d06dc323c8a6c0458298e0a0584b7bde4d47e8 tests/test_sovereign_shell.mjs 3addb20df209d6f6ad84ff37adaa7dd67bf2c10adfc4c372559bc32543311415 tests/test_sovereign_shelter_environment.mjs 218e8e718765fa3e19aead2359ab5cec36778c6fda596176d8b016a38843da00 tests/test_sovereign_society.mjs +6bbebe865059f94c75518de6c8d4540e883092805080e5152db8242f95588a25 tests/test_sovereign_strategy.mjs 5357326e16f8493d08ecc0a56bae1a48773c1f2e979f830f99181054e144ce7a tests/test_sovereign_transient_physics_upgrade.mjs -0fb98d54388b831fdf91bf0ac02de518e1c0b65b176e8f305801e816bd858fff worker/src/index.js +eaeba5dbf791929880a0f1ef87334aa87d32910c6d97dbe6a81c5c8c128cae67 worker/src/index.js +4b00db1fd2286f8d9dd4b522a9eb1111bd24b4e1befc705b5d6c18b45a4b3c0b worker/src/neuron-governor.js +e56357ae087309b86d670288998026568373b7268778157b38612fb38d81eba7 worker/src/persistence.js 2eac2b46fb9337eb62b4b716be26eb11f92359c58e6c6685039f3506346a09fb world/actions.js +1717b8df1d08631e472577d0f4d0fd53925d3a4e5c382c6e86c018f5272d5067 world/affordances.js 79cdf84d53e19cb1500f2a782aad657d49281005c7809e06bfcf190f7411f294 world/artifacts.js 251ef01a7616cef68bba68b1bc81ac62d30b0bd1685a8beb9784c4ba55fa469b world/beliefs.js 93bf3acd637a2851b6acf89655f26743b5b5d29a52130bede8ceeed01703b778 world/biology.js 395a7995698994e5ed007f50a5e625ceef117f5506ba1c1421d31d60bf63fc5a world/clock.js -fcb18f6cbd21ce838ec43a3307466a5ff6cd9c7279db7b25514f3afe0ec88e79 world/cognition.js +cd7a7faa5ea4f4e790d76d89b7d0a1e0783eeb1a81ed0e0248d7ec090787cc1a world/cognition-queue.js +5fa6282a4070ee3507d75928cc524cead401df2f15925da1b01fe0b58af6c4f4 world/cognition-state.js +a0eb700e098ce6abc2177ce346e5f39de18b2c801bc8af86a99d61f522904adf world/cognition.js a1a6c600a884801a45fe446aed7e992dedca6a05a99fac175050c4eb12d8b5fe world/constants.js f1ab5bb183f1fdb33a6e4ad0c1ac53bc5ac07e57a247659ad3002341b721c4a8 world/designs.js 3bb66b15a8796801310e798ea0e12961ae0744536d7048142e2d452a0b64e18b world/disease.js -2e0a95cb98f4528674298fa2bcd69495c29da8ac5dc40854345a9874b1ae4e47 world/engine.js +d6851ab34a7a197acbac1ea0b99831faf2fdb867fee47ab1e0b0c3af849d831e world/engine.js ac99b325f527a31bf06df73bbd62c65f9076363d2a3fd53211b323157623942e world/environment.js af7f4895306b7bcf4f00e7404d0b1376f970f160f64ec687b605717ae6fa5783 world/epistemics.js -232a8a73ae65d8a1358328aec5c62fa0601d2139b4edd6a4d05ff40ea5cc58b8 world/genesis.js +d7b7cef8b5466204b876f7649f2a80346bfea983dce6a34120aa45243226fefc world/genesis.js 7f8a07c4dd491f6eee3baf10ea55c4dad637f47ebf68d9be89f62c1766cfc658 world/genetics.js 9a81044a1f2cb7b45c20aaed5af4066a247ef7fbd4005fa68e99cbeddde13260 world/impact.js -8df29eb53ea1136aa1319910e394838bfdc8592e4d3fc6ee6cb82a57d478cf11 world/index.js +4b17ec1013f45cb1ecf8982f5809caf108437722e400c5ba82c8d258e2cbf9bb world/index.js 63f4750e0eb337f625741a3e1ff11679bca2ec6474d8dc29a221554361c472ce world/language.js 51c03174999c83c2d6e9e72f98d0cde8f767953fa6fe60c9df6f57de0694a1af world/ledger.js e3cd176c6ff5bc16e7f387309f7a1b9f1857ecac0e9e0cc6d9c4daa37adea6fa world/materials.js fbef3330108ff1485174a9577eeef409245c73351a485b91dbf3b54168066374 world/memory.js 0e5e83b98cc2d43df3e29e3504fcd85eb9c6aeecc36121f21198f4908a8c6777 world/observer.js 610623a0e181fc52f62af637083fb7f0d34c5a0c09f26574dee1ea4a65a1eb17 world/perception.js -a194417944ce2001b5ce5fa88f56759671ff80e7566607f4095f687ddc6bd057 world/reproduction.js +f4bcea1137f81635c8c83eefeb6c0812a3505bbd80d620863329126db4d2ce5f world/reproduction.js e5ee59a711be61616602326a70dfa597e7cf124b4c1ae09ad36fce2ca4ec5e8b world/rng.js 5cb722696dccf4875645c6af4ffe868745d525510fb224f9e85bbabb3cca40cf world/society.js +a380f9dcd4a7ade231f7817c244efd9d0b55d9ddd86c2053bfa5e3a6c358ad6c world/strategy.js fba432ea0721d0c250d7b91aa9df1c914219740354dcd941f92ea65354d00a96 world/terrain.js 7ac83852beeae3be027e5fca7474c07edc55d0f98a063f2a2594deb6426d4b0c wrangler.toml 8763f37e90f1dd214c35bd44e821d09fd84b5b5945e9087367ef83f61572af5b wrangler.world.toml -e56357ae087309b86d670288998026568373b7268778157b38612fb38d81eba7 worker/src/persistence.js -279876578ed24eade882ad165e12f0952f1ac985d4b22065f24067dd720c886e tests/test_sovereign_persistence_budget.mjs -1219637898a43e4d623897ea15f1ff495554b4b6f2279e6e2466eba072e7b59e tests/test_sovereign_ledger_compaction.mjs -9942697150f9cc3be949b4869f67c678731db377e284602df4faef8bc6c4fb5e site/observer-concepts.js -ab0d9a1e3aa3efbc09f241543d9a0714a196d997849049e62fb0a22eae73b1c9 site/citizen-animation.js -dab555575cd5a79e1e1be63a155033788d40995f6b5c69b3b82fe87132233009 site/spine-citizen-adapter.js -4fd200df9a9fc14a16eb12445665f4dfd58777fdba8426da4f00ca99b21d8fbe docs/SPINE_OBSERVER_INTEGRATION.md -164c56738f05469e36d0cb3c913656154381bda6a177505d5d2f06d87d1665ad tests/test_sovereign_observer_readability_upgrade.mjs -98d88f126a1bca9443485b2cbc2f4614dbc975f9f96d0dfb72ed37465e2c3e56 tests/test_sovereign_citizen_animation_upgrade.mjs -f0d644436e621ebd5b6e28cb8af3673b6884fbb487725e5816ef1dc08291d7b8 tests/test_sovereign_avatar_observer_upgrade.mjs diff --git a/README.md b/README.md index 7a98dfa..3e91d2b 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,38 @@ The product shell is a full-screen isometric strategy world with animated terrai - **D1:** GitHub identity and session records for human-linked avatars. - **GitHub Actions:** CI and deployment only; it is not the world heartbeat. +## Autonomous cognition economy + +Citizens continue to act when Workers AI is unavailable. Cognition is split into four layers: + +1. **Tier 0 — Reflex:** urgent hydration, nutrition, sleep and environmental safety. +2. **Tier 1 — Local Brain:** a deterministic affordance planner scores exploration, gathering, + experimentation, transformation, communication, teaching, care, transfer, cooperation, + construction and rest from the citizen's own evidence. +3. **Tier 2 — Adaptive planning:** bounded outcome memory and a persistent strategy alter later + choices without bypassing the physical or epistemic validators. +4. **Tier 3 — AI cognition:** Workers AI is used only for novel or urgent reinterpretation. It + returns a compact multi-day strategy, never a minute-by-minute action script. + +The novelty queue merges repeated events and uses cognition debt so a few Citizens cannot +monopolize AI. One accepted strategy can guide many days of local action. Concrete outcomes remain +deterministic: resource discovery can lead to gathering, property experiments, coined signals, +shared knowledge, material transformation and construction without an AI call or a rule saying +what society must build. + +Workers AI admission is governed by measured neurons rather than a call-count limit. The default +daily ceilings are 8,000 neurons for normal novelty, 9,000 including high-priority events, and +9,500 including human/emergency work, preserving 500 neurons of the documented free allocation as +headroom. Actual `usage.prompt_tokens` and `usage.completion_tokens` replace the reservation after +each response; missing usage is charged conservatively rather than treated as free. + ## Repository map - `world/` — causal kernel, biology, cognition, epistemics, language, society and artifacts. +- `world/affordances.js` — deterministic Local Brain candidate generation and scoring. +- `world/cognition-queue.js` — mergeable novelty queue and fair debt-aware scheduler. - `worker/src/index.js` — canonical Durable Object world. +- `worker/src/neuron-governor.js` — Workers AI reservation and measured neuron accounting. - `functions/api/v2/` — state, history, WHY, avatar, intent and stream facade. - `functions/api/auth/` — GitHub OAuth and session bridge. - `functions/_lib/identity.js` — identity-only D1 store. @@ -34,6 +62,7 @@ The product shell is a full-screen isometric strategy world with animated terrai - `scripts/build_sovereign_genesis.mjs` — reproducible fallback builder. - `tests/test_sovereign_*.mjs` — world invariants and delivery contracts. - `docs/superpowers/specs/2026-09-15-sovereign-world-v2-design.md` — design specification. +- `docs/superpowers/specs/2026-09-21-emergent-local-brain-design.md` — cognition economy specification. ## Verify locally diff --git a/docs/deployment-sovereign-world.md b/docs/deployment-sovereign-world.md index db9051c..8ab09fa 100644 --- a/docs/deployment-sovereign-world.md +++ b/docs/deployment-sovereign-world.md @@ -51,3 +51,48 @@ Snapshot persistence uses a 40,000-row soft daily budget and a 60,000-row emerge Observer streaming uses the Durable Objects WebSocket Hibernation API. Active sockets are enumerated from `ctx.getWebSockets()` and carry serialized connection metadata so they remain usable after object eviction. A WebSocket-only outage does not mark the canonical world degraded while REST polling continues to return valid state; the stream reconnects independently with exponential backoff. Cloudflare observability emits `CYMONIA_WS_CLOSE`, `CYMONIA_WS_ERROR`, and `CYMONIA_WS_SEND_FAILED` events for stream diagnostics without persisting additional rows. + +## Workers AI neuron governor + +The Worker uses `@cf/zai-org/glm-4.7-flash` unless `BRAIN_MODEL` is overridden. AI produces a +persistent strategy with `max_completion_tokens: 200`; deterministic cognition executes the +concrete actions. There is no fixed calls-per-day admission limit. + +Defaults current on 2026-09-21 are: + +- 8,000 neurons for normal novelty; +- 9,000 neurons including the high-priority reserve; +- 9,500 neurons including human direction and emergency work; +- 5,500 neurons per million GLM-4.7-Flash input tokens; +- 36,400 neurons per million GLM-4.7-Flash output tokens. + +Cloudflare's free allocation is 10,000 neurons per day and resets at 00:00 UTC. CYMONIA keeps the +last 500 unallocated. Before a call, the governor reserves a conservative prompt estimate plus the +full 200-token output cap. It then reconciles the reservation with +`usage.prompt_tokens` and `usage.completion_tokens`. A missing, negative or malformed usage object +charges the full reservation and records an accounting warning; it never produces a zero-cost +call. An unknown model is fail-closed unless explicit rates are supplied. + +These optional Worker variables are supported: + +| Variable | Purpose | Safety behavior | +| --- | --- | --- | +| `AI_NEURON_SOFT_LIMIT` | Normal-work ceiling | Can lower, but not raise, 8,000 | +| `AI_NEURON_PRIORITY_LIMIT` | High-priority ceiling | Can lower, but not raise, 9,000 | +| `AI_NEURON_HARD_LIMIT` | Human/emergency ceiling | Can lower, but not raise, 9,500 | +| `AI_INPUT_NEURONS_PER_MILLION` | Model input rate | Positive numeric override | +| `AI_OUTPUT_NEURONS_PER_MILLION` | Model output rate | Positive numeric override | + +For a custom `BRAIN_MODEL`, set both rate variables. A partial rate override can reuse the other +known rate only when the model already exists in the built-in table. + +`GET /api/v2/health` exposes only safe accounting metadata under `ai_budget`: UTC day, +`used_neurons`, `reserved_neurons`, input/output token totals, call count, all three ceilings, +capacity by reserve class, `model_rate_id`, and `last_accounting_warning`. It never returns prompts, +memories, private directions or strategy content. + +Cloudflare references for the dated assumptions and cache-compatible prompt layout: + +- [Workers AI pricing and neurons](https://developers.cloudflare.com/workers-ai/platform/pricing/) +- [GLM-4.7-Flash model](https://developers.cloudflare.com/workers-ai/models/glm-4.7-flash/) +- [Workers AI prompt caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) From 216f8bfbf137e17ac63d1fabd3f5b6263611912e Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 21 Sep 2026 09:00:24 +0200 Subject: [PATCH 10/10] fix: harden cognition accounting and artifact continuity --- MANIFEST.sha256 | 8 ++++---- tests/test_sovereign_material_transform.mjs | 2 ++ tests/test_sovereign_runtime_contract.mjs | 21 +++++++++++++++++++-- worker/src/index.js | 7 ++++--- world/materials.js | 2 +- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index f2c799e..4ec5fba 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -81,7 +81,7 @@ ca70bc968311b97bf9676c762c25a41c5d682d7753d32ea1ecd0108b5eea6e78 tests/test_sov 1219637898a43e4d623897ea15f1ff495554b4b6f2279e6e2466eba072e7b59e tests/test_sovereign_ledger_compaction.mjs b69ab8b0976018cc7e66ca363bb46822cfaa000a5455b0ea738864d9aef67cac tests/test_sovereign_local_deliberation.mjs 703462b8f7462e2c31f530d888c6dd200f96da25e8b5332703c89fd26a935e77 tests/test_sovereign_mass_closure.mjs -85a614caa659785b2408e8e76ab409898d9b5f317bd2d83d893e054061e8f4a9 tests/test_sovereign_material_transform.mjs +24b63aaacf6f701860597e7045141849fb5e9944da64329d41e561ab2e7681ea tests/test_sovereign_material_transform.mjs 2f40d80c718ea215d8c06b4bbceee129c24b81f941eb6507cd29840ffb1da37f tests/test_sovereign_neuron_governor.mjs d4d912fbec5d674a16c2c12242332335924405ebc6bdf080c904d32e08d15396 tests/test_sovereign_observer_connection_upgrade.mjs 6cad1ecae06fba49dca0b776cb7379fd42f5f317e9f17be408dba561c2253332 tests/test_sovereign_observer_emergence.mjs @@ -95,13 +95,13 @@ adbb5aa1a3338376a41c12645a5b02a24fcfbbe2808e7b3ab5f056c7e19fd26b tests/test_sov 583706bc944261a63b317d75ad666604155d3a286ae99ad21b13d2321d7d7341 tests/test_sovereign_renderer_contract.mjs 6238a56aa042a20b86e3b555786d34849094c894d8473fd80d6a5255d38a5108 tests/test_sovereign_renderer_motion_upgrade.mjs 3f63626f40bebc0746521643f1fff6bc522e402218965ee327b8725b6e50dc94 tests/test_sovereign_reproduction.mjs -6caa9394e2eb76091c9253ff8dc7f3e9049b0112e64cae895deb3596135cc86a tests/test_sovereign_runtime_contract.mjs +f98169e1dcffc3f599a090d4b1f9ab26379f00e13d3b9f7dcb4db9f467134b46 tests/test_sovereign_runtime_contract.mjs 6c69813ab0bc4cb1534b82e8a7d06dc323c8a6c0458298e0a0584b7bde4d47e8 tests/test_sovereign_shell.mjs 3addb20df209d6f6ad84ff37adaa7dd67bf2c10adfc4c372559bc32543311415 tests/test_sovereign_shelter_environment.mjs 218e8e718765fa3e19aead2359ab5cec36778c6fda596176d8b016a38843da00 tests/test_sovereign_society.mjs 6bbebe865059f94c75518de6c8d4540e883092805080e5152db8242f95588a25 tests/test_sovereign_strategy.mjs 5357326e16f8493d08ecc0a56bae1a48773c1f2e979f830f99181054e144ce7a tests/test_sovereign_transient_physics_upgrade.mjs -eaeba5dbf791929880a0f1ef87334aa87d32910c6d97dbe6a81c5c8c128cae67 worker/src/index.js +7cdd1568e98eb9ecf4902ff875ba5376e968651e8d6b07ef6ff6d27080fc9dbe worker/src/index.js 4b00db1fd2286f8d9dd4b522a9eb1111bd24b4e1befc705b5d6c18b45a4b3c0b worker/src/neuron-governor.js e56357ae087309b86d670288998026568373b7268778157b38612fb38d81eba7 worker/src/persistence.js 2eac2b46fb9337eb62b4b716be26eb11f92359c58e6c6685039f3506346a09fb world/actions.js @@ -125,7 +125,7 @@ d7b7cef8b5466204b876f7649f2a80346bfea983dce6a34120aa45243226fefc world/genesis. 4b17ec1013f45cb1ecf8982f5809caf108437722e400c5ba82c8d258e2cbf9bb world/index.js 63f4750e0eb337f625741a3e1ff11679bca2ec6474d8dc29a221554361c472ce world/language.js 51c03174999c83c2d6e9e72f98d0cde8f767953fa6fe60c9df6f57de0694a1af world/ledger.js -e3cd176c6ff5bc16e7f387309f7a1b9f1857ecac0e9e0cc6d9c4daa37adea6fa world/materials.js +34acdb744667d479c2965ec956df83e3dc36efccae541b630b7bafe15ad94383 world/materials.js fbef3330108ff1485174a9577eeef409245c73351a485b91dbf3b54168066374 world/memory.js 0e5e83b98cc2d43df3e29e3504fcd85eb9c6aeecc36121f21198f4908a8c6777 world/observer.js 610623a0e181fc52f62af637083fb7f0d34c5a0c09f26574dee1ea4a65a1eb17 world/perception.js diff --git a/tests/test_sovereign_material_transform.mjs b/tests/test_sovereign_material_transform.mjs index 49544e4..a13b87e 100644 --- a/tests/test_sovereign_material_transform.mjs +++ b/tests/test_sovereign_material_transform.mjs @@ -11,4 +11,6 @@ test('generic transformations conserve tracked mass',()=>{ const result=transformMaterials(w,c,{inputObjectIds:[o.id],consume:[{objectId:o.id,quantity:10}],output:{material:'timber',quantity:10,massPerUnitKg:1,kind:'shaped_material'},process:'shape'},0); assert.equal(result.provenance.type,'TRANSFORMATION'); assert.equal(totalTrackedMass(w),before); + assert.ok(c.knownEntityIds.includes(result.id)); + assert.ok(c.possessions.includes(result.id)); }); diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index bab5730..db9b996 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -135,9 +135,26 @@ test('Workers AI emits compact persistent strategies instead of short action scr const ask=worker.slice(askStart,askEnd); assert.match(worker,/const MAX_COMPLETION_TOKENS=200/); assert.match(ask,/max_completion_tokens:MAX_COMPLETION_TOKENS/); - assert.match(ask,/"intent"/); - assert.match(ask,/"actionBias"/); + assert.match(worker,/"intent"/); + assert.match(worker,/"actionBias"/); assert.match(ask,/usage/); assert.doesNotMatch(ask,/max_tokens/); assert.doesNotMatch(ask,/"actions"/); }); + +test('neuron reservation includes the same static system prompt sent to Workers AI',()=>{ + const start=worker.indexOf('async processCognition'); + const end=worker.indexOf('websocketMeta',start); + const process=worker.slice(start,end); + assert.match(worker,/const AI_SYSTEM_PROMPT=/); + assert.match(worker,/function serializeAIPrompt/); + assert.match(worker,/serializeAIPrompt\(context\).*content:JSON\.stringify\(context\)/); + assert.match(askAIBlock(),/content:AI_SYSTEM_PROMPT/); + assert.match(process,/estimateReservation\(model,serializeAIPrompt\(context\)/); +}); + +function askAIBlock(){ + const start=worker.indexOf('async function askAI'); + const end=worker.indexOf('async function withTimeout',start); + return worker.slice(start,end); +} diff --git a/worker/src/index.js b/worker/src/index.js index b98664d..bd9507d 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -40,6 +40,7 @@ const MODEL='@cf/zai-org/glm-4.7-flash'; const ALARM_MS=60_000; const PERSIST_INTERVAL_WORLD_MINUTES=60; const MAX_COMPLETION_TOKENS=200; +const AI_SYSTEM_PROMPT=`You are the private strategic cognition of one CYMONIA citizen. Use ONLY opaque concept IDs, citizen IDs, evidence, memories and entities present in the supplied context. Never invent Earth knowledge or concrete actions. Return strict compact JSON only: {"focus":"known concept id or null","intent":"explore|understand|share|cooperate|care|construct|adapt","actionBias":["supported action type"],"partnerIds":["known citizen id"],"successSignals":["known concept id"],"horizonMinutes":4320,"confidence":0.7}. The strategy should guide several world-days of local autonomous behavior. Prefer novelty or reinterpretation; use adapt when evidence is insufficient.`; const AI_RETRY_COOLDOWN_MS=60_000; const AI_CALL_TIMEOUT_MS=3_000; const CHECKPOINT_WORLD_MINUTES=60; @@ -71,12 +72,12 @@ async function sha256Hex(text){ } async function askAI(env,context){ if(!env.AI?.run)throw new Error('ai_unavailable'); - const system=`You are the private strategic cognition of one CYMONIA citizen. Use ONLY opaque concept IDs, citizen IDs, evidence, memories and entities present in the supplied context. Never invent Earth knowledge or concrete actions. Return strict compact JSON only: {"focus":"known concept id or null","intent":"explore|understand|share|cooperate|care|construct|adapt","actionBias":["supported action type"],"partnerIds":["known citizen id"],"successSignals":["known concept id"],"horizonMinutes":4320,"confidence":0.7}. The strategy should guide several world-days of local autonomous behavior. Prefer novelty or reinterpretation; use adapt when evidence is insufficient.`; - const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:system},{role:'user',content:JSON.stringify(context)}],max_completion_tokens:MAX_COMPLETION_TOKENS,temperature:.45}); + const out=await env.AI.run(env.BRAIN_MODEL||MODEL,{messages:[{role:'system',content:AI_SYSTEM_PROMPT},{role:'user',content:JSON.stringify(context)}],max_completion_tokens:MAX_COMPLETION_TOKENS,temperature:.45}); const text=out?.response??out?.result?.response??out?.result??out; const usage=out?.usage??out?.result?.usage??out?.result?.response?.usage??null; return {strategy:sanitizeAIStrategy(parseJsonText(text)),usage}; } +function serializeAIPrompt(context){return JSON.stringify([{role:'system',content:AI_SYSTEM_PROMPT},{role:'user',content:JSON.stringify(context)}]);} async function withTimeout(promise,timeoutMs,label='operation_timeout'){ let timer; try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label)),timeoutMs);})]);} @@ -329,7 +330,7 @@ export class SovereignWorld { if(!c)continue; const context=buildCognitiveContext(this.world,c,this.world.clock.worldMinute); const reserveClass=item.reserve==='emergency'?'emergency':item.reserve==='priority'?'priority':'normal'; - const estimate=estimateReservation(model,JSON.stringify(context),MAX_COMPLETION_TOKENS,config.rates); + const estimate=estimateReservation(model,serializeAIPrompt(context),MAX_COMPLETION_TOKENS,config.rates); const admission=reserveNeurons(budget,estimate,reserveClass,config.limits); if(!admission.ok){ exhaustedReason=admission.reason; diff --git a/world/materials.js b/world/materials.js index 80eab4d..0df7e93 100644 --- a/world/materials.js +++ b/world/materials.js @@ -21,5 +21,5 @@ export function totalTrackedMass(world){ } export function transferObject(world,objectId,toHolderId,at=world.clock.worldMinute){const o=world.objects.find(x=>x.id===objectId);if(!o)throw new Error('object_not_found');const from=o.holderId;o.holderId=toHolderId;appendEvent(world,'OBJECT_TRANSFERRED',from,{objectId,toHolderId},[],at);return o;} export function consumeObjectQuantity(world,objectId,quantity,causeId=null,at=world.clock.worldMinute){const o=world.objects.find(x=>x.id===objectId);if(!o)throw new Error('object_not_found');const q=Math.max(0,Number(quantity));if(o.quantityx.id===item.objectId);if(!o||o.holderId!==citizen.id)throw new Error('transformation_input_unavailable');consumedMass+=Math.max(0,Number(item.quantity))*Math.max(0,Number(o.massPerUnitKg)||1);}const outMass=Math.max(0,Number(spec.output.quantity))*Math.max(0,Number(spec.output.massPerUnitKg)||1);if(Math.abs(consumedMass-outMass)>.000001)throw new Error('mass_not_conserved');const cause=appendEvent(world,'TRANSFORMATION_STARTED',citizen.id,{process:spec.process||'generic',inputObjectIds:(spec.consume||[]).map(x=>x.objectId)},[],at);for(const item of spec.consume||[])consumeObjectQuantity(world,item.objectId,item.quantity,cause.id,at);const inputs=(spec.consume||[]).map(item=>world.objects.find(x=>x.id===item.objectId)).filter(Boolean);const numericKeys=new Set(inputs.flatMap(o=>Object.keys(o.properties||MATERIAL_PROPERTIES[o.material]||{}).filter(k=>typeof (o.properties||MATERIAL_PROPERTIES[o.material]||{})[k]==='number')));const properties={};for(const k of numericKeys){const vals=inputs.map(o=>(o.properties||MATERIAL_PROPERTIES[o.material]||{})[k]).filter(v=>typeof v==='number');if(vals.length)properties[k]=vals.reduce((a,b)=>a+b,0)/vals.length;}const o={id:stableId('obj',citizen.id,world.objects.length,at,spec.process),kind:spec.output.kind||'artifact',material:spec.output.material||'composite',quantity:Number(spec.output.quantity),massPerUnitKg:Number(spec.output.massPerUnitKg)||1,properties,holderId:citizen.id,position:{...citizen.position},condition:1,provenance:{type:'TRANSFORMATION',process:spec.process||'generic',eventId:cause.id,inputObjectIds:(spec.consume||[]).map(x=>x.objectId)}};world.objects.push(o);appendEvent(world,'TRANSFORMATION_COMPLETED',citizen.id,{objectId:o.id,process:spec.process||'generic',massKg:outMass},[cause.id],at);return o;} +export function transformMaterials(world,citizen,spec,at=world.clock.worldMinute){let consumedMass=0;for(const item of spec.consume||[]){const o=world.objects.find(x=>x.id===item.objectId);if(!o||o.holderId!==citizen.id)throw new Error('transformation_input_unavailable');consumedMass+=Math.max(0,Number(item.quantity))*Math.max(0,Number(o.massPerUnitKg)||1);}const outMass=Math.max(0,Number(spec.output.quantity))*Math.max(0,Number(spec.output.massPerUnitKg)||1);if(Math.abs(consumedMass-outMass)>.000001)throw new Error('mass_not_conserved');const cause=appendEvent(world,'TRANSFORMATION_STARTED',citizen.id,{process:spec.process||'generic',inputObjectIds:(spec.consume||[]).map(x=>x.objectId)},[],at);for(const item of spec.consume||[])consumeObjectQuantity(world,item.objectId,item.quantity,cause.id,at);const inputs=(spec.consume||[]).map(item=>world.objects.find(x=>x.id===item.objectId)).filter(Boolean);const numericKeys=new Set(inputs.flatMap(o=>Object.keys(o.properties||MATERIAL_PROPERTIES[o.material]||{}).filter(k=>typeof (o.properties||MATERIAL_PROPERTIES[o.material]||{})[k]==='number')));const properties={};for(const k of numericKeys){const vals=inputs.map(o=>(o.properties||MATERIAL_PROPERTIES[o.material]||{})[k]).filter(v=>typeof v==='number');if(vals.length)properties[k]=vals.reduce((a,b)=>a+b,0)/vals.length;}const o={id:stableId('obj',citizen.id,world.objects.length,at,spec.process),kind:spec.output.kind||'artifact',material:spec.output.material||'composite',quantity:Number(spec.output.quantity),massPerUnitKg:Number(spec.output.massPerUnitKg)||1,properties,holderId:citizen.id,position:{...citizen.position},condition:1,provenance:{type:'TRANSFORMATION',process:spec.process||'generic',eventId:cause.id,inputObjectIds:(spec.consume||[]).map(x=>x.objectId)}};world.objects.push(o);citizen.possessions??=[];if(!citizen.possessions.includes(o.id))citizen.possessions.push(o.id);citizen.knownEntityIds??=[citizen.id];if(!citizen.knownEntityIds.includes(o.id))citizen.knownEntityIds.push(o.id);appendEvent(world,'TRANSFORMATION_COMPLETED',citizen.id,{objectId:o.id,process:spec.process||'generic',massKg:outMass},[cause.id],at);return o;} export function advanceDecomposition(world,deltaMinutes,at=world.clock.worldMinute){const e=world.environment;e.naturalReservoirs??={waterKg:0,biomassKg:0,mineralKg:0};for(const o of world.objects){if(o.provenance?.type!=='CORPSE'||o.quantity<=0)continue;const fraction=Math.min(.02,Math.max(0,deltaMinutes)/525600*.9),q=o.quantity*fraction;if(q<=0)continue;o.quantity-=q;e.naturalReservoirs.biomassKg=(e.naturalReservoirs.biomassKg||0)+q*o.massPerUnitKg;o.condition=Math.max(0,o.condition-fraction);appendEvent(world,'DECOMPOSITION','world',{objectId:o.id,massKg:q*o.massPerUnitKg},[],at);}}