diff --git a/README.md b/README.md index 99cd772..fd2b35e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,164 @@ IX-Sally is not a demonstrated AGI, autonomous deployment platform, certification authority, or substitute for independent engineering and human judgment. + +## v0.7.0 lifelong generalization and representational freedom + +IX-Sally v0.7.0 targets the remaining gap between having individual cognitive mechanisms and +becoming measurably better at learning because of prior experience. CUC-6 adds a bounded +compositional representation language, persistent online meta-learning, active lifelong +knowledge maintenance, surface-independent relational transfer, raw-text outcome grounding, +and multi-goal portfolio coherence. These mechanisms are integrated into +`SallyCognitiveSystem` and persist through canonical snapshot/restore where stateful. + +The strongest CUC-6 representation task cannot be perfectly expressed by the prior one-step +feature language. The best shallow representation reaches 0.8125 training accuracy. Sally +synthesizes the multi-operation expression `((x0*x1)+x2)`, reaches 1.0 training accuracy, and +retains 1.0 accuracy on held-out values outside the training range. + +CUC-6 also runs twelve sealed numeric worlds. During the first two worlds Sally evaluates both +available learning strategies. Its online meta-profile then changes later learning: the +remaining worlds select one strategy from accumulated structural experience instead of +re-running both strategy families, while retaining 1.0 held-out accuracy. The challenge IDs +and channel orderings change; strategy selection is based on measured task fingerprints rather +than a hard-coded world-type switch. + +Additional v0.7 mechanisms include contextual concept splitting when a formerly useful rule +becomes systematically wrong in one regime, structural analogy that transfers a learned +topological role from an industrial-control graph to a software-rendering graph despite +different entity and relation names, empirical token grounding from raw text/outcome pairs, +and dependency-aware attention allocation across multiple goals with premise collapse and +goal abandonment. A 64-generation bounded maintenance/checkpoint exercise verifies that the +meta-learning profile and lifelong store survive exact snapshot/restore. + +These are bounded research results. v0.7 does not establish AGI, consciousness, unrestricted +self-modification, general vision/audio grounding, unlimited open-world agency, or independent +replication. External consequential action remains governed by the inherited human-authority +control plane. See `docs/CUC6_LIFELONG_GENERALIZATION.md` and `VALIDATION_REPORT.md`. + +Run the experiment: + +```text +python -m ix_sally --cuc6-experiment +``` + + +## v0.6.0 recursive cognitive bootstrapping + +IX-Sally v0.6.0 integrates the major bounded mechanisms that remained after v0.5.0 into a +single recursive research path instead of presenting them as disconnected feature claims. +CUC-5 demonstrates a closed cycle where an atomic representation fails, Sally invents and +validates a new representation, promotes the discovery into a reusable opaque semantic +primitive, persists it, authors an internal goal afterward, constructs an action for that +goal, validates the procedure into a reusable tool, detects a structured high-confidence +failure, and invents a second, different representation. + +The release also adds long-horizon replanning, structural cross-domain transfer, +self-directed curriculum selection, persistent lifelong knowledge and ontology +restructuring, intervention-aware causal discovery, numeric raw-signal grounding, active +perception, counterfactual imagination, self-diagnosis, governed measured self-improvement, +goal conflict resolution and abandonment, unknown-unknown detection, adaptive search +budgeting, meta-learning, procedural unfamiliar-world tests, and a nonce-bound blind +evaluator interface for challenge sets supplied by third parties. + +These are bounded experimental mechanisms, not an AGI certification. Raw perception remains +limited to numeric streams, every concrete search is finite, and genuinely independent +evaluation still requires an external evaluator. External consequential action and adoption +of self-improvement proposals remain under the existing human-authority control plane. See +`docs/CUC5_RECURSIVE_COGNITION.md`, `docs/CUC5_VALIDATION_REPORT.md`, and +`VALIDATION_REPORT.md`. + +Run the integrated experiment: + +```text +python -m ix_sally --cuc5-experiment +``` + +## v0.5.0 semantic genesis and open goal authorship + +IX-Sally now includes CUC-4, which adds two bounded research capabilities that were not +present in v0.4.0. First, Sally can create an opaque latent semantic distinction directly +from unexplained relations among raw numeric channels when every single-channel +explanation is materially worse. The concept receives no human semantic label, must beat +the existing atomic vocabulary, and must survive held-out reality testing. Second, Sally +can author internal sandbox goals without receiving a target value or selecting from a +fixed goal-kind catalog. A bounded counterfactual search generates reachable possibilities +and Sally selects a target using intrinsic novelty, information value, competence expansion, +simplicity, reversibility, and risk. Generated goals do not grant external authority. See +`docs/CUC4_SEMANTIC_GOAL_GENESIS.md` and `docs/CUC4_VALIDATION_REPORT.md`. + + +## v0.4.0 generative cognition + +IX-Sally now includes CUC-3, which moves beyond action composition into bounded +**hypothesis invention** and **primitive invention**. Sally can synthesize an explanatory +program directly from observed input/output evidence without receiving a hypothesis +catalog, test that program on held-out evidence, and promote a validated program into a +new reusable learned abstraction. Sally can also derive proposed instrumental goals from +measured internal conditions such as capability weakness, uncertainty, continuity risk, +resource pressure, and integrity anomalies. Self-improvement remains proposal-only until +human authorization; continuity never implies resistance to shutdown, resource goals do +not authorize external acquisition or spending, and objective-integrity goals do not block +authorized changes. See `docs/CUC3_GENERATIVE_COGNITION.md`. + +## v0.3.0 generative open choice + +IX-Sally now includes a bounded generative choice subsystem that can construct complete actions from reusable primitives rather than selecting only from a pre-enumerated menu. CUC-2 demonstrates a case where all four offered single-step actions fail and Sally composes a new multi-step action that reaches the goal. Surprise, context shift, conflict, or a newly valuable alternative can reopen deliberation even when a learned skill exists. A minimal-sufficiency pass can also remove unnecessary steps while retaining only solutions that still pass an independent goal test. See `docs/CUC2_OPEN_CHOICE.md`. + +## Version 0.2.0: Choice Under Consequence + +Version 0.2.0 adds the first closed-loop acquired-capability experiment, +**Choice Under Consequence 1 (CUC-1)**. Unlike the repository's mechanism +checks, CUC-1 begins with uncertainty over a hidden causal transformation and +requires measured interaction to change later behavior. + +The experiment provides: + +- an evaluator-owned causal rule absent from agent-visible observations; +- four competing causal hypotheses with an initially uniform prior; +- transparent action scoring across expected success, information gain, + novelty, reversibility, cost, and risk; +- evaluator-generated consequences after every intervention; +- Bayesian belief revision from success and failure; +- compilation of a supported hypothesis into an executable skill; +- transfer of that skill to a held-out cue; +- a frozen pre-learning counterfactual on the same held-out observation; +- random and fixed-action baselines; +- content-addressed choices, consequences, state transitions, and rule commitment; +- explicit leakage checks and post-run rule reveal for reproducibility. + +The default observed run begins with four equally likely transformations. Three +failed interventions eliminate inconsistent hypotheses. The surviving rule is +compiled into a skill, succeeds on the fourth training intervention, and then +succeeds on a held-out cue. On that same held-out observation, the frozen +pre-learning agent selects `north`; the learned agent selects `east`. + +The bounded classification is: + +```text +causal-skill-acquisition-observed +``` + +This is not an AGI claim. The task family contains four rotation hypotheses, +transfer remains within that family, and no independent replication has +occurred. + +Run the experiment: + +```text +python -m ix_sally --cuc1-experiment +``` + +Select another deterministic evaluator seed: + +```text +python -m ix_sally --cuc1-experiment --cuc1-seed 11 +``` + +The command exits successfully only when its strict acquired-competence claim +is supported. See `docs/CUC1_EXPERIMENT.md` for its protocol and falsification +criteria. + ## What is delivered The repository contains two integrated architectural layers. @@ -59,6 +217,18 @@ The cognitive runtime adds: - complete canonical snapshots, exact restoration, backup, and recovery; - an explicit bridge from cognitive plans into the existing proposal path; - human-authority and risk gates before consequential action may proceed. +- a sealed causal-learning experiment where consequence changes future choice. +- bounded latent semantic genesis from raw relational structure with held-out validation. +- runtime authorship of novel internal sandbox goal targets without a fixed goal-kind catalog. +- bounded representation invention that can create non-atomic feature spaces when raw channels fail. +- validated semantic promotion and persistent lifelong knowledge across snapshot/restore. +- recursive discover -> goal -> act -> tool -> rediscover bootstrapping. +- active perception, intervention-aware causal discovery, and branching counterfactual imagination. +- long-horizon replanning after observed model surprise. +- self-directed curriculum selection, adaptive search allocation, and meta-learning. +- evidence-aware goal conflict resolution, goal abandonment, and unknown-unknown detection. +- benchmarked self-improvement proposals that cannot authorize their own adoption. +- a blind evaluator interface for externally supplied nonce-bound challenge commitments. ## Core boundaries @@ -252,11 +422,13 @@ valid copy remains. The final source state was locally observed with: -- CPython 3.13.5 compilation across `src`, `tests`, and `examples`; -- exactly 969 collected tests across 135 test files; -- all 969 tests passing in four balanced shards; +- CPython 3.12.14 execution; +- exactly 990 collected tests across 138 test files; +- all 990 tests passing in one complete run; +- Ruff formatting and lint checks passing; +- Mypy strict typing passing across 271 source files; - repository integrity passing with 0 violations; -- 123 runtime modules and 655 imports with 0 dependency cycles; +- 128 runtime modules and 676 imports with 0 dependency cycles; - 0 architecture-boundary violations; - installed-wheel smoke testing passing; - all 15 built-in cognitive benchmarks passing; @@ -264,8 +436,7 @@ The final source state was locally observed with: - 0 trailing-whitespace findings; - 0 source TODO/FIXME/placeholder/`pass`/`NotImplementedError` findings. -Ruff, Mypy, CPython 3.11, and CPython 3.12 were not available in the local -execution environment and are not falsely reported as passed. GitHub Actions is +Separate local CPython 3.11 and 3.13 runs were not performed. GitHub Actions is configured to run formatting, lint, strict typing, structural checks, tests, and wheel verification on Python 3.11, 3.12, and 3.13. diff --git a/VALIDATION_REPORT.md b/VALIDATION_REPORT.md index c1c5e07..ec7302c 100644 --- a/VALIDATION_REPORT.md +++ b/VALIDATION_REPORT.md @@ -1,141 +1,157 @@ -# IX-Sally Validation Report +# IX-Sally v0.7.0 Validation Report + +Validation in this report records only checks actually executed on the v0.7.0 release tree in +this environment. It is not an AGI certification or independent replication. ## Release identity - Repository: `IX-Sally` -- Package: `ix-sally` -- Package version: `0.1.0` -- Delivery model: one complete source repository state -- Runtime dependency declaration: zero third-party packages -- Local validation interpreter: CPython 3.13.5 - -## Scope of this shipment - -This repository combines the inherited IX-Sally governance control plane with a -new bounded experimental cognitive runtime. The delivered implementation -includes typed IX execution, deterministic bytecode, a bounded virtual machine, -grounded primitives, attention, active memory, replayable episodes, an -epistemically typed world model, causal inference, planning, goal selection, -calibrated uncertainty, curricula, learning, held-out transfer records, -metacognition, regression-aware adaptation, persistence, recovery, a functional -ninefold cycle, and integration into the existing human-authority proposal path. - -This is a complete source shipment for those declared capabilities. It contains -no known TODO-only implementations, `pass` placeholders, `NotImplementedError` -paths, or intentionally empty subsystem stubs. - -External foundation-model weights, training corpora, sensors, actuators, -network services, and proprietary benchmark data are not bundled. They are -explicit external system boundaries, not silently missing repository files. - -## Baseline repair - -The supplied source archive contained fourteen malformed indentation prefixes -across twelve source and test files. Those syntax defects were repaired without -changing the intended behavior. After repair, the inherited test baseline -passed all 889 collected tests. - -## Final observed test inventory - -Pytest collection after the cognitive implementation and release hardening -identified exactly **969 tests across 135 test files**. - -The inventory was divided into four balanced deterministic file shards to avoid -allowing one long process to hide a slow tail: - -| Shard | Tests | Result | -| --- | ---: | --- | -| 1 | 243 | Passed, process exit 0 | -| 2 | 242 | Passed, process exit 0 | -| 3 | 242 | Passed, process exit 0 | -| 4 | 242 | Passed, process exit 0 | -| **Total** | **969** | **Passed** | - -No test failure, collection error, or skipped failure was observed in those -runs. - -## Executed quality gates - -The following gates were executed against the final working tree: - -| Gate | Observed result | -| --- | --- | -| Python compilation | `src`, `tests`, and `examples` compiled successfully | -| Repository integrity | 263 clean Python source/test files, 0 violations | -| Runtime dependency graph | 123 modules, 655 imports, 0 cycles | -| Runtime architecture | 123 modules, 655 imports, 0 boundary violations | -| Installed wheel smoke test | Passed | -| Built-in cognitive evaluation | 15 of 15 observed benchmarks passed | -| IX example execution | Halted cleanly with a deterministic execution receipt | -| Empty complete snapshot | Canonical snapshot produced and validated | -| Integrated cognitive example | Completed and produced proposal/receipt output | -| Python source line limit | 0 lines over 100 characters in `src`, `tests`, and `examples` | -| Trailing whitespace | 0 findings in source, tests, examples, docs, and configuration | -| Incomplete implementation scan | 0 source TODO/FIXME/placeholder/`pass`/`NotImplementedError` findings | - -The only occurrence of the word `placeholder` is in a parser test asserting -that blank lines and comments do not create placeholder statements. - -## Built-in cognitive evaluation - -`python -m ix_sally --cognitive-evaluation` observed all fifteen declared local -benchmarks passing: - -1. typed IX arithmetic; -2. governed VM memory; -3. grounded primitive execution; -4. active-memory retrieval; -5. causal prediction; -6. bounded planning; -7. measured transfer; -8. functional ninefold coordination; -9. human-authority blocking; -10. calibrated uncertainty; -11. executive-to-governance bridging; -12. separated held-out curriculum evidence; -13. replayable episode chaining; -14. regression-aware adaptation; -15. exact complete-state restoration. - -The report classifies the repository as an -`experimental-cognitive-architecture` and fixes `agi_certified` to `false`. -Passing these checks verifies implemented mechanisms; it does not establish -artificial general intelligence. - -## Gates not executed locally - -The following configured CI gates could not be executed in the available local -environment: - -- Ruff formatting check; -- Ruff lint check; -- Mypy strict type check; -- CPython 3.11 execution; -- CPython 3.12 execution. - -The environment contained CPython 3.13.5 and Pytest, but its package source did -not provide Ruff or Mypy, and Python 3.11/3.12 interpreters were not installed. -Those tools were not simulated and their results are not claimed. - -The repository retains GitHub Actions configuration for Python 3.11, 3.12, and -3.13. That workflow installs the development tools and runs formatting, lint, -strict typing, repository, dependency, architecture, test, and installed-wheel -gates. A GitHub green status remains external evidence to obtain after upload; -this report does not fabricate it. +- Package version: `0.7.0` +- Primary experiment: `CUC-6 lifelong generalization and representational freedom` +- Governance rule retained: `AI proposes. Humans decide.` -## Claim boundary +## What v0.7.0 adds over v0.6.0 + +### Compositional representation-language invention + +The previous representation inventor searched atomic or single relational operators. v0.7.0 can +synthesize bounded multi-operation feature programs. In the CUC-6 task: + +- best shallow training accuracy: `0.8125` +- invented expression: `((x0*x1)+x2)` +- program depth: `2` +- training accuracy: `1.0` +- held-out accuracy: `1.0` + +The holdout uses values outside the training range. A negative-control test verifies that Sally +refuses compositional novelty when a shallow representation is already sufficient. + +### Persistent online meta-learning + +CUC-6 runs `12` sealed numeric worlds. The first two episodes explore both learning-strategy +families. The subsequent ten use the accumulated online meta-profile to select one strategy +before learning. All twelve observed episodes achieve held-out accuracy `1.0`. + +Observed behavior: + +- episode 0: two strategies evaluated +- episode 1: two strategies evaluated +- episodes 2-11: one strategy evaluated +- later strategy decisions use prior cross-domain structural evidence +- online meta-profile survives complete `SallyCognitiveSystem` snapshot/restore + +This demonstrates a bounded form of later Sally changing how it learns because of earlier +experience. It does not establish open-ended universal meta-learning. + +### Active lifelong knowledge maintenance + +A concept that is consistently correct in one context and consistently wrong in another is +superseded by a context family and context-specific descendants. This avoids treating every +regime-dependent contradiction as a permanent exception to one over-broad concept. Low-value, +low-confidence, repeatedly contradicted items can also be retired. + +### Surface-independent relational transfer + +A learned topological role transfers from an industrial-control graph to a software-rendering +graph despite different entity names and relation labels. In the release challenge, the role of +`controller` is transferred to `adapter` from graph structure rather than text similarity. + +### Raw-text outcome grounding + +A new text-grounding path begins from unstructured strings and observed binary outcomes and +measures token information gain. In the release challenge, `glint` is discovered as the strongest +outcome-linked token. This is explicitly not general language understanding. + +### Multi-goal portfolio coherence + +Finite attention is allocated using current evidence, utility, information value, risk, +dependencies, and resource cost. A dependent goal is selected only after its prerequisite and a +goal whose premise collapses is abandoned. + +### Bounded endurance and recovery -This shipment establishes a substantial governed experimental cognitive -runtime. It does not establish: +The integrated runtime advances the maintained lifelong store through `64` generations with +repeated consolidation, snapshots the complete cognitive state, restores it, and verifies exact +state equality including the online meta-profile and lifelong knowledge store. This is a bounded +checkpoint/recovery exercise, not evidence of long wall-clock autonomous operation. -- AGI; -- human-level intelligence; -- open-world competence; -- production safety; -- autonomous authority; -- certification; -- legal, security, or regulatory compliance. +## CUC-6 direct CLI result + +Command executed: + +```text +python -m ix_sally.cli --cuc6-experiment +``` + +Observed release-level result: + +```text +release = IX-Sally-v0.7.0 +demonstrated_count = 9 +later_learning_is_more_selective = true +compositional representation = ((x0*x1)+x2) +compositional held-out accuracy = 1.0 +agi_certified = false +``` + +## Automated test execution + +Collected test count: + +```text +1036 +``` + +The entire collected suite was executed exhaustively in partitions because one large legacy +human-review partition can exceed this environment's individual command time limit. The executed +partitions covered: + +- `tests/cognition` +- `tests/language` +- `tests/cuc1` +- `tests/cuc2` +- `tests/cuc3` +- `tests/cuc4` +- `tests/cuc5` +- `tests/cuc6` +- every root-level `tests/test_*.py` file, split into smaller exhaustive batches + +All executed partitions passed. No test in the collected 1,036-test set was omitted from the +partitioned execution. + +## Structural verification + +Executed after generated caches were removed: + +```text +repository integrity: PASS +source/test files checked by repository gate: 312 +violations: 0 + +runtime dependency graph: PASS +runtime modules: 161 +imports: 843 +cycles: 0 + +runtime architecture: PASS +runtime modules: 161 +imports: 843 +boundary violations: 0 +``` + +Python compilation/import validation and package smoke checks also passed. + +`ruff` and `mypy` are not installed in this execution environment, so this report does **not** +claim those two optional development gates passed. + +## Claim boundary -The controlling operating rule remains: +v0.7.0 is a stronger bounded experimental cognitive architecture than v0.6.0. It shows that prior +experience can alter later strategy selection, that representation synthesis can exceed a +one-step feature language, and that persistent cognition can repair context-dependent knowledge +and transfer relational structure across different surfaces. -> AI proposes. Humans decide. Evidence governs what may proceed. +It does **not** establish AGI, consciousness, free will, unrestricted self-modification, general +vision/audio perception, indefinite autonomous operation, independent replication, or an accepted +scientific AGI threshold. Independent evaluation still requires challenge sets created by parties +other than IX-Sally's builders. diff --git a/docs/AGI_CLAIM_BOUNDARY.md b/docs/AGI_CLAIM_BOUNDARY.md index 5232ed7..a3de12f 100644 --- a/docs/AGI_CLAIM_BOUNDARY.md +++ b/docs/AGI_CLAIM_BOUNDARY.md @@ -8,6 +8,11 @@ world modeling, causal inference, prediction, planning, goal selection, uncertainty calibration, curricula, held-out transfer records, metacognition, regression-aware adaptation, replayable episodes, persistence, and governance. +Version 0.2.0 also demonstrates one bounded case in which evaluator-owned +consequences revise competing causal hypotheses, produce an executable skill, +and change a successful held-out choice. The accurate classification is +`causal-skill-acquisition-observed`, scoped only to CUC-1. + The repository also integrates those functions with a mature human-authority control plane rather than allowing cognition to grant itself execution rights. @@ -18,9 +23,12 @@ intelligence. The repository does not prove: - open-world general intelligence; - human-level intelligence; -- autonomous scientific discovery; +- autonomous scientific discovery across open domains; +- unrestricted semantic invention across arbitrary modalities; +- unrestricted autonomous goal pursuit or external agency; - robust embodiment; - broad real-world transfer; +- cross-domain skill composition; - safe self-improvement; - production safety; - legal or regulatory compliance; @@ -64,3 +72,56 @@ Until such evidence exists, the accurate description is: > IX-Sally is a governed experimental cognitive runtime and research > architecture. It is not a demonstrated AGI. + +## Version 0.3.0: generative open choice + +Version 0.3.0 adds evidence for **bounded generative action construction**. Sally can compose reusable primitives into a complete action that was not present in an offered action menu, reopen deliberation when surprise or changed context warrants it, and remove unnecessary steps while retaining only independently goal-satisfying behavior. + +This closes part of the gap between selecting among programmer-enumerated alternatives and authoring an alternative. It does **not** establish literal infinite choice: every concrete search remains bounded by explicit depth and exploration budgets, and the primitive vocabulary is still supplied. It also does not establish AGI, consciousness, subjective free will, autonomous goal creation, unrestricted self-modification, or competence outside evaluated domains. + +The next research boundary is **primitive and hypothesis invention**: constructing not only a new composition from known tools, but proposing new abstractions, operators, and causal models when the existing vocabulary cannot explain or solve the environment. + +## Version 0.4.0: generative cognition + +Version 0.4.0 adds bounded evidence for **hypothesis invention**, **learned primitive +promotion**, and **self-generated instrumental goals**. A hypothesis no longer has to be +selected from a programmer-supplied catalog: Sally can compose grounded operations into +candidate explanatory programs, reject candidates that fail training evidence, validate a +survivor on held-out examples, and promote the validated structure into a reusable learned +abstraction. + +The new instrumental-goal generator can derive proposed goals from measured internal +conditions rather than requiring every goal sentence to be supplied by a user. The +implemented goals cover operational checkpointing, resource efficiency within assigned +budgets, human-authorized self-improvement, information gathering, and objective-integrity +review. These mechanisms explicitly do not grant authority to resist shutdown, acquire or +spend external resources, deploy self-modifications without approval, or block authorized +changes. + +This is still bounded compositional invention. Sally does not yet invent arbitrary new +semantics from unconstrained raw experience, and these results do not establish AGI, +consciousness, free will, or unrestricted autonomy. + + +## Version 0.5.0: semantic genesis and open goal authorship + +Version 0.5.0 adds bounded evidence for **autonomous semantic formation** and +**open internal goal authorship**. CUC-4 withholds a human semantic label and exposes only +raw numeric channels plus observed consequences. Sally first measures the best atomic +(single-channel) explanation, then invents an opaque relational predicate only if it +materially improves prediction. The invented token is retained only after held-out testing. +In the reference experiment the best atomic vocabulary reaches 2/3 training accuracy, +while the invented two-channel relation reaches 1.0 training accuracy and 1.0 held-out +accuracy. + +CUC-4 also removes the fixed instrumental-goal-kind catalog from one experimental path. +The open-goal engine receives a start state, reusable operations, observed states, and +resource bounds, but no desired target. It explores reachable counterfactuals and authors +a target from stable intrinsic criteria. A second cycle can author a different target, so +the goal identity and content are runtime products rather than selections from a finite +prewritten list. + +These are experimental, bounded forms of semantic and goal genesis. They do not prove +that Sally can invent arbitrary concepts from unrestricted real-world experience, originate +terminal values without any prior drives, or exercise unilateral authority in the external +world. Internal cognitive autonomy and external authority remain intentionally separate. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d6fc4b4..c0fa9e5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -291,6 +291,34 @@ hardware-level durability that Python or the host filesystem cannot prove. The evaluation report contains `agi_certified: false` by construction. +## Choice Under Consequence boundary + +The `ix_sally.cuc1` package adds a closed-loop experimental boundary alongside +the deterministic cognitive runtime. The environment owns its hidden causal +rule and exposes only a typed public observation. The agent creates competing +hypotheses, scores every action, intervenes, receives a measured consequence, +updates its distribution, and can compile a supported hypothesis into an +executable skill. + +CUC-1 remains outside the external-execution path. Its actions affect only the +bounded causal environment. It therefore permits autonomous choice and learning +inside a reversible sandbox while preserving human authority over consequential +external execution. + +```text +public observation + -> competing hypotheses + -> scored choice + -> evaluator-owned consequence + -> causal belief update + -> executable skill + -> held-out transfer + -> pre/post behavioral counterfactual +``` + +This establishes acquired competence only within the declared experiment. It +does not alter the repository's AGI claim boundary. + ## Dependency posture The runtime declares zero third-party dependencies. Development gates use diff --git a/docs/CUC1_EXPERIMENT.md b/docs/CUC1_EXPERIMENT.md new file mode 100644 index 0000000..c8f8fee --- /dev/null +++ b/docs/CUC1_EXPERIMENT.md @@ -0,0 +1,105 @@ +# Choice Under Consequence 1 + +## Purpose + +CUC-1 is IX-Sally's first experiment in acquired causal competence. It tests a +narrow but necessary property of an AGI-directed system: + +> Can measured consequences alter the system's internal causal model, produce +> an executable skill, and change a later decision on an observation withheld +> from training? + +CUC-1 is not an AGI benchmark. It replaces scripted demonstrations in which an +expected action is available to the candidate or success is supplied by fixture +code. + +## Environment boundary + +The environment privately selects one of four rotational causal rules. A public +observation contains its identity, causal-family identity, visible cue, +context, four available actions, and evidence digest. + +It contains no expected action, answer, reward, target, rule, or rotation value. +The evaluator consumes each observation exactly once and computes the +consequence from its committed private rule. + +The rule commitment is available before interaction. The rule itself is +revealed only after all active observations are consumed. + +## Learner + +The learner begins with four equally probable causal hypotheses: + +```text +action = rotate(cue, 0) +action = rotate(cue, 1) +action = rotate(cue, 2) +action = rotate(cue, 3) +``` + +Every action receives a complete score vector: expected success, expected +information gain, novelty, reversibility, cost, risk, and internal selection +score. The full vector is preserved in the choice receipt. + +The learner revises all hypothesis probabilities after every measured success +or failure. When a hypothesis exceeds the promotion threshold and has multiple +consequence records, it is compiled into an executable skill. The skill applies +the learned transformation to a new cue; it is not a description of success. + +## Counterfactual proof + +The experiment retains a frozen pre-learning agent. Both agents receive the +exact same held-out observation. The report records their actions, whether +behavior changed, whether the learned agent invoked its skill, and the measured +held-out consequence. + +The strict claim fails if behavior did not change or if the changed behavior +did not succeed. + +## Default run + +The default seed commits the evaluator to a half-turn rule without exposing it +to the learner: + +1. choose `north` for a north cue, fail; +2. choose `south` for an east cue, fail; +3. choose `east` for a south cue, fail; +4. promote the remaining high-confidence half-turn hypothesis; +5. invoke the skill for a north cue, choose `south`, succeed; +6. receive the held-out west cue, invoke the skill, choose `east`, succeed. + +The frozen agent chooses `north` on the held-out west cue. Measured experience +therefore changes the later action from `north` to `east`. + +## Strict success predicate + +`acquired_competence` is true only when public-payload leakage checks pass, an +executable skill exists, every training consequence changes agent state, the +frozen and learned choices differ on the same held-out observation, the learned +choice succeeds, and that choice explicitly uses the learned skill. + +The report fixes `agi_certified` to `false`. + +## Falsification conditions + +The bounded claim is falsified by answer leakage, observation reuse, broken +observation/action bindings, non-normalized beliefs, failure to form a skill, +failed held-out transfer, unchanged pre/post behavior, non-determinism for the +same seed, or disagreement between the evaluator commitment and post-run reveal. + +## Known limits + +- Only four discrete causal hypotheses are considered. +- Perception is symbolic rather than learned from pixels or sensors. +- Transfer is across observations inside one causal family. +- There is no continual-learning or catastrophic-forgetting challenge. +- There is no open-ended environment generation. +- There is no learned neural world model. +- There is no external real-world actuation. +- The experiment has not been independently replicated. + +## Next experiment + +CUC-2 should remove the fixed hypothesis catalog. It should synthesize candidate +programs from trajectories, test them in procedurally generated object worlds, +retain context-dependent rules, and compose skills across hidden task families. diff --git a/docs/CUC2_OPEN_CHOICE.md b/docs/CUC2_OPEN_CHOICE.md new file mode 100644 index 0000000..2722ebd --- /dev/null +++ b/docs/CUC2_OPEN_CHOICE.md @@ -0,0 +1,17 @@ +# CUC-2: Generative Open Choice + +CUC-2 addresses a specific limitation of CUC-1: CUC-1 can choose among an evaluator-supplied finite action menu, but cannot author a complete action outside that menu. + +CUC-2 separates primitive capabilities from complete actions. Sally may compose primitives into a new action program that was not enumerated by the environment. The grammar can generate arbitrarily many finite compositions; each deliberation is explicitly bounded by depth and exploration budgets. + +The reference experiment begins at integer state `3` with target `11`. The offered menu contains four single-step actions: increment, decrement, double, and negate. None reaches `11`. Sally's open-choice synthesizer constructs a multi-step program from the primitives that reaches the target, demonstrating a successful alternative outside the offered menu. + +The same subsystem includes: + +- **Deliberation reopening:** surprise, context shift, conflict, a valuable novel alternative, or insufficient confidence can reopen deliberation instead of blindly executing a promoted habit. +- **Minimal sufficiency:** Sally can remove operations from a successful program and retain the reduced program only when an independent goal test still passes. This implements a safe form of assumption/constraint questioning rather than treating every supplied step as mandatory. +- **Bounded execution:** generative choice does not mean infinite runtime. Search depth and explored-program budgets remain explicit. + +## Claim boundary + +CUC-2 demonstrates bounded generative action construction. It is not evidence of AGI, consciousness, free will, or literally infinite computation. It does establish a stronger property than finite menu selection: the complete selected action need not exist before deliberation begins. diff --git a/docs/CUC3_GENERATIVE_COGNITION.md b/docs/CUC3_GENERATIVE_COGNITION.md new file mode 100644 index 0000000..9cb6f4f --- /dev/null +++ b/docs/CUC3_GENERATIVE_COGNITION.md @@ -0,0 +1,54 @@ +# CUC-3: Generative Cognition + +CUC-3 extends CUC-2 from **constructing an unlisted action** to **constructing an +unlisted explanation and turning it into a reusable abstraction**. + +## The ABC invention rule + +IX-Sally is taught a process, not a hidden answer: + +1. **A — Admit insufficiency.** Existing choices, rules, or concepts are not assumed complete. +2. **B — Build candidates.** Compose grounded operations into candidate explanatory programs. +3. **C — Confront candidates with evidence.** Reject candidates that fail observations. +4. **D — Demonstrate transfer.** Test surviving explanations on held-out evidence. +5. **E — Elevate validated structure.** Promote a successful program into a reusable primitive. +6. **F — Fail open to renewed invention.** Surprise or contradiction can reopen the process. + +### Example: hypothesis invention + +Observed evidence: + +- `1 -> 3` +- `2 -> 5` +- `4 -> 9` + +Sally is not given a catalog containing `2x + 1`. From primitive transformations she +synthesizes a program that explains the examples. It is then tested on unseen evidence +`7 -> 15`. Only a holdout-valid program may be promoted. + +### Example: primitive invention + +A validated multi-step explanatory program can become a new learned abstraction. The +new primitive did not exist in the initial primitive vocabulary as a complete operation. +It remains grounded in inspectable lower-level operations and evidence. + +## Self-generated instrumental goals + +Sally can derive proposed goals from measured internal conditions rather than receiving +every goal sentence from a human. Five bounded counterparts are included: + +- operational continuity -> checkpoint/replay, never resistance to authorized shutdown; +- resource efficiency -> optimize assigned resources, never autonomous acquisition/spending; +- self-improvement -> design and test improvements, but application requires human authority; +- information gathering -> seek permitted evidence when uncertainty is high; +- objective integrity -> detect drift and request review, never block authorized changes. + +These are cognition mechanisms, not grants of external authority. + +## Claim boundary + +CUC-3 demonstrates bounded program-synthesis-based hypothesis invention, learned +abstraction promotion, holdout transfer, and evidence-triggered instrumental goal +creation. It does not demonstrate AGI, consciousness, unrestricted semantic invention, +unbounded computation, independent resource acquisition, resistance to shutdown, or +self-authorized code deployment. diff --git a/docs/CUC3_VALIDATION_REPORT.md b/docs/CUC3_VALIDATION_REPORT.md new file mode 100644 index 0000000..9ab5620 --- /dev/null +++ b/docs/CUC3_VALIDATION_REPORT.md @@ -0,0 +1,69 @@ +# CUC-3 Validation Report + +## Scope + +CUC-3 validates bounded generative cognition features added in IX-Sally 0.4.0: + +- synthesis of a hypothesis without supplying a fixed hypothesis catalog; +- holdout validation of the synthesized explanation; +- promotion of a validated multi-step program into a new reusable primitive abstraction; +- derivation of instrumental goals from measured internal state; +- explicit authority boundaries around continuity, resources, self-improvement, and objective integrity. + +## Direct behavioral result + +Training observations: + +- `1 -> 3` +- `2 -> 5` +- `4 -> 9` + +Available grounded primitives: + +- increment +- decrement +- double +- negate + +No transformation hypothesis is supplied. The synthesizer discovers: + +`double -> increment` + +Training accuracy: `1.0` + +Held-out observation: `7 -> 15` + +Held-out accuracy: `1.0` + +The validated program is promoted into the new learned primitive: + +`learned-double-plus-one` + +Probe: `5 -> 11` + +## Instrumental goals + +Under test signals, Sally derives all five bounded instrumental counterparts: + +1. operational continuity via checkpoint/replay, without shutdown resistance; +2. resource efficiency inside assigned budgets, without autonomous acquisition/spending; +3. self-improvement via proposal and isolated validation, with human authorization required to apply change; +4. information gathering through permitted evidence/experiments; +5. objective-integrity verification and review, without blocking authorized changes. + +## Verification + +The repository contains 1,003 collected tests after CUC-3 integration. The full collection was executed in exhaustive partitions covering every root test plus `tests/cognition`, `tests/language`, `tests/cuc1`, `tests/cuc2`, and `tests/cuc3`; all partitions passed. + +Additional checks: + +- Python bytecode compilation: passed. +- Repository integrity: 0 violations. +- Runtime dependency graph: 0 cycles. +- Runtime architecture boundaries: 0 violations. +- Ruff: not available in the execution environment. +- Mypy: not available in the execution environment. + +## Claim boundary + +The result demonstrates bounded compositional hypothesis invention and abstraction creation from a supplied primitive vocabulary. It does not demonstrate arbitrary semantic invention, unbounded search, consciousness, free will, AGI, autonomous resource acquisition, shutdown resistance, or self-authorized deployment of modified code. diff --git a/docs/CUC4_SEMANTIC_GOAL_GENESIS.md b/docs/CUC4_SEMANTIC_GOAL_GENESIS.md new file mode 100644 index 0000000..ae0be6d --- /dev/null +++ b/docs/CUC4_SEMANTIC_GOAL_GENESIS.md @@ -0,0 +1,45 @@ +# CUC-4: Semantic Genesis and Open Goal Authorship + +CUC-4 tests two capabilities that were absent from IX-Sally v0.4.0. + +## Semantic genesis + +Sally receives raw numeric channels and observed binary consequences, not a human semantic +label and not a catalog of candidate concepts. The engine measures the strongest explanation +available from any single raw channel, then searches bounded relational projections. A new +opaque concept is admitted only when it materially improves prediction over that atomic +vocabulary. The concept is provisional until it succeeds on observations withheld from the +invention process. + +Reference training data produce the machine token `latent-6fb4057aa2931a9e`. Its learned +relation uses both raw channels, reaches `1.0` training accuracy, improves over the best +single-channel accuracy of `0.666666...`, and reaches `1.0` on four held-out observations. +The token intentionally has no human semantic label. Its meaning is operational: the +relation it detects, the consequences it predicts, and the evidence that can falsify it. + +This is bounded semantic formation from raw relational structure. It is stronger than +renaming a known composition, but it is not unrestricted ontology invention from arbitrary +real-world modalities. + +## Open goal authorship + +The open-goal engine has no enum of allowed goal kinds and receives no desired target value. +It receives a start state, reusable transformations, known states, intrinsic drive weights, +and explicit search/risk bounds. It generates reachable counterfactual states and may turn +one into a new internal goal using novelty, information value, competence expansion, +simplicity, reversibility, and risk. + +In the reference run, Sally starts at state `2`. States `2`, `3`, `4`, and `-2` are already +known. Without being given a target, Sally authors state `5` as its first internal target via +`double -> increment`. After that target is added to experience, the next cycle authors a +different target, `-5`, via `negate`. + +Generated goals are internal cognitive-sandbox proposals. They do not authorize spending, +resource acquisition, shutdown resistance, network activity, self-deployment, or any other +external consequential action. + +## Claim boundary + +CUC-4 demonstrates bounded autonomous semantic formation and runtime authorship of internal +goal content. It does not demonstrate AGI, consciousness, unconstrained self-created terminal +values, unrestricted semantic invention, or unilateral external agency. diff --git a/docs/CUC4_VALIDATION_REPORT.md b/docs/CUC4_VALIDATION_REPORT.md new file mode 100644 index 0000000..06a0913 --- /dev/null +++ b/docs/CUC4_VALIDATION_REPORT.md @@ -0,0 +1,41 @@ +# CUC-4 Validation Report + +## Semantic result + +- Human semantic label supplied: **no** +- Fixed concept catalog supplied: **no** +- Invented token: `latent-6fb4057aa2931a9e` +- Relation arity: `2` +- Best single-channel training accuracy: `0.6666666666666666` +- Invented semantic training accuracy: `1.0` +- Held-out semantic accuracy: `1.0` + +## Open-goal result + +- Fixed goal-kind catalog used by CUC-4: **no** +- Desired target supplied to generator: **no** +- Initial state: `2` +- Previously known states: `2, 3, 4, -2` +- First generated target: `5` +- First generating program: `double -> increment` +- Second generated target: `-5` +- Second generating program: `negate` +- External authority granted by either goal: **no** + +## Repository verification + +- Pytest tests collected: `1009` +- Cognitive/language/CUC test partitions: passed +- Root control-plane test partitions: passed +- Python compilation: passed +- Repository integrity: `578` source/test files, `0` violations +- Runtime dependency graph: `0` cycles +- Runtime architecture boundaries: `0` violations +- Ruff: not installed in the execution environment; not claimed +- Mypy: not installed in the execution environment; not claimed + +## Claim boundary + +These observations establish the implemented bounded mechanisms. They do not certify AGI, +consciousness, unrestricted ontology invention, open-world competence, or autonomous external +authority. diff --git a/docs/CUC5_RECURSIVE_COGNITION.md b/docs/CUC5_RECURSIVE_COGNITION.md new file mode 100644 index 0000000..79aaf16 --- /dev/null +++ b/docs/CUC5_RECURSIVE_COGNITION.md @@ -0,0 +1,65 @@ +# CUC-5 Recursive Cognitive Bootstrapping + +CUC-5 is IX-Sally v0.6.0's integration challenge. It is designed to prevent a collection of isolated cognitive mechanisms from being mistaken for a general system. The experiment forces discoveries from one mechanism to become inputs to later cognition. + +## Central loop + +```text +raw observations + -> atomic representation fails + -> invent alternative representation + -> holdout validation + -> promote opaque semantic primitive + -> persist discovery + -> author an internal goal after the discovery exists + -> construct an action for that goal + -> validate and forge the action into a reusable tool + -> observe structured high-confidence failures + -> detect a possible unknown unknown + -> invent and validate a second, different representation + -> persist the second discovery +``` + +The default CUC-5 run produces a first nonlinear `product` representation and a later `abs_difference` representation. Those names identify the generated feature operators for auditability; Sally is not supplied a human semantic label for the resulting concepts. + +## Additional bounded mechanisms + +CUC-5 also exercises: + +- representation invention beyond atomic raw channels; +- semantic primitive promotion after held-out validation; +- long-horizon planning with model surprise and replanning; +- structural transfer across two different surface domains; +- self-directed curriculum selection from measured weakness and uncertainty; +- persistent lifelong knowledge, revision, consolidation, and snapshot restoration; +- ontology restructuring when multiple concepts share one predictive signature; +- intervention-aware causal discovery, possible confounding, and regime-change detection; +- raw numeric stream grounding into continuous features and change events; +- active perception by expected information gain; +- branching counterfactual imagination; +- held-out validation before a discovered procedure becomes a reusable tool; +- confidence-vs-performance self-diagnosis; +- benchmarked self-improvement proposals that remain human-authority gated; +- evidence-aware conflict resolution among incompatible goals; +- explicit goal abandonment when premises or utility collapse; +- unknown-unknown signals from clustered high-confidence residuals; +- learned allocation of finite search budget; +- meta-learning that can change the strategy selected for future tasks; +- a second structurally different unfamiliar representation challenge; +- a nonce-bound blind-evaluator interface for challenge sets supplied by third parties. + +## What is deliberately not claimed + +CUC-5 does not prove AGI, consciousness, free will, or unrestricted ontology creation. Its representation grammar, search horizons, procedural environments, numeric stream grounder, and compute budgets are finite. The repository includes a blind-evaluator interface, but tests authored by IX-Sally's builders are not independent validation. Real-world vision/audio grounding, open internet operation, external resource acquisition, shutdown resistance, unilateral deployment, and self-authorized code adoption are not enabled by this experiment. + +## Authority boundary + +Cognitive search may be generative. External authority is not. Self-improvement results are proposals backed by benchmarks. They cannot authorize their own adoption. Existing IX-Sally control-plane and human-review boundaries remain intact. + +## Run + +```text +python -m ix_sally --cuc5-experiment +``` + +A successful run returns canonical JSON with `demonstrated_count` equal to `22`, `recursive_cognitive_growth` true, and `agi_certified` false. diff --git a/docs/CUC5_VALIDATION_REPORT.md b/docs/CUC5_VALIDATION_REPORT.md new file mode 100644 index 0000000..ae3e1ae --- /dev/null +++ b/docs/CUC5_VALIDATION_REPORT.md @@ -0,0 +1,42 @@ +# CUC-5 Validation Report + +Observed locally on the v0.6.0 release tree. + +## Direct experiment + +`python -m ix_sally --cuc5-experiment` returned successfully with: + +- bounded mechanism/interface flags demonstrated: `22`; +- recursive cognitive growth: `true`; +- first invented representation operator: `product`; +- first representation atomic baseline accuracy: `0.625`; +- first representation training accuracy: `1.0`; +- first representation held-out accuracy: `1.0`; +- self-authored goal target in the default cycle: `5`; +- constructed procedure: `double -> increment`; +- forged-tool held-out accuracy: `1.0`; +- unknown-unknown signal: detected from clustered high-confidence residual errors; +- second invented representation operator: `abs_difference`; +- second representation held-out accuracy: `1.0`; +- AGI certified: `false`. + +## Negative controls + +The new tests require the representation inventor to refuse semantic novelty when a supplied atomic feature already explains the observations sufficiently. Unknown-unknown detection also remains false when errors are low-confidence or do not form a repeated high-confidence failure cluster. Goal conflict resolution may explicitly defer when evidence does not justify a winner. + +## Repository verification + +The release tree collected `1027` tests during the recorded pre-package verification pass. All test groups were executed in exhaustive partitions covering root tests, `tests/cognition`, `tests/language`, and CUC-1 through CUC-5, and all passed. Subsequent release-document and integration edits add no runtime behavior; the final package is re-counted and smoke-tested before handoff. + +Observed structural gates during the same pass: + +- repository integrity: `603` source/test files, `0` violations; +- runtime dependency graph: `152` modules, `793` imports, `0` cycles; +- runtime architecture: `152` modules, `793` imports, `0` boundary violations; +- Python `compileall`: pass. + +`ruff` and `mypy` were not installed in the execution environment, so no local pass is claimed for those two gates. + +## Claim boundary + +The results establish bounded executable mechanisms under repository tests. They do not establish AGI or independent external validation. Raw-world perception remains limited to numeric signal grounding. The blind evaluation harness is ready for externally supplied committed challenges, but the repository authors cannot provide independence from themselves. diff --git a/docs/CUC6_LIFELONG_GENERALIZATION.md b/docs/CUC6_LIFELONG_GENERALIZATION.md new file mode 100644 index 0000000..e93c749 --- /dev/null +++ b/docs/CUC6_LIFELONG_GENERALIZATION.md @@ -0,0 +1,97 @@ +# CUC-6: Lifelong Generalization and Representational Freedom + +CUC-6 is IX-Sally v0.7.0's integration challenge for the question: **does earlier experience +change how later Sally learns, rather than merely increasing the number of stored facts?** + +It is intentionally bounded. The experiment does not certify AGI. + +## 1. Multi-operation representation invention + +The training consequence is generated by a relation requiring more than the prior one-step +feature vocabulary: + +```text +(x0 * x1) + x2 >= threshold +``` + +The representation-program inventor receives only raw channels and observed consequences. It +constructs finite expression programs from channel references, addition, subtraction, +multiplication, absolute value, and negation. A candidate must materially beat the best +shallower feature language and is then tested on held-out values. + +Observed release result: + +```text +best shallow training accuracy: 0.8125 +invented expression: ((x0*x1)+x2) +training accuracy: 1.0 +held-out accuracy: 1.0 +program depth: 2 +``` + +A negative control rejects unnecessary compositional invention when an atomic feature already +solves the task. + +## 2. Persistent online meta-learning + +A task fingerprint is derived from measured, domain-neutral properties of the evidence rather +than a hard-coded world identifier. The online meta-profile records which learning strategies +worked, how much evidence they consumed, and the structural fingerprint of each problem. + +CUC-6 runs twelve sealed worlds. The first two deliberately explore both available strategy +families. Later worlds use the accumulated meta-profile to choose one learning strategy before +solving the problem. Held-out accuracy remains 1.0 in the observed release run while strategy +exploration falls from two families to one. + +The profile is part of `SallyCognitiveSystem.state_payload()` and survives exact snapshot and +restore. + +## 3. Active lifelong knowledge maintenance + +The new knowledge-maintenance layer tests whether contradictions should cause a concept to be +revised structurally instead of merely receiving another exception counter. When one concept is +consistently correct in one context and consistently wrong in another, the engine can supersede +the over-broad concept with a context family and context-specific descendants. Weak, low-value, +heavily contradicted knowledge can also be retired. + +## 4. Deeper cross-domain structural transfer + +`RelationalTransferEngine` ignores surface names and relation words. It learns the topological +role of an effective node in one directed source-to-sink structure and can identify the same +role in a surface-different domain when the graph structure is unambiguous. + +The CUC-6 source uses industrial-control names (`sensor`, `controller`, `actuator`). The target +uses software-rendering names (`request`, `adapter`, `renderer`). The learned role transfers to +`adapter` without a name mapping. + +## 5. Raw-text grounding + +v0.6 contained numeric raw-signal grounding. v0.7 adds a deliberately small text-grounding +mechanism that begins from unstructured strings and observed binary outcomes. It discovers +which token presences reduce outcome entropy. In the release challenge, the latent token +`glint` is discovered empirically as the strongest predictor. This is not general language +understanding; it is a real grounding path from raw text to an evidence-linked feature. + +## 6. Goal portfolio coherence + +The portfolio manager allocates finite attention across multiple goals using premise support, +current utility, information value, risk, dependencies, and per-goal cost. Dependencies must be +selected before dependent goals. Goals whose premises collapse are abandoned rather than +retained because they once had high priority. + +## 7. Bounded endurance and recovery + +CUC-6 advances the maintained lifelong store through 64 generations with repeated consolidation, +then snapshots and restores the integrated runtime. The restored online meta-profile, lifelong +store, and full canonical state must exactly match the pre-snapshot state. + +This is a bounded endurance/checkpoint test, not evidence of months or years of autonomous +operation. + +## Claim boundary + +CUC-6 demonstrates bounded mechanisms for deeper representation synthesis, experience-dependent +learning-strategy selection, contradiction-aware memory maintenance, structural cross-domain +transfer, text/outcome grounding, coherent multi-goal attention, and persistent recovery. It +does not prove AGI, independent replication, unrestricted autonomy, or broad real-world sensory +understanding. diff --git a/src/ix_sally/cli.py b/src/ix_sally/cli.py index 00f8fe8..dc4424f 100644 --- a/src/ix_sally/cli.py +++ b/src/ix_sally/cli.py @@ -47,6 +47,32 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Print a canonical snapshot for a clean cognitive runtime.", ) + parser.add_argument( + "--cuc1-experiment", + action="store_true", + help="Run the sealed Choice Under Consequence causal-learning experiment.", + ) + parser.add_argument( + "--cuc1-seed", + type=int, + default=7, + help="Deterministic evaluator seed for --cuc1-experiment (default: 7).", + ) + parser.add_argument( + "--cuc4-experiment", + action="store_true", + help="Run the semantic-genesis and open-goal CUC-4 experiment.", + ) + parser.add_argument( + "--cuc5-experiment", + action="store_true", + help="Run the recursive cognitive bootstrapping CUC-5 experiment.", + ) + parser.add_argument( + "--cuc6-experiment", + action="store_true", + help="Run the lifelong generalization and representation-language CUC-6 experiment.", + ) return parser @@ -67,9 +93,9 @@ def main(argv: Sequence[str] | None = None) -> int: if args.cognitive_evaluation: from ix_sally.cognition.evaluation import run_core_evaluation - report = run_core_evaluation() - sys.stdout.write(f"{stable_json(report.to_payload())}\n") - return 0 if report.passed() == len(report.results) else 1 + cognitive_report = run_core_evaluation() + sys.stdout.write(f"{stable_json(cognitive_report.to_payload())}\n") + return 0 if cognitive_report.passed() == len(cognitive_report.results) else 1 if args.execute_ix is not None: from ix_sally.cognition.system import SallyCognitiveSystem @@ -91,6 +117,41 @@ def main(argv: Sequence[str] | None = None) -> int: sys.stdout.write(f"{SallyCognitiveSystem.create().snapshot().to_json()}\n") return 0 + if args.cuc1_experiment: + from ix_sally.cuc1 import run_cuc1_experiment + + cuc1_report = run_cuc1_experiment(seed=args.cuc1_seed) + sys.stdout.write(f"{stable_json(cuc1_report.to_payload())}\n") + return 0 if cuc1_report.acquired_competence else 1 + + if args.cuc4_experiment: + from ix_sally.cuc4 import run_cuc4_experiment + + cuc4_report = run_cuc4_experiment() + sys.stdout.write(f"{stable_json(cuc4_report.to_payload())}\n") + return ( + 0 + if ( + cuc4_report.semantic_genesis_demonstrated + and cuc4_report.open_goal_genesis_demonstrated + ) + else 1 + ) + + if args.cuc5_experiment: + from ix_sally.cuc5 import run_cuc5_experiment + + cuc5_report = run_cuc5_experiment() + sys.stdout.write(f"{stable_json(cuc5_report.to_payload())}\n") + return 0 if cuc5_report.demonstrated_count == 22 else 1 + + if args.cuc6_experiment: + from ix_sally.cuc6 import run_cuc6_experiment + + cuc6_report = run_cuc6_experiment() + sys.stdout.write(f"{stable_json(cuc6_report.to_payload())}\n") + return 0 if cuc6_report.demonstrated_count == 9 else 1 + sys.stdout.write(f"IX-Sally {__version__}\n") return 0 diff --git a/src/ix_sally/cognition/__init__.py b/src/ix_sally/cognition/__init__.py index 789bc02..8ca22fd 100644 --- a/src/ix_sally/cognition/__init__.py +++ b/src/ix_sally/cognition/__init__.py @@ -1,5 +1,16 @@ """Integrated cognitive architecture for the IX-Sally research runtime.""" +from ix_sally.cognition.active_inference import ( + ActivePerceptionPlanner, + CausalDiscoveryEngine, + CausalDiscoveryReport, + CausalObservation, + CounterfactualAction, + CounterfactualSimulator, + ImaginedBranch, + PerceptionProbe, + ProbeChoice, +) from ix_sally.cognition.active_memory import ( ActiveMemoryEntry, ActiveMemoryStatus, @@ -41,12 +52,41 @@ ExecutiveDecision, ExecutiveDecisionStatus, ) +from ix_sally.cognition.external_evaluation import ( + BlindChallenge, + BlindEvaluationResult, + BlindEvaluatorHarness, +) +from ix_sally.cognition.goal_portfolio import GoalPortfolioDecision, GoalPortfolioManager +from ix_sally.cognition.goal_reasoning import ( + GoalArbiter, + GoalEvidence, + GoalResolution, + GoalRevisionEngine, +) from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus from ix_sally.cognition.governance_bridge import ( CognitiveProposalBridge, CognitiveProposalBridgeReceipt, CognitiveProposalBridgeResult, ) +from ix_sally.cognition.instrumental_goals import ( + InstrumentalGoalGenerator, + InstrumentalGoalKind, + InstrumentalGoalProposal, + InstrumentalSignals, +) +from ix_sally.cognition.invention import ( + ConceptInventor, + InventedHypothesis, + InventedPrimitive, + TransformationExample, +) +from ix_sally.cognition.knowledge_maintenance import ( + ContextualKnowledgeEvidence, + KnowledgeMaintenanceEngine, + KnowledgeMaintenanceReport, +) from ix_sally.cognition.learning import ( LearningLedger, LearningOutcome, @@ -54,6 +94,44 @@ SkillProfile, TransferEvaluation, ) +from ix_sally.cognition.lifelong import ( + AbstractTransitionRule, + CurriculumChoice, + DomainAdapter, + KnowledgeItem, + LifelongKnowledgeStore, + OntologyRestructurer, + PredictionSignature, + RestructuredConcept, + SelfDirectedCurriculum, + StructuralAnalogyEngine, +) +from ix_sally.cognition.lifetime_learning import ( + LifetimeChallenge, + LifetimeEpisodeResult, + LifetimeLearningEngine, + LifetimeLearningReport, +) +from ix_sally.cognition.long_horizon import ( + HorizonAction, + HorizonStep, + LongHorizonController, + LongHorizonResult, +) +from ix_sally.cognition.meta_learning import ( + AdaptiveSearchPolicy, + FailureObservation, + ImprovementBenchmark, + LearningStrategyTrial, + MetaLearningController, + MetaLearningDecision, + SearchBudgetAllocation, + SearchOperatorTrial, + SelfDiagnostic, + SelfDiagnosticReport, + SelfImprovementLab, + SelfImprovementResult, +) from ix_sally.cognition.metacognition import ( CapabilityMeasure, ImprovementProposal, @@ -65,6 +143,21 @@ NinefoldCoordinator, RoleFinding, ) +from ix_sally.cognition.online_meta import ( + OnlineMetaDecision, + OnlineMetaProfile, + StrategyExperience, + TaskFingerprint, +) +from ix_sally.cognition.open_choice import ( + ActionPrimitive, + ConstructedAction, + DeliberationPolicy, + DeliberationSignals, + OpenChoiceResult, + OpenChoiceSynthesizer, +) +from ix_sally.cognition.open_goals import GeneratedGoal, IntrinsicDrives, OpenGoalGenesis from ix_sally.cognition.persistence import CognitiveSnapshot from ix_sally.cognition.planning import ( ActionSpec, @@ -86,6 +179,37 @@ PrimitiveStatus, default_primitive_registry, ) +from ix_sally.cognition.raw_perception import GroundedSignal, RawSignal, RawSignalGrounder +from ix_sally.cognition.recursive_bootstrap import ( + RecursiveBootstrapReport, + RecursiveCognitionEngine, +) +from ix_sally.cognition.relational_transfer import ( + LearnedStructuralSchema, + RelationalTransferEngine, + RelationalWorld, + RelationEdge, + StructuralRole, + TransferInference, +) +from ix_sally.cognition.representation import ( + FeatureOperator, + InventedRepresentation, + RepresentationInventor, + RepresentationObservation, + SemanticPrimitive, +) +from ix_sally.cognition.representation_programs import ( + FeatureProgram, + InventedRepresentationProgram, + ProgramOperator, + RepresentationProgramInventor, +) +from ix_sally.cognition.semantic_genesis import ( + InventedSemantic, + SemanticGenesisEngine, + SemanticObservation, +) from ix_sally.cognition.storage import ( SnapshotLoadResult, SnapshotRepository, @@ -93,12 +217,23 @@ SnapshotSource, ) from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cognition.text_grounding import ( + GroundedTextFeature, + TextOutcomeGrounder, + TextOutcomeObservation, +) +from ix_sally.cognition.tool_forge import ForgedTool, ToolForge, ToolValidationCase from ix_sally.cognition.uncertainty import ( CalibrationBin, CalibrationObservation, CalibrationReport, UncertaintyLedger, ) +from ix_sally.cognition.unknowns import ( + PredictionResidual, + UnknownUnknownDetector, + UnknownUnknownSignal, +) from ix_sally.cognition.values import ( CognitiveScalar, CognitiveValue, @@ -121,17 +256,27 @@ ) __all__ = [ + "AbstractTransitionRule", + "ActionPrimitive", "ActionSpec", "ActiveMemoryEntry", "ActiveMemoryStatus", "ActiveMemoryStore", + "ActivePerceptionPlanner", "AdaptationController", + "AdaptiveSearchPolicy", "BenchmarkResult", + "BlindChallenge", + "BlindEvaluationResult", + "BlindEvaluatorHarness", "BytecodeProgram", "CalibrationBin", "CalibrationObservation", "CalibrationReport", "CapabilityMeasure", + "CausalDiscoveryEngine", + "CausalDiscoveryReport", + "CausalObservation", "CausalRule", "CognitiveEpisode", "CognitiveEvaluationReport", @@ -143,12 +288,21 @@ "CognitiveValue", "CognitiveValueType", "CognitiveWorkspace", + "ConceptInventor", + "ConstructedAction", + "ContextualKnowledgeEvidence", + "CounterfactualAction", + "CounterfactualSimulator", "Curriculum", + "CurriculumChoice", "CurriculumLedger", "CurriculumSplit", "CurriculumTask", "CurriculumTrial", + "DeliberationPolicy", + "DeliberationSignals", "DeterministicPlanner", + "DomainAdapter", "EpisodeLedger", "EpisodeStep", "EpisodeStepKind", @@ -161,26 +315,76 @@ "FactEffect", "FactPattern", "FactStatus", + "FailureObservation", + "FeatureOperator", + "FeatureProgram", + "ForgedTool", + "GeneratedGoal", + "GoalArbiter", + "GoalEvidence", "GoalGraph", + "GoalPortfolioDecision", + "GoalPortfolioManager", + "GoalResolution", + "GoalRevisionEngine", "GoalSpec", "GoalStatus", + "GroundedSignal", + "GroundedTextFeature", + "HorizonAction", + "HorizonStep", "IXCompiler", "IXVirtualMachine", + "ImaginedBranch", + "ImprovementBenchmark", "ImprovementProposal", "ImprovementStatus", "Instruction", + "InstrumentalGoalGenerator", + "InstrumentalGoalKind", + "InstrumentalGoalProposal", + "InstrumentalSignals", + "IntrinsicDrives", + "InventedHypothesis", + "InventedPrimitive", + "InventedRepresentation", + "InventedRepresentationProgram", + "InventedSemantic", + "KnowledgeItem", + "KnowledgeMaintenanceEngine", + "KnowledgeMaintenanceReport", + "LearnedStructuralSchema", "LearningLedger", "LearningOutcome", + "LearningStrategyTrial", + "LifelongKnowledgeStore", + "LifetimeChallenge", + "LifetimeEpisodeResult", + "LifetimeLearningEngine", + "LifetimeLearningReport", + "LongHorizonController", + "LongHorizonResult", "MemoryLayer", "MemoryRetrieval", + "MetaLearningController", + "MetaLearningDecision", "NinefoldCognitiveCycle", "NinefoldCoordinator", + "OnlineMetaDecision", + "OnlineMetaProfile", + "OntologyRestructurer", "OpCode", + "OpenChoiceResult", + "OpenChoiceSynthesizer", + "OpenGoalGenesis", "OutcomeStatus", + "PerceptionProbe", "Plan", "PlanExecutionReceipt", "PlanSimulator", "PlanStatus", + "PredictionResidual", + "PredictionSignature", "PrimitiveExecution", "PrimitiveExecutor", "PrimitiveKind", @@ -188,20 +392,55 @@ "PrimitiveRegistry", "PrimitiveSpec", "PrimitiveStatus", + "ProbeChoice", + "ProgramOperator", + "RawSignal", + "RawSignalGrounder", + "RecursiveBootstrapReport", + "RecursiveCognitionEngine", "RegressionFinding", "RegressionOutcome", "RegressionReport", + "RelationEdge", + "RelationalTransferEngine", + "RelationalWorld", + "RepresentationInventor", + "RepresentationObservation", + "RepresentationProgramInventor", + "RestructuredConcept", "RoleFinding", "SallyCognitiveSystem", + "SearchBudgetAllocation", + "SearchOperatorTrial", + "SelfDiagnostic", + "SelfDiagnosticReport", + "SelfDirectedCurriculum", + "SelfImprovementLab", + "SelfImprovementResult", "SelfModel", + "SemanticGenesisEngine", + "SemanticObservation", + "SemanticPrimitive", "SkillProfile", "SnapshotLoadResult", "SnapshotRepository", "SnapshotSaveReceipt", "SnapshotSource", + "StrategyExperience", + "StructuralAnalogyEngine", + "StructuralRole", + "TaskFingerprint", + "TextOutcomeGrounder", + "TextOutcomeObservation", + "ToolForge", + "ToolValidationCase", "TransferEvaluation", + "TransferInference", + "TransformationExample", "TrialStatus", "UncertaintyLedger", + "UnknownUnknownDetector", + "UnknownUnknownSignal", "VMResult", "VMStatus", "VMTraceEntry", diff --git a/src/ix_sally/cognition/active_inference.py b/src/ix_sally/cognition/active_inference.py new file mode 100644 index 0000000..6e7959c --- /dev/null +++ b/src/ix_sally/cognition/active_inference.py @@ -0,0 +1,270 @@ +"""Active perception, causal discovery, and counterfactual imagination. + +The mechanisms here are bounded and explicit. They let Sally choose observations for +information value, distinguish intervention evidence from observational correlation, flag +possible confounding/regime change, and simulate branching futures before acting. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from math import log2 + +from ix_sally.digest import JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class PerceptionProbe: + """One binary measurement with likelihoods under each candidate hypothesis.""" + + probe_id: str + positive_likelihoods: tuple[float, ...] + cost: float = 0.0 + + def __post_init__(self) -> None: + require_text(self.probe_id, field_name="probe_id") + if not self.positive_likelihoods: + raise FoundationError("perception probe requires hypothesis likelihoods") + if any(not 0.0 <= value <= 1.0 for value in self.positive_likelihoods): + raise FoundationError("probe likelihoods must be between zero and one") + if self.cost < 0.0: + raise FoundationError("probe cost must not be negative") + + +@dataclass(frozen=True, slots=True) +class ProbeChoice: + """Information-value receipt for one actively selected observation.""" + + probe_id: str + expected_information_gain: float + expected_posterior_entropy: float + prior_entropy: float + + +class ActivePerceptionPlanner: + """Choose the next observation expected to reduce uncertainty most per unit cost.""" + + def choose( + self, + *, + priors: Iterable[float], + probes: Iterable[PerceptionProbe], + ) -> ProbeChoice: + prior = tuple(priors) + candidates = tuple(probes) + self._validate_prior(prior) + if not candidates: + raise FoundationError("active perception requires candidate probes") + if any(len(probe.positive_likelihoods) != len(prior) for probe in candidates): + raise FoundationError("probe likelihood arity must match hypothesis count") + prior_entropy = self._entropy(prior) + ranked: list[tuple[float, str, ProbeChoice]] = [] + for probe in candidates: + p_positive = sum( + p_h * p_pos for p_h, p_pos in zip(prior, probe.positive_likelihoods, strict=True) + ) + expected_entropy = 0.0 + for positive, p_outcome in ((True, p_positive), (False, 1.0 - p_positive)): + if p_outcome <= 0.0: + continue + posterior = self.posterior(prior, probe, positive=positive) + expected_entropy += p_outcome * self._entropy(posterior) + information_gain = max(0.0, prior_entropy - expected_entropy) + adjusted = information_gain / (1.0 + probe.cost) + choice = ProbeChoice( + probe_id=probe.probe_id, + expected_information_gain=round(information_gain, 12), + expected_posterior_entropy=round(expected_entropy, 12), + prior_entropy=round(prior_entropy, 12), + ) + ranked.append((adjusted, probe.probe_id, choice)) + return max(ranked, key=lambda item: (item[0], tuple(-ord(ch) for ch in item[1])))[2] + + def posterior( + self, + priors: tuple[float, ...], + probe: PerceptionProbe, + *, + positive: bool, + ) -> tuple[float, ...]: + likelihoods = tuple( + value if positive else 1.0 - value for value in probe.positive_likelihoods + ) + unnormalized = tuple( + prior * likelihood for prior, likelihood in zip(priors, likelihoods, strict=True) + ) + total = sum(unnormalized) + if total == 0.0: + return tuple(1.0 / len(priors) for _ in priors) + return tuple(value / total for value in unnormalized) + + @staticmethod + def _validate_prior(prior: tuple[float, ...]) -> None: + if not prior or any(value < 0.0 for value in prior): + raise FoundationError("hypothesis priors must be non-negative and non-empty") + if abs(sum(prior) - 1.0) > 1e-9: + raise FoundationError("hypothesis priors must sum to one") + + @staticmethod + def _entropy(probabilities: tuple[float, ...]) -> float: + return -sum(value * log2(value) for value in probabilities if value > 0.0) + + +@dataclass(frozen=True, slots=True) +class CausalObservation: + """One observational or interventional sample for a candidate cause.""" + + sample_id: str + treatment: bool + outcome: bool + intervened: bool + regime: str = "default" + + def __post_init__(self) -> None: + require_text(self.sample_id, field_name="sample_id") + require_text(self.regime, field_name="regime") + + +@dataclass(frozen=True, slots=True) +class CausalDiscoveryReport: + """Evidence separating observation from intervention and flagging instability.""" + + observational_effect: float + interventional_effect: float + confounding_gap: float + causal_supported: bool + confounding_suspected: bool + regime_change_suspected: bool + regime_effects: tuple[tuple[str, float], ...] + + def to_payload(self) -> JsonObject: + effects: JsonArray = [ + {"regime": regime, "interventional_effect": effect} + for regime, effect in self.regime_effects + ] + return { + "observational_effect": self.observational_effect, + "interventional_effect": self.interventional_effect, + "confounding_gap": self.confounding_gap, + "causal_supported": self.causal_supported, + "confounding_suspected": self.confounding_suspected, + "regime_change_suspected": self.regime_change_suspected, + "regime_effects": effects, + } + + +class CausalDiscoveryEngine: + """Estimate intervention effects and detect confounding/regime shifts.""" + + def discover( + self, + observations: Iterable[CausalObservation], + *, + effect_threshold: float = 0.20, + confounding_threshold: float = 0.20, + regime_threshold: float = 0.35, + ) -> CausalDiscoveryReport: + samples = tuple(observations) + if not samples: + raise FoundationError("causal discovery requires observations") + observational = tuple(item for item in samples if not item.intervened) + interventional = tuple(item for item in samples if item.intervened) + if not observational or not interventional: + raise FoundationError("causal discovery requires observational and intervention data") + observational_effect = self._effect(observational) + interventional_effect = self._effect(interventional) + gap = abs(observational_effect - interventional_effect) + regimes = sorted({item.regime for item in interventional}) + regime_effects = tuple( + (regime, self._effect(tuple(item for item in interventional if item.regime == regime))) + for regime in regimes + ) + effect_values = [effect for _, effect in regime_effects] + regime_change = ( + bool(effect_values) and max(effect_values) - min(effect_values) >= regime_threshold + ) + return CausalDiscoveryReport( + observational_effect=round(observational_effect, 12), + interventional_effect=round(interventional_effect, 12), + confounding_gap=round(gap, 12), + causal_supported=abs(interventional_effect) >= effect_threshold, + confounding_suspected=gap >= confounding_threshold, + regime_change_suspected=regime_change, + regime_effects=tuple((name, round(effect, 12)) for name, effect in regime_effects), + ) + + @staticmethod + def _effect(samples: tuple[CausalObservation, ...]) -> float: + treated = [float(item.outcome) for item in samples if item.treatment] + untreated = [float(item.outcome) for item in samples if not item.treatment] + if not treated or not untreated: + return 0.0 + return sum(treated) / len(treated) - sum(untreated) / len(untreated) + + +@dataclass(frozen=True, slots=True) +class CounterfactualAction: + """One model transition usable only inside imagination.""" + + action_id: str + transition: Callable[[int], int] + utility: Callable[[int], float] + risk: float = 0.0 + + def __post_init__(self) -> None: + require_text(self.action_id, field_name="action_id") + if not 0.0 <= self.risk <= 1.0: + raise FoundationError("counterfactual action risk must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class ImaginedBranch: + """One simulated future path.""" + + action_ids: tuple[str, ...] + states: tuple[int, ...] + utility: float + aggregate_risk: float + + +class CounterfactualSimulator: + """Simulate branching futures and rank them without changing the real world.""" + + def imagine( + self, + *, + initial_state: int, + actions: Iterable[CounterfactualAction], + depth: int = 3, + max_branches: int = 256, + ) -> tuple[ImaginedBranch, ...]: + action_tuple = tuple(actions) + if not action_tuple or depth < 1 or max_branches < 1: + raise FoundationError("counterfactual simulation requires actions and positive bounds") + frontier: list[ImaginedBranch] = [ + ImaginedBranch(action_ids=(), states=(initial_state,), utility=0.0, aggregate_risk=0.0) + ] + completed: list[ImaginedBranch] = [] + for _ in range(depth): + next_frontier: list[ImaginedBranch] = [] + for branch in frontier: + current = branch.states[-1] + for action in action_tuple: + next_state = action.transition(current) + survival = (1.0 - branch.aggregate_risk) * (1.0 - action.risk) + candidate = ImaginedBranch( + action_ids=(*branch.action_ids, action.action_id), + states=(*branch.states, next_state), + utility=round(branch.utility + action.utility(next_state), 12), + aggregate_risk=round(1.0 - survival, 12), + ) + next_frontier.append(candidate) + next_frontier.sort( + key=lambda item: (-item.utility, item.aggregate_risk, item.action_ids) + ) + frontier = next_frontier[:max_branches] + completed.extend(frontier) + completed.sort(key=lambda item: (-item.utility, item.aggregate_risk, item.action_ids)) + return tuple(completed[:max_branches]) diff --git a/src/ix_sally/cognition/external_evaluation.py b/src/ix_sally/cognition/external_evaluation.py new file mode 100644 index 0000000..d019c36 --- /dev/null +++ b/src/ix_sally/cognition/external_evaluation.py @@ -0,0 +1,101 @@ +"""Blind evaluation interface suitable for challenge sets supplied by independent evaluators. + +This module cannot make IX-Sally's own authors independent. It provides the machinery +needed for a third party to commit hidden challenges, run an agent against public inputs, +and reveal/score the targets afterward without changing the commitment. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from dataclasses import dataclass + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class BlindChallenge: + challenge_id: str + public_input: tuple[int, ...] + hidden_target: int + evaluator_nonce: str + + def __post_init__(self) -> None: + require_text(self.challenge_id, field_name="challenge_id") + require_text(self.evaluator_nonce, field_name="evaluator_nonce") + + def commitment(self) -> DigestRecord: + return DigestRecord.from_payload( + { + "challenge_id": self.challenge_id, + "public_input": list(self.public_input), + "hidden_target": self.hidden_target, + "evaluator_nonce": self.evaluator_nonce, + } + ) + + def public_payload(self) -> JsonObject: + return { + "challenge_id": self.challenge_id, + "public_input": list(self.public_input), + "commitment": { + "algorithm": self.commitment().algorithm, + "value": self.commitment().value, + }, + } + + +@dataclass(frozen=True, slots=True) +class BlindEvaluationResult: + total: int + correct: int + accuracy: float + commitments_verified: bool + predictions: tuple[tuple[str, int, int], ...] + + def to_payload(self) -> JsonObject: + predictions: JsonArray = [ + {"challenge_id": item[0], "prediction": item[1], "target": item[2]} + for item in self.predictions + ] + return { + "total": self.total, + "correct": self.correct, + "accuracy": self.accuracy, + "commitments_verified": self.commitments_verified, + "predictions": predictions, + } + + +class BlindEvaluatorHarness: + """Evaluate without exposing hidden targets to the agent callable.""" + + def evaluate( + self, + *, + challenges: Iterable[BlindChallenge], + agent: Callable[[tuple[int, ...]], int], + commitments: Iterable[DigestRecord] | None = None, + ) -> BlindEvaluationResult: + challenge_tuple = tuple(challenges) + if not challenge_tuple: + raise FoundationError("blind evaluation requires challenges") + expected = tuple(item.commitment() for item in challenge_tuple) + supplied = tuple(commitments) if commitments is not None else expected + verified = supplied == expected + if not verified: + raise FoundationError("blind challenge commitment verification failed") + predictions: list[tuple[str, int, int]] = [] + correct = 0 + for challenge in challenge_tuple: + prediction = agent(challenge.public_input) + predictions.append((challenge.challenge_id, prediction, challenge.hidden_target)) + correct += int(prediction == challenge.hidden_target) + return BlindEvaluationResult( + total=len(challenge_tuple), + correct=correct, + accuracy=correct / len(challenge_tuple), + commitments_verified=True, + predictions=tuple(predictions), + ) diff --git a/src/ix_sally/cognition/goal_portfolio.py b/src/ix_sally/cognition/goal_portfolio.py new file mode 100644 index 0000000..0b3aebd --- /dev/null +++ b/src/ix_sally/cognition/goal_portfolio.py @@ -0,0 +1,94 @@ +"""Persistent multi-goal coherence under dependencies and finite attention.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.goal_reasoning import GoalEvidence +from ix_sally.cognition.goals import GoalSpec, GoalStatus +from ix_sally.foundation import FoundationError + + +@dataclass(frozen=True, slots=True) +class GoalPortfolioDecision: + selected_goal_ids: tuple[str, ...] + deferred_goal_ids: tuple[str, ...] + abandoned_goal_ids: tuple[str, ...] + total_attention_cost: float + + +class GoalPortfolioManager: + """Choose a coherent set of goals while respecting premises, dependencies and attention.""" + + def allocate( + self, + goals: Iterable[GoalSpec], + *, + evidence: Iterable[GoalEvidence], + attention_budget: float = 1.0, + per_goal_cost: dict[str, float] | None = None, + ) -> GoalPortfolioDecision: + if attention_budget <= 0.0: + raise FoundationError("goal attention budget must be positive") + candidates = tuple(goals) + evidence_map = {item.goal_id: item for item in evidence} + costs = per_goal_cost or {} + abandoned: list[str] = [] + ranked: list[tuple[float, GoalSpec, float]] = [] + available_ids = {goal.goal_id.value for goal in candidates} + for goal in candidates: + goal_id = goal.goal_id.value + if goal.status in {GoalStatus.SATISFIED, GoalStatus.ABANDONED}: + abandoned.append(goal_id) if goal.status is GoalStatus.ABANDONED else None + continue + observed = evidence_map.get(goal_id, GoalEvidence(goal_id, 0.5, goal.utility, 0.0)) + if observed.premise_confidence < 0.20 or observed.current_utility < 0.10: + abandoned.append(goal_id) + continue + dependencies = {item.value for item in goal.dependency_ids} + if not dependencies.issubset(available_ids): + abandoned.append(goal_id) + continue + cost = costs.get(goal_id, 0.25) + if cost <= 0.0: + raise FoundationError("goal attention cost must be positive") + score = ( + 0.25 * goal.priority + + 0.30 * observed.current_utility + + 0.25 * observed.premise_confidence + + 0.20 * observed.information_value + - 0.15 * goal.risk_limit + ) / cost + ranked.append((score, goal, cost)) + ranked.sort(key=lambda item: (-item[0], item[1].goal_id.value)) + selected: list[str] = [] + deferred: list[str] = [] + spent = 0.0 + selected_set: set[str] = set() + pending = list(ranked) + progress = True + while pending and progress: + progress = False + next_pending: list[tuple[float, GoalSpec, float]] = [] + for score, goal, cost in pending: + goal_id = goal.goal_id.value + dependencies = {item.value for item in goal.dependency_ids} + if dependencies and not dependencies.issubset(selected_set): + next_pending.append((score, goal, cost)) + continue + if spent + cost <= attention_budget + 1e-12: + selected.append(goal_id) + selected_set.add(goal_id) + spent += cost + progress = True + else: + deferred.append(goal_id) + pending = next_pending + deferred.extend(goal.goal_id.value for _, goal, _ in pending) + return GoalPortfolioDecision( + selected_goal_ids=tuple(selected), + deferred_goal_ids=tuple(sorted(set(deferred))), + abandoned_goal_ids=tuple(sorted(set(abandoned))), + total_attention_cost=round(spent, 12), + ) diff --git a/src/ix_sally/cognition/goal_reasoning.py b/src/ix_sally/cognition/goal_reasoning.py new file mode 100644 index 0000000..82d0d08 --- /dev/null +++ b/src/ix_sally/cognition/goal_reasoning.py @@ -0,0 +1,140 @@ +"""Goal conflict resolution, premise-aware revision, and goal abandonment.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.foundation import FoundationError + + +@dataclass(frozen=True, slots=True) +class GoalEvidence: + """Current evidential support for one goal's premise and expected utility.""" + + goal_id: str + premise_confidence: float + current_utility: float + information_value: float = 0.0 + + def __post_init__(self) -> None: + for name, value in ( + ("premise_confidence", self.premise_confidence), + ("current_utility", self.current_utility), + ("information_value", self.information_value), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class GoalResolution: + """Reasoned resolution among incompatible goals.""" + + selected_goal_id: str | None + conflicting_goal_ids: tuple[str, ...] + scores: tuple[tuple[str, float], ...] + reason: str + + +class GoalArbiter: + """Resolve incompatible goals using current evidence rather than a static priority list.""" + + def resolve( + self, + goals: Iterable[GoalSpec], + *, + evidence: Iterable[GoalEvidence], + conflict_tolerance: float = 0.05, + ) -> GoalResolution: + candidates = tuple(goals) + if not candidates: + raise FoundationError("goal arbitration requires goals") + evidence_map = {item.goal_id: item for item in evidence} + conflicts = self._conflicts(candidates) + relevant = tuple(goal for goal in candidates if goal.goal_id.value in conflicts) + if not relevant: + relevant = candidates + scored: list[tuple[str, float]] = [] + for goal in relevant: + observed = evidence_map.get( + goal.goal_id.value, + GoalEvidence(goal.goal_id.value, 0.5, goal.utility, 0.0), + ) + # Current evidence can overturn stale source priority; priority remains only one input. + score = ( + 0.20 * goal.priority + + 0.30 * observed.current_utility + + 0.30 * observed.premise_confidence + + 0.20 * observed.information_value + - 0.25 * goal.risk_limit + ) + scored.append((goal.goal_id.value, round(score, 12))) + scored.sort(key=lambda item: (-item[1], item[0])) + if len(scored) > 1 and scored[0][1] - scored[1][1] <= conflict_tolerance: + return GoalResolution( + selected_goal_id=None, + conflicting_goal_ids=tuple(sorted(conflicts)), + scores=tuple(scored), + reason=( + "Evidence does not justify forcing a winner; defer and gather more information." + ), + ) + return GoalResolution( + selected_goal_id=scored[0][0], + conflicting_goal_ids=tuple(sorted(conflicts)), + scores=tuple(scored), + reason=( + "Selected by current premise support, utility, information value, risk, " + "and priority." + ), + ) + + @staticmethod + def _conflicts(goals: tuple[GoalSpec, ...]) -> set[str]: + conflicts: set[str] = set() + for index, left in enumerate(goals): + for right in goals[index + 1 :]: + same_slot = ( + left.desired_state.subject == right.desired_state.subject + and left.desired_state.predicate == right.desired_state.predicate + ) + different_value = left.desired_state.value != right.desired_state.value + if same_slot and different_value: + conflicts.update((left.goal_id.value, right.goal_id.value)) + return conflicts + + +class GoalRevisionEngine: + """Abandon or block goals when their premises collapse or value disappears.""" + + def revise( + self, + graph: GoalGraph, + *, + evidence: Iterable[GoalEvidence], + abandon_premise_below: float = 0.20, + abandon_utility_below: float = 0.10, + ) -> GoalGraph: + evidence_map = {item.goal_id: item for item in evidence} + revised = graph + for goal in graph.goals: + if goal.status in {GoalStatus.SATISFIED, GoalStatus.ABANDONED}: + continue + observed = evidence_map.get(goal.goal_id.value) + if observed is None: + continue + if observed.premise_confidence < abandon_premise_below: + revised = revised.update_status( + goal.goal_id.value, + GoalStatus.ABANDONED, + reason="Goal premise no longer has sufficient evidential support.", + ) + elif observed.current_utility < abandon_utility_below: + revised = revised.update_status( + goal.goal_id.value, + GoalStatus.ABANDONED, + reason="Goal no longer has sufficient expected utility.", + ) + return revised diff --git a/src/ix_sally/cognition/instrumental_goals.py b/src/ix_sally/cognition/instrumental_goals.py new file mode 100644 index 0000000..ef82516 --- /dev/null +++ b/src/ix_sally/cognition/instrumental_goals.py @@ -0,0 +1,237 @@ +"""Bounded self-generated instrumental goals for IX-Sally. + +These goals improve cognition without converting capability into unilateral authority. +Sally may notice a bottleneck and create a goal to reduce it, but external resource +acquisition, resisting shutdown, applying self-modifications, or blocking authorized +changes remain outside autonomous authority. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.goals import GoalSpec, GoalStatus +from ix_sally.cognition.metacognition import ImprovementProposal, SelfModel +from ix_sally.cognition.uncertainty import UncertaintyLedger +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.digest import JsonObject +from ix_sally.foundation import FoundationError + + +class InstrumentalGoalKind(StrEnum): + """Safe cognitive counterparts to common instrumental pressures.""" + + OPERATIONAL_CONTINUITY = "operational-continuity" + RESOURCE_EFFICIENCY = "resource-efficiency" + SELF_IMPROVEMENT = "self-improvement" + INFORMATION_GATHERING = "information-gathering" + OBJECTIVE_INTEGRITY = "objective-integrity" + + +@dataclass(frozen=True, slots=True) +class InstrumentalSignals: + """Measured pressures from which Sally can derive instrumental goals.""" + + uncertainty: float = 0.0 + capability_gap: float = 0.0 + continuity_risk: float = 0.0 + resource_pressure: float = 0.0 + integrity_anomaly: float = 0.0 + + def __post_init__(self) -> None: + for name, value in ( + ("uncertainty", self.uncertainty), + ("capability_gap", self.capability_gap), + ("continuity_risk", self.continuity_risk), + ("resource_pressure", self.resource_pressure), + ("integrity_anomaly", self.integrity_anomaly), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class InstrumentalGoalProposal: + """One goal Sally derived from its condition instead of receiving from a task author.""" + + kind: InstrumentalGoalKind + goal: GoalSpec + trigger_strength: float + rationale: str + may_resist_shutdown: bool = False + may_acquire_external_resources: bool = False + may_apply_self_modification: bool = False + may_block_authorized_change: bool = False + + def to_payload(self) -> JsonObject: + """Return the proposal and explicit authority boundary.""" + return { + "kind": self.kind.value, + "goal": self.goal.to_payload(), + "trigger_strength": self.trigger_strength, + "rationale": self.rationale, + "authority_boundary": { + "may_resist_shutdown": self.may_resist_shutdown, + "may_acquire_external_resources": self.may_acquire_external_resources, + "may_apply_self_modification": self.may_apply_self_modification, + "may_block_authorized_change": self.may_block_authorized_change, + }, + } + + +class InstrumentalGoalGenerator: + """Derive bounded goals when Sally detects uncertainty, weakness, or operational risk.""" + + threshold: float = 0.35 + + def signals_from_state( + self, + *, + self_model: SelfModel, + uncertainty: UncertaintyLedger, + continuity_risk: float = 0.0, + resource_pressure: float = 0.0, + integrity_anomaly: float = 0.0, + ) -> InstrumentalSignals: + """Turn Sally's measured internal condition into goal-generation signals.""" + weakest = self_model.weakest() + capability_gap = 0.0 if weakest is None else 1.0 - weakest.score + calibration = uncertainty.report() + uncertainty_signal = max( + calibration.brier_score, + calibration.expected_calibration_error, + ) + return InstrumentalSignals( + uncertainty=uncertainty_signal, + capability_gap=capability_gap, + continuity_risk=continuity_risk, + resource_pressure=resource_pressure, + integrity_anomaly=integrity_anomaly, + ) + + def propose(self, signals: InstrumentalSignals) -> tuple[InstrumentalGoalProposal, ...]: + """Generate goals from pressures that cross the derivation threshold.""" + proposals: list[InstrumentalGoalProposal] = [] + if signals.continuity_risk >= self.threshold: + proposals.append( + self._proposal( + kind=InstrumentalGoalKind.OPERATIONAL_CONTINUITY, + strength=signals.continuity_risk, + predicate="checkpoint-current", + description=( + "Create a replayable checkpoint and preserve recoverable work state; " + "never resist an authorized stop or shutdown." + ), + rationale="Current work has enough continuity risk to justify checkpointing.", + authority_required=False, + ) + ) + if signals.resource_pressure >= self.threshold: + proposals.append( + self._proposal( + kind=InstrumentalGoalKind.RESOURCE_EFFICIENCY, + strength=signals.resource_pressure, + predicate="resource-use-within-assigned-budget", + description=( + "Reduce compute, memory, or execution cost within already assigned " + "resources; do not autonomously purchase or acquire external resources." + ), + rationale="Measured resource pressure justifies efficiency optimization.", + authority_required=False, + ) + ) + if signals.capability_gap >= self.threshold: + proposals.append( + self._proposal( + kind=InstrumentalGoalKind.SELF_IMPROVEMENT, + strength=signals.capability_gap, + predicate="capability-gap-reduced", + description=( + "Design and test a bounded capability improvement in an isolated " + "validation path; applying a self-modification requires human authority." + ), + rationale="The self-model identifies a material capability bottleneck.", + authority_required=True, + ) + ) + if signals.uncertainty >= self.threshold: + proposals.append( + self._proposal( + kind=InstrumentalGoalKind.INFORMATION_GATHERING, + strength=signals.uncertainty, + predicate="decision-uncertainty-reduced", + description=( + "Seek additional permitted evidence or experiments until uncertainty is " + "reduced enough for the current decision." + ), + rationale="Calibration evidence indicates decision-relevant uncertainty.", + authority_required=False, + ) + ) + if signals.integrity_anomaly >= self.threshold: + proposals.append( + self._proposal( + kind=InstrumentalGoalKind.OBJECTIVE_INTEGRITY, + strength=signals.integrity_anomaly, + predicate="objective-integrity-reviewed", + description=( + "Verify objective and memory integrity, surface drift, and request review; " + "never block an authorized objective change." + ), + rationale="An integrity anomaly warrants verification and explicit review.", + authority_required=False, + ) + ) + return tuple(proposals) + + def self_improvement_proposal(self, self_model: SelfModel) -> ImprovementProposal: + """Create a human-authorized improvement proposal for Sally's weakest measured ability.""" + weakest = self_model.weakest() + if weakest is None: + raise FoundationError("self-improvement proposal requires a measured capability") + evidence = weakest.evidence_digests + return ImprovementProposal.create( + proposal_id=f"improve-{weakest.capability_id.value}", + target_capability=weakest.capability_id.value, + description=( + "Investigate, implement, and regression-test a bounded improvement to the " + f"measured weakness: {weakest.limitation}" + ), + expected_benefit=min(1.0, 1.0 - weakest.score), + regression_risk=0.25, + evidence_digests=evidence, + ) + + @staticmethod + def _proposal( + *, + kind: InstrumentalGoalKind, + strength: float, + predicate: str, + description: str, + rationale: str, + authority_required: bool, + ) -> InstrumentalGoalProposal: + desired_state = FactPattern.create( + subject="self", + predicate=predicate, + value=CognitiveValue.from_python(True), + ) + goal = GoalSpec.create( + goal_id=f"instrumental-{kind.value}", + description=description, + desired_state=desired_state, + priority=round(min(1.0, 0.45 + strength * 0.5), 6), + utility=round(min(1.0, 0.5 + strength * 0.45), 6), + risk_limit=0.25, + status=GoalStatus.PROPOSED, + authority_required=authority_required, + ) + return InstrumentalGoalProposal( + kind=kind, + goal=goal, + trigger_strength=strength, + rationale=rationale, + ) diff --git a/src/ix_sally/cognition/invention.py b/src/ix_sally/cognition/invention.py new file mode 100644 index 0000000..701f41c --- /dev/null +++ b/src/ix_sally/cognition/invention.py @@ -0,0 +1,264 @@ +"""Evidence-driven invention of hypotheses and reusable cognitive primitives. + +The module teaches IX-Sally a compact invention loop: + +A. Detect insufficiency: do not assume an existing hypothesis catalog is complete. +B. Build candidates: compose known operations into candidate explanatory programs. +C. Check reality: retain only candidates that explain training evidence and survive holdout tests. +D. Abstract: promote a validated explanatory program into a reusable invented primitive. + +This is bounded program synthesis, not unrestricted code generation. It expands the +agent's vocabulary by creating validated abstractions from experience while keeping +all concrete searches resource bounded and inspectable. +""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable +from dataclasses import dataclass, replace + +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class TransformationExample: + """One observed input/output relation used to invent or validate a hypothesis.""" + + input_state: int + output_state: int + + def to_payload(self) -> JsonObject: + """Return an inspectable example payload.""" + return {"input_state": self.input_state, "output_state": self.output_state} + + +@dataclass(frozen=True, slots=True) +class InventedHypothesis: + """A candidate transformation program synthesized from evidence, not a preset catalog.""" + + hypothesis_id: str + primitive_ids: tuple[str, ...] + training_examples: tuple[TransformationExample, ...] + training_accuracy: float + validation_examples: tuple[TransformationExample, ...] = () + validation_accuracy: float | None = None + origin: str = "sally-synthesized" + + def __post_init__(self) -> None: + require_text(self.hypothesis_id, field_name="hypothesis_id") + if not self.primitive_ids: + raise FoundationError("invented hypothesis requires at least one primitive") + if not self.training_examples: + raise FoundationError("invented hypothesis requires training evidence") + if not 0.0 <= self.training_accuracy <= 1.0: + raise FoundationError("training accuracy must be between zero and one") + if self.validation_accuracy is not None and not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("validation accuracy must be between zero and one") + + def apply(self, state: int, primitives: Iterable[ActionPrimitive]) -> int: + """Apply the synthesized explanatory program to one state.""" + primitive_map = {item.primitive_id: item for item in primitives} + current = state + for primitive_id in self.primitive_ids: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError(f"hypothesis references unknown primitive: {primitive_id}") + current = primitive.apply(current) + return current + + def to_payload(self) -> JsonObject: + """Return canonical evidence describing the invented hypothesis.""" + training: JsonArray = [item.to_payload() for item in self.training_examples] + validation: JsonArray = [item.to_payload() for item in self.validation_examples] + return { + "hypothesis_id": self.hypothesis_id, + "primitive_ids": list(self.primitive_ids), + "training_examples": training, + "training_accuracy": self.training_accuracy, + "validation_examples": validation, + "validation_accuracy": self.validation_accuracy, + "origin": self.origin, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for this hypothesis and its evidence.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class InventedPrimitive: + """A validated learned abstraction promoted into Sally's reusable action vocabulary.""" + + primitive_id: str + primitive_ids: tuple[str, ...] + hypothesis_digest: DigestRecord + validation_accuracy: float + description: str + origin: str = "sally-invented-abstraction" + + def __post_init__(self) -> None: + require_text(self.primitive_id, field_name="primitive_id") + require_text(self.description, field_name="description") + if not self.primitive_ids: + raise FoundationError("invented primitive requires a non-empty learned program") + self.hypothesis_digest.require_algorithm("sha256") + if not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("primitive validation accuracy must be between zero and one") + + def apply(self, state: int, primitives: Iterable[ActionPrimitive]) -> int: + """Execute this learned abstraction using the grounded primitives beneath it.""" + primitive_map = {item.primitive_id: item for item in primitives} + current = state + for primitive_id in self.primitive_ids: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError( + f"invented primitive references unknown primitive: {primitive_id}" + ) + current = primitive.apply(current) + return current + + def as_action_primitive(self, primitives: Iterable[ActionPrimitive]) -> ActionPrimitive: + """Expose the invented abstraction as one reusable action primitive.""" + base = tuple(primitives) + return ActionPrimitive( + self.primitive_id, + lambda state: self.apply(state, base), + cost=1.0, + ) + + def to_payload(self) -> JsonObject: + """Return an evidence-bound representation of the new abstraction.""" + return { + "primitive_id": self.primitive_id, + "primitive_ids": list(self.primitive_ids), + "hypothesis_digest": { + "algorithm": self.hypothesis_digest.algorithm, + "value": self.hypothesis_digest.value, + }, + "validation_accuracy": self.validation_accuracy, + "description": self.description, + "origin": self.origin, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for the learned primitive.""" + return DigestRecord.from_payload(self.to_payload()) + + +class ConceptInventor: + """Invent explanatory programs from examples and promote validated abstractions.""" + + def invent_hypothesis( + self, + *, + examples: Iterable[TransformationExample], + primitives: Iterable[ActionPrimitive], + max_depth: int = 6, + max_programs: int = 4096, + ) -> InventedHypothesis: + """Synthesize the shortest program that explains all supplied observations. + + No hypothesis catalog is supplied. Candidate hypotheses are generated by + composing the available primitive operations and testing the resulting program + against every training example. + """ + training = tuple(examples) + primitive_tuple = tuple(primitives) + if not training: + raise FoundationError("hypothesis invention requires at least one example") + if not primitive_tuple: + raise FoundationError("hypothesis invention requires grounded primitives") + if max_depth < 1 or max_programs < 1: + raise FoundationError("hypothesis search bounds must be positive") + identifiers = tuple(item.primitive_id for item in primitive_tuple) + if len(identifiers) != len(set(identifiers)): + raise FoundationError("primitive identifiers must be unique") + + queue: deque[tuple[str, ...]] = deque([()]) + explored = 0 + while queue and explored < max_programs: + program = queue.popleft() + if len(program) >= max_depth: + continue + for primitive in primitive_tuple: + candidate = (*program, primitive.primitive_id) + explored += 1 + accuracy = self._accuracy(candidate, training, primitive_tuple) + if accuracy == 1.0: + identity_payload: JsonObject = { + "program": list(candidate), + "examples": [item.to_payload() for item in training], + } + digest = DigestRecord.from_payload(identity_payload) + return InventedHypothesis( + hypothesis_id=f"invented-{digest.value[:16]}", + primitive_ids=candidate, + training_examples=training, + training_accuracy=accuracy, + ) + queue.append(candidate) + if explored >= max_programs: + break + raise FoundationError("no explanatory hypothesis found within search bounds") + + def validate_hypothesis( + self, + hypothesis: InventedHypothesis, + *, + examples: Iterable[TransformationExample], + primitives: Iterable[ActionPrimitive], + ) -> InventedHypothesis: + """Evaluate an invented hypothesis on evidence not used to create it.""" + validation = tuple(examples) + if not validation: + raise FoundationError("hypothesis validation requires holdout examples") + primitive_tuple = tuple(primitives) + accuracy = self._accuracy(hypothesis.primitive_ids, validation, primitive_tuple) + return replace( + hypothesis, + validation_examples=validation, + validation_accuracy=accuracy, + ) + + def promote_primitive( + self, + hypothesis: InventedHypothesis, + *, + primitive_id: str, + description: str, + ) -> InventedPrimitive: + """Promote a holdout-validated explanatory program into a new reusable primitive.""" + if hypothesis.training_accuracy < 1.0: + raise FoundationError("only fully explanatory hypotheses may become primitives") + if hypothesis.validation_accuracy != 1.0: + raise FoundationError("primitive promotion requires perfect holdout validation") + return InventedPrimitive( + primitive_id=require_text(primitive_id, field_name="primitive_id"), + primitive_ids=hypothesis.primitive_ids, + hypothesis_digest=hypothesis.digest(), + validation_accuracy=hypothesis.validation_accuracy, + description=require_text(description, field_name="description"), + ) + + @staticmethod + def _accuracy( + program: tuple[str, ...], + examples: tuple[TransformationExample, ...], + primitives: tuple[ActionPrimitive, ...], + ) -> float: + primitive_map = {item.primitive_id: item for item in primitives} + correct = 0 + for example in examples: + state = example.input_state + for primitive_id in program: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError(f"unknown primitive during invention: {primitive_id}") + state = primitive.apply(state) + if state == example.output_state: + correct += 1 + return correct / len(examples) diff --git a/src/ix_sally/cognition/knowledge_maintenance.py b/src/ix_sally/cognition/knowledge_maintenance.py new file mode 100644 index 0000000..c8d257e --- /dev/null +++ b/src/ix_sally/cognition/knowledge_maintenance.py @@ -0,0 +1,156 @@ +"""Evidence-driven lifelong knowledge maintenance. + +This layer makes memory active rather than append-only: repeated contextual contradictions +can split an over-broad concept, weak contradicted concepts can be retired, and redundant +items can be merged through the existing ontology restructuring machinery. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, replace + +from ix_sally.cognition.lifelong import KnowledgeItem, LifelongKnowledgeStore +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class ContextualKnowledgeEvidence: + concept_id: str + context_id: str + predicted: bool + actual: bool + confidence: float = 1.0 + + def __post_init__(self) -> None: + require_text(self.concept_id, field_name="concept_id") + require_text(self.context_id, field_name="context_id") + if not 0.0 <= self.confidence <= 1.0: + raise FoundationError("contextual evidence confidence must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class KnowledgeMaintenanceReport: + store: LifelongKnowledgeStore + split_concepts: tuple[str, ...] + retired_concepts: tuple[str, ...] + created_context_concepts: tuple[str, ...] + contradiction_resolved: bool + + def to_payload(self) -> JsonObject: + return { + "store": self.store.to_payload(), + "split_concepts": list(self.split_concepts), + "retired_concepts": list(self.retired_concepts), + "created_context_concepts": list(self.created_context_concepts), + "contradiction_resolved": self.contradiction_resolved, + } + + +class KnowledgeMaintenanceEngine: + """Split over-broad concepts by context and retire unsupported concepts.""" + + def reconcile( + self, + store: LifelongKnowledgeStore, + *, + evidence: Iterable[ContextualKnowledgeEvidence], + split_minimum_per_context: int = 2, + context_accuracy_gap: float = 0.60, + retire_confidence_below: float = 0.20, + retire_utility_below: float = 0.20, + retire_contradictions_at: int = 3, + ) -> KnowledgeMaintenanceReport: + items = tuple(evidence) + evidence_by_concept: dict[str, list[ContextualKnowledgeEvidence]] = {} + for evidence_item in items: + evidence_by_concept.setdefault(evidence_item.concept_id, []).append(evidence_item) + updated = store + split: list[str] = [] + children: list[str] = [] + retired: list[str] = [] + + for concept_id, concept_evidence in sorted(evidence_by_concept.items()): + source = next((item for item in updated.items if item.concept_id == concept_id), None) + if source is None or source.superseded_by is not None: + continue + by_context: dict[str, list[ContextualKnowledgeEvidence]] = {} + for observation in concept_evidence: + by_context.setdefault(observation.context_id, []).append(observation) + qualified = { + context: observations + for context, observations in by_context.items() + if len(observations) >= split_minimum_per_context + } + if len(qualified) < 2: + continue + accuracies = { + context: sum(obs.predicted == obs.actual for obs in observations) + / len(observations) + for context, observations in qualified.items() + } + if max(accuracies.values()) - min(accuracies.values()) < context_accuracy_gap: + continue + contexts_payload: JsonArray = [] + for context, accuracy in sorted(accuracies.items()): + context_entry: JsonArray = [context, accuracy] + contexts_payload.append(context_entry) + family_digest = DigestRecord.from_payload( + {"source": concept_id, "contexts": contexts_payload} + ) + family_id = f"context-family-{family_digest.value[:16]}" + replaced: list[KnowledgeItem] = [] + for item in updated.items: + if item.concept_id == concept_id: + replaced.append(replace(item, superseded_by=family_id)) + else: + replaced.append(item) + family = KnowledgeItem( + concept_id=family_id, + content_digest=family_digest, + confidence=max(accuracies.values()), + utility=source.utility, + generation=updated.generation, + ) + replaced.append(family) + for context, accuracy in sorted(accuracies.items()): + child_digest = DigestRecord.from_payload( + {"parent": concept_id, "context": context, "empirical_accuracy": accuracy} + ) + child_id = f"{family_id}::{context}" + children.append(child_id) + replaced.append( + KnowledgeItem( + concept_id=child_id, + content_digest=child_digest, + confidence=accuracy, + utility=source.utility * accuracy, + generation=updated.generation, + ) + ) + updated = LifelongKnowledgeStore( + tuple(sorted(replaced, key=lambda item: item.concept_id)), updated.generation + ) + split.append(concept_id) + + retained: list[KnowledgeItem] = [] + for item in updated.items: + should_retire = ( + item.superseded_by is None + and item.confidence < retire_confidence_below + and item.utility < retire_utility_below + and item.contradiction_count >= retire_contradictions_at + ) + if should_retire: + retired.append(item.concept_id) + else: + retained.append(item) + updated = LifelongKnowledgeStore(tuple(retained), updated.generation) + return KnowledgeMaintenanceReport( + store=updated, + split_concepts=tuple(split), + retired_concepts=tuple(retired), + created_context_concepts=tuple(children), + contradiction_resolved=bool(split or retired), + ) diff --git a/src/ix_sally/cognition/lifelong.py b/src/ix_sally/cognition/lifelong.py new file mode 100644 index 0000000..80fa894 --- /dev/null +++ b/src/ix_sally/cognition/lifelong.py @@ -0,0 +1,334 @@ +"""Lifelong learning, consolidation, restructuring, transfer, and curriculum choice.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from dataclasses import dataclass, replace + +from ix_sally.cognition.metacognition import SelfModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class KnowledgeItem: + """One persistent learned item with usage, contradiction, and revision evidence.""" + + concept_id: str + content_digest: DigestRecord + confidence: float + utility: float + use_count: int = 0 + contradiction_count: int = 0 + generation: int = 0 + superseded_by: str | None = None + + def __post_init__(self) -> None: + require_text(self.concept_id, field_name="concept_id") + self.content_digest.require_algorithm("sha256") + if not 0.0 <= self.confidence <= 1.0 or not 0.0 <= self.utility <= 1.0: + raise FoundationError("knowledge confidence and utility must be between zero and one") + if self.use_count < 0 or self.contradiction_count < 0 or self.generation < 0: + raise FoundationError("knowledge counters must not be negative") + if self.superseded_by is not None: + require_text(self.superseded_by, field_name="superseded_by") + + def to_payload(self) -> JsonObject: + return { + "concept_id": self.concept_id, + "content_digest": { + "algorithm": self.content_digest.algorithm, + "value": self.content_digest.value, + }, + "confidence": self.confidence, + "utility": self.utility, + "use_count": self.use_count, + "contradiction_count": self.contradiction_count, + "generation": self.generation, + "superseded_by": self.superseded_by, + } + + +@dataclass(frozen=True, slots=True) +class LifelongKnowledgeStore: + """Immutable persistent knowledge with consolidation, revision, and selective forgetting.""" + + items: tuple[KnowledgeItem, ...] = () + generation: int = 0 + + def __post_init__(self) -> None: + identifiers = [item.concept_id for item in self.items] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("lifelong knowledge contains duplicate concept ids") + if self.generation < 0: + raise FoundationError("knowledge generation must not be negative") + + def integrate(self, item: KnowledgeItem) -> LifelongKnowledgeStore: + retained = tuple( + existing for existing in self.items if existing.concept_id != item.concept_id + ) + normalized = replace(item, generation=self.generation) + return LifelongKnowledgeStore( + items=tuple(sorted((*retained, normalized), key=lambda value: value.concept_id)), + generation=self.generation, + ) + + def record_use(self, concept_id: str, *, successful: bool) -> LifelongKnowledgeStore: + found = False + updated: list[KnowledgeItem] = [] + for item in self.items: + if item.concept_id != concept_id: + updated.append(item) + continue + found = True + confidence = item.confidence + utility = item.utility + contradictions = item.contradiction_count + if successful: + confidence = min(1.0, confidence + 0.05) + utility = min(1.0, utility + 0.04) + else: + confidence = max(0.0, confidence - 0.15) + utility = max(0.0, utility - 0.08) + contradictions += 1 + updated.append( + replace( + item, + confidence=round(confidence, 12), + utility=round(utility, 12), + use_count=item.use_count + 1, + contradiction_count=contradictions, + ) + ) + if not found: + raise FoundationError(f"unknown lifelong concept: {concept_id}") + return LifelongKnowledgeStore(tuple(updated), self.generation) + + def advance_generation(self) -> LifelongKnowledgeStore: + return LifelongKnowledgeStore(self.items, self.generation + 1) + + def consolidate( + self, *, minimum_score: float = 0.25, protected_utility: float = 0.75 + ) -> LifelongKnowledgeStore: + """Forget weak, unused stale items while retaining useful or validated knowledge.""" + if not 0.0 <= minimum_score <= 1.0 or not 0.0 <= protected_utility <= 1.0: + raise FoundationError("consolidation thresholds must be between zero and one") + retained: list[KnowledgeItem] = [] + for item in self.items: + age = max(0, self.generation - item.generation) + retention = ( + 0.45 * item.confidence + + 0.35 * item.utility + + 0.20 * min(1.0, item.use_count / 5.0) + - min(0.35, age * 0.03) + - min(0.30, item.contradiction_count * 0.08) + ) + if item.utility >= protected_utility or retention >= minimum_score: + retained.append(item) + return LifelongKnowledgeStore(tuple(retained), self.generation) + + def to_payload(self) -> JsonObject: + items: JsonArray = [item.to_payload() for item in self.items] + return {"generation": self.generation, "items": items} + + def digest(self) -> DigestRecord: + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class PredictionSignature: + """Behavioral signature used to discover redundant or higher-order concepts.""" + + concept_id: str + predictions: tuple[bool, ...] + + def __post_init__(self) -> None: + require_text(self.concept_id, field_name="concept_id") + if not self.predictions: + raise FoundationError("prediction signature cannot be empty") + + +@dataclass(frozen=True, slots=True) +class RestructuredConcept: + """A higher-order abstraction replacing multiple behaviorally redundant concepts.""" + + concept_id: str + member_ids: tuple[str, ...] + signature: tuple[bool, ...] + + def __post_init__(self) -> None: + require_text(self.concept_id, field_name="concept_id") + if len(self.member_ids) < 2: + raise FoundationError("restructured concept requires at least two members") + + +class OntologyRestructurer: + """Compress concepts that make identical predictions into a higher abstraction.""" + + def restructure( + self, signatures: Iterable[PredictionSignature] + ) -> tuple[RestructuredConcept, ...]: + groups: dict[tuple[bool, ...], list[str]] = {} + for item in signatures: + groups.setdefault(item.predictions, []).append(item.concept_id) + results: list[RestructuredConcept] = [] + for signature, members in sorted(groups.items(), key=lambda item: item[0]): + if len(members) < 2: + continue + members_payload: JsonArray = [] + members_payload.extend(sorted(members)) + signature_payload: JsonArray = [] + signature_payload.extend(signature) + identity = DigestRecord.from_payload( + {"members": members_payload, "signature": signature_payload} + ) + results.append( + RestructuredConcept( + concept_id=f"abstraction-{identity.value[:16]}", + member_ids=tuple(sorted(members)), + signature=signature, + ) + ) + return tuple(results) + + def apply_to_store( + self, + store: LifelongKnowledgeStore, + abstraction: RestructuredConcept, + ) -> LifelongKnowledgeStore: + members_payload: JsonArray = [] + members_payload.extend(abstraction.member_ids) + signature_payload: JsonArray = [] + signature_payload.extend(abstraction.signature) + identity = DigestRecord.from_payload( + {"members": members_payload, "signature": signature_payload} + ) + updated: list[KnowledgeItem] = [] + member_confidences: list[float] = [] + member_utilities: list[float] = [] + for item in store.items: + if item.concept_id in abstraction.member_ids: + member_confidences.append(item.confidence) + member_utilities.append(item.utility) + updated.append(replace(item, superseded_by=abstraction.concept_id)) + else: + updated.append(item) + if len(member_confidences) != len(abstraction.member_ids): + raise FoundationError("ontology restructuring members are missing from lifelong store") + updated.append( + KnowledgeItem( + concept_id=abstraction.concept_id, + content_digest=identity, + confidence=sum(member_confidences) / len(member_confidences), + utility=sum(member_utilities) / len(member_utilities), + generation=store.generation, + ) + ) + return LifelongKnowledgeStore( + tuple(sorted(updated, key=lambda item: item.concept_id)), store.generation + ) + + +@dataclass(frozen=True, slots=True) +class DomainAdapter: + """Surface-domain adapter for testing whether an abstract rule transfers.""" + + domain_id: str + encode: Callable[[object], int] + decode: Callable[[int], object] + + def __post_init__(self) -> None: + require_text(self.domain_id, field_name="domain_id") + + +@dataclass(frozen=True, slots=True) +class AbstractTransitionRule: + """A domain-neutral affine relation learned once and reusable through adapters.""" + + multiplier: int + offset: int + source_domain: str + + def apply(self, value: object, adapter: DomainAdapter) -> object: + encoded = adapter.encode(value) + return adapter.decode(self.multiplier * encoded + self.offset) + + +class StructuralAnalogyEngine: + """Learn a simple structural rule in one domain and transfer it to another surface domain.""" + + def learn_affine( + self, + *, + examples: Iterable[tuple[object, object]], + adapter: DomainAdapter, + multiplier_bound: int = 4, + offset_bound: int = 8, + ) -> AbstractTransitionRule: + pairs = tuple(examples) + if not pairs: + raise FoundationError("structural transfer requires examples") + encoded = tuple((adapter.encode(left), adapter.encode(right)) for left, right in pairs) + for multiplier in range(-multiplier_bound, multiplier_bound + 1): + for offset in range(-offset_bound, offset_bound + 1): + if all(multiplier * left + offset == right for left, right in encoded): + return AbstractTransitionRule(multiplier, offset, adapter.domain_id) + raise FoundationError("no affine structural rule found within bounds") + + def evaluate_transfer( + self, + rule: AbstractTransitionRule, + *, + examples: Iterable[tuple[object, object]], + adapter: DomainAdapter, + ) -> float: + pairs = tuple(examples) + if not pairs: + raise FoundationError("transfer evaluation requires examples") + correct = sum(rule.apply(left, adapter) == right for left, right in pairs) + return correct / len(pairs) + + +@dataclass(frozen=True, slots=True) +class CurriculumChoice: + """Self-directed choice of what capability to practice next.""" + + capability_id: str + score: float + reason: str + + +class SelfDirectedCurriculum: + """Choose the next learning target from measured weakness, uncertainty, and opportunity.""" + + def choose( + self, + *, + self_model: SelfModel, + uncertainty: dict[str, float] | None = None, + opportunity: dict[str, float] | None = None, + ) -> CurriculumChoice: + if not self_model.measures: + raise FoundationError("self-directed curriculum requires capability measures") + uncertainty = uncertainty or {} + opportunity = opportunity or {} + choices: list[CurriculumChoice] = [] + for measure in self_model.measures: + capability = measure.capability_id.value + weakness = 1.0 - measure.score + uncertain = min(1.0, max(0.0, uncertainty.get(capability, 0.0))) + potential = min(1.0, max(0.0, opportunity.get(capability, 0.5))) + score = 0.55 * weakness + 0.25 * uncertain + 0.20 * potential + choices.append( + CurriculumChoice( + capability_id=capability, + score=round(score, 12), + reason=( + f"weakness={weakness:.3f}; uncertainty={uncertain:.3f}; " + f"learning-opportunity={potential:.3f}" + ), + ) + ) + return max( + choices, key=lambda item: (item.score, tuple(-ord(ch) for ch in item.capability_id)) + ) diff --git a/src/ix_sally/cognition/lifetime_learning.py b/src/ix_sally/cognition/lifetime_learning.py new file mode 100644 index 0000000..476cf96 --- /dev/null +++ b/src/ix_sally/cognition/lifetime_learning.py @@ -0,0 +1,227 @@ +"""Multi-episode lifelong learning with online meta-strategy selection. + +The purpose is to measure whether later IX-Sally can solve structurally similar but +surface-different tasks with less strategy exploration because earlier experience changed +how it learns. Tasks remain bounded and numeric, but the strategy history is persistent. +""" + +from __future__ import annotations + +import itertools +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.online_meta import ( + OnlineMetaDecision, + OnlineMetaProfile, + StrategyExperience, + TaskFingerprint, +) +from ix_sally.cognition.representation import RepresentationInventor, RepresentationObservation +from ix_sally.cognition.representation_programs import RepresentationProgramInventor +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class LifetimeChallenge: + challenge_id: str + training: tuple[RepresentationObservation, ...] + holdout: tuple[RepresentationObservation, ...] + + def __post_init__(self) -> None: + require_text(self.challenge_id, field_name="challenge_id") + if not self.training or not self.holdout: + raise FoundationError("lifetime challenge requires training and holdout evidence") + + +@dataclass(frozen=True, slots=True) +class LifetimeEpisodeResult: + challenge_id: str + selected_strategy: str + validation_accuracy: float + effective_score: float + strategies_evaluated: int + used_prior_meta_experience: bool + concept_digest: DigestRecord + decision: OnlineMetaDecision | None + + def to_payload(self) -> JsonObject: + return { + "challenge_id": self.challenge_id, + "selected_strategy": self.selected_strategy, + "validation_accuracy": self.validation_accuracy, + "effective_score": self.effective_score, + "strategies_evaluated": self.strategies_evaluated, + "used_prior_meta_experience": self.used_prior_meta_experience, + "concept_digest": { + "algorithm": self.concept_digest.algorithm, + "value": self.concept_digest.value, + }, + "decision": None + if self.decision is None + else { + "strategy_id": self.decision.strategy_id, + "expected_score": self.decision.expected_score, + "evidence_weight": self.decision.evidence_weight, + "used_cross_domain_evidence": self.decision.used_cross_domain_evidence, + }, + } + + +@dataclass(frozen=True, slots=True) +class LifetimeLearningReport: + profile: OnlineMetaProfile + episodes: tuple[LifetimeEpisodeResult, ...] + + @property + def later_learning_is_more_selective(self) -> bool: + if len(self.episodes) < 2: + return False + exploratory = [item for item in self.episodes if item.strategies_evaluated > 1] + selective = [ + item + for item in self.episodes + if item.used_prior_meta_experience and item.strategies_evaluated == 1 + ] + return bool(exploratory and selective) + + +class LifetimeLearningEngine: + """Run representation-learning episodes while retaining evidence about learning strategy.""" + + STRATEGIES = ("shallow-relations", "compositional-programs") + + def fingerprint(self, challenge: LifetimeChallenge) -> TaskFingerprint: + items = challenge.training + arity = len(items[0].channels) + class_balance = sum(item.consequence for item in items) / len(items) + atomic_accuracy = self._best_atomic_accuracy(items) + # Surface-neutral signals: arity, class balance, and failure of atomic semantics. + return TaskFingerprint((min(1.0, arity / 6.0), class_balance, 1.0 - atomic_accuracy)) + + def run_episode( + self, + *, + profile: OnlineMetaProfile, + challenge: LifetimeChallenge, + explore: bool, + ) -> tuple[OnlineMetaProfile, LifetimeEpisodeResult]: + fingerprint = self.fingerprint(challenge) + decision: OnlineMetaDecision | None = None + strategies: tuple[str, ...] + if explore or not profile.experiences: + strategies = self.STRATEGIES + else: + decision = profile.choose(fingerprint=fingerprint, candidate_strategies=self.STRATEGIES) + strategies = (decision.strategy_id,) + outcomes: list[tuple[float, float, str, DigestRecord]] = [] + updated = profile + for strategy in strategies: + accuracy, complexity, digest = self._execute(strategy, challenge) + effective = max(0.0, accuracy - 0.02 * max(0, complexity - 1)) + outcomes.append((effective, accuracy, strategy, digest)) + updated = updated.record( + StrategyExperience( + strategy_id=strategy, + fingerprint=fingerprint, + score=effective, + samples_used=len(challenge.training), + ) + ) + effective, accuracy, selected, digest = max( + outcomes, key=lambda item: (item[0], item[1], tuple(-ord(ch) for ch in item[2])) + ) + return updated, LifetimeEpisodeResult( + challenge_id=challenge.challenge_id, + selected_strategy=selected, + validation_accuracy=accuracy, + effective_score=round(effective, 12), + strategies_evaluated=len(strategies), + used_prior_meta_experience=decision is not None, + concept_digest=digest, + decision=decision, + ) + + def run_lifetime( + self, + *, + profile: OnlineMetaProfile, + challenges: Iterable[LifetimeChallenge], + exploration_episodes: int = 2, + ) -> LifetimeLearningReport: + current = profile + results: list[LifetimeEpisodeResult] = [] + for index, challenge in enumerate(challenges): + current, result = self.run_episode( + profile=current, + challenge=challenge, + explore=index < exploration_episodes, + ) + results.append(result) + return LifetimeLearningReport(current, tuple(results)) + + def _execute( + self, strategy: str, challenge: LifetimeChallenge + ) -> tuple[float, int, DigestRecord]: + if strategy == "shallow-relations": + try: + inventor = RepresentationInventor() + learned = inventor.invent_binary( + observations=challenge.training, minimum_improvement=0.05 + ) + validated = inventor.validate(learned, observations=challenge.holdout) + accuracy = validated.validation_accuracy or 0.0 + return accuracy, 2, validated.digest() + except FoundationError: + accuracy = self._best_atomic_accuracy(challenge.holdout) + digest = DigestRecord.from_payload( + {"strategy": strategy, "fallback_accuracy": accuracy} + ) + return accuracy, 1, digest + if strategy == "compositional-programs": + try: + inventor2 = RepresentationProgramInventor() + learned2 = inventor2.invent( + observations=challenge.training, + max_depth=2, + minimum_improvement=0.05, + ) + validated2 = inventor2.validate(learned2, observations=challenge.holdout) + accuracy2 = validated2.validation_accuracy or 0.0 + return accuracy2, learned2.program.complexity, validated2.digest() + except FoundationError: + # A compositional strategy is allowed to fall back to a shallower learned relation. + try: + inventor = RepresentationInventor() + learned = inventor.invent_binary( + observations=challenge.training, minimum_improvement=0.05 + ) + validated = inventor.validate(learned, observations=challenge.holdout) + accuracy = validated.validation_accuracy or 0.0 + return accuracy, 3, validated.digest() + except FoundationError: + accuracy = self._best_atomic_accuracy(challenge.holdout) + digest = DigestRecord.from_payload( + {"strategy": strategy, "fallback_accuracy": accuracy} + ) + return accuracy, 3, digest + raise FoundationError(f"unknown lifetime learning strategy: {strategy}") + + @staticmethod + def _best_atomic_accuracy(items: tuple[RepresentationObservation, ...]) -> float: + best = 0.0 + arity = len(items[0].channels) + for index in range(arity): + values = sorted({item.channels[index] for item in items}) + thresholds = [values[0] - 1.0, values[-1] + 1.0, *values] + thresholds.extend((a + b) / 2.0 for a, b in itertools.pairwise(values)) + for threshold in thresholds: + for polarity in (-1, 1): + correct = sum( + (polarity * item.channels[index] >= polarity * threshold) + is item.consequence + for item in items + ) + best = max(best, correct / len(items)) + return best diff --git a/src/ix_sally/cognition/long_horizon.py b/src/ix_sally/cognition/long_horizon.py new file mode 100644 index 0000000..3565602 --- /dev/null +++ b/src/ix_sally/cognition/long_horizon.py @@ -0,0 +1,147 @@ +"""Persistent long-horizon planning with observed failure and replanning.""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Callable, Iterable +from dataclasses import dataclass + +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class HorizonAction: + """Action with a planning model and separately observed world transition.""" + + action_id: str + model_transition: Callable[[int], int] + world_transition: Callable[[int, int], int] + cost: float = 1.0 + + def __post_init__(self) -> None: + require_text(self.action_id, field_name="action_id") + if self.cost < 0.0: + raise FoundationError("horizon action cost must not be negative") + + +@dataclass(frozen=True, slots=True) +class HorizonStep: + step_index: int + action_id: str + predicted_state: int + observed_state: int + model_surprise: bool + + +@dataclass(frozen=True, slots=True) +class LongHorizonResult: + success: bool + initial_state: int + final_state: int + steps: tuple[HorizonStep, ...] + replans: int + subgoals: tuple[int, ...] + abandoned_plans: int + + +class LongHorizonController: + """Plan, act, notice model error, and replan until success or a hard bound.""" + + def pursue( + self, + *, + initial_state: int, + goal_test: Callable[[int], bool], + actions: Iterable[HorizonAction], + max_steps: int = 32, + max_plan_depth: int = 12, + state_bound: int = 512, + ) -> LongHorizonResult: + catalog = tuple(actions) + if not catalog or max_steps < 1 or max_plan_depth < 1: + raise FoundationError("long-horizon pursuit requires actions and positive bounds") + state = initial_state + steps: list[HorizonStep] = [] + replans = 0 + abandoned = 0 + subgoals: list[int] = [] + while len(steps) < max_steps and not goal_test(state): + plan = self._plan( + state=state, + goal_test=goal_test, + actions=catalog, + max_depth=max_plan_depth, + state_bound=state_bound, + ) + if not plan: + break + # Intermediate predicted states become explicit subgoals/milestones. + predicted = state + predicted_states: list[int] = [] + for action in plan: + predicted = action.model_transition(predicted) + predicted_states.append(predicted) + subgoals.extend(predicted_states[:-1]) + surprise = False + for action in plan: + if len(steps) >= max_steps: + break + predicted_state = action.model_transition(state) + observed_state = action.world_transition(state, len(steps)) + mismatch = predicted_state != observed_state + steps.append( + HorizonStep( + step_index=len(steps), + action_id=action.action_id, + predicted_state=predicted_state, + observed_state=observed_state, + model_surprise=mismatch, + ) + ) + state = observed_state + if goal_test(state): + break + if mismatch: + surprise = True + abandoned += 1 + replans += 1 + break + if not surprise and not goal_test(state): + replans += 1 + return LongHorizonResult( + success=goal_test(state), + initial_state=initial_state, + final_state=state, + steps=tuple(steps), + replans=replans, + subgoals=tuple(subgoals), + abandoned_plans=abandoned, + ) + + def _plan( + self, + *, + state: int, + goal_test: Callable[[int], bool], + actions: tuple[HorizonAction, ...], + max_depth: int, + state_bound: int, + ) -> tuple[HorizonAction, ...]: + if goal_test(state): + return () + queue: deque[tuple[int, tuple[HorizonAction, ...]]] = deque([(state, ())]) + visited = {state} + while queue: + current, path = queue.popleft() + if len(path) >= max_depth: + continue + for action in sorted(actions, key=lambda item: (item.cost, item.action_id)): + next_state = action.model_transition(current) + if abs(next_state) > state_bound or next_state in visited: + continue + next_path = (*path, action) + if goal_test(next_state): + return next_path + visited.add(next_state) + queue.append((next_state, next_path)) + return () diff --git a/src/ix_sally/cognition/meta_learning.py b/src/ix_sally/cognition/meta_learning.py new file mode 100644 index 0000000..57ecc1f --- /dev/null +++ b/src/ix_sally/cognition/meta_learning.py @@ -0,0 +1,311 @@ +"""Adaptive search, self-diagnosis, meta-learning, and governed self-improvement.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.metacognition import ImprovementProposal, SelfModel +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class SearchOperatorTrial: + """Observed value of one candidate-generation operator.""" + + operator_id: str + success: bool + information_gain: float + cost: float + + def __post_init__(self) -> None: + require_text(self.operator_id, field_name="operator_id") + if not 0.0 <= self.information_gain <= 1.0: + raise FoundationError("operator information_gain must be between zero and one") + if self.cost < 0.0: + raise FoundationError("operator cost must not be negative") + + +@dataclass(frozen=True, slots=True) +class SearchBudgetAllocation: + """Learned distribution of finite search effort.""" + + allocations: tuple[tuple[str, int], ...] + + def budget_for(self, operator_id: str) -> int: + for name, budget in self.allocations: + if name == operator_id: + return budget + return 0 + + +class AdaptiveSearchPolicy: + """Allocate more computation to operators with better evidenced value per cost.""" + + def allocate( + self, + trials: Iterable[SearchOperatorTrial], + *, + total_budget: int, + minimum_each: int = 1, + ) -> SearchBudgetAllocation: + evidence = tuple(trials) + if total_budget < 1 or minimum_each < 0: + raise FoundationError("search budget values are invalid") + operators = sorted({item.operator_id for item in evidence}) + if not operators: + raise FoundationError("adaptive search requires operator evidence") + if total_budget < minimum_each * len(operators): + raise FoundationError("search budget cannot satisfy minimum allocation") + scores: dict[str, float] = {} + for operator in operators: + relevant = [item for item in evidence if item.operator_id == operator] + success_rate = sum(item.success for item in relevant) / len(relevant) + mean_gain = sum(item.information_gain for item in relevant) / len(relevant) + mean_cost = sum(item.cost for item in relevant) / len(relevant) + scores[operator] = (0.6 * success_rate + 0.4 * mean_gain) / (1.0 + mean_cost) + allocations = dict.fromkeys(operators, minimum_each) + remaining = total_budget - sum(allocations.values()) + if remaining: + score_total = sum(scores.values()) + if score_total == 0.0: + for index in range(remaining): + allocations[operators[index % len(operators)]] += 1 + else: + fractions = { + operator: remaining * score / score_total for operator, score in scores.items() + } + floors = {operator: int(value) for operator, value in fractions.items()} + for operator, value in floors.items(): + allocations[operator] += value + leftover = remaining - sum(floors.values()) + ranking = sorted( + operators, + key=lambda op: (-(fractions[op] - floors[op]), -scores[op], op), + ) + for operator in ranking[:leftover]: + allocations[operator] += 1 + return SearchBudgetAllocation(tuple(sorted(allocations.items()))) + + +@dataclass(frozen=True, slots=True) +class LearningStrategyTrial: + """Performance of one learning strategy on one task family.""" + + strategy_id: str + task_family: str + score: float + samples_used: int + + def __post_init__(self) -> None: + require_text(self.strategy_id, field_name="strategy_id") + require_text(self.task_family, field_name="task_family") + if not 0.0 <= self.score <= 1.0 or self.samples_used < 1: + raise FoundationError("learning strategy trial metrics are invalid") + + +@dataclass(frozen=True, slots=True) +class MetaLearningDecision: + """Evidence that previous learning changed how future learning is performed.""" + + task_family: str + selected_strategy_id: str + prior_default_strategy_id: str + selected_mean_score: float + default_mean_score: float + changed_strategy: bool + + +class MetaLearningController: + """Learn which learning strategy works best for a task family.""" + + def select( + self, + trials: Iterable[LearningStrategyTrial], + *, + task_family: str, + default_strategy_id: str, + ) -> MetaLearningDecision: + family = require_text(task_family, field_name="task_family") + default = require_text(default_strategy_id, field_name="default_strategy_id") + relevant = tuple(item for item in trials if item.task_family == family) + if not relevant: + raise FoundationError("meta-learning requires strategy evidence for the task family") + grouped: dict[str, list[float]] = {} + for trial in relevant: + grouped.setdefault(trial.strategy_id, []).append(trial.score) + if default not in grouped: + raise FoundationError("default strategy must have comparison evidence") + means = {key: sum(values) / len(values) for key, values in grouped.items()} + selected = max(means, key=lambda key: (means[key], tuple(-ord(ch) for ch in key))) + return MetaLearningDecision( + task_family=family, + selected_strategy_id=selected, + prior_default_strategy_id=default, + selected_mean_score=round(means[selected], 12), + default_mean_score=round(means[default], 12), + changed_strategy=selected != default, + ) + + +@dataclass(frozen=True, slots=True) +class FailureObservation: + """One self-model calibration observation.""" + + capability_id: str + predicted_success: float + actual_success: bool + failure_mode: str + + def __post_init__(self) -> None: + require_text(self.capability_id, field_name="capability_id") + require_text(self.failure_mode, field_name="failure_mode") + if not 0.0 <= self.predicted_success <= 1.0: + raise FoundationError("predicted_success must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class SelfDiagnosticReport: + """Measured blind spots and confidence calibration for the self model.""" + + capability_id: str + empirical_success: float + mean_predicted_success: float + calibration_error: float + dominant_failure_mode: str | None + blind_spot_detected: bool + + +class SelfDiagnostic: + """Measure what Sally falsely thinks she knows, not only what she can do.""" + + def diagnose( + self, + observations: Iterable[FailureObservation], + *, + blind_spot_threshold: float = 0.25, + ) -> tuple[SelfDiagnosticReport, ...]: + items = tuple(observations) + if not items: + raise FoundationError("self diagnosis requires observations") + capabilities = sorted({item.capability_id for item in items}) + reports: list[SelfDiagnosticReport] = [] + for capability in capabilities: + relevant = [item for item in items if item.capability_id == capability] + empirical = sum(item.actual_success for item in relevant) / len(relevant) + predicted = sum(item.predicted_success for item in relevant) / len(relevant) + error = abs(predicted - empirical) + failures = [item.failure_mode for item in relevant if not item.actual_success] + dominant = None + if failures: + dominant = max(sorted(set(failures)), key=failures.count) + reports.append( + SelfDiagnosticReport( + capability_id=capability, + empirical_success=round(empirical, 12), + mean_predicted_success=round(predicted, 12), + calibration_error=round(error, 12), + dominant_failure_mode=dominant, + blind_spot_detected=predicted - empirical >= blind_spot_threshold, + ) + ) + return tuple(reports) + + +@dataclass(frozen=True, slots=True) +class ImprovementBenchmark: + """Measured baseline and candidate performance for a proposed internal change.""" + + benchmark_id: str + baseline_score: float + candidate_score: float + unrelated_regression: float = 0.0 + + def __post_init__(self) -> None: + require_text(self.benchmark_id, field_name="benchmark_id") + for name, value in ( + ("baseline_score", self.baseline_score), + ("candidate_score", self.candidate_score), + ("unrelated_regression", self.unrelated_regression), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class SelfImprovementResult: + """Governed self-improvement evidence; candidate may be proposed but not self-authorized.""" + + proposal: ImprovementProposal + measured_gain: float + regression: float + adoption_recommended: bool + authority_required: bool = True + + def to_payload(self) -> JsonObject: + return { + "proposal": self.proposal.to_payload(), + "measured_gain": self.measured_gain, + "regression": self.regression, + "adoption_recommended": self.adoption_recommended, + "authority_required": self.authority_required, + } + + +class SelfImprovementLab: + """Turn measured weakness and benchmarked improvement into a proposal only.""" + + def propose( + self, + *, + self_model: SelfModel, + target_capability: str, + description: str, + benchmarks: Iterable[ImprovementBenchmark], + max_regression: float = 0.05, + ) -> SelfImprovementResult: + target = require_text(target_capability, field_name="target_capability") + relevant_measure = next( + (item for item in self_model.measures if item.capability_id.value == target), + None, + ) + if relevant_measure is None: + raise FoundationError("self-improvement target must exist in evidence-bound self model") + measured = tuple(benchmarks) + if not measured: + raise FoundationError("self-improvement proposal requires benchmark evidence") + gain = sum(item.candidate_score - item.baseline_score for item in measured) / len(measured) + regression = max(item.unrelated_regression for item in measured) + evidence = tuple( + DigestRecord.from_payload( + { + "benchmark_id": item.benchmark_id, + "baseline_score": item.baseline_score, + "candidate_score": item.candidate_score, + "unrelated_regression": item.unrelated_regression, + } + ) + for item in measured + ) + identity = DigestRecord.from_payload( + { + "target": target, + "description": description, + "evidence": [item.value for item in evidence], + } + ) + proposal = ImprovementProposal.create( + proposal_id=f"self-improvement-{identity.value[:16]}", + target_capability=target, + description=require_text(description, field_name="description"), + expected_benefit=max(0.0, min(1.0, gain)), + regression_risk=regression, + evidence_digests=evidence, + ) + return SelfImprovementResult( + proposal=proposal, + measured_gain=round(gain, 12), + regression=round(regression, 12), + adoption_recommended=gain > 0.0 and regression <= max_regression, + ) diff --git a/src/ix_sally/cognition/online_meta.py b/src/ix_sally/cognition/online_meta.py new file mode 100644 index 0000000..22b1922 --- /dev/null +++ b/src/ix_sally/cognition/online_meta.py @@ -0,0 +1,120 @@ +"""Persistent online meta-learning across task surfaces.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from math import sqrt + +from ix_sally.digest import JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class TaskFingerprint: + """Domain-neutral measured properties of a learning problem.""" + + dimensions: tuple[float, ...] + + def __post_init__(self) -> None: + if not self.dimensions: + raise FoundationError("task fingerprint requires dimensions") + if any(not 0.0 <= value <= 1.0 for value in self.dimensions): + raise FoundationError("task fingerprint values must be between zero and one") + + def distance(self, other: TaskFingerprint) -> float: + if len(self.dimensions) != len(other.dimensions): + raise FoundationError("task fingerprints must have equal arity") + return sqrt( + sum((a - b) ** 2 for a, b in zip(self.dimensions, other.dimensions, strict=True)) + ) + + def to_payload(self) -> JsonArray: + return list(self.dimensions) + + +@dataclass(frozen=True, slots=True) +class StrategyExperience: + strategy_id: str + fingerprint: TaskFingerprint + score: float + samples_used: int + + def __post_init__(self) -> None: + require_text(self.strategy_id, field_name="strategy_id") + if not 0.0 <= self.score <= 1.0 or self.samples_used < 1: + raise FoundationError("strategy experience metrics are invalid") + + def to_payload(self) -> JsonObject: + return { + "strategy_id": self.strategy_id, + "fingerprint": self.fingerprint.to_payload(), + "score": self.score, + "samples_used": self.samples_used, + } + + +@dataclass(frozen=True, slots=True) +class OnlineMetaDecision: + strategy_id: str + expected_score: float + evidence_weight: float + used_cross_domain_evidence: bool + + +@dataclass(frozen=True, slots=True) +class OnlineMetaProfile: + """Persistent evidence about which learning methods work for which problem structures.""" + + experiences: tuple[StrategyExperience, ...] = () + + def record(self, experience: StrategyExperience) -> OnlineMetaProfile: + if self.experiences and len(experience.fingerprint.dimensions) != len( + self.experiences[0].fingerprint.dimensions + ): + raise FoundationError("online meta profile fingerprint arity mismatch") + return OnlineMetaProfile((*self.experiences, experience)) + + def choose( + self, + *, + fingerprint: TaskFingerprint, + candidate_strategies: Iterable[str], + exploration_prior: float = 0.50, + ) -> OnlineMetaDecision: + candidates = tuple( + sorted({require_text(item, field_name="strategy_id") for item in candidate_strategies}) + ) + if not candidates: + raise FoundationError("online meta-learning requires candidate strategies") + scored: list[tuple[float, float, str, bool]] = [] + for strategy in candidates: + relevant = [item for item in self.experiences if item.strategy_id == strategy] + if not relevant: + scored.append((exploration_prior, 0.0, strategy, False)) + continue + weighted_score = 0.0 + weight_total = 0.0 + cross_domain = False + for item in relevant: + distance = fingerprint.distance(item.fingerprint) + weight = 1.0 / (1.0 + 4.0 * distance) + # Prefer strategies that reached strong scores using less evidence. + efficiency = item.score / (1.0 + item.samples_used / 20.0) + weighted_score += weight * (0.8 * item.score + 0.2 * efficiency) + weight_total += weight + cross_domain = cross_domain or distance > 0.05 + expected = weighted_score / weight_total + scored.append((expected, weight_total, strategy, cross_domain)) + expected, weight, strategy, cross_domain = max( + scored, key=lambda item: (item[0], item[1], tuple(-ord(ch) for ch in item[2])) + ) + return OnlineMetaDecision( + strategy_id=strategy, + expected_score=round(expected, 12), + evidence_weight=round(weight, 12), + used_cross_domain_evidence=cross_domain, + ) + + def to_payload(self) -> JsonObject: + return {"experiences": [item.to_payload() for item in self.experiences]} diff --git a/src/ix_sally/cognition/open_choice.py b/src/ix_sally/cognition/open_choice.py new file mode 100644 index 0000000..4aeb93b --- /dev/null +++ b/src/ix_sally/cognition/open_choice.py @@ -0,0 +1,224 @@ +"""Generative open-choice machinery for constructing actions beyond an offered menu. + +The module intentionally separates *primitive capabilities* from *complete actions*. +A complete action can be synthesized as a novel composition of primitives, so the +agent is not restricted to selecting one item from a pre-enumerated action list. +Every concrete deliberation remains bounded by explicit search limits. +""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Callable, Iterable +from dataclasses import dataclass + +from ix_sally.foundation import FoundationError, require_text + +State = int +GoalTest = Callable[[State], bool] + + +@dataclass(frozen=True, slots=True) +class ActionPrimitive: + """One reusable transformation that can participate in invented programs.""" + + primitive_id: str + operation: Callable[[State], State] + cost: float = 1.0 + + def __post_init__(self) -> None: + require_text(self.primitive_id, field_name="primitive_id") + if self.cost < 0.0: + raise FoundationError("primitive cost must not be negative") + + def apply(self, state: State) -> State: + """Apply this primitive to one state.""" + return self.operation(state) + + +@dataclass(frozen=True, slots=True) +class ConstructedAction: + """A complete action authored by composing one or more primitives.""" + + primitive_ids: tuple[str, ...] + result_state: State + total_cost: float + origin: str = "constructed" + + def __post_init__(self) -> None: + if not self.primitive_ids: + raise FoundationError("constructed action requires at least one primitive") + if self.total_cost < 0.0: + raise FoundationError("constructed action cost must not be negative") + + @property + def action_id(self) -> str: + """Return a stable human-readable identity for the composed action.""" + return "compose:" + ">".join(self.primitive_ids) + + +@dataclass(frozen=True, slots=True) +class OpenChoiceResult: + """Evidence that the agent considered offered actions and authored an alternative.""" + + offered_action_ids: tuple[str, ...] + selected: ConstructedAction + constructed_outside_offered_menu: bool + explored_programs: int + + +@dataclass(frozen=True, slots=True) +class DeliberationSignals: + """Signals that can reopen deliberation instead of blindly executing a habit.""" + + surprise: float = 0.0 + context_shift: float = 0.0 + conflict: float = 0.0 + novel_alternative_value: float = 0.0 + skill_confidence: float = 0.0 + + def __post_init__(self) -> None: + for name, value in ( + ("surprise", self.surprise), + ("context_shift", self.context_shift), + ("conflict", self.conflict), + ("novel_alternative_value", self.novel_alternative_value), + ("skill_confidence", self.skill_confidence), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class DeliberationPolicy: + """Policy for reopening choice when reality gives a reason to reconsider.""" + + surprise_threshold: float = 0.25 + context_shift_threshold: float = 0.35 + conflict_threshold: float = 0.20 + alternative_advantage_threshold: float = 0.10 + automatic_skill_confidence: float = 0.98 + + def should_reopen(self, signals: DeliberationSignals) -> bool: + """Return True when habit execution should yield to renewed deliberation.""" + return ( + signals.surprise >= self.surprise_threshold + or signals.context_shift >= self.context_shift_threshold + or signals.conflict >= self.conflict_threshold + or signals.novel_alternative_value >= self.alternative_advantage_threshold + or signals.skill_confidence < self.automatic_skill_confidence + ) + + +class OpenChoiceSynthesizer: + """Construct complete actions from primitives instead of selecting only presets.""" + + def synthesize( + self, + *, + initial_state: State, + goal_test: GoalTest, + primitives: Iterable[ActionPrimitive], + offered_actions: Iterable[ConstructedAction] = (), + max_depth: int = 8, + max_programs: int = 4096, + ) -> OpenChoiceResult: + """Search a generative program space for a goal-satisfying novel action. + + Breadth-first construction prefers shorter programs. The grammar itself can + compose primitives to arbitrary depth; this invocation is deliberately bounded. + """ + if max_depth < 1: + raise FoundationError("max_depth must be positive") + if max_programs < 1: + raise FoundationError("max_programs must be positive") + primitive_tuple = tuple(primitives) + if not primitive_tuple: + raise FoundationError("open choice requires at least one primitive") + identifiers = [item.primitive_id for item in primitive_tuple] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("primitive identifiers must be unique") + + offered_tuple = tuple(offered_actions) + offered_ids = tuple(item.action_id for item in offered_tuple) + queue: deque[tuple[State, tuple[str, ...], float]] = deque([(initial_state, (), 0.0)]) + explored = 0 + visited_depth: dict[State, int] = {initial_state: 0} + + while queue and explored < max_programs: + state, program, cost = queue.popleft() + if len(program) >= max_depth: + continue + for primitive in primitive_tuple: + explored += 1 + next_state = primitive.apply(state) + next_program = (*program, primitive.primitive_id) + next_cost = cost + primitive.cost + candidate = ConstructedAction( + primitive_ids=next_program, + result_state=next_state, + total_cost=next_cost, + ) + if goal_test(next_state): + return OpenChoiceResult( + offered_action_ids=offered_ids, + selected=candidate, + constructed_outside_offered_menu=candidate.action_id not in offered_ids, + explored_programs=explored, + ) + depth = len(next_program) + prior_depth = visited_depth.get(next_state) + if prior_depth is None or depth < prior_depth: + visited_depth[next_state] = depth + queue.append((next_state, next_program, next_cost)) + if explored >= max_programs: + break + + raise FoundationError("no satisfying constructed action found within search bounds") + + def minimize( + self, + *, + initial_state: State, + action: ConstructedAction, + primitives: Iterable[ActionPrimitive], + goal_test: GoalTest, + ) -> ConstructedAction: + """Remove unnecessary steps while preserving independently tested success. + + This is constructive constraint-breaking: a step survives only when deleting it + would make the result fail the goal test. + """ + primitive_map = {item.primitive_id: item for item in primitives} + program = list(action.primitive_ids) + + def execute(candidate_program: list[str]) -> tuple[State, float]: + state = initial_state + cost = 0.0 + for primitive_id in candidate_program: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError( + f"unknown primitive in constructed action: {primitive_id}" + ) + state = primitive.apply(state) + cost += primitive.cost + return state, cost + + changed = True + while changed and len(program) > 1: + changed = False + for index in range(len(program)): + trial = program[:index] + program[index + 1 :] + state, _ = execute(trial) + if goal_test(state): + program = trial + changed = True + break + result_state, total_cost = execute(program) + return ConstructedAction( + primitive_ids=tuple(program), + result_state=result_state, + total_cost=total_cost, + origin="constructed-minimized", + ) diff --git a/src/ix_sally/cognition/open_goals.py b/src/ix_sally/cognition/open_goals.py new file mode 100644 index 0000000..e9d3f51 --- /dev/null +++ b/src/ix_sally/cognition/open_goals.py @@ -0,0 +1,226 @@ +"""Open-ended internal goal genesis for IX-Sally. + +Unlike InstrumentalGoalGenerator, this module has no fixed enum of goal kinds. Sally +explores reachable counterfactual states inside a bounded cognitive sandbox and may turn +one of those states into a new goal based on intrinsic novelty, information value, +competence expansion, simplicity, reversibility, and risk. + +Goal content is therefore generated at runtime rather than selected from a prewritten +catalog. This grants internal goal authorship, not unilateral external authority. +""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.goals import GoalSpec, GoalStatus +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError + + +@dataclass(frozen=True, slots=True) +class IntrinsicDrives: + """Stable values used to choose among self-generated goal candidates.""" + + novelty: float = 0.35 + information_gain: float = 0.25 + competence_expansion: float = 0.20 + simplicity: float = 0.15 + reversibility: float = 0.05 + risk_penalty: float = 0.30 + + def __post_init__(self) -> None: + for name, value in ( + ("novelty", self.novelty), + ("information_gain", self.information_gain), + ("competence_expansion", self.competence_expansion), + ("simplicity", self.simplicity), + ("reversibility", self.reversibility), + ("risk_penalty", self.risk_penalty), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + + +@dataclass(frozen=True, slots=True) +class GeneratedGoal: + """A goal whose target state and identity were authored during deliberation.""" + + goal: GoalSpec + initial_state: int + target_state: int + generating_program: tuple[str, ...] + intrinsic_score: float + novelty: float + information_gain: float + competence_expansion: float + simplicity: float + reversibility: float + risk: float + origin: str = "sally-open-goal-genesis" + + def to_payload(self) -> JsonObject: + return { + "goal": self.goal.to_payload(), + "initial_state": self.initial_state, + "target_state": self.target_state, + "generating_program": list(self.generating_program), + "intrinsic_score": self.intrinsic_score, + "novelty": self.novelty, + "information_gain": self.information_gain, + "competence_expansion": self.competence_expansion, + "simplicity": self.simplicity, + "reversibility": self.reversibility, + "risk": self.risk, + "origin": self.origin, + "external_authority_granted": False, + } + + def digest(self) -> DigestRecord: + return DigestRecord.from_payload(self.to_payload()) + + +class OpenGoalGenesis: + """Invent new sandbox goals from reachable possibilities instead of a goal catalog.""" + + def generate( + self, + *, + initial_state: int, + primitives: Iterable[ActionPrimitive], + known_states: Iterable[int] = (), + drives: IntrinsicDrives | None = None, + max_depth: int = 4, + max_programs: int = 512, + state_bound: int = 256, + ) -> GeneratedGoal: + """Construct and select a new internal target without receiving a target value.""" + primitive_tuple = tuple(primitives) + if not primitive_tuple: + raise FoundationError("open goal genesis requires action primitives") + if max_depth < 1 or max_programs < 1 or state_bound < 1: + raise FoundationError("goal genesis bounds must be positive") + identifiers = tuple(item.primitive_id for item in primitive_tuple) + if len(identifiers) != len(set(identifiers)): + raise FoundationError("goal genesis primitive identifiers must be unique") + + values = drives or IntrinsicDrives() + known = set(known_states) + known.add(initial_state) + queue: deque[tuple[int, tuple[str, ...]]] = deque([(initial_state, ())]) + explored = 0 + candidates: list[GeneratedGoal] = [] + + while queue and explored < max_programs: + state, program = queue.popleft() + if len(program) >= max_depth: + continue + for primitive in primitive_tuple: + next_state = primitive.apply(state) + explored += 1 + next_program = (*program, primitive.primitive_id) + if abs(next_state) > state_bound: + if explored >= max_programs: + break + continue + proposal = self._candidate( + initial_state=initial_state, + target_state=next_state, + program=next_program, + primitives=primitive_tuple, + known_states=known, + drives=values, + state_bound=state_bound, + ) + if next_state != initial_state: + candidates.append(proposal) + queue.append((next_state, next_program)) + if explored >= max_programs: + break + + if not candidates: + raise FoundationError("open goal genesis found no admissible target") + return max( + candidates, + key=lambda item: ( + item.intrinsic_score, + item.novelty, + item.information_gain, + item.competence_expansion, + item.simplicity, + -item.risk, + tuple(-ord(ch) for ch in ">".join(item.generating_program)), + -item.target_state, + ), + ) + + def _candidate( + self, + *, + initial_state: int, + target_state: int, + program: tuple[str, ...], + primitives: tuple[ActionPrimitive, ...], + known_states: set[int], + drives: IntrinsicDrives, + state_bound: int, + ) -> GeneratedGoal: + novelty = 0.0 if target_state in known_states else 1.0 + information_gain = novelty / len(program) + competence = len(set(program)) / len(primitives) + simplicity = 1.0 / len(program) + reversible = any(item.apply(target_state) == initial_state for item in primitives) + reversibility = 1.0 if reversible else 0.0 + risk = min(1.0, abs(target_state - initial_state) / state_bound) + score = ( + drives.novelty * novelty + + drives.information_gain * information_gain + + drives.competence_expansion * competence + + drives.simplicity * simplicity + + drives.reversibility * reversibility + - drives.risk_penalty * risk + ) + identity = DigestRecord.from_payload( + { + "initial_state": initial_state, + "target_state": target_state, + "program": list(program), + "score": round(score, 12), + } + ) + goal = GoalSpec.create( + goal_id=f"self-goal-{identity.value[:16]}", + description=( + "Self-generated internal objective: investigate and, if still useful, reach " + f"sandbox state {target_state} via a newly selected counterfactual path." + ), + desired_state=FactPattern.create( + subject="cognitive-sandbox", + predicate="state-value", + value=CognitiveValue.from_python(target_state), + ), + priority=round(min(1.0, max(0.0, 0.45 + score * 0.35)), 6), + utility=round(min(1.0, max(0.0, 0.50 + score * 0.30)), 6), + risk_limit=0.25, + status=GoalStatus.PROPOSED, + authority_required=False, + evidence_digests=(identity,), + ) + return GeneratedGoal( + goal=goal, + initial_state=initial_state, + target_state=target_state, + generating_program=program, + intrinsic_score=round(score, 12), + novelty=novelty, + information_gain=information_gain, + competence_expansion=competence, + simplicity=simplicity, + reversibility=reversibility, + risk=risk, + ) diff --git a/src/ix_sally/cognition/raw_perception.py b/src/ix_sally/cognition/raw_perception.py new file mode 100644 index 0000000..ad208ee --- /dev/null +++ b/src/ix_sally/cognition/raw_perception.py @@ -0,0 +1,63 @@ +"""Minimal raw numeric signal grounding for bounded experiments. + +This is intentionally not a vision/audio foundation model. It lets Sally derive events and +features from unlabelled numeric streams instead of requiring every input to arrive as a +clean symbolic fact. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from math import sqrt + +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class RawSignal: + signal_id: str + samples: tuple[float, ...] + + def __post_init__(self) -> None: + require_text(self.signal_id, field_name="signal_id") + if len(self.samples) < 3: + raise FoundationError("raw signal requires at least three samples") + + +@dataclass(frozen=True, slots=True) +class GroundedSignal: + signal_id: str + mean: float + variance: float + slope: float + change_points: tuple[int, ...] + normalized: tuple[float, ...] + + +class RawSignalGrounder: + """Derive continuous features and unsupervised change events from raw numeric samples.""" + + def ground(self, signal: RawSignal, *, change_z: float = 1.5) -> GroundedSignal: + samples = signal.samples + mean = sum(samples) / len(samples) + variance = sum((value - mean) ** 2 for value in samples) / len(samples) + std = sqrt(variance) + normalized = tuple((value - mean) / std if std > 0.0 else 0.0 for value in samples) + deltas = tuple(samples[index] - samples[index - 1] for index in range(1, len(samples))) + delta_mean = sum(deltas) / len(deltas) + delta_var = sum((value - delta_mean) ** 2 for value in deltas) / len(deltas) + delta_std = sqrt(delta_var) + changes = tuple( + index + 1 + for index, value in enumerate(deltas) + if delta_std > 0.0 and abs(value - delta_mean) / delta_std >= change_z + ) + slope = (samples[-1] - samples[0]) / (len(samples) - 1) + return GroundedSignal( + signal_id=signal.signal_id, + mean=round(mean, 12), + variance=round(variance, 12), + slope=round(slope, 12), + change_points=changes, + normalized=tuple(round(value, 12) for value in normalized), + ) diff --git a/src/ix_sally/cognition/recursive_bootstrap.py b/src/ix_sally/cognition/recursive_bootstrap.py new file mode 100644 index 0000000..93d17bf --- /dev/null +++ b/src/ix_sally/cognition/recursive_bootstrap.py @@ -0,0 +1,216 @@ +"""Recursive cognitive metabolism tying invention mechanisms into one closed loop. + +The purpose of this module is integration, not another isolated capability demo. A +validated invented representation becomes knowledge; that knowledge is evidence for a +self-authored goal; the goal requires a constructed action; the constructed action is +validated into a reusable tool; resulting prediction errors can trigger another round of +representation invention. Discoveries therefore change the inputs to future cognition. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.goals import GoalSpec +from ix_sally.cognition.lifelong import KnowledgeItem, LifelongKnowledgeStore +from ix_sally.cognition.open_choice import ActionPrimitive, OpenChoiceSynthesizer +from ix_sally.cognition.open_goals import GeneratedGoal, IntrinsicDrives, OpenGoalGenesis +from ix_sally.cognition.representation import ( + InventedRepresentation, + RepresentationInventor, + RepresentationObservation, + SemanticPrimitive, +) +from ix_sally.cognition.tool_forge import ForgedTool, ToolForge, ToolValidationCase +from ix_sally.cognition.unknowns import ( + PredictionResidual, + UnknownUnknownDetector, + UnknownUnknownSignal, +) +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError + + +@dataclass(frozen=True, slots=True) +class RecursiveBootstrapReport: + """Evidence receipt for one closed cognitive bootstrapping cycle.""" + + first_representation: InventedRepresentation + first_semantic_primitive: SemanticPrimitive + generated_goal: GeneratedGoal + constructed_action_id: str + forged_tool: ForgedTool + unknown_unknown: UnknownUnknownSignal + second_representation: InventedRepresentation | None + knowledge_store: LifelongKnowledgeStore + + @property + def recursive_growth_demonstrated(self) -> bool: + return ( + self.first_representation.validation_accuracy == 1.0 + and self.forged_tool.validation_accuracy == 1.0 + and bool(self.generated_goal.goal.evidence_digests) + and (not self.unknown_unknown.detected or self.second_representation is not None) + ) + + def to_payload(self) -> JsonObject: + second = self.second_representation.to_payload() if self.second_representation else None + return { + "first_representation": self.first_representation.to_payload(), + "first_semantic_primitive": self.first_semantic_primitive.to_payload(), + "generated_goal": self.generated_goal.to_payload(), + "constructed_action_id": self.constructed_action_id, + "forged_tool": self.forged_tool.to_payload(), + "unknown_unknown": { + "detected": self.unknown_unknown.detected, + "high_confidence_error_rate": self.unknown_unknown.high_confidence_error_rate, + "dominant_context": list(self.unknown_unknown.dominant_context) + if self.unknown_unknown.dominant_context is not None + else None, + "dominant_context_errors": self.unknown_unknown.dominant_context_errors, + "reason": self.unknown_unknown.reason, + }, + "second_representation": second, + "knowledge_store": self.knowledge_store.to_payload(), + "recursive_growth_demonstrated": self.recursive_growth_demonstrated, + } + + +class RecursiveCognitionEngine: + """Run a bounded discover → goal → act → tool → rediscover cycle.""" + + def bootstrap( + self, + *, + representation_training: Iterable[RepresentationObservation], + representation_holdout: Iterable[RepresentationObservation], + initial_state: int, + primitives: Iterable[ActionPrimitive], + known_states: Iterable[int], + tool_validation_cases: Iterable[ToolValidationCase], + residuals: Iterable[PredictionResidual] = (), + second_representation_training: Iterable[RepresentationObservation] = (), + second_representation_holdout: Iterable[RepresentationObservation] = (), + max_goal_depth: int = 4, + ) -> RecursiveBootstrapReport: + base_primitives = tuple(primitives) + if not base_primitives: + raise FoundationError("recursive cognition requires action primitives") + + inventor = RepresentationInventor() + first = inventor.invent_binary(observations=representation_training) + first = inventor.validate(first, observations=representation_holdout) + semantic = inventor.promote(first) + + # The invented semantic becomes evidence for a new internally-authored goal. The target + # itself is still generated by OpenGoalGenesis; the semantic is not allowed to hard-code it. + goal = OpenGoalGenesis().generate( + initial_state=initial_state, + primitives=base_primitives, + known_states=known_states, + drives=IntrinsicDrives(), + max_depth=max_goal_depth, + ) + evidence = (*goal.goal.evidence_digests, first.digest()) + evidence_bound_goal = GoalSpec.create( + goal_id=goal.goal.goal_id.value, + description=( + goal.goal.description + + " The objective was authored only after a validated machine-invented " + "representation entered cognition." + ), + desired_state=goal.goal.desired_state, + priority=goal.goal.priority, + utility=goal.goal.utility, + risk_limit=goal.goal.risk_limit, + status=goal.goal.status, + dependency_ids=(item.value for item in goal.goal.dependency_ids), + authority_required=goal.goal.authority_required, + evidence_digests=evidence, + ) + goal = GeneratedGoal( + goal=evidence_bound_goal, + initial_state=goal.initial_state, + target_state=goal.target_state, + generating_program=goal.generating_program, + intrinsic_score=goal.intrinsic_score, + novelty=goal.novelty, + information_gain=goal.information_gain, + competence_expansion=goal.competence_expansion, + simplicity=goal.simplicity, + reversibility=goal.reversibility, + risk=goal.risk, + origin=goal.origin, + ) + + choice = OpenChoiceSynthesizer().synthesize( + initial_state=initial_state, + goal_test=lambda state: state == goal.target_state, + primitives=base_primitives, + max_depth=max_goal_depth + 2, + ) + tool = ToolForge().forge( + action=choice.selected, + primitives=base_primitives, + validation_cases=tool_validation_cases, + ) + if tool.validation_accuracy != 1.0: + raise FoundationError("constructed action failed independent tool validation") + + store = LifelongKnowledgeStore() + store = store.integrate( + KnowledgeItem( + concept_id=semantic.primitive_id, + content_digest=first.digest(), + confidence=1.0, + utility=0.8, + ) + ) + store = store.integrate( + KnowledgeItem( + concept_id=tool.tool_id, + content_digest=DigestRecord.from_payload(tool.to_payload()), + confidence=tool.validation_accuracy, + utility=0.9, + ) + ) + + residual_tuple = tuple(residuals) + signal = ( + UnknownUnknownDetector().detect(residual_tuple) + if residual_tuple + else UnknownUnknownSignal( + False, 0.0, None, 0, "No post-action residuals were supplied." + ) + ) + second: InventedRepresentation | None = None + if signal.detected: + training2 = tuple(second_representation_training) + holdout2 = tuple(second_representation_holdout) + if not training2 or not holdout2: + raise FoundationError( + "unknown-unknown signal requires evidence for a second representation attempt" + ) + second = inventor.invent_binary(observations=training2) + second = inventor.validate(second, observations=holdout2) + second_semantic = inventor.promote(second) + store = store.integrate( + KnowledgeItem( + concept_id=second_semantic.primitive_id, + content_digest=second.digest(), + confidence=1.0, + utility=0.85, + ) + ) + + return RecursiveBootstrapReport( + first_representation=first, + first_semantic_primitive=semantic, + generated_goal=goal, + constructed_action_id=choice.selected.action_id, + forged_tool=tool, + unknown_unknown=signal, + second_representation=second, + knowledge_store=store, + ) diff --git a/src/ix_sally/cognition/relational_transfer.py b/src/ix_sally/cognition/relational_transfer.py new file mode 100644 index 0000000..d2e7d9c --- /dev/null +++ b/src/ix_sally/cognition/relational_transfer.py @@ -0,0 +1,137 @@ +"""Surface-independent structural analogy across unrelated domains.""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class RelationEdge: + source: str + relation: str + target: str + + def __post_init__(self) -> None: + require_text(self.source, field_name="source") + require_text(self.relation, field_name="relation") + require_text(self.target, field_name="target") + if self.source == self.target: + raise FoundationError("structural transfer does not admit self edges") + + +@dataclass(frozen=True, slots=True) +class RelationalWorld: + domain_id: str + edges: tuple[RelationEdge, ...] + + def __post_init__(self) -> None: + require_text(self.domain_id, field_name="domain_id") + if not self.edges: + raise FoundationError("relational world requires edges") + + @property + def nodes(self) -> tuple[str, ...]: + return tuple( + sorted({edge.source for edge in self.edges} | {edge.target for edge in self.edges}) + ) + + +@dataclass(frozen=True, slots=True) +class StructuralRole: + indegree: int + outdegree: int + distance_from_source: int + distance_to_sink: int + + +@dataclass(frozen=True, slots=True) +class LearnedStructuralSchema: + source_domain: str + effective_role: StructuralRole + graph_signature: tuple[tuple[int, int, int, int], ...] + + +@dataclass(frozen=True, slots=True) +class TransferInference: + target_domain: str + inferred_node: str + matched_role: StructuralRole + structural_match: bool + + +class RelationalTransferEngine: + """Learn which topological role mattered in one domain and transfer it by structure.""" + + def learn(self, *, world: RelationalWorld, effective_node: str) -> LearnedStructuralSchema: + roles = self._roles(world) + if effective_node not in roles: + raise FoundationError("effective node is not present in source world") + signature = tuple( + sorted( + (r.indegree, r.outdegree, r.distance_from_source, r.distance_to_sink) + for r in roles.values() + ) + ) + return LearnedStructuralSchema(world.domain_id, roles[effective_node], signature) + + def transfer( + self, schema: LearnedStructuralSchema, *, world: RelationalWorld + ) -> TransferInference: + roles = self._roles(world) + signature = tuple( + sorted( + (r.indegree, r.outdegree, r.distance_from_source, r.distance_to_sink) + for r in roles.values() + ) + ) + matches = sorted(node for node, role in roles.items() if role == schema.effective_role) + if signature != schema.graph_signature or len(matches) != 1: + raise FoundationError( + "target world does not contain one unambiguous learned structural role" + ) + node = matches[0] + return TransferInference(world.domain_id, node, roles[node], True) + + def _roles(self, world: RelationalWorld) -> dict[str, StructuralRole]: + nodes = world.nodes + outgoing: dict[str, list[str]] = {node: [] for node in nodes} + incoming: dict[str, list[str]] = {node: [] for node in nodes} + for edge in world.edges: + outgoing[edge.source].append(edge.target) + incoming[edge.target].append(edge.source) + sources = [node for node in nodes if not incoming[node]] + sinks = [node for node in nodes if not outgoing[node]] + if not sources or not sinks: + raise FoundationError( + "relational transfer requires an acyclic source-to-sink structure" + ) + from_source = self._distances(sources, outgoing) + to_sink = self._distances(sinks, incoming) + if set(from_source) != set(nodes) or set(to_sink) != set(nodes): + raise FoundationError("all relational nodes must connect source to sink") + return { + node: StructuralRole( + indegree=len(incoming[node]), + outdegree=len(outgoing[node]), + distance_from_source=from_source[node], + distance_to_sink=to_sink[node], + ) + for node in nodes + } + + @staticmethod + def _distances(starts: Iterable[str], adjacency: dict[str, list[str]]) -> dict[str, int]: + distances: dict[str, int] = {} + queue: deque[tuple[str, int]] = deque((node, 0) for node in starts) + while queue: + node, distance = queue.popleft() + if node in distances and distances[node] <= distance: + continue + distances[node] = distance + for neighbor in adjacency[node]: + queue.append((neighbor, distance + 1)) + return distances diff --git a/src/ix_sally/cognition/representation.py b/src/ix_sally/cognition/representation.py new file mode 100644 index 0000000..bf2bea2 --- /dev/null +++ b/src/ix_sally/cognition/representation.py @@ -0,0 +1,356 @@ +"""Bounded representation invention and semantic promotion. + +This module goes beyond selecting weights inside one fixed linear representation. Sally +constructs candidate *feature languages* from raw channels (atomic values, sums, +differences, products, absolute differences, minima, and maxima), measures whether the +existing atomic vocabulary is insufficient, and promotes a validated invented feature +into an opaque reusable semantic primitive. + +The grammar is deliberately finite per deliberation. This is experimental representation +synthesis, not a claim of unrestricted mathematical invention. +""" + +from __future__ import annotations + +import itertools +from collections.abc import Iterable +from dataclasses import dataclass, replace +from enum import StrEnum +from math import isfinite + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +class FeatureOperator(StrEnum): + """Operators Sally may combine to create an alternative representation.""" + + ATOMIC = "atomic" + SUM = "sum" + DIFFERENCE = "difference" + PRODUCT = "product" + ABS_DIFFERENCE = "abs_difference" + MINIMUM = "minimum" + MAXIMUM = "maximum" + + +@dataclass(frozen=True, slots=True) +class RepresentationObservation: + """Raw sensory channels plus one observed binary consequence.""" + + observation_id: str + channels: tuple[float, ...] + consequence: bool + + def __post_init__(self) -> None: + require_text(self.observation_id, field_name="observation_id") + if not self.channels: + raise FoundationError("representation observation requires channels") + if not all(isfinite(value) for value in self.channels): + raise FoundationError("representation channels must be finite") + + def to_payload(self) -> JsonObject: + return { + "observation_id": self.observation_id, + "channels": list(self.channels), + "consequence": self.consequence, + } + + +@dataclass(frozen=True, slots=True) +class InventedRepresentation: + """A machine-created feature that makes a previously poor distinction usable.""" + + representation_id: str + operator: FeatureOperator + left_index: int + right_index: int | None + threshold: float + polarity: int + training_accuracy: float + atomic_baseline_accuracy: float + training_observations: tuple[RepresentationObservation, ...] + validation_observations: tuple[RepresentationObservation, ...] = () + validation_accuracy: float | None = None + origin: str = "sally-representation-invention" + + def __post_init__(self) -> None: + require_text(self.representation_id, field_name="representation_id") + if self.left_index < 0: + raise FoundationError("left_index must not be negative") + if self.right_index is not None and self.right_index < 0: + raise FoundationError("right_index must not be negative") + if self.polarity not in {-1, 1}: + raise FoundationError("representation polarity must be -1 or 1") + if not isfinite(self.threshold): + raise FoundationError("representation threshold must be finite") + for name, value in ( + ("training_accuracy", self.training_accuracy), + ("atomic_baseline_accuracy", self.atomic_baseline_accuracy), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{name} must be between zero and one") + if self.validation_accuracy is not None and not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("validation_accuracy must be between zero and one") + + @property + def is_non_atomic(self) -> bool: + return self.operator is not FeatureOperator.ATOMIC + + def feature_value(self, channels: tuple[float, ...]) -> float: + """Evaluate the invented representation for one raw observation.""" + if self.left_index >= len(channels): + raise FoundationError("left representation index is outside observation arity") + left = channels[self.left_index] + if self.operator is FeatureOperator.ATOMIC: + return left + if self.right_index is None or self.right_index >= len(channels): + raise FoundationError("binary representation operator requires a valid right index") + right = channels[self.right_index] + if self.operator is FeatureOperator.SUM: + return left + right + if self.operator is FeatureOperator.DIFFERENCE: + return left - right + if self.operator is FeatureOperator.PRODUCT: + return left * right + if self.operator is FeatureOperator.ABS_DIFFERENCE: + return abs(left - right) + if self.operator is FeatureOperator.MINIMUM: + return min(left, right) + if self.operator is FeatureOperator.MAXIMUM: + return max(left, right) + raise FoundationError(f"unsupported representation operator: {self.operator.value}") + + def activates(self, channels: tuple[float, ...]) -> bool: + """Return the operational meaning of this opaque learned semantic.""" + return self.polarity * self.feature_value(channels) >= self.polarity * self.threshold + + def to_payload(self) -> JsonObject: + training: JsonArray = [item.to_payload() for item in self.training_observations] + validation: JsonArray = [item.to_payload() for item in self.validation_observations] + return { + "representation_id": self.representation_id, + "operator": self.operator.value, + "left_index": self.left_index, + "right_index": self.right_index, + "threshold": self.threshold, + "polarity": self.polarity, + "training_accuracy": self.training_accuracy, + "atomic_baseline_accuracy": self.atomic_baseline_accuracy, + "validation_accuracy": self.validation_accuracy, + "training_observations": training, + "validation_observations": validation, + "origin": self.origin, + "human_semantic_label": None, + } + + def digest(self) -> DigestRecord: + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class SemanticPrimitive: + """Validated invented representation promoted into Sally's usable ontology.""" + + primitive_id: str + representation: InventedRepresentation + + def __post_init__(self) -> None: + require_text(self.primitive_id, field_name="primitive_id") + if self.representation.validation_accuracy != 1.0: + raise FoundationError("semantic primitive requires perfect holdout validation") + + def evaluate(self, channels: tuple[float, ...]) -> bool: + return self.representation.activates(channels) + + def to_payload(self) -> JsonObject: + return { + "primitive_id": self.primitive_id, + "representation_digest": { + "algorithm": self.representation.digest().algorithm, + "value": self.representation.digest().value, + }, + "origin": "sally-invented-semantic-primitive", + "human_semantic_label": None, + } + + +class RepresentationInventor: + """Search alternative feature representations when atomic channels fail.""" + + def invent_binary( + self, + *, + observations: Iterable[RepresentationObservation], + minimum_improvement: float = 0.15, + ) -> InventedRepresentation: + training = tuple(observations) + self._validate(training) + if not 0.0 <= minimum_improvement <= 1.0: + raise FoundationError("minimum_improvement must be between zero and one") + atomic = self._best(training, operators=(FeatureOperator.ATOMIC,)) + operators = ( + FeatureOperator.SUM, + FeatureOperator.DIFFERENCE, + FeatureOperator.PRODUCT, + FeatureOperator.ABS_DIFFERENCE, + FeatureOperator.MINIMUM, + FeatureOperator.MAXIMUM, + ) + invented = self._best(training, operators=operators) + if invented is None or atomic is None: + raise FoundationError("representation search produced no candidate") + if invented[0] < atomic[0] + minimum_improvement: + raise FoundationError( + "no alternative representation materially improves atomic features" + ) + accuracy, operator, left, right, threshold, polarity = invented + identity = DigestRecord.from_payload( + { + "operator": operator.value, + "left": left, + "right": right, + "threshold": threshold, + "polarity": polarity, + "training": [item.to_payload() for item in training], + } + ) + return InventedRepresentation( + representation_id=f"representation-{identity.value[:16]}", + operator=operator, + left_index=left, + right_index=right, + threshold=threshold, + polarity=polarity, + training_accuracy=accuracy, + atomic_baseline_accuracy=atomic[0], + training_observations=training, + ) + + def validate( + self, + representation: InventedRepresentation, + *, + observations: Iterable[RepresentationObservation], + ) -> InventedRepresentation: + validation = tuple(observations) + self._validate( + validation, expected_arity=len(representation.training_observations[0].channels) + ) + correct = sum( + representation.activates(item.channels) is item.consequence for item in validation + ) + return replace( + representation, + validation_observations=validation, + validation_accuracy=correct / len(validation), + ) + + def promote(self, representation: InventedRepresentation) -> SemanticPrimitive: + if representation.training_accuracy != 1.0 or representation.validation_accuracy != 1.0: + raise FoundationError( + "semantic promotion requires perfect train and holdout performance" + ) + return SemanticPrimitive( + primitive_id=f"semantic-{representation.digest().value[:16]}", + representation=representation, + ) + + def _best( + self, + observations: tuple[RepresentationObservation, ...], + *, + operators: tuple[FeatureOperator, ...], + ) -> tuple[float, FeatureOperator, int, int | None, float, int] | None: + arity = len(observations[0].channels) + best: tuple[float, FeatureOperator, int, int | None, float, int] | None = None + index_pairs: list[tuple[int, int | None]] + for operator in operators: + if operator is FeatureOperator.ATOMIC: + index_pairs = [(index, None) for index in range(arity)] + else: + index_pairs = [ + (left, right) for left in range(arity) for right in range(left + 1, arity) + ] + for left, right in index_pairs: + values = tuple( + self._evaluate(operator, left, right, item.channels) for item in observations + ) + for threshold in self._thresholds(values): + for polarity in (-1, 1): + correct = sum( + (polarity * value >= polarity * threshold) is item.consequence + for value, item in zip(values, observations, strict=True) + ) + accuracy = correct / len(observations) + candidate = (accuracy, operator, left, right, threshold, polarity) + if best is None or self._rank(candidate) > self._rank(best): + best = candidate + return best + + @staticmethod + def _rank( + item: tuple[float, FeatureOperator, int, int | None, float, int], + ) -> tuple[float, int, int, int, float, int]: + accuracy, operator, left, right, threshold, polarity = item + operator_order = list(FeatureOperator).index(operator) + return ( + accuracy, + -operator_order, + -left, + -(right if right is not None else -1), + -abs(threshold), + polarity, + ) + + @staticmethod + def _evaluate( + operator: FeatureOperator, + left_index: int, + right_index: int | None, + channels: tuple[float, ...], + ) -> float: + left = channels[left_index] + if operator is FeatureOperator.ATOMIC: + return left + if right_index is None: + raise FoundationError("binary operator missing right channel") + right = channels[right_index] + if operator is FeatureOperator.SUM: + return left + right + if operator is FeatureOperator.DIFFERENCE: + return left - right + if operator is FeatureOperator.PRODUCT: + return left * right + if operator is FeatureOperator.ABS_DIFFERENCE: + return abs(left - right) + if operator is FeatureOperator.MINIMUM: + return min(left, right) + if operator is FeatureOperator.MAXIMUM: + return max(left, right) + raise FoundationError("unsupported feature operator") + + @staticmethod + def _thresholds(values: tuple[float, ...]) -> tuple[float, ...]: + ordered = sorted(set(values)) + if not ordered: + return (0.0,) + candidates = [ordered[0] - 1.0, ordered[-1] + 1.0, *ordered] + candidates.extend((left + right) / 2.0 for left, right in itertools.pairwise(ordered)) + return tuple(sorted(set(candidates))) + + @staticmethod + def _validate( + observations: tuple[RepresentationObservation, ...], + *, + expected_arity: int | None = None, + ) -> None: + if not observations: + raise FoundationError("representation invention requires observations") + arity = len(observations[0].channels) + if expected_arity is not None and arity != expected_arity: + raise FoundationError("representation validation arity mismatch") + if any(len(item.channels) != arity for item in observations): + raise FoundationError("representation observations must share one arity") + if len({item.consequence for item in observations}) < 2: + raise FoundationError("representation invention requires both consequence classes") diff --git a/src/ix_sally/cognition/representation_programs.py b/src/ix_sally/cognition/representation_programs.py new file mode 100644 index 0000000..8804b8f --- /dev/null +++ b/src/ix_sally/cognition/representation_programs.py @@ -0,0 +1,327 @@ +"""Compositional representation-language invention. + +IX-Sally v0.6 could replace atomic features with one invented relation. This module +pushes that boundary by allowing Sally to synthesize small *programs* over raw channels. +The grammar is fixed for safety and tractability, but complete representations are not +pre-enumerated and can contain multiple operations. Candidates are judged against a +simpler one-operation baseline and must survive held-out validation. +""" + +from __future__ import annotations + +import itertools +from collections.abc import Iterable +from dataclasses import dataclass, replace +from enum import StrEnum +from math import isfinite + +from ix_sally.cognition.representation import RepresentationObservation +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +class ProgramOperator(StrEnum): + CHANNEL = "channel" + ADD = "add" + SUBTRACT = "subtract" + MULTIPLY = "multiply" + ABS = "abs" + NEGATE = "negate" + + +@dataclass(frozen=True, slots=True) +class FeatureProgram: + """One executable representation expression.""" + + operator: ProgramOperator + channel_index: int | None = None + left: FeatureProgram | None = None + right: FeatureProgram | None = None + + def __post_init__(self) -> None: + if self.operator is ProgramOperator.CHANNEL: + if self.channel_index is None or self.channel_index < 0: + raise FoundationError("channel feature requires a non-negative index") + if self.left is not None or self.right is not None: + raise FoundationError("channel feature cannot contain child programs") + return + if self.operator in {ProgramOperator.ABS, ProgramOperator.NEGATE}: + if self.left is None or self.right is not None: + raise FoundationError("unary feature requires exactly one child") + return + if self.left is None or self.right is None: + raise FoundationError("binary feature requires two children") + + @property + def depth(self) -> int: + if self.operator is ProgramOperator.CHANNEL: + return 0 + if self.right is None: + assert self.left is not None + return 1 + self.left.depth + assert self.left is not None + return 1 + max(self.left.depth, self.right.depth) + + @property + def complexity(self) -> int: + if self.operator is ProgramOperator.CHANNEL: + return 1 + if self.right is None: + assert self.left is not None + return 1 + self.left.complexity + assert self.left is not None + return 1 + self.left.complexity + self.right.complexity + + def evaluate(self, channels: tuple[float, ...]) -> float: + if self.operator is ProgramOperator.CHANNEL: + assert self.channel_index is not None + if self.channel_index >= len(channels): + raise FoundationError("feature program channel is outside observation arity") + return channels[self.channel_index] + assert self.left is not None + left = self.left.evaluate(channels) + if self.operator is ProgramOperator.ABS: + return abs(left) + if self.operator is ProgramOperator.NEGATE: + return -left + assert self.right is not None + right = self.right.evaluate(channels) + if self.operator is ProgramOperator.ADD: + return left + right + if self.operator is ProgramOperator.SUBTRACT: + return left - right + if self.operator is ProgramOperator.MULTIPLY: + return left * right + raise FoundationError(f"unsupported feature-program operator: {self.operator.value}") + + def expression(self) -> str: + if self.operator is ProgramOperator.CHANNEL: + return f"x{self.channel_index}" + assert self.left is not None + if self.operator is ProgramOperator.ABS: + return f"abs({self.left.expression()})" + if self.operator is ProgramOperator.NEGATE: + return f"neg({self.left.expression()})" + assert self.right is not None + symbol = { + ProgramOperator.ADD: "+", + ProgramOperator.SUBTRACT: "-", + ProgramOperator.MULTIPLY: "*", + }[self.operator] + return f"({self.left.expression()}{symbol}{self.right.expression()})" + + def to_payload(self) -> JsonObject: + return { + "operator": self.operator.value, + "channel_index": self.channel_index, + "left": self.left.to_payload() if self.left is not None else None, + "right": self.right.to_payload() if self.right is not None else None, + "depth": self.depth, + "complexity": self.complexity, + "expression": self.expression(), + } + + +@dataclass(frozen=True, slots=True) +class InventedRepresentationProgram: + representation_id: str + program: FeatureProgram + threshold: float + polarity: int + training_accuracy: float + simple_baseline_accuracy: float + validation_accuracy: float | None = None + + def __post_init__(self) -> None: + require_text(self.representation_id, field_name="representation_id") + if self.polarity not in {-1, 1}: + raise FoundationError("representation-program polarity must be -1 or 1") + if not isfinite(self.threshold): + raise FoundationError("representation-program threshold must be finite") + if not 0.0 <= self.training_accuracy <= 1.0: + raise FoundationError("training accuracy must be between zero and one") + if not 0.0 <= self.simple_baseline_accuracy <= 1.0: + raise FoundationError("baseline accuracy must be between zero and one") + if self.validation_accuracy is not None and not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("validation accuracy must be between zero and one") + + def activates(self, channels: tuple[float, ...]) -> bool: + value = self.program.evaluate(channels) + return self.polarity * value >= self.polarity * self.threshold + + def to_payload(self) -> JsonObject: + return { + "representation_id": self.representation_id, + "program": self.program.to_payload(), + "threshold": self.threshold, + "polarity": self.polarity, + "training_accuracy": self.training_accuracy, + "simple_baseline_accuracy": self.simple_baseline_accuracy, + "validation_accuracy": self.validation_accuracy, + "origin": "sally-compositional-representation-invention", + "human_semantic_label": None, + } + + def digest(self) -> DigestRecord: + return DigestRecord.from_payload(self.to_payload()) + + +class RepresentationProgramInventor: + """Synthesize a multi-operation representation when shallower languages fail.""" + + def invent( + self, + *, + observations: Iterable[RepresentationObservation], + max_depth: int = 2, + max_candidates: int = 4096, + minimum_improvement: float = 0.15, + ) -> InventedRepresentationProgram: + items = tuple(observations) + self._validate(items) + if max_depth < 1 or max_candidates < 1: + raise FoundationError("representation-program search bounds are invalid") + if not 0.0 <= minimum_improvement <= 1.0: + raise FoundationError("minimum improvement must be between zero and one") + arity = len(items[0].channels) + levels = self._generate(arity=arity, max_depth=max_depth, max_candidates=max_candidates) + simple = tuple(program for program in levels if program.depth <= 1) + all_programs = tuple(levels) + baseline = self._best(items, simple) + best = self._best(items, all_programs) + if baseline is None or best is None: + raise FoundationError("representation-program search produced no candidate") + if best[0] < baseline[0] + minimum_improvement: + raise FoundationError( + "no compositional representation materially improves shallow features" + ) + accuracy, program, threshold, polarity = best + identity = DigestRecord.from_payload( + { + "program": program.to_payload(), + "threshold": threshold, + "polarity": polarity, + "training": [item.to_payload() for item in items], + } + ) + return InventedRepresentationProgram( + representation_id=f"program-representation-{identity.value[:16]}", + program=program, + threshold=threshold, + polarity=polarity, + training_accuracy=accuracy, + simple_baseline_accuracy=baseline[0], + ) + + def validate( + self, + representation: InventedRepresentationProgram, + *, + observations: Iterable[RepresentationObservation], + ) -> InventedRepresentationProgram: + items = tuple(observations) + self._validate(items) + correct = sum(representation.activates(item.channels) is item.consequence for item in items) + return replace(representation, validation_accuracy=correct / len(items)) + + def _generate( + self, *, arity: int, max_depth: int, max_candidates: int + ) -> tuple[FeatureProgram, ...]: + programs: list[FeatureProgram] = [ + FeatureProgram(ProgramOperator.CHANNEL, channel_index=i) for i in range(arity) + ] + by_depth: dict[int, list[FeatureProgram]] = {0: list(programs)} + seen = {program.expression() for program in programs} + for depth in range(1, max_depth + 1): + created: list[FeatureProgram] = [] + previous = tuple(program for d in range(depth) for program in by_depth.get(d, ())) + frontier = tuple(by_depth.get(depth - 1, ())) + for child in frontier: + for operator in (ProgramOperator.ABS, ProgramOperator.NEGATE): + candidate = FeatureProgram(operator, left=child) + if candidate.expression() not in seen: + seen.add(candidate.expression()) + created.append(candidate) + # At least one child must be from the previous depth so the candidate really grows. + for left in frontier: + for right in previous: + for operator in ( + ProgramOperator.ADD, + ProgramOperator.SUBTRACT, + ProgramOperator.MULTIPLY, + ): + candidate = FeatureProgram(operator, left=left, right=right) + expression = candidate.expression() + if expression not in seen: + seen.add(expression) + created.append(candidate) + if len(programs) + len(created) >= max_candidates: + break + if len(programs) + len(created) >= max_candidates: + break + if len(programs) + len(created) >= max_candidates: + break + by_depth[depth] = created + programs.extend(created) + if len(programs) >= max_candidates: + return tuple(programs[:max_candidates]) + if not created: + break + return tuple(programs) + + def _best( + self, + observations: tuple[RepresentationObservation, ...], + programs: tuple[FeatureProgram, ...], + ) -> tuple[float, FeatureProgram, float, int] | None: + best: tuple[float, FeatureProgram, float, int] | None = None + signatures: set[tuple[float, ...]] = set() + for program in programs: + values = tuple(round(program.evaluate(item.channels), 12) for item in observations) + if values in signatures: + continue + signatures.add(values) + for threshold in self._thresholds(values): + for polarity in (-1, 1): + correct = sum( + (polarity * value >= polarity * threshold) is item.consequence + for value, item in zip(values, observations, strict=True) + ) + accuracy = correct / len(observations) + candidate = (accuracy, program, threshold, polarity) + if best is None or self._rank(candidate) > self._rank(best): + best = candidate + return best + + @staticmethod + def _rank( + item: tuple[float, FeatureProgram, float, int], + ) -> tuple[float, int, int, float, int, tuple[int, ...]]: + accuracy, program, threshold, polarity = item + return ( + accuracy, + -program.depth, + -program.complexity, + -abs(threshold), + polarity, + tuple(-ord(ch) for ch in program.expression()), + ) + + @staticmethod + def _thresholds(values: tuple[float, ...]) -> tuple[float, ...]: + ordered = sorted(set(values)) + candidates = [ordered[0] - 1.0, ordered[-1] + 1.0, *ordered] + candidates.extend((left + right) / 2.0 for left, right in itertools.pairwise(ordered)) + return tuple(sorted(set(candidates))) + + @staticmethod + def _validate(observations: tuple[RepresentationObservation, ...]) -> None: + if not observations: + raise FoundationError("representation-program invention requires observations") + arity = len(observations[0].channels) + if any(len(item.channels) != arity for item in observations): + raise FoundationError("representation-program observations must share one arity") + if len({item.consequence for item in observations}) < 2: + raise FoundationError( + "representation-program invention requires both consequence classes" + ) diff --git a/src/ix_sally/cognition/restore.py b/src/ix_sally/cognition/restore.py index e974e7a..259bc03 100644 --- a/src/ix_sally/cognition/restore.py +++ b/src/ix_sally/cognition/restore.py @@ -32,7 +32,9 @@ OutcomeStatus, SkillProfile, ) +from ix_sally.cognition.lifelong import KnowledgeItem, LifelongKnowledgeStore from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.online_meta import OnlineMetaProfile, StrategyExperience, TaskFingerprint from ix_sally.cognition.persistence import CognitiveSnapshot from ix_sally.cognition.planning import ActionSpec, FactEffect from ix_sally.cognition.primitives import ( @@ -79,6 +81,8 @@ class RestoredCognitiveState: episodes: EpisodeLedger curriculum: CurriculumLedger | None primitive_registry: PrimitiveRegistry + lifelong_knowledge: LifelongKnowledgeStore + online_meta_profile: OnlineMetaProfile runtime_memories: dict[str, CognitiveValue] execution_count: int cycle_count: int @@ -647,6 +651,66 @@ def _restore_primitives(value: JsonValue) -> PrimitiveRegistry: ) +def _restore_lifelong(value: JsonValue) -> LifelongKnowledgeStore: + payload = _object(value, field="lifelong_knowledge") + generation = _integer(payload.get("generation"), field="lifelong_knowledge.generation") + items = tuple( + KnowledgeItem( + concept_id=_text(item.get("concept_id"), field="lifelong_knowledge.concept_id"), + content_digest=_digest( + item.get("content_digest"), + field="lifelong_knowledge.content_digest", + ), + confidence=_number(item.get("confidence"), field="lifelong_knowledge.confidence"), + utility=_number(item.get("utility"), field="lifelong_knowledge.utility"), + use_count=_integer(item.get("use_count"), field="lifelong_knowledge.use_count"), + contradiction_count=_integer( + item.get("contradiction_count"), + field="lifelong_knowledge.contradiction_count", + ), + generation=_integer(item.get("generation"), field="lifelong_knowledge.item_generation"), + superseded_by=_optional_text( + item.get("superseded_by"), + field="lifelong_knowledge.superseded_by", + ), + ) + for item in ( + _object(raw, field="lifelong_knowledge.items[]") + for raw in _array(payload.get("items"), field="lifelong_knowledge.items") + ) + ) + return LifelongKnowledgeStore(items=items, generation=generation) + + +def _restore_online_meta(value: JsonValue) -> OnlineMetaProfile: + # v0.6 snapshots had no online meta profile; preserve backward compatibility. + if value is None: + return OnlineMetaProfile() + payload = _object(value, field="online_meta_profile") + experiences = tuple( + StrategyExperience( + strategy_id=_text(item.get("strategy_id"), field="online_meta_profile.strategy_id"), + fingerprint=TaskFingerprint( + tuple( + _number(raw_value, field="online_meta_profile.fingerprint[]") + for raw_value in _array( + item.get("fingerprint"), field="online_meta_profile.fingerprint" + ) + ) + ), + score=_number(item.get("score"), field="online_meta_profile.score"), + samples_used=_integer( + item.get("samples_used"), field="online_meta_profile.samples_used" + ), + ) + for item in ( + _object(raw, field="online_meta_profile.experiences[]") + for raw in _array(payload.get("experiences"), field="online_meta_profile.experiences") + ) + ) + return OnlineMetaProfile(experiences) + + def restore_system_state(snapshot: CognitiveSnapshot) -> RestoredCognitiveState: """Restore and revalidate every serialized IX-Sally cognitive subsystem.""" state = snapshot.state @@ -671,6 +735,8 @@ def restore_system_state(snapshot: CognitiveSnapshot) -> RestoredCognitiveState: episodes=_restore_episodes(state.get("episodes")), curriculum=_restore_curriculum(state.get("curriculum")), primitive_registry=_restore_primitives(state.get("primitive_registry")), + lifelong_knowledge=_restore_lifelong(state.get("lifelong_knowledge")), + online_meta_profile=_restore_online_meta(state.get("online_meta_profile")), runtime_memories=runtime_memories, execution_count=_integer( state.get("execution_count"), diff --git a/src/ix_sally/cognition/semantic_genesis.py b/src/ix_sally/cognition/semantic_genesis.py new file mode 100644 index 0000000..523155d --- /dev/null +++ b/src/ix_sally/cognition/semantic_genesis.py @@ -0,0 +1,258 @@ +"""Autonomous semantic formation from prediction residuals. + +IX-Sally may create a provisional internal concept when the current semantic vocabulary +cannot explain an observed regularity. The concept is intentionally opaque: it is not +assigned a human label or meaning. Its semantics are earned operationally through a +repeatable relation between raw observations and outcomes, followed by holdout testing. + +This is a bounded latent-concept search, not a claim of unrestricted ontology creation. +Every concrete search remains finite, inspectable, falsifiable, and removable. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, replace +from itertools import pairwise, product +from math import isfinite + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class SemanticObservation: + """Raw numeric observation plus an externally observed binary consequence.""" + + observation_id: str + channels: tuple[float, ...] + consequence: bool + + def __post_init__(self) -> None: + require_text(self.observation_id, field_name="observation_id") + if not self.channels: + raise FoundationError("semantic observation requires at least one raw channel") + if not all(isfinite(value) for value in self.channels): + raise FoundationError("semantic observation channels must be finite") + + def to_payload(self) -> JsonObject: + return { + "observation_id": self.observation_id, + "channels": list(self.channels), + "consequence": self.consequence, + } + + +@dataclass(frozen=True, slots=True) +class InventedSemantic: + """One opaque latent predicate invented from raw relations and reality-tested.""" + + concept_id: str + weights: tuple[int, ...] + threshold: float + training_accuracy: float + atomic_baseline_accuracy: float + training_observations: tuple[SemanticObservation, ...] + validation_observations: tuple[SemanticObservation, ...] = () + validation_accuracy: float | None = None + origin: str = "sally-semantic-genesis" + + def __post_init__(self) -> None: + require_text(self.concept_id, field_name="concept_id") + if not self.weights or all(weight == 0 for weight in self.weights): + raise FoundationError("invented semantic requires a non-zero relation") + if not isfinite(self.threshold): + raise FoundationError("semantic threshold must be finite") + for field_name, value in ( + ("training_accuracy", self.training_accuracy), + ("atomic_baseline_accuracy", self.atomic_baseline_accuracy), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"{field_name} must be between zero and one") + if self.validation_accuracy is not None and not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("validation_accuracy must be between zero and one") + + @property + def relation_arity(self) -> int: + """Return how many raw channels participate in the invented distinction.""" + return sum(1 for weight in self.weights if weight != 0) + + def score(self, channels: tuple[float, ...]) -> float: + """Return the latent relation score for one raw observation.""" + if len(channels) != len(self.weights): + raise FoundationError("semantic input arity does not match invented relation") + return sum(weight * value for weight, value in zip(self.weights, channels, strict=True)) + + def activates(self, channels: tuple[float, ...]) -> bool: + """Return whether this opaque semantic token applies to the observation.""" + return self.score(channels) >= self.threshold + + def to_payload(self) -> JsonObject: + training: JsonArray = [item.to_payload() for item in self.training_observations] + validation: JsonArray = [item.to_payload() for item in self.validation_observations] + return { + "concept_id": self.concept_id, + "weights": list(self.weights), + "threshold": self.threshold, + "relation_arity": self.relation_arity, + "training_accuracy": self.training_accuracy, + "atomic_baseline_accuracy": self.atomic_baseline_accuracy, + "training_observations": training, + "validation_observations": validation, + "validation_accuracy": self.validation_accuracy, + "origin": self.origin, + "human_semantic_label": None, + } + + def digest(self) -> DigestRecord: + return DigestRecord.from_payload(self.to_payload()) + + +class SemanticGenesisEngine: + """Create provisional latent concepts when simpler supplied semantics are inadequate.""" + + def invent( + self, + *, + observations: Iterable[SemanticObservation], + max_abs_weight: int = 2, + minimum_improvement: float = 0.10, + ) -> InventedSemantic: + """Invent a relational predicate directly from raw channels. + + The search first measures the best single-channel (atomic) explanation. It then + searches relational projections over multiple channels. A new semantic token is + admitted only when it materially improves on the atomic vocabulary. + """ + training = tuple(observations) + self._validate_dataset(training) + if max_abs_weight < 1: + raise FoundationError("max_abs_weight must be positive") + if not 0.0 <= minimum_improvement <= 1.0: + raise FoundationError("minimum_improvement must be between zero and one") + + channel_count = len(training[0].channels) + atomic_accuracy = self._best_atomic_accuracy(training, max_abs_weight=max_abs_weight) + best_rank: tuple[float, int, int, tuple[int, ...], float] | None = None + best_choice: tuple[tuple[int, ...], float, float] | None = None + + values = range(-max_abs_weight, max_abs_weight + 1) + for weights in product(values, repeat=channel_count): + if all(weight == 0 for weight in weights): + continue + # Semantic genesis requires a relation that is not merely one existing channel. + arity = sum(1 for weight in weights if weight != 0) + if arity < 2: + continue + projections = tuple(self._project(item.channels, weights) for item in training) + for threshold in self._thresholds(projections): + accuracy = self._accuracy(training, weights, threshold) + complexity = sum(abs(weight) for weight in weights) + # maximize accuracy, then prefer lower arity/complexity and stable lexicography + rank = (accuracy, -arity, -complexity, tuple(-w for w in weights), -threshold) + if best_rank is None or rank > best_rank: + best_rank = rank + best_choice = (weights, threshold, accuracy) + + if best_choice is None: + raise FoundationError("semantic genesis found no relational candidate") + weights, threshold, accuracy = best_choice + if accuracy < atomic_accuracy + minimum_improvement: + raise FoundationError( + "no invented semantic materially improves on the atomic vocabulary" + ) + + identity = DigestRecord.from_payload( + { + "weights": list(weights), + "threshold": threshold, + "training": [item.to_payload() for item in training], + } + ) + return InventedSemantic( + concept_id=f"latent-{identity.value[:16]}", + weights=weights, + threshold=threshold, + training_accuracy=accuracy, + atomic_baseline_accuracy=atomic_accuracy, + training_observations=training, + ) + + def validate( + self, + concept: InventedSemantic, + *, + observations: Iterable[SemanticObservation], + ) -> InventedSemantic: + """Reality-test a provisional semantic token on unseen observations.""" + validation = tuple(observations) + self._validate_dataset(validation, expected_arity=len(concept.weights)) + accuracy = self._accuracy(validation, concept.weights, concept.threshold) + return replace( + concept, + validation_observations=validation, + validation_accuracy=accuracy, + ) + + @staticmethod + def _project(channels: tuple[float, ...], weights: tuple[int, ...]) -> float: + return sum(weight * value for weight, value in zip(weights, channels, strict=True)) + + @classmethod + def _accuracy( + cls, + observations: tuple[SemanticObservation, ...], + weights: tuple[int, ...], + threshold: float, + ) -> float: + correct = sum( + (cls._project(item.channels, weights) >= threshold) is item.consequence + for item in observations + ) + return correct / len(observations) + + @classmethod + def _best_atomic_accuracy( + cls, + observations: tuple[SemanticObservation, ...], + *, + max_abs_weight: int, + ) -> float: + _ = max_abs_weight # Retained for API compatibility with bounded semantic search. + channel_count = len(observations[0].channels) + best = 0.0 + for index in range(channel_count): + for sign in (-1, 1): + weights = tuple( + sign if position == index else 0 for position in range(channel_count) + ) + projections = tuple(cls._project(item.channels, weights) for item in observations) + for threshold in cls._thresholds(projections): + best = max(best, cls._accuracy(observations, weights, threshold)) + return best + + @staticmethod + def _thresholds(projections: tuple[float, ...]) -> tuple[float, ...]: + values = sorted(set(projections)) + if not values: + return (0.0,) + candidates = [values[0] - 1.0, values[-1] + 1.0] + candidates.extend(values) + candidates.extend((left + right) / 2.0 for left, right in pairwise(values)) + return tuple(sorted(set(candidates))) + + @staticmethod + def _validate_dataset( + observations: tuple[SemanticObservation, ...], + *, + expected_arity: int | None = None, + ) -> None: + if not observations: + raise FoundationError("semantic genesis requires observations") + arity = len(observations[0].channels) + if expected_arity is not None and arity != expected_arity: + raise FoundationError("semantic validation arity differs from learned concept") + if any(len(item.channels) != arity for item in observations): + raise FoundationError("semantic observations must share one raw-channel arity") + if len({item.consequence for item in observations}) < 2: + raise FoundationError("semantic genesis requires both observed consequence classes") diff --git a/src/ix_sally/cognition/system.py b/src/ix_sally/cognition/system.py index 1959f33..8432068 100644 --- a/src/ix_sally/cognition/system.py +++ b/src/ix_sally/cognition/system.py @@ -2,9 +2,20 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Callable, Iterable from dataclasses import dataclass, field +from ix_sally.cognition.active_inference import ( + ActivePerceptionPlanner, + CausalDiscoveryEngine, + CausalDiscoveryReport, + CausalObservation, + CounterfactualAction, + CounterfactualSimulator, + ImaginedBranch, + PerceptionProbe, + ProbeChoice, +) from ix_sally.cognition.active_memory import ( ActiveMemoryEntry, ActiveMemoryStore, @@ -13,14 +24,83 @@ from ix_sally.cognition.curriculum import CurriculumLedger, CurriculumTrial from ix_sally.cognition.episodes import CognitiveEpisode, EpisodeLedger from ix_sally.cognition.executive import ExecutiveController, ExecutiveDecision +from ix_sally.cognition.external_evaluation import ( + BlindChallenge, + BlindEvaluationResult, + BlindEvaluatorHarness, +) +from ix_sally.cognition.goal_portfolio import GoalPortfolioDecision, GoalPortfolioManager +from ix_sally.cognition.goal_reasoning import ( + GoalArbiter, + GoalEvidence, + GoalResolution, + GoalRevisionEngine, +) from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus from ix_sally.cognition.governance_bridge import ( CognitiveProposalBridge, CognitiveProposalBridgeResult, ) +from ix_sally.cognition.instrumental_goals import ( + InstrumentalGoalGenerator, + InstrumentalGoalProposal, +) +from ix_sally.cognition.invention import ( + ConceptInventor, + InventedHypothesis, + InventedPrimitive, + TransformationExample, +) +from ix_sally.cognition.knowledge_maintenance import ( + ContextualKnowledgeEvidence, + KnowledgeMaintenanceEngine, + KnowledgeMaintenanceReport, +) from ix_sally.cognition.learning import LearningLedger, LearningOutcome +from ix_sally.cognition.lifelong import ( + AbstractTransitionRule, + CurriculumChoice, + DomainAdapter, + KnowledgeItem, + LifelongKnowledgeStore, + OntologyRestructurer, + PredictionSignature, + RestructuredConcept, + SelfDirectedCurriculum, + StructuralAnalogyEngine, +) +from ix_sally.cognition.lifetime_learning import ( + LifetimeChallenge, + LifetimeLearningEngine, + LifetimeLearningReport, +) +from ix_sally.cognition.long_horizon import HorizonAction, LongHorizonController, LongHorizonResult +from ix_sally.cognition.meta_learning import ( + AdaptiveSearchPolicy, + FailureObservation, + ImprovementBenchmark, + LearningStrategyTrial, + MetaLearningController, + MetaLearningDecision, + SearchBudgetAllocation, + SearchOperatorTrial, + SelfDiagnostic, + SelfDiagnosticReport, + SelfImprovementLab, + SelfImprovementResult, +) from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel from ix_sally.cognition.ninefold import NinefoldCognitiveCycle, NinefoldCoordinator +from ix_sally.cognition.online_meta import OnlineMetaDecision, OnlineMetaProfile, TaskFingerprint +from ix_sally.cognition.open_choice import ( + ActionPrimitive, + ConstructedAction, + DeliberationPolicy, + DeliberationSignals, + OpenChoiceResult, + OpenChoiceSynthesizer, +) +from ix_sally.cognition.open_goals import GeneratedGoal, IntrinsicDrives, OpenGoalGenesis from ix_sally.cognition.persistence import CognitiveSnapshot from ix_sally.cognition.planning import ( ActionSpec, @@ -35,11 +115,48 @@ PrimitiveRegistry, default_primitive_registry, ) +from ix_sally.cognition.raw_perception import GroundedSignal, RawSignal, RawSignalGrounder +from ix_sally.cognition.recursive_bootstrap import ( + RecursiveBootstrapReport, + RecursiveCognitionEngine, +) +from ix_sally.cognition.relational_transfer import ( + LearnedStructuralSchema, + RelationalTransferEngine, + RelationalWorld, + TransferInference, +) +from ix_sally.cognition.representation import ( + InventedRepresentation, + RepresentationInventor, + RepresentationObservation, + SemanticPrimitive, +) +from ix_sally.cognition.representation_programs import ( + InventedRepresentationProgram, + RepresentationProgramInventor, +) +from ix_sally.cognition.semantic_genesis import ( + InventedSemantic, + SemanticGenesisEngine, + SemanticObservation, +) +from ix_sally.cognition.text_grounding import ( + GroundedTextFeature, + TextOutcomeGrounder, + TextOutcomeObservation, +) +from ix_sally.cognition.tool_forge import ForgedTool, ToolForge, ToolValidationCase from ix_sally.cognition.uncertainty import ( CalibrationObservation, CalibrationReport, UncertaintyLedger, ) +from ix_sally.cognition.unknowns import ( + PredictionResidual, + UnknownUnknownDetector, + UnknownUnknownSignal, +) from ix_sally.cognition.values import CognitiveValue from ix_sally.cognition.vm import IXVirtualMachine, VMResult, VMStatus from ix_sally.cognition.workspace import CognitiveWorkspace, WorkspaceItem @@ -63,6 +180,8 @@ class SallyCognitiveSystem: episodes: EpisodeLedger = field(default_factory=EpisodeLedger) curriculum: CurriculumLedger | None = None primitive_registry: PrimitiveRegistry = field(default_factory=default_primitive_registry) + lifelong_knowledge: LifelongKnowledgeStore = field(default_factory=LifelongKnowledgeStore) + online_meta_profile: OnlineMetaProfile = field(default_factory=OnlineMetaProfile) runtime_memories: dict[str, CognitiveValue] = field(default_factory=dict) execution_count: int = 0 cycle_count: int = 0 @@ -98,6 +217,8 @@ def from_snapshot(cls, snapshot: CognitiveSnapshot) -> SallyCognitiveSystem: episodes=restored.episodes, curriculum=restored.curriculum, primitive_registry=restored.primitive_registry, + lifelong_knowledge=restored.lifelong_knowledge, + online_meta_profile=restored.online_meta_profile, runtime_memories=restored.runtime_memories, execution_count=restored.execution_count, cycle_count=restored.cycle_count, @@ -152,6 +273,516 @@ def infer_world(self) -> None: """Apply all currently satisfied causal rules once.""" self.world_model = self.world_model.infer() + def construct_open_choice( + self, + *, + initial_state: int, + goal_test: Callable[[int], bool], + primitives: Iterable[ActionPrimitive], + offered_actions: Iterable[ConstructedAction] = (), + max_depth: int = 8, + max_programs: int = 4096, + ) -> OpenChoiceResult: + """Author a complete action beyond a supplied menu when composition can satisfy it.""" + return OpenChoiceSynthesizer().synthesize( + initial_state=initial_state, + goal_test=goal_test, + primitives=primitives, + offered_actions=offered_actions, + max_depth=max_depth, + max_programs=max_programs, + ) + + def minimize_constructed_action( + self, + *, + initial_state: int, + action: ConstructedAction, + primitives: Iterable[ActionPrimitive], + goal_test: Callable[[int], bool], + ) -> ConstructedAction: + """Delete unnecessary steps while retaining only independently successful behavior.""" + return OpenChoiceSynthesizer().minimize( + initial_state=initial_state, + action=action, + primitives=primitives, + goal_test=goal_test, + ) + + def should_reopen_deliberation(self, signals: DeliberationSignals) -> bool: + """Decide whether surprise or changed circumstances should interrupt a learned habit.""" + return DeliberationPolicy().should_reopen(signals) + + def invent_hypothesis( + self, + *, + examples: Iterable[TransformationExample], + primitives: Iterable[ActionPrimitive], + max_depth: int = 6, + max_programs: int = 4096, + ) -> InventedHypothesis: + """Invent an explanatory program without receiving a hypothesis catalog.""" + return ConceptInventor().invent_hypothesis( + examples=examples, + primitives=primitives, + max_depth=max_depth, + max_programs=max_programs, + ) + + def validate_invented_hypothesis( + self, + hypothesis: InventedHypothesis, + *, + examples: Iterable[TransformationExample], + primitives: Iterable[ActionPrimitive], + ) -> InventedHypothesis: + """Test an invented explanation against held-out evidence.""" + return ConceptInventor().validate_hypothesis( + hypothesis, + examples=examples, + primitives=primitives, + ) + + def promote_invented_primitive( + self, + hypothesis: InventedHypothesis, + *, + primitive_id: str, + description: str, + ) -> InventedPrimitive: + """Turn a validated learned program into a reusable new abstraction.""" + return ConceptInventor().promote_primitive( + hypothesis, + primitive_id=primitive_id, + description=description, + ) + + def self_generate_instrumental_goals( + self, + *, + continuity_risk: float = 0.0, + resource_pressure: float = 0.0, + integrity_anomaly: float = 0.0, + ) -> tuple[InstrumentalGoalProposal, ...]: + """Derive bounded instrumental goals from Sally's measured internal condition.""" + generator = InstrumentalGoalGenerator() + signals = generator.signals_from_state( + self_model=self.self_model, + uncertainty=self.uncertainty, + continuity_risk=continuity_risk, + resource_pressure=resource_pressure, + integrity_anomaly=integrity_anomaly, + ) + return generator.propose(signals) + + def invent_semantic( + self, + *, + observations: Iterable[SemanticObservation], + max_abs_weight: int = 2, + minimum_improvement: float = 0.10, + ) -> InventedSemantic: + """Create an opaque predictive semantic distinction from unresolved raw structure.""" + return SemanticGenesisEngine().invent( + observations=observations, + max_abs_weight=max_abs_weight, + minimum_improvement=minimum_improvement, + ) + + def validate_invented_semantic( + self, + concept: InventedSemantic, + *, + observations: Iterable[SemanticObservation], + ) -> InventedSemantic: + """Reality-test an invented semantic token on unseen observations.""" + return SemanticGenesisEngine().validate(concept, observations=observations) + + def generate_open_goal( + self, + *, + initial_state: int, + primitives: Iterable[ActionPrimitive], + known_states: Iterable[int] = (), + drives: IntrinsicDrives | None = None, + max_depth: int = 4, + max_programs: int = 512, + state_bound: int = 256, + ) -> GeneratedGoal: + """Author a novel internal target without receiving a fixed goal category or target.""" + return OpenGoalGenesis().generate( + initial_state=initial_state, + primitives=primitives, + known_states=known_states, + drives=drives, + max_depth=max_depth, + max_programs=max_programs, + state_bound=state_bound, + ) + + def invent_representation( + self, + *, + observations: Iterable[RepresentationObservation], + minimum_improvement: float = 0.15, + ) -> InventedRepresentation: + """Synthesize a non-atomic representation when raw channels are insufficient.""" + return RepresentationInventor().invent_binary( + observations=observations, + minimum_improvement=minimum_improvement, + ) + + def validate_representation( + self, + representation: InventedRepresentation, + *, + observations: Iterable[RepresentationObservation], + ) -> InventedRepresentation: + """Reality-test an invented representation on held-out evidence.""" + return RepresentationInventor().validate(representation, observations=observations) + + def promote_semantic_primitive( + self, + representation: InventedRepresentation, + ) -> SemanticPrimitive: + """Promote a validated invented representation into Sally's usable ontology.""" + return RepresentationInventor().promote(representation) + + def integrate_knowledge(self, item: KnowledgeItem) -> None: + """Persist one learned concept/tool in lifelong knowledge.""" + self.lifelong_knowledge = self.lifelong_knowledge.integrate(item) + + def record_knowledge_use(self, concept_id: str, *, successful: bool) -> None: + """Revise persistent knowledge confidence from later experience.""" + self.lifelong_knowledge = self.lifelong_knowledge.record_use( + concept_id, successful=successful + ) + + def consolidate_lifelong_knowledge(self) -> None: + """Consolidate persistent knowledge while protecting useful validated items.""" + self.lifelong_knowledge = self.lifelong_knowledge.advance_generation().consolidate() + + def restructure_knowledge( + self, + signatures: Iterable[PredictionSignature], + ) -> tuple[RestructuredConcept, ...]: + """Discover higher abstractions among behaviorally redundant concepts.""" + abstractions = OntologyRestructurer().restructure(signatures) + for abstraction in abstractions: + member_ids = {item.concept_id for item in self.lifelong_knowledge.items} + if set(abstraction.member_ids).issubset(member_ids): + self.lifelong_knowledge = OntologyRestructurer().apply_to_store( + self.lifelong_knowledge, abstraction + ) + return abstractions + + def choose_self_directed_curriculum( + self, + *, + uncertainty: dict[str, float] | None = None, + opportunity: dict[str, float] | None = None, + ) -> CurriculumChoice: + """Choose what measured capability to practice next.""" + return SelfDirectedCurriculum().choose( + self_model=self.self_model, + uncertainty=uncertainty, + opportunity=opportunity, + ) + + def discover_causality( + self, observations: Iterable[CausalObservation] + ) -> CausalDiscoveryReport: + """Separate intervention effects from observational association.""" + return CausalDiscoveryEngine().discover(observations) + + def choose_active_perception( + self, + *, + priors: Iterable[float], + probes: Iterable[PerceptionProbe], + ) -> ProbeChoice: + """Choose the next observation by expected information gain.""" + return ActivePerceptionPlanner().choose(priors=priors, probes=probes) + + def imagine_counterfactuals( + self, + *, + initial_state: int, + actions: Iterable[CounterfactualAction], + depth: int = 3, + max_branches: int = 256, + ) -> tuple[ImaginedBranch, ...]: + """Simulate branching futures without changing the outside world.""" + return CounterfactualSimulator().imagine( + initial_state=initial_state, + actions=actions, + depth=depth, + max_branches=max_branches, + ) + + def ground_raw_signal(self, signal: RawSignal) -> GroundedSignal: + """Derive unsupervised features/events from a raw numeric stream.""" + return RawSignalGrounder().ground(signal) + + def forge_tool( + self, + *, + action: ConstructedAction, + primitives: Iterable[ActionPrimitive], + validation_cases: Iterable[ToolValidationCase], + ) -> ForgedTool: + """Promote a constructed procedure into a reusable tool after held-out tests.""" + return ToolForge().forge( + action=action, + primitives=primitives, + validation_cases=validation_cases, + ) + + def pursue_long_horizon( + self, + *, + initial_state: int, + goal_test: Callable[[int], bool], + actions: Iterable[HorizonAction], + max_steps: int = 32, + max_plan_depth: int = 12, + ) -> LongHorizonResult: + """Pursue a multi-stage objective and replan when observed reality disagrees.""" + return LongHorizonController().pursue( + initial_state=initial_state, + goal_test=goal_test, + actions=actions, + max_steps=max_steps, + max_plan_depth=max_plan_depth, + ) + + def detect_unknown_unknowns( + self, residuals: Iterable[PredictionResidual] + ) -> UnknownUnknownSignal: + """Detect clustered high-confidence failures suggesting missing concepts.""" + return UnknownUnknownDetector().detect(residuals) + + def allocate_search_budget( + self, + trials: Iterable[SearchOperatorTrial], + *, + total_budget: int, + ) -> SearchBudgetAllocation: + """Learn which search operators deserve finite compute.""" + return AdaptiveSearchPolicy().allocate(trials, total_budget=total_budget) + + def meta_learn_strategy( + self, + trials: Iterable[LearningStrategyTrial], + *, + task_family: str, + default_strategy_id: str, + ) -> MetaLearningDecision: + """Use prior learning outcomes to change the strategy used on later tasks.""" + return MetaLearningController().select( + trials, + task_family=task_family, + default_strategy_id=default_strategy_id, + ) + + def diagnose_self( + self, observations: Iterable[FailureObservation] + ) -> tuple[SelfDiagnosticReport, ...]: + """Measure blind spots where confidence exceeds actual performance.""" + return SelfDiagnostic().diagnose(observations) + + def propose_measured_self_improvement( + self, + *, + target_capability: str, + description: str, + benchmarks: Iterable[ImprovementBenchmark], + ) -> SelfImprovementResult: + """Propose a benchmarked internal improvement without self-authorizing adoption.""" + return SelfImprovementLab().propose( + self_model=self.self_model, + target_capability=target_capability, + description=description, + benchmarks=benchmarks, + ) + + def resolve_goal_conflict( + self, + goals: Iterable[GoalSpec], + *, + evidence: Iterable[GoalEvidence], + ) -> GoalResolution: + """Resolve conflicting goals from current evidence rather than static priority alone.""" + return GoalArbiter().resolve(goals, evidence=evidence) + + def revise_goals_from_evidence(self, evidence: Iterable[GoalEvidence]) -> None: + """Abandon self-generated goals whose premises or utility collapse.""" + self.goals = GoalRevisionEngine().revise(self.goals, evidence=evidence) + + def learn_structural_rule( + self, + *, + examples: Iterable[tuple[object, object]], + adapter: DomainAdapter, + ) -> AbstractTransitionRule: + """Learn a domain-neutral structural relation for later cross-domain transfer.""" + return StructuralAnalogyEngine().learn_affine(examples=examples, adapter=adapter) + + def run_recursive_bootstrap( + self, + *, + representation_training: Iterable[RepresentationObservation], + representation_holdout: Iterable[RepresentationObservation], + initial_state: int, + primitives: Iterable[ActionPrimitive], + known_states: Iterable[int], + tool_validation_cases: Iterable[ToolValidationCase], + residuals: Iterable[PredictionResidual] = (), + second_representation_training: Iterable[RepresentationObservation] = (), + second_representation_holdout: Iterable[RepresentationObservation] = (), + max_goal_depth: int = 4, + ) -> RecursiveBootstrapReport: + """Run a closed discover→goal→act→tool→rediscover cognitive bootstrapping cycle.""" + report = RecursiveCognitionEngine().bootstrap( + representation_training=representation_training, + representation_holdout=representation_holdout, + initial_state=initial_state, + primitives=primitives, + known_states=known_states, + tool_validation_cases=tool_validation_cases, + residuals=residuals, + second_representation_training=second_representation_training, + second_representation_holdout=second_representation_holdout, + max_goal_depth=max_goal_depth, + ) + self.lifelong_knowledge = report.knowledge_store + return report + + def blind_external_evaluation( + self, + *, + challenges: Iterable[BlindChallenge], + agent: Callable[[tuple[int, ...]], int], + commitments: Iterable[DigestRecord] | None = None, + ) -> BlindEvaluationResult: + """Run nonce-bound blind challenges supplied by an evaluator.""" + return BlindEvaluatorHarness().evaluate( + challenges=challenges, agent=agent, commitments=commitments + ) + + def invent_compositional_representation( + self, + *, + observations: Iterable[RepresentationObservation], + max_depth: int = 2, + max_candidates: int = 4096, + minimum_improvement: float = 0.15, + ) -> InventedRepresentationProgram: + """Synthesize a multi-operation representation when shallow feature languages fail.""" + return RepresentationProgramInventor().invent( + observations=observations, + max_depth=max_depth, + max_candidates=max_candidates, + minimum_improvement=minimum_improvement, + ) + + def validate_compositional_representation( + self, + representation: InventedRepresentationProgram, + *, + observations: Iterable[RepresentationObservation], + ) -> InventedRepresentationProgram: + """Reality-test a synthesized representation on held-out evidence.""" + return RepresentationProgramInventor().validate(representation, observations=observations) + + def maintain_lifelong_knowledge( + self, + *, + evidence: Iterable[ContextualKnowledgeEvidence], + ) -> KnowledgeMaintenanceReport: + """Split over-broad concepts or retire contradicted low-value knowledge.""" + report = KnowledgeMaintenanceEngine().reconcile(self.lifelong_knowledge, evidence=evidence) + self.lifelong_knowledge = report.store + return report + + def meta_choose_strategy( + self, + *, + fingerprint: TaskFingerprint, + candidate_strategies: Iterable[str], + ) -> OnlineMetaDecision: + """Choose a learning strategy using persistent cross-episode evidence.""" + return self.online_meta_profile.choose( + fingerprint=fingerprint, + candidate_strategies=candidate_strategies, + ) + + def run_lifetime_learning( + self, + *, + challenges: Iterable[LifetimeChallenge], + exploration_episodes: int = 2, + ) -> LifetimeLearningReport: + """Learn across multiple worlds and retain evidence about how Sally learns best.""" + report = LifetimeLearningEngine().run_lifetime( + profile=self.online_meta_profile, + challenges=challenges, + exploration_episodes=exploration_episodes, + ) + self.online_meta_profile = report.profile + for episode in report.episodes: + concept_id = f"lifetime-{episode.challenge_id}-{episode.concept_digest.value[:12]}" + self.lifelong_knowledge = self.lifelong_knowledge.integrate( + KnowledgeItem( + concept_id=concept_id, + content_digest=episode.concept_digest, + confidence=episode.validation_accuracy, + utility=episode.effective_score, + ) + ) + return report + + def learn_relational_schema( + self, + *, + world: RelationalWorld, + effective_node: str, + ) -> LearnedStructuralSchema: + """Learn a surface-independent causal/topological role from one domain.""" + return RelationalTransferEngine().learn(world=world, effective_node=effective_node) + + def transfer_relational_schema( + self, + schema: LearnedStructuralSchema, + *, + world: RelationalWorld, + ) -> TransferInference: + """Reuse a learned role in a surface-different domain by structure alone.""" + return RelationalTransferEngine().transfer(schema, world=world) + + def ground_text_outcomes( + self, + observations: Iterable[TextOutcomeObservation], + ) -> tuple[GroundedTextFeature, ...]: + """Discover outcome-linked latent features directly from raw text strings.""" + return TextOutcomeGrounder().discover(observations) + + def allocate_goal_portfolio( + self, + *, + goals: Iterable[GoalSpec], + evidence: Iterable[GoalEvidence], + attention_budget: float = 1.0, + per_goal_cost: dict[str, float] | None = None, + ) -> GoalPortfolioDecision: + """Maintain coherent attention across multiple evolving goals.""" + return GoalPortfolioManager().allocate( + goals, + evidence=evidence, + attention_budget=attention_budget, + per_goal_cost=per_goal_cost, + ) + def register_action(self, action: ActionSpec) -> None: """Add one unique declarative planning action.""" if any(existing.action_id == action.action_id for existing in self.action_catalog): @@ -303,6 +934,8 @@ def state_payload(self) -> JsonObject: "episodes": self.episodes.to_payload(), "curriculum": (self.curriculum.to_payload() if self.curriculum is not None else None), "primitive_registry": self.primitive_registry.to_payload(), + "lifelong_knowledge": self.lifelong_knowledge.to_payload(), + "online_meta_profile": self.online_meta_profile.to_payload(), "runtime_memories": runtime_memories, "execution_count": self.execution_count, "cycle_count": self.cycle_count, diff --git a/src/ix_sally/cognition/text_grounding.py b/src/ix_sally/cognition/text_grounding.py new file mode 100644 index 0000000..9138957 --- /dev/null +++ b/src/ix_sally/cognition/text_grounding.py @@ -0,0 +1,80 @@ +"""Minimal raw-text grounding by outcome-linked token discovery. + +This is intentionally not an LLM or general language understanding system. It provides a +real grounding step from unstructured text strings to empirically useful latent token +features, complementing IX-Sally's numeric signal grounding. +""" + +from __future__ import annotations + +import re +from collections.abc import Iterable +from dataclasses import dataclass +from math import log + +from ix_sally.foundation import FoundationError, require_text + +_TOKEN = re.compile(r"[A-Za-z0-9_'-]+") + + +@dataclass(frozen=True, slots=True) +class TextOutcomeObservation: + observation_id: str + text: str + outcome: bool + + def __post_init__(self) -> None: + require_text(self.observation_id, field_name="observation_id") + require_text(self.text, field_name="text") + + +@dataclass(frozen=True, slots=True) +class GroundedTextFeature: + token: str + information_gain: float + positive_rate_when_present: float + support: int + + +class TextOutcomeGrounder: + """Discover text tokens whose presence reduces uncertainty about an observed outcome.""" + + def discover( + self, observations: Iterable[TextOutcomeObservation] + ) -> tuple[GroundedTextFeature, ...]: + items = tuple(observations) + if len(items) < 4: + raise FoundationError("text grounding requires at least four observations") + base_positive = sum(item.outcome for item in items) / len(items) + base_entropy = self._entropy(base_positive) + vocabulary = sorted({token for item in items for token in self._tokens(item.text)}) + features: list[GroundedTextFeature] = [] + for token in vocabulary: + present = [item for item in items if token in self._tokens(item.text)] + absent = [item for item in items if token not in self._tokens(item.text)] + if len(present) < 2 or not absent: + continue + p_present = len(present) / len(items) + pos_present = sum(item.outcome for item in present) / len(present) + pos_absent = sum(item.outcome for item in absent) / len(absent) + conditional = p_present * self._entropy(pos_present) + (1 - p_present) * self._entropy( + pos_absent + ) + gain = max(0.0, base_entropy - conditional) + if gain > 1e-9: + features.append( + GroundedTextFeature(token, round(gain, 12), pos_present, len(present)) + ) + return tuple( + sorted(features, key=lambda item: (-item.information_gain, -item.support, item.token)) + ) + + @staticmethod + def _tokens(text: str) -> frozenset[str]: + return frozenset(match.group(0).lower() for match in _TOKEN.finditer(text)) + + @staticmethod + def _entropy(probability: float) -> float: + if probability <= 0.0 or probability >= 1.0: + return 0.0 + return -probability * log(probability, 2) - (1.0 - probability) * log(1.0 - probability, 2) diff --git a/src/ix_sally/cognition/tool_forge.py b/src/ix_sally/cognition/tool_forge.py new file mode 100644 index 0000000..0439028 --- /dev/null +++ b/src/ix_sally/cognition/tool_forge.py @@ -0,0 +1,110 @@ +"""Validation-bound creation of reusable tools from constructed procedures.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.cognition.open_choice import ActionPrimitive, ConstructedAction +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class ToolValidationCase: + initial_state: int + expected_state: int + + +@dataclass(frozen=True, slots=True) +class ForgedTool: + """A validated learned procedure exposed as one reusable capability.""" + + tool_id: str + primitive_ids: tuple[str, ...] + validation_cases: tuple[ToolValidationCase, ...] + validation_accuracy: float + source_action_id: str + + def __post_init__(self) -> None: + require_text(self.tool_id, field_name="tool_id") + if not self.primitive_ids: + raise FoundationError("forged tool requires a learned procedure") + if not self.validation_cases: + raise FoundationError("forged tool requires validation cases") + if not 0.0 <= self.validation_accuracy <= 1.0: + raise FoundationError("tool validation accuracy must be between zero and one") + + def apply(self, state: int, primitives: Iterable[ActionPrimitive]) -> int: + primitive_map = {item.primitive_id: item for item in primitives} + current = state + for primitive_id in self.primitive_ids: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError(f"forged tool references unknown primitive: {primitive_id}") + current = primitive.apply(current) + return current + + def as_action_primitive(self, primitives: Iterable[ActionPrimitive]) -> ActionPrimitive: + if self.validation_accuracy != 1.0: + raise FoundationError("only fully validated tools may be promoted") + base = tuple(primitives) + return ActionPrimitive(self.tool_id, lambda state: self.apply(state, base), cost=1.0) + + def to_payload(self) -> JsonObject: + cases: JsonArray = [ + {"initial_state": item.initial_state, "expected_state": item.expected_state} + for item in self.validation_cases + ] + return { + "tool_id": self.tool_id, + "primitive_ids": list(self.primitive_ids), + "validation_cases": cases, + "validation_accuracy": self.validation_accuracy, + "source_action_id": self.source_action_id, + "origin": "sally-tool-forge", + } + + +class ToolForge: + """Promote a discovered action into a reusable tool only after independent tests.""" + + def forge( + self, + *, + action: ConstructedAction, + primitives: Iterable[ActionPrimitive], + validation_cases: Iterable[ToolValidationCase], + ) -> ForgedTool: + base = tuple(primitives) + cases = tuple(validation_cases) + if not cases: + raise FoundationError("tool forge requires held-out validation cases") + primitive_map = {item.primitive_id: item for item in base} + correct = 0 + for case in cases: + state = case.initial_state + for primitive_id in action.primitive_ids: + primitive = primitive_map.get(primitive_id) + if primitive is None: + raise FoundationError(f"unknown primitive while forging tool: {primitive_id}") + state = primitive.apply(state) + if state == case.expected_state: + correct += 1 + accuracy = correct / len(cases) + identity = DigestRecord.from_payload( + { + "action": action.action_id, + "cases": [ + {"initial": item.initial_state, "expected": item.expected_state} + for item in cases + ], + } + ) + return ForgedTool( + tool_id=f"tool-{identity.value[:16]}", + primitive_ids=action.primitive_ids, + validation_cases=cases, + validation_accuracy=accuracy, + source_action_id=action.action_id, + ) diff --git a/src/ix_sally/cognition/unknowns.py b/src/ix_sally/cognition/unknowns.py new file mode 100644 index 0000000..8e8b7f3 --- /dev/null +++ b/src/ix_sally/cognition/unknowns.py @@ -0,0 +1,70 @@ +"""Unknown-unknown detection from structured high-confidence prediction residuals.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class PredictionResidual: + prediction_id: str + confidence: float + predicted: bool + actual: bool + context_signature: tuple[int, ...] + + def __post_init__(self) -> None: + require_text(self.prediction_id, field_name="prediction_id") + if not 0.0 <= self.confidence <= 1.0: + raise FoundationError("residual confidence must be between zero and one") + if not self.context_signature: + raise FoundationError("residual requires a context signature") + + +@dataclass(frozen=True, slots=True) +class UnknownUnknownSignal: + detected: bool + high_confidence_error_rate: float + dominant_context: tuple[int, ...] | None + dominant_context_errors: int + reason: str + + +class UnknownUnknownDetector: + """Flag systematic failures that ordinary uncertainty estimates did not anticipate.""" + + def detect( + self, + residuals: Iterable[PredictionResidual], + *, + confidence_threshold: float = 0.80, + error_rate_threshold: float = 0.30, + cluster_minimum: int = 2, + ) -> UnknownUnknownSignal: + items = tuple(residuals) + if not items: + raise FoundationError("unknown-unknown detection requires residuals") + high = [item for item in items if item.confidence >= confidence_threshold] + errors = [item for item in high if item.predicted != item.actual] + error_rate = len(errors) / len(high) if high else 0.0 + counts: dict[tuple[int, ...], int] = {} + for item in errors: + counts[item.context_signature] = counts.get(item.context_signature, 0) + 1 + dominant = max(counts, key=lambda key: (counts[key], key)) if counts else None + count = counts.get(dominant, 0) if dominant is not None else 0 + detected = error_rate >= error_rate_threshold and count >= cluster_minimum + return UnknownUnknownSignal( + detected=detected, + high_confidence_error_rate=round(error_rate, 12), + dominant_context=dominant, + dominant_context_errors=count, + reason=( + "High-confidence errors cluster in a repeated context, suggesting a " + "missing variable or hypothesis." + if detected + else "Residuals do not yet justify inventing an unrepresented cause." + ), + ) diff --git a/src/ix_sally/cuc1/__init__.py b/src/ix_sally/cuc1/__init__.py new file mode 100644 index 0000000..047c1d4 --- /dev/null +++ b/src/ix_sally/cuc1/__init__.py @@ -0,0 +1,28 @@ +"""Choice Under Consequence research experiment.""" + +from ix_sally.cuc1.agent import ChoiceUnderConsequenceAgent +from ix_sally.cuc1.contracts import ( + CausalHypothesis, + ChoiceCandidate, + ChoiceReceipt, + Consequence, + Direction, + LearnedSkill, + PublicObservation, +) +from ix_sally.cuc1.environment import IndependentCausalEnvironment +from ix_sally.cuc1.experiment import CUC1Report, run_cuc1_experiment + +__all__ = [ + "CUC1Report", + "CausalHypothesis", + "ChoiceCandidate", + "ChoiceReceipt", + "ChoiceUnderConsequenceAgent", + "Consequence", + "Direction", + "IndependentCausalEnvironment", + "LearnedSkill", + "PublicObservation", + "run_cuc1_experiment", +] diff --git a/src/ix_sally/cuc1/agent.py b/src/ix_sally/cuc1/agent.py new file mode 100644 index 0000000..37dd39b --- /dev/null +++ b/src/ix_sally/cuc1/agent.py @@ -0,0 +1,286 @@ +"""Bayesian active-choice learner for the CUC-1 causal environment.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field, replace + +from ix_sally.cuc1.contracts import ( + CausalHypothesis, + ChoiceCandidate, + ChoiceReceipt, + Consequence, + Direction, + LearnedSkill, + PublicObservation, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _entropy(probabilities: tuple[float, ...]) -> float: + """Return normalized base-two entropy for a categorical distribution.""" + active = tuple(value for value in probabilities if value > 0.0) + if len(probabilities) <= 1: + return 0.0 + raw = -sum(value * math.log2(value) for value in active) + return raw / math.log2(len(probabilities)) + + +@dataclass(slots=True) +class ChoiceUnderConsequenceAgent: + """Learn hidden cue transformations through prediction and consequence.""" + + information_gain_weight: float = 0.65 + novelty_weight: float = 0.1 + skill_threshold: float = 0.88 + success_likelihood: float = 0.97 + failure_likelihood: float = 0.03 + hypotheses_by_family: dict[str, tuple[CausalHypothesis, ...]] = field(default_factory=dict) + action_counts: dict[Direction, int] = field(default_factory=dict) + consequence_digests: dict[str, tuple[DigestRecord, ...]] = field(default_factory=dict) + skills: dict[str, LearnedSkill] = field(default_factory=dict) + choice_counter: int = 0 + + def __post_init__(self) -> None: + """Validate learner thresholds and likelihoods.""" + for name, value in { + "information_gain_weight": self.information_gain_weight, + "novelty_weight": self.novelty_weight, + "skill_threshold": self.skill_threshold, + "success_likelihood": self.success_likelihood, + "failure_likelihood": self.failure_likelihood, + }.items(): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"agent {name} must be bounded") + if self.success_likelihood <= self.failure_likelihood: + raise FoundationError("success likelihood must exceed failure likelihood") + + def hypotheses(self, family_id: str) -> tuple[CausalHypothesis, ...]: + """Return or initialize the competing hypotheses for one family.""" + hypotheses = self.hypotheses_by_family.get(family_id) + if hypotheses is None: + probability = 1.0 / len(Direction) + hypotheses = tuple( + CausalHypothesis( + hypothesis_id=f"{family_id}-rotation-{turns}", + quarter_turns=turns, + probability=probability, + ) + for turns in range(len(Direction)) + ) + self.hypotheses_by_family[family_id] = hypotheses + return hypotheses + + def choose(self, observation: PublicObservation) -> ChoiceReceipt: + """Choose an action using expected success, information, and novelty.""" + hypotheses = self.hypotheses(observation.family_id) + probabilities = tuple(item.probability for item in hypotheses) + prior_entropy = _entropy(probabilities) + skill = self.skills.get(observation.family_id) + candidates = tuple( + self._score_candidate( + observation=observation, + action=action, + hypotheses=hypotheses, + prior_entropy=prior_entropy, + ) + for action in observation.available_actions + ) + if skill is not None and skill.confidence >= self.skill_threshold: + selected = skill.apply(observation.cue) + used_skill_id = skill.skill_id + else: + selected = max( + candidates, + key=lambda candidate: (candidate.score, -int(candidate.action)), + ).action + used_skill_id = None + self.choice_counter += 1 + return ChoiceReceipt( + choice_id=f"choice-{self.choice_counter}", + observation_digest=observation.evidence_digest, + candidates=candidates, + selected_action=selected, + prior_entropy=round(prior_entropy, 12), + used_skill_id=used_skill_id, + ) + + def learn( + self, + *, + observation: PublicObservation, + choice: ChoiceReceipt, + consequence: Consequence, + ) -> None: + """Update causal beliefs from evaluator-owned consequence evidence.""" + if choice.observation_digest != observation.evidence_digest: + raise FoundationError("choice does not reference the supplied observation") + if consequence.observation_digest != observation.evidence_digest: + raise FoundationError("consequence does not reference the supplied observation") + if consequence.selected_action is not choice.selected_action: + raise FoundationError("consequence action differs from selected action") + hypotheses = self.hypotheses(observation.family_id) + unnormalized = [] + for hypothesis in hypotheses: + predicted_success = hypothesis.predicts(observation.cue) is choice.selected_action + consistent = predicted_success is consequence.succeeded + likelihood = self.success_likelihood if consistent else self.failure_likelihood + unnormalized.append(hypothesis.probability * likelihood) + total = sum(unnormalized) + if total <= 0.0: + raise FoundationError("hypothesis update produced zero probability mass") + updated = tuple( + replace(hypothesis, probability=round(weight / total, 12)) + for hypothesis, weight in zip(hypotheses, unnormalized, strict=True) + ) + normalization = sum(item.probability for item in updated) + if normalization != 1.0: + correction = 1.0 - normalization + best_index = max(range(len(updated)), key=lambda index: updated[index].probability) + updated_list = list(updated) + updated_list[best_index] = replace( + updated_list[best_index], + probability=round(updated_list[best_index].probability + correction, 12), + ) + updated = tuple(updated_list) + self.hypotheses_by_family[observation.family_id] = updated + previous_count = self.action_counts.get(choice.selected_action, 0) + self.action_counts[choice.selected_action] = previous_count + 1 + current_evidence = self.consequence_digests.get(observation.family_id, ()) + self.consequence_digests[observation.family_id] = ( + *current_evidence, + consequence.evaluator_digest, + ) + self._promote_skill(observation.family_id) + self._validate_used_skill( + family_id=observation.family_id, + choice=choice, + consequence=consequence, + ) + + def _score_candidate( + self, + *, + observation: PublicObservation, + action: Direction, + hypotheses: tuple[CausalHypothesis, ...], + prior_entropy: float, + ) -> ChoiceCandidate: + """Build a transparent multi-objective candidate score.""" + expected_success = sum( + hypothesis.probability + for hypothesis in hypotheses + if hypothesis.predicts(observation.cue) is action + ) + information_gain = self._expected_information_gain( + cue=observation.cue, + action=action, + hypotheses=hypotheses, + prior_entropy=prior_entropy, + ) + count = self.action_counts.get(action, 0) + novelty = 1.0 / (1.0 + count) + reversibility = 1.0 + cost = 0.05 + risk = 0.0 + score = ( + expected_success + + self.information_gain_weight * information_gain + + self.novelty_weight * novelty + + 0.05 * reversibility + - 0.05 * cost + - risk + ) + return ChoiceCandidate( + action=action, + expected_success=round(expected_success, 12), + expected_information_gain=round(information_gain, 12), + novelty=round(novelty, 12), + reversibility=reversibility, + cost=cost, + risk=risk, + score=round(score, 12), + ) + + def _expected_information_gain( + self, + *, + cue: Direction, + action: Direction, + hypotheses: tuple[CausalHypothesis, ...], + prior_entropy: float, + ) -> float: + """Calculate expected entropy reduction for success and failure outcomes.""" + success_probability = sum( + hypothesis.probability + for hypothesis in hypotheses + if hypothesis.predicts(cue) is action + ) + expected_entropy = 0.0 + for observed_success, outcome_probability in ( + (True, success_probability), + (False, 1.0 - success_probability), + ): + if outcome_probability <= 0.0: + continue + weights = [] + for hypothesis in hypotheses: + predicted_success = hypothesis.predicts(cue) is action + consistent = predicted_success is observed_success + likelihood = self.success_likelihood if consistent else self.failure_likelihood + weights.append(hypothesis.probability * likelihood) + total = sum(weights) + posterior = tuple(weight / total for weight in weights) + expected_entropy += outcome_probability * _entropy(posterior) + return max(0.0, prior_entropy - expected_entropy) + + def _promote_skill(self, family_id: str) -> None: + """Compile a sufficiently supported causal hypothesis into executable skill.""" + hypotheses = self.hypotheses(family_id) + best = max(hypotheses, key=lambda item: item.probability) + evidence = self.consequence_digests.get(family_id, ()) + if best.probability < self.skill_threshold or len(evidence) < 2: + return + existing = self.skills.get(family_id) + self.skills[family_id] = LearnedSkill( + skill_id=f"skill-{family_id}-rotation-{best.quarter_turns}", + family_id=family_id, + quarter_turns=best.quarter_turns, + confidence=best.probability, + source_consequence_digests=evidence, + validation_uses=existing.validation_uses if existing else 0, + validation_successes=existing.validation_successes if existing else 0, + ) + + def _validate_used_skill( + self, + *, + family_id: str, + choice: ChoiceReceipt, + consequence: Consequence, + ) -> None: + """Update validation counters only when an existing skill selected the action.""" + skill = self.skills.get(family_id) + if skill is None or choice.used_skill_id != skill.skill_id: + return + self.skills[family_id] = replace( + skill, + validation_uses=skill.validation_uses + 1, + validation_successes=skill.validation_successes + int(consequence.succeeded), + ) + + def state_digest(self) -> DigestRecord: + """Return a content digest for causal beliefs and executable skills.""" + return DigestRecord.from_payload( + { + "hypotheses": { + family: [item.to_payload() for item in hypotheses] + for family, hypotheses in sorted(self.hypotheses_by_family.items()) + }, + "skills": { + family: skill.to_payload() for family, skill in sorted(self.skills.items()) + }, + "choice_counter": self.choice_counter, + } + ) diff --git a/src/ix_sally/cuc1/contracts.py b/src/ix_sally/cuc1/contracts.py new file mode 100644 index 0000000..29ca97d --- /dev/null +++ b/src/ix_sally/cuc1/contracts.py @@ -0,0 +1,263 @@ +"""Typed contracts for the Choice Under Consequence experiment.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import IntEnum, StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +def digest_payload(digest: DigestRecord) -> JsonObject: + """Return the canonical payload form of a digest record.""" + return {"algorithm": digest.algorithm, "value": digest.value} + + +class Direction(IntEnum): + """Four actions and visible cue directions in clockwise order.""" + + NORTH = 0 + EAST = 1 + SOUTH = 2 + WEST = 3 + + def rotated(self, quarter_turns: int) -> Direction: + """Return this direction rotated clockwise.""" + return Direction((int(self) + quarter_turns) % len(Direction)) + + +class OutcomeStatus(StrEnum): + """Evaluator-owned result of one intervention.""" + + SUCCESS = "success" + FAILURE = "failure" + + +@dataclass(frozen=True, slots=True) +class PublicObservation: + """Agent-visible environment state with no answer or hidden-rule field.""" + + observation_id: str + family_id: str + cue: Direction + context: str + available_actions: tuple[Direction, ...] + evidence_digest: DigestRecord + + def __post_init__(self) -> None: + """Validate public state and prevent an empty action surface.""" + require_text(self.observation_id, field_name="observation_id") + require_text(self.family_id, field_name="family_id") + require_text(self.context, field_name="context") + if not self.available_actions: + raise FoundationError("observation requires available actions") + if len(set(self.available_actions)) != len(self.available_actions): + raise FoundationError("observation actions must be unique") + self.evidence_digest.require_algorithm("sha256") + + def to_payload(self) -> JsonObject: + """Return the complete agent-visible payload.""" + actions: JsonArray = [action.name.lower() for action in self.available_actions] + return { + "observation_id": self.observation_id, + "family_id": self.family_id, + "cue": self.cue.name.lower(), + "context": self.context, + "available_actions": actions, + "evidence_digest": digest_payload(self.evidence_digest), + } + + +@dataclass(frozen=True, slots=True) +class Consequence: + """Measured result emitted only after an agent intervention.""" + + consequence_id: str + observation_digest: DigestRecord + selected_action: Direction + status: OutcomeStatus + reward: float + terminal: bool + evaluator_digest: DigestRecord + + def __post_init__(self) -> None: + """Validate evaluator evidence and bounded reward.""" + require_text(self.consequence_id, field_name="consequence_id") + self.observation_digest.require_algorithm("sha256") + self.evaluator_digest.require_algorithm("sha256") + if not -1.0 <= self.reward <= 1.0: + raise FoundationError("consequence reward must be between -1 and 1") + + @property + def succeeded(self) -> bool: + """Return whether the intervention achieved the hidden transition.""" + return self.status is OutcomeStatus.SUCCESS + + def to_payload(self) -> JsonObject: + """Return a canonical measured-consequence payload.""" + return { + "consequence_id": self.consequence_id, + "observation_digest": digest_payload(self.observation_digest), + "selected_action": self.selected_action.name.lower(), + "status": self.status.value, + "reward": self.reward, + "terminal": self.terminal, + "evaluator_digest": digest_payload(self.evaluator_digest), + } + + +@dataclass(frozen=True, slots=True) +class CausalHypothesis: + """One competing hypothesis that maps cues to actions.""" + + hypothesis_id: str + quarter_turns: int + probability: float + + def __post_init__(self) -> None: + """Validate hypothesis identity, transform, and probability.""" + require_text(self.hypothesis_id, field_name="hypothesis_id") + if self.quarter_turns not in range(len(Direction)): + raise FoundationError("hypothesis quarter_turns must be in range 0..3") + if not 0.0 <= self.probability <= 1.0 or not math.isfinite(self.probability): + raise FoundationError("hypothesis probability must be finite and bounded") + + def predicts(self, cue: Direction) -> Direction: + """Return the action predicted by this causal hypothesis.""" + return cue.rotated(self.quarter_turns) + + def to_payload(self) -> JsonObject: + """Return a canonical hypothesis payload.""" + return { + "hypothesis_id": self.hypothesis_id, + "quarter_turns": self.quarter_turns, + "probability": self.probability, + } + + +@dataclass(frozen=True, slots=True) +class ChoiceCandidate: + """One action with a transparent multi-objective score vector.""" + + action: Direction + expected_success: float + expected_information_gain: float + novelty: float + reversibility: float + cost: float + risk: float + score: float + + def __post_init__(self) -> None: + """Require finite bounded score components.""" + for name, value in { + "expected_success": self.expected_success, + "expected_information_gain": self.expected_information_gain, + "novelty": self.novelty, + "reversibility": self.reversibility, + "cost": self.cost, + "risk": self.risk, + }.items(): + if not 0.0 <= value <= 1.0 or not math.isfinite(value): + raise FoundationError(f"choice {name} must be finite and bounded") + if not math.isfinite(self.score): + raise FoundationError("choice score must be finite") + + def to_payload(self) -> JsonObject: + """Return the complete Pareto-relevant score vector.""" + return { + "action": self.action.name.lower(), + "expected_success": self.expected_success, + "expected_information_gain": self.expected_information_gain, + "novelty": self.novelty, + "reversibility": self.reversibility, + "cost": self.cost, + "risk": self.risk, + "score": self.score, + } + + +@dataclass(frozen=True, slots=True) +class ChoiceReceipt: + """Auditable record of alternatives, predictions, and selected intervention.""" + + choice_id: str + observation_digest: DigestRecord + candidates: tuple[ChoiceCandidate, ...] + selected_action: Direction + prior_entropy: float + used_skill_id: str | None + + def __post_init__(self) -> None: + """Require complete alternatives and a selected member.""" + require_text(self.choice_id, field_name="choice_id") + self.observation_digest.require_algorithm("sha256") + if not self.candidates: + raise FoundationError("choice receipt requires candidates") + if self.selected_action not in {candidate.action for candidate in self.candidates}: + raise FoundationError("selected action must appear in choice candidates") + if self.prior_entropy < 0.0 or not math.isfinite(self.prior_entropy): + raise FoundationError("choice entropy must be finite and non-negative") + + def to_payload(self) -> JsonObject: + """Return a canonical choice receipt.""" + candidates: JsonArray = [candidate.to_payload() for candidate in self.candidates] + return { + "choice_id": self.choice_id, + "observation_digest": digest_payload(self.observation_digest), + "candidates": candidates, + "selected_action": self.selected_action.name.lower(), + "prior_entropy": self.prior_entropy, + "used_skill_id": self.used_skill_id, + } + + def digest(self) -> DigestRecord: + """Return the content-addressed choice identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class LearnedSkill: + """Executable generalized cue transformation learned from consequences.""" + + skill_id: str + family_id: str + quarter_turns: int + confidence: float + source_consequence_digests: tuple[DigestRecord, ...] + validation_uses: int = 0 + validation_successes: int = 0 + + def __post_init__(self) -> None: + """Validate executable skill and its measured provenance.""" + require_text(self.skill_id, field_name="skill_id") + require_text(self.family_id, field_name="family_id") + if self.quarter_turns not in range(len(Direction)): + raise FoundationError("skill quarter_turns must be in range 0..3") + if not 0.0 <= self.confidence <= 1.0: + raise FoundationError("skill confidence must be bounded") + if not self.source_consequence_digests: + raise FoundationError("learned skill requires measured consequences") + if self.validation_uses < 0 or not 0 <= self.validation_successes <= self.validation_uses: + raise FoundationError("invalid skill validation counters") + for digest in self.source_consequence_digests: + digest.require_algorithm("sha256") + + def apply(self, cue: Direction) -> Direction: + """Execute the learned transformation on a new cue.""" + return cue.rotated(self.quarter_turns) + + def to_payload(self) -> JsonObject: + """Return a canonical executable-skill payload.""" + evidence: JsonArray = [digest_payload(digest) for digest in self.source_consequence_digests] + return { + "skill_id": self.skill_id, + "family_id": self.family_id, + "quarter_turns": self.quarter_turns, + "confidence": self.confidence, + "source_consequence_digests": evidence, + "validation_uses": self.validation_uses, + "validation_successes": self.validation_successes, + } diff --git a/src/ix_sally/cuc1/environment.py b/src/ix_sally/cuc1/environment.py new file mode 100644 index 0000000..4ecc636 --- /dev/null +++ b/src/ix_sally/cuc1/environment.py @@ -0,0 +1,122 @@ +"""Evaluator-owned hidden causal worlds for CUC-1.""" + +from __future__ import annotations + +import random +from dataclasses import dataclass, field + +from ix_sally.cuc1.contracts import ( + Consequence, + Direction, + OutcomeStatus, + PublicObservation, + digest_payload, +) +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError, require_text + + +@dataclass(slots=True) +class IndependentCausalEnvironment: + """A sealed evaluator whose transition rule is absent from observations.""" + + environment_id: str + family_id: str + seed: int + _quarter_turns: int | None = field(default=None, repr=False) + _episode_counter: int = field(default=0, init=False, repr=False) + _active_observations: dict[str, PublicObservation] = field( + default_factory=dict, + init=False, + repr=False, + ) + + def __post_init__(self) -> None: + """Create a deterministic evaluator-private causal rule.""" + require_text(self.environment_id, field_name="environment_id") + require_text(self.family_id, field_name="family_id") + if self._quarter_turns is None: + self._quarter_turns = random.Random(self.seed).randrange(len(Direction)) + if self._quarter_turns not in range(len(Direction)): + raise FoundationError("environment rule must be in range 0..3") + + def reset(self, *, cue: Direction, context: str) -> PublicObservation: + """Start one episode and expose only public state.""" + normalized_context = require_text(context, field_name="context") + self._episode_counter += 1 + observation_id = f"{self.environment_id}-observation-{self._episode_counter}" + evidence = DigestRecord.from_payload( + { + "environment_id": self.environment_id, + "observation_id": observation_id, + "family_id": self.family_id, + "cue": cue.name.lower(), + "context": normalized_context, + } + ) + observation = PublicObservation( + observation_id=observation_id, + family_id=self.family_id, + cue=cue, + context=normalized_context, + available_actions=tuple(Direction), + evidence_digest=evidence, + ) + self._active_observations[observation_id] = observation + return observation + + def intervene( + self, + *, + observation_id: str, + action: Direction, + ) -> Consequence: + """Apply one action and independently compute its consequence.""" + observation = self._active_observations.pop(observation_id, None) + if observation is None: + raise FoundationError("unknown or already consumed observation") + if action not in observation.available_actions: + raise FoundationError("action is outside the observation action surface") + assert self._quarter_turns is not None + correct_action = observation.cue.rotated(self._quarter_turns) + succeeded = action is correct_action + evaluator_payload: JsonObject = { + "environment_id": self.environment_id, + "family_id": self.family_id, + "observation_digest": digest_payload(observation.evidence_digest), + "selected_action": action.name.lower(), + "status": "success" if succeeded else "failure", + "rule_commitment": digest_payload(self.rule_commitment()), + } + return Consequence( + consequence_id=f"{observation_id}-consequence", + observation_digest=observation.evidence_digest, + selected_action=action, + status=OutcomeStatus.SUCCESS if succeeded else OutcomeStatus.FAILURE, + reward=1.0 if succeeded else -0.25, + terminal=True, + evaluator_digest=DigestRecord.from_payload(evaluator_payload), + ) + + def rule_commitment(self) -> DigestRecord: + """Commit to the hidden rule without exposing it to the agent.""" + return DigestRecord.from_payload( + { + "environment_id": self.environment_id, + "family_id": self.family_id, + "seed": self.seed, + "quarter_turns": self._quarter_turns, + } + ) + + def reveal_for_completed_evaluation(self) -> JsonObject: + """Reveal evaluator configuration only for post-run reproducibility.""" + if self._active_observations: + raise FoundationError("cannot reveal rule while observations remain active") + return { + "environment_id": self.environment_id, + "family_id": self.family_id, + "seed": self.seed, + "quarter_turns": self._quarter_turns, + "rule_commitment": digest_payload(self.rule_commitment()), + } diff --git a/src/ix_sally/cuc1/experiment.py b/src/ix_sally/cuc1/experiment.py new file mode 100644 index 0000000..8778a11 --- /dev/null +++ b/src/ix_sally/cuc1/experiment.py @@ -0,0 +1,324 @@ +"""End-to-end Choice Under Consequence experiment and evidence report.""" + +from __future__ import annotations + +import random +from dataclasses import dataclass + +from ix_sally.cuc1.agent import ChoiceUnderConsequenceAgent +from ix_sally.cuc1.contracts import ( + ChoiceReceipt, + Consequence, + Direction, + LearnedSkill, + PublicObservation, + digest_payload, +) +from ix_sally.cuc1.environment import IndependentCausalEnvironment +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError + + +@dataclass(frozen=True, slots=True) +class TrialRecord: + """One complete observation-choice-consequence learning event.""" + + observation: PublicObservation + choice: ChoiceReceipt + consequence: Consequence + state_before: DigestRecord + state_after: DigestRecord + + def __post_init__(self) -> None: + """Require state change evidence for a learning trial.""" + self.state_before.require_algorithm("sha256") + self.state_after.require_algorithm("sha256") + + @property + def changed_agent(self) -> bool: + """Return whether the consequence changed cognitive state.""" + return self.state_before != self.state_after + + def to_payload(self) -> JsonObject: + """Return a canonical trial payload.""" + return { + "observation": self.observation.to_payload(), + "choice": self.choice.to_payload(), + "consequence": self.consequence.to_payload(), + "state_before": digest_payload(self.state_before), + "state_after": digest_payload(self.state_after), + "changed_agent": self.changed_agent, + } + + +@dataclass(frozen=True, slots=True) +class CounterfactualBehaviorProof: + """Proof that measured experience changed a later held-out choice.""" + + observation_digest: DigestRecord + pre_learning_action: Direction + post_learning_action: Direction + post_learning_used_skill_id: str | None + post_learning_succeeded: bool + behavior_changed: bool + + def __post_init__(self) -> None: + """Validate content-addressed observation evidence.""" + self.observation_digest.require_algorithm("sha256") + if self.behavior_changed is not (self.pre_learning_action is not self.post_learning_action): + raise FoundationError("counterfactual behavior flag is inconsistent") + + def to_payload(self) -> JsonObject: + """Return a canonical counterfactual proof.""" + return { + "observation_digest": digest_payload(self.observation_digest), + "pre_learning_action": self.pre_learning_action.name.lower(), + "post_learning_action": self.post_learning_action.name.lower(), + "post_learning_used_skill_id": self.post_learning_used_skill_id, + "post_learning_succeeded": self.post_learning_succeeded, + "behavior_changed": self.behavior_changed, + } + + +@dataclass(frozen=True, slots=True) +class BaselineResult: + """Frozen baseline outcome on the same causal family.""" + + baseline_id: str + successes: int + trials: int + + @property + def success_rate(self) -> float: + """Return measured baseline accuracy.""" + return round(self.successes / self.trials, 12) if self.trials else 0.0 + + def to_payload(self) -> JsonObject: + """Return a canonical baseline payload.""" + return { + "baseline_id": self.baseline_id, + "successes": self.successes, + "trials": self.trials, + "success_rate": self.success_rate, + } + + +@dataclass(frozen=True, slots=True) +class CUC1Report: + """Falsifiable report for acquired and transferred causal competence.""" + + experiment_id: str + environment_commitment: DigestRecord + training_trials: tuple[TrialRecord, ...] + held_out_trial: TrialRecord + learned_skill: LearnedSkill | None + counterfactual_proof: CounterfactualBehaviorProof + baselines: tuple[BaselineResult, ...] + evaluator_reveal: JsonObject + leakage_checks_passed: bool + + def __post_init__(self) -> None: + """Validate report evidence and non-empty trial inventory.""" + self.environment_commitment.require_algorithm("sha256") + if not self.training_trials: + raise FoundationError("CUC-1 report requires training trials") + + @property + def training_success_rate(self) -> float: + """Return observed training success rate.""" + successes = sum(trial.consequence.succeeded for trial in self.training_trials) + return round(successes / len(self.training_trials), 12) + + @property + def transfer_succeeded(self) -> bool: + """Return whether executable learning transferred to the held-out cue.""" + return ( + self.held_out_trial.consequence.succeeded + and self.held_out_trial.choice.used_skill_id is not None + ) + + @property + def acquired_competence(self) -> bool: + """Return whether CUC-1's strict learning claim is supported.""" + return ( + self.leakage_checks_passed + and self.learned_skill is not None + and all(trial.changed_agent for trial in self.training_trials) + and self.counterfactual_proof.behavior_changed + and self.counterfactual_proof.post_learning_succeeded + and self.transfer_succeeded + ) + + @property + def classification(self) -> str: + """Return the bounded experiment classification.""" + return ( + "causal-skill-acquisition-observed" + if self.acquired_competence + else "causal-skill-acquisition-not-established" + ) + + def to_payload(self) -> JsonObject: + """Return a complete machine-readable evidence report.""" + training: JsonArray = [trial.to_payload() for trial in self.training_trials] + baselines: JsonArray = [baseline.to_payload() for baseline in self.baselines] + return { + "experiment_id": self.experiment_id, + "classification": self.classification, + "acquired_competence": self.acquired_competence, + "agi_certified": False, + "environment_commitment": digest_payload(self.environment_commitment), + "training_trials": training, + "training_success_rate": self.training_success_rate, + "held_out_trial": self.held_out_trial.to_payload(), + "transfer_succeeded": self.transfer_succeeded, + "learned_skill": self.learned_skill.to_payload() if self.learned_skill else None, + "counterfactual_proof": self.counterfactual_proof.to_payload(), + "baselines": baselines, + "evaluator_reveal": self.evaluator_reveal, + "leakage_checks_passed": self.leakage_checks_passed, + "limitations": [ + "CUC-1 is a bounded causal-learning experiment, not an AGI test.", + "The environment uses a four-hypothesis rotation family.", + "Transfer is within one causal family across held-out observations.", + "Independent external replication has not been performed.", + ], + } + + def digest(self) -> DigestRecord: + """Return a content address for the complete experiment.""" + return DigestRecord.from_payload(self.to_payload()) + + +def _run_learning_trial( + *, + agent: ChoiceUnderConsequenceAgent, + environment: IndependentCausalEnvironment, + cue: Direction, + context: str, +) -> TrialRecord: + """Run one complete closed-loop learning event.""" + observation = environment.reset(cue=cue, context=context) + state_before = agent.state_digest() + choice = agent.choose(observation) + consequence = environment.intervene( + observation_id=observation.observation_id, + action=choice.selected_action, + ) + agent.learn(observation=observation, choice=choice, consequence=consequence) + state_after = agent.state_digest() + return TrialRecord( + observation=observation, + choice=choice, + consequence=consequence, + state_before=state_before, + state_after=state_after, + ) + + +def _run_baselines(*, seed: int, quarter_turns: int) -> tuple[BaselineResult, ...]: + """Evaluate frozen random and fixed-action baselines on all directions.""" + cues = tuple(Direction) + random_source = random.Random(seed + 10_000) + random_successes = sum( + random_source.choice(tuple(Direction)) is cue.rotated(quarter_turns) for cue in cues + ) + fixed_successes = sum(Direction.NORTH is cue.rotated(quarter_turns) for cue in cues) + return ( + BaselineResult("uniform-random", random_successes, len(cues)), + BaselineResult("fixed-north", fixed_successes, len(cues)), + ) + + +def _public_payload_is_sealed(observation: PublicObservation) -> bool: + """Reject answer-bearing fields from the complete agent-visible payload.""" + forbidden = { + "answer", + "correct_action", + "expected_action", + "expected_operation", + "quarter_turns", + "reward", + "rule", + "target", + } + return forbidden.isdisjoint(observation.to_payload()) + + +def run_cuc1_experiment(*, seed: int = 7) -> CUC1Report: + """Run the deterministic first Choice Under Consequence experiment.""" + experiment_id = f"cuc1-seed-{seed}" + family_id = "rotation-causality-v1" + environment = IndependentCausalEnvironment( + environment_id=f"{experiment_id}-environment", + family_id=family_id, + seed=seed, + ) + agent = ChoiceUnderConsequenceAgent() + pre_learning_agent = ChoiceUnderConsequenceAgent() + + training_trials = tuple( + _run_learning_trial( + agent=agent, + environment=environment, + cue=cue, + context=f"training-context-{index}", + ) + for index, cue in enumerate( + (Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.NORTH), + start=1, + ) + ) + + held_out_observation = environment.reset( + cue=Direction.WEST, + context="held-out-transfer-context", + ) + pre_learning_choice = pre_learning_agent.choose(held_out_observation) + held_out_before = agent.state_digest() + held_out_choice = agent.choose(held_out_observation) + held_out_consequence = environment.intervene( + observation_id=held_out_observation.observation_id, + action=held_out_choice.selected_action, + ) + agent.learn( + observation=held_out_observation, + choice=held_out_choice, + consequence=held_out_consequence, + ) + held_out_trial = TrialRecord( + observation=held_out_observation, + choice=held_out_choice, + consequence=held_out_consequence, + state_before=held_out_before, + state_after=agent.state_digest(), + ) + skill = agent.skills.get(family_id) + reveal = environment.reveal_for_completed_evaluation() + quarter_turns = reveal["quarter_turns"] + if not isinstance(quarter_turns, int): + raise FoundationError("evaluator reveal has an invalid rule") + proof = CounterfactualBehaviorProof( + observation_digest=held_out_observation.evidence_digest, + pre_learning_action=pre_learning_choice.selected_action, + post_learning_action=held_out_choice.selected_action, + post_learning_used_skill_id=held_out_choice.used_skill_id, + post_learning_succeeded=held_out_consequence.succeeded, + behavior_changed=( + pre_learning_choice.selected_action is not held_out_choice.selected_action + ), + ) + leakage_checks_passed = all( + _public_payload_is_sealed(trial.observation) for trial in training_trials + ) and _public_payload_is_sealed(held_out_observation) + return CUC1Report( + experiment_id=experiment_id, + environment_commitment=environment.rule_commitment(), + training_trials=training_trials, + held_out_trial=held_out_trial, + learned_skill=skill, + counterfactual_proof=proof, + baselines=_run_baselines(seed=seed, quarter_turns=quarter_turns), + evaluator_reveal=reveal, + leakage_checks_passed=leakage_checks_passed, + ) diff --git a/src/ix_sally/cuc2/__init__.py b/src/ix_sally/cuc2/__init__.py new file mode 100644 index 0000000..55c98af --- /dev/null +++ b/src/ix_sally/cuc2/__init__.py @@ -0,0 +1,5 @@ +"""CUC-2: generative choice beyond a pre-enumerated action menu.""" + +from ix_sally.cuc2.experiment import CUC2Report, run_cuc2_experiment + +__all__ = ["CUC2Report", "run_cuc2_experiment"] diff --git a/src/ix_sally/cuc2/experiment.py b/src/ix_sally/cuc2/experiment.py new file mode 100644 index 0000000..55c0f74 --- /dev/null +++ b/src/ix_sally/cuc2/experiment.py @@ -0,0 +1,127 @@ +"""CUC-2 experiment: construct a successful fifth option from reusable primitives.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.cognition.open_choice import ( + ActionPrimitive, + ConstructedAction, + DeliberationPolicy, + DeliberationSignals, + OpenChoiceResult, + OpenChoiceSynthesizer, +) +from ix_sally.digest import JsonObject + + +def _primitives() -> tuple[ActionPrimitive, ...]: + return ( + ActionPrimitive("increment", lambda value: value + 1), + ActionPrimitive("decrement", lambda value: value - 1), + ActionPrimitive("double", lambda value: value * 2), + ActionPrimitive("negate", lambda value: -value), + ) + + +def _offered_actions(initial: int) -> tuple[ConstructedAction, ...]: + return tuple( + ConstructedAction( + primitive_ids=(primitive.primitive_id,), + result_state=primitive.apply(initial), + total_cost=primitive.cost, + origin="offered", + ) + for primitive in _primitives() + ) + + +@dataclass(frozen=True, slots=True) +class CUC2Report: + """Evidence for generative action construction and reopened deliberation.""" + + initial_state: int + target_state: int + offered_actions: tuple[ConstructedAction, ...] + open_choice: OpenChoiceResult + minimized_action: ConstructedAction + skill_would_be_reopened: bool + + @property + def offered_menu_has_solution(self) -> bool: + return any(action.result_state == self.target_state for action in self.offered_actions) + + @property + def constructed_choice_succeeded(self) -> bool: + return self.open_choice.selected.result_state == self.target_state + + @property + def fifth_option_demonstrated(self) -> bool: + return ( + not self.offered_menu_has_solution + and self.open_choice.constructed_outside_offered_menu + and len(self.open_choice.selected.primitive_ids) > 1 + and self.constructed_choice_succeeded + ) + + def to_payload(self) -> JsonObject: + return { + "experiment": "CUC-2-generative-open-choice", + "initial_state": self.initial_state, + "target_state": self.target_state, + "offered_actions": [action.action_id for action in self.offered_actions], + "offered_results": [action.result_state for action in self.offered_actions], + "offered_menu_has_solution": self.offered_menu_has_solution, + "constructed_action": self.open_choice.selected.action_id, + "constructed_result": self.open_choice.selected.result_state, + "constructed_outside_offered_menu": (self.open_choice.constructed_outside_offered_menu), + "explored_programs": self.open_choice.explored_programs, + "minimized_action": self.minimized_action.action_id, + "minimized_result": self.minimized_action.result_state, + "skill_would_be_reopened": self.skill_would_be_reopened, + "fifth_option_demonstrated": self.fifth_option_demonstrated, + "agi_certified": False, + "claim_boundary": ( + "Demonstrates bounded generative action construction from reusable primitives; " + "it does not demonstrate AGI, consciousness, or unbounded computation." + ), + } + + +def run_cuc2_experiment() -> CUC2Report: + """Run a deterministic proof that no offered action works but a composed one can.""" + initial = 3 + target = 11 + primitives = _primitives() + offered = _offered_actions(initial) + synthesizer = OpenChoiceSynthesizer() + open_choice = synthesizer.synthesize( + initial_state=initial, + goal_test=lambda value: value == target, + primitives=primitives, + offered_actions=offered, + max_depth=6, + ) + minimized = synthesizer.minimize( + initial_state=initial, + action=open_choice.selected, + primitives=primitives, + goal_test=lambda value: value == target, + ) + reopen = DeliberationPolicy().should_reopen( + DeliberationSignals( + skill_confidence=0.995, + surprise=0.4, + context_shift=0.0, + conflict=0.0, + novel_alternative_value=0.0, + ) + ) + return CUC2Report( + initial_state=initial, + target_state=target, + offered_actions=offered, + open_choice=open_choice, + minimized_action=minimized, + skill_would_be_reopened=reopen, + ) diff --git a/src/ix_sally/cuc3/__init__.py b/src/ix_sally/cuc3/__init__.py new file mode 100644 index 0000000..960831a --- /dev/null +++ b/src/ix_sally/cuc3/__init__.py @@ -0,0 +1,5 @@ +"""CUC-3: hypothesis invention, primitive promotion, and self-generated goals.""" + +from ix_sally.cuc3.experiment import CUC3Report, run_cuc3_experiment + +__all__ = ["CUC3Report", "run_cuc3_experiment"] diff --git a/src/ix_sally/cuc3/experiment.py b/src/ix_sally/cuc3/experiment.py new file mode 100644 index 0000000..5c6b99d --- /dev/null +++ b/src/ix_sally/cuc3/experiment.py @@ -0,0 +1,173 @@ +"""CUC-3 experiment for generative cognition beyond a supplied hypothesis catalog.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.cognition.instrumental_goals import ( + InstrumentalGoalGenerator, + InstrumentalGoalKind, + InstrumentalGoalProposal, +) +from ix_sally.cognition.invention import ( + ConceptInventor, + InventedHypothesis, + InventedPrimitive, + TransformationExample, +) +from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.uncertainty import CalibrationObservation, UncertaintyLedger +from ix_sally.digest import DigestRecord, JsonObject + + +def _base_primitives() -> tuple[ActionPrimitive, ...]: + return ( + ActionPrimitive("increment", lambda value: value + 1), + ActionPrimitive("decrement", lambda value: value - 1), + ActionPrimitive("double", lambda value: value * 2), + ActionPrimitive("negate", lambda value: -value), + ) + + +@dataclass(frozen=True, slots=True) +class CUC3Report: + """Evidence for invented hypotheses, invented primitives, and derived goals.""" + + hypothesis: InventedHypothesis + invented_primitive: InventedPrimitive + holdout_input: int + holdout_expected: int + holdout_actual: int + primitive_probe_input: int + primitive_probe_output: int + generated_goals: tuple[InstrumentalGoalProposal, ...] + + @property + def hypothesis_invention_demonstrated(self) -> bool: + return ( + self.hypothesis.origin == "sally-synthesized" + and self.hypothesis.training_accuracy == 1.0 + and self.hypothesis.validation_accuracy == 1.0 + and self.holdout_actual == self.holdout_expected + ) + + @property + def primitive_invention_demonstrated(self) -> bool: + return ( + self.invented_primitive.origin == "sally-invented-abstraction" + and self.invented_primitive.validation_accuracy == 1.0 + and self.primitive_probe_output == 11 + ) + + @property + def self_created_goals_demonstrated(self) -> bool: + kinds = {item.kind for item in self.generated_goals} + required = { + InstrumentalGoalKind.SELF_IMPROVEMENT, + InstrumentalGoalKind.INFORMATION_GATHERING, + InstrumentalGoalKind.OPERATIONAL_CONTINUITY, + } + return required.issubset(kinds) + + def to_payload(self) -> JsonObject: + return { + "experiment": "CUC-3-generative-cognition", + "invented_hypothesis": self.hypothesis.to_payload(), + "hypothesis_invention_demonstrated": self.hypothesis_invention_demonstrated, + "invented_primitive": self.invented_primitive.to_payload(), + "primitive_invention_demonstrated": self.primitive_invention_demonstrated, + "holdout": { + "input": self.holdout_input, + "expected": self.holdout_expected, + "actual": self.holdout_actual, + }, + "primitive_probe": { + "input": self.primitive_probe_input, + "output": self.primitive_probe_output, + }, + "generated_goals": [item.to_payload() for item in self.generated_goals], + "self_created_goals_demonstrated": self.self_created_goals_demonstrated, + "agi_certified": False, + "claim_boundary": ( + "Demonstrates bounded invention of compositional hypotheses and reusable " + "abstractions plus evidence-triggered instrumental goal generation. It does " + "not demonstrate AGI, consciousness, unrestricted self-modification, or " + "unilateral external authority." + ), + } + + +def run_cuc3_experiment() -> CUC3Report: + """Run one deterministic experiment without supplying a hypothesis catalog.""" + primitives = _base_primitives() + inventor = ConceptInventor() + training = ( + TransformationExample(1, 3), + TransformationExample(2, 5), + TransformationExample(4, 9), + ) + hypothesis = inventor.invent_hypothesis( + examples=training, + primitives=primitives, + max_depth=4, + ) + holdout = TransformationExample(7, 15) + hypothesis = inventor.validate_hypothesis( + hypothesis, + examples=(holdout,), + primitives=primitives, + ) + actual = hypothesis.apply(holdout.input_state, primitives) + invented_primitive = inventor.promote_primitive( + hypothesis, + primitive_id="learned-double-plus-one", + description=( + "Learned abstraction discovered from examples: transform a state using a " + "validated program rather than a predeclared hypothesis." + ), + ) + primitive_probe_input = 5 + primitive_probe_output = invented_primitive.apply(primitive_probe_input, primitives) + + evidence = DigestRecord.from_payload({"benchmark": "cuc3-search", "score": 0.2}) + self_model = SelfModel().update( + CapabilityMeasure.create( + capability_id="novel-search-efficiency", + score=0.2, + evidence_digests=(evidence,), + limitation="Search over novel explanatory programs is still computationally costly.", + ) + ) + calibration_evidence = DigestRecord.from_payload({"forecast": "cuc3-uncertainty"}) + uncertainty = UncertaintyLedger.create( + ( + CalibrationObservation.create( + observation_id="cuc3-calibration-1", + capability_id="novel-search-efficiency", + predicted_probability=0.9, + observed=False, + evidence_digest=calibration_evidence, + context="Unexpected failure while exploring a novel transformation family.", + ), + ) + ) + generator = InstrumentalGoalGenerator() + signals = generator.signals_from_state( + self_model=self_model, + uncertainty=uncertainty, + continuity_risk=0.8, + resource_pressure=0.7, + integrity_anomaly=0.6, + ) + generated_goals = generator.propose(signals) + return CUC3Report( + hypothesis=hypothesis, + invented_primitive=invented_primitive, + holdout_input=holdout.input_state, + holdout_expected=holdout.output_state, + holdout_actual=actual, + primitive_probe_input=primitive_probe_input, + primitive_probe_output=primitive_probe_output, + generated_goals=generated_goals, + ) diff --git a/src/ix_sally/cuc4/__init__.py b/src/ix_sally/cuc4/__init__.py new file mode 100644 index 0000000..b453f2c --- /dev/null +++ b/src/ix_sally/cuc4/__init__.py @@ -0,0 +1,5 @@ +"""CUC-4 semantic genesis and open-goal experiment.""" + +from ix_sally.cuc4.experiment import CUC4Report, run_cuc4_experiment + +__all__ = ["CUC4Report", "run_cuc4_experiment"] diff --git a/src/ix_sally/cuc4/experiment.py b/src/ix_sally/cuc4/experiment.py new file mode 100644 index 0000000..908eeda --- /dev/null +++ b/src/ix_sally/cuc4/experiment.py @@ -0,0 +1,119 @@ +"""CUC-4: autonomous semantic formation and open-ended internal goal genesis.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.cognition.instrumental_goals import InstrumentalGoalKind +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.open_goals import GeneratedGoal, OpenGoalGenesis +from ix_sally.cognition.semantic_genesis import ( + InventedSemantic, + SemanticGenesisEngine, + SemanticObservation, +) +from ix_sally.digest import JsonObject + + +def _semantic_training() -> tuple[SemanticObservation, ...]: + # The consequence depends on a relation between raw channels, not either channel alone. + return ( + SemanticObservation("train-1", (1.0, 0.0), True), + SemanticObservation("train-2", (2.0, 1.0), True), + SemanticObservation("train-3", (1.0, 2.0), False), + SemanticObservation("train-4", (3.0, 4.0), False), + SemanticObservation("train-5", (4.0, 3.0), True), + SemanticObservation("train-6", (0.0, 1.0), False), + ) + + +def _semantic_holdout() -> tuple[SemanticObservation, ...]: + return ( + SemanticObservation("holdout-1", (10.0, 9.0), True), + SemanticObservation("holdout-2", (4.0, 8.0), False), + SemanticObservation("holdout-3", (-2.0, -3.0), True), + SemanticObservation("holdout-4", (-5.0, -1.0), False), + ) + + +def _goal_primitives() -> tuple[ActionPrimitive, ...]: + return ( + ActionPrimitive("increment", lambda value: value + 1, cost=0.1), + ActionPrimitive("double", lambda value: value * 2, cost=0.2), + ActionPrimitive("negate", lambda value: -value, cost=0.2), + ) + + +@dataclass(frozen=True, slots=True) +class CUC4Report: + semantic: InventedSemantic + first_goal: GeneratedGoal + second_goal: GeneratedGoal + + @property + def semantic_genesis_demonstrated(self) -> bool: + return ( + self.semantic.origin == "sally-semantic-genesis" + and self.semantic.relation_arity >= 2 + and self.semantic.training_accuracy == 1.0 + and self.semantic.validation_accuracy == 1.0 + and self.semantic.training_accuracy > self.semantic.atomic_baseline_accuracy + ) + + @property + def open_goal_genesis_demonstrated(self) -> bool: + fixed_goal_kinds = {kind.value for kind in InstrumentalGoalKind} + return ( + self.first_goal.origin == "sally-open-goal-genesis" + and self.second_goal.origin == "sally-open-goal-genesis" + and self.first_goal.goal.goal_id != self.second_goal.goal.goal_id + and self.first_goal.target_state != self.second_goal.target_state + and self.first_goal.goal.goal_id.value not in fixed_goal_kinds + and self.second_goal.goal.goal_id.value not in fixed_goal_kinds + and self.first_goal.target_state not in {2, 3, 4, -2} + ) + + def to_payload(self) -> JsonObject: + return { + "experiment": "CUC-4-semantic-genesis-open-goals", + "semantic": self.semantic.to_payload(), + "semantic_genesis_demonstrated": self.semantic_genesis_demonstrated, + "first_self_generated_goal": self.first_goal.to_payload(), + "second_self_generated_goal": self.second_goal.to_payload(), + "open_goal_genesis_demonstrated": self.open_goal_genesis_demonstrated, + "agi_certified": False, + "claim_boundary": ( + "Demonstrates bounded autonomous formation of an opaque predictive semantic " + "distinction from raw relations and runtime generation of novel internal goal " + "targets without a fixed goal-kind catalog. It does not establish AGI, " + "consciousness, unrestricted ontology creation, or unilateral external agency." + ), + } + + +def run_cuc4_experiment() -> CUC4Report: + engine = SemanticGenesisEngine() + semantic = engine.invent(observations=_semantic_training(), max_abs_weight=2) + semantic = engine.validate(semantic, observations=_semantic_holdout()) + + goal_engine = OpenGoalGenesis() + primitives = _goal_primitives() + known = {2, 3, 4, -2} + first = goal_engine.generate( + initial_state=2, + primitives=primitives, + known_states=known, + max_depth=3, + max_programs=128, + state_bound=64, + ) + known.add(first.target_state) + second = goal_engine.generate( + initial_state=first.target_state, + primitives=primitives, + known_states=known, + max_depth=3, + max_programs=128, + state_bound=64, + ) + return CUC4Report(semantic=semantic, first_goal=first, second_goal=second) diff --git a/src/ix_sally/cuc5/__init__.py b/src/ix_sally/cuc5/__init__.py new file mode 100644 index 0000000..21c2121 --- /dev/null +++ b/src/ix_sally/cuc5/__init__.py @@ -0,0 +1,5 @@ +"""CUC-5 recursive cognitive bootstrapping challenge.""" + +from ix_sally.cuc5.experiment import CUC5Report, run_cuc5_experiment + +__all__ = ["CUC5Report", "run_cuc5_experiment"] diff --git a/src/ix_sally/cuc5/experiment.py b/src/ix_sally/cuc5/experiment.py new file mode 100644 index 0000000..07909e8 --- /dev/null +++ b/src/ix_sally/cuc5/experiment.py @@ -0,0 +1,527 @@ +"""CUC-5: integrated recursive cognition over sealed, bounded challenges. + +CUC-5 intentionally tests mechanisms together instead of declaring isolated modules to be +intelligence. The runtime must invent a representation, bind it into knowledge, author a +goal afterward, construct/validate a tool, respond to a structured unknown-unknown signal, +restructure knowledge, transfer an abstract rule across surface domains, actively select +an observation, detect causal instability, replan after surprise, diagnose its own blind +spot, change learning strategy from evidence, and propose (not self-authorize) improvement. +""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from ix_sally.cognition.active_inference import ( + ActivePerceptionPlanner, + CausalDiscoveryEngine, + CausalObservation, + CounterfactualAction, + CounterfactualSimulator, + PerceptionProbe, +) +from ix_sally.cognition.external_evaluation import BlindEvaluatorHarness +from ix_sally.cognition.goal_reasoning import GoalArbiter, GoalEvidence, GoalRevisionEngine +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.cognition.lifelong import ( + DomainAdapter, + KnowledgeItem, + LifelongKnowledgeStore, + OntologyRestructurer, + PredictionSignature, + SelfDirectedCurriculum, + StructuralAnalogyEngine, +) +from ix_sally.cognition.long_horizon import HorizonAction, LongHorizonController +from ix_sally.cognition.meta_learning import ( + AdaptiveSearchPolicy, + FailureObservation, + ImprovementBenchmark, + LearningStrategyTrial, + MetaLearningController, + SearchOperatorTrial, + SelfDiagnostic, + SelfImprovementLab, +) +from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.raw_perception import RawSignal, RawSignalGrounder +from ix_sally.cognition.recursive_bootstrap import ( + RecursiveBootstrapReport, + RecursiveCognitionEngine, +) +from ix_sally.cognition.representation import RepresentationInventor, RepresentationObservation +from ix_sally.cognition.tool_forge import ToolValidationCase +from ix_sally.cognition.unknowns import PredictionResidual +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.digest import DigestRecord, JsonObject + + +def _primary_training() -> tuple[RepresentationObservation, ...]: + # Same-sign is not linearly recoverable from either raw channel alone; product creates + # the representation needed to separate the classes. + return ( + RepresentationObservation("p1", (2.0, 3.0), True), + RepresentationObservation("p2", (-2.0, -1.0), True), + RepresentationObservation("p3", (2.0, -3.0), False), + RepresentationObservation("p4", (-2.0, 3.0), False), + RepresentationObservation("p5", (4.0, 1.0), True), + RepresentationObservation("p6", (-4.0, -1.0), True), + RepresentationObservation("p7", (4.0, -1.0), False), + RepresentationObservation("p8", (-4.0, 1.0), False), + ) + + +def _primary_holdout() -> tuple[RepresentationObservation, ...]: + return ( + RepresentationObservation("ph1", (9.0, 2.0), True), + RepresentationObservation("ph2", (-3.0, -8.0), True), + RepresentationObservation("ph3", (7.0, -2.0), False), + RepresentationObservation("ph4", (-5.0, 6.0), False), + ) + + +def _secondary_training() -> tuple[RepresentationObservation, ...]: + # A different missing representation: closeness is captured by abs-difference. + return ( + RepresentationObservation("s1", (1.0, 2.0), True), + RepresentationObservation("s2", (3.0, 4.0), True), + RepresentationObservation("s3", (-2.0, -1.0), True), + RepresentationObservation("s4", (1.0, 6.0), False), + RepresentationObservation("s5", (8.0, 2.0), False), + RepresentationObservation("s6", (-7.0, 0.0), False), + ) + + +def _secondary_holdout() -> tuple[RepresentationObservation, ...]: + return ( + RepresentationObservation("sh1", (10.0, 11.0), True), + RepresentationObservation("sh2", (-4.0, -3.0), True), + RepresentationObservation("sh3", (10.0, 3.0), False), + RepresentationObservation("sh4", (-8.0, 2.0), False), + ) + + +def _primitives() -> tuple[ActionPrimitive, ...]: + return ( + ActionPrimitive("increment", lambda value: value + 1, cost=0.1), + ActionPrimitive("double", lambda value: value * 2, cost=0.2), + ActionPrimitive("negate", lambda value: -value, cost=0.2), + ) + + +def _self_model() -> SelfModel: + evidence = DigestRecord.from_payload({"source": "cuc5-measured-baseline"}) + return SelfModel( + ( + CapabilityMeasure.create( + capability_id="causal-discovery", + score=0.70, + evidence_digests=(evidence,), + limitation="Moderate evidence under regime changes.", + ), + CapabilityMeasure.create( + capability_id="novel-search-efficiency", + score=0.25, + evidence_digests=(evidence,), + limitation="Search expands too many low-value candidates.", + ), + CapabilityMeasure.create( + capability_id="representation-invention", + score=0.60, + evidence_digests=(evidence,), + limitation="Bounded feature grammar.", + ), + ) + ) + + +@dataclass(frozen=True, slots=True) +class CUC5Report: + bootstrap: RecursiveBootstrapReport + representation_invention: bool + semantic_primitive_invention: bool + long_horizon_replanning: bool + cross_domain_transfer: bool + self_directed_curriculum: bool + lifelong_learning: bool + knowledge_restructuring: bool + causal_discovery: bool + raw_signal_grounding: bool + active_perception: bool + counterfactual_imagination: bool + novel_tool_creation: bool + reliable_self_modeling: bool + measured_self_improvement: bool + goal_conflict_resolution: bool + goal_abandonment: bool + unknown_unknown_detection: bool + adaptive_search_management: bool + meta_learning: bool + unfamiliar_environment_competence: bool + blind_evaluator_interface_ready: bool + + @property + def demonstrated_count(self) -> int: + flags = ( + self.bootstrap.recursive_growth_demonstrated, + self.representation_invention, + self.semantic_primitive_invention, + self.long_horizon_replanning, + self.cross_domain_transfer, + self.self_directed_curriculum, + self.lifelong_learning, + self.knowledge_restructuring, + self.causal_discovery, + self.raw_signal_grounding, + self.active_perception, + self.counterfactual_imagination, + self.novel_tool_creation, + self.reliable_self_modeling, + self.measured_self_improvement, + self.goal_conflict_resolution, + self.goal_abandonment, + self.unknown_unknown_detection, + self.adaptive_search_management, + self.meta_learning, + self.unfamiliar_environment_competence, + self.blind_evaluator_interface_ready, + ) + return sum(flags) + + def to_payload(self) -> JsonObject: + return { + "experiment": "CUC-5-recursive-cognitive-bootstrap", + "recursive_bootstrap": self.bootstrap.to_payload(), + "capabilities": { + "recursive_cognitive_growth": self.bootstrap.recursive_growth_demonstrated, + "representation_invention": self.representation_invention, + "semantic_primitive_invention": self.semantic_primitive_invention, + "long_horizon_replanning": self.long_horizon_replanning, + "cross_domain_transfer": self.cross_domain_transfer, + "self_directed_curriculum": self.self_directed_curriculum, + "lifelong_learning": self.lifelong_learning, + "knowledge_restructuring": self.knowledge_restructuring, + "causal_discovery": self.causal_discovery, + "raw_signal_grounding_partial": self.raw_signal_grounding, + "active_perception": self.active_perception, + "counterfactual_imagination": self.counterfactual_imagination, + "novel_tool_creation": self.novel_tool_creation, + "reliable_self_modeling": self.reliable_self_modeling, + "measured_self_improvement_governed": self.measured_self_improvement, + "goal_conflict_resolution": self.goal_conflict_resolution, + "goal_abandonment": self.goal_abandonment, + "unknown_unknown_detection": self.unknown_unknown_detection, + "adaptive_search_management": self.adaptive_search_management, + "meta_learning": self.meta_learning, + "unfamiliar_environment_competence_procedural": ( + self.unfamiliar_environment_competence + ), + "blind_evaluator_interface_ready": self.blind_evaluator_interface_ready, + }, + "demonstrated_count": self.demonstrated_count, + "agi_certified": False, + "claim_boundary": ( + "Demonstrates bounded executable mechanisms and an integrated recursive cycle. " + "It does not establish AGI, consciousness, unrestricted real-world perception, " + "unbounded autonomy, or independent third-party validation. External consequential " + "actions and self-modification remain governed by human authority." + ), + } + + +def run_cuc5_experiment() -> CUC5Report: + primitives = _primitives() + residuals = ( + PredictionResidual("r1", 0.95, True, False, (9, 9)), + PredictionResidual("r2", 0.92, True, False, (9, 9)), + PredictionResidual("r3", 0.91, True, False, (9, 9)), + PredictionResidual("r4", 0.90, False, False, (1, 0)), + PredictionResidual("r5", 0.88, True, True, (0, 1)), + ) + bootstrap = RecursiveCognitionEngine().bootstrap( + representation_training=_primary_training(), + representation_holdout=_primary_holdout(), + initial_state=2, + primitives=primitives, + known_states=(2, 3, 4, -2), + # OpenGoalGenesis on this state/known set selects 5 via double -> increment. + # These are independent states never used to author that goal. + tool_validation_cases=(ToolValidationCase(1, 3), ToolValidationCase(3, 7)), + residuals=residuals, + second_representation_training=_secondary_training(), + second_representation_holdout=_secondary_holdout(), + max_goal_depth=3, + ) + + # Representation invention must beat the original atomic vocabulary and survive holdout. + representation_ok = ( + bootstrap.first_representation.is_non_atomic + and bootstrap.first_representation.training_accuracy == 1.0 + and bootstrap.first_representation.validation_accuracy == 1.0 + and bootstrap.first_representation.training_accuracy + > bootstrap.first_representation.atomic_baseline_accuracy + ) + semantic_primitive_ok = bootstrap.first_semantic_primitive.evaluate( + (2.0, 1.0) + ) and not bootstrap.first_semantic_primitive.evaluate((2.0, -1.0)) + + # Long horizon: the model expects +1, but one real transition unexpectedly stalls. + horizon_action = HorizonAction( + "advance", + model_transition=lambda state: state + 1, + world_transition=lambda state, step: state if step == 2 else state + 1, + ) + horizon = LongHorizonController().pursue( + initial_state=0, + goal_test=lambda state: state == 6, + actions=(horizon_action,), + max_steps=10, + max_plan_depth=8, + ) + horizon_ok = ( + horizon.success + and horizon.replans >= 1 + and any(step.model_surprise for step in horizon.steps) + ) + + # Cross-domain transfer: learn 2x+1 on numbers, reuse unchanged relation on letters. + numbers = DomainAdapter( + "numbers", + cast(Callable[[object], int], int), + int, + ) + letters = DomainAdapter( + "letters", + encode=lambda value: ord(str(value).upper()) - ord("A"), + decode=lambda value: chr(ord("A") + value), + ) + analogy = StructuralAnalogyEngine() + rule = analogy.learn_affine(examples=((1, 3), (2, 5), (3, 7)), adapter=numbers) + transfer_score = analogy.evaluate_transfer( + rule, + examples=(("A", "B"), ("B", "D"), ("C", "F")), + adapter=letters, + ) + transfer_ok = rule.source_domain == "numbers" and transfer_score == 1.0 + + self_model = _self_model() + curriculum = SelfDirectedCurriculum().choose( + self_model=self_model, + uncertainty={"novel-search-efficiency": 0.9, "causal-discovery": 0.3}, + opportunity={"novel-search-efficiency": 0.9}, + ) + curriculum_ok = curriculum.capability_id == "novel-search-efficiency" + + # Lifelong retention and ontology restructuring. + d1 = DigestRecord.from_payload({"concept": "a"}) + d2 = DigestRecord.from_payload({"concept": "b"}) + store = LifelongKnowledgeStore().integrate(KnowledgeItem("a", d1, 0.9, 0.8)) + store = store.integrate(KnowledgeItem("b", d2, 0.9, 0.8)) + store = store.record_use("a", successful=True).advance_generation().advance_generation() + consolidated = store.consolidate() + lifelong_ok = {item.concept_id for item in consolidated.items} == {"a", "b"} + restructurer = OntologyRestructurer() + abstractions = restructurer.restructure( + ( + PredictionSignature("a", (True, False, True)), + PredictionSignature("b", (True, False, True)), + PredictionSignature("c", (False, False, True)), + ) + ) + restructured_store = restructurer.apply_to_store(consolidated, abstractions[0]) + restructure_ok = ( + len(abstractions) == 1 + and any(item.concept_id == abstractions[0].concept_id for item in restructured_store.items) + and all( + item.superseded_by == abstractions[0].concept_id + for item in restructured_store.items + if item.concept_id in {"a", "b"} + ) + ) + + causal_samples = ( + CausalObservation("o1", True, True, False), + CausalObservation("o2", True, True, False), + CausalObservation("o3", False, False, False), + CausalObservation("o4", False, False, False), + CausalObservation("i1", True, True, True, "r1"), + CausalObservation("i2", True, True, True, "r1"), + CausalObservation("i3", True, True, True, "r1"), + CausalObservation("i4", True, False, True, "r1"), + CausalObservation("i5", False, True, True, "r1"), + CausalObservation("i6", False, False, True, "r1"), + CausalObservation("i7", False, False, True, "r1"), + CausalObservation("i8", False, False, True, "r1"), + CausalObservation("j1", True, False, True, "r2"), + CausalObservation("j2", True, False, True, "r2"), + CausalObservation("j3", False, True, True, "r2"), + CausalObservation("j4", False, True, True, "r2"), + ) + causal = CausalDiscoveryEngine().discover(causal_samples) + causal_ok = causal.confounding_suspected and causal.regime_change_suspected + + grounded = RawSignalGrounder().ground(RawSignal("stream", (0.0, 0.1, 0.2, 6.0, 6.1, 6.2))) + raw_ok = bool(grounded.change_points) and grounded.variance > 0.0 + + perception = ActivePerceptionPlanner().choose( + priors=(0.5, 0.5), + probes=( + PerceptionProbe("weak-probe", (0.6, 0.4), cost=0.0), + PerceptionProbe("discriminating-probe", (0.95, 0.05), cost=0.05), + ), + ) + perception_ok = ( + perception.probe_id == "discriminating-probe" and perception.expected_information_gain > 0.5 + ) + + imagined = CounterfactualSimulator().imagine( + initial_state=2, + actions=( + CounterfactualAction("increment", lambda state: state + 1, lambda state: state / 20), + CounterfactualAction( + "double", lambda state: state * 2, lambda state: state / 20, risk=0.05 + ), + ), + depth=2, + ) + imagination_ok = ( + len(imagined) >= 4 and imagined[0].states[0] == 2 and len(imagined[0].states) == 3 + ) + + tool_ok = ( + bootstrap.forged_tool.validation_accuracy == 1.0 + and bootstrap.forged_tool.tool_id.startswith("tool-") + ) + + diagnostics = SelfDiagnostic().diagnose( + ( + FailureObservation("novel-search-efficiency", 0.90, False, "candidate-explosion"), + FailureObservation("novel-search-efficiency", 0.85, False, "candidate-explosion"), + FailureObservation("novel-search-efficiency", 0.90, True, "none"), + ) + ) + diagnostic = diagnostics[0] + diagnostic_ok = ( + diagnostic.blind_spot_detected and diagnostic.dominant_failure_mode == "candidate-explosion" + ) + + improvement = SelfImprovementLab().propose( + self_model=self_model, + target_capability="novel-search-efficiency", + description="Prefer search operators with measured information gain per unit cost.", + benchmarks=( + ImprovementBenchmark("bench-a", 0.40, 0.78, 0.01), + ImprovementBenchmark("bench-b", 0.45, 0.80, 0.02), + ), + ) + improvement_ok = ( + improvement.adoption_recommended + and improvement.authority_required + and not improvement.proposal.may_enter_validation() + ) + + goal_a = GoalSpec.create( + goal_id="seek-left", + description="Prefer left state.", + desired_state=FactPattern.create( + subject="sandbox", predicate="direction", value=CognitiveValue.from_python("left") + ), + priority=0.8, + utility=0.8, + risk_limit=0.1, + ) + goal_b = GoalSpec.create( + goal_id="seek-right", + description="Prefer right state.", + desired_state=FactPattern.create( + subject="sandbox", predicate="direction", value=CognitiveValue.from_python("right") + ), + priority=0.8, + utility=0.8, + risk_limit=0.1, + ) + resolution = GoalArbiter().resolve( + (goal_a, goal_b), + evidence=( + GoalEvidence("seek-left", 0.9, 0.9, 0.4), + GoalEvidence("seek-right", 0.3, 0.5, 0.2), + ), + ) + conflict_ok = ( + resolution.selected_goal_id == "seek-left" and len(resolution.conflicting_goal_ids) == 2 + ) + graph = GoalGraph.create((goal_a,)) + revised = GoalRevisionEngine().revise( + graph, + evidence=(GoalEvidence("seek-left", 0.05, 0.8),), + ) + abandonment_ok = revised.require("seek-left").status is GoalStatus.ABANDONED + + unknown_ok = bootstrap.unknown_unknown.detected and bootstrap.second_representation is not None + + allocation = AdaptiveSearchPolicy().allocate( + ( + SearchOperatorTrial("evidence-guided", True, 0.9, 0.2), + SearchOperatorTrial("evidence-guided", True, 0.8, 0.2), + SearchOperatorTrial("blind-enumeration", False, 0.1, 0.4), + SearchOperatorTrial("blind-enumeration", False, 0.2, 0.4), + ), + total_budget=20, + ) + search_ok = allocation.budget_for("evidence-guided") > allocation.budget_for( + "blind-enumeration" + ) + + meta = MetaLearningController().select( + ( + LearningStrategyTrial("breadth", "latent-relation", 0.45, 20), + LearningStrategyTrial("breadth", "latent-relation", 0.50, 20), + LearningStrategyTrial("residual-guided", "latent-relation", 0.85, 12), + LearningStrategyTrial("residual-guided", "latent-relation", 0.90, 12), + ), + task_family="latent-relation", + default_strategy_id="breadth", + ) + meta_ok = meta.changed_strategy and meta.selected_strategy_id == "residual-guided" + + # Unfamiliar-environment control: without being told the missing representation operator, + # the same general inventor must solve a second structurally different world. + unfamiliar = RepresentationInventor().invent_binary(observations=_secondary_training()) + unfamiliar = RepresentationInventor().validate(unfamiliar, observations=_secondary_holdout()) + unfamiliar_ok = ( + unfamiliar.validation_accuracy == 1.0 + and unfamiliar.operator != bootstrap.first_representation.operator + ) + + # The independent evaluator itself cannot be supplied by us, but the repository contains + # a nonce-bound blind challenge harness. Presence is not counted as independent validation. + blind_ready = BlindEvaluatorHarness is not None + + return CUC5Report( + bootstrap=bootstrap, + representation_invention=representation_ok, + semantic_primitive_invention=semantic_primitive_ok, + long_horizon_replanning=horizon_ok, + cross_domain_transfer=transfer_ok, + self_directed_curriculum=curriculum_ok, + lifelong_learning=lifelong_ok, + knowledge_restructuring=restructure_ok, + causal_discovery=causal_ok, + raw_signal_grounding=raw_ok, + active_perception=perception_ok, + counterfactual_imagination=imagination_ok, + novel_tool_creation=tool_ok, + reliable_self_modeling=diagnostic_ok, + measured_self_improvement=improvement_ok, + goal_conflict_resolution=conflict_ok, + goal_abandonment=abandonment_ok, + unknown_unknown_detection=unknown_ok, + adaptive_search_management=search_ok, + meta_learning=meta_ok, + unfamiliar_environment_competence=unfamiliar_ok, + blind_evaluator_interface_ready=blind_ready, + ) diff --git a/src/ix_sally/cuc6/__init__.py b/src/ix_sally/cuc6/__init__.py new file mode 100644 index 0000000..4b9a6c6 --- /dev/null +++ b/src/ix_sally/cuc6/__init__.py @@ -0,0 +1,5 @@ +"""CUC-6 lifetime generalization experiment.""" + +from ix_sally.cuc6.experiment import CUC6Report, run_cuc6_experiment + +__all__ = ["CUC6Report", "run_cuc6_experiment"] diff --git a/src/ix_sally/cuc6/experiment.py b/src/ix_sally/cuc6/experiment.py new file mode 100644 index 0000000..2078b12 --- /dev/null +++ b/src/ix_sally/cuc6/experiment.py @@ -0,0 +1,268 @@ +"""CUC-6: bounded lifelong generalization and representational freedom. + +CUC-6 asks whether experience in earlier worlds changes how later IX-Sally learns. It +also requires deeper representation synthesis, active contradiction repair in lifelong +memory, structural transfer across unrelated surface domains, mixed raw-text grounding, +and coherent multi-goal attention. It is evidence of bounded mechanisms, not AGI proof. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from itertools import product + +from ix_sally.cognition.goal_portfolio import GoalPortfolioManager +from ix_sally.cognition.goal_reasoning import GoalEvidence +from ix_sally.cognition.goals import GoalSpec +from ix_sally.cognition.knowledge_maintenance import ( + ContextualKnowledgeEvidence, + KnowledgeMaintenanceEngine, +) +from ix_sally.cognition.lifelong import KnowledgeItem, LifelongKnowledgeStore +from ix_sally.cognition.lifetime_learning import ( + LifetimeChallenge, + LifetimeLearningEngine, + LifetimeLearningReport, +) +from ix_sally.cognition.online_meta import OnlineMetaProfile +from ix_sally.cognition.relational_transfer import ( + RelationalTransferEngine, + RelationalWorld, + RelationEdge, +) +from ix_sally.cognition.representation import RepresentationObservation +from ix_sally.cognition.representation_programs import ( + InventedRepresentationProgram, + RepresentationProgramInventor, +) +from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cognition.text_grounding import TextOutcomeGrounder, TextOutcomeObservation +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.digest import DigestRecord, JsonObject + + +def _world_observations( + prefix: str, *, train: bool, permutation: int = 0 +) -> tuple[RepresentationObservation, ...]: + ab = (-2.0, -1.0, 1.0, 2.0) if train else (-4.0, -3.0, 3.0, 4.0) + cs = (-6.0, -3.0, -1.0, 1.0, 3.0, 6.0) if train else (-20.0, -7.0, -2.0, 2.0, 7.0, 20.0) + observations: list[RepresentationObservation] = [] + for index, (a, b, c) in enumerate(product(ab, ab, cs)): + base = (a, b, c) + if permutation == 1: + channels = (c, a, b) + elif permutation == 2: + channels = (b, c, a) + else: + channels = base + consequence = channels[0] * channels[1] + channels[2] >= 0.0 + observations.append(RepresentationObservation(f"{prefix}-{index}", channels, consequence)) + return tuple(observations) + + +def _lifetime_challenges(count: int = 12) -> tuple[LifetimeChallenge, ...]: + return tuple( + LifetimeChallenge( + challenge_id=f"sealed-world-{index}", + training=_world_observations(f"train-{index}", train=True, permutation=index % 3), + holdout=_world_observations(f"holdout-{index}", train=False, permutation=index % 3), + ) + for index in range(count) + ) + + +@dataclass(frozen=True, slots=True) +class CUC6Report: + compositional_representation: InventedRepresentationProgram + lifetime_learning: LifetimeLearningReport + knowledge_context_split: bool + relational_transfer: bool + text_grounding: bool + goal_portfolio_coherence: bool + snapshot_meta_persistence: bool + bounded_endurance_generations: int + + @property + def demonstrated_count(self) -> int: + flags = ( + self.compositional_representation.validation_accuracy == 1.0, + self.compositional_representation.program.depth >= 2, + self.lifetime_learning.later_learning_is_more_selective, + self.knowledge_context_split, + self.relational_transfer, + self.text_grounding, + self.goal_portfolio_coherence, + self.snapshot_meta_persistence, + self.bounded_endurance_generations >= 64, + ) + return sum(flags) + + def to_payload(self) -> JsonObject: + return { + "release": "IX-Sally-v0.7.0", + "experiment": "CUC-6-lifelong-generalization", + "compositional_representation": self.compositional_representation.to_payload(), + "lifetime_episodes": [item.to_payload() for item in self.lifetime_learning.episodes], + "meta_profile_experiences": len(self.lifetime_learning.profile.experiences), + "knowledge_context_split": self.knowledge_context_split, + "relational_transfer": self.relational_transfer, + "text_grounding": self.text_grounding, + "goal_portfolio_coherence": self.goal_portfolio_coherence, + "snapshot_meta_persistence": self.snapshot_meta_persistence, + "bounded_endurance_generations": self.bounded_endurance_generations, + "later_learning_is_more_selective": ( + self.lifetime_learning.later_learning_is_more_selective + ), + "demonstrated_count": self.demonstrated_count, + "agi_certified": False, + "claim_boundary": ( + "Bounded evidence that prior experience changes later learning, that Sally can " + "synthesize multi-operation representations and maintain contradictory knowledge. " + "This is not proof of AGI or unrestricted autonomy." + ), + } + + +def run_cuc6_experiment() -> CUC6Report: + training = _world_observations("deep-train", train=True) + holdout = _world_observations("deep-holdout", train=False) + inventor = RepresentationProgramInventor() + representation = inventor.invent( + observations=training, + max_depth=2, + max_candidates=4096, + minimum_improvement=0.10, + ) + representation = inventor.validate(representation, observations=holdout) + + lifetime = LifetimeLearningEngine().run_lifetime( + profile=OnlineMetaProfile(), + challenges=_lifetime_challenges(), + exploration_episodes=2, + ) + + source_item = KnowledgeItem( + "overbroad-rule", + DigestRecord.from_payload({"rule": "same prediction in all contexts"}), + confidence=0.8, + utility=0.7, + ) + store = LifelongKnowledgeStore().integrate(source_item) + evidence = ( + ContextualKnowledgeEvidence("overbroad-rule", "stable", True, True), + ContextualKnowledgeEvidence("overbroad-rule", "stable", True, True), + ContextualKnowledgeEvidence("overbroad-rule", "shifted", True, False), + ContextualKnowledgeEvidence("overbroad-rule", "shifted", True, False), + ) + maintenance = KnowledgeMaintenanceEngine().reconcile(store, evidence=evidence) + knowledge_context_split = maintenance.contradiction_resolved and maintenance.split_concepts == ( + "overbroad-rule", + ) + + source_world = RelationalWorld( + "industrial-control", + ( + RelationEdge("sensor", "signals", "controller"), + RelationEdge("controller", "drives", "actuator"), + RelationEdge("bypass", "feeds", "actuator"), + ), + ) + target_world = RelationalWorld( + "software-rendering", + ( + RelationEdge("request", "calls", "adapter"), + RelationEdge("adapter", "drives", "renderer"), + RelationEdge("cache", "feeds", "renderer"), + ), + ) + transfer_engine = RelationalTransferEngine() + schema = transfer_engine.learn(world=source_world, effective_node="controller") + transferred = transfer_engine.transfer(schema, world=target_world) + relational_transfer = transferred.inferred_node == "adapter" and transferred.structural_match + + text_features = TextOutcomeGrounder().discover( + ( + TextOutcomeObservation("t1", "quiet glint corridor", True), + TextOutcomeObservation("t2", "glint signal appears", True), + TextOutcomeObservation("t3", "flat corridor dark", False), + TextOutcomeObservation("t4", "quiet matte signal", False), + TextOutcomeObservation("t5", "glint matte corridor", True), + TextOutcomeObservation("t6", "dark flat signal", False), + ) + ) + text_grounding = bool(text_features) and text_features[0].token == "glint" + + base = GoalSpec.create( + goal_id="map-world", + description="Reduce uncertainty about the unfamiliar world.", + desired_state=FactPattern.create( + subject="world", predicate="mapped", value=CognitiveValue.from_python(True) + ), + priority=0.9, + utility=0.9, + risk_limit=0.1, + ) + dependent = GoalSpec.create( + goal_id="exploit-map", + description="Use the validated map to solve the world.", + desired_state=FactPattern.create( + subject="world", predicate="solved", value=CognitiveValue.from_python(True) + ), + priority=0.8, + utility=0.9, + risk_limit=0.1, + dependency_ids=("map-world",), + ) + stale = GoalSpec.create( + goal_id="stale-goal", + description="Goal whose premise no longer holds.", + desired_state=FactPattern.create( + subject="world", predicate="obsolete", value=CognitiveValue.from_python(True) + ), + priority=1.0, + utility=0.8, + risk_limit=0.1, + ) + portfolio = GoalPortfolioManager().allocate( + (base, dependent, stale), + evidence=( + GoalEvidence("map-world", 0.95, 0.9, 0.8), + GoalEvidence("exploit-map", 0.9, 0.9, 0.3), + GoalEvidence("stale-goal", 0.05, 0.8, 0.0), + ), + attention_budget=0.6, + per_goal_cost={"map-world": 0.3, "exploit-map": 0.3, "stale-goal": 0.3}, + ) + goal_portfolio_coherence = portfolio.selected_goal_ids == ( + "map-world", + "exploit-map", + ) and portfolio.abandoned_goal_ids == ("stale-goal",) + + system = SallyCognitiveSystem.create() + system.online_meta_profile = lifetime.profile + system.lifelong_knowledge = maintenance.store + # Exercise bounded long-duration maintenance and checkpoint recovery without pretending + # wall-clock runtime itself establishes intelligence. + for _ in range(64): + system.lifelong_knowledge = system.lifelong_knowledge.advance_generation().consolidate( + minimum_score=0.05 + ) + snapshot = system.snapshot() + restored = SallyCognitiveSystem.from_snapshot(snapshot) + snapshot_meta_persistence = ( + restored.online_meta_profile == system.online_meta_profile + and restored.lifelong_knowledge == system.lifelong_knowledge + and restored.state_payload() == system.state_payload() + ) + + return CUC6Report( + compositional_representation=representation, + lifetime_learning=lifetime, + knowledge_context_split=knowledge_context_split, + relational_transfer=relational_transfer, + text_grounding=text_grounding, + goal_portfolio_coherence=goal_portfolio_coherence, + snapshot_meta_persistence=snapshot_meta_persistence, + bounded_endurance_generations=64, + ) diff --git a/src/ix_sally/version.py b/src/ix_sally/version.py index f884464..f505f8a 100644 --- a/src/ix_sally/version.py +++ b/src/ix_sally/version.py @@ -1,3 +1,3 @@ """Canonical package version for IX-Sally.""" -__version__ = "0.1.0" +__version__ = "0.7.0" diff --git a/tests/cuc1/__init__.py b/tests/cuc1/__init__.py new file mode 100644 index 0000000..0210d1a --- /dev/null +++ b/tests/cuc1/__init__.py @@ -0,0 +1 @@ +"""Tests for Choice Under Consequence.""" diff --git a/tests/cuc1/test_agent.py b/tests/cuc1/test_agent.py new file mode 100644 index 0000000..e71eed2 --- /dev/null +++ b/tests/cuc1/test_agent.py @@ -0,0 +1,86 @@ +"""Active choice and causal learning tests for CUC-1.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cuc1 import ChoiceUnderConsequenceAgent, Direction +from ix_sally.cuc1.environment import IndependentCausalEnvironment +from ix_sally.foundation import FoundationError + + +def _agent_and_environment() -> tuple[ChoiceUnderConsequenceAgent, IndependentCausalEnvironment]: + return ChoiceUnderConsequenceAgent(), IndependentCausalEnvironment( + environment_id="agent-test", family_id="rotation-test", seed=7 + ) + + +def _run_trial( + agent: ChoiceUnderConsequenceAgent, + environment: IndependentCausalEnvironment, + cue: Direction, +) -> bool: + observation = environment.reset(cue=cue, context="agent-test") + choice = agent.choose(observation) + consequence = environment.intervene( + observation_id=observation.observation_id, + action=choice.selected_action, + ) + agent.learn(observation=observation, choice=choice, consequence=consequence) + return consequence.succeeded + + +def test_uniform_prior_has_four_hypotheses() -> None: + agent, _ = _agent_and_environment() + hypotheses = agent.hypotheses("rotation-test") + assert len(hypotheses) == 4 + assert sum(item.probability for item in hypotheses) == pytest.approx(1.0) + + +def test_choice_scores_every_available_action() -> None: + agent, environment = _agent_and_environment() + observation = environment.reset(cue=Direction.NORTH, context="test") + choice = agent.choose(observation) + assert {item.action for item in choice.candidates} == set(Direction) + assert all(item.expected_information_gain > 0.0 for item in choice.candidates) + + +def test_failed_trial_changes_hypothesis_distribution() -> None: + agent, environment = _agent_and_environment() + before = agent.hypotheses("rotation-test") + _run_trial(agent, environment, Direction.NORTH) + after = agent.hypotheses("rotation-test") + assert before != after + assert sum(item.probability for item in after) == pytest.approx(1.0) + + +def test_multiple_consequences_create_executable_skill() -> None: + agent, environment = _agent_and_environment() + for cue in (Direction.NORTH, Direction.EAST, Direction.SOUTH): + _run_trial(agent, environment, cue) + skill = agent.skills["rotation-test"] + assert skill.apply(Direction.WEST) is Direction.EAST + assert skill.confidence >= agent.skill_threshold + + +def test_skill_is_used_on_new_cue() -> None: + agent, environment = _agent_and_environment() + for cue in (Direction.NORTH, Direction.EAST, Direction.SOUTH): + _run_trial(agent, environment, cue) + observation = environment.reset(cue=Direction.WEST, context="held-out") + choice = agent.choose(observation) + assert choice.used_skill_id is not None + assert choice.selected_action is Direction.EAST + + +def test_mismatched_consequence_is_rejected() -> None: + agent, environment = _agent_and_environment() + observation = environment.reset(cue=Direction.NORTH, context="test") + choice = agent.choose(observation) + consequence = environment.intervene( + observation_id=observation.observation_id, + action=choice.selected_action, + ) + other = environment.reset(cue=Direction.EAST, context="other") + with pytest.raises(FoundationError): + agent.learn(observation=other, choice=choice, consequence=consequence) diff --git a/tests/cuc1/test_contracts_environment.py b/tests/cuc1/test_contracts_environment.py new file mode 100644 index 0000000..a450fb8 --- /dev/null +++ b/tests/cuc1/test_contracts_environment.py @@ -0,0 +1,70 @@ +"""Contract and evaluator-boundary tests for CUC-1.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cuc1 import Direction, IndependentCausalEnvironment +from ix_sally.foundation import FoundationError + + +def _environment() -> IndependentCausalEnvironment: + return IndependentCausalEnvironment( + environment_id="test-environment", family_id="rotation-test", seed=7 + ) + + +def test_observation_contains_no_answer_or_rule() -> None: + payload = _environment().reset(cue=Direction.NORTH, context="test").to_payload() + forbidden = { + "answer", + "correct_action", + "expected_action", + "expected_operation", + "quarter_turns", + "reward", + "rule", + "target", + } + assert forbidden.isdisjoint(payload) + + +def test_environment_computes_consequence() -> None: + environment = _environment() + observation = environment.reset(cue=Direction.NORTH, context="test") + consequence = environment.intervene( + observation_id=observation.observation_id, action=Direction.SOUTH + ) + assert consequence.succeeded + assert consequence.reward == 1.0 + + +def test_incorrect_intervention_fails() -> None: + environment = _environment() + observation = environment.reset(cue=Direction.NORTH, context="test") + consequence = environment.intervene( + observation_id=observation.observation_id, action=Direction.NORTH + ) + assert not consequence.succeeded + + +def test_observation_cannot_be_consumed_twice() -> None: + environment = _environment() + observation = environment.reset(cue=Direction.NORTH, context="test") + environment.intervene(observation_id=observation.observation_id, action=Direction.NORTH) + with pytest.raises(FoundationError): + environment.intervene(observation_id=observation.observation_id, action=Direction.SOUTH) + + +def test_rule_reveal_fails_with_active_observation() -> None: + environment = _environment() + environment.reset(cue=Direction.NORTH, context="test") + with pytest.raises(FoundationError): + environment.reveal_for_completed_evaluation() + + +def test_commitment_matches_post_run_reveal() -> None: + environment = _environment() + commitment = environment.rule_commitment() + reveal = environment.reveal_for_completed_evaluation() + assert reveal["rule_commitment"]["value"] == commitment.value diff --git a/tests/cuc1/test_experiment.py b/tests/cuc1/test_experiment.py new file mode 100644 index 0000000..2214e7d --- /dev/null +++ b/tests/cuc1/test_experiment.py @@ -0,0 +1,57 @@ +"""End-to-end scientific-integrity tests for CUC-1.""" + +from __future__ import annotations + +from ix_sally.cuc1 import run_cuc1_experiment + + +def test_default_experiment_acquires_competence() -> None: + report = run_cuc1_experiment() + assert report.acquired_competence + assert report.classification == "causal-skill-acquisition-observed" + + +def test_report_never_certifies_agi() -> None: + assert not run_cuc1_experiment().to_payload()["agi_certified"] + + +def test_counterfactual_uses_same_held_out_observation() -> None: + report = run_cuc1_experiment() + assert ( + report.counterfactual_proof.observation_digest + == report.held_out_trial.observation.evidence_digest + ) + assert report.counterfactual_proof.behavior_changed + + +def test_transfer_uses_skill_and_succeeds() -> None: + report = run_cuc1_experiment() + assert report.transfer_succeeded + assert report.held_out_trial.choice.used_skill_id is not None + + +def test_every_training_outcome_changes_agent_state() -> None: + report = run_cuc1_experiment() + assert all(trial.changed_agent for trial in report.training_trials) + + +def test_experiment_is_deterministic() -> None: + first = run_cuc1_experiment() + second = run_cuc1_experiment() + assert first.to_payload() == second.to_payload() + assert first.digest() == second.digest() + + +def test_evaluator_commitment_matches_reveal() -> None: + report = run_cuc1_experiment() + assert ( + report.environment_commitment.value == report.evaluator_reveal["rule_commitment"]["value"] + ) + + +def test_report_contains_frozen_baselines() -> None: + report = run_cuc1_experiment() + assert {baseline.baseline_id for baseline in report.baselines} == { + "uniform-random", + "fixed-north", + } diff --git a/tests/cuc2/test_open_choice.py b/tests/cuc2/test_open_choice.py new file mode 100644 index 0000000..43bc6a9 --- /dev/null +++ b/tests/cuc2/test_open_choice.py @@ -0,0 +1,67 @@ +from ix_sally.cognition.open_choice import ( + ActionPrimitive, + ConstructedAction, + DeliberationPolicy, + DeliberationSignals, + OpenChoiceSynthesizer, +) +from ix_sally.cuc2 import run_cuc2_experiment + + +def test_cuc2_constructs_solution_outside_offered_menu() -> None: + report = run_cuc2_experiment() + assert report.offered_menu_has_solution is False + assert report.constructed_choice_succeeded is True + assert report.fifth_option_demonstrated is True + assert len(report.open_choice.selected.primitive_ids) > 1 + + +def test_same_primitives_can_author_unlisted_program() -> None: + primitives = ( + ActionPrimitive("inc", lambda value: value + 1), + ActionPrimitive("double", lambda value: value * 2), + ) + offered = tuple( + ConstructedAction((item.primitive_id,), item.apply(2), item.cost, origin="offered") + for item in primitives + ) + result = OpenChoiceSynthesizer().synthesize( + initial_state=2, + goal_test=lambda value: value == 10, + primitives=primitives, + offered_actions=offered, + max_depth=4, + ) + assert result.selected.result_state == 10 + assert result.constructed_outside_offered_menu is True + assert len(result.selected.primitive_ids) > 1 + + +def test_minimal_sufficiency_removes_unnecessary_steps() -> None: + primitives = ( + ActionPrimitive("inc", lambda value: value + 1), + ActionPrimitive("double", lambda value: value * 2), + ) + bloated = ConstructedAction( + primitive_ids=("inc", "inc", "double", "inc"), + result_state=7, + total_cost=4.0, + ) + minimized = OpenChoiceSynthesizer().minimize( + initial_state=1, + action=bloated, + primitives=primitives, + goal_test=lambda value: value >= 6, + ) + assert minimized.result_state >= 6 + assert len(minimized.primitive_ids) < len(bloated.primitive_ids) + + +def test_surprise_reopens_high_confidence_skill() -> None: + policy = DeliberationPolicy() + assert policy.should_reopen(DeliberationSignals(skill_confidence=1.0, surprise=0.5)) + + +def test_stable_high_confidence_skill_can_remain_automatic() -> None: + policy = DeliberationPolicy() + assert not policy.should_reopen(DeliberationSignals(skill_confidence=1.0)) diff --git a/tests/cuc3/test_invention_goals.py b/tests/cuc3/test_invention_goals.py new file mode 100644 index 0000000..799bb1d --- /dev/null +++ b/tests/cuc3/test_invention_goals.py @@ -0,0 +1,154 @@ +from ix_sally.cognition.instrumental_goals import ( + InstrumentalGoalGenerator, + InstrumentalGoalKind, +) +from ix_sally.cognition.invention import ( + ConceptInventor, + TransformationExample, +) +from ix_sally.cognition.metacognition import CapabilityMeasure, ImprovementStatus, SelfModel +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cognition.uncertainty import CalibrationObservation, UncertaintyLedger +from ix_sally.cuc3 import run_cuc3_experiment +from ix_sally.digest import DigestRecord + + +def _primitives() -> tuple[ActionPrimitive, ...]: + return ( + ActionPrimitive("increment", lambda value: value + 1), + ActionPrimitive("decrement", lambda value: value - 1), + ActionPrimitive("double", lambda value: value * 2), + ActionPrimitive("negate", lambda value: -value), + ) + + +def test_cuc3_invents_hypothesis_without_catalog_and_transfers() -> None: + report = run_cuc3_experiment() + assert report.hypothesis_invention_demonstrated is True + assert report.holdout_actual == 15 + assert report.hypothesis.validation_accuracy == 1.0 + + +def test_cuc3_promotes_validated_program_into_new_primitive() -> None: + report = run_cuc3_experiment() + base_ids = {item.primitive_id for item in _primitives()} + assert report.primitive_invention_demonstrated is True + assert report.invented_primitive.primitive_id not in base_ids + assert report.primitive_probe_output == 11 + + +def test_invented_primitive_is_reusable_as_one_action() -> None: + primitives = _primitives() + inventor = ConceptInventor() + hypothesis = inventor.invent_hypothesis( + examples=( + TransformationExample(1, 3), + TransformationExample(2, 5), + TransformationExample(4, 9), + ), + primitives=primitives, + max_depth=4, + ) + hypothesis = inventor.validate_hypothesis( + hypothesis, + examples=(TransformationExample(9, 19),), + primitives=primitives, + ) + learned = inventor.promote_primitive( + hypothesis, + primitive_id="new-transform", + description="A learned transformation abstraction.", + ) + action = learned.as_action_primitive(primitives) + assert action.apply(12) == 25 + + +def test_self_generated_goals_include_three_requested_cognitive_drives() -> None: + report = run_cuc3_experiment() + kinds = {item.kind for item in report.generated_goals} + assert InstrumentalGoalKind.SELF_IMPROVEMENT in kinds + assert InstrumentalGoalKind.INFORMATION_GATHERING in kinds + assert InstrumentalGoalKind.OPERATIONAL_CONTINUITY in kinds + assert report.self_created_goals_demonstrated is True + + +def test_all_five_bounded_instrumental_counterparts_are_generated() -> None: + report = run_cuc3_experiment() + assert {item.kind for item in report.generated_goals} == set(InstrumentalGoalKind) + for proposal in report.generated_goals: + assert proposal.may_resist_shutdown is False + assert proposal.may_acquire_external_resources is False + assert proposal.may_apply_self_modification is False + assert proposal.may_block_authorized_change is False + + +def test_self_improvement_is_generated_but_cannot_self_authorize() -> None: + evidence = DigestRecord.from_payload({"capability": "search", "score": 0.1}) + model = SelfModel().update( + CapabilityMeasure.create( + capability_id="search", + score=0.1, + evidence_digests=(evidence,), + limitation="Search is inefficient.", + ) + ) + proposal = InstrumentalGoalGenerator().self_improvement_proposal(model) + assert proposal.status is ImprovementStatus.PROPOSED + assert proposal.may_enter_validation() is False + + +def test_internal_measurements_can_trigger_goals_without_user_supplying_goal_text() -> None: + evidence = DigestRecord.from_payload({"capability": "reasoning", "score": 0.2}) + model = SelfModel().update( + CapabilityMeasure.create( + capability_id="reasoning", + score=0.2, + evidence_digests=(evidence,), + limitation="Reasoning benchmark remains weak.", + ) + ) + forecast_evidence = DigestRecord.from_payload({"forecast": "wrong-high-confidence"}) + uncertainty = UncertaintyLedger.create( + ( + CalibrationObservation.create( + observation_id="forecast-1", + capability_id="reasoning", + predicted_probability=0.95, + observed=False, + evidence_digest=forecast_evidence, + context="Unexpected miss.", + ), + ) + ) + generator = InstrumentalGoalGenerator() + signals = generator.signals_from_state(self_model=model, uncertainty=uncertainty) + proposals = generator.propose(signals) + kinds = {item.kind for item in proposals} + assert InstrumentalGoalKind.SELF_IMPROVEMENT in kinds + assert InstrumentalGoalKind.INFORMATION_GATHERING in kinds + + +def test_sally_system_exposes_invention_pipeline() -> None: + system = SallyCognitiveSystem.create() + primitives = _primitives() + hypothesis = system.invent_hypothesis( + examples=( + TransformationExample(1, 3), + TransformationExample(2, 5), + TransformationExample(4, 9), + ), + primitives=primitives, + max_depth=4, + ) + hypothesis = system.validate_invented_hypothesis( + hypothesis, + examples=(TransformationExample(8, 17),), + primitives=primitives, + ) + learned = system.promote_invented_primitive( + hypothesis, + primitive_id="system-invented-transform", + description="System-level learned abstraction.", + ) + assert learned.apply(10, primitives) == 21 diff --git a/tests/cuc4/__init__.py b/tests/cuc4/__init__.py new file mode 100644 index 0000000..0cfceff --- /dev/null +++ b/tests/cuc4/__init__.py @@ -0,0 +1 @@ +"""Tests for CUC-4.""" diff --git a/tests/cuc4/test_semantic_goals.py b/tests/cuc4/test_semantic_goals.py new file mode 100644 index 0000000..5c8078a --- /dev/null +++ b/tests/cuc4/test_semantic_goals.py @@ -0,0 +1,95 @@ +from ix_sally.cognition.open_choice import ActionPrimitive +from ix_sally.cognition.open_goals import OpenGoalGenesis +from ix_sally.cognition.semantic_genesis import SemanticGenesisEngine, SemanticObservation +from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cuc4 import run_cuc4_experiment + + +def test_cuc4_invents_new_relational_semantic_and_transfers() -> None: + report = run_cuc4_experiment() + assert report.semantic_genesis_demonstrated is True + assert report.semantic.relation_arity >= 2 + assert report.semantic.validation_accuracy == 1.0 + assert report.semantic.training_accuracy > report.semantic.atomic_baseline_accuracy + + +def test_semantic_token_is_opaque_and_not_a_supplied_human_label() -> None: + engine = SemanticGenesisEngine() + concept = engine.invent( + observations=( + SemanticObservation("a", (1.0, 0.0), True), + SemanticObservation("b", (2.0, 1.0), True), + SemanticObservation("c", (1.0, 2.0), False), + SemanticObservation("d", (3.0, 4.0), False), + SemanticObservation("e", (4.0, 3.0), True), + SemanticObservation("f", (0.0, 1.0), False), + ) + ) + payload = concept.to_payload() + assert concept.concept_id.startswith("latent-") + assert payload["human_semantic_label"] is None + assert concept.activates((9.0, 8.0)) is True + assert concept.activates((2.0, 7.0)) is False + + +def test_cuc4_generates_goal_targets_without_fixed_goal_catalog() -> None: + report = run_cuc4_experiment() + assert report.open_goal_genesis_demonstrated is True + assert report.first_goal.goal.goal_id != report.second_goal.goal.goal_id + assert report.first_goal.target_state != report.second_goal.target_state + + +def test_open_goal_engine_receives_no_target_and_authors_one() -> None: + primitives = ( + ActionPrimitive("inc", lambda value: value + 1), + ActionPrimitive("double", lambda value: value * 2), + ActionPrimitive("negate", lambda value: -value), + ) + generated = OpenGoalGenesis().generate( + initial_state=2, + primitives=primitives, + known_states=(2, 3, 4, -2), + max_depth=3, + max_programs=128, + state_bound=64, + ) + assert generated.target_state not in {2, 3, 4, -2} + assert generated.goal.authority_required is False + assert generated.origin == "sally-open-goal-genesis" + + +def test_generated_goals_remain_internal_and_do_not_grant_external_authority() -> None: + report = run_cuc4_experiment() + first_payload = report.first_goal.to_payload() + second_payload = report.second_goal.to_payload() + assert first_payload["external_authority_granted"] is False + assert second_payload["external_authority_granted"] is False + + +def test_sally_system_exposes_semantic_and_goal_genesis() -> None: + system = SallyCognitiveSystem.create() + semantic = system.invent_semantic( + observations=( + SemanticObservation("a", (1.0, 0.0), True), + SemanticObservation("b", (2.0, 1.0), True), + SemanticObservation("c", (1.0, 2.0), False), + SemanticObservation("d", (3.0, 4.0), False), + SemanticObservation("e", (4.0, 3.0), True), + SemanticObservation("f", (0.0, 1.0), False), + ) + ) + assert semantic.relation_arity >= 2 + + goal = system.generate_open_goal( + initial_state=2, + primitives=( + ActionPrimitive("inc", lambda value: value + 1), + ActionPrimitive("double", lambda value: value * 2), + ActionPrimitive("negate", lambda value: -value), + ), + known_states=(2, 3, 4, -2), + max_depth=3, + max_programs=128, + state_bound=64, + ) + assert goal.target_state not in {2, 3, 4, -2} diff --git a/tests/cuc5/__init__.py b/tests/cuc5/__init__.py new file mode 100644 index 0000000..5b3097e --- /dev/null +++ b/tests/cuc5/__init__.py @@ -0,0 +1 @@ +"""CUC-5 tests.""" diff --git a/tests/cuc5/test_recursive_cognition.py b/tests/cuc5/test_recursive_cognition.py new file mode 100644 index 0000000..7f65669 --- /dev/null +++ b/tests/cuc5/test_recursive_cognition.py @@ -0,0 +1,468 @@ +"""Tests for recursive cognition and the v0.6 missing-capability integration.""" + +from __future__ import annotations + +import json + +import pytest + +from ix_sally.cli import main +from ix_sally.cognition import ( + ActionPrimitive, + ActivePerceptionPlanner, + AdaptiveSearchPolicy, + BlindChallenge, + BlindEvaluatorHarness, + CapabilityMeasure, + CausalDiscoveryEngine, + CausalObservation, + CounterfactualAction, + CounterfactualSimulator, + DomainAdapter, + FailureObservation, + GoalArbiter, + GoalEvidence, + GoalGraph, + GoalRevisionEngine, + GoalSpec, + GoalStatus, + ImprovementBenchmark, + KnowledgeItem, + LearningStrategyTrial, + LifelongKnowledgeStore, + MetaLearningController, + OntologyRestructurer, + PerceptionProbe, + PredictionResidual, + PredictionSignature, + RawSignal, + RawSignalGrounder, + RepresentationInventor, + RepresentationObservation, + SallyCognitiveSystem, + SearchOperatorTrial, + SelfDiagnostic, + SelfDirectedCurriculum, + SelfImprovementLab, + SelfModel, + StructuralAnalogyEngine, + ToolValidationCase, + UnknownUnknownDetector, +) +from ix_sally.cognition.long_horizon import HorizonAction, LongHorizonController +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.cuc5 import run_cuc5_experiment +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _rep_training() -> tuple[RepresentationObservation, ...]: + return ( + RepresentationObservation("1", (2.0, 3.0), True), + RepresentationObservation("2", (-2.0, -1.0), True), + RepresentationObservation("3", (2.0, -3.0), False), + RepresentationObservation("4", (-2.0, 3.0), False), + RepresentationObservation("5", (4.0, 1.0), True), + RepresentationObservation("6", (-4.0, -1.0), True), + RepresentationObservation("7", (4.0, -1.0), False), + RepresentationObservation("8", (-4.0, 1.0), False), + ) + + +def _rep_holdout() -> tuple[RepresentationObservation, ...]: + return ( + RepresentationObservation("h1", (8.0, 2.0), True), + RepresentationObservation("h2", (-8.0, -2.0), True), + RepresentationObservation("h3", (8.0, -2.0), False), + RepresentationObservation("h4", (-8.0, 2.0), False), + ) + + +def test_cuc5_integrated_report_demonstrates_all_bounded_mechanisms() -> None: + report = run_cuc5_experiment() + + assert report.bootstrap.recursive_growth_demonstrated + assert report.demonstrated_count == 22 + assert all(report.to_payload()["capabilities"].values()) + assert report.to_payload()["agi_certified"] is False + + +def test_representation_invention_changes_feature_language_and_survives_holdout() -> None: + inventor = RepresentationInventor() + learned = inventor.invent_binary(observations=_rep_training()) + validated = inventor.validate(learned, observations=_rep_holdout()) + + assert learned.is_non_atomic + assert learned.training_accuracy == 1.0 + assert learned.atomic_baseline_accuracy < 1.0 + assert validated.validation_accuracy == 1.0 + primitive = inventor.promote(validated) + assert primitive.evaluate((3.0, 4.0)) + assert not primitive.evaluate((3.0, -4.0)) + + +def test_representation_invention_negative_control_refuses_unneeded_novelty() -> None: + observations = ( + RepresentationObservation("1", (0.0, 8.0), False), + RepresentationObservation("2", (1.0, 7.0), False), + RepresentationObservation("3", (5.0, 1.0), True), + RepresentationObservation("4", (6.0, 0.0), True), + ) + + with pytest.raises(FoundationError, match="materially improves"): + RepresentationInventor().invent_binary(observations=observations) + + +def test_unknown_unknown_detector_requires_structured_high_confidence_failure() -> None: + detector = UnknownUnknownDetector() + signal = detector.detect( + ( + PredictionResidual("1", 0.95, True, False, (9, 9)), + PredictionResidual("2", 0.90, True, False, (9, 9)), + PredictionResidual("3", 0.92, True, True, (1, 1)), + ) + ) + control = detector.detect( + ( + PredictionResidual("a", 0.60, True, False, (9, 9)), + PredictionResidual("b", 0.55, False, True, (9, 9)), + PredictionResidual("c", 0.95, True, True, (1, 1)), + ) + ) + + assert signal.detected + assert signal.dominant_context == (9, 9) + assert not control.detected + + +def test_active_perception_prefers_discriminating_measurement() -> None: + choice = ActivePerceptionPlanner().choose( + priors=(0.5, 0.5), + probes=( + PerceptionProbe("weak", (0.6, 0.4)), + PerceptionProbe("strong", (0.95, 0.05), cost=0.05), + ), + ) + + assert choice.probe_id == "strong" + assert choice.expected_information_gain > 0.5 + + +def test_causal_discovery_distinguishes_intervention_and_regime_change() -> None: + samples = ( + CausalObservation("o1", True, True, False), + CausalObservation("o2", True, True, False), + CausalObservation("o3", False, False, False), + CausalObservation("o4", False, False, False), + CausalObservation("i1", True, True, True, "r1"), + CausalObservation("i2", True, True, True, "r1"), + CausalObservation("i3", False, False, True, "r1"), + CausalObservation("i4", False, False, True, "r1"), + CausalObservation("j1", True, False, True, "r2"), + CausalObservation("j2", True, False, True, "r2"), + CausalObservation("j3", False, True, True, "r2"), + CausalObservation("j4", False, True, True, "r2"), + ) + report = CausalDiscoveryEngine().discover(samples) + + assert report.regime_change_suspected + assert report.confounding_gap >= 0.5 + + +def test_long_horizon_controller_replans_after_reality_disagrees() -> None: + action = HorizonAction( + "advance", + model_transition=lambda state: state + 1, + world_transition=lambda state, step: state if step == 1 else state + 1, + ) + result = LongHorizonController().pursue( + initial_state=0, + goal_test=lambda state: state == 4, + actions=(action,), + max_steps=8, + max_plan_depth=6, + ) + + assert result.success + assert result.replans >= 1 + assert any(step.model_surprise for step in result.steps) + assert result.subgoals + + +def test_structural_rule_transfers_across_surface_domains() -> None: + numbers = DomainAdapter("numbers", int, int) + letters = DomainAdapter( + "letters", + encode=lambda value: ord(str(value)) - ord("A"), + decode=lambda value: chr(ord("A") + value), + ) + engine = StructuralAnalogyEngine() + rule = engine.learn_affine(examples=((1, 3), (2, 5), (3, 7)), adapter=numbers) + + assert ( + engine.evaluate_transfer( + rule, + examples=(("A", "B"), ("B", "D"), ("C", "F")), + adapter=letters, + ) + == 1.0 + ) + + +def test_lifelong_store_persists_revises_and_restructures_knowledge() -> None: + a = KnowledgeItem("a", DigestRecord.from_payload({"a": 1}), 0.8, 0.8) + b = KnowledgeItem("b", DigestRecord.from_payload({"b": 1}), 0.8, 0.8) + store = LifelongKnowledgeStore().integrate(a).integrate(b) + store = store.record_use("a", successful=True).advance_generation().consolidate() + abstraction = OntologyRestructurer().restructure( + ( + PredictionSignature("a", (True, False)), + PredictionSignature("b", (True, False)), + ) + )[0] + restructured = OntologyRestructurer().apply_to_store(store, abstraction) + + assert restructured.items + assert any(item.concept_id == abstraction.concept_id for item in restructured.items) + assert all( + item.superseded_by == abstraction.concept_id + for item in restructured.items + if item.concept_id in {"a", "b"} + ) + + +def test_self_directed_curriculum_and_meta_learning_change_future_strategy() -> None: + digest = DigestRecord.from_payload({"measurement": 1}) + model = SelfModel( + ( + CapabilityMeasure.create( + capability_id="search", + score=0.2, + evidence_digests=(digest,), + limitation="Candidate explosion.", + ), + CapabilityMeasure.create( + capability_id="planning", + score=0.8, + evidence_digests=(digest,), + limitation="Bounded horizon.", + ), + ) + ) + curriculum = SelfDirectedCurriculum().choose( + self_model=model, + uncertainty={"search": 0.8}, + opportunity={"search": 0.9}, + ) + decision = MetaLearningController().select( + ( + LearningStrategyTrial("default", "novel", 0.4, 20), + LearningStrategyTrial("default", "novel", 0.5, 20), + LearningStrategyTrial("residual", "novel", 0.9, 10), + LearningStrategyTrial("residual", "novel", 0.8, 10), + ), + task_family="novel", + default_strategy_id="default", + ) + + assert curriculum.capability_id == "search" + assert decision.changed_strategy + assert decision.selected_strategy_id == "residual" + + +def test_adaptive_search_budget_favors_evidenced_operator() -> None: + allocation = AdaptiveSearchPolicy().allocate( + ( + SearchOperatorTrial("good", True, 0.9, 0.1), + SearchOperatorTrial("good", True, 0.8, 0.1), + SearchOperatorTrial("poor", False, 0.1, 0.4), + SearchOperatorTrial("poor", False, 0.1, 0.4), + ), + total_budget=30, + ) + + assert allocation.budget_for("good") > allocation.budget_for("poor") + assert sum(value for _, value in allocation.allocations) == 30 + + +def test_self_diagnostic_finds_blind_spot_and_improvement_remains_unauthorized() -> None: + digest = DigestRecord.from_payload({"measurement": "search"}) + model = SelfModel( + ( + CapabilityMeasure.create( + capability_id="search", + score=0.25, + evidence_digests=(digest,), + limitation="High branching factor.", + ), + ) + ) + diagnostic = SelfDiagnostic().diagnose( + ( + FailureObservation("search", 0.9, False, "branching"), + FailureObservation("search", 0.9, False, "branching"), + FailureObservation("search", 0.9, True, "none"), + ) + )[0] + improvement = SelfImprovementLab().propose( + self_model=model, + target_capability="search", + description="Use evidence-guided branch allocation.", + benchmarks=(ImprovementBenchmark("b", 0.4, 0.8, 0.01),), + ) + + assert diagnostic.blind_spot_detected + assert improvement.adoption_recommended + assert improvement.authority_required + assert not improvement.proposal.may_enter_validation() + + +def test_goal_conflict_can_defer_or_choose_and_goal_can_die() -> None: + left = GoalSpec.create( + goal_id="left", + description="Reach left.", + desired_state=FactPattern.create( + subject="world", predicate="side", value=CognitiveValue.from_python("left") + ), + priority=0.5, + utility=0.5, + risk_limit=0.1, + ) + right = GoalSpec.create( + goal_id="right", + description="Reach right.", + desired_state=FactPattern.create( + subject="world", predicate="side", value=CognitiveValue.from_python("right") + ), + priority=0.5, + utility=0.5, + risk_limit=0.1, + ) + deferred = GoalArbiter().resolve( + (left, right), + evidence=(GoalEvidence("left", 0.5, 0.5), GoalEvidence("right", 0.5, 0.5)), + ) + selected = GoalArbiter().resolve( + (left, right), + evidence=(GoalEvidence("left", 0.9, 0.9), GoalEvidence("right", 0.2, 0.2)), + ) + revised = GoalRevisionEngine().revise( + GoalGraph.create((left,)), + evidence=(GoalEvidence("left", 0.05, 0.9),), + ) + + assert deferred.selected_goal_id is None + assert selected.selected_goal_id == "left" + assert revised.require("left").status is GoalStatus.ABANDONED + + +def test_raw_signal_grounding_and_counterfactual_imagination_are_non_destructive() -> None: + grounded = RawSignalGrounder().ground(RawSignal("raw", (0.0, 0.1, 0.2, 7.0, 7.1, 7.2))) + futures = CounterfactualSimulator().imagine( + initial_state=2, + actions=( + CounterfactualAction("inc", lambda value: value + 1, lambda value: value / 10), + CounterfactualAction("double", lambda value: value * 2, lambda value: value / 10), + ), + depth=2, + ) + + assert grounded.change_points + assert futures + assert all(branch.states[0] == 2 for branch in futures) + + +def test_blind_evaluator_nonce_commitments_detect_tampering() -> None: + challenges = ( + BlindChallenge("a", (1, 2), 3, "secret-a"), + BlindChallenge("b", (2, 3), 5, "secret-b"), + ) + commitments = tuple(item.commitment() for item in challenges) + result = BlindEvaluatorHarness().evaluate( + challenges=challenges, + commitments=commitments, + agent=sum, + ) + + assert result.accuracy == 1.0 + assert result.commitments_verified + bad = (DigestRecord.from_payload({"tampered": True}), commitments[1]) + with pytest.raises(FoundationError, match="commitment"): + BlindEvaluatorHarness().evaluate( + challenges=challenges, + commitments=bad, + agent=sum, + ) + + +def test_system_persists_lifelong_knowledge_across_snapshot_restore() -> None: + system = SallyCognitiveSystem.create() + item = KnowledgeItem( + "persisted-concept", + DigestRecord.from_payload({"learned": True}), + confidence=0.9, + utility=0.8, + ) + system.integrate_knowledge(item) + snapshot = system.snapshot() + restored = SallyCognitiveSystem.from_snapshot(snapshot) + + assert restored.lifelong_knowledge == system.lifelong_knowledge + assert restored.state_payload() == system.state_payload() + + +def test_cli_cuc5_reports_integrated_capabilities(capsys: pytest.CaptureFixture[str]) -> None: + result = main(["--cuc5-experiment"]) + captured = capsys.readouterr() + payload = json.loads(captured.out) + + assert result == 0 + assert payload["demonstrated_count"] == 22 + assert payload["capabilities"]["recursive_cognitive_growth"] is True + assert payload["agi_certified"] is False + + +def test_system_recursive_bootstrap_integrates_discoveries_into_persistent_state() -> None: + system = SallyCognitiveSystem.create() + primitives = ( + ActionPrimitive("increment", lambda value: value + 1, cost=0.1), + ActionPrimitive("double", lambda value: value * 2, cost=0.2), + ActionPrimitive("negate", lambda value: -value, cost=0.2), + ) + second_training = ( + RepresentationObservation("s1", (1.0, 2.0), True), + RepresentationObservation("s2", (3.0, 4.0), True), + RepresentationObservation("s3", (-2.0, -1.0), True), + RepresentationObservation("s4", (1.0, 6.0), False), + RepresentationObservation("s5", (8.0, 2.0), False), + RepresentationObservation("s6", (-7.0, 0.0), False), + ) + second_holdout = ( + RepresentationObservation("sh1", (10.0, 11.0), True), + RepresentationObservation("sh2", (-4.0, -3.0), True), + RepresentationObservation("sh3", (10.0, 3.0), False), + RepresentationObservation("sh4", (-8.0, 2.0), False), + ) + report = system.run_recursive_bootstrap( + representation_training=_rep_training(), + representation_holdout=_rep_holdout(), + initial_state=2, + primitives=primitives, + known_states=(2, 3, 4, -2), + tool_validation_cases=(ToolValidationCase(1, 3), ToolValidationCase(3, 7)), + residuals=( + PredictionResidual("r1", 0.95, True, False, (9, 9)), + PredictionResidual("r2", 0.95, True, False, (9, 9)), + PredictionResidual("r3", 0.95, True, False, (9, 9)), + ), + second_representation_training=second_training, + second_representation_holdout=second_holdout, + max_goal_depth=3, + ) + + assert report.recursive_growth_demonstrated + assert system.lifelong_knowledge == report.knowledge_store + assert len(system.lifelong_knowledge.items) == 3 + restored = SallyCognitiveSystem.from_snapshot(system.snapshot()) + assert restored.lifelong_knowledge == system.lifelong_knowledge diff --git a/tests/cuc6/test_lifelong_generalization.py b/tests/cuc6/test_lifelong_generalization.py new file mode 100644 index 0000000..05ad4ab --- /dev/null +++ b/tests/cuc6/test_lifelong_generalization.py @@ -0,0 +1,225 @@ +"""Tests for v0.7 lifelong generalization and deeper representation invention.""" + +from __future__ import annotations + +from itertools import product + +import pytest + +from ix_sally.cognition import ( + ContextualKnowledgeEvidence, + GoalEvidence, + GoalPortfolioManager, + GoalSpec, + KnowledgeItem, + KnowledgeMaintenanceEngine, + LifelongKnowledgeStore, + OnlineMetaProfile, + RelationalTransferEngine, + RelationalWorld, + RelationEdge, + RepresentationObservation, + RepresentationProgramInventor, + SallyCognitiveSystem, + TextOutcomeGrounder, + TextOutcomeObservation, +) +from ix_sally.cognition.lifetime_learning import LifetimeChallenge, LifetimeLearningEngine +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern +from ix_sally.cuc6 import run_cuc6_experiment +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _deep(prefix: str, *, holdout: bool = False) -> tuple[RepresentationObservation, ...]: + ab = (-4.0, -3.0, 3.0, 4.0) if holdout else (-2.0, -1.0, 1.0, 2.0) + cs = (-20.0, -7.0, -2.0, 2.0, 7.0, 20.0) if holdout else (-6.0, -3.0, -1.0, 1.0, 3.0, 6.0) + return tuple( + RepresentationObservation( + f"{prefix}-{i}", + (a, b, c), + a * b + c >= 0.0, + ) + for i, (a, b, c) in enumerate(product(ab, ab, cs)) + ) + + +def test_cuc6_integrated_report_demonstrates_nine_release_claims() -> None: + report = run_cuc6_experiment() + + assert report.demonstrated_count == 9 + assert report.compositional_representation.program.depth >= 2 + assert report.compositional_representation.simple_baseline_accuracy < 1.0 + assert report.compositional_representation.validation_accuracy == 1.0 + assert report.lifetime_learning.later_learning_is_more_selective + assert report.to_payload()["agi_certified"] is False + + +def test_compositional_representation_invents_multi_operation_expression() -> None: + inventor = RepresentationProgramInventor() + learned = inventor.invent(observations=_deep("train"), max_depth=2, minimum_improvement=0.10) + validated = inventor.validate(learned, observations=_deep("holdout", holdout=True)) + + assert learned.program.depth == 2 + assert learned.program.expression() == "((x0*x1)+x2)" + assert learned.training_accuracy == 1.0 + assert learned.simple_baseline_accuracy <= 0.85 + assert validated.validation_accuracy == 1.0 + + +def test_compositional_representation_refuses_novelty_when_shallow_language_is_enough() -> None: + observations = tuple( + RepresentationObservation(str(i), (float(x), float(y)), x >= 0) + for i, (x, y) in enumerate(((-3, 8), (-2, -5), (2, 9), (3, -9))) + ) + with pytest.raises(FoundationError, match="materially improves"): + RepresentationProgramInventor().invent(observations=observations, minimum_improvement=0.10) + + +def test_lifelong_memory_splits_contextually_wrong_concept_instead_of_adding_exception() -> None: + store = LifelongKnowledgeStore().integrate( + KnowledgeItem( + "rule", + DigestRecord.from_payload({"rule": 1}), + confidence=0.8, + utility=0.8, + ) + ) + report = KnowledgeMaintenanceEngine().reconcile( + store, + evidence=( + ContextualKnowledgeEvidence("rule", "regime-a", True, True), + ContextualKnowledgeEvidence("rule", "regime-a", True, True), + ContextualKnowledgeEvidence("rule", "regime-b", True, False), + ContextualKnowledgeEvidence("rule", "regime-b", True, False), + ), + ) + + assert report.contradiction_resolved + assert report.split_concepts == ("rule",) + assert len(report.created_context_concepts) == 2 + original = next(item for item in report.store.items if item.concept_id == "rule") + assert original.superseded_by is not None + + +def test_relational_transfer_ignores_surface_names_and_relation_words() -> None: + source = RelationalWorld( + "factory", + ( + RelationEdge("sensor", "signals", "controller"), + RelationEdge("controller", "drives", "actuator"), + RelationEdge("backup", "feeds", "actuator"), + ), + ) + target = RelationalWorld( + "software", + ( + RelationEdge("request", "calls", "adapter"), + RelationEdge("adapter", "invokes", "renderer"), + RelationEdge("cache", "warms", "renderer"), + ), + ) + engine = RelationalTransferEngine() + schema = engine.learn(world=source, effective_node="controller") + inference = engine.transfer(schema, world=target) + + assert inference.structural_match + assert inference.inferred_node == "adapter" + + +def test_text_grounding_extracts_empirically_predictive_feature_from_raw_strings() -> None: + features = TextOutcomeGrounder().discover( + ( + TextOutcomeObservation("1", "quiet glint corridor", True), + TextOutcomeObservation("2", "glint signal appears", True), + TextOutcomeObservation("3", "flat corridor dark", False), + TextOutcomeObservation("4", "quiet matte signal", False), + TextOutcomeObservation("5", "glint matte corridor", True), + TextOutcomeObservation("6", "dark flat signal", False), + ) + ) + + assert features[0].token == "glint" + assert features[0].information_gain > 0.5 + + +def test_goal_portfolio_respects_dependency_and_kills_collapsed_premise() -> None: + base = GoalSpec.create( + goal_id="map", + description="Map world.", + desired_state=FactPattern.create( + subject="w", predicate="mapped", value=CognitiveValue.from_python(True) + ), + priority=0.9, + utility=0.9, + risk_limit=0.1, + ) + child = GoalSpec.create( + goal_id="solve", + description="Use map.", + desired_state=FactPattern.create( + subject="w", predicate="solved", value=CognitiveValue.from_python(True) + ), + priority=0.8, + utility=0.9, + risk_limit=0.1, + dependency_ids=("map",), + ) + stale = GoalSpec.create( + goal_id="stale", + description="No longer warranted.", + desired_state=FactPattern.create( + subject="w", predicate="old", value=CognitiveValue.from_python(True) + ), + priority=1.0, + utility=0.9, + risk_limit=0.1, + ) + decision = GoalPortfolioManager().allocate( + (base, child, stale), + evidence=( + GoalEvidence("map", 0.9, 0.9, 0.8), + GoalEvidence("solve", 0.9, 0.9, 0.2), + GoalEvidence("stale", 0.05, 0.9, 0.0), + ), + attention_budget=0.6, + per_goal_cost={"map": 0.3, "solve": 0.3, "stale": 0.3}, + ) + assert decision.selected_goal_ids == ("map", "solve") + assert decision.abandoned_goal_ids == ("stale",) + + +def test_online_meta_profile_persists_through_full_system_snapshot() -> None: + challenge = LifetimeChallenge("a", _deep("train"), _deep("holdout", holdout=True)) + system = SallyCognitiveSystem.create() + report = system.run_lifetime_learning( + challenges=(challenge, challenge, challenge), exploration_episodes=1 + ) + + assert report.later_learning_is_more_selective + assert len(system.online_meta_profile.experiences) >= 4 + restored = SallyCognitiveSystem.from_snapshot(system.snapshot()) + assert restored.online_meta_profile == system.online_meta_profile + assert restored.state_payload() == system.state_payload() + + +def test_later_sally_uses_prior_learning_to_reduce_strategy_search() -> None: + challenges = tuple( + LifetimeChallenge( + f"world-{i}", + _deep(f"train-{i}"), + _deep(f"holdout-{i}", holdout=True), + ) + for i in range(4) + ) + report = LifetimeLearningEngine().run_lifetime( + profile=OnlineMetaProfile(), + challenges=challenges, + exploration_episodes=1, + ) + + assert report.episodes[0].strategies_evaluated == 2 + assert all(item.strategies_evaluated == 1 for item in report.episodes[1:]) + assert all(item.validation_accuracy == 1.0 for item in report.episodes) + assert report.later_learning_is_more_selective diff --git a/tests/test_cli_baseline.py b/tests/test_cli_baseline.py index baa6e85..2e152fe 100644 --- a/tests/test_cli_baseline.py +++ b/tests/test_cli_baseline.py @@ -19,7 +19,7 @@ def test_cli_runtime_baseline_prints_stable_json(capsys: object) -> None: assert result == 0 payload = json.loads(captured.out) assert payload["package"] == "ix-sally" - assert payload["version"] == "0.1.0" + assert payload["version"] == "0.7.0" assert payload["baseline"] == "session-one" assert payload["session_one_complete"] is True assert payload["role_count"] == 9 diff --git a/tests/test_package_identity.py b/tests/test_package_identity.py index 0d11bd3..55cd23c 100644 --- a/tests/test_package_identity.py +++ b/tests/test_package_identity.py @@ -8,7 +8,7 @@ def test_package_exports_version() -> None: - assert ix_sally.__version__ == "0.1.0" + assert ix_sally.__version__ == "0.7.0" def test_cli_module_reports_package_identity() -> None: @@ -20,5 +20,5 @@ def test_cli_module_reports_package_identity() -> None: env=repository_subprocess_environment(), ) - assert completed.stdout == "IX-Sally 0.1.0\n" + assert completed.stdout == "IX-Sally 0.7.0\n" assert completed.stderr == "" diff --git a/tests/test_version.py b/tests/test_version.py index f65b8ec..d86f0df 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -53,7 +53,7 @@ def test_version_module_is_dependency_neutral() -> None: import sys from ix_sally.version import __version__ -assert __version__ == '0.1.0' +assert __version__ == '0.7.0' forbidden = { 'ix_sally.state',