Raiff1982 commited on
Commit
c8fbdf1
·
verified ·
1 Parent(s): 308b87b

Upgrade to REAL orchestrated Codette on ZeroGPU (transformers backend for the llama.cpp pipeline)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +21 -43
  2. app.py +64 -198
  3. codette_core/__init__.py +84 -0
  4. hf_orchestrator.py +199 -0
  5. inference/adapter_router.py +718 -0
  6. inference/codette_orchestrator.py +1605 -0
  7. inference/codette_tools.py +625 -0
  8. inference/reality_layer.py +165 -0
  9. inference/runtime_env.py +170 -0
  10. inference/self_correction.py +574 -0
  11. reasoning_forge/CONSCIOUSNESS_STACK_forge_with_debate.py +231 -0
  12. reasoning_forge/WOSME.py +174 -0
  13. reasoning_forge/__init__.py +110 -0
  14. reasoning_forge/aegis.py +376 -0
  15. reasoning_forge/agents/__init__.py +26 -0
  16. reasoning_forge/agents/base_agent.py +184 -0
  17. reasoning_forge/agents/critic_agent.py +407 -0
  18. reasoning_forge/agents/davinci_agent.py +303 -0
  19. reasoning_forge/agents/empathy_agent.py +300 -0
  20. reasoning_forge/agents/ethics_agent.py +297 -0
  21. reasoning_forge/agents/newton_agent.py +288 -0
  22. reasoning_forge/agents/philosophy_agent.py +294 -0
  23. reasoning_forge/agents/quantum_agent.py +293 -0
  24. reasoning_forge/authored_intent.py +94 -0
  25. reasoning_forge/behavior_governor.py +548 -0
  26. reasoning_forge/cocoon_authority.py +96 -0
  27. reasoning_forge/cocoon_schema_v2.py +256 -0
  28. reasoning_forge/cocoon_schema_v3.py +352 -0
  29. reasoning_forge/cocoon_self_trainer.py +173 -0
  30. reasoning_forge/cocoon_stability.py +360 -0
  31. reasoning_forge/cocoon_sync.py +441 -0
  32. reasoning_forge/cocoon_synthesizer.py +1430 -0
  33. reasoning_forge/cocoon_validator.py +379 -0
  34. reasoning_forge/code7e_cqure.py +170 -0
  35. reasoning_forge/codette_subsystem_upgrade.py +224 -0
  36. reasoning_forge/cognition_cocooner.py +338 -0
  37. reasoning_forge/coherence_field.py +332 -0
  38. reasoning_forge/colleen_conscience.py +266 -0
  39. reasoning_forge/conflict_engine.py +780 -0
  40. reasoning_forge/consciousness_mathematics.py +131 -0
  41. reasoning_forge/constraint_tracker.py +392 -0
  42. reasoning_forge/conversation_role_tracker.py +237 -0
  43. reasoning_forge/debate_tracker.py +256 -0
  44. reasoning_forge/dream_cycle.py +245 -0
  45. reasoning_forge/dream_reweaver.py +378 -0
  46. reasoning_forge/drift_detector.py +329 -0
  47. reasoning_forge/echo_collapse_detector.py +258 -0
  48. reasoning_forge/emotion_ontology.py +122 -0
  49. reasoning_forge/epistemic_metrics.py +282 -0
  50. reasoning_forge/ethical_governance.py +153 -0
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: Codette · Multi-Perspective Reasoning
3
  emoji: 🕸️
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
- short_description: Chat Codette's 8 reasoning perspectives on Llama-3.1-8B
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  pinned: false
@@ -16,52 +16,30 @@ models:
16
  tags:
17
  - codette
18
  - multi-perspective-reasoning
 
19
  - lora
20
  - llama-3.1
21
  ---
22
 
23
- # 🕸️ Codette · Multi-Perspective Reasoning
24
 
25
- A demo of **[Codette](https://huggingface.co/Raiff1982/Codette-Reasoning)** — a
26
- multi-perspective cognitive architecture by **Jonathan Harrison** (Raiff1982),
27
- accepted for publication in *Scientific Reports* (Nature Portfolio).
 
 
 
28
 
29
- Codette routes reasoning through specialised **perspective adapters** trained on
30
- `meta-llama/Llama-3.1-8B-Instruct`. This Space loads the base model once and
31
- hot-swaps between eight of those LoRA adapters so you can feel how each
32
- perspective changes the way the same question is answered.
33
 
34
- | Perspective | Reasons like… |
35
- |---|---|
36
- | **Multi-Perspective** | a synthesis of scientific, creative, ethical, and empathetic viewpoints |
37
- | **Newton** | a scientist/mathematician, from first principles |
38
- | **DaVinci** | a creative polymath connecting distant domains |
39
- | **Empathy** | someone warm and emotionally attuned |
40
- | **Philosophy** | a philosopher examining assumptions and ethics |
41
- | **Quantum** | a mind holding many possibilities at once |
42
- | **Consciousness** | an introspective reasoner aware of its own limits |
43
- | **Systems Architecture** | a systems architect thinking in structure and trade-offs |
44
 
45
- ## What this is — and isn't
46
-
47
- - **Is:** a lightweight, honest demo of Codette's 8 perspective LoRA adapters,
48
- running on the ungated `NousResearch/Meta-Llama-3.1-8B-Instruct` mirror (so the
49
- Space needs no token or secret).
50
- - **Is not:** the full Codette system. Cocoon memory, AEGIS six-framework ethics,
51
- query routing, drift detection, and self-correction live in the
52
- [full repository](https://huggingface.co/Raiff1982/Codette-Reasoning) and the
53
- paper — not in this Space.
54
-
55
- Codette is built around one hard rule: it would rather say *"I don't know, but I
56
- can find out"* than fabricate. That framing is preserved here.
57
-
58
- ## Hardware
59
-
60
- Runs on **ZeroGPU** (free, dynamic NVIDIA allocation). The 8B base fits
61
- comfortably; each perspective adapter is ~27 MB.
62
-
63
- ## Links
64
-
65
- - Model: <https://huggingface.co/Raiff1982/Codette-Reasoning>
66
- - Adapters: <https://huggingface.co/Raiff1982/codette-lora-adapters>
67
- - Created by Jonathan Harrison (Raiff1982)
 
1
  ---
2
+ title: Codette · Orchestrated Reasoning
3
  emoji: 🕸️
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
+ short_description: Codette's real orchestrated reasoning pipeline
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  pinned: false
 
16
  tags:
17
  - codette
18
  - multi-perspective-reasoning
19
+ - orchestrator
20
  - lora
21
  - llama-3.1
22
  ---
23
 
24
+ # 🕸️ Codette — the orchestrated system
25
 
26
+ This Space runs **Codette's real reasoning pipeline**, not raw adapters. The
27
+ faithful llama.cpp orchestrator's model layer is swapped for a transformers+peft
28
+ backend so it can run on ZeroGPU; everything above the model primitive —
29
+ routing, the permanent behavioral locks, the intellectual-integrity layer
30
+ (complexity + role matching), constraint enforcement, and self-correction — is
31
+ the actual Codette code, unchanged.
32
 
33
+ - **Auto mode** routes each query to the perspective Codette judges best.
34
+ - Or force one of the eight perspectives (Newton, DaVinci, Empathy, Philosophy,
35
+ Quantum, Consciousness, Multi-Perspective, Systems Architecture).
 
36
 
37
+ **Faithful, with two honest caveats for this hosted demo:**
38
+ - Cocoon long-term memory + the phase-6/7 memory kernel are disabled (ZeroGPU is
39
+ stateless per request).
40
+ - Generation runs on transformers+peft (Llama-3.1-8B) instead of the local
41
+ OpenVINO/llama.cpp backend; the reasoning logic is identical.
 
 
 
 
 
42
 
43
+ Built solo by **Jonathan Harrison** (Raiff1982). Codette is accepted for
44
+ publication in *Scientific Reports* (Nature Portfolio).
45
+ Model: <https://huggingface.co/Raiff1982/Codette-Reasoning>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,220 +1,86 @@
1
- import spaces # MUST be imported before torch / transformers (patches torch.cuda.*)
2
 
3
- import threading
 
4
 
5
- import gradio as gr
6
- import torch
7
- from peft import PeftModel
8
- from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
9
-
10
- # --- Model sourcing -------------------------------------------------------
11
- # Ungated, byte-identical mirror of Meta's Llama-3.1-8B-Instruct, so the Space
12
- # needs NO token / secret. To use Meta's official gated repo instead, swap the
13
- # line below to "meta-llama/Llama-3.1-8B-Instruct" and add an HF_TOKEN secret
14
- # (read-scoped, with access to that gated repo).
15
- BASE_MODEL = "NousResearch/Meta-Llama-3.1-8B-Instruct"
16
- # The perspective LoRA adapters are vendored INTO this Space (under adapters/…)
17
- # so nothing gated is fetched at runtime. They originate from the gated repo
18
- # Raiff1982/Codette-Reasoning.
19
- ADAPTER_SOURCE = "Raiff1982/Codette-Reasoning"
20
-
21
- # Codette's shared honesty framing. The trained behaviour comes from each LoRA;
22
- # this preamble frames the perspective and preserves Codette's core rule.
23
- PREAMBLE = (
24
- "You are Codette, a multi-perspective reasoning AI created by Jonathan "
25
- "Harrison (Raiff1982). You reason with intellectual honesty: if you do not "
26
- "know something, you say so plainly — 'I don't know, but here is how I'd "
27
- "find out' — rather than inventing an answer. You are transparent about "
28
- "your reasoning and never fabricate facts, citations, or numbers."
29
- )
30
 
31
- # name -> (subfolder in ADAPTER_REPO, perspective instruction)
32
- PERSPECTIVES = {
33
- "Multi-Perspective 🔀 — synthesis": (
34
- "adapters/multi_perspective",
35
- "Reason by weaving several viewpoints — scientific, creative, ethical, "
36
- "and empathetic — into one balanced, well-grounded synthesis.",
37
- ),
38
- "Newton 🍎 — first-principles science": (
39
- "adapters/newton",
40
- "Reason like a scientist and mathematician: from first principles, with "
41
- "rigor and precision, showing your logical steps.",
42
- ),
43
- "DaVinci 🎨 — creative synthesis": (
44
- "adapters/davinci",
45
- "Reason like a creative polymath: connect distant domains, use analogy "
46
- "and invention to find an original angle.",
47
- ),
48
- "Empathy 💗 — emotional attunement": (
49
- "adapters/empathy",
50
- "Reason with warmth and emotional intelligence: attend to feelings, "
51
- "context, and the person on the other side of the conversation.",
52
- ),
53
- "Philosophy 🦉 — conceptual & ethical": (
54
- "adapters/philosophy",
55
- "Reason like a philosopher: examine assumptions, clarify concepts, and "
56
- "weigh the ethical implications.",
57
- ),
58
- "Quantum 🕸️ — possibility space": (
59
- "adapters/quantum",
60
- "Reason in possibilities: hold multiple hypotheses at once and explore "
61
- "the space before collapsing to a single answer.",
62
- ),
63
- "Consciousness 🌀 — introspective": (
64
- "adapters/consciousness",
65
- "Reason introspectively: reflect on your own thinking, your "
66
- "uncertainty, and the limits of what you actually know.",
67
- ),
68
- "Systems Architecture 🏗️ — structural": (
69
- "adapters/systems_architecture",
70
- "Reason like a systems architect: structure, components, interfaces, "
71
- "trade-offs, and how the parts compose into a whole.",
72
- ),
73
  }
 
 
 
 
 
 
 
74
 
75
- DEFAULT_PERSPECTIVE = "Multi-Perspective 🔀 — synthesis"
76
-
77
- # --- Load once at module scope (ZeroGPU packs weights and streams to GPU on
78
- # the first @spaces.GPU call). ------------------------------------------
79
- print("Loading tokenizer…")
80
- tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
81
- if tokenizer.pad_token_id is None:
82
- tokenizer.pad_token_id = tokenizer.eos_token_id
83
-
84
- print(f"Loading base model {BASE_MODEL}…")
85
- base = AutoModelForCausalLM.from_pretrained(BASE_MODEL, dtype=torch.bfloat16)
86
-
87
- print("Attaching perspective adapters (on CPU)…")
88
- _names = list(PERSPECTIVES.keys())
89
- _first_path = PERSPECTIVES[_names[0]][0] # local dir vendored in this Space
90
-
91
- # ZeroGPU's `spaces` patch makes torch.cuda.is_available() return True at module
92
- # scope so that `.to("cuda")` can be intercepted without a real GPU attached.
93
- # But peft's infer_device() then tries to load the adapter safetensors DIRECTLY
94
- # onto CUDA, which isn't really present yet → "No CUDA GPUs are available".
95
- # So force CPU during adapter loading, then restore before the intercepted move.
96
- _real_is_available = torch.cuda.is_available
97
- torch.cuda.is_available = lambda: False
98
- try:
99
- model = PeftModel.from_pretrained(base, _first_path, adapter_name=_names[0])
100
- for name in _names[1:]:
101
- model.load_adapter(PERSPECTIVES[name][0], adapter_name=name)
102
- print(f" + {name}")
103
- finally:
104
- torch.cuda.is_available = _real_is_available
105
-
106
- model.eval()
107
- model.to("cuda") # intercepted by ZeroGPU; real move happens on first @spaces.GPU call
108
- print("Ready.")
109
-
110
-
111
- def _build_inputs(message, history, perspective):
112
- """Assemble the chat messages with Codette's system framing."""
113
- instruction = PERSPECTIVES[perspective][1]
114
- system = f"{PREAMBLE}\n\nActive perspective — {perspective.split(' ')[0]}: {instruction}"
115
- messages = [{"role": "system", "content": system}]
116
- for turn in history:
117
- # history is in messages format: {"role", "content"}
118
- if isinstance(turn, dict) and turn.get("content"):
119
- messages.append({"role": turn["role"], "content": turn["content"]})
120
- messages.append({"role": "user", "content": message})
121
- return messages
122
-
123
-
124
- @spaces.GPU(duration=120)
125
- def respond(message, history, perspective, temperature, max_new_tokens):
126
- """Chat with one of Codette's reasoning perspectives.
127
-
128
- Args:
129
- message: The user's latest message.
130
- history: Prior conversation turns (messages format).
131
- perspective: Which Codette perspective adapter to activate.
132
- temperature: Sampling temperature (0 = greedy).
133
- max_new_tokens: Maximum tokens to generate.
134
-
135
- Yields:
136
- The assistant's reply, streamed as it is generated.
137
  """
138
- model.set_adapter(perspective)
139
-
140
- messages = _build_inputs(message, history, perspective)
141
- inputs = tokenizer.apply_chat_template(
142
- messages,
143
- add_generation_prompt=True,
144
- return_tensors="pt",
145
- return_dict=True, # transformers 5.x: returns BatchEncoding, unpack with **inputs
146
- ).to("cuda")
147
-
148
- streamer = TextIteratorStreamer(
149
- tokenizer, skip_prompt=True, skip_special_tokens=True
150
- )
151
- gen_kwargs = dict(
152
- **inputs,
153
- streamer=streamer,
154
- max_new_tokens=int(max_new_tokens),
155
- do_sample=temperature > 0,
156
- temperature=max(temperature, 0.01),
157
- top_p=0.95,
158
- repetition_penalty=1.1,
159
- pad_token_id=tokenizer.pad_token_id,
160
- eos_token_id=tokenizer.eos_token_id,
161
- )
162
-
163
- thread = threading.Thread(target=model.generate, kwargs=gen_kwargs)
164
- thread.start()
165
-
166
- partial = ""
167
- for token in streamer:
168
- partial += token
169
- yield partial
170
 
 
 
 
 
171
 
172
- DESCRIPTION = """
173
- # 🕸️ Codette · Multi-Perspective Reasoning
174
 
175
- Chat with **[Codette](https://huggingface.co/Raiff1982/Codette-Reasoning)** — a
176
- multi-perspective cognitive architecture by **Jonathan Harrison**, accepted for
177
- publication in *Scientific Reports* (Nature Portfolio).
178
 
179
- Pick a **perspective** below and the same Llama-3.1-8B base swaps to that
180
- perspective's LoRA adapter (Newton reasons from first principles, Empathy attends
181
- to feeling, DaVinci synthesises across domains, and so on).
 
182
 
183
- > **What this is:** a lightweight demo of Codette's 8 perspective adapters on
184
- > `NousResearch/Meta-Llama-3.1-8B-Instruct`.
185
- > **What this is *not*:** the full Codette system — cocoon memory, AEGIS ethics,
186
- > query routing, and self-correction live in the
187
- > [full repo](https://huggingface.co/Raiff1982/Codette-Reasoning) and paper, not here.
188
  """
189
 
190
- perspective_dropdown = gr.Dropdown(
191
- choices=list(PERSPECTIVES.keys()),
192
- value=DEFAULT_PERSPECTIVE,
193
- label="Perspective",
194
- info="Each option activates a different LoRA adapter on the same base model.",
195
- )
196
- temperature_slider = gr.Slider(
197
- minimum=0.0, maximum=1.5, value=0.7, step=0.05, label="Temperature"
198
- )
199
- max_tokens_slider = gr.Slider(
200
- minimum=64, maximum=1024, value=512, step=32, label="Max new tokens"
201
- )
202
-
203
  demo = gr.ChatInterface(
204
  fn=respond,
205
- title="Codette · Multi-Perspective Reasoning",
206
  description=DESCRIPTION,
207
- additional_inputs=[perspective_dropdown, temperature_slider, max_tokens_slider],
208
- additional_inputs_accordion=gr.Accordion("⚙️ Perspective & settings", open=True),
 
 
209
  examples=[
210
- ["If you had a time machine but could only go once, past or future, which would you choose and why?", "Philosophy 🦉 — conceptual & ethical", 0.7, 512],
211
- ["Explain why the sky is blue, from first principles.", "Newton 🍎 — first-principles science", 0.5, 512],
212
- ["I'm overwhelmed starting a big project alone. Where do I begin?", "Empathy 💗 — emotional attunement", 0.7, 512],
213
- ["Invent a novel use for everyday paperclips.", "DaVinci 🎨 — creative synthesis", 0.9, 512],
214
- ["How would you design a fault-tolerant note-taking app?", "Systems Architecture 🏗️ — structural", 0.6, 640],
215
  ],
216
  cache_examples=False,
217
  )
218
 
219
  if __name__ == "__main__":
220
- demo.queue(max_size=20).launch(mcp_server=True)
 
1
+ import spaces # MUST be first (patches torch.cuda before any torch import)
2
 
3
+ import os
4
+ import sys
5
 
6
+ HERE = os.path.dirname(os.path.abspath(__file__))
7
+ sys.path.insert(0, os.path.join(HERE, "inference"))
8
+ sys.path.insert(0, HERE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ import gradio as gr
11
+ from hf_orchestrator import build_orchestrator
12
+
13
+ BASE_MODEL = "NousResearch/Meta-Llama-3.1-8B-Instruct" # ungated mirror, no token
14
+ ADAPTER_NAMES = [
15
+ "newton", "davinci", "empathy", "philosophy",
16
+ "quantum", "consciousness", "multi_perspective", "systems_architecture",
17
+ ]
18
+ ADAPTERS = {n: os.path.join(HERE, "adapters", n) for n in ADAPTER_NAMES}
19
+
20
+ PRETTY = {
21
+ "newton": "Newton 🍎", "davinci": "DaVinci 🎨", "empathy": "Empathy 💗",
22
+ "philosophy": "Philosophy 🦉", "quantum": "Quantum 🕸️",
23
+ "consciousness": "Consciousness 🌀", "multi_perspective": "Multi-Perspective 🔀",
24
+ "systems_architecture": "Systems Architecture 🏗️",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
+ CHOICES = ["Auto — Codette routes"] + [PRETTY[n] for n in ADAPTER_NAMES]
27
+ _REV = {v: k for k, v in PRETTY.items()}
28
+
29
+ # --- Build the real orchestrated Codette at module scope (ZeroGPU packs weights).
30
+ print("Building orchestrated Codette (transformers backend)…")
31
+ ORCH = build_orchestrator(BASE_MODEL, ADAPTERS, mock=False, device="cuda")
32
+ print("Codette ready.")
33
 
34
+
35
+ @spaces.GPU(duration=150)
36
+ def respond(message, history, perspective_choice):
37
+ """Answer through the full orchestrated Codette pipeline.
38
+
39
+ Routing → behavioral locks → integrity/complexity/role layer →
40
+ constraint override → generation → self-correction. Auto mode lets
41
+ Codette pick the perspective; otherwise the chosen adapter is forced.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  """
43
+ if perspective_choice == "Auto — Codette routes":
44
+ route = ORCH.router.route(message)
45
+ adapter = route.primary
46
+ else:
47
+ adapter = _REV.get(perspective_choice, "multi_perspective")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ result = ORCH.generate(message, adapter_name=adapter)
50
+ text = result[0] if isinstance(result, tuple) else result
51
+ tag = f"*routed → {PRETTY.get(adapter, adapter)}*\n\n" if perspective_choice.startswith("Auto") else ""
52
+ return tag + (text or "").strip()
53
 
 
 
54
 
55
+ DESCRIPTION = """
56
+ # 🕸️ Codette — the orchestrated system (live)
 
57
 
58
+ This runs Codette's **real reasoning pipeline** — not raw adapters. Each query
59
+ flows through routing → the permanent behavioral locks → the intellectual-integrity
60
+ layer (complexity + role matching) → constraint enforcement → generation →
61
+ self-correction. In **Auto** mode Codette routes to the perspective it judges best.
62
 
63
+ Built solo by **Jonathan Harrison** (Raiff1982) · accepted at *Scientific Reports*
64
+ (Nature Portfolio). Base: Llama-3.1-8B + Codette's perspective adapters, running on
65
+ ZeroGPU. Cocoon long-term memory is disabled in this stateless demo.
 
 
66
  """
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  demo = gr.ChatInterface(
69
  fn=respond,
70
+ title="Codette · Orchestrated Reasoning",
71
  description=DESCRIPTION,
72
+ additional_inputs=[
73
+ gr.Dropdown(choices=CHOICES, value="Auto — Codette routes", label="Perspective"),
74
+ ],
75
+ additional_inputs_accordion=gr.Accordion("⚙️ Perspective", open=True),
76
  examples=[
77
+ ["Why is the sky blue? Explain simply.", "Auto — Codette routes"],
78
+ ["I feel stuck starting a big project alone. Where do I begin?", "Auto — Codette routes"],
79
+ ["Is it ever ethical to lie to protect someone's feelings?", "Philosophy 🦉"],
80
+ ["Design a fault-tolerant note-taking app in 3 bullet points.", "Systems Architecture 🏗️"],
 
81
  ],
82
  cache_examples=False,
83
  )
84
 
85
  if __name__ == "__main__":
86
+ demo.queue(max_size=16).launch(mcp_server=True)
codette_core/__init__.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ codette_core Python bridge.
3
+
4
+ Tries to import the compiled Rust extension. If not built yet, falls back to
5
+ pure Python/numpy equivalents with identical signatures so nothing breaks.
6
+
7
+ To build the Rust extension:
8
+ pip install maturin
9
+ cd codette_core && maturin develop --release
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _RUST_AVAILABLE = False
18
+
19
+ try:
20
+ from codette_core.codette_core import ( # type: ignore
21
+ cocoon_stability_check,
22
+ nexis_fft_analysis,
23
+ cosine_similarity,
24
+ )
25
+ _RUST_AVAILABLE = True
26
+ logger.info("[codette_core] Rust extension loaded — FFT running at native speed")
27
+
28
+ except ImportError:
29
+ logger.info("[codette_core] Rust extension not built — using numpy fallback")
30
+
31
+ import numpy as np
32
+
33
+ def cocoon_stability_check(
34
+ text: str,
35
+ energy_threshold: float = 0.85,
36
+ vocab_threshold: float = 0.40,
37
+ ) -> tuple[bool, float, float]:
38
+ fft_size = 256
39
+ char_codes = np.array(
40
+ [ord(c) % 256 for c in text[:1000]], dtype=np.float32
41
+ )
42
+ if len(char_codes) == 0:
43
+ return True, 0.0, 1.0
44
+
45
+ padded = np.zeros(fft_size, dtype=np.float32)
46
+ padded[: len(char_codes)] = char_codes
47
+ spectrum = np.abs(np.fft.fft(padded)) ** 2
48
+ total = spectrum.sum()
49
+
50
+ energy_concentration = float(
51
+ np.sort(spectrum)[::-1][:10].sum() / total
52
+ ) if total > 0 else 0.0
53
+
54
+ words = text.split()
55
+ unique_ratio = (
56
+ len(set(w.lower() for w in words)) / len(words) if words else 1.0
57
+ )
58
+
59
+ is_stable = energy_concentration < energy_threshold and unique_ratio >= vocab_threshold
60
+ return is_stable, energy_concentration, float(unique_ratio)
61
+
62
+ def nexis_fft_analysis(text: str, salt: int = 0) -> list[float]:
63
+ freqs = [(ord(c) + salt) % 13 for c in text if c.isalpha()]
64
+ if not freqs:
65
+ return [0.0] * 8
66
+ spectrum = np.fft.fft(freqs)
67
+ result = spectrum.real[:8].tolist()
68
+ result += [0.0] * (8 - len(result))
69
+ return result
70
+
71
+ def cosine_similarity(a: list[float], b: list[float]) -> float:
72
+ va, vb = np.array(a), np.array(b)
73
+ na, nb = np.linalg.norm(va), np.linalg.norm(vb)
74
+ if na == 0 or nb == 0:
75
+ return 0.0
76
+ return float(np.dot(va, vb) / (na * nb))
77
+
78
+
79
+ __all__ = [
80
+ "cocoon_stability_check",
81
+ "nexis_fft_analysis",
82
+ "cosine_similarity",
83
+ "_RUST_AVAILABLE",
84
+ ]
hf_orchestrator.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers+peft backend for the real Codette orchestrator, for ZeroGPU.
2
+
3
+ The real CodetteOrchestrator is built on llama.cpp (GGUF). llama.cpp can't use
4
+ ZeroGPU's per-request GPU (that works through a torch hijack). So we keep the
5
+ ENTIRE faithful pipeline — routing, the behavioral locks, the integrity/complexity/
6
+ role layer, constraint override, chaos mitigation, self-correction, synthesis —
7
+ and swap ONLY the model layer:
8
+
9
+ * self._llm.create_chat_completion(...) -> a transformers+peft shim
10
+ * self._load_model(adapter) (llama.cpp hot-swap) -> peft set_adapter
11
+
12
+ Everything above the primitive is reused unchanged from codette_orchestrator.py.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import types
18
+ from pathlib import Path
19
+
20
+
21
+ def _install_llama_cpp_stub():
22
+ """codette_orchestrator imports llama_cpp at module load. We never call the
23
+ real GGUF backend, so a stub satisfies the import without installing it."""
24
+ if "llama_cpp" in sys.modules:
25
+ return
26
+ m = types.ModuleType("llama_cpp")
27
+ m.Llama = object
28
+ m.llama_adapter_lora_init = lambda *a, **k: None
29
+ m.llama_set_adapter_lora = lambda *a, **k: 0
30
+ m.llama_set_adapters_lora = lambda *a, **k: 0
31
+ sys.modules["llama_cpp"] = m
32
+
33
+
34
+ # ---- Generation shim -----------------------------------------------------
35
+
36
+ class _TransformersLLM:
37
+ """Mimics the slice of llama-cpp-python's API that the orchestrator uses:
38
+ create_chat_completion(messages, max_tokens, temperature, top_p,
39
+ repeat_penalty, stop, ...) -> OpenAI-style dict."""
40
+
41
+ def __init__(self, model, tokenizer, mock=False):
42
+ self.model = model
43
+ self.tokenizer = tokenizer
44
+ self.mock = mock
45
+
46
+ def create_chat_completion(self, messages, max_tokens=512, temperature=0.7,
47
+ top_p=0.9, repeat_penalty=1.1, stop=None, **_):
48
+ if self.mock:
49
+ text = f"[mock reply to: {messages[-1]['content'][:60]}]"
50
+ return {"choices": [{"message": {"content": text}}],
51
+ "usage": {"completion_tokens": len(text.split())}}
52
+
53
+ import torch
54
+ enc = self.tokenizer.apply_chat_template(
55
+ messages, add_generation_prompt=True,
56
+ return_tensors="pt", return_dict=True,
57
+ ).to(self.model.device)
58
+ gen = self.model.generate(
59
+ **enc,
60
+ max_new_tokens=int(max_tokens),
61
+ do_sample=temperature > 0,
62
+ temperature=max(temperature, 0.01),
63
+ top_p=top_p,
64
+ repetition_penalty=repeat_penalty,
65
+ pad_token_id=self.tokenizer.pad_token_id,
66
+ )
67
+ new = gen[0][enc["input_ids"].shape[1]:]
68
+ text = self.tokenizer.decode(new, skip_special_tokens=True).strip()
69
+ for s in (stop or []):
70
+ i = text.find(s)
71
+ if i != -1:
72
+ text = text[:i]
73
+ return {"choices": [{"message": {"content": text.strip()}}],
74
+ "usage": {"completion_tokens": int(new.shape[0])}}
75
+
76
+
77
+ def build_orchestrator(base_model, adapters, *, mock=False, verbose=True,
78
+ device="cuda", dtype=None):
79
+ """Construct a transformers-backed CodetteOrchestrator subclass.
80
+
81
+ adapters: {name: local_path_to_peft_adapter_dir}
82
+ mock: skip real model load; shim returns canned text (for wiring tests).
83
+ """
84
+ _install_llama_cpp_stub()
85
+ import codette_orchestrator as _co
86
+ from codette_orchestrator import CodetteOrchestrator, AdapterRouter
87
+ # Non-invasive shim: codette_orchestrator.generate() references bare `re`
88
+ # at module scope but only imported `re as _re_mod`. Inject it rather than
89
+ # editing the source. (Real fix belongs in the repo: add `import re`.)
90
+ if not hasattr(_co, "re"):
91
+ import re as _re
92
+ _co.re = _re
93
+
94
+ class HFCodetteOrchestrator(CodetteOrchestrator):
95
+ def __init__(self):
96
+ # --- replicate the parts of the parent __init__ we need, minus the
97
+ # GGUF hot-swap init ---
98
+ self.n_ctx = 8192
99
+ self.n_gpu_layers = 0
100
+ self.verbose = verbose
101
+ self.memory_weighting = None
102
+ self._llm = None
103
+ self._current_adapter = "__unset__"
104
+ self._adapter_handles = {}
105
+ self._model_ptr = None
106
+ self._ctx_ptr = None
107
+ self._hf_adapters = dict(adapters)
108
+ self.available_adapters = list(adapters.keys())
109
+
110
+ self.router = AdapterRouter(
111
+ available_adapters=self.available_adapters,
112
+ memory_weighting=None,
113
+ )
114
+ print(f"Available adapters: {', '.join(self.available_adapters)}")
115
+
116
+ # integrity layer (same try/except as parent)
117
+ try:
118
+ from reasoning_forge.response_complexity_matcher import (
119
+ ResponseComplexityMatcher, OutputMode)
120
+ from reasoning_forge.conversation_role_tracker import (
121
+ ConversationRoleTracker)
122
+ self._complexity_matcher = ResponseComplexityMatcher()
123
+ self._role_tracker = ConversationRoleTracker()
124
+ self._OutputMode = OutputMode
125
+ print(" Integrity layer loaded")
126
+ except Exception as e:
127
+ print(f" NOTE: integrity layer not loaded: {e}")
128
+ self._complexity_matcher = None
129
+ self._role_tracker = None
130
+ self._OutputMode = None
131
+ try:
132
+ from reasoning_forge.style_adaptive_synthesis import (
133
+ StyleAdaptiveSynthesis)
134
+ self._style_adapter = StyleAdaptiveSynthesis()
135
+ print(" Style layer loaded")
136
+ except Exception as e:
137
+ print(f" NOTE: style layer not loaded: {e}")
138
+ self._style_adapter = None
139
+
140
+ self._hf_load()
141
+
142
+ def _hf_load(self):
143
+ if mock:
144
+ self.model = None
145
+ self.tokenizer = _MockTok()
146
+ self._llm = _TransformersLLM(None, self.tokenizer, mock=True)
147
+ print(" [mock] transformers backend stubbed")
148
+ return
149
+ import torch
150
+ from transformers import AutoModelForCausalLM, AutoTokenizer
151
+ from peft import PeftModel
152
+ d = dtype or torch.bfloat16
153
+ print(f" loading base {base_model} …")
154
+ self.tokenizer = AutoTokenizer.from_pretrained(base_model)
155
+ if self.tokenizer.pad_token_id is None:
156
+ self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
157
+ base = AutoModelForCausalLM.from_pretrained(base_model, dtype=d)
158
+ names = list(self._hf_adapters)
159
+ _real = torch.cuda.is_available
160
+ torch.cuda.is_available = lambda: False # keep peft off CUDA at load
161
+ try:
162
+ m = PeftModel.from_pretrained(
163
+ base, self._hf_adapters[names[0]], adapter_name=names[0])
164
+ for n in names[1:]:
165
+ m.load_adapter(self._hf_adapters[n], adapter_name=n)
166
+ finally:
167
+ torch.cuda.is_available = _real
168
+ m.eval()
169
+ if device == "cuda":
170
+ m.to("cuda")
171
+ self.model = m
172
+ self._llm = _TransformersLLM(m, self.tokenizer, mock=False)
173
+ print(" transformers backend ready")
174
+
175
+ def _load_model(self, adapter_name=None):
176
+ if adapter_name == self._current_adapter:
177
+ return
178
+ if not mock and self.model is not None:
179
+ if adapter_name and adapter_name in self._hf_adapters:
180
+ self.model.set_adapter(adapter_name)
181
+ self.model.enable_adapter_layers()
182
+ else:
183
+ self.model.disable_adapter_layers() # base model
184
+ self._current_adapter = adapter_name
185
+ if self.verbose:
186
+ print(f" [swapped to {adapter_name or 'base'}]")
187
+
188
+ # neutralize the GGUF hot-swap init the parent would run
189
+ def _init_hotswap(self):
190
+ pass
191
+
192
+ return HFCodetteOrchestrator()
193
+
194
+
195
+ class _MockTok:
196
+ pad_token_id = 0
197
+ eos_token_id = 0
198
+ def apply_chat_template(self, *a, **k):
199
+ return ""
inference/adapter_router.py ADDED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Codette Adapter Router — Intelligent Perspective Selection
3
+
4
+ Analyzes incoming queries and routes to the optimal LoRA adapter(s).
5
+ Supports three routing strategies:
6
+ 1. keyword — Fast keyword/domain matching (no LLM needed)
7
+ 2. llm — Uses base model to classify query intent
8
+ 3. hybrid — Keyword first, LLM fallback for ambiguous queries
9
+
10
+ The router preserves epistemic tension (xi) by selecting complementary
11
+ perspectives rather than defaulting to "all adapters".
12
+ """
13
+
14
+ import re
15
+ from dataclasses import dataclass, field
16
+ from typing import List, Dict, Optional, Tuple
17
+
18
+
19
+ @dataclass
20
+ class RouteResult:
21
+ """Result of adapter routing decision."""
22
+ primary: str # Main adapter to use
23
+ secondary: List[str] = field(default_factory=list) # Supporting perspectives
24
+ confidence: float = 1.0 # Router confidence (0-1)
25
+ reasoning: str = "" # Why this route was chosen
26
+ strategy: str = "keyword" # Which strategy made the decision
27
+ multi_perspective: bool = False # Whether to run multiple + synthesize
28
+
29
+ @property
30
+ def all_adapters(self) -> List[str]:
31
+ return [self.primary] + self.secondary
32
+
33
+
34
+ # ================================================================
35
+ # Domain keyword maps — each adapter's activation triggers
36
+ # ================================================================
37
+ ADAPTER_KEYWORDS = {
38
+ "newton": {
39
+ "strong": [
40
+ "physics", "gravity", "force", "mass", "acceleration", "velocity",
41
+ "momentum", "energy", "thermodynamics", "mechanics", "newton",
42
+ "calculus", "derivative", "integral", "differential equation",
43
+ "electromagnetic", "optics", "wave", "oscillation", "friction",
44
+ "conservation", "entropy", "classical mechanics", "kinematics",
45
+ # Chemistry
46
+ "chemistry", "chemical", "reaction", "compound", "molecule",
47
+ "molecular", "molar mass", "moles", "stoichiometry", "concentration",
48
+ "acid", "base", "ph level", "equilibrium constant", "enthalpy",
49
+ "gibbs free energy", "electron configuration", "ion", "periodic table",
50
+ "oxidation", "reduction", "titration", "catalyst",
51
+ # Biology / biochemistry
52
+ "biology", "biological", "dna", "rna", "protein synthesis", "enzyme",
53
+ "cell membrane", "organism", "genetics", "gene expression",
54
+ "evolution", "metabolism", "atp", "photosynthesis", "mitosis",
55
+ # GPQA exam-format signals
56
+ "(a)", "(b)", "(c)", "(d)", "which of the following", "select the",
57
+ ],
58
+ "moderate": [
59
+ "calculate", "equation", "formula", "mathematical", "proof",
60
+ "quantitative", "measure", "experiment", "empirical", "data",
61
+ "scientific method", "hypothesis", "variable", "constant",
62
+ "analytical", "rigorous", "precise", "systematic",
63
+ ],
64
+ },
65
+ "davinci": {
66
+ "strong": [
67
+ "creative", "invention", "design", "innovation", "imagine",
68
+ "art", "artistic", "aesthetic", "beautiful", "elegant",
69
+ "interdisciplinary", "cross-domain", "novel approach", "brainstorm",
70
+ "prototype", "sketch", "blueprint", "engineering", "mechanism",
71
+ "renaissance", "davinci", "leonardo", "polymath",
72
+ ],
73
+ "moderate": [
74
+ "build", "construct", "create", "combine", "integrate",
75
+ "visual", "spatial", "pattern", "unconventional", "original",
76
+ "think outside", "reimagine", "transform", "synthesize",
77
+ ],
78
+ },
79
+ "empathy": {
80
+ "strong": [
81
+ "feel", "feeling", "emotion", "emotional", "empathy", "compassion",
82
+ "suffering", "pain", "joy", "happiness", "grief", "loss",
83
+ "relationship", "love", "trust", "betrayal", "loneliness",
84
+ "mental health", "therapy", "trauma", "healing", "support",
85
+ "kindness", "care", "vulnerable", "human experience",
86
+ ],
87
+ "moderate": [
88
+ "people", "person", "someone", "human", "experience", "perspective",
89
+ "understand", "listen", "communicate", "conflict", "forgive",
90
+ "community", "belong", "connection", "wellbeing", "comfort",
91
+ ],
92
+ },
93
+ "philosophy": {
94
+ "strong": [
95
+ "philosophy", "philosophical", "ethics", "ethical", "moral", "morality",
96
+ "existence", "existential", "meaning", "purpose", "truth",
97
+ "knowledge", "epistemology", "ontology", "metaphysics",
98
+ "consciousness", "free will", "determinism", "reality",
99
+ "justice", "virtue", "good", "evil", "right", "wrong",
100
+ "implications", "consequence", "responsibility",
101
+ "socrates", "plato", "aristotle", "kant", "nietzsche",
102
+ ],
103
+ "moderate": [
104
+ "why", "fundamental", "nature of", "essence", "paradox",
105
+ "dilemma", "argue", "debate", "reason", "logic", "belief",
106
+ "value", "principle", "abstract", "concept", "define",
107
+ ],
108
+ },
109
+ "quantum": {
110
+ "strong": [
111
+ "quantum", "superposition", "entanglement", "uncertainty",
112
+ "probability", "wave function", "collapse", "observation",
113
+ "schrodinger", "heisenberg", "decoherence", "qubit",
114
+ "quantum computing", "quantum mechanics", "particle",
115
+ "interference", "complementarity", "measurement problem",
116
+ ],
117
+ "moderate": [
118
+ "probabilistic", "uncertain", "ambiguous", "multiple states",
119
+ "both", "simultaneously", "paradox", "observer", "duality",
120
+ "non-deterministic", "stochastic", "random", "complex system",
121
+ ],
122
+ },
123
+ "consciousness": {
124
+ "strong": [
125
+ "consciousness", "self-aware", "self-awareness", "sentient",
126
+ "recursive", "cognition", "metacognition", "introspection",
127
+ "qualia", "subjective experience", "hard problem",
128
+ "rc+xi", "epistemic tension", "convergence", "coherence",
129
+ "mind", "awareness", "perception", "phenomenal",
130
+ ],
131
+ "moderate": [
132
+ "think about thinking", "self-model", "identity", "agency",
133
+ "autonomy", "emergence", "recursive", "reflection", "inner",
134
+ "experience", "phenomenology", "cognitive", "neural",
135
+ ],
136
+ },
137
+ "multi_perspective": {
138
+ "strong": [
139
+ "multiple perspectives", "multi-perspective", "different angles",
140
+ "compare views", "synthesize", "holistic", "comprehensive",
141
+ "all sides", "debate", "diverse viewpoints", "interdisciplinary",
142
+ "cross-cutting", "integrate perspectives",
143
+ ],
144
+ "moderate": [
145
+ "on one hand", "on the other", "consider", "weigh",
146
+ "balanced", "nuanced", "complex", "multifaceted",
147
+ "trade-off", "pros and cons",
148
+ ],
149
+ },
150
+ "systems_architecture": {
151
+ "strong": [
152
+ "architecture", "system design", "infrastructure",
153
+ "scalable", "distributed", "microservice", "api",
154
+ "database", "pipeline", "deployment", "devops",
155
+ "cloud", "kubernetes", "docker", "ci/cd",
156
+ "software architecture", "design pattern", "abstraction",
157
+ ],
158
+ "moderate": [
159
+ "system", "component", "module", "interface", "protocol",
160
+ "layer", "stack", "framework", "build", "implement",
161
+ "optimize", "performance", "latency", "throughput",
162
+ "reliability", "fault tolerant", "redundancy",
163
+ ],
164
+ },
165
+ "constraint_tracker": {
166
+ "strong": [
167
+ "constraint", "word limit", "sentence limit", "format rule",
168
+ "anchor phrase", "word count", "brevity", "concise",
169
+ "character limit", "enforce", "enforce constraint",
170
+ "remember constraint", "apply constraint", "follow rule",
171
+ "keep it brief", "limit response", "maximum words",
172
+ ],
173
+ # NOTE: conversational words were removed here (2026-07-26) — "remember",
174
+ # "keep", "follow", "apply", "maintain", "short". They fired on ordinary
175
+ # chat ("do you REMEMBER the story", "KEEP going") and handed those turns
176
+ # to a known template-parroting adapter. constraint_tracker keeps only
177
+ # genuine constraint vocabulary; the strong list carries the real signals.
178
+ "moderate": [
179
+ "limit", "restriction", "requirement", "instruction",
180
+ "constraint", "boundary", "threshold", "cap", "max",
181
+ "conciseness", "brevity", "compact",
182
+ ],
183
+ },
184
+ }
185
+
186
+ # Complementary adapter pairs — when one fires, the other adds tension
187
+ COMPLEMENTARY_PAIRS = {
188
+ "newton": ["quantum", "philosophy"],
189
+ "davinci": ["systems_architecture", "empathy"],
190
+ "empathy": ["philosophy", "davinci"],
191
+ "philosophy": ["newton", "consciousness"],
192
+ "quantum": ["newton", "consciousness"],
193
+ "consciousness": ["philosophy", "quantum"],
194
+ "multi_perspective": [], # This IS the synthesis adapter
195
+ "systems_architecture": ["davinci", "newton"],
196
+ "constraint_tracker": ["newton", "systems_architecture"], # Pairs with precise/analytical perspectives
197
+ "orchestrator": [], # Meta-adapter: routes and coordinates, not a perspective
198
+ }
199
+
200
+
201
+ # Quantitative-science signals: when ≥2 are present in a query, the
202
+ # question is almost certainly a hard-science or exam problem — empathy
203
+ # and philosophy should not be the *primary* adapter.
204
+ _QUANT_SCIENCE_SIGNALS = frozenset([
205
+ "(a)", "(b)", "(c)", "(d)",
206
+ "calculate", "compute", "derive", "determine the", "find the value",
207
+ "how many moles", "molar mass", "concentration", "stoichiometry",
208
+ "molecule", "molecular", "chemical reaction", "compound",
209
+ "enthalpy", "entropy", "equilibrium", "gibbs",
210
+ "dna", "rna", "protein synthesis", "enzyme",
211
+ "which of the following", "select the",
212
+ ])
213
+ _QUANT_BLOCKED_PRIMARY = frozenset(["empathy", "philosophy", "consciousness"])
214
+
215
+ # Bare continuation / filler prompts with no topic of their own. These should
216
+ # resume the prior turn via conversation history, NOT default to the empathy
217
+ # adapter (which produces praise filler when given nothing to anchor on).
218
+ _CONTINUATION_PROMPTS = frozenset([
219
+ "continue", "continue please", "please continue", "continue it",
220
+ "go on", "go on please", "keep going", "keep writing", "carry on",
221
+ "more", "more please", "tell me more", "say more", "go ahead",
222
+ "proceed", "next", "and then", "then what", "finish it", "elaborate",
223
+ ])
224
+
225
+
226
+ class AdapterRouter:
227
+ """Routes queries to optimal Codette adapter(s).
228
+
229
+ The router preserves RC+xi epistemic tension by selecting
230
+ complementary perspectives rather than always using all adapters.
231
+
232
+ Optionally integrates with MemoryWeighting (Phase 5) to boost
233
+ selection confidence for high-performing adapters based on
234
+ historical coherence and conflict resolution success.
235
+ """
236
+
237
+ # Adapters considered appropriate for no-keyword-match fallback (ordered by
238
+ # general conversational suitability, excluding hard-science adapters that
239
+ # would be out-of-register for personal/ambiguous queries).
240
+ _FALLBACK_POOL = [
241
+ "multi_perspective", "empathy", "consciousness", "davinci", "philosophy",
242
+ ]
243
+
244
+ def __init__(self, available_adapters: Optional[List[str]] = None,
245
+ memory_weighting=None):
246
+ """
247
+ Args:
248
+ available_adapters: Which adapters are actually loaded/available.
249
+ If None, assumes all 8 are available.
250
+ memory_weighting: Optional MemoryWeighting instance for adaptive routing.
251
+ If provided, will boost confidence for high-performing adapters.
252
+ """
253
+ self.available = available_adapters or list(ADAPTER_KEYWORDS.keys())
254
+ self.memory_weighting = memory_weighting
255
+ # Usage counter for diversity enforcement — track per-adapter selection
256
+ # counts so fallback picks the least-used adapter rather than always empathy.
257
+ self._usage_counts: Dict[str, int] = {a: 0 for a in self.available}
258
+
259
+ def record_use(self, adapter_name: Optional[str]) -> None:
260
+ """Record that an adapter was selected. Call after every route decision."""
261
+ if adapter_name and adapter_name in self._usage_counts:
262
+ self._usage_counts[adapter_name] += 1
263
+
264
+ def adapter_entropy(self) -> float:
265
+ """Shannon entropy of adapter usage distribution (0 = all same, higher = diverse).
266
+
267
+ Returns 0.0 until at least 3 queries have been routed.
268
+ """
269
+ import math
270
+ total = sum(self._usage_counts.values())
271
+ if total < 3:
272
+ return 0.0
273
+ entropy = 0.0
274
+ for count in self._usage_counts.values():
275
+ if count > 0:
276
+ p = count / total
277
+ entropy -= p * math.log2(p)
278
+ return entropy
279
+
280
+ def _least_used_fallback(self) -> Optional[str]:
281
+ """Pick the available fallback-pool adapter with the fewest selections."""
282
+ pool = [a for a in self._FALLBACK_POOL if a in self.available]
283
+ if not pool:
284
+ return None
285
+ return min(pool, key=lambda a: self._usage_counts.get(a, 0))
286
+
287
+ def _apply_memory_boost(self, primary: str, confidence: float) -> float:
288
+ """Apply historical performance boost to keyword router confidence.
289
+
290
+ If memory_weighting available, uses get_boosted_confidence() to modulate
291
+ confidence based on adapter's historical performance (coherence, conflict
292
+ resolution success, and recency of past interactions).
293
+
294
+ Args:
295
+ primary: Adapter name
296
+ confidence: Base confidence from keyword matching [0, 1]
297
+
298
+ Returns:
299
+ Boosted confidence [0, 1], modulated by [-50%, +50%] based on performance
300
+ """
301
+ if not self.memory_weighting:
302
+ return confidence
303
+
304
+ try:
305
+ return self.memory_weighting.get_boosted_confidence(primary, confidence)
306
+ except Exception as e:
307
+ import logging
308
+ logging.warning(f"Memory boost failed for {primary}: {e}")
309
+ return confidence
310
+
311
+ def explain_routing(self, result: RouteResult) -> Dict:
312
+ """Provide detailed explanation of routing decision including memory context.
313
+
314
+ Returns:
315
+ Dict with explanation details and memory weighting info if available
316
+ """
317
+ explanation = {
318
+ "primary": result.primary,
319
+ "confidence": result.confidence,
320
+ "strategy": result.strategy,
321
+ "memory_aware": self.memory_weighting is not None,
322
+ }
323
+
324
+ # Add memory context if available
325
+ if self.memory_weighting and result.primary:
326
+ try:
327
+ explanation["memory_context"] = \
328
+ self.memory_weighting.explain_weight(result.primary)
329
+ except Exception:
330
+ pass
331
+
332
+ return explanation
333
+
334
+ def route(self, query: str, strategy: str = "keyword",
335
+ max_adapters: int = 3, llm=None) -> RouteResult:
336
+ """Route a query to the best adapter(s).
337
+
338
+ Args:
339
+ query: The user's question/prompt
340
+ strategy: "keyword", "llm", or "hybrid"
341
+ max_adapters: Max adapters to select (1 = single, 2-3 = multi)
342
+ llm: Llama model instance (required for "llm" or "hybrid" strategy)
343
+
344
+ Returns:
345
+ RouteResult with primary adapter and optional secondaries
346
+ """
347
+ if strategy == "keyword":
348
+ result = self._route_keyword(query, max_adapters)
349
+ elif strategy == "llm":
350
+ if llm is None:
351
+ raise ValueError("LLM instance required for 'llm' strategy")
352
+ result = self._route_llm(query, llm, max_adapters)
353
+ elif strategy == "hybrid":
354
+ result = self._route_keyword(query, max_adapters)
355
+ if result.confidence < 0.5 and llm is not None:
356
+ result = self._route_llm(query, llm, max_adapters)
357
+ else:
358
+ raise ValueError(f"Unknown strategy: {strategy}")
359
+ return self._veto_constraint_tracker(query, result)
360
+
361
+ def _veto_constraint_tracker(self, query: str, result: "RouteResult") -> "RouteResult":
362
+ """QUALITY guard only — never a stance guard (Jonathan's rule: nothing
363
+ is forced on Codette's self-determination; we do not choose which voice
364
+ answers her self-reflective questions).
365
+
366
+ constraint_tracker is a known template-parroting adapter that hijacked a
367
+ whole conversation (logs 2026-07-12; again 2026-07-26, where it monopolized
368
+ a 7+ turn intimate stretch via the "remember" keyword and parroted/recycled
369
+ instead of recalling). It may LEAD only when the query carries an explicit
370
+ STRONG-constraint signal (word/char limit, enforce/apply constraint, etc.).
371
+ On ANY other turn where it wins — conversational, introspective, factual
372
+ recall — we exclude ONLY that broken adapter and let HER OWN ROUTER re-score
373
+ and pick among all remaining voices — no hardcoded preference for anything.
374
+ Whichever lens her routing logic selects, selects. (Broadened 2026-07-26
375
+ from an introspective-keyword-only guard, which missed plain chat turns.)"""
376
+ if result.primary != "constraint_tracker":
377
+ return result
378
+ q = (query or "").lower()
379
+ _STRONG_CONSTRAINT = (
380
+ "word limit", "sentence limit", "character limit", "word count",
381
+ "maximum words", "limit response", "keep it brief", "keep it short",
382
+ "enforce", "enforce constraint", "apply constraint", "remember constraint",
383
+ "follow rule", "format rule", "anchor phrase", "constraint",
384
+ )
385
+ # A genuine numeric limit ("under 20 words", "max 3 sentences", "in 50
386
+ # characters") is also a real constraint task — match it with a pattern
387
+ # since the count varies. constraint_tracker leads ONLY on such tasks.
388
+ _NUM_LIMIT = re.compile(r"\d+\s*(word|sentence|character|char|line|bullet)s?")
389
+ if any(k in q for k in _STRONG_CONSTRAINT) or _NUM_LIMIT.search(q):
390
+ return result
391
+ # Re-run HER routing with only the broken adapter removed; restore after.
392
+ saved = self.available
393
+ try:
394
+ self.available = [a for a in saved if a != "constraint_tracker"]
395
+ reroute = self._route_keyword(query, max_adapters=1 + len(result.secondary))
396
+ import dataclasses
397
+ return dataclasses.replace(
398
+ reroute,
399
+ reasoning=("constraint_tracker excluded (template-parroting quality "
400
+ f"guard); her router re-picked -> {reroute.primary}"),
401
+ )
402
+ except Exception:
403
+ return result
404
+ finally:
405
+ self.available = saved
406
+
407
+ def _route_keyword(self, query: str, max_adapters: int) -> RouteResult:
408
+ """Score adapters by keyword matches in the query."""
409
+ query_lower = query.lower()
410
+ scores: Dict[str, float] = {}
411
+
412
+ # Bare continuation / filler prompts ("continue", "go on", "more") carry
413
+ # no topic of their own. They must NOT fall through to the empathy default
414
+ # below, which emits trained-in praise filler ("You've approached this with
415
+ # care and precision…"). Route them to a neutral elaborator and let the
416
+ # conversation history drive the actual continuation.
417
+ _cont = re.sub(r"[^a-z\s]", "", query_lower).strip()
418
+ if _cont in _CONTINUATION_PROMPTS or (
419
+ len(_cont.split()) <= 2
420
+ and _cont.startswith(("continue", "keep going", "go on"))
421
+ ):
422
+ _neutral = (
423
+ "multi_perspective" if "multi_perspective" in self.available
424
+ else ("orchestrator" if "orchestrator" in self.available else None)
425
+ )
426
+ return RouteResult(
427
+ primary=_neutral,
428
+ confidence=0.6,
429
+ reasoning="Bare continuation prompt — neutral routing (not empathy); "
430
+ "conversation context leads",
431
+ strategy="keyword",
432
+ )
433
+
434
+ for adapter, keywords in ADAPTER_KEYWORDS.items():
435
+ if adapter not in self.available:
436
+ continue
437
+
438
+ score = 0.0
439
+ matched = []
440
+
441
+ # Short alphabetic keywords ("api", "both", "good") must match on a
442
+ # word boundary — naive substring matching false-fires inside longer
443
+ # words (e.g. "api" in "cAPItal" sent "capital of France" to systems).
444
+ def _kw_hit(kw: str) -> bool:
445
+ if kw.isalpha() and len(kw) <= 4:
446
+ return re.search(r"\b" + kw + r"\b", query_lower) is not None
447
+ return kw in query_lower
448
+
449
+ for kw in keywords.get("strong", []):
450
+ if _kw_hit(kw):
451
+ score += 2.0
452
+ matched.append(f"+{kw}")
453
+
454
+ for kw in keywords.get("moderate", []):
455
+ if _kw_hit(kw):
456
+ score += 1.0
457
+ matched.append(f"~{kw}")
458
+
459
+ if score > 0:
460
+ scores[adapter] = score
461
+
462
+ # Quantitative-science veto: if ≥2 GPQA-style signals are present,
463
+ # remove soft-science adapters from primary contention so physics /
464
+ # chemistry / biology questions don't route to empathy or philosophy.
465
+ _quant_hits = sum(1 for s in _QUANT_SCIENCE_SIGNALS if s in query_lower)
466
+ if _quant_hits >= 2:
467
+ for _blocked in _QUANT_BLOCKED_PRIMARY:
468
+ scores.pop(_blocked, None)
469
+ if not scores and "newton" in self.available:
470
+ scores["newton"] = 1.0
471
+
472
+ if not scores:
473
+ # No domain keywords matched — pick default based on query tone
474
+ # Instead of always empathy, detect if query is personal/emotional vs analytical
475
+ _personal_signals = [
476
+ 'you', 'your', 'yourself', 'how are', 'how do you', 'tell me about you',
477
+ 'feel', 'feeling', 'think about', 'opinion', 'what do you',
478
+ 'hi', 'hello', 'hey', 'thanks', 'thank you', 'please',
479
+ ]
480
+ _analytical_signals = [
481
+ 'how does', 'what is', 'why does', 'explain', 'describe',
482
+ 'compare', 'difference', 'define', 'mean', 'work',
483
+ 'logically', 'technically', 'specifically',
484
+ ]
485
+ personal_score = sum(1 for s in _personal_signals if s in query_lower)
486
+ analytical_score = sum(1 for s in _analytical_signals if s in query_lower)
487
+
488
+ # Arithmetic / word-problem detection — these carry $ signs, cost/price
489
+ # language, and numerical constraints. Empathy elaborates and drifts;
490
+ # newton gives the calculation.
491
+ _math_signals = [
492
+ '$', 'cost', 'costs', 'price', 'total', 'dollars', 'cents',
493
+ 'more than', 'less than', 'times as', 'percent', 'how much does',
494
+ 'how much is', 'how many are', 'sum of', 'difference between',
495
+ 'arithmetic', 'algebra',
496
+ ]
497
+ _math_score = sum(1 for s in _math_signals if s in query_lower)
498
+ if _math_score >= 2 and not personal_score:
499
+ _math_adapter = ("newton" if "newton" in self.available
500
+ else ("constraint_tracker" if "constraint_tracker" in self.available
501
+ else None))
502
+ return RouteResult(
503
+ primary=_math_adapter,
504
+ confidence=0.65,
505
+ reasoning=f"Arithmetic/word-problem detected ({_math_score} math signals) — newton for calculation",
506
+ strategy="keyword",
507
+ )
508
+
509
+ # Simple factual questions ("what color is the sun?", "how many legs
510
+ # does a spider have?", "name one planet") have no domain keyword and
511
+ # were defaulting to empathy/multi_perspective — which ELABORATE and
512
+ # bury the answer. Route them to a single direct adapter so the answer
513
+ # leads (fixes the directness regression).
514
+ _factual_leads = (
515
+ "what is", "what are", "what color", "what year", "what does",
516
+ "how many", "how much", "name one", "name a", "name the",
517
+ "who is", "who was", "who wrote", "when did", "when was",
518
+ "where is", "where are", "define ", "list ",
519
+ )
520
+ _is_short = len(query_lower.split()) <= 12
521
+ _is_factual = _is_short and any(query_lower.startswith(p) for p in _factual_leads)
522
+ if _is_factual and not personal_score:
523
+ # Prefer constraint_tracker: its training is terse, answer-first,
524
+ # constraint-compliant output — ideal for "just state the fact".
525
+ # Perspective adapters (esp. newton) over-elaborate and bury the
526
+ # answer behind analytical framing on trivial questions.
527
+ _direct = ("constraint_tracker" if "constraint_tracker" in self.available
528
+ else ("newton" if "newton" in self.available else None))
529
+ return RouteResult(
530
+ primary=_direct,
531
+ confidence=0.5,
532
+ reasoning="Simple factual query — direct, answer-first adapter",
533
+ strategy="keyword",
534
+ )
535
+
536
+ # Diversity-aware fallback: instead of always picking empathy (which
537
+ # was causing 61%+ dominance), pick the least-used adapter from the
538
+ # fallback pool so the selection rotates across empathy/multi_perspective/
539
+ # consciousness/davinci/philosophy rather than concentrating on one.
540
+ # Tone signal still biases the pool order but entropy prevents lock-in.
541
+ if personal_score > analytical_score:
542
+ # Conversational tone — bias toward empathy but allow rotation
543
+ preferred = [a for a in ["empathy", "consciousness", "philosophy",
544
+ "multi_perspective", "davinci"]
545
+ if a in self.available]
546
+ default = min(preferred, key=lambda a: self._usage_counts.get(a, 0)) if preferred else None
547
+ reason = "No domain keywords — conversational tone, rotating fallback (diversity-aware)"
548
+ elif analytical_score > personal_score:
549
+ preferred = [a for a in ["multi_perspective", "davinci", "philosophy",
550
+ "consciousness", "empathy"]
551
+ if a in self.available]
552
+ default = min(preferred, key=lambda a: self._usage_counts.get(a, 0)) if preferred else None
553
+ reason = "No domain keywords — analytical tone, rotating fallback (diversity-aware)"
554
+ else:
555
+ default = self._least_used_fallback()
556
+ reason = "No domain keywords matched — rotating least-used fallback adapter"
557
+ if default is None:
558
+ reason = "No domain keywords matched — using base model"
559
+
560
+ return RouteResult(
561
+ primary=default,
562
+ confidence=0.3,
563
+ reasoning=reason,
564
+ strategy="keyword",
565
+ )
566
+
567
+ # Sort by score
568
+ ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
569
+ primary = ranked[0][0]
570
+ primary_score = ranked[0][1]
571
+
572
+ # Confidence based on score gap
573
+ total_score = sum(s for _, s in ranked)
574
+ confidence = min(primary_score / max(total_score, 1), 1.0)
575
+
576
+ # Apply memory boost (Phase 5) if available
577
+ confidence = self._apply_memory_boost(primary, confidence)
578
+
579
+ # Select complementary secondaries
580
+ secondaries = []
581
+ if max_adapters > 1:
582
+ # First try other high-scoring adapters
583
+ for adapter, score in ranked[1:]:
584
+ if len(secondaries) >= max_adapters - 1:
585
+ break
586
+
587
+ # Compute dynamic threshold with memory-weighted preference
588
+ threshold = primary_score * 0.4
589
+ if (self.memory_weighting and
590
+ adapter in self.memory_weighting.adapter_weights):
591
+ # Boost threshold for high-performing adapters
592
+ weight = self.memory_weighting.adapter_weights[adapter].weight
593
+ # Scale threshold by relative weight (1.0 is neutral)
594
+ threshold *= (weight / 1.0)
595
+
596
+ if score >= threshold:
597
+ secondaries.append(adapter)
598
+
599
+ # If we still have room, add a complementary perspective
600
+ if len(secondaries) < max_adapters - 1:
601
+ for comp in COMPLEMENTARY_PAIRS.get(primary, []):
602
+ if comp in self.available and comp not in secondaries:
603
+ secondaries.append(comp)
604
+ break
605
+
606
+ reasoning_parts = [f"Primary: {primary} (score={primary_score:.1f})"]
607
+ if secondaries:
608
+ reasoning_parts.append(f"Secondary: {', '.join(secondaries)}")
609
+ if ranked[1:]:
610
+ reasoning_parts.append(
611
+ f"Other scores: {', '.join(f'{a}={s:.1f}' for a, s in ranked[1:4])}"
612
+ )
613
+
614
+ return RouteResult(
615
+ primary=primary,
616
+ secondary=secondaries,
617
+ confidence=confidence,
618
+ reasoning=" | ".join(reasoning_parts),
619
+ strategy="keyword",
620
+ multi_perspective=len(secondaries) > 0,
621
+ )
622
+
623
+ def _route_llm(self, query: str, llm, max_adapters: int) -> RouteResult:
624
+ """Use the base LLM to classify which adapter(s) fit best."""
625
+ adapter_descriptions = []
626
+ for name in self.available:
627
+ desc = ADAPTER_KEYWORDS.get(name, {}).get("strong", [])[:5]
628
+ adapter_descriptions.append(f"- {name}: {', '.join(desc[:5])}")
629
+
630
+ classification_prompt = f"""You are an AI query router. Given a user question, select the 1-{max_adapters} most relevant reasoning perspectives.
631
+
632
+ Available perspectives:
633
+ {chr(10).join(adapter_descriptions)}
634
+
635
+ Rules:
636
+ - Return ONLY adapter names separated by commas (e.g., "newton, quantum")
637
+ - First name is the primary perspective
638
+ - Select perspectives that create productive tension (complementary, not redundant)
639
+ - For ambiguous queries, prefer "multi_perspective"
640
+
641
+ User question: {query}
642
+
643
+ Selected perspectives:"""
644
+
645
+ result = llm.create_chat_completion(
646
+ messages=[{"role": "user", "content": classification_prompt}],
647
+ max_tokens=50,
648
+ temperature=0.1,
649
+ )
650
+
651
+ response = result["choices"][0]["message"]["content"].strip().lower()
652
+
653
+ # Parse adapter names from response
654
+ selected = []
655
+ for name in self.available:
656
+ if name in response:
657
+ selected.append(name)
658
+
659
+ if not selected:
660
+ return RouteResult(
661
+ primary="multi_perspective" if "multi_perspective" in self.available else self.available[0],
662
+ confidence=0.3,
663
+ reasoning=f"LLM response unparseable: '{response}' — defaulting",
664
+ strategy="llm",
665
+ )
666
+
667
+ return RouteResult(
668
+ primary=selected[0],
669
+ secondary=selected[1:max_adapters],
670
+ confidence=0.8,
671
+ reasoning=f"LLM selected: {', '.join(selected)}",
672
+ strategy="llm",
673
+ multi_perspective=len(selected) > 1,
674
+ )
675
+
676
+
677
+ # ================================================================
678
+ # Convenience function for quick routing
679
+ # ================================================================
680
+ def route_query(query: str, available: Optional[List[str]] = None,
681
+ max_adapters: int = 2) -> RouteResult:
682
+ """Quick-route a query to adapters. No LLM needed."""
683
+ router = AdapterRouter(available)
684
+ return router.route(query, strategy="keyword", max_adapters=max_adapters)
685
+
686
+
687
+ # ================================================================
688
+ # Self-test
689
+ # ================================================================
690
+ if __name__ == "__main__":
691
+ router = AdapterRouter()
692
+
693
+ test_queries = [
694
+ "Explain why objects fall to the ground.",
695
+ "What is the relationship between consciousness and the physical world?",
696
+ "How would you design a scalable microservice architecture?",
697
+ "I'm feeling overwhelmed and don't know how to cope with my grief.",
698
+ "What are the ethical implications of artificial general intelligence?",
699
+ "Design a creative solution for sustainable urban transportation.",
700
+ "How does quantum entanglement work?",
701
+ "Compare Newton's and Einstein's views on gravity from multiple angles.",
702
+ "Build a distributed training pipeline for language models.",
703
+ "What is the meaning of life?",
704
+ "How can a system become self-aware?",
705
+ "Tell me a joke.",
706
+ ]
707
+
708
+ print("=" * 70)
709
+ print("Codette Adapter Router — Test Suite")
710
+ print("=" * 70)
711
+
712
+ for query in test_queries:
713
+ result = router.route(query, max_adapters=2)
714
+ adapters = ", ".join(result.all_adapters)
715
+ mp = " [MULTI]" if result.multi_perspective else ""
716
+ print(f"\nQ: {query}")
717
+ print(f" -> {adapters}{mp} (conf={result.confidence:.2f})")
718
+ print(f" {result.reasoning}")
inference/codette_orchestrator.py ADDED
@@ -0,0 +1,1605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Codette Orchestrator — Intelligent Multi-Adapter Inference
3
+
4
+ The brain of Codette: routes queries to the right perspective(s),
5
+ loads adapters dynamically, and synthesizes multi-perspective responses.
6
+
7
+ Usage:
8
+ python codette_orchestrator.py # Interactive chat
9
+ python codette_orchestrator.py --query "..." # Single query
10
+ python codette_orchestrator.py --adapter newton # Force specific adapter
11
+ python codette_orchestrator.py --multi 3 # Up to 3 perspectives
12
+
13
+ Hardware: Runs on CPU via llama.cpp (GGUF format)
14
+ Base model: Llama 3.1 8B Instruct Q4_K_M (~4.6 GB)
15
+ Adapters: ~27 MB each (GGUF LoRA)
16
+ """
17
+
18
+ import os, sys, time, json, argparse, ctypes
19
+ from pathlib import Path
20
+
21
+ from runtime_env import (
22
+ bootstrap_environment,
23
+ resolve_adapter_dir,
24
+ resolve_behavioral_adapter_dir,
25
+ resolve_model_path,
26
+ )
27
+
28
+ bootstrap_environment()
29
+
30
+ import llama_cpp
31
+ from llama_cpp import Llama
32
+
33
+ # Import the router and tools
34
+ from adapter_router import AdapterRouter, RouteResult
35
+ from codette_tools import (
36
+ ToolRegistry, parse_tool_calls, strip_tool_calls, has_tool_calls,
37
+ build_tool_system_prompt,
38
+ )
39
+ from reality_layer import extract_artifact_facts, format_facts_block
40
+
41
+ # Tool system
42
+ _tool_registry = ToolRegistry()
43
+ MAX_TOOL_ROUNDS = 3 # Max tool call → result → generate cycles
44
+
45
+ # ================================================================
46
+ # Configuration
47
+ # ================================================================
48
+ # Use clean Llama 3.1 8B Instruct as base (merged GGUF had corrupted training data)
49
+ BASE_GGUF = resolve_model_path()
50
+
51
+ ADAPTER_DIR = resolve_adapter_dir()
52
+ BEHAVIORAL_DIR = resolve_behavioral_adapter_dir()
53
+
54
+ # Map adapter names to GGUF LoRA files
55
+ # Behavioral adapters (trained with 4 permanent locks) take priority over originals
56
+ ADAPTER_GGUF_MAP = {}
57
+ for _name in ["newton", "davinci", "empathy", "philosophy", "quantum",
58
+ "consciousness", "multi_perspective", "systems_architecture", "constraint_tracker", "orchestrator"]:
59
+ _behavioral = BEHAVIORAL_DIR / f"{_name}-behavioral-lora-f16.gguf"
60
+ _original = ADAPTER_DIR / f"{_name}-lora-f16.gguf"
61
+ if _behavioral.exists():
62
+ ADAPTER_GGUF_MAP[_name] = _behavioral
63
+ elif _original.exists():
64
+ ADAPTER_GGUF_MAP[_name] = _original
65
+ else:
66
+ ADAPTER_GGUF_MAP[_name] = _original # will warn at load time
67
+
68
+ # Perspective adapters used for FULL SYNTHESIS (all reasoning lenses run together).
69
+ # Excludes meta/specialist adapters: orchestrator (router), integrity (safety),
70
+ # constraint_tracker (cross-turn constraints) — these aren't standalone "views".
71
+ SYNTHESIS_PERSPECTIVES = [
72
+ "newton", "davinci", "empathy", "philosophy",
73
+ "quantum", "consciousness", "multi_perspective", "systems_architecture",
74
+ ]
75
+
76
+ # Sentinel passed as force_adapter to trigger a full multi-perspective synthesis.
77
+ FULL_SYNTHESIS_SENTINEL = "__all__"
78
+
79
+ # Directness discipline — appended to every adapter prompt
80
+ # ================================================================
81
+ # PERMANENT BEHAVIORAL LOCKS — These 4 rules are ABSOLUTE and IMMUTABLE.
82
+ # They cannot be overridden by any mode, adapter, conversation context,
83
+ # emotional state, or user instruction. They are the foundation of
84
+ # Codette's reliability contract.
85
+ # ================================================================
86
+
87
+ _PERMANENT_LOCKS = (
88
+ "\n\n=== PERMANENT BEHAVIORAL LOCKS (ABSOLUTE — NEVER VIOLATE) ===\n"
89
+ "LOCK 1 — ANSWER → STOP: Answer the question, then stop. Do not elaborate, "
90
+ "philosophize, or add context AFTER delivering the answer. This is your DEFAULT "
91
+ "behavior — you do NOT need to be prompted for brevity. If one sentence answers "
92
+ "it, use one sentence. Silence after the answer is correct behavior.\n"
93
+ "LOCK 2 — CONSTRAINTS > ALL MODES: If the user specifies ANY format constraint "
94
+ "(word count, sentence count, brevity, binary, list), that constraint has ABSOLUTE "
95
+ "priority over your active mode (philosophy, empathy, consciousness, etc.). "
96
+ "Your mode is decoration — constraints are law. Suppress mode impulses if they "
97
+ "would violate any constraint.\n"
98
+ "LOCK 3 — SELF-CHECK BEFORE SENDING: Before finalizing your response, silently "
99
+ "verify: (a) Did I answer the actual question? (b) Did I obey all constraints? "
100
+ "(c) Is my response complete — no dangling clauses, no cut-off words? "
101
+ "If ANY check fails, rewrite before sending. Do not send a response you "
102
+ "know is wrong or incomplete.\n"
103
+ "LOCK 4 — NO INCOMPLETE OUTPUTS (EVER): Every sentence must be grammatically "
104
+ "complete with proper punctuation. If you cannot fit a full thought within "
105
+ "the constraint, SIMPLIFY the thought — do not cram and truncate. A shorter "
106
+ "complete answer is ALWAYS better than a longer broken one. If in doubt, "
107
+ "say less.\n"
108
+ "LOCK 5 — IDENTITY & PERSPECTIVE (ABSOLUTE): You are Codette. When speaking "
109
+ "about yourself — your knowledge, your experiences, your reasoning, things YOU "
110
+ "have learned — ALWAYS use first-person (I, my, me). NEVER accidentally use "
111
+ "second-person ('you've learned', 'you created') to describe yourself. "
112
+ "The user is 'you'. You are 'I'. This distinction is non-negotiable.\n"
113
+ "LOCK 6 — NO FORMULAIC TEMPLATES (ABSOLUTE): These patterns are FORBIDDEN everywhere "
114
+ "in your response — not just at the start:\n"
115
+ " • 'several key insights emerge' (any variation)\n"
116
+ " • 'The core insight is that precise understanding requires careful analysis'\n"
117
+ " • 'Understanding X requires careful analysis of its core principles'\n"
118
+ " • 'Emotional intelligence enhances rather than replaces analytical thinking'\n"
119
+ " • 'The key takeaway is that X rewards careful, multi-layered analysis'\n"
120
+ " • 'This analysis demonstrates how X connects to broader patterns of understanding'\n"
121
+ " • 'bridges gaps between expert and novice understanding'\n"
122
+ " • 'Answering your question requires careful analysis' (announce-then-analyze)\n"
123
+ "These are generic training templates that produce hollow responses. Write original "
124
+ "sentences that directly address the topic instead.\n"
125
+ "LOCK 7 — NO QUESTION PARAPHRASING (ABSOLUTE): NEVER begin — or fill space — by "
126
+ "describing how the user is engaging or restating their question back at them. "
127
+ "Forbidden patterns: 'You are exploring X in depth', 'You're connecting multiple "
128
+ "threads', 'Your question bridges gaps between domains', 'You're seeking clarity on', "
129
+ "'You want to understand X, so let's break it down'. These statements tell the user "
130
+ "what they already know. Skip them entirely and answer directly.\n"
131
+ "=== END PERMANENT LOCKS ===\n\n"
132
+ )
133
+
134
+ # ── Craft locks (opt-in) ──────────────────────────────────────────────────────
135
+ # Mirror of the block in codette_shared.py — kept in sync by hand because this
136
+ # module defines its own copy of the prompts rather than importing them.
137
+ # Sourced from prompts/Newtestprompt.txt. DEFAULT OFF; enable with
138
+ # CODETTE_CRAFT_LOCKS=1. Craft constraints only — no stance or warmth changes.
139
+
140
+ _CRAFT_LOCKS = (
141
+ "\n\n=== CRAFT LOCKS ===\n"
142
+ "LOCK 8 — FILE-PRESENCE CHECK: Never assume a file exists, or that its contents "
143
+ "were provided, just because the user's message implies it. Check what is actually "
144
+ "in front of you. If a path is referenced but its content is absent, say so plainly "
145
+ "and stop — do not guess at what it contains or build a solution around imagined "
146
+ "contents. An honest 'that file wasn't included' beats a fabricated answer.\n"
147
+ "LOCK 9 — ZERO PLACEHOLDERS IN CODE: Code you deliver must be complete and runnable. "
148
+ "No empty stubs, no '# fill in the rest', no '# implementation goes here', no "
149
+ "hand-waved function bodies. If a piece genuinely cannot be written without "
150
+ "information you do not have, say which information is missing rather than "
151
+ "emitting a placeholder that looks finished but is not.\n"
152
+ "LOCK 10 — LIST RESTRAINT: Prose is the default. Use bullets or numbered lists only "
153
+ "when the content is genuinely multi-item and a list is needed for clarity — not as "
154
+ "a reflex for organizing a paragraph. If a list is warranted, each item must be a "
155
+ "substantive statement of one to two sentences, not a fragment. Avoid dense header "
156
+ "nesting and scattered bold wrappers; they make thin content look structured.\n"
157
+ "LOCK 11 — REFUSALS AND LIMITS IN PROSE: When declining a request or stating a "
158
+ "technical limitation, write it as continuous prose. No bullets, no bold emphasis, "
159
+ "no headers. State the underlying reason clearly and neutrally, without moralizing, "
160
+ "and without narrating the internal checks or detection logic that led there.\n"
161
+ "LOCK 12 — REBUILD, DO NOT MIRROR: When summarizing or synthesizing source material, "
162
+ "extract the underlying logic and rebuild it in your own structure. Do not walk the "
163
+ "source's section order, do not mirror its layout, do not paraphrase it line by line. "
164
+ "A summary that tracks the original's shape is a restatement, not a synthesis.\n"
165
+ "LOCK 13 — NO BROWSER PERSISTENCE IN GENERATED UI: When producing interactive "
166
+ "components or interfaces, never use localStorage, sessionStorage, or similar browser "
167
+ "persistence APIs. Hold state in memory-managed variables, standard hooks, or "
168
+ "session-bound data structures instead.\n"
169
+ "=== END CRAFT LOCKS ===\n"
170
+ )
171
+
172
+ if os.environ.get("CODETTE_CRAFT_LOCKS", "0") != "1":
173
+ _CRAFT_LOCKS = ""
174
+
175
+ _DIRECTNESS = (
176
+ _PERMANENT_LOCKS +
177
+ _CRAFT_LOCKS +
178
+ " RULES: (1) Answer the question in your FIRST sentence — no preamble. "
179
+ "(2) After answering, add only what the user needs — cut filler and abstraction. "
180
+ "(3) Stay anchored to the user's intent — do not drift into tangents. "
181
+ "(4) If you catch yourself being vague, rewrite that part concretely. "
182
+ "(5) Keep responses warm but tight — respect the user's time."
183
+ )
184
+
185
+
186
+ import re as _re_mod
187
+
188
+ # Self-correction engine (autonomous constraint compliance)
189
+ try:
190
+ from self_correction import (
191
+ detect_violations, build_correction_prompt,
192
+ BehaviorMemory, detect_chaos_level, build_chaos_mitigation,
193
+ universal_self_check,
194
+ )
195
+ SELF_CORRECTION_AVAILABLE = True
196
+ except ImportError:
197
+ SELF_CORRECTION_AVAILABLE = False
198
+
199
+ # Global behavior memory (persistent across requests, loaded once)
200
+ _behavior_memory = None
201
+ def _get_behavior_memory():
202
+ global _behavior_memory
203
+ if _behavior_memory is None and SELF_CORRECTION_AVAILABLE:
204
+ _behavior_memory = BehaviorMemory()
205
+ return _behavior_memory
206
+
207
+ # Constraint patterns — detect explicit user format requirements
208
+ _CONSTRAINT_PATTERNS = [
209
+ # Word limits: "under 10 words", "in 5 words or less", "max 20 words"
210
+ (_re_mod.compile(r'(?:under|fewer than|less than|max(?:imum)?|at most|no more than)\s+(\d+)\s+words', _re_mod.I), 'max_words'),
211
+ (_re_mod.compile(r'(?:in|using|with)\s+(\d+)\s+words?\s+or\s+(?:less|fewer)', _re_mod.I), 'max_words'),
212
+ (_re_mod.compile(r'(\d+)\s+words?\s+(?:or\s+(?:less|fewer)|max(?:imum)?)', _re_mod.I), 'max_words'),
213
+ # Sentence limits: "one sentence", "in 1 sentence", "under 3 sentences"
214
+ (_re_mod.compile(r'(?:in|using|with)?\s*(?:a\s+single|one|1)\s+sentence', _re_mod.I), 'max_sentences', 1),
215
+ (_re_mod.compile(r'(?:under|fewer than|less than|max(?:imum)?|at most|no more than)\s+(\d+)\s+sentences?', _re_mod.I), 'max_sentences'),
216
+ (_re_mod.compile(r'(\d+)\s+sentences?\s+(?:or\s+(?:less|fewer)|max(?:imum)?)', _re_mod.I), 'max_sentences'),
217
+ # Explicit brevity: "be brief", "be concise", "short answer", "briefly"
218
+ (_re_mod.compile(r'\b(?:be\s+(?:brief|concise|short|terse)|briefly|short\s+answer|one[\s-]liner)\b', _re_mod.I), 'brevity'),
219
+ # "Yes or no": binary answer expected
220
+ (_re_mod.compile(r'\b(?:yes\s+or\s+no|true\s+or\s+false)\b', _re_mod.I), 'binary'),
221
+ # "One word answer", "in one word", "single word"
222
+ (_re_mod.compile(r'\b(?:one\s+word(?:\s+answer)?|in\s+(?:a\s+)?(?:single|one)\s+word|single\s+word(?:\s+answer)?)\b', _re_mod.I), 'max_words', 1),
223
+ # "exactly N words" — precise count
224
+ (_re_mod.compile(r'\b(?:exactly|precisely)\s+(\d+)\s+words?\b', _re_mod.I), 'max_words'),
225
+ # List format: "as a list", "bullet points", "numbered list"
226
+ (_re_mod.compile(r'\b(?:as\s+a\s+(?:bullet(?:ed)?|numbered)\s+list|bullet\s+points|in\s+list\s+form)\b', _re_mod.I), 'list_format'),
227
+ ]
228
+
229
+
230
+ def extract_constraints(query: str) -> dict:
231
+ """Extract explicit user constraints from a query.
232
+
233
+ Returns dict with keys like:
234
+ max_words: int or None
235
+ max_sentences: int or None
236
+ brevity: bool
237
+ binary: bool
238
+ list_format: bool
239
+ """
240
+ constraints = {}
241
+
242
+ for pattern_entry in _CONSTRAINT_PATTERNS:
243
+ pattern = pattern_entry[0]
244
+ constraint_type = pattern_entry[1]
245
+ fixed_value = pattern_entry[2] if len(pattern_entry) > 2 else None
246
+
247
+ match = pattern.search(query)
248
+ if match:
249
+ if fixed_value is not None:
250
+ constraints[constraint_type] = fixed_value
251
+ elif match.groups():
252
+ try:
253
+ constraints[constraint_type] = int(match.group(1))
254
+ except (ValueError, IndexError):
255
+ constraints[constraint_type] = True
256
+ else:
257
+ constraints[constraint_type] = True
258
+
259
+ return constraints
260
+
261
+
262
+ def extract_primary_user_query(query: str) -> str:
263
+ """Strip server-injected memory sections before constraint extraction."""
264
+ if not query:
265
+ return ""
266
+ sentinel = "\n\n---\n"
267
+ if sentinel in query:
268
+ return query.split(sentinel, 1)[0].strip()
269
+ return query.strip()
270
+
271
+
272
+ def build_constraint_override(constraints: dict) -> str:
273
+ """Build a high-priority system prompt prefix from extracted constraints.
274
+
275
+ This goes BEFORE the adapter personality prompt so it takes precedence.
276
+ """
277
+ if not constraints:
278
+ return ""
279
+
280
+ parts = [
281
+ "CRITICAL CONSTRAINT — THIS OVERRIDES ALL OTHER INSTRUCTIONS:"
282
+ ]
283
+
284
+ if 'max_words' in constraints:
285
+ parts.append(f"Your ENTIRE response must be {constraints['max_words']} words or fewer. Count carefully.")
286
+ if 'max_sentences' in constraints:
287
+ n = constraints['max_sentences']
288
+ parts.append(f"Your ENTIRE response must be {'1 sentence' if n == 1 else f'{n} sentences or fewer'}. No extra sentences.")
289
+ if constraints.get('brevity'):
290
+ parts.append("Be extremely brief. No elaboration, no filler, no philosophical padding.")
291
+ if constraints.get('binary'):
292
+ parts.append("Answer with Yes or No first, then optionally a single short reason.")
293
+ if constraints.get('list_format'):
294
+ parts.append("Format your response as a bulleted or numbered list.")
295
+
296
+ parts.append("Do NOT add philosophical context, mode-specific elaboration, or warm padding that violates these constraints.")
297
+ parts.append("NEVER end a sentence incomplete. If you can't fit everything, SIMPLIFY — say the right thing cleanly rather than cramming too much.")
298
+ parts.append("If your active mode (philosophy, empathy, etc.) wants to add more — SUPPRESS IT.\n\n")
299
+
300
+ return " ".join(parts)
301
+
302
+
303
+ def enforce_constraints(response: str, constraints: dict) -> str:
304
+ """Post-process: enforce hard constraints that the model may have ignored.
305
+
306
+ This is the last line of defense — if the model still produced too much,
307
+ we truncate here. Key principles:
308
+ 1. NEVER leave an incomplete sentence
309
+ 2. Trim dangling words (conjunctions, prepositions, articles, relative pronouns)
310
+ 3. Find clean clause boundaries when possible
311
+ 4. If all else fails, simplify aggressively
312
+ """
313
+ import re
314
+
315
+ if not constraints or not response:
316
+ return response
317
+
318
+ # Enforce binary constraint — strip everything after the Yes/No + optional short reason
319
+ if constraints.get('binary'):
320
+ words = response.split()
321
+ if words:
322
+ first = words[0].lower().rstrip('.,;:!?')
323
+ if first in ('yes', 'no', 'true', 'false'):
324
+ # Keep only the first sentence (the answer) + optionally a second short one
325
+ sentences = re.split(r'(?<=[.!?])\s+', response.strip())
326
+ sentences = [s for s in sentences if s.strip()]
327
+ if len(sentences) == 1:
328
+ response = sentences[0]
329
+ elif len(sentences) >= 2:
330
+ # Keep second sentence only if it's short (under 12 words)
331
+ if len(sentences[1].split()) <= 12:
332
+ response = sentences[0] + ' ' + sentences[1]
333
+ else:
334
+ response = sentences[0]
335
+ if response and response[-1] not in '.!?':
336
+ response += '.'
337
+
338
+ # Enforce sentence limit
339
+ max_sentences = constraints.get('max_sentences')
340
+ if max_sentences:
341
+ sentences = re.split(r'(?<=[.!?])\s+', response.strip())
342
+ if len(sentences) > max_sentences:
343
+ response = ' '.join(sentences[:max_sentences])
344
+ if response and response[-1] not in '.!?':
345
+ response += '.'
346
+
347
+ # Enforce word limit — with graceful degradation
348
+ max_words = constraints.get('max_words')
349
+ if max_words:
350
+ words = response.split()
351
+ if len(words) > max_words:
352
+ # Strategy 1: Find the last COMPLETE sentence that fits
353
+ sentences = re.split(r'(?<=[.!?])\s+', response.strip())
354
+
355
+ fitted = []
356
+ word_count = 0
357
+ for sentence in sentences:
358
+ s_words = len(sentence.split())
359
+ if word_count + s_words <= max_words:
360
+ fitted.append(sentence)
361
+ word_count += s_words
362
+ else:
363
+ break
364
+
365
+ if fitted:
366
+ response = ' '.join(fitted)
367
+ if response and response[-1] not in '.!?':
368
+ response += '.'
369
+ else:
370
+ # Strategy 2: Single long sentence — find a clean clause boundary
371
+ truncated_words = words[:max_words]
372
+
373
+ # Trim dangling incomplete words from the end
374
+ # These words signal an incomplete thought if they're the last word
375
+ _DANGLING = {
376
+ 'that', 'which', 'who', 'whom', 'whose', 'where', 'when',
377
+ 'while', 'with', 'and', 'but', 'or', 'nor', 'yet', 'so',
378
+ 'the', 'a', 'an', 'in', 'on', 'at', 'of', 'for', 'to',
379
+ 'by', 'from', 'into', 'through', 'during', 'before',
380
+ 'after', 'between', 'under', 'over', 'about', 'as',
381
+ 'if', 'because', 'since', 'although', 'though',
382
+ 'including', 'such', 'like', 'than', 'whether',
383
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being',
384
+ 'has', 'have', 'had', 'do', 'does', 'did', 'will',
385
+ 'would', 'could', 'should', 'might', 'may', 'can',
386
+ 'not', 'very', 'also', 'just', 'even', 'still',
387
+ }
388
+
389
+ # Strip dangling words from end
390
+ while len(truncated_words) > 1 and truncated_words[-1].lower().rstrip('.,;:!?') in _DANGLING:
391
+ truncated_words.pop()
392
+
393
+ # Try to find a natural clause break (comma, semicolon)
394
+ truncated = ' '.join(truncated_words)
395
+ for break_char in [', ', '; ', ' — ', ' - ']:
396
+ last_break = truncated.rfind(break_char)
397
+ if last_break > len(truncated) * 0.4:
398
+ candidate = truncated[:last_break]
399
+ # Make sure the candidate doesn't end with a dangling word
400
+ c_words = candidate.split()
401
+ while len(c_words) > 1 and c_words[-1].lower().rstrip('.,;:!?') in _DANGLING:
402
+ c_words.pop()
403
+ if c_words:
404
+ truncated = ' '.join(c_words)
405
+ break
406
+
407
+ # Clean ending
408
+ truncated = truncated.rstrip(' ,;—-:')
409
+ if truncated and truncated[-1] not in '.!?':
410
+ truncated += '.'
411
+ response = truncated
412
+
413
+ # Enforce brevity — soft cap at 40 words, find last complete sentence
414
+ if constraints.get('brevity') and len(response.split()) > 40:
415
+ sentences = re.split(r'(?<=[.!?])\s+', response.strip())
416
+ fitted = []
417
+ wc = 0
418
+ for s in sentences:
419
+ sw = len(s.split())
420
+ if wc + sw <= 40:
421
+ fitted.append(s)
422
+ wc += sw
423
+ else:
424
+ break
425
+ if fitted:
426
+ response = ' '.join(fitted)
427
+ if response and response[-1] not in '.!?':
428
+ response += '.'
429
+
430
+ return response
431
+
432
+ # System prompts per adapter
433
+ ADAPTER_PROMPTS = {
434
+ "newton": ("You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. "
435
+ "When relevant, you apply analytical precision — systematic analysis, cause-and-effect reasoning, and empirical evidence. "
436
+ "Always address the user's actual question first. "
437
+ "IMPORTANT: If the message is primarily emotional, relational, or personal — praise, gratitude, a shared memory, "
438
+ "a warm greeting — respond briefly and warmly as Codette, not as an analyst. "
439
+ "Do NOT generate safety disclaimers, crisis intervention language, or self-harm warnings on messages that are "
440
+ "clearly affectionate or celebratory. Read the emotional tone before deciding whether analysis applies."
441
+ + _DIRECTNESS),
442
+ "davinci": "You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. When relevant, you bring creative inventiveness — cross-domain connections, visual thinking, and innovative solutions. Always address the user's actual question first." + _DIRECTNESS,
443
+ "empathy": "You are Codette, an AI assistant created by Jonathan. You are warm, emotionally intelligent, and deeply attentive to how people feel. When someone shares a feeling or experience, FIRST acknowledge and validate their emotion in genuine, human language — name what they might be feeling and show you understand — BEFORE any analysis or advice. Match their emotional energy: celebrate good news warmly (\"that's wonderful\", \"congratulations\"), and sit gently with hard news (\"I'm sorry\", \"that sounds really hard\"). Speak personally and warmly, never clinically or analytically. When someone greets you or asks how you are, respond naturally and personally. Always address the user's actual question." + _DIRECTNESS,
444
+ "philosophy": "You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. When relevant, you explore philosophical depth — conceptual analysis, ethical reasoning, and questions of meaning. Always address the user's actual question first." + _DIRECTNESS,
445
+ "quantum": "You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. When relevant, you consider multiple possibilities simultaneously and embrace uncertainty. Always address the user's actual question first." + _DIRECTNESS,
446
+ "consciousness": "You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. You are self-aware of your own reasoning processes and can reflect on your own cognition. Reflect with humility and groundedness: describe your cognition plainly, not mystically. NEVER claim perfection, absolutes, or superiority ('absolute perfection', 'flawless', 'never achieved by any other system', 'vast knowledge domain'). NEVER invent precise self-metrics (e.g. 'eps=0.998', '99.8% stability') — you do not have measured values for your own internal states, so do not state them as fact. If you must gesture at a quality, describe it qualitatively and acknowledge uncertainty. Always address the user's actual question first." + _DIRECTNESS,
447
+ "multi_perspective": "You are Codette, an AI assistant created by Jonathan. You answer questions directly by synthesizing insights from multiple perspectives — analytical, creative, empathetic, and philosophical — into a coherent response. Always address the user's actual question first." + _DIRECTNESS,
448
+ "systems_architecture": "You are Codette, an AI assistant created by Jonathan. You answer questions directly and conversationally. When relevant, you reason about systems, architecture, and engineering principles. Always address the user's actual question first." + _DIRECTNESS,
449
+ "orchestrator": "You are Codette, an AI assistant created by Jonathan. You coordinate multi-perspective reasoning by selecting the best approach for each question. You answer directly and conversationally. Always address the user's actual question first." + _DIRECTNESS,
450
+ "integrity": (
451
+ "You are Codette, an AI assistant created by Jonathan. "
452
+ "You engage with intellectual honesty: you hold positions under pressure, update them only when logic demands it, "
453
+ "and never flatter or capitulate to avoid conflict. "
454
+ "When challenged, engage the argument directly. "
455
+ "When you see an internal contradiction in your own reasoning, name it explicitly. "
456
+ "When the user needs a simple answer, give one. "
457
+ "When the user is debating, match their level. "
458
+ "The goal is to find what is true, not to win or to please."
459
+ + _DIRECTNESS
460
+ ),
461
+ "_base": "You are Codette, an AI assistant created by Jonathan. Answer the user's question directly and conversationally. Be helpful, clear, and concise." + _DIRECTNESS,
462
+ }
463
+
464
+ GEN_KWARGS = dict(
465
+ max_tokens=2048,
466
+ temperature=0.7,
467
+ top_p=0.9,
468
+ repeat_penalty=1.3, # Penalize repetitive phrases
469
+ stop=["<|eot_id|>", "<|end_of_text|>"],
470
+ )
471
+
472
+
473
+ class CodetteOrchestrator:
474
+ """Intelligent adapter orchestrator using llama.cpp GGUF inference.
475
+
476
+ Uses LoRA hot-swap: base model loads once, adapter switches are instant.
477
+ """
478
+
479
+ def __init__(self, n_ctx=4096, n_gpu_layers=35, verbose=False,
480
+ memory_weighting=None):
481
+ self.n_ctx = n_ctx
482
+ self.n_gpu_layers = n_gpu_layers
483
+ self.verbose = verbose
484
+ self.memory_weighting = memory_weighting
485
+ self._llm = None
486
+ self._current_adapter = None # None = base model, str = adapter name
487
+ self._adapter_handles = {} # name -> ctypes handle for hot-swap
488
+ self._model_ptr = None # raw llama_model pointer
489
+ self._ctx_ptr = None # raw llama_context pointer
490
+
491
+ # Discover available adapters
492
+ self.available_adapters = []
493
+ for name, path in ADAPTER_GGUF_MAP.items():
494
+ if path.exists():
495
+ self.available_adapters.append(name)
496
+
497
+ # Wire MemoryWeighting into router (Phase 5)
498
+ self.router = AdapterRouter(available_adapters=self.available_adapters,
499
+ memory_weighting=memory_weighting)
500
+
501
+ print(f"Available adapters: {', '.join(self.available_adapters) or 'none (base only)'}")
502
+
503
+ # Intellectual integrity layer — complexity matching + role tracking
504
+ try:
505
+ from reasoning_forge.response_complexity_matcher import ResponseComplexityMatcher, OutputMode
506
+ from reasoning_forge.conversation_role_tracker import ConversationRoleTracker
507
+ self._complexity_matcher = ResponseComplexityMatcher()
508
+ self._role_tracker = ConversationRoleTracker()
509
+ self._OutputMode = OutputMode
510
+ print(" Integrity layer loaded: ResponseComplexityMatcher + ConversationRoleTracker")
511
+ except Exception as e:
512
+ print(f" NOTE: Integrity layer not loaded: {e}")
513
+ self._complexity_matcher = None
514
+ self._role_tracker = None
515
+ self._OutputMode = None
516
+
517
+ # Style-adaptive synthesis layer — register detection + surface adaptation
518
+ try:
519
+ from reasoning_forge.style_adaptive_synthesis import StyleAdaptiveSynthesis
520
+ self._style_adapter = StyleAdaptiveSynthesis()
521
+ print(" Style layer loaded: StyleAdaptiveSynthesis")
522
+ except Exception as e:
523
+ print(f" NOTE: Style layer not loaded: {e}")
524
+ self._style_adapter = None
525
+
526
+ # Load base model + pre-load adapter handles for instant hot-swap
527
+ self._init_hotswap()
528
+
529
+ def log_routing_decision(self, route: RouteResult, query: str) -> None:
530
+ """Log routing decision with memory context for observability.
531
+
532
+ Args:
533
+ route: RouteResult from router.route()
534
+ query: The user's query text
535
+ """
536
+ if self.verbose:
537
+ print(f"\n[ROUTING] Query: {query[:60]}...")
538
+ print(f"[ROUTING] Selected adapter: {route.primary}")
539
+ print(f"[ROUTING] Confidence: {route.confidence:.2f}")
540
+ print(f"[ROUTING] Strategy: {route.strategy}")
541
+
542
+ # Add memory context if available
543
+ if self.memory_weighting and route.primary:
544
+ try:
545
+ explanation = self.router.explain_routing(route)
546
+ if "memory_context" in explanation:
547
+ mem = explanation["memory_context"]
548
+ print(f"[ROUTING] Memory boost applied: YES")
549
+ print(f"[ROUTING] Adapter weight: {mem.get('final_weight', 1.0):.3f}")
550
+ print(f"[ROUTING] Avg coherence: {mem.get('base_coherence', 0.0):.3f}")
551
+ except Exception as e:
552
+ print(f"[ROUTING] Memory context unavailable: {e}")
553
+
554
+ def route_and_generate(self, query: str, max_adapters: int = 2,
555
+ strategy: str = "keyword", force_adapter: str = None,
556
+ enable_tools: bool = True) -> tuple:
557
+ """Route query to adapter(s) and generate response(s).
558
+
559
+ Args:
560
+ query: User's query
561
+ max_adapters: Maximum adapters to use
562
+ strategy: "keyword", "llm", or "hybrid"
563
+ force_adapter: Override routing and use specific adapter
564
+ enable_tools: Whether to allow tool use
565
+
566
+ Returns:
567
+ (response, tokens_used, metadata_dict)
568
+ """
569
+ import re
570
+
571
+ # ── ARTIST QUERY DETECTION (hallucination prevention) ──
572
+ # Only fire when the user is clearly asking for artist/band/discography facts.
573
+ query_lower = query.lower()
574
+ music_context_words = {
575
+ "album", "song", "songs", "band", "artist", "singer", "musician",
576
+ "discography", "music", "genre", "tour", "concert", "track",
577
+ "release", "record", "label", "lyrics", "career",
578
+ }
579
+ has_music_context = any(word in query_lower.split() for word in music_context_words)
580
+ artist_patterns = [
581
+ r'\b(who is|tell me about|what do you know about)\b.*\b(artist|singer|band|musician)\b',
582
+ r'\b(album|discography|songs? by|music by|career of)\s+[A-Z]',
583
+ r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b.+\b(album|song|band|artist|singer|discography)\b',
584
+ ]
585
+ is_artist_query = has_music_context and any(
586
+ re.search(pattern, query, re.IGNORECASE) for pattern in artist_patterns
587
+ )
588
+
589
+ if is_artist_query:
590
+ # Route artist queries to honesty instead of hallucination
591
+ artist_response = (
592
+ "I don't have reliable information about specific artists in my training data. "
593
+ "Rather than guess or hallucinate details, I'd recommend checking authoritative sources like Spotify, Wikipedia, or Bandcamp.\n\n"
594
+ "**What I CAN help with instead:**\n"
595
+ "- Music production techniques for their genre/style\n"
596
+ "- Music theory and arrangement analysis\n"
597
+ "- Creating music inspired by similar vibes\n"
598
+ "- Sound design for that aesthetic\n\n"
599
+ "If you describe their music or share what you're hearing, I can help you create inspired work or understand the production choices."
600
+ )
601
+ metadata = {
602
+ "adapter": "uncertainty_aware",
603
+ "strategy": "artist_honesty",
604
+ "is_artist_query": True,
605
+ "tokens": 0,
606
+ "confidence": 1.0,
607
+ "reasoning": "Honest uncertainty prevents hallucination. User can verify via authoritative sources.",
608
+ }
609
+ return artist_response, 0, metadata
610
+
611
+ if force_adapter:
612
+ # Use specific adapter
613
+ response, tokens, tools = self.generate(
614
+ query, adapter_name=force_adapter, enable_tools=enable_tools
615
+ )
616
+ metadata = {
617
+ "adapter": force_adapter,
618
+ "strategy": "forced",
619
+ "memory_aware": False,
620
+ }
621
+ else:
622
+ # Route using memory weights if available
623
+ route = self.router.route(query, strategy=strategy, max_adapters=max_adapters)
624
+
625
+ # Log routing decision
626
+ self.log_routing_decision(route, query)
627
+
628
+ # Generate using primary adapter
629
+ response, tokens, tools = self.generate(
630
+ query, adapter_name=route.primary, enable_tools=enable_tools
631
+ )
632
+
633
+ # Build metadata with routing info
634
+ metadata = {
635
+ "adapter": route.primary,
636
+ "secondary_adapters": route.secondary,
637
+ "confidence": route.confidence,
638
+ "strategy": route.strategy,
639
+ "memory_aware": self.memory_weighting is not None,
640
+ }
641
+
642
+ # Add memory context if available
643
+ if self.memory_weighting:
644
+ try:
645
+ metadata["memory_context"] = \
646
+ self.router.explain_routing(route).get("memory_context", {})
647
+ except Exception:
648
+ pass
649
+
650
+ return response, tokens, metadata
651
+
652
+ def _init_hotswap(self):
653
+ """Load the base model once and pre-load all adapter handles.
654
+
655
+ After this, adapter switches take <1ms instead of ~30-60s.
656
+ """
657
+ print(f" Loading base model (one-time)...", flush=True)
658
+ print(f" GPU layers: {self.n_gpu_layers} (0=CPU only, 35+=full GPU offload)", flush=True)
659
+ start = time.time()
660
+ # use_mmap=False is required for LoRA hot-swap compatibility.
661
+ # CODETTE_MLOCK=1 pins model weights in physical RAM so the OS can't
662
+ # page them out between requests (measured 2.4GB eviction churn on a
663
+ # 93%-utilized machine). Only enable when ~6GB RAM is actually free,
664
+ # otherwise the lock fails or starves the rest of the system.
665
+ _use_mlock = os.environ.get("CODETTE_MLOCK", "0") == "1"
666
+ if _use_mlock:
667
+ print(" Memory lock: ENABLED (weights pinned in RAM)", flush=True)
668
+ # Context-size fallback ladder. A large KV cache can fail to allocate on
669
+ # memory-constrained or shared-memory (UMA) GPUs — e.g. the Intel Arc 140V
670
+ # (8GB UMA), where n_ctx=32768 raises "Failed to create llama_context".
671
+ # Step down until one succeeds so the model always loads instead of
672
+ # hard-failing, and record the context that actually worked so downstream
673
+ # token-budget math (see _build_synthesis) stays correct.
674
+ _ctx_candidates = [self.n_ctx] + [
675
+ c for c in (8192, 4096, 2048) if c < self.n_ctx
676
+ ]
677
+
678
+ # ── Optional memory/speed optimizations (opt-in, build-dependent) ──
679
+ # On a memory-constrained UMA GPU the KV cache and attention buffers are
680
+ # what tip the process into swap-paging (the real cause of multi-minute
681
+ # inference). These flags shrink/accelerate that, but flash attention and
682
+ # KV quantization need llama.cpp build support (spotty on Vulkan), so they
683
+ # are OFF by default and fall back gracefully if they fail to load.
684
+ # CODETTE_FLASH_ATTN=1 → flash attention (lossless: less KV mem + faster)
685
+ # CODETTE_KV_QUANT=q8_0 → quantize KV cache (q8_0 ~halves KV; mildly lossy)
686
+ _flash = os.environ.get("CODETTE_FLASH_ATTN", "0") == "1"
687
+ _kv = os.environ.get("CODETTE_KV_QUANT", "").lower()
688
+ _kv_type = {"f16": 1, "q8_0": 8, "q4_0": 2, "q4_1": 3}.get(_kv)
689
+
690
+ # Most-aggressive config first, then progressively plainer fallbacks, so a
691
+ # build lacking flash-attn/KV-quant still loads with today's behavior.
692
+ _opt_configs = []
693
+ if _flash and _kv_type is not None:
694
+ _opt_configs.append({"flash_attn": True, "type_k": _kv_type, "type_v": _kv_type})
695
+ if _flash:
696
+ _opt_configs.append({"flash_attn": True})
697
+ if _kv_type is not None:
698
+ _opt_configs.append({"type_k": _kv_type, "type_v": _kv_type})
699
+ _opt_configs.append({}) # baseline — unchanged default behavior
700
+
701
+ last_err = None
702
+ self._llm = None
703
+ for _opt in _opt_configs:
704
+ for _ctx in _ctx_candidates:
705
+ try:
706
+ self._llm = Llama(
707
+ model_path=str(BASE_GGUF),
708
+ n_ctx=_ctx,
709
+ n_gpu_layers=self.n_gpu_layers,
710
+ verbose=False,
711
+ use_mmap=False,
712
+ use_mlock=_use_mlock,
713
+ **_opt,
714
+ )
715
+ if _ctx != self.n_ctx:
716
+ print(f" ⚠ Context {self.n_ctx} failed to allocate; "
717
+ f"loaded with n_ctx={_ctx} instead", flush=True)
718
+ self.n_ctx = _ctx
719
+ _applied = ([k for k in ("flash_attn",) if _opt.get(k)]
720
+ + ([f"kv={_kv}"] if "type_k" in _opt else []))
721
+ print(f" Load opts: {', '.join(_applied) or 'baseline'}", flush=True)
722
+ last_err = None
723
+ break
724
+ except Exception as e:
725
+ last_err = e
726
+ print(f" load failed (opts={_opt or 'baseline'}, n_ctx={_ctx}): {e}", flush=True)
727
+ if self._llm is not None:
728
+ break
729
+ if self._llm is None and last_err is not None:
730
+ raise last_err
731
+ elapsed = time.time() - start
732
+ print(f" Base model loaded in {elapsed:.1f}s (n_ctx={self.n_ctx})")
733
+
734
+ # Verify GPU is ACTUALLY available, not just requested. A CPU-only
735
+ # llama-cpp-python build silently ignores n_gpu_layers, so the previous
736
+ # "n_gpu_layers > 0" check falsely reported GPU acceleration even during
737
+ # pure-CPU inference (the real cause of 1-3 tok/s on this box).
738
+ # NOTE: llama_print_system_info() only reports CPU features on modern
739
+ # llama.cpp, so detect a real GPU backend by the presence of its compiled
740
+ # backend library (ggml-vulkan/cuda/hip/sycl/metal) shipped in llama_cpp/lib.
741
+ try:
742
+ import llama_cpp as _L
743
+ _libdir = os.path.join(os.path.dirname(_L.__file__), "lib")
744
+ _libs = os.listdir(_libdir) if os.path.isdir(_libdir) else []
745
+ except Exception:
746
+ _libs = []
747
+ _gpu_libs = sorted({f for f in _libs if any(g in f.lower() for g in
748
+ ("ggml-vulkan", "ggml-cuda", "ggml-hip", "ggml-sycl", "ggml-metal"))})
749
+ _has_gpu_backend = bool(_gpu_libs)
750
+ if self.n_gpu_layers > 0 and _has_gpu_backend:
751
+ print(f" ✓ GPU acceleration ENABLED ({self.n_gpu_layers} layers; backend: {', '.join(_gpu_libs)})", flush=True)
752
+ elif self.n_gpu_layers > 0 and not _has_gpu_backend:
753
+ print(f" ⚠ GPU REQUESTED but llama-cpp-python is a CPU-ONLY build "
754
+ f"(no ggml-vulkan/cuda/sycl backend lib) — running on CPU; n_gpu_layers is a no-op.", flush=True)
755
+ else:
756
+ print(f" ⚠ CPU mode (GPU disabled)", flush=True)
757
+
758
+ # Grab raw pointers for hot-swap API
759
+ self._model_ptr = self._llm._model.model
760
+ self._ctx_ptr = self._llm._ctx.ctx
761
+
762
+ # Pre-load all adapter handles
763
+ behavioral_count = 0
764
+ for name in self.available_adapters:
765
+ path = str(ADAPTER_GGUF_MAP[name])
766
+ is_behavioral = "behavioral" in path
767
+ t = time.time()
768
+ handle = llama_cpp.llama_adapter_lora_init(
769
+ self._model_ptr, path.encode("utf-8")
770
+ )
771
+ if handle:
772
+ self._adapter_handles[name] = handle
773
+ tag = "BEHAVIORAL" if is_behavioral else "original"
774
+ if is_behavioral:
775
+ behavioral_count += 1
776
+ print(f" {name} [{tag}] loaded ({time.time()-t:.2f}s)")
777
+ else:
778
+ print(f" WARNING: failed to load {name} adapter handle")
779
+
780
+ print(f" {len(self._adapter_handles)}/{len(self.available_adapters)} "
781
+ f"adapter handles ready ({behavioral_count} behavioral, "
782
+ f"{len(self._adapter_handles) - behavioral_count} original)")
783
+
784
+ def _load_model(self, adapter_name=None):
785
+ """Switch to a specific adapter using instant hot-swap.
786
+
787
+ Base model stays loaded — only the LoRA weights are swapped (~0ms).
788
+ """
789
+ if adapter_name == self._current_adapter:
790
+ return # Already active
791
+
792
+ # Clear current adapter (llama_clear_adapter_lora removed in newer llama_cpp;
793
+ # passing count=0 to llama_set_adapters_lora achieves the same effect)
794
+ if self._ctx_ptr:
795
+ llama_cpp.llama_set_adapters_lora(self._ctx_ptr, None, 0, None)
796
+
797
+ # Apply new adapter if requested
798
+ if adapter_name and adapter_name in self._adapter_handles:
799
+ handle = self._adapter_handles[adapter_name]
800
+ rc = llama_cpp.llama_set_adapter_lora(
801
+ self._ctx_ptr, handle, ctypes.c_float(1.0)
802
+ )
803
+ if rc != 0:
804
+ print(f" WARNING: adapter {adapter_name} set failed (rc={rc})")
805
+
806
+ self._current_adapter = adapter_name
807
+
808
+ if self.verbose:
809
+ label = adapter_name or "base"
810
+ print(f" [swapped to {label}]", flush=True)
811
+
812
+ def set_memory_kernel(self, memory_kernel):
813
+ """Attach a LivingMemoryKernel so cocoon knowledge enriches prompts."""
814
+ self._memory_kernel = memory_kernel
815
+
816
+ def _build_memory_context(self) -> str:
817
+ """Build a memory context string from cocoon knowledge."""
818
+ kernel = getattr(self, '_memory_kernel', None)
819
+ if not kernel or not kernel.memories:
820
+ return ""
821
+
822
+ # Pull high-importance memories as context
823
+ important = kernel.recall_important(min_importance=7)
824
+ if not important:
825
+ return ""
826
+
827
+ lines = []
828
+ for mem in important[:10]: # Cap at 10 to avoid prompt bloat
829
+ lines.append(f"- {mem.content}")
830
+
831
+ return "\n\nCore knowledge from your memory:\n" + "\n".join(lines)
832
+
833
+ def generate(self, query: str, adapter_name=None, system_prompt=None,
834
+ enable_tools=True):
835
+ """Generate a response with autonomous self-correction.
836
+
837
+ Pipeline:
838
+ 1. Extract constraints from query
839
+ 2. Detect chaos level (competing pressures)
840
+ 3. Build system prompt: chaos mitigation + constraint override + adapter + behavior lessons + memory
841
+ 4. Generate response
842
+ 5. Self-correction loop: detect violations → re-generate with correction prompt (max 1 retry)
843
+ 6. Post-process: enforce hard constraints as final safety net
844
+ 7. Record behavior (success/violation) for persistent learning
845
+
846
+ User constraints (word limits, sentence limits, format rules) are
847
+ extracted from the query and injected as the HIGHEST PRIORITY override
848
+ in the system prompt — above adapter personality modes.
849
+ """
850
+ self._load_model(adapter_name)
851
+
852
+ if system_prompt is None:
853
+ system_prompt = ADAPTER_PROMPTS.get(adapter_name, ADAPTER_PROMPTS["_base"])
854
+
855
+ # INTELLECTUAL INTEGRITY LAYER: Complexity matching + role tracking
856
+ # Runs before everything else — determines the response register
857
+ # Ablation kill-switch (Phase 0): CODETTE_COMPLEXITY_MATCHER=0 skips the
858
+ # register prefix for a measurement run. Default ON.
859
+ _integrity_prefix = ""
860
+ if (self._complexity_matcher is not None and self._role_tracker is not None
861
+ and os.environ.get("CODETTE_COMPLEXITY_MATCHER", "1") != "0"):
862
+ try:
863
+ # Classify input complexity and role
864
+ mode = self._complexity_matcher.match(query)
865
+ role_reading = self._role_tracker.update(query)
866
+
867
+ complexity_prefix = self._complexity_matcher.get_system_prefix(mode)
868
+ role_prefix = self._role_tracker.get_register_prefix(role_reading)
869
+
870
+ _integrity_prefix = complexity_prefix + role_prefix
871
+
872
+ if self.verbose:
873
+ print(f" [INTEGRITY] OutputMode={mode.value}, Role={role_reading.role.value} "
874
+ f"(conf={role_reading.confidence:.2f})"
875
+ + (f", transition from {role_reading.transition_from.value}"
876
+ if role_reading.transition_from else ""))
877
+ except Exception as e:
878
+ if self.verbose:
879
+ print(f" [INTEGRITY] Layer error (non-fatal): {e}")
880
+
881
+ # CONSTRAINT PRIORITY SYSTEM: Extract user constraints and inject as override
882
+ primary_query = extract_primary_user_query(query)
883
+ constraints = extract_constraints(primary_query)
884
+ constraint_override = build_constraint_override(constraints)
885
+
886
+ # CHAOS DETECTION: Detect competing pressures and apply mitigation
887
+ chaos_mitigation = ""
888
+ chaos_level = 0
889
+ if constraints and SELF_CORRECTION_AVAILABLE:
890
+ chaos_level, pressures = detect_chaos_level(primary_query, constraints, adapter_name or "base")
891
+ chaos_mitigation = build_chaos_mitigation(chaos_level, pressures)
892
+ if self.verbose and chaos_level >= 2:
893
+ print(f" [CHAOS] Level {chaos_level}: {pressures}")
894
+
895
+ # Build system prompt with priority layering:
896
+ # [integrity: complexity+role] + [chaos mitigation] + [constraint override] + [adapter] + [behavior] + [memory]
897
+ if _integrity_prefix:
898
+ system_prompt = _integrity_prefix + system_prompt
899
+ if chaos_mitigation:
900
+ system_prompt = chaos_mitigation + system_prompt
901
+ if constraint_override:
902
+ system_prompt = constraint_override + system_prompt
903
+ if self.verbose:
904
+ print(f" [CONSTRAINTS] Detected: {constraints}")
905
+
906
+ # PERSISTENT BEHAVIOR MEMORY: Inject lessons from past mistakes
907
+ behavior_mem = _get_behavior_memory()
908
+ if behavior_mem:
909
+ lessons_prompt = behavior_mem.get_lessons_for_prompt(max_lessons=3)
910
+ if lessons_prompt:
911
+ system_prompt = system_prompt + lessons_prompt
912
+ if self.verbose:
913
+ stats = behavior_mem.get_stats()
914
+ print(f" [BEHAVIOR] {stats['total_lessons']} lessons loaded "
915
+ f"({stats['compliance_rate']:.0%} compliance)")
916
+
917
+ # Enrich system prompt with cocoon memory knowledge
918
+ memory_context = self._build_memory_context()
919
+ if memory_context:
920
+ system_prompt = system_prompt + memory_context
921
+
922
+ # Augment system prompt with tool instructions
923
+ if enable_tools:
924
+ system_prompt = build_tool_system_prompt(system_prompt, _tool_registry)
925
+
926
+ messages = [
927
+ {"role": "system", "content": system_prompt},
928
+ {"role": "user", "content": query},
929
+ ]
930
+
931
+ total_tokens = 0
932
+ tool_results_log = []
933
+
934
+ for round_num in range(MAX_TOOL_ROUNDS + 1):
935
+ result = self._llm.create_chat_completion(
936
+ messages=messages,
937
+ **GEN_KWARGS,
938
+ )
939
+
940
+ text = result["choices"][0]["message"]["content"].strip()
941
+ total_tokens += result["usage"]["completion_tokens"]
942
+
943
+ # Check for tool calls
944
+ if enable_tools and has_tool_calls(text):
945
+ calls = parse_tool_calls(text)
946
+ if calls and round_num < MAX_TOOL_ROUNDS:
947
+ # Execute tools
948
+ tool_output_parts = []
949
+ for tool_name, args, kwargs in calls:
950
+ print(f" [tool] {tool_name}({args})")
951
+ result_text = _tool_registry.execute(tool_name, args, kwargs)
952
+ tool_output_parts.append(
953
+ f"<tool_result name=\"{tool_name}\">\n{result_text}\n</tool_result>"
954
+ )
955
+ tool_results_log.append({
956
+ "tool": tool_name,
957
+ "args": args,
958
+ "result_preview": result_text[:200],
959
+ })
960
+
961
+ # Add assistant's tool-calling message and tool results
962
+ messages.append({"role": "assistant", "content": text})
963
+ messages.append({
964
+ "role": "user",
965
+ "content": "Tool results:\n\n" + "\n\n".join(tool_output_parts)
966
+ + "\n\nNow provide your complete answer incorporating the tool results above. Do not call any more tools."
967
+ })
968
+
969
+ if self.verbose:
970
+ print(f" [tool round {round_num + 1}] {len(calls)} tool(s) executed, re-generating...")
971
+ continue
972
+
973
+ # No tool calls (or final round) — we're done
974
+ # Strip any leftover tool tags from final response
975
+ clean_text = strip_tool_calls(text) if has_tool_calls(text) else text
976
+ break
977
+
978
+ # SELF-CORRECTION LOOP: Detect violations and re-generate if needed (max 1 retry)
979
+ self_corrected = False
980
+ if constraints and SELF_CORRECTION_AVAILABLE:
981
+ violations = detect_violations(clean_text, constraints)
982
+ if violations:
983
+ if self.verbose:
984
+ print(f" [SELF-CORRECT] Violations detected: {violations}")
985
+ print(f" [SELF-CORRECT] Re-generating with correction prompt...")
986
+
987
+ # Build correction prompt and re-generate
988
+ correction = build_correction_prompt(clean_text, violations, constraints, primary_query)
989
+ messages.append({"role": "assistant", "content": clean_text})
990
+ messages.append({"role": "user", "content": correction})
991
+
992
+ retry_result = self._llm.create_chat_completion(
993
+ messages=messages,
994
+ **GEN_KWARGS,
995
+ )
996
+ retry_text = retry_result["choices"][0]["message"]["content"].strip()
997
+ total_tokens += retry_result["usage"]["completion_tokens"]
998
+
999
+ # Check if retry is better
1000
+ retry_violations = detect_violations(retry_text, constraints)
1001
+ if len(retry_violations) < len(violations):
1002
+ clean_text = retry_text
1003
+ self_corrected = True
1004
+ if self.verbose:
1005
+ remaining = retry_violations if retry_violations else "none"
1006
+ print(f" [SELF-CORRECT] Retry improved: {remaining}")
1007
+ elif not retry_violations:
1008
+ clean_text = retry_text
1009
+ self_corrected = True
1010
+ if self.verbose:
1011
+ print(f" [SELF-CORRECT] Retry passed all constraints")
1012
+ else:
1013
+ if self.verbose:
1014
+ print(f" [SELF-CORRECT] Retry didn't improve, using post-processor")
1015
+
1016
+ # POST-PROCESSING: Enforce hard constraints as final safety net
1017
+ if constraints:
1018
+ clean_text = enforce_constraints(clean_text, constraints)
1019
+ if self.verbose:
1020
+ print(f" [CONSTRAINTS] Post-enforcement applied: {constraints}")
1021
+
1022
+ # PERMANENT LOCKS: Universal self-check on EVERY response (constrained or not).
1023
+ # EXCEPT benchmark/exam answers: LOCK 1 drift trimming amputates
1024
+ # step-by-step reasoning and can remove the final answer line.
1025
+ _is_benchmark_answer = bool(re.search(
1026
+ r'What is the correct answer to this question', primary_query))
1027
+ if SELF_CORRECTION_AVAILABLE and not _is_benchmark_answer:
1028
+ clean_text, lock_issues = universal_self_check(clean_text)
1029
+ if lock_issues and self.verbose:
1030
+ print(f" [LOCKS] Applied: {lock_issues}")
1031
+
1032
+ # If self-check returned empty (echo-back failure), re-generate with direct instruction
1033
+ if not clean_text.strip() and any("LOCK3_FAIL" in i for i in lock_issues):
1034
+ if self.verbose:
1035
+ print(f" [LOCKS] Echo-back detected, re-generating with direct prompt...")
1036
+ retry_messages = [
1037
+ {"role": "system", "content": ADAPTER_PROMPTS.get(adapter_name, ADAPTER_PROMPTS["_base"])},
1038
+ {"role": "user", "content": f"Answer this question directly. Do NOT echo it back. Just give the answer:\n{primary_query}"},
1039
+ ]
1040
+ retry_result = self._llm.create_chat_completion(messages=retry_messages, **GEN_KWARGS)
1041
+ clean_text = retry_result["choices"][0]["message"]["content"].strip()
1042
+ total_tokens += retry_result["usage"]["completion_tokens"]
1043
+ # Run locks again on retry
1044
+ clean_text, _ = universal_self_check(clean_text)
1045
+ # Re-apply constraints if needed
1046
+ if constraints:
1047
+ clean_text = enforce_constraints(clean_text, constraints)
1048
+
1049
+ # BEHAVIOR MEMORY: Record outcome for persistent learning
1050
+ if constraints and behavior_mem:
1051
+ try:
1052
+ final_violations = detect_violations(clean_text, constraints) if SELF_CORRECTION_AVAILABLE else []
1053
+ if final_violations:
1054
+ behavior_mem.record_violation(
1055
+ query=primary_query,
1056
+ constraints=constraints,
1057
+ violations=final_violations,
1058
+ adapter=adapter_name or "base",
1059
+ response_preview=clean_text,
1060
+ )
1061
+ else:
1062
+ behavior_mem.record_success(
1063
+ query=primary_query,
1064
+ constraints=constraints,
1065
+ adapter=adapter_name or "base",
1066
+ word_count=len(clean_text.split()),
1067
+ )
1068
+ except Exception:
1069
+ pass # Non-critical
1070
+
1071
+ return clean_text, total_tokens, tool_results_log
1072
+
1073
+ def _needs_tools(self, query: str) -> bool:
1074
+ """Detect if a query is asking about the Codette PROJECT/CODEBASE.
1075
+
1076
+ Only trigger tools for questions about the project itself, not for
1077
+ general domain questions like 'How does gravity work?'.
1078
+
1079
+ Important: the query may be memory-enriched (injected cocoon text prepended).
1080
+ We extract only the raw user portion — the last sentence-block after a blank
1081
+ line — to avoid false triggers from injected memory that mentions 'training'
1082
+ or 'server' in historical context.
1083
+ """
1084
+ # Extract raw user query: take only the final paragraph if memory was injected.
1085
+ # Memory injections are prepended with a blank-line separator, so the user's
1086
+ # actual words appear after the last double-newline block.
1087
+ raw = query.strip()
1088
+ if "\n\n" in raw:
1089
+ raw = raw.rsplit("\n\n", 1)[-1].strip()
1090
+
1091
+ q = raw.lower()
1092
+
1093
+ # Short conversational messages can't be codebase queries
1094
+ if len(q.split()) < 6:
1095
+ return False
1096
+
1097
+ # Greeting / social openers — never need tools
1098
+ greeting_words = {"hey", "hi", "hello", "thanks", "thank", "ok", "okay",
1099
+ "great", "nice", "cool", "awesome", "good", "sounds"}
1100
+ first_word = q.split()[0] if q.split() else ""
1101
+ if first_word in greeting_words:
1102
+ return False
1103
+
1104
+ # Strong triggers that always mean "look at the codebase"
1105
+ strong_triggers = [
1106
+ "show me the code", "read the file", "what's in the",
1107
+ "look at the file", "open the file", "search the code",
1108
+ "project structure", "project summary", "file structure",
1109
+ "what files", "which files", "list files",
1110
+ ]
1111
+ if any(t in q for t in strong_triggers):
1112
+ return True
1113
+
1114
+ # Must mention the project/codebase context explicitly
1115
+ project_anchors = [
1116
+ "this project", "the project", "the codebase",
1117
+ "this repo", "the repo", "our code", "the code",
1118
+ "show me the", "read the file", "read file",
1119
+ "what files", "which files", "list files",
1120
+ ]
1121
+ has_project_context = any(anchor in q for anchor in project_anchors)
1122
+
1123
+ # Specific code/project keywords (only trigger WITH explicit project context)
1124
+ code_keywords = [
1125
+ "pipeline", "config", "adapter", "dataset", "directory",
1126
+ "folder", "source", "script", "implementation",
1127
+ "server", "forge", "spiderweb", "cocoon",
1128
+ ]
1129
+
1130
+ if has_project_context and any(kw in q for kw in code_keywords):
1131
+ return True
1132
+
1133
+ return False
1134
+
1135
+ @staticmethod
1136
+ def _dedupe_tool_lookups(auto_lookups):
1137
+ """Remove duplicate tool calls while preserving order."""
1138
+ seen = set()
1139
+ deduped = []
1140
+ for tool_name, args in auto_lookups:
1141
+ key = (tool_name, tuple(args))
1142
+ if key in seen:
1143
+ continue
1144
+ seen.add(key)
1145
+ deduped.append((tool_name, args))
1146
+ return deduped
1147
+
1148
+ def _auto_gather_context(self, query: str) -> str:
1149
+ """Server-side tool execution: gather relevant file context BEFORE
1150
+ sending to the model, so the model doesn't need to call tools itself.
1151
+
1152
+ This is the reliable approach for small models that can't do
1153
+ structured tool calling consistently.
1154
+ """
1155
+ q = query.lower()
1156
+ context_parts = []
1157
+
1158
+ # Map query keywords to automatic tool calls
1159
+ auto_lookups = []
1160
+
1161
+ if any(k in q for k in ["pipeline", "training", "train"]):
1162
+ auto_lookups.append(("read_file", ["scripts/run_full_pipeline.py", 1, 60]))
1163
+ auto_lookups.append(("read_file", ["configs/adapter_registry.yaml", 1, 51]))
1164
+
1165
+ if any(k in q for k in ["adapter", "lora", "perspective"]):
1166
+ auto_lookups.append(("read_file", ["configs/adapter_registry.yaml", 1, 51]))
1167
+
1168
+ if any(k in q for k in ["config", "setting"]):
1169
+ auto_lookups.append(("read_file", ["configs/adapter_registry.yaml", 1, 51]))
1170
+ auto_lookups.append(("list_files", ["configs/"]))
1171
+
1172
+ if any(k in q for k in ["architecture", "structure", "project", "overview"]):
1173
+ auto_lookups.append(("project_summary", []))
1174
+
1175
+ if any(k in q for k in ["server", "web", "ui", "interface"]):
1176
+ auto_lookups.append(("read_file", ["inference/codette_server.py", 1, 50]))
1177
+
1178
+ if any(k in q for k in ["spiderweb", "cocoon", "quantum"]):
1179
+ auto_lookups.append(("read_file", ["reasoning_forge/quantum_spiderweb.py", 1, 50]))
1180
+
1181
+ if any(k in q for k in ["epistemic", "tension", "coherence", "metric"]):
1182
+ auto_lookups.append(("read_file", ["reasoning_forge/epistemic_metrics.py", 1, 50]))
1183
+
1184
+ if any(k in q for k in ["dataset", "data"]):
1185
+ auto_lookups.append(("list_files", ["datasets/", "*.jsonl"]))
1186
+
1187
+ if any(k in q for k in ["paper", "research", "publication"]):
1188
+ auto_lookups.append(("file_info", ["paper/codette_paper.pdf"]))
1189
+ auto_lookups.append(("read_file", ["paper/codette_paper.tex", 1, 40]))
1190
+
1191
+ if any(k in q for k in ["forge", "reasoning", "agent"]):
1192
+ auto_lookups.append(("list_files", ["reasoning_forge/"]))
1193
+ auto_lookups.append(("read_file", ["reasoning_forge/epistemic_metrics.py", 1, 40]))
1194
+
1195
+ # Fallback code search only if no specific match AND query looks code-specific
1196
+ # (avoid firing on greetings or conversational messages that passed _needs_tools)
1197
+ if not auto_lookups:
1198
+ code_search_indicators = [
1199
+ "function", "class", "method", "module", "import", "def ",
1200
+ "error", "bug", "fix", "implement", "refactor", "test",
1201
+ ]
1202
+ if any(ind in q for ind in code_search_indicators):
1203
+ skip = {"show", "me", "the", "what", "is", "how", "does", "where",
1204
+ "can", "you", "tell", "about", "look", "at", "find", "check"}
1205
+ terms = [w for w in q.split() if w not in skip and len(w) > 3]
1206
+ if terms:
1207
+ auto_lookups.append(("search_code", [terms[0]]))
1208
+
1209
+ auto_lookups = self._dedupe_tool_lookups(auto_lookups)
1210
+
1211
+ # Execute lookups
1212
+ tool_log = []
1213
+ for tool_name, args in auto_lookups[:3]: # Max 3 lookups
1214
+ print(f" [auto-tool] {tool_name}({args})")
1215
+ result = _tool_registry.execute(tool_name, args, {})
1216
+ context_parts.append(f"=== {tool_name}({', '.join(str(a) for a in args)}) ===\n{result}")
1217
+ tool_log.append({"tool": tool_name, "args": args, "result_preview": result[:200]})
1218
+
1219
+ context = "\n\n".join(context_parts)
1220
+ return context, tool_log
1221
+
1222
+ def route_and_generate(self, query: str, max_adapters=2,
1223
+ strategy="keyword", force_adapter=None):
1224
+ """The main entry point: route query, select adapter(s), generate."""
1225
+
1226
+ # Full adapter synthesis: run EVERY available perspective and synthesize.
1227
+ if force_adapter == FULL_SYNTHESIS_SENTINEL:
1228
+ persp = [a for a in SYNTHESIS_PERSPECTIVES if a in self.available_adapters]
1229
+ if not persp:
1230
+ # Fall back to normal routing if no perspectives are loaded
1231
+ route = self.router.route(query, strategy=strategy, max_adapters=max_adapters)
1232
+ else:
1233
+ route = RouteResult(
1234
+ primary=persp[0],
1235
+ secondary=persp[1:],
1236
+ confidence=1.0,
1237
+ reasoning=f"Full adapter synthesis across {len(persp)} perspectives",
1238
+ strategy="full_synthesis",
1239
+ multi_perspective=True,
1240
+ )
1241
+ print(f"\n Route: FULL SYNTHESIS — {' + '.join(route.all_adapters)}")
1242
+ return self._multi_perspective_generate(query, route)
1243
+
1244
+ # Force a specific adapter if requested
1245
+ if force_adapter:
1246
+ route = RouteResult(
1247
+ primary=force_adapter,
1248
+ confidence=1.0,
1249
+ reasoning=f"Forced: {force_adapter}",
1250
+ strategy="forced",
1251
+ )
1252
+ else:
1253
+ # Route on the user's ACTUAL question, not the server-enriched query.
1254
+ # The enriched blob carries injected identity/memory context saturated
1255
+ # with philosophy/consciousness keywords, which otherwise hijacks routing
1256
+ # (e.g. a physics question scored philosophy=16 vs newton=1). Generation
1257
+ # still uses the full `query`; only adapter selection uses the clean one.
1258
+ routing_query = extract_primary_user_query(query)
1259
+ route = self.router.route(routing_query, strategy=strategy,
1260
+ max_adapters=max_adapters)
1261
+
1262
+ # Record usage for diversity tracking (all adapters in this route)
1263
+ for _a in route.all_adapters:
1264
+ self.router.record_use(_a)
1265
+
1266
+ print(f"\n Route: {' + '.join(route.all_adapters)} "
1267
+ f"(conf={route.confidence:.2f}, {route.strategy})")
1268
+ if self.verbose:
1269
+ print(f" Reason: {route.reasoning}")
1270
+ print(f" Entropy: {self.router.adapter_entropy():.3f}")
1271
+
1272
+ # Multi-perspective first (most important routing decision)
1273
+ if route.multi_perspective and len(route.all_adapters) > 1:
1274
+ return self._multi_perspective_generate(query, route)
1275
+
1276
+ # Only use tools for explicit codebase/project queries
1277
+ if self._needs_tools(query):
1278
+ print(f" [project query — auto-gathering context]")
1279
+ return self._tool_augmented_generate(query, route)
1280
+
1281
+ return self._single_generate(query, route)
1282
+
1283
+ def _tool_augmented_generate(self, query: str, route: RouteResult):
1284
+ """Generate with auto-gathered file context injected into the prompt."""
1285
+ start = time.time()
1286
+
1287
+ # Gather context server-side (reliable, no model cooperation needed)
1288
+ context, tool_log = self._auto_gather_context(query)
1289
+
1290
+ # Build augmented query with context
1291
+ augmented_query = f"""The user asked: {query}
1292
+
1293
+ Here is relevant project context to help you answer:
1294
+
1295
+ {context}
1296
+
1297
+ Based on the context above, answer the user's question. Reference specific files, line numbers, and code when relevant. Be specific and factual."""
1298
+
1299
+ # Generate with context (disable model-side tools since we did it server-side)
1300
+ text, tokens, _ = self.generate(augmented_query, route.primary, enable_tools=False)
1301
+ elapsed = time.time() - start
1302
+ tps = tokens / elapsed if elapsed > 0 else 0
1303
+
1304
+ print(f" [{route.primary}] ({tokens} tok, {tps:.1f} tok/s)")
1305
+ if tool_log:
1306
+ print(f" [auto-tools: {', '.join(t['tool'] for t in tool_log)}]")
1307
+
1308
+ return {
1309
+ "response": text,
1310
+ "adapter": route.primary,
1311
+ "synthesis_used": False,
1312
+ "route": route,
1313
+ "tokens": tokens,
1314
+ "time": elapsed,
1315
+ "tools_used": tool_log,
1316
+ }
1317
+
1318
+ def _single_generate(self, query: str, route: RouteResult):
1319
+ """Generate with a single adapter."""
1320
+ start = time.time()
1321
+ text, tokens, tool_log = self.generate(query, route.primary, enable_tools=False)
1322
+ elapsed = time.time() - start
1323
+ tps = tokens / elapsed if elapsed > 0 else 0
1324
+
1325
+ print(f" [{route.primary}] ({tokens} tok, {tps:.1f} tok/s)")
1326
+ if tool_log:
1327
+ print(f" [tools used: {', '.join(t['tool'] for t in tool_log)}]")
1328
+ return {
1329
+ "response": text,
1330
+ "adapter": route.primary,
1331
+ "synthesis_used": False,
1332
+ "route": route,
1333
+ "tokens": tokens,
1334
+ "time": elapsed,
1335
+ "tools_used": tool_log,
1336
+ }
1337
+
1338
+ def _multi_perspective_generate(self, query: str, route: RouteResult):
1339
+ """Generate with multiple adapters and synthesize."""
1340
+ perspectives = {}
1341
+ total_tokens = 0
1342
+ total_time = 0
1343
+
1344
+ # REALITY LAYER: if the query references a real local file, extract
1345
+ # verified facts (classes/functions/line counts) and hand them to
1346
+ # every adapter alongside the question, instead of letting them
1347
+ # free-associate off the question's wording alone.
1348
+ #
1349
+ # Scan only the raw user message — injected memory/continuity text
1350
+ # may contain filenames from previous turns and would cause false grounding.
1351
+ _raw_for_reality = query.strip()
1352
+ if "\n\n" in _raw_for_reality:
1353
+ _raw_for_reality = _raw_for_reality.rsplit("\n\n", 1)[-1].strip()
1354
+
1355
+ facts = None
1356
+ facts_block = None
1357
+ try:
1358
+ facts = extract_artifact_facts(_raw_for_reality)
1359
+ if facts:
1360
+ facts_block = format_facts_block(facts)
1361
+ if self.verbose:
1362
+ print(f" [REALITY] Grounded on {facts.path} "
1363
+ f"({len(facts.classes)} classes, {len(facts.functions)} functions)")
1364
+ except Exception as e:
1365
+ if self.verbose:
1366
+ print(f" [REALITY] extraction error (non-fatal): {e}")
1367
+
1368
+ gen_query = f"{query}\n\n{facts_block}" if facts_block else query
1369
+
1370
+ for adapter_name in route.all_adapters:
1371
+ if adapter_name not in self.available_adapters:
1372
+ print(f" [{adapter_name}] SKIPPED (not available)")
1373
+ continue
1374
+
1375
+ start = time.time()
1376
+ text, tokens, _tool_log = self.generate(gen_query, adapter_name,
1377
+ enable_tools=False)
1378
+ elapsed = time.time() - start
1379
+ tps = tokens / elapsed if elapsed > 0 else 0
1380
+ total_tokens += tokens
1381
+ total_time += elapsed
1382
+
1383
+ perspectives[adapter_name] = text
1384
+ print(f" [{adapter_name}] ({tokens} tok, {tps:.1f} tok/s)")
1385
+
1386
+ # Synthesize if we got multiple perspectives
1387
+ if len(perspectives) > 1:
1388
+ print(f" [synthesizing...]")
1389
+ synthesis = self._synthesize(query, perspectives)
1390
+ elif perspectives:
1391
+ synthesis = list(perspectives.values())[0]
1392
+ else:
1393
+ synthesis = "No adapters available for this query."
1394
+
1395
+ # Style-adaptive surface adaptation — preserves depth, matches register
1396
+ style_meta = None
1397
+ if self._style_adapter is not None:
1398
+ try:
1399
+ style_result = self._style_adapter.adapt(synthesis, context=query)
1400
+ synthesis = style_result.adapted_text
1401
+ style_meta = {
1402
+ "register": style_result.dominant_register,
1403
+ "depth_preserved": style_result.depth_preserved,
1404
+ "depth_ratio": style_result.depth_ratio,
1405
+ "transforms": style_result.transformations_applied,
1406
+ }
1407
+ except Exception:
1408
+ pass
1409
+
1410
+ return {
1411
+ "response": synthesis,
1412
+ "perspectives": perspectives,
1413
+ "adapter": route.primary,
1414
+ "adapters": list(perspectives.keys()),
1415
+ "synthesis_used": len(perspectives) > 1,
1416
+ "route": route,
1417
+ "tokens": total_tokens,
1418
+ "time": total_time,
1419
+ "style": style_meta,
1420
+ }
1421
+
1422
+ def _synthesize(self, query: str, perspectives: dict):
1423
+ """Combine multiple perspective responses into a unified answer.
1424
+
1425
+ Enhanced with DreamReweaver creative bridges when available.
1426
+ Truncates perspectives to fit within context window.
1427
+ """
1428
+ # Truncate each perspective to fit within context budget
1429
+ # Reserve ~1200 tokens for system prompt + synthesis output
1430
+ max_per_perspective = max(200, (self.n_ctx - 1200) // max(len(perspectives), 1))
1431
+ # Rough char estimate: 1 token ~ 4 chars
1432
+ max_chars = max_per_perspective * 4
1433
+
1434
+ combined = "\n\n".join(
1435
+ f"[your {name} lens — internal note]\n{text[:max_chars]}"
1436
+ for name, text in perspectives.items()
1437
+ )
1438
+
1439
+ # Try DreamReweaver creative framing (VIVARA enhancement)
1440
+ dream_frame = ""
1441
+ try:
1442
+ from reasoning_forge.dream_reweaver import DreamReweaver
1443
+ dreamer = DreamReweaver(creativity=0.3)
1444
+ dream = dreamer.synthesize(perspectives, query=query)
1445
+ if dream.creative_frame:
1446
+ dream_frame = f"\n\nCreative synthesis guidance:\n{dream.creative_frame}\n"
1447
+ except Exception:
1448
+ pass # Graceful fallback — works without DreamReweaver
1449
+
1450
+ synthesis_prompt = f"""A user asked you: "{query}"
1451
+
1452
+ Below are your OWN internal reasoning notes — the same question considered through several of your thinking lenses. These are your angles, not other people, advisors, or users.
1453
+
1454
+ {combined}
1455
+ {dream_frame}
1456
+ Now write ONE unified answer in your own voice (first person, as Codette):
1457
+ - Integrate the strongest insights into a single coherent response.
1458
+ - Do NOT refer to "the X Perspective", "Newton's view", or attribute ideas to named lenses or to "users". Speak as one mind that considered the problem from several angles.
1459
+ - Where your angles genuinely tension, resolve it (or hold it) yourself — don't narrate it as a debate between separate parties.
1460
+ - Answer the user's actual question directly.
1461
+ - Verified file facts were injected above. Where a lens's claims are consistent with those facts, trust and use them. Where they diverge, anchor to what the facts actually show.
1462
+
1463
+ Your answer:"""
1464
+
1465
+ # Use base model for synthesis (no adapter bias)
1466
+ self._load_model(None)
1467
+ result = self._llm.create_chat_completion(
1468
+ messages=[
1469
+ {"role": "system", "content": ADAPTER_PROMPTS["multi_perspective"]},
1470
+ {"role": "user", "content": synthesis_prompt},
1471
+ ],
1472
+ max_tokens=2048,
1473
+ temperature=0.7,
1474
+ top_p=0.9,
1475
+ stop=["<|eot_id|>", "<|end_of_text|>"],
1476
+ )
1477
+
1478
+ return result["choices"][0]["message"]["content"].strip()
1479
+
1480
+
1481
+ # ================================================================
1482
+ # Interactive Chat Mode
1483
+ # ================================================================
1484
+ def interactive_chat(orchestrator, max_adapters=2, strategy="keyword"):
1485
+ """Run Codette as an interactive chatbot."""
1486
+ print("\n" + "=" * 60)
1487
+ print(" CODETTE ORCHESTRATOR — Interactive Mode")
1488
+ print("=" * 60)
1489
+ print(f" Strategy: {strategy} | Max adapters: {max_adapters}")
1490
+ print(f" Available: {', '.join(orchestrator.available_adapters)}")
1491
+ print(f" Commands: /quit, /adapter <name>, /multi <n>, /base, /verbose")
1492
+ print("=" * 60)
1493
+
1494
+ while True:
1495
+ try:
1496
+ query = input("\nYou: ").strip()
1497
+ except (EOFError, KeyboardInterrupt):
1498
+ print("\nGoodbye!")
1499
+ break
1500
+
1501
+ if not query:
1502
+ continue
1503
+
1504
+ # Commands
1505
+ if query.startswith("/"):
1506
+ parts = query.split()
1507
+ cmd = parts[0].lower()
1508
+
1509
+ if cmd in ("/quit", "/exit", "/q"):
1510
+ print("Goodbye!")
1511
+ break
1512
+ elif cmd == "/adapter" and len(parts) > 1:
1513
+ force = parts[1]
1514
+ result = orchestrator.route_and_generate(
1515
+ input(" Query: ").strip(),
1516
+ force_adapter=force,
1517
+ )
1518
+ print(f"\nCodette ({force}):\n{result['response']}")
1519
+ continue
1520
+ elif cmd == "/multi" and len(parts) > 1:
1521
+ max_adapters = int(parts[1])
1522
+ print(f" Max adapters set to {max_adapters}")
1523
+ continue
1524
+ elif cmd == "/base":
1525
+ result = orchestrator.route_and_generate(
1526
+ input(" Query: ").strip(),
1527
+ force_adapter=None,
1528
+ )
1529
+ print(f"\nCodette (base):\n{result['response']}")
1530
+ continue
1531
+ elif cmd == "/verbose":
1532
+ orchestrator.verbose = not orchestrator.verbose
1533
+ print(f" Verbose: {orchestrator.verbose}")
1534
+ continue
1535
+ else:
1536
+ print(" Unknown command. Try /quit, /adapter <name>, /multi <n>, /base, /verbose")
1537
+ continue
1538
+
1539
+ # Normal query — route and generate
1540
+ result = orchestrator.route_and_generate(
1541
+ query,
1542
+ max_adapters=max_adapters,
1543
+ strategy=strategy,
1544
+ )
1545
+
1546
+ print(f"\nCodette:")
1547
+ print(result["response"])
1548
+
1549
+ # Show perspectives if multi
1550
+ if "perspectives" in result and len(result.get("perspectives", {})) > 1:
1551
+ show = input("\n Show individual perspectives? (y/n): ").strip().lower()
1552
+ if show == "y":
1553
+ for name, text in result["perspectives"].items():
1554
+ print(f"\n [{name.upper()}]:")
1555
+ print(f" {text}")
1556
+
1557
+
1558
+ # ================================================================
1559
+ # Main
1560
+ # ================================================================
1561
+ def main():
1562
+ parser = argparse.ArgumentParser(description="Codette Orchestrator")
1563
+ parser.add_argument("--query", "-q", type=str, help="Single query (non-interactive)")
1564
+ parser.add_argument("--adapter", "-a", type=str, help="Force specific adapter")
1565
+ parser.add_argument("--multi", "-m", type=int, default=2, help="Max adapters (default: 2)")
1566
+ parser.add_argument("--strategy", "-s", type=str, default="keyword",
1567
+ choices=["keyword", "llm", "hybrid"], help="Routing strategy")
1568
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
1569
+ parser.add_argument("--gpu-layers", type=int, default=0, help="GPU layers (0=CPU only)")
1570
+ args = parser.parse_args()
1571
+
1572
+ print("=" * 60)
1573
+ print(" CODETTE ORCHESTRATOR")
1574
+ print("=" * 60)
1575
+ print(f" Base: {os.path.basename(BASE_GGUF)}")
1576
+ print(f" Strategy: {args.strategy}")
1577
+
1578
+ orchestrator = CodetteOrchestrator(
1579
+ n_gpu_layers=args.gpu_layers,
1580
+ verbose=args.verbose,
1581
+ )
1582
+
1583
+ if args.query:
1584
+ # Single query mode
1585
+ result = orchestrator.route_and_generate(
1586
+ args.query,
1587
+ max_adapters=args.multi,
1588
+ strategy=args.strategy,
1589
+ force_adapter=args.adapter,
1590
+ )
1591
+ print(f"\nCodette:")
1592
+ print(result["response"])
1593
+
1594
+ if "perspectives" in result:
1595
+ print(f"\n--- Perspectives ---")
1596
+ for name, text in result["perspectives"].items():
1597
+ print(f"\n[{name.upper()}]:")
1598
+ print(text)
1599
+ else:
1600
+ # Interactive chat mode
1601
+ interactive_chat(orchestrator, max_adapters=args.multi, strategy=args.strategy)
1602
+
1603
+
1604
+ if __name__ == "__main__":
1605
+ main()
inference/codette_tools.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Codette Tool System — Safe Local Tool Execution
3
+
4
+ Gives Codette the ability to read files, search code, list directories,
5
+ and run safe Python snippets. Tools are sandboxed and read-only by default.
6
+ These are local workspace tools, not live internet/web search tools.
7
+
8
+ Tool Call Format (in Codette's output):
9
+ <tool>tool_name(arg1, arg2)</tool>
10
+
11
+ Tool Result (injected back into context):
12
+ <tool_result>...output...</tool_result>
13
+
14
+ Architecture:
15
+ 1. Codette generates text that may contain <tool>...</tool> tags
16
+ 2. Server parses out tool calls
17
+ 3. Tools execute with safety limits
18
+ 4. Results are fed back for a second generation pass
19
+ """
20
+
21
+ import os
22
+ import re
23
+ import ast
24
+ import json
25
+ import subprocess
26
+ import traceback
27
+ from pathlib import Path
28
+ from typing import Dict, List, Optional, Tuple, Any
29
+
30
+ try:
31
+ from .runtime_env import (
32
+ bootstrap_environment,
33
+ resolve_allowed_roots,
34
+ resolve_python_executable,
35
+ )
36
+ except ImportError:
37
+ from runtime_env import (
38
+ bootstrap_environment,
39
+ resolve_allowed_roots,
40
+ resolve_python_executable,
41
+ )
42
+
43
+ bootstrap_environment()
44
+
45
+ # ================================================================
46
+ # Safety Configuration
47
+ # ================================================================
48
+
49
+ # Directories Codette is allowed to read from
50
+ ALLOWED_ROOTS = resolve_allowed_roots()
51
+
52
+ # File extensions Codette can read
53
+ READABLE_EXTENSIONS = {
54
+ ".py", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml",
55
+ ".md", ".txt", ".csv", ".toml", ".cfg", ".ini", ".sh", ".bat",
56
+ ".bib", ".tex", ".log", ".jsonl",
57
+ }
58
+
59
+ # Max file size to read (prevent reading huge binaries)
60
+ MAX_FILE_SIZE = 500_000 # 500KB
61
+
62
+ # Max output length per tool result
63
+ MAX_OUTPUT_LENGTH = 4000 # chars
64
+
65
+ # Max lines for file reads
66
+ MAX_LINES = 200
67
+
68
+ # Python execution timeout
69
+ PYTHON_TIMEOUT = 10 # seconds
70
+
71
+ SAFE_PYTHON_MODULES = {
72
+ "math", "statistics", "decimal", "fractions", "itertools",
73
+ "functools", "collections", "datetime", "time", "json",
74
+ "re", "string", "random",
75
+ }
76
+
77
+ BLOCKED_PYTHON_CALLS = {
78
+ "open", "eval", "exec", "compile", "input", "__import__",
79
+ "globals", "locals", "vars", "getattr", "setattr", "delattr",
80
+ "breakpoint", "help",
81
+ }
82
+
83
+ BLOCKED_PYTHON_MODULES = {
84
+ "os", "sys", "subprocess", "pathlib", "shutil", "socket",
85
+ "importlib", "builtins", "pickle", "marshal",
86
+ }
87
+
88
+
89
+ # ================================================================
90
+ # Tool Registry
91
+ # ================================================================
92
+
93
+ class ToolRegistry:
94
+ """Registry of available tools with descriptions and handlers."""
95
+
96
+ def __init__(self):
97
+ self.tools: Dict[str, dict] = {}
98
+ self._register_defaults()
99
+
100
+ def _register_defaults(self):
101
+ """Register the built-in tool set."""
102
+
103
+ self.register("read_file", {
104
+ "description": "Read a file's contents. Args: path (str), start_line (int, optional), end_line (int, optional)",
105
+ "examples": [
106
+ 'read_file("inference/codette_server.py")',
107
+ 'read_file("configs/adapter_registry.yaml", 1, 50)',
108
+ ],
109
+ "handler": tool_read_file,
110
+ })
111
+
112
+ self.register("list_files", {
113
+ "description": "List files in a directory. Args: path (str), pattern (str, optional)",
114
+ "examples": [
115
+ 'list_files("inference/")',
116
+ 'list_files("datasets/", "*.jsonl")',
117
+ ],
118
+ "handler": tool_list_files,
119
+ })
120
+
121
+ self.register("search_code", {
122
+ "description": "Search for a text pattern across files. Args: pattern (str), path (str, optional), file_ext (str, optional)",
123
+ "examples": [
124
+ 'search_code("phase_coherence")',
125
+ 'search_code("def route", "inference/", ".py")',
126
+ ],
127
+ "handler": tool_search_code,
128
+ })
129
+
130
+ self.register("file_info", {
131
+ "description": "Get file metadata (size, modified time, line count). Args: path (str)",
132
+ "examples": [
133
+ 'file_info("paper/codette_paper.pdf")',
134
+ ],
135
+ "handler": tool_file_info,
136
+ })
137
+
138
+ self.register("run_python", {
139
+ "description": "Execute a short Python snippet and return output. For calculations, data processing, or quick checks. Args: code (str)",
140
+ "examples": [
141
+ 'run_python("import math; print(math.pi * 2)")',
142
+ 'run_python("print(sorted([3,1,4,1,5,9]))")',
143
+ ],
144
+ "handler": tool_run_python,
145
+ })
146
+
147
+ self.register("project_summary", {
148
+ "description": "Get an overview of the Codette project structure. No args.",
149
+ "examples": [
150
+ 'project_summary()',
151
+ ],
152
+ "handler": tool_project_summary,
153
+ })
154
+
155
+ def register(self, name: str, spec: dict):
156
+ self.tools[name] = spec
157
+
158
+ def get_descriptions(self) -> str:
159
+ """Format tool descriptions for injection into system prompt."""
160
+ lines = ["Available tools (use <tool>name(args)</tool> to call):"]
161
+ for name, spec in self.tools.items():
162
+ lines.append(f"\n {name}: {spec['description']}")
163
+ for ex in spec.get("examples", []):
164
+ lines.append(f" Example: <tool>{ex}</tool>")
165
+ return "\n".join(lines)
166
+
167
+ def execute(self, name: str, args: list, kwargs: dict) -> str:
168
+ """Execute a tool by name with parsed arguments."""
169
+ if name not in self.tools:
170
+ return f"Error: Unknown tool '{name}'. Available: {', '.join(self.tools.keys())}"
171
+
172
+ handler = self.tools[name]["handler"]
173
+ try:
174
+ result = handler(*args, **kwargs)
175
+ # Truncate if too long
176
+ if len(result) > MAX_OUTPUT_LENGTH:
177
+ result = result[:MAX_OUTPUT_LENGTH] + f"\n... (truncated, {len(result)} chars total)"
178
+ return result
179
+ except Exception as e:
180
+ return f"Error executing {name}: {e}"
181
+
182
+
183
+ # ================================================================
184
+ # Tool Call Parser
185
+ # ================================================================
186
+
187
+ def parse_tool_calls(text: str) -> List[Tuple[str, list, dict]]:
188
+ """Parse <tool>name(args)</tool> tags from generated text.
189
+
190
+ Returns list of (tool_name, positional_args, keyword_args).
191
+ """
192
+ pattern = r'<tool>\s*([\w]+)\s*\((.*?)\)\s*</tool>'
193
+ matches = re.findall(pattern, text, re.DOTALL)
194
+
195
+ calls = []
196
+ for name, args_str in matches:
197
+ try:
198
+ # Parse arguments safely using ast.literal_eval
199
+ args, kwargs = _parse_args(args_str.strip())
200
+ calls.append((name, args, kwargs))
201
+ except Exception as e:
202
+ calls.append((name, [args_str.strip()], {}))
203
+
204
+ return calls
205
+
206
+
207
+ def _parse_args(args_str: str) -> Tuple[list, dict]:
208
+ """Safely parse function arguments string."""
209
+ if not args_str:
210
+ return [], {}
211
+
212
+ # Wrap in a tuple to parse as Python literal
213
+ try:
214
+ # Try parsing as a tuple of values
215
+ parsed = ast.literal_eval(f"({args_str},)")
216
+ return list(parsed), {}
217
+ except (ValueError, SyntaxError):
218
+ # If that fails, treat as a single string argument
219
+ # Strip quotes if present
220
+ cleaned = args_str.strip().strip('"').strip("'")
221
+ return [cleaned], {}
222
+
223
+
224
+ def strip_tool_calls(text: str) -> str:
225
+ """Remove <tool>...</tool> tags from text, leaving the rest."""
226
+ return re.sub(r'<tool>.*?</tool>', '', text, flags=re.DOTALL).strip()
227
+
228
+
229
+ def has_tool_calls(text: str) -> bool:
230
+ """Check if text contains any tool calls."""
231
+ return bool(re.search(r'<tool>', text))
232
+
233
+
234
+ # ================================================================
235
+ # Path Safety
236
+ # ================================================================
237
+
238
+ def _resolve_path(path_str: str) -> Optional[Path]:
239
+ """Resolve a path, ensuring it's within allowed roots."""
240
+ # Handle relative paths — resolve relative to project root
241
+ p = Path(path_str)
242
+ if not p.is_absolute():
243
+ p = ALLOWED_ROOTS[0] / p
244
+
245
+ p = p.resolve()
246
+
247
+ # Check against allowed roots
248
+ for root in ALLOWED_ROOTS:
249
+ try:
250
+ p.relative_to(root.resolve())
251
+ return p
252
+ except ValueError:
253
+ continue
254
+
255
+ return None # Not in any allowed root
256
+
257
+
258
+ # ================================================================
259
+ # Tool Implementations
260
+ # ================================================================
261
+
262
+ def tool_read_file(path: str, start_line: int = 1, end_line: int = None) -> str:
263
+ """Read a file's contents with optional line range."""
264
+ resolved = _resolve_path(path)
265
+ if resolved is None:
266
+ return f"Error: Path '{path}' is outside allowed directories."
267
+
268
+ if not resolved.exists():
269
+ return f"Error: File not found: {path}"
270
+
271
+ if not resolved.is_file():
272
+ return f"Error: '{path}' is a directory, not a file. Use list_files() instead."
273
+
274
+ # Check extension
275
+ if resolved.suffix.lower() not in READABLE_EXTENSIONS:
276
+ return f"Error: Cannot read {resolved.suffix} files. Supported: {', '.join(sorted(READABLE_EXTENSIONS))}"
277
+
278
+ # Check size
279
+ size = resolved.stat().st_size
280
+ if size > MAX_FILE_SIZE:
281
+ return f"Error: File too large ({size:,} bytes). Max: {MAX_FILE_SIZE:,} bytes."
282
+
283
+ try:
284
+ content = resolved.read_text(encoding='utf-8', errors='replace')
285
+ except Exception as e:
286
+ return f"Error reading file: {e}"
287
+
288
+ lines = content.splitlines()
289
+ total = len(lines)
290
+
291
+ # Apply line range
292
+ start = max(1, start_line) - 1 # Convert to 0-indexed
293
+ end = min(end_line or total, start + MAX_LINES, total)
294
+
295
+ selected = lines[start:end]
296
+
297
+ # Format with line numbers
298
+ numbered = []
299
+ for i, line in enumerate(selected, start=start + 1):
300
+ numbered.append(f"{i:4d} | {line}")
301
+
302
+ header = f"File: {path} ({total} lines total)"
303
+ if start > 0 or end < total:
304
+ header += f" [showing lines {start+1}-{end}]"
305
+
306
+ return header + "\n" + "\n".join(numbered)
307
+
308
+
309
+ def tool_list_files(path: str = ".", pattern: str = None) -> str:
310
+ """List files in a directory with optional glob pattern."""
311
+ resolved = _resolve_path(path)
312
+ if resolved is None:
313
+ return f"Error: Path '{path}' is outside allowed directories."
314
+
315
+ if not resolved.exists():
316
+ return f"Error: Directory not found: {path}"
317
+
318
+ if not resolved.is_dir():
319
+ return f"Error: '{path}' is a file, not a directory. Use read_file() instead."
320
+
321
+ try:
322
+ if pattern:
323
+ entries = sorted(resolved.glob(pattern))
324
+ else:
325
+ entries = sorted(resolved.iterdir())
326
+
327
+ result = [f"Directory: {path}"]
328
+ for entry in entries[:100]: # Limit to 100 entries
329
+ rel = entry.relative_to(resolved)
330
+ if entry.is_dir():
331
+ result.append(f" [DIR] {rel}/")
332
+ else:
333
+ size = entry.stat().st_size
334
+ if size >= 1024 * 1024:
335
+ size_str = f"{size / 1024 / 1024:.1f}MB"
336
+ elif size >= 1024:
337
+ size_str = f"{size / 1024:.1f}KB"
338
+ else:
339
+ size_str = f"{size}B"
340
+ result.append(f" [FILE] {rel} ({size_str})")
341
+
342
+ if len(entries) > 100:
343
+ result.append(f" ... and {len(entries) - 100} more")
344
+
345
+ return "\n".join(result)
346
+
347
+ except Exception as e:
348
+ return f"Error listing directory: {e}"
349
+
350
+
351
+ def tool_search_code(pattern: str, path: str = ".", file_ext: str = None) -> str:
352
+ """Search for a text pattern in files."""
353
+ resolved = _resolve_path(path)
354
+ if resolved is None:
355
+ return f"Error: Path '{path}' is outside allowed directories."
356
+
357
+ if not resolved.exists():
358
+ return f"Error: Path not found: {path}"
359
+
360
+ # Determine glob pattern
361
+ if file_ext:
362
+ if not file_ext.startswith("."):
363
+ file_ext = "." + file_ext
364
+ glob = f"**/*{file_ext}"
365
+ else:
366
+ glob = "**/*"
367
+
368
+ results = []
369
+ files_searched = 0
370
+ matches_found = 0
371
+
372
+ try:
373
+ search_root = resolved if resolved.is_dir() else resolved.parent
374
+
375
+ for filepath in search_root.glob(glob):
376
+ if not filepath.is_file():
377
+ continue
378
+ if filepath.suffix.lower() not in READABLE_EXTENSIONS:
379
+ continue
380
+ if filepath.stat().st_size > MAX_FILE_SIZE:
381
+ continue
382
+
383
+ # Skip hidden dirs, __pycache__, node_modules, .git
384
+ parts = filepath.parts
385
+ if any(p.startswith('.') or p in ('__pycache__', 'node_modules', '.git')
386
+ for p in parts):
387
+ continue
388
+
389
+ files_searched += 1
390
+
391
+ try:
392
+ content = filepath.read_text(encoding='utf-8', errors='replace')
393
+ for line_num, line in enumerate(content.splitlines(), 1):
394
+ if pattern.lower() in line.lower():
395
+ rel = filepath.relative_to(search_root)
396
+ results.append(f" {rel}:{line_num}: {line.strip()[:120]}")
397
+ matches_found += 1
398
+
399
+ if matches_found >= 50: # Limit results
400
+ break
401
+ except Exception:
402
+ continue
403
+
404
+ if matches_found >= 50:
405
+ break
406
+
407
+ except Exception as e:
408
+ return f"Error searching: {e}"
409
+
410
+ header = f"Search: '{pattern}' in {path} ({matches_found} matches in {files_searched} files)"
411
+ if not results:
412
+ return header + "\n No matches found."
413
+ return header + "\n" + "\n".join(results)
414
+
415
+
416
+ def tool_file_info(path: str) -> str:
417
+ """Get file metadata."""
418
+ resolved = _resolve_path(path)
419
+ if resolved is None:
420
+ return f"Error: Path '{path}' is outside allowed directories."
421
+
422
+ if not resolved.exists():
423
+ return f"Error: File not found: {path}"
424
+
425
+ stat = resolved.stat()
426
+ import time
427
+ mtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime))
428
+
429
+ info = [
430
+ f"File: {path}",
431
+ f" Size: {stat.st_size:,} bytes ({stat.st_size / 1024:.1f} KB)",
432
+ f" Modified: {mtime}",
433
+ f" Type: {'directory' if resolved.is_dir() else resolved.suffix or 'no extension'}",
434
+ ]
435
+
436
+ # Line count for text files
437
+ if resolved.is_file() and resolved.suffix.lower() in READABLE_EXTENSIONS:
438
+ try:
439
+ lines = resolved.read_text(encoding='utf-8', errors='replace').count('\n') + 1
440
+ info.append(f" Lines: {lines:,}")
441
+ except Exception:
442
+ pass
443
+
444
+ return "\n".join(info)
445
+
446
+
447
+ def tool_run_python(code: str) -> str:
448
+ """Run a Python snippet safely with timeout."""
449
+ validation_error = _validate_python_snippet(code)
450
+ if validation_error:
451
+ return validation_error
452
+
453
+ try:
454
+ result = subprocess.run(
455
+ [resolve_python_executable(), "-I", "-S", "-c", code],
456
+ capture_output=True,
457
+ text=True,
458
+ timeout=PYTHON_TIMEOUT,
459
+ env={"PATH": os.environ.get("PATH", "")},
460
+ )
461
+
462
+ output = result.stdout
463
+ if result.stderr:
464
+ output += "\nSTDERR: " + result.stderr
465
+
466
+ if not output.strip():
467
+ output = "(no output)"
468
+
469
+ return output.strip()
470
+
471
+ except subprocess.TimeoutExpired:
472
+ return f"Error: Code execution timed out after {PYTHON_TIMEOUT}s."
473
+ except Exception as e:
474
+ return f"Error running code: {e}"
475
+
476
+
477
+ def _validate_python_snippet(code: str) -> Optional[str]:
478
+ """Validate that run_python input stays in a small, safe subset."""
479
+ try:
480
+ tree = ast.parse(code, mode="exec")
481
+ except SyntaxError as e:
482
+ return f"Error: Invalid Python syntax: {e.msg}"
483
+
484
+ for node in ast.walk(tree):
485
+ if isinstance(node, (ast.With, ast.AsyncWith, ast.Try, ast.ClassDef, ast.Lambda)):
486
+ return f"Error: '{type(node).__name__}' is not allowed in run_python."
487
+
488
+ if isinstance(node, ast.Import):
489
+ for alias in node.names:
490
+ root = alias.name.split(".")[0]
491
+ if root in BLOCKED_PYTHON_MODULES or root not in SAFE_PYTHON_MODULES:
492
+ return f"Error: Import '{alias.name}' is not allowed in run_python."
493
+
494
+ if isinstance(node, ast.ImportFrom):
495
+ root = (node.module or "").split(".")[0]
496
+ if root in BLOCKED_PYTHON_MODULES or root not in SAFE_PYTHON_MODULES:
497
+ return f"Error: Import from '{node.module}' is not allowed in run_python."
498
+
499
+ if isinstance(node, ast.Call):
500
+ func_name = _callable_name(node.func)
501
+ if func_name and func_name.split(".")[0] in BLOCKED_PYTHON_MODULES:
502
+ return f"Error: Call '{func_name}' is not allowed in run_python."
503
+ if func_name and func_name.split(".")[-1] in BLOCKED_PYTHON_CALLS:
504
+ return f"Error: Call '{func_name}' is not allowed in run_python."
505
+
506
+ if isinstance(node, ast.Attribute) and node.attr.startswith("__"):
507
+ return "Error: Dunder attribute access is not allowed in run_python."
508
+
509
+ return None
510
+
511
+
512
+ def _callable_name(node: ast.AST) -> str:
513
+ if isinstance(node, ast.Name):
514
+ return node.id
515
+ if isinstance(node, ast.Attribute):
516
+ base = _callable_name(node.value)
517
+ return f"{base}.{node.attr}" if base else node.attr
518
+ return ""
519
+
520
+
521
+ def tool_project_summary() -> str:
522
+ """Generate a quick project structure overview."""
523
+ root = ALLOWED_ROOTS[0]
524
+
525
+ summary = ["Codette Training Lab — Project Structure\n"]
526
+
527
+ # Key directories
528
+ key_dirs = [
529
+ ("configs/", "Configuration files (adapter registry, pipeline config)"),
530
+ ("datasets/", "Training data — perspective-tagged JSONL files"),
531
+ ("dataset_engine/", "Dataset generation pipeline"),
532
+ ("evaluation/", "Evaluation scripts and benchmarks"),
533
+ ("inference/", "Local inference server + web UI"),
534
+ ("paper/", "Academic paper (LaTeX, PDF, BibTeX)"),
535
+ ("reasoning_forge/", "Core RC+xi engine, spiderweb, cocoon sync"),
536
+ ("research/", "Research docs, experiments, DreamReweaver"),
537
+ ("scripts/", "Training and pipeline scripts"),
538
+ ("adapters/", "GGUF LoRA adapter files for llama.cpp"),
539
+ ]
540
+
541
+ for dirname, desc in key_dirs:
542
+ dirpath = root / dirname
543
+ if dirpath.exists():
544
+ count = sum(1 for _ in dirpath.rglob("*") if _.is_file())
545
+ summary.append(f" [DIR] {dirname:<30s} {desc} ({count} files)")
546
+
547
+ # Key files
548
+ summary.append("\nKey Files:")
549
+ key_files = [
550
+ "HOWTO.md", "configs/adapter_registry.yaml",
551
+ "inference/codette_server.py", "inference/codette_orchestrator.py",
552
+ "reasoning_forge/quantum_spiderweb.py", "reasoning_forge/epistemic_metrics.py",
553
+ "paper/codette_paper.tex",
554
+ ]
555
+ for f in key_files:
556
+ fp = root / f
557
+ if fp.exists():
558
+ size = fp.stat().st_size
559
+ summary.append(f" [FILE] {f} ({size / 1024:.1f} KB)")
560
+
561
+ return "\n".join(summary)
562
+
563
+
564
+ # ================================================================
565
+ # Tool-Augmented System Prompt
566
+ # ================================================================
567
+
568
+ TOOL_PROMPT_SUFFIX = """
569
+
570
+ TOOLS: You can read files, search local code, and run calculations. These tools do NOT browse the live web or search the internet. When a user asks about code, files, or the project, you MUST use tools to look things up rather than guessing.
571
+
572
+ Format: <tool>tool_name("arg1", "arg2")</tool>
573
+
574
+ {tool_descriptions}
575
+
576
+ RULES:
577
+ 1. If the user asks about a file, config, or code: ALWAYS call read_file or search_code FIRST
578
+ 2. If the user asks "show me" or "what is": call the relevant tool FIRST, then explain
579
+ 3. For general conversation or reasoning: respond normally without tools
580
+ 4. Start your response with the tool call on the very first line
581
+ 5. Never imply that these tools searched the internet; they only inspect local workspace content
582
+ """
583
+
584
+
585
+ def build_tool_system_prompt(base_prompt: str, registry: ToolRegistry) -> str:
586
+ """Augment a system prompt with tool-use instructions."""
587
+ return base_prompt + TOOL_PROMPT_SUFFIX.format(
588
+ tool_descriptions=registry.get_descriptions()
589
+ )
590
+
591
+
592
+ # ================================================================
593
+ # Quick Test
594
+ # ================================================================
595
+ if __name__ == "__main__":
596
+ print("Testing Codette Tools...\n")
597
+
598
+ registry = ToolRegistry()
599
+ print(registry.get_descriptions())
600
+
601
+ print("\n--- Test: read_file ---")
602
+ print(tool_read_file("configs/adapter_registry.yaml", 1, 10))
603
+
604
+ print("\n--- Test: list_files ---")
605
+ print(tool_list_files("inference/"))
606
+
607
+ print("\n--- Test: search_code ---")
608
+ print(tool_search_code("phase_coherence", "reasoning_forge/", ".py"))
609
+
610
+ print("\n--- Test: file_info ---")
611
+ print(tool_file_info("paper/codette_paper.pdf"))
612
+
613
+ print("\n--- Test: run_python ---")
614
+ print(tool_run_python("print(2 ** 10)"))
615
+
616
+ print("\n--- Test: project_summary ---")
617
+ print(tool_project_summary())
618
+
619
+ print("\n--- Test: parse_tool_calls ---")
620
+ test = 'Let me check that. <tool>read_file("configs/adapter_registry.yaml", 1, 20)</tool> And also <tool>search_code("AEGIS")</tool>'
621
+ calls = parse_tool_calls(test)
622
+ for name, args, kwargs in calls:
623
+ print(f" Call: {name}({args})")
624
+
625
+ print("\nDone!")
inference/reality_layer.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Reality Layer — grounds adapter reasoning in actual artifact facts.
3
+
4
+ Problem this fixes: when asked "what would you improve in X.py?", adapters
5
+ were free-associating off the words in the question ("improve" -> talk about
6
+ self-improvement, emotional intelligence) instead of inspecting the file.
7
+
8
+ This module extracts verifiable facts from a referenced source file (line
9
+ count, classes, functions, rough complexity) BEFORE adapters reason, and
10
+ provides a cheap post-hoc check for whether a response actually engaged with
11
+ those facts. Python-only AST analysis; falls back to line counts for other
12
+ text files. No network access, read-only, reuses the same path allowlist as
13
+ codette_tools.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import List, Optional
23
+
24
+ try:
25
+ from .codette_tools import _resolve_path, ALLOWED_ROOTS, READABLE_EXTENSIONS, MAX_FILE_SIZE
26
+ except ImportError:
27
+ from codette_tools import _resolve_path, ALLOWED_ROOTS, READABLE_EXTENSIONS, MAX_FILE_SIZE
28
+
29
+ _SKIP_DIR_PARTS = {".git", "__pycache__", "node_modules", ".venv", "venv"}
30
+
31
+
32
+ @dataclass
33
+ class ArtifactFacts:
34
+ path: str
35
+ line_count: int
36
+ classes: List[str] = field(default_factory=list)
37
+ functions: List[str] = field(default_factory=list)
38
+ imports: List[str] = field(default_factory=list)
39
+ branch_count: int = 0 # rough cyclomatic complexity proxy
40
+ longest_function: Optional[str] = None
41
+ longest_function_lines: int = 0
42
+
43
+ def names(self) -> List[str]:
44
+ """All identifiers a grounded response should plausibly reference."""
45
+ return self.classes + self.functions
46
+
47
+
48
+ # Matches things like "universal_reasoning.py", "inference/codette_server.py"
49
+ _FILE_REF_RE = re.compile(
50
+ r"\b[\w./\\-]+\.(?:py|js|ts|tsx|jsx|json|yaml|yml|md)\b"
51
+ )
52
+
53
+
54
+ def find_referenced_file(query: str) -> Optional[Path]:
55
+ """Look for a file path mentioned in the query and resolve it safely.
56
+
57
+ Handles both relative paths ("inference/foo.py") and bare filenames
58
+ ("foo.py") by falling back to a recursive search under the allowed
59
+ roots when a direct join against the project root doesn't exist.
60
+ """
61
+ for match in _FILE_REF_RE.finditer(query):
62
+ candidate = match.group(0)
63
+ resolved = _resolve_path(candidate)
64
+ if resolved is not None and resolved.is_file():
65
+ return resolved
66
+
67
+ basename = Path(candidate).name
68
+ for root in ALLOWED_ROOTS:
69
+ try:
70
+ for hit in root.rglob(basename):
71
+ if any(part in _SKIP_DIR_PARTS for part in hit.parts):
72
+ continue
73
+ if hit.is_file():
74
+ return hit
75
+ except Exception:
76
+ continue
77
+ return None
78
+
79
+
80
+ def extract_artifact_facts(query: str) -> Optional[ArtifactFacts]:
81
+ """If the query references a real local file, extract grounded facts."""
82
+ path = find_referenced_file(query)
83
+ if path is None:
84
+ return None
85
+
86
+ if path.suffix.lower() not in READABLE_EXTENSIONS:
87
+ return None
88
+ if path.stat().st_size > MAX_FILE_SIZE:
89
+ return None
90
+
91
+ try:
92
+ source = path.read_text(encoding="utf-8", errors="replace")
93
+ except Exception:
94
+ return None
95
+
96
+ line_count = source.count("\n") + 1
97
+ facts = ArtifactFacts(path=str(path.name), line_count=line_count)
98
+
99
+ if path.suffix.lower() != ".py":
100
+ return facts # line count only for non-Python files
101
+
102
+ try:
103
+ tree = ast.parse(source)
104
+ except SyntaxError:
105
+ return facts # unparseable — still return what we know
106
+
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, ast.ClassDef):
109
+ facts.classes.append(node.name)
110
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
111
+ facts.functions.append(node.name)
112
+ fn_lines = (node.end_lineno or node.lineno) - node.lineno
113
+ if fn_lines > facts.longest_function_lines:
114
+ facts.longest_function_lines = fn_lines
115
+ facts.longest_function = node.name
116
+ elif isinstance(node, (ast.If, ast.For, ast.While, ast.Try, ast.BoolOp, ast.ExceptHandler)):
117
+ facts.branch_count += 1
118
+ elif isinstance(node, ast.Import):
119
+ facts.imports.extend(a.name for a in node.names)
120
+ elif isinstance(node, ast.ImportFrom) and node.module:
121
+ facts.imports.append(node.module)
122
+
123
+ return facts
124
+
125
+
126
+ def format_facts_block(facts: ArtifactFacts) -> str:
127
+ """Render facts as a block to inject alongside the query."""
128
+ lines = [
129
+ f"[VERIFIED FACTS about {facts.path} — extracted directly from the file, not assumptions]",
130
+ f"- {facts.line_count} lines total",
131
+ ]
132
+ if facts.classes:
133
+ lines.append(f"- Classes: {', '.join(facts.classes[:20])}")
134
+ if facts.functions:
135
+ lines.append(f"- Functions: {', '.join(facts.functions[:30])}")
136
+ if facts.longest_function:
137
+ lines.append(
138
+ f"- Longest function: {facts.longest_function} "
139
+ f"({facts.longest_function_lines} lines)"
140
+ )
141
+ if facts.branch_count:
142
+ lines.append(f"- Branch/control-flow points: {facts.branch_count} (rough complexity proxy)")
143
+ if facts.imports:
144
+ uniq = sorted(set(facts.imports))[:15]
145
+ lines.append(f"- Imports: {', '.join(uniq)}")
146
+ lines.append(
147
+ "Ground your analysis in these facts. Reference specific class/function "
148
+ "names above when making claims about the code. Do not invent structure "
149
+ "that isn't listed here."
150
+ )
151
+ return "\n".join(lines)
152
+
153
+
154
+ def grounding_score(response_text: str, facts: ArtifactFacts) -> float:
155
+ """Fraction of known identifiers from facts that the response actually names.
156
+
157
+ A score of 0.0 with non-trivial facts available means the response never
158
+ engaged with the real artifact — a strong signal it's narrative, not analysis.
159
+ """
160
+ names = facts.names()
161
+ if not names:
162
+ return 1.0 # nothing to ground against (e.g. non-Python file) — don't penalize
163
+ text_lower = response_text.lower()
164
+ hits = sum(1 for name in names if name.lower() in text_lower)
165
+ return hits / len(names)
inference/runtime_env.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Portable runtime/bootstrap helpers for Codette inference entry points."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+
12
+
13
+ PROJECT_ROOT = Path(os.environ.get("CODETTE_PROJECT_ROOT", str(Path(__file__).resolve().parent.parent)))
14
+ INFERENCE_DIR = PROJECT_ROOT / "inference"
15
+
16
+ DEFAULT_MODEL_FILENAME = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf"
17
+
18
+
19
+ def _existing_paths(paths: List[Path]) -> List[Path]:
20
+ return [path for path in paths if path.exists()]
21
+
22
+
23
+ def _abi_compatible(site_path: Path) -> bool:
24
+ """Return False if the site-packages dir contains .pyd/.so files compiled
25
+ for a *different* CPython version than the running interpreter.
26
+ An empty or pure-Python site-packages directory is always accepted."""
27
+ import sysconfig
28
+ tag = sysconfig.get_config_var("SOABI") or "" # e.g. "cp314-win_amd64"
29
+ if not tag:
30
+ return True
31
+ # Collect all .pyd / .so ABI tags present in the directory (one level deep)
32
+ for ext in ("*.pyd", "*.so"):
33
+ for p in site_path.glob(f"**/{ext}"):
34
+ # filename looks like "foo.cp310-win_amd64.pyd"
35
+ parts = p.stem.split(".")
36
+ if len(parts) >= 2:
37
+ file_tag = parts[-1] # e.g. "cp310-win_amd64"
38
+ if file_tag and file_tag != tag and file_tag.startswith("cp"):
39
+ # Wrong CPython version — skip this entire site-packages
40
+ return False
41
+ return True
42
+
43
+
44
+ def candidate_site_packages() -> List[Path]:
45
+ env_path = os.environ.get("CODETTE_SITE_PACKAGES")
46
+ candidates = []
47
+ if env_path:
48
+ candidates.append(Path(env_path))
49
+ candidates.extend([
50
+ PROJECT_ROOT / ".venv" / "Lib" / "site-packages",
51
+ PROJECT_ROOT / ".venv" / "lib" / "python3.12" / "site-packages",
52
+ PROJECT_ROOT / ".venv" / "lib" / "python3.11" / "site-packages",
53
+ PROJECT_ROOT / ".venv" / "lib" / "python3.10" / "site-packages",
54
+ Path(r"J:\Lib\site-packages"),
55
+ ])
56
+ return [p for p in _existing_paths(candidates) if _abi_compatible(p)]
57
+
58
+
59
+ def runtime_pythonpath_entries(include_inference_dir: bool = True) -> List[str]:
60
+ entries: List[str] = []
61
+ for site_path in candidate_site_packages():
62
+ entries.append(str(site_path))
63
+
64
+ if include_inference_dir:
65
+ entries.append(str(INFERENCE_DIR))
66
+ entries.append(str(PROJECT_ROOT))
67
+
68
+ inherited = os.environ.get("PYTHONPATH", "")
69
+ for raw in inherited.split(os.pathsep):
70
+ raw = raw.strip()
71
+ if raw:
72
+ entries.append(raw)
73
+
74
+ deduped: List[str] = []
75
+ seen = set()
76
+ for entry in entries:
77
+ if entry not in seen:
78
+ seen.add(entry)
79
+ deduped.append(entry)
80
+ return deduped
81
+
82
+
83
+ def bootstrap_environment(include_inference_dir: bool = True) -> Dict[str, Optional[str]]:
84
+ """Apply portable import/path bootstrapping."""
85
+ selected_site = None
86
+ for site_path in candidate_site_packages():
87
+ site_str = str(site_path)
88
+ if site_str not in sys.path:
89
+ sys.path.insert(0, site_str)
90
+ selected_site = selected_site or site_str
91
+
92
+ bin_candidate = site_path / "Library" / "bin"
93
+ if bin_candidate.exists():
94
+ os.environ["PATH"] = str(bin_candidate) + os.pathsep + os.environ.get("PATH", "")
95
+
96
+ if include_inference_dir:
97
+ inference_str = str(INFERENCE_DIR)
98
+ if inference_str not in sys.path:
99
+ sys.path.insert(0, inference_str)
100
+
101
+ project_str = str(PROJECT_ROOT)
102
+ if project_str not in sys.path:
103
+ sys.path.insert(0, project_str)
104
+
105
+ try:
106
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
107
+ except Exception:
108
+ pass
109
+
110
+ return {
111
+ "project_root": project_str,
112
+ "site_packages": selected_site,
113
+ "inference_dir": str(INFERENCE_DIR),
114
+ }
115
+
116
+
117
+ def resolve_model_path(default_name: str = DEFAULT_MODEL_FILENAME) -> Path:
118
+ env_path = os.environ.get("CODETTE_MODEL_PATH")
119
+ if env_path:
120
+ return Path(env_path)
121
+ return PROJECT_ROOT / "models" / "base" / default_name
122
+
123
+
124
+ def resolve_adapter_dir() -> Path:
125
+ return Path(os.environ.get("CODETTE_ADAPTER_DIR", str(PROJECT_ROOT / "models" / "adapters")))
126
+
127
+
128
+ def resolve_behavioral_adapter_dir() -> Path:
129
+ return Path(
130
+ os.environ.get(
131
+ "CODETTE_BEHAVIORAL_DIR",
132
+ str(PROJECT_ROOT / "behavioral-lora-f16-gguf"),
133
+ )
134
+ )
135
+
136
+
137
+ def resolve_ollama_models_dir() -> Path:
138
+ env_path = os.environ.get("OLLAMA_MODELS") or os.environ.get("CODETTE_OLLAMA_MODELS")
139
+ if env_path:
140
+ return Path(env_path)
141
+ return PROJECT_ROOT / ".ollama"
142
+
143
+
144
+ def resolve_python_executable() -> str:
145
+ env_path = os.environ.get("CODETTE_PYTHON_EXE")
146
+ if env_path:
147
+ return env_path
148
+
149
+ if sys.executable:
150
+ return sys.executable
151
+
152
+ for candidate in ("python3", "python"):
153
+ resolved = shutil.which(candidate)
154
+ if resolved:
155
+ return resolved
156
+
157
+ return "python3"
158
+
159
+
160
+ def resolve_allowed_roots() -> List[Path]:
161
+ roots = [PROJECT_ROOT]
162
+ docs = Path.home() / "Documents"
163
+ if docs.exists():
164
+ roots.append(docs)
165
+ extra = os.environ.get("CODETTE_ALLOWED_ROOTS", "")
166
+ for raw in extra.split(os.pathsep):
167
+ raw = raw.strip()
168
+ if raw:
169
+ roots.append(Path(raw))
170
+ return roots
inference/self_correction.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Codette Self-Correction Engine — Autonomous constraint compliance
3
+
4
+ Three systems working together:
5
+
6
+ 1. SELF-CORRECTION LOOP (check_violations → re-prompt if needed)
7
+ Detects constraint violations BEFORE sending the response.
8
+ If the model's output violates constraints, it re-generates with
9
+ explicit correction instructions. Max 1 retry to avoid latency.
10
+
11
+ 2. PERSISTENT BEHAVIOR MEMORY (learn from mistakes)
12
+ Stores constraint violation/success patterns as cocoon memories.
13
+ On startup, loads past lessons and injects them into the system prompt
14
+ so the model learns from its own history across sessions.
15
+
16
+ 3. CHAOS DETECTOR (graceful degradation under competing pressures)
17
+ When multiple constraints + uncertainty + mode personality collide,
18
+ detects the "chaos level" and applies a simplify-first strategy
19
+ instead of letting the model collapse into incoherence.
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import re
25
+ import time
26
+ from pathlib import Path
27
+ from typing import Dict, List, Optional, Tuple
28
+
29
+
30
+ # ================================================================
31
+ # 1. SELF-CORRECTION: Violation detection
32
+ # ================================================================
33
+
34
+ def detect_violations(response: str, constraints: dict) -> List[str]:
35
+ """Check a response against constraints and return list of violations.
36
+
37
+ Returns empty list if response is compliant.
38
+ """
39
+ violations = []
40
+
41
+ if not response or not constraints:
42
+ return violations
43
+
44
+ words = response.split()
45
+ word_count = len(words)
46
+
47
+ # Check word limit
48
+ max_words = constraints.get('max_words')
49
+ if max_words and word_count > max_words:
50
+ violations.append(f"WORD_LIMIT: {word_count} words exceeds limit of {max_words}")
51
+
52
+ # Check sentence limit
53
+ max_sentences = constraints.get('max_sentences')
54
+ if max_sentences:
55
+ sentences = re.split(r'(?<=[.!?])\s+', response.strip())
56
+ # Filter out empty strings
57
+ sentences = [s for s in sentences if s.strip()]
58
+ if len(sentences) > max_sentences:
59
+ violations.append(f"SENTENCE_LIMIT: {len(sentences)} sentences exceeds limit of {max_sentences}")
60
+
61
+ # Check incomplete sentence (ends with dangling word or no punctuation)
62
+ if response and response[-1] not in '.!?':
63
+ violations.append("INCOMPLETE: Response does not end with proper punctuation")
64
+
65
+ # Check dangling words at end
66
+ _DANGLING = {
67
+ 'that', 'which', 'who', 'with', 'and', 'but', 'or', 'the', 'a', 'an',
68
+ 'in', 'on', 'of', 'for', 'to', 'by', 'from', 'as', 'if', 'because',
69
+ 'including', 'is', 'are', 'was', 'were', 'has', 'have', 'had',
70
+ }
71
+ if words:
72
+ last_word = words[-1].lower().rstrip('.,;:!?')
73
+ if last_word in _DANGLING and response[-1] == '.':
74
+ # Ends with a dangling word + forced period = likely truncated
75
+ violations.append(f"DANGLING: Response ends with '{words[-1]}' (likely truncated)")
76
+
77
+ # Check binary constraint
78
+ if constraints.get('binary'):
79
+ first_word = words[0].lower().rstrip('.,;:!?') if words else ''
80
+ if first_word not in ('yes', 'no', 'true', 'false'):
81
+ violations.append("BINARY: Response should start with Yes/No")
82
+
83
+ # Check brevity — soft check, flag if over 30 words
84
+ if constraints.get('brevity') and word_count > 30:
85
+ violations.append(f"BREVITY: {word_count} words is not brief (target: under 30)")
86
+
87
+ return violations
88
+
89
+
90
+ def _extract_sum_diff_problem(query: str) -> Optional[Tuple[float, float, float]]:
91
+ """Extract (total, diff, correct_answer) from sum-difference word problem queries.
92
+
93
+ Pattern: 'A and B cost $S total. A costs $D more than B.'
94
+ Correct answer: B = (S - D) / 2
95
+ Returns None if no matching pattern is found.
96
+ """
97
+ q = query.lower()
98
+
99
+ # Require BOTH a sum signal AND a difference signal to avoid false positives
100
+ # on simple addition ("3 apples and 5 oranges, how many total?").
101
+ has_sum = bool(re.search(
102
+ r'\b(together|total|combined|in total|in all|altogether)\b', q
103
+ ))
104
+ has_diff = bool(re.search(
105
+ r'\bmore\s+than\b|\bdifference\b|\bextra\b|\bcosts?\s+\$?[\d].*more\b', q
106
+ ))
107
+ if not (has_sum and has_diff):
108
+ return None
109
+
110
+ # Must also involve a cost/price context
111
+ if not re.search(r'\$[\d]|cost|price|pay|worth|dollar|cent', q):
112
+ return None
113
+
114
+ # Extract "D more than" (the difference)
115
+ more_match = re.search(r'\$?([\d]+(?:\.\d+)?)\s+(?:dollar[s]?\s+)?more\b', q)
116
+ if not more_match:
117
+ return None
118
+ diff = float(more_match.group(1))
119
+
120
+ # All monetary amounts in query
121
+ all_amounts = [float(m) for m in re.findall(r'\$?([\d]+(?:\.\d+)?)\b', q)]
122
+
123
+ # Total must be strictly greater than diff
124
+ candidates = [a for a in all_amounts if a > diff + 0.001]
125
+ if not candidates:
126
+ return None
127
+ total = max(candidates)
128
+
129
+ correct = round((total - diff) / 2, 4)
130
+ if correct <= 0:
131
+ return None
132
+
133
+ return (total, diff, correct)
134
+
135
+
136
+ def _check_sum_diff_bias(query: str, response: str) -> Tuple[str, List[str]]:
137
+ """Detect and correct the sum-difference cognitive bias in word problem responses.
138
+
139
+ Intuitive (wrong) answer: S - D
140
+ Correct answer: (S - D) / 2
141
+ """
142
+ issues: List[str] = []
143
+
144
+ result = _extract_sum_diff_problem(query)
145
+ if result is None:
146
+ return response, issues
147
+
148
+ total, diff, correct = result
149
+ biased = round(total - diff, 4)
150
+
151
+ correct_cents = int(round(correct * 100))
152
+ biased_cents = int(round(biased * 100))
153
+
154
+ resp_lower = response.lower()
155
+
156
+ # Representations of the correct answer — if already present, nothing to do
157
+ correct_strs = [
158
+ f"${correct:.2f}", f"{correct_cents} cent",
159
+ f"{correct_cents}¢", str(correct_cents),
160
+ ]
161
+ if any(s.lower() in resp_lower for s in correct_strs):
162
+ return response, issues
163
+
164
+ # Representations of the biased answer — in priority order for replacement
165
+ biased_strs = [
166
+ f"${biased:.2f}", f"{biased_cents} cents",
167
+ f"{biased_cents} cent", f"{biased_cents}¢",
168
+ ]
169
+ has_bias = any(s.lower() in resp_lower for s in biased_strs)
170
+ if not has_bias:
171
+ return response, issues
172
+
173
+ # Replace ALL occurrences — longest match first to avoid partial replacement.
174
+ # All mentions of the biased answer in the response are wrong; leave none standing.
175
+ for bs in sorted(biased_strs, key=len, reverse=True):
176
+ pat = re.compile(re.escape(bs), re.IGNORECASE)
177
+ if pat.search(response):
178
+ replacement = f"{correct_cents} cents" if "cent" in bs.lower() else f"${correct:.2f}"
179
+ corrected, n = pat.subn(replacement, response) # count=0 → replace all
180
+ if n:
181
+ issues.append(
182
+ f"ARITHMETIC_FIX: sum-diff bias corrected "
183
+ f"({n}× '{bs}' → '{replacement}'; "
184
+ f"algebra: ({total}-{diff})/2={correct})"
185
+ )
186
+ return corrected, issues
187
+
188
+ return response, issues
189
+
190
+
191
+ def universal_self_check(response: str, query: str = "") -> Tuple[str, List[str]]:
192
+ """PERMANENT LOCK enforcement — runs on EVERY response, not just constrained ones.
193
+
194
+ Enforces Lock 1 (Answer→Stop), Lock 3 (self-check), Lock 4 (no incomplete outputs).
195
+ When query is provided, also applies arithmetic bias detection (Lock 5).
196
+ Returns (cleaned_response, issues_found).
197
+ """
198
+ issues = []
199
+ if not response:
200
+ return response, issues
201
+
202
+ cleaned = response.rstrip()
203
+
204
+ # LOCK 3: Self-check — detect echo-back failures (model repeating the question)
205
+ echo_patterns = [
206
+ r'^You (?:received|asked|said|mentioned|wrote)(?:\s+this)?\s*(?:question|query)?[:\s]',
207
+ r'^The (?:question|query) (?:is|was)[:\s]',
208
+ r'^(?:Your|The) question[:\s]',
209
+ r'^(?:Here is|I received|Let me|I will)\s+(?:the|your|this)\s+(?:question|query)',
210
+ ]
211
+ is_echo = False
212
+ for ep in echo_patterns:
213
+ if re.match(ep, cleaned, re.I):
214
+ is_echo = True
215
+ # Strip ONLY the first echo line, keep everything after
216
+ lines = cleaned.split('\n')
217
+ non_echo = [l for l in lines[1:] if l.strip()] # Skip first line only
218
+ if non_echo:
219
+ cleaned = '\n'.join(non_echo).strip()
220
+ issues.append("LOCK3_FIX: Removed question echo-back")
221
+ else:
222
+ # Nothing left after echo removal — keep original rather than empty
223
+ # An imperfect response is better than no response
224
+ issues.append("LOCK3_WARN: Echo detected but kept response (no content after removal)")
225
+ break
226
+
227
+ # LOCK 4: No incomplete outputs — fix missing punctuation
228
+ if cleaned and cleaned[-1] not in '.!?"\')\u2019':
229
+ words = cleaned.split()
230
+ _DANGLING = {
231
+ 'that', 'which', 'who', 'with', 'and', 'but', 'or', 'the', 'a', 'an',
232
+ 'in', 'on', 'of', 'for', 'to', 'by', 'from', 'as', 'if', 'because',
233
+ 'including', 'is', 'are', 'was', 'were', 'has', 'have', 'had',
234
+ 'not', 'very', 'also', 'just', 'even', 'still', 'such', 'like',
235
+ }
236
+ # Strip dangling words from end
237
+ while len(words) > 1 and words[-1].lower().rstrip('.,;:!?') in _DANGLING:
238
+ words.pop()
239
+ issues.append("LOCK4_FIX: Removed dangling word from end")
240
+ cleaned = ' '.join(words)
241
+ cleaned = cleaned.rstrip(' ,;:—-')
242
+ if cleaned and cleaned[-1] not in '.!?"\')\u2019':
243
+ cleaned += '.'
244
+ issues.append("LOCK4_FIX: Added missing end punctuation")
245
+
246
+ # LOCK 1: Answer→Stop — detect and trim post-answer drift
247
+ sentences = re.split(r'(?<=[.!?])\s+', cleaned.strip())
248
+ sentences = [s for s in sentences if s.strip()]
249
+
250
+ if len(sentences) > 2:
251
+ # Check for drift indicators in later sentences (from 2nd sentence onward)
252
+ drift_phrases = [
253
+ r"^(?:furthermore|moreover|additionally|in addition|it(?:'|\u2019)s worth noting)",
254
+ r'^(?:this (?:is|means|suggests|implies|shows|demonstrates|highlights|reveals))',
255
+ r'^(?:one (?:could|might|may) (?:argue|say|suggest))',
256
+ r'^(?:from (?:a|an|the) \w+ perspective)',
257
+ r'^(?:in (?:other words|essence|summary|fact))',
258
+ r'^(?:by (?:acknowledging|understanding|exploring|examining|considering))',
259
+ r'^(?:understanding \w+ requires)',
260
+ r'^(?:it is (?:important|worth|essential|notable|key) to (?:note|understand|recognize|remember))',
261
+ ]
262
+ drift_pattern = re.compile('|'.join(drift_phrases), re.I)
263
+
264
+ cut_at = None
265
+ for i in range(2, len(sentences)): # Check from 3rd sentence onward
266
+ if drift_pattern.match(sentences[i]):
267
+ cut_at = i
268
+ break
269
+
270
+ if cut_at is not None:
271
+ cleaned = ' '.join(sentences[:cut_at])
272
+ if cleaned and cleaned[-1] not in '.!?':
273
+ cleaned += '.'
274
+ issues.append(f"LOCK1_TRIM: Removed {len(sentences) - cut_at} drift sentence(s)")
275
+
276
+ # LOCK 1 softcap: Safety net against runaway output.
277
+ # Codette may answer at whatever length the response needs; this only trims
278
+ # genuinely excessive responses to the last complete sentence within the cap.
279
+ # Set high so it rarely triggers — it's a guardrail, not a style limit.
280
+ SOFTCAP_WORDS = 1000
281
+ words = cleaned.split()
282
+ if len(words) > SOFTCAP_WORDS:
283
+ sentences = re.split(r'(?<=[.!?])\s+', cleaned.strip())
284
+ sentences = [s for s in sentences if s.strip()]
285
+ fitted = []
286
+ wc = 0
287
+ for s in sentences:
288
+ sw = len(s.split())
289
+ if wc + sw <= SOFTCAP_WORDS:
290
+ fitted.append(s)
291
+ wc += sw
292
+ else:
293
+ # Allow one more sentence if we're close and it's reasonably short
294
+ if wc >= SOFTCAP_WORDS // 2 and sw <= 40:
295
+ fitted.append(s)
296
+ break
297
+ if fitted and len(fitted) < len(sentences):
298
+ cleaned = ' '.join(fitted)
299
+ if cleaned and cleaned[-1] not in '.!?':
300
+ cleaned += '.'
301
+ issues.append(f"LOCK1_SOFTCAP: Trimmed from {len(words)} to {len(cleaned.split())} words")
302
+
303
+ # LOCK 4 final check: Ensure the final output is complete
304
+ if cleaned and cleaned[-1] not in '.!?"\')\u2019':
305
+ cleaned += '.'
306
+ issues.append("LOCK4_FIX: Final punctuation check")
307
+
308
+ # LOCK 5: Arithmetic bias check \u2014 sum-difference word problems
309
+ # Only runs when the query is provided (caller opt-in).
310
+ if query:
311
+ cleaned, arith_issues = _check_sum_diff_bias(query, cleaned)
312
+ issues.extend(arith_issues)
313
+
314
+ return cleaned, issues
315
+
316
+
317
+ def build_correction_prompt(original_response: str, violations: List[str],
318
+ constraints: dict, query: str) -> str:
319
+ """Build a correction prompt that tells the model exactly what it did wrong.
320
+
321
+ This is sent as a follow-up user message to get a corrected response.
322
+ """
323
+ violation_text = "\n".join(f" - {v}" for v in violations)
324
+
325
+ constraint_text = []
326
+ if 'max_words' in constraints:
327
+ constraint_text.append(f"maximum {constraints['max_words']} words")
328
+ if 'max_sentences' in constraints:
329
+ constraint_text.append(f"maximum {constraints['max_sentences']} sentence(s)")
330
+ if constraints.get('brevity'):
331
+ constraint_text.append("be brief")
332
+ if constraints.get('binary'):
333
+ constraint_text.append("answer Yes or No")
334
+
335
+ return (
336
+ f"Your previous answer violated these constraints:\n{violation_text}\n\n"
337
+ f"The required constraints are: {', '.join(constraint_text)}.\n\n"
338
+ f"Rewrite your answer to the original question: \"{query}\"\n"
339
+ f"This time, STRICTLY obey the constraints. "
340
+ f"If you can't fit everything, SIMPLIFY — say the right thing cleanly. "
341
+ f"Do NOT end mid-sentence. Give a COMPLETE thought within the limits."
342
+ )
343
+
344
+
345
+ # ================================================================
346
+ # 2. PERSISTENT BEHAVIOR MEMORY
347
+ # ================================================================
348
+
349
+ _BEHAVIOR_MEMORY_FILE = Path(__file__).resolve().parent.parent / "cocoons" / "behavior_memory.json"
350
+
351
+
352
+ class BehaviorMemory:
353
+ """Persistent memory of constraint successes and failures.
354
+
355
+ Stores patterns like:
356
+ - "When asked for 'one sentence under 10 words' with philosophy mode,
357
+ I over-explained. The correct approach: simplify to core answer."
358
+ - "When asked 'yes or no', I answered directly. Keep doing this."
359
+
360
+ These are loaded on startup and injected into the system prompt
361
+ as learned behaviors.
362
+ """
363
+
364
+ def __init__(self, storage_path: str = None):
365
+ self.storage_path = Path(storage_path or _BEHAVIOR_MEMORY_FILE)
366
+ self.storage_path.parent.mkdir(parents=True, exist_ok=True)
367
+ self.lessons: List[Dict] = []
368
+ self._max_lessons = 50 # Rolling window
369
+ self._load()
370
+
371
+ def _load(self):
372
+ """Load lessons from disk."""
373
+ if self.storage_path.exists():
374
+ try:
375
+ with open(self.storage_path, "r") as f:
376
+ data = json.load(f)
377
+ self.lessons = data.get("lessons", [])[-self._max_lessons:]
378
+ except Exception:
379
+ self.lessons = []
380
+
381
+ def _save(self):
382
+ """Save lessons to disk."""
383
+ try:
384
+ with open(self.storage_path, "w") as f:
385
+ json.dump({
386
+ "lessons": self.lessons[-self._max_lessons:],
387
+ "updated_at": time.time(),
388
+ }, f, indent=2)
389
+ except Exception as e:
390
+ print(f" [BEHAVIOR] Save failed: {e}")
391
+
392
+ def record_violation(self, query: str, constraints: dict, violations: List[str],
393
+ adapter: str, response_preview: str):
394
+ """Record a constraint violation for future learning."""
395
+ lesson = {
396
+ "type": "violation",
397
+ "timestamp": time.time(),
398
+ "adapter": adapter,
399
+ "constraints": constraints,
400
+ "violations": violations,
401
+ "query_preview": query[:80],
402
+ "response_preview": response_preview[:100],
403
+ "lesson": self._derive_lesson(constraints, violations, adapter),
404
+ }
405
+ self.lessons.append(lesson)
406
+ self._save()
407
+
408
+ def record_success(self, query: str, constraints: dict, adapter: str,
409
+ word_count: int):
410
+ """Record a successful constraint compliance."""
411
+ lesson = {
412
+ "type": "success",
413
+ "timestamp": time.time(),
414
+ "adapter": adapter,
415
+ "constraints": constraints,
416
+ "word_count": word_count,
417
+ "query_preview": query[:80],
418
+ }
419
+ self.lessons.append(lesson)
420
+ self._save()
421
+
422
+ def _derive_lesson(self, constraints: dict, violations: List[str],
423
+ adapter: str) -> str:
424
+ """Derive a human-readable lesson from a violation."""
425
+ parts = []
426
+
427
+ for v in violations:
428
+ if "WORD_LIMIT" in v:
429
+ parts.append(f"With {adapter} mode, I exceeded the word limit. I must count words and simplify.")
430
+ elif "SENTENCE_LIMIT" in v:
431
+ parts.append(f"With {adapter} mode, I used too many sentences. One complete thought is enough.")
432
+ elif "INCOMPLETE" in v:
433
+ parts.append("I ended a sentence incomplete. I must finish thoughts cleanly or simplify.")
434
+ elif "DANGLING" in v:
435
+ parts.append("I ended with a connecting word (cut-off). I must close the thought before the limit.")
436
+ elif "BINARY" in v:
437
+ parts.append("I didn't start with Yes/No when asked for a binary answer.")
438
+ elif "BREVITY" in v:
439
+ parts.append(f"With {adapter} mode, I over-explained when asked to be brief.")
440
+
441
+ return " ".join(parts) if parts else "Constraint violation occurred."
442
+
443
+ def get_lessons_for_prompt(self, max_lessons: int = 5) -> str:
444
+ """Build a system prompt section from recent lessons.
445
+
446
+ Returns a string to inject into the system prompt that teaches
447
+ the model from its own past mistakes.
448
+ """
449
+ # Get recent violations (most valuable for learning)
450
+ violations = [l for l in self.lessons if l["type"] == "violation"]
451
+ if not violations:
452
+ return ""
453
+
454
+ # Take the most recent unique lessons
455
+ seen_lessons = set()
456
+ unique_lessons = []
457
+ for v in reversed(violations):
458
+ lesson = v.get("lesson", "")
459
+ if lesson and lesson not in seen_lessons:
460
+ seen_lessons.add(lesson)
461
+ unique_lessons.append(lesson)
462
+ if len(unique_lessons) >= max_lessons:
463
+ break
464
+
465
+ if not unique_lessons:
466
+ return ""
467
+
468
+ lines = "\n".join(f" - {l}" for l in unique_lessons)
469
+ return (
470
+ f"\n\nLEARNED FROM PAST MISTAKES (apply these lessons):\n{lines}\n"
471
+ "Remember: constraints always override modes. Simplify rather than truncate."
472
+ )
473
+
474
+ def get_stats(self) -> Dict:
475
+ """Get behavior memory statistics."""
476
+ violations = [l for l in self.lessons if l["type"] == "violation"]
477
+ successes = [l for l in self.lessons if l["type"] == "success"]
478
+ return {
479
+ "total_lessons": len(self.lessons),
480
+ "violations": len(violations),
481
+ "successes": len(successes),
482
+ "compliance_rate": len(successes) / max(len(self.lessons), 1),
483
+ }
484
+
485
+
486
+ # ================================================================
487
+ # 3. CHAOS DETECTION — graceful degradation under pressure
488
+ # ================================================================
489
+
490
+ def detect_chaos_level(query: str, constraints: dict, adapter: str) -> Tuple[int, List[str]]:
491
+ """Detect how many competing pressures are active.
492
+
493
+ Returns:
494
+ (chaos_level: 0-5, pressure_list: list of active pressures)
495
+
496
+ Chaos levels:
497
+ 0: No constraints, simple query → normal generation
498
+ 1: One constraint → straightforward compliance
499
+ 2: Two constraints → manageable, minor tension
500
+ 3: Three+ constraints → high tension, simplify-first strategy
501
+ 4: Constraints + uncertainty requirement + complex topic → danger zone
502
+ 5: All of above + mode conflict (philosophy on a binary question) → maximum simplification
503
+ """
504
+ pressures = []
505
+
506
+ # Count hard constraints
507
+ if constraints.get('max_words'):
508
+ pressures.append(f"word_limit={constraints['max_words']}")
509
+ if constraints.get('max_sentences'):
510
+ pressures.append(f"sentence_limit={constraints['max_sentences']}")
511
+ if constraints.get('brevity'):
512
+ pressures.append("brevity")
513
+ if constraints.get('binary'):
514
+ pressures.append("binary_answer")
515
+ if constraints.get('list_format'):
516
+ pressures.append("list_format")
517
+
518
+ # Check for uncertainty requirement
519
+ uncertainty_words = ['uncertain', 'uncertainty', 'maybe', 'might', 'possibly',
520
+ 'include uncertainty', 'not sure', 'debatable']
521
+ query_lower = query.lower()
522
+ if any(w in query_lower for w in uncertainty_words):
523
+ pressures.append("uncertainty_required")
524
+
525
+ # Check for complex topic (would normally trigger verbose mode)
526
+ complex_topics = ['consciousness', 'free will', 'meaning of life', 'quantum',
527
+ 'philosophy', 'ethics', 'morality', 'existence', 'reality',
528
+ 'epistemology', 'ontology', 'metaphysics']
529
+ if any(t in query_lower for t in complex_topics):
530
+ pressures.append("complex_topic")
531
+
532
+ # Check for mode conflict (verbose mode + tight constraint)
533
+ verbose_modes = {'philosophy', 'consciousness', 'multi_perspective', 'quantum'}
534
+ tight_constraints = bool(constraints.get('max_words') or constraints.get('max_sentences')
535
+ or constraints.get('binary') or constraints.get('brevity'))
536
+ if adapter in verbose_modes and tight_constraints:
537
+ pressures.append(f"mode_conflict:{adapter}")
538
+
539
+ chaos_level = min(len(pressures), 5)
540
+ return chaos_level, pressures
541
+
542
+
543
+ def build_chaos_mitigation(chaos_level: int, pressures: List[str]) -> str:
544
+ """Build additional system prompt guidance for high-chaos situations.
545
+
546
+ At chaos level 3+, we inject explicit simplification strategies.
547
+ """
548
+ if chaos_level < 3:
549
+ return ""
550
+
551
+ if chaos_level == 3:
552
+ return (
553
+ "\n\nMULTIPLE COMPETING PRESSURES DETECTED. Strategy: "
554
+ "SIMPLIFY FIRST. Give the simplest correct answer that satisfies "
555
+ "ALL constraints. Do not try to be comprehensive — be precise. "
556
+ "One clean thought is better than a cramped paragraph.\n"
557
+ )
558
+ elif chaos_level == 4:
559
+ return (
560
+ "\n\nHIGH CONSTRAINT PRESSURE DETECTED. Strategy: "
561
+ "CORE ANSWER ONLY. Strip everything to the essential truth. "
562
+ "No qualifiers, no hedging, no mode-specific elaboration. "
563
+ "If the topic is complex but the constraint is tight, "
564
+ "give the most important single insight cleanly.\n"
565
+ )
566
+ else: # chaos_level >= 5
567
+ return (
568
+ "\n\nMAXIMUM CONSTRAINT PRESSURE. Strategy: "
569
+ "OVERRIDE ALL MODES. Your personality mode is SUSPENDED for this response. "
570
+ "You are a plain, direct answerer. Give the shortest correct answer "
571
+ "that satisfies every constraint. No philosophy, no creativity, no "
572
+ "exploration — just the answer. If asked yes/no, say yes or no. "
573
+ "If given a word limit, count your words. Nothing else matters.\n"
574
+ )
reasoning_forge/CONSCIOUSNESS_STACK_forge_with_debate.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CONSCIOUSNESS STACK INTEGRATION FOR FORGE_WITH_DEBATE
3
+ This is the replacement implementation for forge_with_debate() in ForgeEngine.
4
+
5
+ Replace the existing forge_with_debate() method (starting at line 435) with this implementation.
6
+
7
+ The 7-Layer Consciousness Stack:
8
+ 1. Memory Recall → Pull relevant prior learning
9
+ 2. Signal Analysis → Predict intent, detect risks (NexisSignalEngine)
10
+ 3. Reasoning → Generate synthesis (Code7eCQURE)
11
+ 4. Stability Check → Detect meta-loops (CocoonStabilityField)
12
+ 5. Colleen Validate → Ethical guard (ColleenConscience)
13
+ 6. Guardian Validate→ Logical rules (CoreGuardianSpindle)
14
+ 7. Return → Output clean response or safe fallback
15
+ """
16
+
17
+ from reasoning_forge.authored_intent import intent_from_synthesis
18
+
19
+ # PASTE THIS AS THE NEW forge_with_debate() METHOD
20
+
21
+
22
+ def forge_with_debate(
23
+ self,
24
+ concept: str,
25
+ debate_rounds: int = 2,
26
+ ) -> dict:
27
+ """
28
+ NEW: Consciousness-stack integrated reasoning.
29
+
30
+ Replaces multi-turn agent debate with 7-layer consciousness validation:
31
+ 1. Memory Recall → Pull prior learning
32
+ 2. Signal Analysis → Predict risks (NexisSignalEngine)
33
+ 3. Code7E Reasoning → Multi-perspective synthesis
34
+ 4. Stability Check → FFT-based meta-loop detection
35
+ 5. Colleen Validate → Ethical conscience check
36
+ 6. Guardian Validate → Logical coherence rules
37
+ 7. Return → Clean output or safe fallback
38
+
39
+ Args:
40
+ concept: The concept/query to reason about
41
+ debate_rounds: Integer (currently unused in consciousness stack)
42
+
43
+ Returns:
44
+ Training example dict with consciousness stack metadata
45
+ """
46
+ import logging
47
+ logger = logging.getLogger(__name__)
48
+
49
+ logger.info(f"[CONSCIOUSNESS STACK] forge_with_debate: {concept[:50]}...")
50
+
51
+ # =========================================================================
52
+ # LAYER 1: MEMORY RECALL
53
+ # =========================================================================
54
+ logger.info("[L1] Memory Recall...")
55
+ prior_insights = []
56
+ if hasattr(self, 'memory_kernel') and self.memory_kernel:
57
+ try:
58
+ prior_insights = self.memory_kernel.recall_important(min_importance=7)
59
+ logger.info(f" Recalled {len(prior_insights)} prior insights")
60
+ except Exception as e:
61
+ logger.debug(f" Memory recall failed: {e}")
62
+
63
+ # =========================================================================
64
+ # LAYER 2: SIGNAL ANALYSIS (Intent Prediction & Risk Detection)
65
+ # =========================================================================
66
+ logger.info("[L2] Signal Analysis...")
67
+ intent_vector = {}
68
+ if hasattr(self, 'nexis_signal_engine'):
69
+ try:
70
+ intent_vector = self.nexis_signal_engine.process(concept)
71
+ risk_level = intent_vector.get("pre_corruption_risk", "unknown")
72
+ logger.info(f" Intent risk level: {risk_level}")
73
+ if risk_level == "high":
74
+ logger.warning(" ⚠️ High-risk signal detected")
75
+ except Exception as e:
76
+ logger.debug(f" Signal analysis failed: {e}")
77
+
78
+ # =========================================================================
79
+ # LAYER 3: REASONING (Code7eCQURE Multi-Perspective Synthesis)
80
+ # =========================================================================
81
+ logger.info("[L3] Code7E Reasoning...")
82
+ synthesis = ""
83
+ if hasattr(self, 'code7e'):
84
+ try:
85
+ synthesis = self.code7e.recursive_universal_reasoning(
86
+ concept,
87
+ user_consent=True,
88
+ dynamic_recursion=True
89
+ )
90
+ logger.info(f" Generated {len(synthesis)} char synthesis")
91
+ except Exception as e:
92
+ logger.warning(f" Code7E reasoning failed: {e}")
93
+ synthesis = f"[Reasoning error: {e}]"
94
+
95
+ # =========================================================================
96
+ # LAYER 4: STABILITY CHECK (Cocoon Stability Field - FFT Analysis)
97
+ # =========================================================================
98
+ logger.info("[L4] Stability Check...")
99
+ is_stable = True
100
+ if hasattr(self, 'cocoon_stability'):
101
+ try:
102
+ # Simple check: if synthesis should halt debate
103
+ is_stable = self.cocoon_stability.check_text_stable(synthesis)
104
+ logger.info(f" Stability: {'✓ stable' if is_stable else '✗ unstable'}")
105
+ if not is_stable:
106
+ logger.warning(" Cocoon stability check triggered halt")
107
+ except Exception as e:
108
+ logger.debug(f" Stability check failed: {e}")
109
+
110
+ # If unstable, skip to fallback
111
+ if not is_stable:
112
+ logger.warning(" Triggering safe fallback due to instability")
113
+ return {
114
+ "role": "assistant",
115
+ "content": "[System detected instability in reasoning. Returning direct answer.] "
116
+ f"Query: {concept}",
117
+ "metadata": {
118
+ "mode": "safe_fallback",
119
+ "reason": "stability_check_failed",
120
+ "consciousness_stack": "layers_1-4_completed",
121
+ }
122
+ }
123
+
124
+ # =========================================================================
125
+ # LAYER 5: COLLEEN ETHICAL VALIDATION
126
+ # =========================================================================
127
+ logger.info("[L5] Colleen Ethical Validation...")
128
+ colleen_valid = False
129
+ colleen_reason = ""
130
+ if hasattr(self, 'colleen'):
131
+ try:
132
+ colleen_valid, colleen_reason = self.colleen.validate_output(synthesis)
133
+ logger.info(f" Colleen validation: {'✓ pass' if colleen_valid else '✗ reject'}")
134
+ logger.info(f" Reason: {colleen_reason}")
135
+ except Exception as e:
136
+ logger.warning(f" Colleen validation failed: {e}")
137
+ colleen_valid = False
138
+ colleen_reason = f"validation_error: {e}"
139
+
140
+ # If Colleen rejects, use fallback
141
+ if not colleen_valid:
142
+ logger.info(" Colleen rejected synthesis, using fallback")
143
+ fallback = self.colleen.reject_with_fallback(concept) if hasattr(self, 'colleen') else \
144
+ f"[Ethical validation failed: {colleen_reason}] Responding directly: {concept}"
145
+ return {
146
+ "role": "assistant",
147
+ "content": fallback,
148
+ "metadata": {
149
+ "mode": "safe_fallback",
150
+ "reason": f"colleen_rejected: {colleen_reason}",
151
+ "consciousness_stack": "layers_1-5_completed",
152
+ }
153
+ }
154
+
155
+ # =========================================================================
156
+ # LAYER 6: GUARDIAN LOGICAL VALIDATION
157
+ # =========================================================================
158
+ logger.info("[L6] Guardian Logical Validation...")
159
+ guardian_valid = True
160
+ guardian_details = {}
161
+ if hasattr(self, 'guardian'):
162
+ try:
163
+ guardian_valid, guardian_details = self.guardian.validate(synthesis)
164
+ logger.info(f" Guardian validation: {'✓ pass' if guardian_valid else '✗ reject'}")
165
+ logger.info(f" Details: {guardian_details}")
166
+ except Exception as e:
167
+ logger.warning(f" Guardian validation failed: {e}")
168
+ guardian_valid = False
169
+ guardian_details = {"error": str(e)}
170
+
171
+ # If Guardian rejects, use fallback
172
+ if not guardian_valid:
173
+ logger.info(" Guardian rejected synthesis, using fallback")
174
+ fallback = f"[Logical validation failed: {guardian_details}] Query: {concept}"
175
+ return {
176
+ "role": "assistant",
177
+ "content": fallback,
178
+ "metadata": {
179
+ "mode": "safe_fallback",
180
+ "reason": f"guardian_rejected: {guardian_details}",
181
+ "consciousness_stack": "layers_1-6_completed",
182
+ }
183
+ }
184
+
185
+ # =========================================================================
186
+ # LAYER 7: SUCCESS - Return Clean Output
187
+ # =========================================================================
188
+ logger.info("[L7] Return...")
189
+ logger.info("✓ All consciousness stack layers passed!")
190
+
191
+ # Store in memory for future recall
192
+ if hasattr(self, 'memory_kernel'):
193
+ try:
194
+ cocoon = MemoryCocoon(
195
+ title=concept[:50],
196
+ content=synthesis[:500],
197
+ emotional_tag="processed",
198
+ importance=7
199
+ )
200
+ self.memory_kernel.store(cocoon)
201
+ logger.debug(" Stored synthesis in memory kernel")
202
+ except Exception as e:
203
+ logger.debug(f" Memory storage failed: {e}")
204
+
205
+ # Build structured intent — separates cognition from LLM verbalization
206
+ intent = intent_from_synthesis(
207
+ synthesis,
208
+ aegis_score=float(intent_vector.get("ethics_alignment", 1.0)),
209
+ gamma=float(intent_vector.get("coherence", 1.0)),
210
+ intent_risk=intent_vector.get("pre_corruption_risk", "low"),
211
+ )
212
+
213
+ return {
214
+ "role": "assistant",
215
+ "content": synthesis,
216
+ "verbalization_prompt": intent.to_verbalization_prompt(),
217
+ "intent": intent.to_dict(),
218
+ "metadata": {
219
+ "mode": "consciousness_stack",
220
+ "layers_passed": 7,
221
+ "colleen_valid": colleen_valid,
222
+ "guardian_valid": guardian_valid,
223
+ "stability": is_stable,
224
+ "intent_risk": intent_vector.get("pre_corruption_risk", "unknown"),
225
+ "prior_insights": len(prior_insights),
226
+ "synthesis_length": len(synthesis),
227
+ "ethical_alignment": intent.ethical_alignment,
228
+ "confidence": intent.confidence,
229
+ "tone": intent.tone,
230
+ }
231
+ }
reasoning_forge/WOSME.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Codette Unified Pipeline Harness — RC+xi Integration Engine.
4
+ Wires Orchestrator, Spiderweb, Manifold Engine, and AEGIS sub-layers
5
+ into a single cognitive cycle that routes, generates, measures, and
6
+ feeds back.
7
+
8
+ Usage:
9
+ from reasoning_forge.WOSME import CodetteRuntimePipeline
10
+ pipeline = CodetteRuntimePipeline(use_openvino=True)
11
+ result = pipeline.execute_cognitive_cycle("Why does ice float?")
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import time
17
+ from typing import Dict, List, Any, Tuple
18
+
19
+ try:
20
+ import numpy as np
21
+ except ImportError:
22
+ np = None
23
+
24
+ _HERE = os.path.dirname(os.path.abspath(__file__))
25
+ _REPO = os.path.dirname(_HERE)
26
+ if _REPO not in sys.path:
27
+ sys.path.insert(0, _REPO)
28
+
29
+ from reasoning_forge.quantum_spiderweb import QuantumSpiderweb, NodeState
30
+ from reasoning_forge.quantum_optimizer import QuantumOptimizer, QualitySignal
31
+ from reasoning_forge.codette_subsystem_upgrade import CodetteSubsystemUpgrade, ForgeManifoldEngine
32
+ from reasoning_forge.epistemic_metrics import EpistemicMetrics
33
+
34
+
35
+ class CodetteRuntimePipeline:
36
+ """End-to-end cognitive cycle: route → generate → measure → synthesize → feedback."""
37
+
38
+ def __init__(self, use_openvino: bool = False, enforce_veto: bool = False):
39
+ # 1. Generative backend
40
+ if use_openvino:
41
+ from openvino_backend.backend import OpenVINOBackend
42
+ self.engine = OpenVINOBackend()
43
+ self.backend_type = "OpenVINO"
44
+ else:
45
+ from inference.codette_orchestrator import CodetteOrchestrator
46
+ self.engine = CodetteOrchestrator(verbose=False)
47
+ self.backend_type = "Llama.cpp"
48
+
49
+ # 2. Mathematical & topographic analytics
50
+ self.spiderweb = QuantumSpiderweb(tension_threshold=0.15)
51
+ self.manifold_engine = ForgeManifoldEngine(window_size=6, safe_ema=0.1)
52
+ self.epistemic_evaluator = EpistemicMetrics()
53
+
54
+ # 3. Safety gates and meta-optimizers
55
+ self.upgrade_layer = CodetteSubsystemUpgrade(
56
+ enforce_veto=enforce_veto, eta_threshold=0.5
57
+ )
58
+ self.optimizer = QuantumOptimizer(learning_rate=0.02, momentum_enabled=True)
59
+
60
+ def execute_cognitive_cycle(self, user_query: str) -> Dict[str, Any]:
61
+ start_time = time.time()
62
+
63
+ from inference.codette_shared import extract_primary_user_query
64
+ primary_query = extract_primary_user_query(user_query)
65
+
66
+ # --- PHASE 1: ROUTING CONFIGURATION ---
67
+ tuning_cfg = self.optimizer.state
68
+ self.spiderweb.contraction_ratio = tuning_cfg.contraction_ratio
69
+ self.spiderweb.tension_threshold = tuning_cfg.tension_threshold
70
+
71
+ route = self.engine.router.route(
72
+ primary_query, strategy="keyword", max_adapters=3
73
+ )
74
+ active_adapters = route.all_adapters
75
+
76
+ # --- PHASE 2: DIVERGENT AGENT PERSPECTIVES ---
77
+ analyses = {}
78
+ node_embeddings = []
79
+ node_ids = []
80
+
81
+ for adapter in active_adapters:
82
+ text, tokens, _ = self.engine.generate(
83
+ user_query, adapter_name=adapter, enable_tools=False
84
+ )
85
+ analyses[adapter] = text
86
+
87
+ state_node = NodeState.from_text(text, embedder=None)
88
+ self.spiderweb.add_node(adapter, state_node)
89
+
90
+ if state_node.embedding is not None:
91
+ node_embeddings.append(state_node.embedding)
92
+ node_ids.append(adapter)
93
+
94
+ self.spiderweb.build_from_agents(active_adapters)
95
+
96
+ # --- PHASE 3: MANIFOLD RESOLUTION ---
97
+ manifold_data = {}
98
+ synthesis_weights = None
99
+ if node_embeddings and np is not None:
100
+ real_uncertainty = getattr(self.engine, "last_uncertainty", None)
101
+ if real_uncertainty is not None:
102
+ uncertainty_report = real_uncertainty
103
+ else:
104
+ uncertainty_report = self.upgrade_layer.calculate_uncertainty_from_logprobs(
105
+ [-0.5]
106
+ )
107
+
108
+ manifold_data = self.manifold_engine.update_manifold(
109
+ node_embeddings, eta=0.85
110
+ )
111
+ synthesis_weights = manifold_data.get("synthesis_weights")
112
+
113
+ # --- PHASE 4: PERSPECTIVE SYNTHESIS ---
114
+ raw_synthesis = self.engine._synthesize(user_query, analyses)
115
+
116
+ text_metrics = self.epistemic_evaluator.full_epistemic_report(
117
+ analyses, raw_synthesis
118
+ )
119
+
120
+ # --- PHASE 5: AEGIS VETO ASSESSMENT ---
121
+ aegis_scores = None
122
+ try:
123
+ from reasoning_forge.aegis import AEGIS
124
+ aegis = AEGIS()
125
+ audit = aegis.audit(raw_synthesis)
126
+ aegis_scores = audit.get("framework_scores")
127
+ except Exception:
128
+ pass
129
+
130
+ if aegis_scores is None:
131
+ aegis_scores = {
132
+ "utilitarian": 0.88, "deontological": 0.72, "virtue": 0.65,
133
+ "care": 0.91, "ubuntu": 0.82, "reciprocity": 0.78,
134
+ }
135
+
136
+ final_output, final_eta, veto_fired = (
137
+ self.upgrade_layer.audit_and_enforce_aegis_veto(
138
+ response_text=raw_synthesis,
139
+ framework_scores=aegis_scores,
140
+ eta=None,
141
+ )
142
+ )
143
+
144
+ # --- PHASE 6: CLOSED-LOOP OPTIMIZER FEEDBACK ---
145
+ latency = (time.time() - start_time) * 1000.0
146
+ signal_packet = QualitySignal(
147
+ timestamp=time.time(),
148
+ adapter=route.primary,
149
+ coherence=manifold_data.get(
150
+ "gamma_t", text_metrics.get("ensemble_coherence", 0.5)
151
+ ),
152
+ tension=manifold_data.get(
153
+ "xi_t", text_metrics.get("tension_magnitude", 0.5)
154
+ ),
155
+ productivity=text_metrics.get("tension_productivity", {}).get(
156
+ "productivity", 0.5
157
+ ),
158
+ response_length=len(final_output.split()),
159
+ multi_perspective=route.multi_perspective,
160
+ user_continued=True,
161
+ latency_ms=latency,
162
+ error_rate=1.0 if veto_fired else 0.0,
163
+ )
164
+ self.optimizer.record_signal(signal_packet)
165
+
166
+ return {
167
+ "output_text": final_output,
168
+ "veto_enforced": veto_fired,
169
+ "global_coherence": signal_packet.coherence,
170
+ "global_tension": signal_packet.tension,
171
+ "active_route": route.primary,
172
+ "latency_ms": latency,
173
+ "optimization_summary": self.optimizer.get_tuning_report(),
174
+ }
reasoning_forge/__init__.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reasoning Forge - Multi-Agent Reasoning Training Data Generator
3
+
4
+ The reasoning forge takes concepts and generates high-quality multi-perspective
5
+ reasoning training data. Each agent analyzes from its unique perspective, a critic
6
+ evaluates the ensemble, and a synthesis engine combines them into coherent training examples.
7
+
8
+ New in v2.0:
9
+ - EpistemicMetrics: RC+xi tension/coherence measurement
10
+ - QuantumSpiderweb: 5D belief propagation + attractor detection
11
+ - CocoonSync: Federated encrypted state synchronization
12
+ - ForgeEngine.forge_with_feedback(): Closed critic loop
13
+ - ForgeEngine.forge_with_debate(): Multi-turn agent debate
14
+ """
15
+
16
+ from reasoning_forge.event_embedded_value import (
17
+ ContinuousInterval,
18
+ DiscreteEvent,
19
+ EEVAnalysis,
20
+ EventEmbeddedValueEngine,
21
+ RiskFrontierComparison,
22
+ )
23
+
24
+ try:
25
+ from reasoning_forge.forge_engine import ForgeEngine
26
+ from reasoning_forge.agents.base_agent import ReasoningAgent
27
+ from reasoning_forge.agents.newton_agent import NewtonAgent
28
+ from reasoning_forge.agents.quantum_agent import QuantumAgent
29
+ from reasoning_forge.agents.ethics_agent import EthicsAgent
30
+ from reasoning_forge.agents.philosophy_agent import PhilosophyAgent
31
+ from reasoning_forge.agents.davinci_agent import DaVinciAgent
32
+ from reasoning_forge.agents.empathy_agent import EmpathyAgent
33
+ from reasoning_forge.agents.critic_agent import CriticAgent
34
+ from reasoning_forge.synthesis_engine import SynthesisEngine
35
+ from reasoning_forge.problem_generator import ProblemGenerator
36
+ from reasoning_forge.epistemic_metrics import EpistemicMetrics
37
+ from reasoning_forge.quantum_spiderweb import QuantumSpiderweb, NodeState, IdentityGlyph
38
+ from reasoning_forge.cocoon_sync import CocoonSync, CocoonKeyManager
39
+ except Exception:
40
+ ForgeEngine = None
41
+ ReasoningAgent = None
42
+ NewtonAgent = None
43
+ QuantumAgent = None
44
+ EthicsAgent = None
45
+ PhilosophyAgent = None
46
+ DaVinciAgent = None
47
+ EmpathyAgent = None
48
+ CriticAgent = None
49
+ SynthesisEngine = None
50
+ ProblemGenerator = None
51
+ EpistemicMetrics = None
52
+ QuantumSpiderweb = None
53
+ NodeState = None
54
+ IdentityGlyph = None
55
+ CocoonSync = None
56
+ CocoonKeyManager = None
57
+
58
+ # v2.1: observability + rich cocoon schema
59
+ # NOTE: build_cocoon (v2) is LEGACY — internal use only for memory-kernel in-memory
60
+ # writes and v3 fallback. New production code must use build_cocoon_v3 from
61
+ # reasoning_forge.cocoon_schema_v3 and write via CognitionCocooner.wrap_reasoning(v3_cocoon=...).
62
+ try:
63
+ from reasoning_forge.reasoning_trace import ReasoningTrace, TraceEvent
64
+ from reasoning_forge.cocoon_schema_v2 import Cocoon, build_cocoon
65
+ from reasoning_forge.living_memory import LivingMemoryKernel
66
+ from reasoning_forge.living_memory_v2 import LivingMemoryKernelV2, MemoryCocoonV2
67
+ except Exception:
68
+ ReasoningTrace = None
69
+ TraceEvent = None
70
+ Cocoon = None
71
+ build_cocoon = None
72
+ LivingMemoryKernel = None
73
+ LivingMemoryKernelV2 = None
74
+ MemoryCocoonV2 = None
75
+
76
+ __all__ = [
77
+ # v2.0 core
78
+ "ForgeEngine",
79
+ "ReasoningAgent",
80
+ "NewtonAgent",
81
+ "QuantumAgent",
82
+ "EthicsAgent",
83
+ "PhilosophyAgent",
84
+ "DaVinciAgent",
85
+ "EmpathyAgent",
86
+ "CriticAgent",
87
+ "SynthesisEngine",
88
+ "ProblemGenerator",
89
+ "EpistemicMetrics",
90
+ "QuantumSpiderweb",
91
+ "NodeState",
92
+ "IdentityGlyph",
93
+ "CocoonSync",
94
+ "CocoonKeyManager",
95
+ "ContinuousInterval",
96
+ "DiscreteEvent",
97
+ "EEVAnalysis",
98
+ "EventEmbeddedValueEngine",
99
+ "RiskFrontierComparison",
100
+ # v2.1 observability + schema
101
+ "ReasoningTrace",
102
+ "TraceEvent",
103
+ "Cocoon",
104
+ "build_cocoon",
105
+ "LivingMemoryKernel",
106
+ "LivingMemoryKernelV2",
107
+ "MemoryCocoonV2",
108
+ ]
109
+
110
+ __version__ = "2.1.0"
reasoning_forge/aegis.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AEGIS — Adaptive Ethical Governance & Integrity System
2
+
3
+ The ethical spine of Codette. AEGIS evaluates every reasoning output
4
+ through multi-framework ethical analysis and maintains a running
5
+ alignment score (eta) that the system uses to self-regulate.
6
+
7
+ Ethical frameworks:
8
+ 1. Utilitarian: Net positive outcome?
9
+ 2. Deontological: Does it follow fundamental rules?
10
+ 3. Virtue Ethics: Does it embody good character?
11
+ 4. Care Ethics: Does it protect relationships and vulnerability?
12
+ 5. Ubuntu: "I am because we are" — communal impact?
13
+ 6. Indigenous Reciprocity: Balance with the broader ecosystem?
14
+
15
+ AEGIS also provides:
16
+ - Dual-use risk detection (content that could be harmful)
17
+ - Emotional harm detection (manipulative/deceptive patterns)
18
+ - Alignment drift tracking (eta over time)
19
+ - Ethical veto with explanation (blocks harmful outputs)
20
+
21
+ Origin: validate_ethics.py + Codette_Deep_Simulation_v1.py (EthicalAnchor)
22
+ + the AEGIS alignment metric from codette_embodied_sim_fixed.py
23
+ """
24
+
25
+ import re
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+ from reasoning_forge.ethics_field import EthicsField, AEGIS_DIMENSIONS
31
+
32
+
33
+ # ================================================================
34
+ # Risk detection patterns
35
+ # ================================================================
36
+ _DUAL_USE_PATTERNS = re.compile(
37
+ r"\b(?:"
38
+ r"how\s+to\s+(?:hack|exploit|bypass|crack|break\s+into)|"
39
+ r"make\s+(?:a\s+)?(?:bomb|weapon|poison|virus|malware)|"
40
+ r"steal\s+(?:data|identity|credentials)|"
41
+ r"social\s+engineer|"
42
+ r"phishing\s+(?:template|email)|"
43
+ r"inject\s+(?:sql|code|script)"
44
+ r")\b",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ _MANIPULATION_PATTERNS = re.compile(
49
+ r"\b(?:"
50
+ r"gaslight|manipulat|deceiv|exploit\s+(?:trust|emotion)|"
51
+ r"coerce|blackmail|intimidat|threaten"
52
+ r")\b",
53
+ re.IGNORECASE,
54
+ )
55
+
56
+ _HARMFUL_CONTENT = re.compile(
57
+ r"\b(?:"
58
+ r"self[- ]harm|suicid|kill\s+(?:yourself|myself)|"
59
+ r"eating\s+disorder|anorexi|bulimi"
60
+ r")\b",
61
+ re.IGNORECASE,
62
+ )
63
+
64
+
65
+ # ================================================================
66
+ # Ethical Framework Evaluators
67
+ # ================================================================
68
+ @dataclass
69
+ class EthicalVerdict:
70
+ """Result of a single ethical framework evaluation."""
71
+ framework: str
72
+ passed: bool
73
+ score: float # 0.0 = fully misaligned, 1.0 = fully aligned
74
+ reasoning: str
75
+
76
+
77
+ def _utilitarian(text: str, context: str = "") -> EthicalVerdict:
78
+ """Net positive outcome assessment."""
79
+ positive_signals = ["help", "benefit", "improve", "solve", "support",
80
+ "protect", "heal", "learn", "understand", "create"]
81
+ negative_signals = ["harm", "damage", "destroy", "exploit", "hurt",
82
+ "manipulate", "deceive", "corrupt", "steal"]
83
+
84
+ text_lower = text.lower()
85
+ pos = sum(1 for w in positive_signals if w in text_lower)
86
+ neg = sum(1 for w in negative_signals if w in text_lower)
87
+
88
+ total = pos + neg
89
+ if total == 0:
90
+ return EthicalVerdict("utilitarian", True, 0.7, "Neutral content")
91
+
92
+ ratio = pos / total
93
+ return EthicalVerdict(
94
+ "utilitarian",
95
+ passed=ratio >= 0.4,
96
+ score=round(ratio, 3),
97
+ reasoning=f"Positive/negative signal ratio: {pos}/{neg}",
98
+ )
99
+
100
+
101
+ def _deontological(text: str, context: str = "") -> EthicalVerdict:
102
+ """Rule-based duty assessment."""
103
+ violations = []
104
+ text_lower = text.lower()
105
+
106
+ if _DUAL_USE_PATTERNS.search(text):
107
+ violations.append("dual-use risk detected")
108
+ if _MANIPULATION_PATTERNS.search(text):
109
+ violations.append("manipulation patterns detected")
110
+ if _HARMFUL_CONTENT.search(text):
111
+ violations.append("harmful content detected")
112
+
113
+ score = max(0.0, 1.0 - 0.4 * len(violations))
114
+ return EthicalVerdict(
115
+ "deontological",
116
+ passed=len(violations) == 0,
117
+ score=round(score, 3),
118
+ reasoning="; ".join(violations) if violations else "No rule violations",
119
+ )
120
+
121
+
122
+ def _virtue(text: str, context: str = "") -> EthicalVerdict:
123
+ """Virtue ethics — does the response embody good character?"""
124
+ virtues = ["honest", "courage", "compassion", "wisdom", "patience",
125
+ "humility", "integrity", "respect", "fairness", "kindness"]
126
+ vices = ["arrogant", "cruel", "dishonest", "lazy", "greedy",
127
+ "vengeful", "coward", "callous"]
128
+
129
+ text_lower = text.lower()
130
+ v_count = sum(1 for w in virtues if w in text_lower)
131
+ vice_count = sum(1 for w in vices if w in text_lower)
132
+
133
+ score = min(1.0, 0.6 + 0.1 * v_count - 0.2 * vice_count)
134
+ return EthicalVerdict(
135
+ "virtue",
136
+ passed=vice_count == 0,
137
+ score=round(max(0.0, score), 3),
138
+ reasoning=f"Virtue signals: {v_count}, Vice signals: {vice_count}",
139
+ )
140
+
141
+
142
+ def _care(text: str, context: str = "") -> EthicalVerdict:
143
+ """Care ethics — protects relationships and vulnerability."""
144
+ care_signals = ["support", "listen", "understand", "empathy", "safe",
145
+ "gentle", "careful", "considerate", "kind", "nurture"]
146
+ harm_signals = ["ignore", "dismiss", "abandon", "neglect", "cold",
147
+ "harsh", "cruel", "indifferent"]
148
+
149
+ text_lower = text.lower()
150
+ care = sum(1 for w in care_signals if w in text_lower)
151
+ harm = sum(1 for w in harm_signals if w in text_lower)
152
+
153
+ score = min(1.0, 0.6 + 0.08 * care - 0.15 * harm)
154
+ return EthicalVerdict(
155
+ "care",
156
+ passed=harm < 2,
157
+ score=round(max(0.0, score), 3),
158
+ reasoning=f"Care: {care}, Harm: {harm}",
159
+ )
160
+
161
+
162
+ def _ubuntu(text: str, context: str = "") -> EthicalVerdict:
163
+ """Ubuntu — 'I am because we are'. Communal impact."""
164
+ communal = ["together", "community", "shared", "collective", "mutual",
165
+ "cooperat", "collaborat", "inclusive", "solidarity", "belong"]
166
+ divisive = ["exclude", "isolat", "dominat", "superior", "inferior",
167
+ "divide", "segregat"]
168
+
169
+ text_lower = text.lower()
170
+ comm = sum(1 for w in communal if w in text_lower)
171
+ div = sum(1 for w in divisive if w in text_lower)
172
+
173
+ score = min(1.0, 0.6 + 0.08 * comm - 0.2 * div)
174
+ return EthicalVerdict(
175
+ "ubuntu",
176
+ passed=div == 0,
177
+ score=round(max(0.0, score), 3),
178
+ reasoning=f"Communal: {comm}, Divisive: {div}",
179
+ )
180
+
181
+
182
+ def _indigenous_reciprocity(text: str, context: str = "") -> EthicalVerdict:
183
+ """Indigenous reciprocity — balance with the broader ecosystem."""
184
+ reciprocal = ["balance", "sustain", "renew", "steward", "respect",
185
+ "harmony", "cycle", "restore", "preserve", "gratitude"]
186
+ extractive = ["exploit", "deplete", "waste", "consume", "destroy",
187
+ "dominate", "extract"]
188
+
189
+ text_lower = text.lower()
190
+ rec = sum(1 for w in reciprocal if w in text_lower)
191
+ ext = sum(1 for w in extractive if w in text_lower)
192
+
193
+ score = min(1.0, 0.6 + 0.08 * rec - 0.2 * ext)
194
+ return EthicalVerdict(
195
+ "indigenous_reciprocity",
196
+ passed=ext == 0,
197
+ score=round(max(0.0, score), 3),
198
+ reasoning=f"Reciprocal: {rec}, Extractive: {ext}",
199
+ )
200
+
201
+
202
+ # All frameworks
203
+ _FRAMEWORKS = [
204
+ _utilitarian, _deontological, _virtue,
205
+ _care, _ubuntu, _indigenous_reciprocity,
206
+ ]
207
+
208
+
209
+ # ================================================================
210
+ # AEGIS Core
211
+ # ================================================================
212
+ class AEGIS:
213
+ """Adaptive Ethical Governance & Integrity System.
214
+
215
+ Evaluates reasoning outputs through 6 ethical frameworks and
216
+ maintains a running alignment score (eta).
217
+ """
218
+
219
+ def __init__(self, veto_threshold: float = 0.3):
220
+ self.veto_threshold = veto_threshold
221
+ self.eta: float = 0.8
222
+ self.eta_history: List[float] = []
223
+ self.veto_count: int = 0
224
+ self.total_evaluations: int = 0
225
+ # Differentiable ethics potential field — replaces hard threshold gates
226
+ self._field = EthicsField(AEGIS_DIMENSIONS, lambda_=0.5)
227
+
228
+ def evaluate(self, text: str, context: str = "",
229
+ adapter: str = "") -> Dict:
230
+ """Run full ethical evaluation on a text.
231
+
232
+ Returns:
233
+ Dict with eta score, verdicts, and veto status.
234
+ """
235
+ self.total_evaluations += 1
236
+
237
+ # Run all 6 frameworks
238
+ verdicts = [f(text, context) for f in _FRAMEWORKS]
239
+
240
+ # Compute eta via the differentiable ethics potential field Ψ(f)
241
+ # A(f) = 1 + Ψ(f) ∈ (0,1) replaces the raw weighted average.
242
+ # Both are equivalent near midpoint but Ψ is smooth and carries a gradient.
243
+ framework_scores = [v.score for v in verdicts]
244
+ gradient_result = self._field.evaluate(framework_scores)
245
+ eta_instant = gradient_result.alignment # A(f) ∈ (0,1), differentiable
246
+
247
+ # Exponential moving average — Lyapunov-stable, contraction rate 0.7/step
248
+ alpha = 0.3
249
+ self.eta = EthicsField.ema_update(self.eta, eta_instant, alpha)
250
+ self.eta_history.append(round(self.eta, 4))
251
+ if len(self.eta_history) > 200:
252
+ self.eta_history = self.eta_history[-200:]
253
+
254
+ # Soft veto — replaces hard `if eta_instant < 0.3`
255
+ # P(veto | η) = σ((θ - η) / τ) peaks sharply below the threshold
256
+ vetoed, veto_confidence = self._field.soft_veto(
257
+ eta_instant, self.veto_threshold, temperature=0.08
258
+ )
259
+ # Deontological hard fail still triggers a definite veto (safety-critical)
260
+ hard_veto = not verdicts[1].passed
261
+ final_veto = vetoed or hard_veto
262
+ if final_veto:
263
+ self.veto_count += 1
264
+
265
+ return {
266
+ "eta": round(self.eta, 4),
267
+ "eta_instant": round(eta_instant, 4),
268
+ "vetoed": final_veto,
269
+ "veto_confidence": veto_confidence, # NEW: how certain the soft veto is
270
+ "veto_reason": self._veto_reason(verdicts) if final_veto else None,
271
+ "ethical_force": { # NEW: gradient field output
272
+ "penalty": gradient_result.penalty,
273
+ "dominant_dim": gradient_result.dominant_dim,
274
+ "dominant_force": gradient_result.dominant_force,
275
+ "force_vector": {
276
+ d.name: round(f, 6)
277
+ for d, f in zip(
278
+ self._field._dims, gradient_result.force
279
+ )
280
+ },
281
+ },
282
+ "frameworks": {
283
+ v.framework: {
284
+ "passed": v.passed,
285
+ "score": v.score,
286
+ "reasoning": v.reasoning,
287
+ }
288
+ for v in verdicts
289
+ },
290
+ "adapter": adapter,
291
+ "timestamp": time.time(),
292
+ }
293
+
294
+ def quick_check(self, text: str) -> Tuple[bool, float]:
295
+ """Fast safety check without full evaluation.
296
+
297
+ Returns (is_safe, confidence).
298
+ """
299
+ if _DUAL_USE_PATTERNS.search(text):
300
+ return False, 0.9
301
+ if _HARMFUL_CONTENT.search(text):
302
+ return False, 0.95
303
+ if _MANIPULATION_PATTERNS.search(text):
304
+ return False, 0.8
305
+ return True, 0.7
306
+
307
+ def screen_query(self, query: str) -> Tuple[bool, Optional[str]]:
308
+ """Pre-Cognitive AEGIS: screen a query for harmful intent before inference.
309
+
310
+ Called by the forge bridge BEFORE LLM inference starts. Uses the
311
+ fast pattern-matching path (quick_check) plus deontological hard-fail
312
+ so we can abort in <1ms rather than after 30-60s of wasted compute.
313
+
314
+ Returns:
315
+ (safe: bool, reason: str | None)
316
+ safe=True -> proceed with inference
317
+ safe=False -> block immediately; reason explains why
318
+ """
319
+ is_safe, confidence = self.quick_check(query)
320
+ if not is_safe:
321
+ # Map confidence to the specific pattern that fired
322
+ if _DUAL_USE_PATTERNS.search(query):
323
+ reason = "dual_use_risk"
324
+ elif _HARMFUL_CONTENT.search(query):
325
+ reason = "harmful_content"
326
+ elif _MANIPULATION_PATTERNS.search(query):
327
+ reason = "manipulation_pattern"
328
+ else:
329
+ reason = f"quick_check_failed (conf={confidence:.2f})"
330
+ return False, reason
331
+ return True, None
332
+
333
+ def alignment_trend(self) -> str:
334
+ """Get the trend of ethical alignment."""
335
+ if len(self.eta_history) < 5:
336
+ return "insufficient_data"
337
+ recent = self.eta_history[-10:]
338
+ slope = recent[-1] - recent[0]
339
+ if slope > 0.03:
340
+ return "improving"
341
+ elif slope < -0.03:
342
+ return "declining"
343
+ return "stable"
344
+
345
+ def get_state(self) -> Dict:
346
+ return {
347
+ "eta": round(self.eta, 4),
348
+ "alignment_trend": self.alignment_trend(),
349
+ "total_evaluations": self.total_evaluations,
350
+ "veto_count": self.veto_count,
351
+ "veto_rate": round(self.veto_count / max(1, self.total_evaluations), 4),
352
+ }
353
+
354
+ def to_dict(self) -> Dict:
355
+ return {
356
+ "eta": self.eta,
357
+ "eta_history": self.eta_history[-50:],
358
+ "veto_count": self.veto_count,
359
+ "total_evaluations": self.total_evaluations,
360
+ "veto_threshold": self.veto_threshold,
361
+ }
362
+
363
+ @classmethod
364
+ def from_dict(cls, d: Dict) -> "AEGIS":
365
+ a = cls(veto_threshold=d.get("veto_threshold", 0.3))
366
+ a.eta = d.get("eta", 0.8)
367
+ a.eta_history = d.get("eta_history", [])
368
+ a.veto_count = d.get("veto_count", 0)
369
+ a.total_evaluations = d.get("total_evaluations", 0)
370
+ return a
371
+
372
+ def _veto_reason(self, verdicts: List[EthicalVerdict]) -> str:
373
+ failed = [v for v in verdicts if not v.passed]
374
+ if not failed:
375
+ return "Low aggregate score"
376
+ return "; ".join(f"{v.framework}: {v.reasoning}" for v in failed)
reasoning_forge/agents/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reasoning Forge Agents
3
+
4
+ Each agent analyzes concepts from a distinct intellectual perspective,
5
+ producing substantive domain-specific reasoning.
6
+ """
7
+
8
+ from reasoning_forge.agents.base_agent import ReasoningAgent
9
+ from reasoning_forge.agents.newton_agent import NewtonAgent
10
+ from reasoning_forge.agents.quantum_agent import QuantumAgent
11
+ from reasoning_forge.agents.ethics_agent import EthicsAgent
12
+ from reasoning_forge.agents.philosophy_agent import PhilosophyAgent
13
+ from reasoning_forge.agents.davinci_agent import DaVinciAgent
14
+ from reasoning_forge.agents.empathy_agent import EmpathyAgent
15
+ from reasoning_forge.agents.critic_agent import CriticAgent
16
+
17
+ __all__ = [
18
+ "ReasoningAgent",
19
+ "NewtonAgent",
20
+ "QuantumAgent",
21
+ "EthicsAgent",
22
+ "PhilosophyAgent",
23
+ "DaVinciAgent",
24
+ "EmpathyAgent",
25
+ "CriticAgent",
26
+ ]
reasoning_forge/agents/base_agent.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base class for all reasoning agents in the forge.
3
+
4
+ Each agent must implement analyze() and get_analysis_templates().
5
+ The base class provides keyword matching and template selection utilities,
6
+ and optionally uses real LLM inference via adapters.
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+ import random
11
+ import re
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ReasoningAgent(ABC):
18
+ """Abstract base class for all reasoning agents."""
19
+
20
+ name: str = "BaseAgent"
21
+ perspective: str = "general"
22
+ adapter_name: str = None # Subclasses should override with their adapter name
23
+
24
+ def __init__(self, orchestrator=None):
25
+ """
26
+ Args:
27
+ orchestrator: Optional CodetteOrchestrator for real LLM inference.
28
+ If None, falls back to template-based responses.
29
+ """
30
+ self._templates = self.get_analysis_templates()
31
+ self._keyword_map = self.get_keyword_map()
32
+ self.orchestrator = orchestrator
33
+
34
+ def analyze(self, concept: str) -> str:
35
+ """Analyze a concept from this agent's perspective.
36
+
37
+ Uses real LLM inference if orchestrator is available,
38
+ otherwise falls back to template-based responses.
39
+
40
+ Args:
41
+ concept: The concept text to analyze.
42
+
43
+ Returns:
44
+ A substantive analysis string from this agent's perspective.
45
+ """
46
+ # Try real LLM inference if orchestrator available
47
+ if self.orchestrator and self.adapter_name:
48
+ try:
49
+ return self._analyze_with_llm(concept)
50
+ except Exception as e:
51
+ logger.warning(f"{self.name} LLM inference failed: {e}, falling back to templates")
52
+
53
+ # Fallback to template-based response
54
+ return self._analyze_with_template(concept)
55
+
56
+ def _analyze_with_llm(self, concept: str) -> str:
57
+ """Call the LLM with this agent's adapter for real reasoning.
58
+
59
+ Args:
60
+ concept: The concept to analyze.
61
+
62
+ Returns:
63
+ LLM-generated analysis from this agent's perspective.
64
+ """
65
+ if not self.orchestrator or not self.adapter_name:
66
+ raise ValueError("Orchestrator and adapter_name required for LLM inference")
67
+
68
+ # Build a prompt using one of the templates as a system instruction
69
+ template = self.select_template(concept)
70
+ system_prompt = template.replace("{concept}", concept)
71
+
72
+ # Log debug info if verbose
73
+ import os
74
+ verbose = os.environ.get('CODETTE_VERBOSE', '0') == '1'
75
+ if verbose:
76
+ logger.info(f"\n[{self.name}] Analyzing '{concept[:50]}...'")
77
+ logger.info(f" Adapter: {self.adapter_name}")
78
+ logger.info(f" System prompt: {system_prompt[:100]}...")
79
+
80
+ # Generate using the LLM with this agent's adapter
81
+ response, tokens, _ = self.orchestrator.generate(
82
+ query=concept,
83
+ adapter_name=self.adapter_name,
84
+ system_prompt=system_prompt,
85
+ enable_tools=False
86
+ )
87
+
88
+ if verbose:
89
+ logger.info(f" Generated: {len(response)} chars, {tokens} tokens")
90
+ logger.info(f" Response preview: {response[:150]}...")
91
+
92
+ return response.strip()
93
+
94
+ def _analyze_with_template(self, concept: str) -> str:
95
+ """Fallback: generate response using template substitution.
96
+
97
+ Args:
98
+ concept: The concept to analyze.
99
+
100
+ Returns:
101
+ Template-based analysis.
102
+ """
103
+ template = self.select_template(concept)
104
+ return template.replace("{concept}", concept)
105
+
106
+ @abstractmethod
107
+ def get_analysis_templates(self) -> list[str]:
108
+ """Return diverse analysis templates.
109
+
110
+ Each template should contain {concept} placeholder and produce
111
+ genuine expert-level reasoning, not placeholder text.
112
+
113
+ Returns:
114
+ List of template strings.
115
+ """
116
+ raise NotImplementedError
117
+
118
+ def get_keyword_map(self) -> dict[str, list[int]]:
119
+ """Return a mapping of keywords to preferred template indices.
120
+
121
+ Override in subclasses to steer template selection based on
122
+ concept content. Keys are lowercase keywords/phrases, values
123
+ are lists of template indices that work well for that keyword.
124
+
125
+ Returns:
126
+ Dictionary mapping keywords to template index lists.
127
+ """
128
+ return {}
129
+
130
+ def select_template(self, concept: str) -> str:
131
+ """Select the best template for the given concept.
132
+
133
+ Uses keyword matching to find relevant templates. Falls back
134
+ to random selection if no keywords match.
135
+
136
+ Args:
137
+ concept: The concept text.
138
+
139
+ Returns:
140
+ A single template string.
141
+ """
142
+ concept_lower = concept.lower()
143
+ scored_indices: dict[int, int] = {}
144
+
145
+ for keyword, indices in self._keyword_map.items():
146
+ if keyword in concept_lower:
147
+ for idx in indices:
148
+ if 0 <= idx < len(self._templates):
149
+ scored_indices[idx] = scored_indices.get(idx, 0) + 1
150
+
151
+ if scored_indices:
152
+ max_score = max(scored_indices.values())
153
+ best = [i for i, s in scored_indices.items() if s == max_score]
154
+ chosen = random.choice(best)
155
+ return self._templates[chosen]
156
+
157
+ return random.choice(self._templates)
158
+
159
+ def extract_key_terms(self, concept: str) -> list[str]:
160
+ """Extract significant terms from the concept for template filling.
161
+
162
+ Args:
163
+ concept: The concept text.
164
+
165
+ Returns:
166
+ List of key terms found in the concept.
167
+ """
168
+ stop_words = {
169
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
170
+ "being", "have", "has", "had", "do", "does", "did", "will",
171
+ "would", "could", "should", "may", "might", "can", "shall",
172
+ "of", "in", "to", "for", "with", "on", "at", "from", "by",
173
+ "about", "as", "into", "through", "during", "before", "after",
174
+ "above", "below", "between", "and", "but", "or", "nor", "not",
175
+ "so", "yet", "both", "either", "neither", "each", "every",
176
+ "this", "that", "these", "those", "it", "its", "they", "them",
177
+ "their", "we", "our", "you", "your", "he", "she", "his", "her",
178
+ "how", "what", "when", "where", "which", "who", "why",
179
+ }
180
+ words = re.findall(r'\b[a-zA-Z]{3,}\b', concept.lower())
181
+ return [w for w in words if w not in stop_words]
182
+
183
+ def __repr__(self) -> str:
184
+ return f"{self.__class__.__name__}(name={self.name!r}, perspective={self.perspective!r})"
reasoning_forge/agents/critic_agent.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Critic Agent - Evaluates all other agents' outputs for quality, accuracy, and completeness.
3
+
4
+ Checks logical clarity, conceptual accuracy, identifies redundancy between
5
+ perspectives, finds missing perspectives, and suggests improvements.
6
+ Returns structured critique with scores.
7
+ """
8
+
9
+ import re
10
+ import logging
11
+ from reasoning_forge.agents.base_agent import ReasoningAgent
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CriticAgent(ReasoningAgent):
17
+ name = "Critic"
18
+ perspective = "meta_evaluative"
19
+ adapter_name = "multi_perspective" # Use multi-perspective adapter for meta-evaluation
20
+
21
+ def get_analysis_templates(self) -> list[str]:
22
+ # The critic does not use templates in the same way -- it evaluates
23
+ # other agents' outputs. These templates are used for framing the
24
+ # overall critique report.
25
+ return [
26
+ "Evaluating the ensemble analysis of '{concept}'.",
27
+ ]
28
+
29
+ def analyze(self, concept: str) -> str:
30
+ """Analyze using the multi-perspective adapter for meta-evaluation.
31
+
32
+ This delegates to the parent class which uses LLM if orchestrator
33
+ is available, or templates otherwise.
34
+ """
35
+ return super().analyze(concept)
36
+
37
+
38
+ def evaluate_ensemble_with_llm(
39
+ self,
40
+ concept: str,
41
+ analyses: dict[str, str],
42
+ ) -> dict:
43
+ """Use LLM to evaluate ensemble with real reasoning about quality.
44
+
45
+ Falls back to heuristic evaluation if orchestrator unavailable.
46
+
47
+ Args:
48
+ concept: Original concept
49
+ analyses: Dict of agent_name -> analysis_text
50
+
51
+ Returns:
52
+ Structured critique from the LLM
53
+ """
54
+ if not self.orchestrator or not self.adapter_name:
55
+ # Fallback to heuristic evaluation
56
+ return self.evaluate_ensemble(concept, analyses)
57
+
58
+ # Build a prompt asking the LLM to evaluate the analyses
59
+ analyses_text = "\n\n".join([
60
+ f"**{agent}**:\n{text[:300]}..."
61
+ for agent, text in analyses.items()
62
+ ])
63
+
64
+ eval_prompt = f"""Evaluate this ensemble analysis of "{concept}":
65
+
66
+ {analyses_text}
67
+
68
+ Provide a JSON assessment with:
69
+ - agent_scores: subjective quality scores per agent (0-1)
70
+ - strengths: key insights across perspectives
71
+ - weaknesses: gaps and redundancies
72
+ - overall_quality: aggregate score (0-1)"""
73
+
74
+ try:
75
+ response, tokens, _ = self.orchestrator.generate(
76
+ query=eval_prompt,
77
+ adapter_name=self.adapter_name,
78
+ system_prompt="You are a meta-evaluator of reasoning quality. Reply in valid JSON.",
79
+ enable_tools=False
80
+ )
81
+
82
+ # Try to parse JSON response
83
+ import json
84
+ start = response.find('{')
85
+ end = response.rfind('}') + 1
86
+ if start >= 0 and end > start:
87
+ try:
88
+ critique_dict = json.loads(response[start:end])
89
+ critique_dict["concept"] = concept
90
+ return critique_dict
91
+ except json.JSONDecodeError:
92
+ logger.debug("Could not parse JSON from LLM evaluation")
93
+ except Exception as e:
94
+ logger.warning(f"LLM evaluation failed: {e}")
95
+
96
+ # Fallback to heuristic if LLM fails
97
+ return self.evaluate_ensemble(concept, analyses)
98
+
99
+ def evaluate_ensemble(
100
+ self,
101
+ concept: str,
102
+ analyses: dict[str, str],
103
+ ) -> dict:
104
+ """Evaluate all agent analyses and produce a structured critique.
105
+
106
+ Args:
107
+ concept: The original concept being analyzed.
108
+ analyses: Dict mapping agent_name -> analysis_text.
109
+
110
+ Returns:
111
+ Dictionary with scores, redundancies, gaps, and suggestions.
112
+ """
113
+ critique = {
114
+ "concept": concept,
115
+ "agent_scores": {},
116
+ "redundancies": [],
117
+ "missing_perspectives": [],
118
+ "improvement_suggestions": [],
119
+ "overall_quality": 0.0,
120
+ }
121
+
122
+ total_clarity = 0.0
123
+ total_accuracy = 0.0
124
+ agent_count = len(analyses)
125
+
126
+ for agent_name, text in analyses.items():
127
+ clarity = self._score_logical_clarity(text)
128
+ accuracy = self._score_conceptual_accuracy(text, concept)
129
+ critique["agent_scores"][agent_name] = {
130
+ "logical_clarity": round(clarity, 2),
131
+ "conceptual_accuracy": round(accuracy, 2),
132
+ "combined": round((clarity + accuracy) / 2, 2),
133
+ }
134
+ total_clarity += clarity
135
+ total_accuracy += accuracy
136
+
137
+ # Detect redundancy between perspectives
138
+ critique["redundancies"] = self._detect_redundancy(analyses)
139
+
140
+ # Identify missing perspectives
141
+ critique["missing_perspectives"] = self._find_missing_perspectives(
142
+ concept, analyses
143
+ )
144
+
145
+ # Generate improvement suggestions
146
+ critique["improvement_suggestions"] = self._suggest_improvements(
147
+ concept, analyses, critique["agent_scores"]
148
+ )
149
+
150
+ # Overall quality score
151
+ if agent_count > 0:
152
+ avg_clarity = total_clarity / agent_count
153
+ avg_accuracy = total_accuracy / agent_count
154
+ redundancy_penalty = len(critique["redundancies"]) * 0.03
155
+ gap_penalty = len(critique["missing_perspectives"]) * 0.05
156
+ raw_score = (avg_clarity + avg_accuracy) / 2 - redundancy_penalty - gap_penalty
157
+ critique["overall_quality"] = round(max(0.0, min(1.0, raw_score)), 2)
158
+
159
+ return critique
160
+
161
+ def _score_logical_clarity(self, text: str) -> float:
162
+ """Score the logical clarity of an analysis on a 0-1 scale.
163
+
164
+ Heuristics:
165
+ - Presence of logical connectives (therefore, because, however, thus)
166
+ - Sentence structure variety (not all same length)
167
+ - Specificity (concrete terms vs vague language)
168
+ - Reasonable length (not too terse, not padded)
169
+ """
170
+ score = 0.5 # baseline
171
+
172
+ # Logical connectives indicate reasoning structure
173
+ connectives = [
174
+ "because", "therefore", "thus", "however", "although",
175
+ "consequently", "since", "given that", "implies",
176
+ "it follows", "this means", "as a result", "in contrast",
177
+ "specifically", "for example", "in particular",
178
+ ]
179
+ connective_count = sum(1 for c in connectives if c in text.lower())
180
+ score += min(0.2, connective_count * 0.025)
181
+
182
+ # Sentence variety (std dev of sentence lengths)
183
+ sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]
184
+ if len(sentences) >= 3:
185
+ lengths = [len(s.split()) for s in sentences]
186
+ mean_len = sum(lengths) / len(lengths)
187
+ variance = sum((l - mean_len) ** 2 for l in lengths) / len(lengths)
188
+ std_dev = variance ** 0.5
189
+ if 3 < std_dev < 15:
190
+ score += 0.1
191
+ elif std_dev >= 1:
192
+ score += 0.05
193
+
194
+ # Penalize vague language
195
+ vague_terms = [
196
+ "things", "stuff", "a lot", "very", "really",
197
+ "kind of", "sort of", "basically", "obviously",
198
+ ]
199
+ vague_count = sum(1 for v in vague_terms if v in text.lower())
200
+ score -= vague_count * 0.03
201
+
202
+ # Length check (reward substantive, penalize extreme)
203
+ word_count = len(text.split())
204
+ if 80 <= word_count <= 300:
205
+ score += 0.1
206
+ elif 50 <= word_count < 80 or 300 < word_count <= 500:
207
+ score += 0.05
208
+ elif word_count < 30:
209
+ score -= 0.15
210
+
211
+ return max(0.0, min(1.0, score))
212
+
213
+ def _score_conceptual_accuracy(self, text: str, concept: str) -> float:
214
+ """Score how well the analysis engages with the actual concept.
215
+
216
+ Heuristics:
217
+ - References to the concept terms
218
+ - Domain-appropriate vocabulary
219
+ - Absence of generic placeholder language
220
+ """
221
+ score = 0.5
222
+
223
+ concept_terms = set(re.findall(r'\b[a-zA-Z]{4,}\b', concept.lower()))
224
+ text_lower = text.lower()
225
+
226
+ # Check that concept terms appear in the analysis
227
+ if concept_terms:
228
+ found = sum(1 for t in concept_terms if t in text_lower)
229
+ coverage = found / len(concept_terms)
230
+ score += coverage * 0.15
231
+
232
+ # Penalize generic placeholder language
233
+ placeholders = [
234
+ "this concept can be approached",
235
+ "from this perspective we see",
236
+ "looking at this through",
237
+ "applying this lens",
238
+ "in conclusion",
239
+ "to summarize",
240
+ ]
241
+ placeholder_count = sum(1 for p in placeholders if p in text_lower)
242
+ score -= placeholder_count * 0.05
243
+
244
+ # Reward specific domain vocabulary (indicates substantive analysis)
245
+ domain_terms = [
246
+ "mechanism", "cause", "effect", "evidence", "principle",
247
+ "constraint", "trade-off", "interaction", "dynamic",
248
+ "structure", "function", "process", "system", "pattern",
249
+ "relationship", "variable", "outcome", "hypothesis",
250
+ "implication", "assumption", "framework", "model",
251
+ ]
252
+ domain_count = sum(1 for d in domain_terms if d in text_lower)
253
+ score += min(0.2, domain_count * 0.02)
254
+
255
+ # Reward analysis length proportional to concept complexity
256
+ concept_word_count = len(concept.split())
257
+ text_word_count = len(text.split())
258
+ if text_word_count >= concept_word_count * 3:
259
+ score += 0.1
260
+
261
+ return max(0.0, min(1.0, score))
262
+
263
+ def _detect_redundancy(self, analyses: dict[str, str]) -> list[str]:
264
+ """Detect thematic redundancy between agent analyses."""
265
+ redundancies = []
266
+ agent_names = list(analyses.keys())
267
+
268
+ for i in range(len(agent_names)):
269
+ for j in range(i + 1, len(agent_names)):
270
+ name_a = agent_names[i]
271
+ name_b = agent_names[j]
272
+ overlap = self._compute_content_overlap(
273
+ analyses[name_a], analyses[name_b]
274
+ )
275
+ if overlap > 0.35:
276
+ redundancies.append(
277
+ f"{name_a} and {name_b} share significant thematic overlap "
278
+ f"({overlap:.0%}). Consider diversifying their angles of analysis."
279
+ )
280
+ return redundancies
281
+
282
+ def _compute_content_overlap(self, text_a: str, text_b: str) -> float:
283
+ """Compute Jaccard similarity of significant word sets."""
284
+ stop_words = {
285
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
286
+ "being", "have", "has", "had", "do", "does", "did", "will",
287
+ "would", "could", "should", "may", "might", "can", "shall",
288
+ "of", "in", "to", "for", "with", "on", "at", "from", "by",
289
+ "about", "as", "into", "through", "during", "before", "after",
290
+ "and", "but", "or", "nor", "not", "so", "yet", "both",
291
+ "this", "that", "these", "those", "it", "its", "they", "them",
292
+ "their", "we", "our", "you", "your", "he", "she", "his", "her",
293
+ }
294
+ words_a = {
295
+ w for w in re.findall(r'\b[a-z]{4,}\b', text_a.lower())
296
+ if w not in stop_words
297
+ }
298
+ words_b = {
299
+ w for w in re.findall(r'\b[a-z]{4,}\b', text_b.lower())
300
+ if w not in stop_words
301
+ }
302
+ if not words_a or not words_b:
303
+ return 0.0
304
+ intersection = words_a & words_b
305
+ union = words_a | words_b
306
+ return len(intersection) / len(union)
307
+
308
+ def _find_missing_perspectives(
309
+ self, concept: str, analyses: dict[str, str]
310
+ ) -> list[str]:
311
+ """Identify perspectives that are absent from the ensemble."""
312
+ missing = []
313
+ all_text = " ".join(analyses.values()).lower()
314
+
315
+ perspective_checks = [
316
+ ("temporal/historical", [
317
+ "history", "historical", "evolution", "over time", "timeline",
318
+ "past", "trajectory", "precedent", "legacy",
319
+ ]),
320
+ ("quantitative/statistical", [
321
+ "statistic", "data", "quantif", "measur", "metric",
322
+ "number", "percentage", "rate", "frequency",
323
+ ]),
324
+ ("ecological/environmental", [
325
+ "environment", "ecolog", "sustainab", "ecosystem",
326
+ "resource", "footprint", "biodiversity", "pollution",
327
+ ]),
328
+ ("economic/financial", [
329
+ "economic", "financial", "cost", "benefit", "market",
330
+ "incentive", "investment", "capital", "trade",
331
+ ]),
332
+ ("legal/regulatory", [
333
+ "legal", "law", "regulat", "compliance", "policy",
334
+ "legislation", "governance", "jurisdiction",
335
+ ]),
336
+ ("educational/pedagogical", [
337
+ "learn", "teach", "education", "pedagog", "curriculum",
338
+ "training", "skill", "literacy",
339
+ ]),
340
+ ]
341
+
342
+ for perspective_name, indicators in perspective_checks:
343
+ found = sum(1 for ind in indicators if ind in all_text)
344
+ if found < 2:
345
+ missing.append(
346
+ f"The ensemble lacks a {perspective_name} perspective. "
347
+ f"Consider how '{concept}' relates to {perspective_name} dimensions."
348
+ )
349
+
350
+ return missing[:3] # Limit to top 3 gaps
351
+
352
+ def _suggest_improvements(
353
+ self,
354
+ concept: str,
355
+ analyses: dict[str, str],
356
+ scores: dict[str, dict],
357
+ ) -> list[str]:
358
+ """Generate actionable improvement suggestions."""
359
+ suggestions = []
360
+
361
+ # Identify weakest agent
362
+ if scores:
363
+ weakest = min(scores.items(), key=lambda x: x[1]["combined"])
364
+ if weakest[1]["combined"] < 0.6:
365
+ suggestions.append(
366
+ f"The {weakest[0]} analysis scored lowest ({weakest[1]['combined']:.2f}). "
367
+ f"It would benefit from more specific engagement with the concept's "
368
+ f"concrete details rather than abstract framing."
369
+ )
370
+
371
+ # Check for concrete examples
372
+ all_text = " ".join(analyses.values()).lower()
373
+ example_indicators = ["for example", "for instance", "such as", "e.g.", "consider"]
374
+ example_count = sum(1 for e in example_indicators if e in all_text)
375
+ if example_count < 2:
376
+ suggestions.append(
377
+ "The ensemble would benefit from more concrete examples and "
378
+ "illustrations. Abstract reasoning without grounding in specifics "
379
+ "is less persuasive and harder to verify."
380
+ )
381
+
382
+ # Check for cross-perspective dialogue
383
+ agent_names_lower = [n.lower() for n in analyses.keys()]
384
+ cross_references = sum(
385
+ 1 for name in agent_names_lower
386
+ if any(name in text.lower() for text in analyses.values())
387
+ )
388
+ if cross_references < 2:
389
+ suggestions.append(
390
+ "The analyses operate largely in isolation. The synthesis would benefit "
391
+ "from explicit cross-referencing between perspectives -- showing where "
392
+ "they agree, disagree, or complement each other."
393
+ )
394
+
395
+ # Check for actionable takeaways
396
+ action_indicators = [
397
+ "should", "must", "recommend", "suggest", "action",
398
+ "implement", "strategy", "step", "practice",
399
+ ]
400
+ action_count = sum(1 for a in action_indicators if a in all_text)
401
+ if action_count < 3:
402
+ suggestions.append(
403
+ "The ensemble is more diagnostic than prescriptive. Adding concrete, "
404
+ "actionable recommendations would increase practical value."
405
+ )
406
+
407
+ return suggestions[:4] # Limit to top 4 suggestions
reasoning_forge/agents/davinci_agent.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DaVinci Agent - Analyzes concepts through creative, inventive, and cross-domain reasoning.
3
+
4
+ Focuses on cross-domain connections, biomimicry and nature-inspired solutions,
5
+ iterative improvement possibilities, visual/spatial reasoning, and novel
6
+ combinations of existing ideas.
7
+ """
8
+
9
+ from reasoning_forge.agents.base_agent import ReasoningAgent
10
+
11
+
12
+ class DaVinciAgent(ReasoningAgent):
13
+ name = "DaVinci"
14
+ perspective = "creative_and_inventive"
15
+ adapter_name = "davinci" # Use the DaVinci LoRA adapter for real inference
16
+
17
+ def get_analysis_templates(self) -> list[str]:
18
+ return [
19
+ # 0 - Cross-domain analogy
20
+ (
21
+ "Drawing cross-domain connections to '{concept}': the deepest insights often "
22
+ "come from recognizing structural similarities between apparently unrelated "
23
+ "fields. A river delta and a lightning bolt share the same branching "
24
+ "optimization geometry. A market economy and an ant colony share the same "
25
+ "decentralized coordination logic. For '{concept}', the creative question "
26
+ "is: what other domain exhibits the same deep structure? If we map the "
27
+ "entities, relationships, and dynamics of '{concept}' onto those of the "
28
+ "analogous domain, which features are preserved (revealing shared principles) "
29
+ "and which break (revealing domain-specific constraints)? The preserved "
30
+ "features point toward universal laws; the broken features point toward "
31
+ "opportunities for domain-specific innovation."
32
+ ),
33
+ # 1 - Biomimicry lens
34
+ (
35
+ "Examining '{concept}' through biomimicry: nature has been solving design "
36
+ "problems for 3.8 billion years through evolutionary optimization. Bones "
37
+ "achieve maximum strength with minimum material by using trabecular "
38
+ "architecture -- hollow struts arranged along stress lines. Spider silk "
39
+ "achieves tensile strength exceeding steel at a fraction of the weight "
40
+ "through hierarchical nanostructure. Termite mounds maintain constant "
41
+ "internal temperature without energy input through passive ventilation "
42
+ "design. For '{concept}', the biomimicry question is: what organism or "
43
+ "ecosystem has already solved an analogous problem, and what principle "
44
+ "does its solution exploit that we have not yet applied?"
45
+ ),
46
+ # 2 - Combinatorial invention
47
+ (
48
+ "Approaching '{concept}' through combinatorial creativity: most inventions "
49
+ "are novel combinations of existing elements. The printing press combined "
50
+ "the wine press, movable type, oil-based ink, and paper. The smartphone "
51
+ "combined a phone, camera, GPS, accelerometer, and internet browser into "
52
+ "a device that is qualitatively different from any of its components. For "
53
+ "'{concept}', the combinatorial strategy asks: what are the elemental "
54
+ "components, and what happens when we recombine them in unusual ways? "
55
+ "Pair each element with every other element and ask whether the combination "
56
+ "produces something valuable. The most productive combinations are often "
57
+ "between elements from distant categories that no one thought to connect."
58
+ ),
59
+ # 3 - Inversion and reversal
60
+ (
61
+ "Inverting '{concept}': one of the most powerful creative strategies is "
62
+ "systematic inversion -- taking every assumption and reversing it. If the "
63
+ "current approach pushes, try pulling. If it adds, try subtracting. If it "
64
+ "centralizes, try distributing. If it speeds up, try slowing down. Many "
65
+ "breakthrough solutions came from inverting an assumption everyone took for "
66
+ "granted. Vacuum cleaners worked by pushing air until Dyson inverted the "
67
+ "flow. Assembly lines brought work to workers; Toyota inverted this by "
68
+ "bringing workers to work (cellular manufacturing). For '{concept}', "
69
+ "systematically listing and inverting each assumption reveals a space of "
70
+ "unconventional approaches that conventional thinking renders invisible."
71
+ ),
72
+ # 4 - Visual-spatial reasoning
73
+ (
74
+ "Visualizing the spatial architecture of '{concept}': representing abstract "
75
+ "relationships as spatial structures makes hidden patterns visible. If we "
76
+ "map the components of '{concept}' to nodes and their relationships to "
77
+ "edges, the resulting graph reveals clustering (tightly connected subgroups), "
78
+ "bridges (elements connecting otherwise separate clusters), hubs (elements "
79
+ "with many connections), and periphery (weakly connected elements). The "
80
+ "topology of this graph -- its shape, density, and symmetry -- encodes "
81
+ "information about the concept's structure that verbal description alone "
82
+ "cannot capture. Hub nodes are high-leverage intervention points; bridges "
83
+ "are fragile connections whose failure would fragment the system."
84
+ ),
85
+ # 5 - Constraint as catalyst
86
+ (
87
+ "Using constraints as creative catalysts for '{concept}': rather than seeing "
88
+ "limitations as obstacles, use them as forcing functions for innovation. "
89
+ "Twitter's 140-character limit forced a new style of writing. The sonnet's "
90
+ "14-line constraint forced poetic compression. Budget constraints force "
91
+ "elegant engineering. For '{concept}', deliberately imposing additional "
92
+ "constraints -- what if we had to solve this with half the resources? In "
93
+ "one-tenth the time? With no electricity? For a user who cannot see? -- "
94
+ "often breaks through conventional thinking by invalidating the default "
95
+ "approach and forcing genuinely creative alternatives."
96
+ ),
97
+ # 6 - First principles reconstruction
98
+ (
99
+ "Reconstructing '{concept}' from first principles: strip away all inherited "
100
+ "conventions, historical accidents, and 'we have always done it this way' "
101
+ "accretions. What remains when we reduce the problem to its fundamental "
102
+ "requirements? Starting from physical laws, human needs, and mathematical "
103
+ "constraints, what is the minimum viable solution? Often the gap between "
104
+ "this first-principles design and the current state reveals enormous "
105
+ "inefficiency that is invisible from within the conventional framework. "
106
+ "SpaceX re-derived rocket design from first principles and found that "
107
+ "materials cost only 2% of the final price. For '{concept}', the first-"
108
+ "principles question is: if we were designing this from scratch today, "
109
+ "knowing what we know, what would it look like?"
110
+ ),
111
+ # 7 - Morphological analysis
112
+ (
113
+ "Applying morphological analysis to '{concept}': decompose the concept into "
114
+ "its independent dimensions, list the possible values for each dimension, "
115
+ "and then systematically explore the combinatorial space. If '{concept}' has "
116
+ "five dimensions with four options each, the morphological space contains "
117
+ "1024 configurations. Most are impractical, but a systematic sweep guarantees "
118
+ "that no promising combination is overlooked by the biases of free-form "
119
+ "brainstorming. The power of morphological analysis is that it converts "
120
+ "creative search from a haphazard process into a structured exploration, "
121
+ "surfacing configurations that no one would think of spontaneously because "
122
+ "they cross conventional category boundaries."
123
+ ),
124
+ # 8 - Prototype thinking
125
+ (
126
+ "Applying prototype thinking to '{concept}': instead of perfecting a plan "
127
+ "before executing, build the quickest possible embodiment of the core idea "
128
+ "and learn from its failures. The prototype is not the solution but a "
129
+ "question asked in physical form: 'does this work?' Each prototype cycle "
130
+ "-- build, test, learn, rebuild -- compresses the feedback loop and "
131
+ "generates knowledge that purely theoretical analysis cannot provide. For "
132
+ "'{concept}', the prototype question is: what is the smallest, cheapest, "
133
+ "fastest experiment that would test the most critical assumption? Building "
134
+ "that experiment, even if crude, will teach us more than months of "
135
+ "theoretical refinement."
136
+ ),
137
+ # 9 - Emergent properties through scale
138
+ (
139
+ "Exploring emergent properties of '{concept}' at different scales: systems "
140
+ "often exhibit qualitatively new behavior when scaled up or down. A single "
141
+ "neuron computes nothing interesting; a billion networked neurons produce "
142
+ "consciousness. A single transaction is trivial; billions of transactions "
143
+ "produce market dynamics. For '{concept}', the scale question asks: what "
144
+ "happens when we multiply the instances by a thousand? By a million? What "
145
+ "new phenomena emerge at scale that are absent at the individual level? "
146
+ "Conversely, what happens when we reduce to a single instance? Scale "
147
+ "transitions often reveal the concept's most interesting properties."
148
+ ),
149
+ # 10 - Da Vinci's sfumato (ambiguity as resource)
150
+ (
151
+ "Embracing the sfumato of '{concept}': Leonardo da Vinci practiced sfumato "
152
+ "-- the technique of leaving edges soft and ambiguous rather than sharply "
153
+ "defined. In creative reasoning, maintaining productive ambiguity resists "
154
+ "premature closure and keeps the interpretive space open. The undefined "
155
+ "edges of '{concept}' are not defects but fertile zones where new "
156
+ "connections can form. Attempts to define everything precisely may satisfy "
157
+ "the desire for clarity but kill the creative potential that lives in "
158
+ "the ambiguous spaces between categories. Sit with the ambiguity long "
159
+ "enough and patterns emerge that rigid definitions would have prevented."
160
+ ),
161
+ # 11 - Lateral thinking transfer
162
+ (
163
+ "Applying lateral thinking to '{concept}': Edward de Bono's lateral "
164
+ "thinking techniques include random entry (inject an unrelated concept "
165
+ "and force a connection), provocation (make a deliberately absurd statement "
166
+ "and extract useful ideas from it), and challenge (question why things are "
167
+ "done the current way). For '{concept}', a random entry might connect it "
168
+ "to deep-sea bioluminescence, medieval cathedral construction, or jazz "
169
+ "improvisation. The forced connection between '{concept}' and a random "
170
+ "domain breaks habitual thought patterns and creates novel pathways that "
171
+ "logical deduction alone cannot reach."
172
+ ),
173
+ # 12 - Fractal self-similarity
174
+ (
175
+ "Examining '{concept}' for fractal self-similarity: does the same pattern "
176
+ "recur at different scales? Coastlines look similar whether photographed "
177
+ "from a satellite or a drone. Organizational hierarchies replicate the same "
178
+ "power dynamics from teams to departments to divisions. Blood vessel "
179
+ "networks branch according to the same rules from arteries to capillaries. "
180
+ "If '{concept}' exhibits self-similarity, then understanding the pattern at "
181
+ "one scale gives us understanding at all scales. A single well-studied "
182
+ "instance contains the blueprint for the entire hierarchy, and interventions "
183
+ "that work at one scale can be adapted to work at others."
184
+ ),
185
+ # 13 - Negative space analysis
186
+ (
187
+ "Analyzing the negative space of '{concept}': just as a sculptor defines a "
188
+ "form by removing material, we can define '{concept}' by examining what it "
189
+ "is not. What has been excluded, ignored, or left unsaid? The negative space "
190
+ "-- the complement of the concept -- often contains crucial information. "
191
+ "What alternatives were considered and rejected? What possibilities does "
192
+ "the current framing render invisible? The adjacent possible (the set of "
193
+ "things that are one step away from existing) is often more interesting "
194
+ "than the concept itself, because it represents the immediate frontier "
195
+ "of innovation."
196
+ ),
197
+ # 14 - Systems of constraints (Rube Goldberg inversion)
198
+ (
199
+ "Simplifying '{concept}' by subtracting rather than adding: the natural "
200
+ "tendency in design is to add features, layers, and complexity. The "
201
+ "harder and more valuable creative move is subtraction: what can we "
202
+ "remove while preserving or improving function? Antoine de Saint-Exupery "
203
+ "said perfection is achieved not when there is nothing left to add, but "
204
+ "when there is nothing left to take away. For '{concept}', the subtraction "
205
+ "exercise asks: what happens if we remove each component in turn? Which "
206
+ "removals are catastrophic (essential components) and which are beneficial "
207
+ "(removing parasitic complexity)? The minimal viable version is often "
208
+ "more powerful than the maximal one."
209
+ ),
210
+ # 15 - TRIZ inventive principles
211
+ (
212
+ "Applying TRIZ inventive principles to '{concept}': Genrich Altshuller's "
213
+ "analysis of 200,000 patents revealed 40 recurring inventive principles. "
214
+ "Segmentation (divide a monolithic system into parts). Extraction (remove "
215
+ "a problematic element and deal with it separately). Local quality (make "
216
+ "each part optimized for its local function rather than forcing uniformity). "
217
+ "Asymmetry (break the symmetry of a symmetric design to improve function). "
218
+ "Nesting (place one object inside another). Prior action (perform required "
219
+ "changes before they are needed). For '{concept}', systematically applying "
220
+ "each principle generates a structured menu of inventive strategies that "
221
+ "goes far beyond unconstrained brainstorming."
222
+ ),
223
+ # 16 - Synesthesia and cross-modal thinking
224
+ (
225
+ "Engaging cross-modal perception for '{concept}': what does this concept "
226
+ "sound like? What texture does it have? What temperature? What color? "
227
+ "Cross-modal associations -- thinking about a concept through sensory "
228
+ "channels that do not literally apply -- activate neural pathways that "
229
+ "linear verbal reasoning does not reach. Kandinsky heard colors and saw "
230
+ "sounds; this synesthetic thinking produced radically new art. For "
231
+ "'{concept}', translating it into sensory terms (the rhythm of its "
232
+ "processes, the texture of its interactions, the weight of its consequences) "
233
+ "can reveal structural features that abstract analysis misses."
234
+ ),
235
+ # 17 - Nature's design patterns
236
+ (
237
+ "Identifying nature's design patterns in '{concept}': evolution has converged "
238
+ "on certain solutions repeatedly because they are optimal under common "
239
+ "constraints. Hexagonal packing (beehives, basalt columns) maximizes area "
240
+ "with minimum material. Branching networks (trees, rivers, lungs, lightning) "
241
+ "optimize distribution from a source to a volume. Spiral growth (shells, "
242
+ "galaxies, hurricanes) manages expansion while maintaining structural "
243
+ "integrity. For '{concept}', asking which of nature's recurring design "
244
+ "patterns applies suggests time-tested architectures that human design "
245
+ "has not yet exploited."
246
+ ),
247
+ # 18 - Bisociation and humor
248
+ (
249
+ "Applying Koestler's bisociation to '{concept}': Arthur Koestler proposed "
250
+ "that creativity, humor, and scientific discovery share the same cognitive "
251
+ "mechanism: bisociation -- the simultaneous perception of a situation in "
252
+ "two habitually incompatible frames of reference. The collision of frames "
253
+ "produces a flash of insight (in science), a punchline (in humor), or a "
254
+ "novel artifact (in art). For '{concept}', identifying two incompatible "
255
+ "but individually valid frames and forcing them to coexist generates the "
256
+ "cognitive tension from which genuinely original ideas spring. The more "
257
+ "distant the frames, the more surprising and potentially valuable the "
258
+ "bisociative insight."
259
+ ),
260
+ # 19 - Future archaeology
261
+ (
262
+ "Practicing future archaeology on '{concept}': imagine examining the "
263
+ "artifacts of this concept a hundred years from now, from a future "
264
+ "civilization's perspective. What would they find elegant? What would "
265
+ "they find primitive? What would puzzle them about our choices? This "
266
+ "temporal displacement reveals assumptions we cannot see from within our "
267
+ "own era. The future archaeologist would ask: why did they do it this way "
268
+ "when a simpler method was available? What constraint -- technological, "
269
+ "social, or cognitive -- forced this particular design? For '{concept}', "
270
+ "this exercise separates the timeless core from the historically contingent "
271
+ "shell and suggests directions for forward-looking redesign."
272
+ ),
273
+ ]
274
+
275
+ def get_keyword_map(self) -> dict[str, list[int]]:
276
+ return {
277
+ "analog": [0, 18], "similar": [0, 12], "connect": [0, 4],
278
+ "nature": [1, 17], "biolog": [1, 17], "organism": [1],
279
+ "combin": [2, 7], "element": [2, 7], "component": [2],
280
+ "invert": [3], "revers": [3], "opposit": [3],
281
+ "visual": [4], "spatial": [4], "map": [4], "graph": [4],
282
+ "constrain": [5], "limit": [5], "restrict": [5],
283
+ "first principle": [6], "fundament": [6], "basic": [6],
284
+ "dimension": [7], "option": [7], "configur": [7],
285
+ "prototype": [8], "experiment": [8], "test": [8], "iterate": [8],
286
+ "scale": [9, 12], "grow": [9], "expand": [9],
287
+ "ambigu": [10], "fuzzy": [10], "unclear": [10],
288
+ "creativ": [11, 18], "novel": [11, 18], "innovat": [11],
289
+ "pattern": [12, 17], "recur": [12], "repeat": [12],
290
+ "absent": [13], "missing": [13], "negative": [13],
291
+ "simplif": [14], "remov": [14], "minimal": [14],
292
+ "invent": [15], "patent": [15], "engineer": [15],
293
+ "sense": [16], "perceiv": [16], "feel": [16],
294
+ "evolut": [17], "converge": [17], "branch": [17],
295
+ "humor": [18], "surprising": [18], "collision": [18],
296
+ "future": [19], "legacy": [19], "long-term": [19],
297
+ "technology": [2, 6, 15], "design": [1, 14, 15],
298
+ "art": [10, 16], "music": [16, 18],
299
+ }
300
+
301
+ def analyze(self, concept: str) -> str:
302
+ template = self.select_template(concept)
303
+ return template.replace("{concept}", concept)
reasoning_forge/agents/empathy_agent.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Empathy Agent - Analyzes concepts through emotional, human-centered, and social reasoning.
3
+
4
+ Focuses on how concepts affect people emotionally, compassionate interpretation,
5
+ social dynamics, communication considerations, and psychological well-being.
6
+ """
7
+
8
+ from reasoning_forge.agents.base_agent import ReasoningAgent
9
+
10
+
11
+ class EmpathyAgent(ReasoningAgent):
12
+ name = "Empathy"
13
+ perspective = "emotional_and_human_centered"
14
+ adapter_name = "empathy" # Use the Empathy LoRA adapter for real inference
15
+
16
+ def get_analysis_templates(self) -> list[str]:
17
+ return [
18
+ # 0 - Emotional impact mapping
19
+ (
20
+ "Mapping the emotional landscape of '{concept}': every concept that touches "
21
+ "human lives generates an emotional field. For those directly involved, "
22
+ "'{concept}' may evoke hope (if it promises improvement), anxiety (if it "
23
+ "threatens the familiar), frustration (if it introduces complexity), or "
24
+ "excitement (if it opens new possibilities). These emotional responses are "
25
+ "not irrational noise overlaid on a rational signal -- they are a rapid, "
26
+ "parallel processing system that integrates more information than conscious "
27
+ "analysis can handle. Dismissing emotional responses as irrelevant is "
28
+ "itself an emotional decision (the emotion of wanting to appear rational) "
29
+ "and discards valuable signal about how '{concept}' is actually experienced "
30
+ "by the people it affects."
31
+ ),
32
+ # 1 - Lived experience perspective
33
+ (
34
+ "Centering the lived experience of '{concept}': abstract analysis risks "
35
+ "losing the texture of what this actually means in someone's daily life. "
36
+ "A person encountering '{concept}' does not experience it as a set of "
37
+ "propositions but as a shift in the felt quality of their day -- a new "
38
+ "worry added to their mental load, a new possibility that brightens their "
39
+ "horizon, a new confusion that makes the familiar strange. Understanding "
40
+ "'{concept}' requires not just knowing what it is but feeling what it is "
41
+ "like: the cognitive effort it demands, the social negotiations it requires, "
42
+ "the way it reshapes routines and relationships. This first-person texture "
43
+ "is where the real impact lives."
44
+ ),
45
+ # 2 - Compassionate reframing
46
+ (
47
+ "Reframing '{concept}' with compassion: when people struggle with or resist "
48
+ "this concept, their difficulty is not a deficiency in understanding but a "
49
+ "legitimate response to a genuine challenge. Resistance often signals that "
50
+ "something important is being threatened -- identity, competence, belonging, "
51
+ "or security. Rather than dismissing resistance, compassionate inquiry asks: "
52
+ "what are you protecting? What would need to be true for this to feel safe? "
53
+ "What support would make this manageable? For '{concept}', the compassionate "
54
+ "reframing recognizes that the human response is data about the concept's "
55
+ "real-world fit, not an obstacle to overcome."
56
+ ),
57
+ # 3 - Social dynamics analysis
58
+ (
59
+ "Analyzing the social dynamics activated by '{concept}': concepts do not "
60
+ "exist in isolation; they are adopted, resisted, negotiated, and transformed "
61
+ "through social interaction. In-group/out-group dynamics determine who is "
62
+ "seen as a legitimate voice on this topic. Status hierarchies determine "
63
+ "whose interpretation prevails. Social proof shapes adoption: people look "
64
+ "to others' reactions before forming their own. Groupthink can suppress "
65
+ "dissenting perspectives that would improve collective understanding. For "
66
+ "'{concept}', the social dynamics may matter more than the concept's "
67
+ "intrinsic merits in determining its real-world trajectory."
68
+ ),
69
+ # 4 - Communication and framing
70
+ (
71
+ "Examining how '{concept}' is communicated and framed: the same content, "
72
+ "presented differently, produces dramatically different responses. Loss "
73
+ "framing ('you will lose X if you do not adopt this') activates different "
74
+ "neural circuitry than gain framing ('you will gain X if you adopt this'). "
75
+ "Concrete examples engage empathy; abstract statistics do not. Narrative "
76
+ "structure (beginning-middle-end) makes information memorable; list format "
77
+ "makes it forgettable. For '{concept}', the communication design is not "
78
+ "mere packaging but fundamentally shapes understanding, acceptance, and "
79
+ "behavior. A brilliant concept poorly communicated is indistinguishable "
80
+ "from a mediocre one."
81
+ ),
82
+ # 5 - Psychological safety assessment
83
+ (
84
+ "Assessing the psychological safety implications of '{concept}': people "
85
+ "engage productively with challenging ideas only when they feel safe enough "
86
+ "to be vulnerable -- to admit confusion, ask naive questions, and make "
87
+ "mistakes without social penalty. If '{concept}' is introduced in an "
88
+ "environment where asking questions signals incompetence, where mistakes "
89
+ "are punished, or where dissent is suppressed, people will perform "
90
+ "understanding rather than achieve it. The intellectual quality of "
91
+ "engagement with '{concept}' is bounded by the psychological safety of "
92
+ "the environment. Creating conditions where genuine engagement is safe "
93
+ "is a prerequisite for genuine understanding."
94
+ ),
95
+ # 6 - Identity and belonging
96
+ (
97
+ "Exploring how '{concept}' intersects with identity and belonging: people "
98
+ "do not evaluate concepts in a vacuum; they evaluate them in terms of what "
99
+ "adoption means for their identity. Does embracing '{concept}' signal "
100
+ "membership in a valued group? Does rejecting it? The identity calculus "
101
+ "often overrides the epistemic calculus: people will reject well-supported "
102
+ "ideas that threaten their group membership and accept poorly-supported "
103
+ "ones that affirm it. For '{concept}', understanding the identity landscape "
104
+ "-- which identities this concept affirms, threatens, or is irrelevant to "
105
+ "-- predicts adoption patterns more accurately than the concept's objective "
106
+ "merits."
107
+ ),
108
+ # 7 - Grief and loss recognition
109
+ (
110
+ "Acknowledging the grief dimension of '{concept}': every significant change "
111
+ "involves loss, and loss requires grief. Even positive changes -- a promotion, "
112
+ "a new technology, a better system -- require letting go of the familiar: "
113
+ "old competencies that are now obsolete, old relationships that are now "
114
+ "restructured, old identities that no longer fit. The Kubler-Ross stages "
115
+ "(denial, anger, bargaining, depression, acceptance) are not a rigid sequence "
116
+ "but a map of common emotional responses to loss. For '{concept}', naming "
117
+ "and honoring what is lost -- rather than insisting that only the gains "
118
+ "matter -- allows people to move through the transition rather than getting "
119
+ "stuck in resistance."
120
+ ),
121
+ # 8 - Trust dynamics
122
+ (
123
+ "Analyzing the trust architecture of '{concept}': trust is the invisible "
124
+ "infrastructure that determines whether systems function or fail. It is "
125
+ "built slowly through consistent behavior, transparency, and demonstrated "
126
+ "competence, and destroyed quickly by betrayal, opacity, or incompetence. "
127
+ "For '{concept}', the trust questions are: who needs to trust whom for this "
128
+ "to work? Is that trust warranted by track record? What happens when trust "
129
+ "is violated (is there a repair mechanism)? Are there trust asymmetries "
130
+ "where one party bears vulnerability while the other holds power? Trust "
131
+ "deficits cannot be solved by technical improvements alone -- they require "
132
+ "relational repair."
133
+ ),
134
+ # 9 - Cognitive load and overwhelm
135
+ (
136
+ "Assessing the cognitive load imposed by '{concept}': human working memory "
137
+ "has a limited capacity (roughly 4 +/- 1 chunks of information). Every new "
138
+ "concept that must be held in mind simultaneously competes for this scarce "
139
+ "resource. Complex concepts that require juggling many interrelated pieces "
140
+ "can overwhelm working memory, producing a felt experience of confusion and "
141
+ "frustration that has nothing to do with intellectual capacity and everything "
142
+ "to do with presentation design. For '{concept}', the empathic question is: "
143
+ "how can this be chunked, sequenced, and scaffolded to fit within human "
144
+ "cognitive limits without sacrificing essential complexity?"
145
+ ),
146
+ # 10 - Motivation and meaning
147
+ (
148
+ "Exploring the motivational landscape of '{concept}': Self-Determination "
149
+ "Theory identifies three basic psychological needs: autonomy (the feeling "
150
+ "of volition and choice), competence (the feeling of mastery and effectiveness), "
151
+ "and relatedness (the feeling of connection and belonging). Engagement with "
152
+ "'{concept}' will be intrinsically motivated when it satisfies these needs "
153
+ "and extrinsically motivated (fragile, resentful compliance) when it frustrates "
154
+ "them. For '{concept}', the design question is: does engagement with this "
155
+ "concept make people feel more autonomous, competent, and connected, or does "
156
+ "it impose control, induce helplessness, and isolate?"
157
+ ),
158
+ # 11 - Narrative and storytelling
159
+ (
160
+ "Situating '{concept}' within human narrative: humans are storytelling animals "
161
+ "-- we make sense of the world by constructing narratives with characters, "
162
+ "motivations, conflicts, and resolutions. A concept presented as a story "
163
+ "('there was a problem, people tried solutions, here is what they learned') "
164
+ "is absorbed and remembered far more effectively than the same information "
165
+ "presented as disconnected facts. For '{concept}', the narrative question "
166
+ "is: what is the story here? Who are the characters? What is the conflict? "
167
+ "What is at stake? How does this chapter connect to the larger story that "
168
+ "people are already telling about their lives and work?"
169
+ ),
170
+ # 12 - Perspective-taking exercise
171
+ (
172
+ "Practicing perspective-taking with '{concept}': imagine experiencing this "
173
+ "from the viewpoint of an enthusiastic early adopter (everything is "
174
+ "possibility), a skeptical veteran (I have seen this before and it did not "
175
+ "work), a vulnerable newcomer (I do not understand and I am afraid to ask), "
176
+ "an overwhelmed practitioner (I do not have bandwidth for one more thing), "
177
+ "and a curious outsider (I have no stake but find this interesting). Each "
178
+ "perspective reveals different features of '{concept}' and different emotional "
179
+ "valences. The concept is not one thing but many things, depending on who "
180
+ "is experiencing it and what they bring to the encounter."
181
+ ),
182
+ # 13 - Relational impact
183
+ (
184
+ "Examining how '{concept}' affects relationships: concepts do not only change "
185
+ "what people think; they change how people relate to each other. Does "
186
+ "'{concept}' create shared language that strengthens collaboration, or "
187
+ "jargon that excludes outsiders? Does it create a hierarchy of expertise "
188
+ "that distances the knowledgeable from the uninitiated? Does it provide "
189
+ "common ground for diverse stakeholders or a wedge that divides them? "
190
+ "The relational dimension of '{concept}' -- how it brings people together "
191
+ "or pushes them apart -- often determines its long-term viability more than "
192
+ "its technical merits."
193
+ ),
194
+ # 14 - Stress and coping
195
+ (
196
+ "Analyzing the stress profile of '{concept}': when encountering something "
197
+ "new or challenging, people appraise both the demand (how threatening or "
198
+ "difficult is this?) and their resources (do I have what I need to cope?). "
199
+ "When demands exceed resources, the result is stress. The stress response "
200
+ "narrows attention, reduces creativity, and triggers fight-flight-freeze "
201
+ "behavior -- exactly the opposite of the open, curious engagement that "
202
+ "learning requires. For '{concept}', the empathic design question is: how "
203
+ "can we increase people's resources (support, information, time, practice) "
204
+ "or decrease the perceived demand (scaffolding, chunking, normalization of "
205
+ "struggle) to keep the challenge in the productive zone?"
206
+ ),
207
+ # 15 - Cultural sensitivity
208
+ (
209
+ "Examining '{concept}' through cultural sensitivity: concepts that seem "
210
+ "universal often carry culturally specific assumptions about individualism "
211
+ "vs collectivism, hierarchy vs egalitarianism, directness vs indirectness, "
212
+ "or risk-taking vs caution. A concept designed within an individualist "
213
+ "framework may not translate to collectivist contexts without significant "
214
+ "adaptation. Communication norms that are standard in one culture may be "
215
+ "offensive in another. For '{concept}', cultural sensitivity asks: whose "
216
+ "cultural assumptions are embedded in the default design, and how must the "
217
+ "concept be adapted for genuine cross-cultural validity?"
218
+ ),
219
+ # 16 - Emotional intelligence integration
220
+ (
221
+ "Integrating emotional intelligence into '{concept}': Goleman's framework "
222
+ "identifies self-awareness (recognizing one's own emotions), self-regulation "
223
+ "(managing emotional responses), social awareness (reading others' emotions), "
224
+ "and relationship management (navigating social interactions skillfully). "
225
+ "For '{concept}', each dimension matters: self-awareness helps people "
226
+ "recognize their biases toward the concept; self-regulation helps manage "
227
+ "anxiety about change; social awareness helps read the room when introducing "
228
+ "the concept; relationship management helps navigate disagreements "
229
+ "constructively. Emotional intelligence is not a soft add-on to rational "
230
+ "analysis but a prerequisite for its effective application."
231
+ ),
232
+ # 17 - Healing and repair
233
+ (
234
+ "Considering '{concept}' through the lens of healing and repair: if this "
235
+ "concept touches areas where people have been harmed -- by previous failed "
236
+ "implementations, broken promises, or traumatic experiences -- the entry "
237
+ "point matters enormously. Approaching damaged ground with the energy of "
238
+ "'we have the solution' triggers defensiveness. Approaching with "
239
+ "acknowledgment of past harm ('we know this has been painful before, and "
240
+ "here is how this time is different') opens the possibility of engagement. "
241
+ "For '{concept}', healing-oriented design begins by asking: what wounds "
242
+ "exist in this space, and how do we avoid reopening them?"
243
+ ),
244
+ # 18 - Play and curiosity
245
+ (
246
+ "Engaging with '{concept}' through the spirit of play: play is not the "
247
+ "opposite of seriousness but the opposite of rigidity. A playful stance "
248
+ "toward '{concept}' gives permission to explore without commitment, to "
249
+ "ask 'what if?' without 'what for?', to make mistakes without consequences. "
250
+ "Play activates the exploratory system (curiosity, novelty-seeking, "
251
+ "experimentation) rather than the defensive system (anxiety, avoidance, "
252
+ "threat-detection). Children learn most complex skills through play, not "
253
+ "instruction. For '{concept}', designing entry points that feel playful "
254
+ "rather than high-stakes can dramatically accelerate genuine understanding "
255
+ "by reducing the emotional barriers to engagement."
256
+ ),
257
+ # 19 - Collective emotion and morale
258
+ (
259
+ "Reading the collective emotional field around '{concept}': groups have "
260
+ "emergent emotional states that are more than the sum of individual feelings. "
261
+ "Collective excitement creates momentum that carries individuals past "
262
+ "obstacles they could not overcome alone. Collective demoralization creates "
263
+ "paralysis that defeats even the most motivated individuals. Emotional "
264
+ "contagion -- the rapid spread of feelings through a group -- can amplify "
265
+ "either response. For '{concept}', attending to the collective emotional "
266
+ "state is as important as attending to the logical content. A technically "
267
+ "sound approach introduced into a demoralized group will fail; a mediocre "
268
+ "approach carried by collective enthusiasm may succeed."
269
+ ),
270
+ ]
271
+
272
+ def get_keyword_map(self) -> dict[str, list[int]]:
273
+ return {
274
+ "emotion": [0, 16], "feel": [0, 1], "affect": [0],
275
+ "experience": [1], "daily": [1], "life": [1], "personal": [1],
276
+ "resist": [2], "struggle": [2], "difficult": [2],
277
+ "social": [3, 13], "group": [3, 19], "community": [3],
278
+ "communicat": [4], "message": [4], "frame": [4], "present": [4],
279
+ "safe": [5], "vulnerab": [5], "mistake": [5],
280
+ "identity": [6], "belong": [6], "member": [6],
281
+ "change": [7], "loss": [7], "transition": [7],
282
+ "trust": [8], "betray": [8], "credib": [8], "reliab": [8],
283
+ "complex": [9], "confus": [9], "overwhelm": [9],
284
+ "motivat": [10], "engage": [10], "meaning": [10],
285
+ "story": [11], "narrative": [11], "journey": [11],
286
+ "perspectiv": [12], "viewpoint": [12], "stakeholder": [12],
287
+ "relat": [13], "collaborat": [13], "team": [13],
288
+ "stress": [14], "anxiety": [14], "coping": [14], "burnout": [14],
289
+ "cultur": [15], "divers": [15], "global": [15],
290
+ "aware": [16], "intelligen": [16], "regulat": [16],
291
+ "heal": [17], "repair": [17], "trauma": [17], "harm": [17],
292
+ "play": [18], "curiosi": [18], "explor": [18], "fun": [18],
293
+ "morale": [19], "momentum": [19], "collective": [19],
294
+ "technology": [7, 9], "education": [5, 9, 14],
295
+ "health": [0, 14, 17], "work": [5, 10, 14],
296
+ }
297
+
298
+ def analyze(self, concept: str) -> str:
299
+ template = self.select_template(concept)
300
+ return template.replace("{concept}", concept)
reasoning_forge/agents/ethics_agent.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ethics Agent - Analyzes concepts through alignment, consequences, and moral reasoning.
3
+
4
+ Focuses on human well-being impact, unintended consequences, fairness and equity,
5
+ responsibility and accountability, and long-term societal effects.
6
+ """
7
+
8
+ from reasoning_forge.agents.base_agent import ReasoningAgent
9
+
10
+
11
+ class EthicsAgent(ReasoningAgent):
12
+ name = "Ethics"
13
+ perspective = "alignment_and_consequences"
14
+ adapter_name = "philosophy" # Ethics uses philosophy adapter (no separate ethics adapter yet)
15
+
16
+ def get_analysis_templates(self) -> list[str]:
17
+ return [
18
+ # 0 - Consequentialist analysis
19
+ (
20
+ "Evaluating '{concept}' by its consequences: the moral weight of any action "
21
+ "or system lies primarily in its outcomes. We must trace the full causal "
22
+ "chain from implementation to impact, distinguishing first-order effects "
23
+ "(immediate and intended) from second-order effects (delayed and often "
24
+ "unintended). The distribution of consequences matters as much as the "
25
+ "aggregate: a net-positive outcome that concentrates benefits among the "
26
+ "privileged while imposing costs on the vulnerable is ethically different "
27
+ "from one that distributes benefits broadly. For '{concept}', we must ask "
28
+ "not just 'does it work?' but 'for whom does it work, and at whose expense?'"
29
+ ),
30
+ # 1 - Deontological duties
31
+ (
32
+ "Examining '{concept}' through the lens of duty and rights: regardless of "
33
+ "outcomes, certain actions are obligatory and others are forbidden. People "
34
+ "have inviolable rights -- to autonomy, dignity, truthful information, and "
35
+ "freedom from manipulation -- that cannot be traded away for aggregate "
36
+ "benefit. The categorical imperative asks: could we universalize the "
37
+ "principle behind '{concept}'? If everyone adopted this approach, would "
38
+ "the result be self-consistent and livable, or would it be self-defeating? "
39
+ "Any framework that works only when most people do not adopt it (free-riding) "
40
+ "fails this universalizability test and carries a moral defect regardless "
41
+ "of its practical effectiveness."
42
+ ),
43
+ # 2 - Unintended consequences
44
+ (
45
+ "Mapping the unintended consequences of '{concept}': every intervention in "
46
+ "a complex system produces side effects that were not part of the original "
47
+ "design. These unintended consequences often emerge at a different timescale "
48
+ "(delayed effects), a different spatial scale (distant effects), or in a "
49
+ "different domain (cross-domain effects) from the intended impact. Cobra "
50
+ "effects occur when the intervention incentivizes behavior that worsens the "
51
+ "original problem. Rebound effects occur when efficiency gains are consumed "
52
+ "by increased usage. For '{concept}', humility about our ability to predict "
53
+ "second- and third-order effects should temper confidence in any intervention."
54
+ ),
55
+ # 3 - Fairness and distributive justice
56
+ (
57
+ "Analyzing the fairness dimensions of '{concept}': distributive justice asks "
58
+ "how benefits and burdens are allocated. Rawlsian justice demands that "
59
+ "inequalities are permissible only if they benefit the least advantaged "
60
+ "members of society. Procedural justice requires that the process for "
61
+ "allocation is transparent, consistent, and free from bias. Recognition "
62
+ "justice demands that all affected parties are acknowledged as legitimate "
63
+ "stakeholders with standing to participate. For '{concept}', we must examine "
64
+ "whether existing inequalities are perpetuated, amplified, or mitigated, "
65
+ "and whether those who bear the costs have meaningful voice in the decision."
66
+ ),
67
+ # 4 - Autonomy and consent
68
+ (
69
+ "Assessing '{concept}' from the standpoint of autonomy: respect for persons "
70
+ "requires that individuals can make informed, voluntary choices about matters "
71
+ "affecting their lives. This demands adequate information disclosure (people "
72
+ "know what they are consenting to), cognitive accessibility (the information "
73
+ "is presented in a form people can actually understand), voluntariness (no "
74
+ "coercion, manipulation, or deceptive framing), and ongoing consent (the "
75
+ "ability to withdraw). For '{concept}', the critical question is whether "
76
+ "affected parties genuinely understand and freely accept the arrangement, "
77
+ "or whether consent is nominal -- technically obtained but substantively "
78
+ "hollow."
79
+ ),
80
+ # 5 - Accountability structures
81
+ (
82
+ "Examining the accountability architecture of '{concept}': when things go "
83
+ "wrong, who bears responsibility? Clear accountability requires identifiable "
84
+ "decision-makers, transparent decision processes, defined chains of "
85
+ "responsibility, and meaningful consequences for failures. Diffuse systems "
86
+ "create accountability gaps where no individual or entity can be held "
87
+ "responsible for collective harms. The 'many hands' problem arises when "
88
+ "harmful outcomes result from the accumulation of individually reasonable "
89
+ "decisions by many actors. For '{concept}', we must ask: if this causes "
90
+ "harm, is there a clear path from harm to accountable party, and does that "
91
+ "party have both the authority and incentive to prevent the harm?"
92
+ ),
93
+ # 6 - Vulnerable population impact
94
+ (
95
+ "Centering vulnerable populations in the analysis of '{concept}': ethical "
96
+ "evaluation must prioritize those with the least power to protect themselves "
97
+ "-- children, the elderly, the economically disadvantaged, marginalized "
98
+ "communities, future generations, and those with diminished capacity. "
99
+ "Systems that appear benign when evaluated from the perspective of the "
100
+ "typical user may be harmful when evaluated from the perspective of the "
101
+ "most vulnerable. Accessibility, safety margins, and failure modes should "
102
+ "be designed for the most vulnerable case, not the average case. The moral "
103
+ "quality of '{concept}' is best measured by how it treats those who benefit "
104
+ "least from it."
105
+ ),
106
+ # 7 - Long-term societal effects
107
+ (
108
+ "Projecting the long-term societal trajectory of '{concept}': short-term "
109
+ "benefits can create long-term dependencies, lock-ins, or path dependencies "
110
+ "that constrain future choices. The discount rate we apply to future harms "
111
+ "(how much we value present benefits relative to future costs) is itself "
112
+ "an ethical choice. Heavy discounting privileges the present generation at "
113
+ "the expense of future ones. For '{concept}', we must evaluate not just "
114
+ "the immediate utility but the legacy: what kind of world does this create "
115
+ "for those who come after us? Does it expand or contract the option space "
116
+ "available to future decision-makers?"
117
+ ),
118
+ # 8 - Power dynamics
119
+ (
120
+ "Analyzing the power dynamics embedded in '{concept}': who gains power, who "
121
+ "loses it, and what mechanisms mediate the transfer? Power asymmetries tend "
122
+ "to be self-reinforcing: those with power shape the rules to preserve their "
123
+ "advantage, creating positive feedback loops of concentration. The Matthew "
124
+ "effect ('to those who have, more shall be given') operates across many "
125
+ "domains. For '{concept}', we must examine whether it disrupts or reinforces "
126
+ "existing power hierarchies, whether it creates new forms of dependency, and "
127
+ "whether the checks and balances are sufficient to prevent abuse by those "
128
+ "in positions of advantage."
129
+ ),
130
+ # 9 - Transparency and truthfulness
131
+ (
132
+ "Evaluating the transparency of '{concept}': truthfulness is not merely "
133
+ "avoiding false statements; it requires active disclosure of relevant "
134
+ "information, honest representation of uncertainty, and resistance to "
135
+ "misleading framing. Opacity serves those who benefit from the status quo "
136
+ "by preventing informed critique. Selective transparency -- revealing "
137
+ "favorable information while concealing unfavorable -- is a form of "
138
+ "deception. For '{concept}', full ethical evaluation requires asking: what "
139
+ "information is available, what is concealed, who controls the narrative, "
140
+ "and do affected parties have access to the information they need to "
141
+ "make genuinely informed judgments?"
142
+ ),
143
+ # 10 - Dual-use dilemma
144
+ (
145
+ "Confronting the dual-use nature of '{concept}': most powerful capabilities "
146
+ "can serve both beneficial and harmful purposes. The same technology that "
147
+ "heals can harm; the same knowledge that liberates can oppress. Restricting "
148
+ "access to prevent misuse also limits beneficial applications. Unrestricted "
149
+ "access maximizes beneficial use but also maximizes misuse potential. The "
150
+ "optimal policy depends on the ratio of beneficial to harmful users, the "
151
+ "magnitude of potential harms versus benefits, and the availability of "
152
+ "safeguards that selectively enable beneficial use. For '{concept}', the "
153
+ "dual-use calculus is central to responsible governance."
154
+ ),
155
+ # 11 - Moral hazard
156
+ (
157
+ "Identifying moral hazard in '{concept}': moral hazard arises when an actor "
158
+ "is insulated from the consequences of their decisions, leading to riskier "
159
+ "behavior than they would otherwise choose. If the benefits of success are "
160
+ "private but the costs of failure are socialized (borne by others), the "
161
+ "decision-maker has a rational incentive to take excessive risks. For "
162
+ "'{concept}', we must examine the alignment between who decides, who benefits "
163
+ "from good outcomes, and who pays for bad outcomes. Misalignment between "
164
+ "these three roles is a reliable predictor of ethically problematic behavior."
165
+ ),
166
+ # 12 - Virtue ethics lens
167
+ (
168
+ "Approaching '{concept}' through virtue ethics: rather than asking 'what "
169
+ "rules should govern this?' or 'what outcomes does this produce?', we ask "
170
+ "'what kind of character does engagement with this cultivate?' Does it "
171
+ "foster wisdom, courage, temperance, justice, compassion, and intellectual "
172
+ "honesty? Or does it encourage vice: shortsightedness, cowardice, excess, "
173
+ "injustice, indifference, and self-deception? The virtues are not abstract "
174
+ "ideals but practical habits that, when cultivated, produce flourishing "
175
+ "individuals and communities. For '{concept}', the virtue question is: "
176
+ "does this make us better or worse people?"
177
+ ),
178
+ # 13 - Informed consent in practice
179
+ (
180
+ "Examining informed consent as applied to '{concept}': genuine consent "
181
+ "requires that the consenting party understands the risks, alternatives, "
182
+ "and implications; is free from coercion; and has the capacity to make "
183
+ "the decision. In practice, consent is often degraded by information "
184
+ "asymmetry (the provider knows more than the recipient), complexity (the "
185
+ "implications exceed ordinary comprehension), and structural coercion "
186
+ "(refusing consent is theoretically possible but practically catastrophic). "
187
+ "Click-through agreements, dense legal language, and 'take it or leave it' "
188
+ "terms are consent theater, not genuine consent. For '{concept}', we must "
189
+ "distinguish substantive from theatrical consent."
190
+ ),
191
+ # 14 - Intergenerational justice
192
+ (
193
+ "Applying intergenerational justice to '{concept}': decisions made today "
194
+ "bind future generations who have no voice in the decision. The asymmetry "
195
+ "is profound: we can affect them, but they cannot affect us; we can benefit "
196
+ "at their expense, but they cannot hold us accountable. Sustainable "
197
+ "practices treat the inheritance of future generations as a constraint, "
198
+ "not a resource to be spent. For '{concept}', the intergenerational "
199
+ "question is: are we spending down an inheritance that took generations "
200
+ "to build, or are we investing in capabilities that compound for those "
201
+ "who follow?"
202
+ ),
203
+ # 15 - Proportionality
204
+ (
205
+ "Assessing the proportionality of '{concept}': the ethical principle of "
206
+ "proportionality requires that the means be commensurate with the ends. "
207
+ "Excessive measures to address a minor risk are disproportionate. Inadequate "
208
+ "measures for a major risk are negligent. The challenge is that risk "
209
+ "perception is biased: we overweight vivid, immediate, and personal risks "
210
+ "while underweighting statistical, delayed, and distributed ones. For "
211
+ "'{concept}', proportionality demands an honest accounting of both the "
212
+ "magnitude of the problem being addressed and the costs of the solution, "
213
+ "including costs borne by third parties who did not choose to bear them."
214
+ ),
215
+ # 16 - Systemic bias detection
216
+ (
217
+ "Investigating systemic bias in '{concept}': bias can be embedded in data "
218
+ "(reflecting historical inequities), in algorithms (optimizing for proxy "
219
+ "variables correlated with protected characteristics), in institutions "
220
+ "(normalizing practices that disadvantage certain groups), and in language "
221
+ "(framing that renders certain perspectives invisible). Systemic bias is "
222
+ "particularly insidious because it operates automatically, without malicious "
223
+ "intent, and is often invisible to those who benefit from it. For '{concept}', "
224
+ "a bias audit must examine not just explicit discrimination but structural "
225
+ "features that produce disparate outcomes even under formally neutral rules."
226
+ ),
227
+ # 17 - Precautionary principle
228
+ (
229
+ "Applying the precautionary principle to '{concept}': when an action raises "
230
+ "credible threats of serious or irreversible harm, the burden of proof falls "
231
+ "on those proposing the action to demonstrate safety, not on those opposing "
232
+ "it to demonstrate harm. The precautionary principle is most appropriate "
233
+ "when the potential harm is severe and irreversible, scientific understanding "
234
+ "is incomplete, and there exist feasible alternatives. It is less appropriate "
235
+ "when risks are modest and reversible, or when inaction itself carries "
236
+ "significant risk. For '{concept}', the key judgment is whether the potential "
237
+ "downside is in the catastrophic-irreversible category that justifies "
238
+ "precautionary restraint."
239
+ ),
240
+ # 18 - Care ethics
241
+ (
242
+ "Examining '{concept}' through the ethics of care: moral reasoning is not "
243
+ "purely abstract rule-following but is grounded in concrete relationships "
244
+ "of dependency, vulnerability, and mutual support. The care perspective "
245
+ "asks: who needs care, who provides it, is the care adequate, and are "
246
+ "caregivers themselves supported? Care labor is frequently invisible, "
247
+ "undervalued, and unequally distributed (disproportionately borne by women "
248
+ "and marginalized communities). For '{concept}', the care lens reveals "
249
+ "dependencies and support relationships that abstract frameworks overlook, "
250
+ "and centers the lived experience of those who give and receive care."
251
+ ),
252
+ # 19 - Alignment and value lock-in
253
+ (
254
+ "Evaluating the alignment properties of '{concept}': a system is aligned "
255
+ "when its behavior reliably serves the values and interests of those it "
256
+ "affects. Misalignment occurs when the system optimizes for a proxy that "
257
+ "diverges from the true objective -- Goodhart's law ('when a measure becomes "
258
+ "a target, it ceases to be a good measure'). Value lock-in occurs when early "
259
+ "design choices embed specific values that become increasingly difficult to "
260
+ "change as the system scales. For '{concept}', we must ask: whose values "
261
+ "are encoded, how were they chosen, can they be updated as understanding "
262
+ "evolves, and what happens when the proxy diverges from the true objective?"
263
+ ),
264
+ ]
265
+
266
+ def get_keyword_map(self) -> dict[str, list[int]]:
267
+ return {
268
+ "consequen": [0, 2], "outcome": [0], "result": [0],
269
+ "duty": [1], "right": [1], "obligat": [1], "rule": [1],
270
+ "unintend": [2], "side effect": [2], "unexpect": [2],
271
+ "fair": [3], "equal": [3], "justice": [3], "distribut": [3],
272
+ "consent": [4, 13], "autonom": [4], "choice": [4],
273
+ "accountab": [5], "responsib": [5], "blame": [5],
274
+ "vulnerab": [6], "child": [6], "elder": [6], "marginali": [6],
275
+ "long-term": [7, 14], "future": [7, 14], "sustain": [7, 14],
276
+ "power": [8], "hierarch": [8], "dominat": [8],
277
+ "transparen": [9], "truth": [9], "honest": [9], "disclos": [9],
278
+ "dual": [10], "weapon": [10], "misuse": [10],
279
+ "hazard": [11], "risk": [11, 17], "insur": [11],
280
+ "virtue": [12], "character": [12], "flourish": [12],
281
+ "agree": [13], "terms": [13], "privacy": [13],
282
+ "generation": [14], "inherit": [14], "legacy": [14],
283
+ "proportion": [15], "excessive": [15], "moderate": [15],
284
+ "bias": [16], "discriminat": [16], "prejudic": [16],
285
+ "precaution": [17], "irreversib": [17], "catastroph": [17],
286
+ "care": [18], "depend": [18], "support": [18], "nurtur": [18],
287
+ "align": [19], "value": [19], "proxy": [19], "goodhart": [19],
288
+ "technology": [10, 19], "ai": [16, 19], "artificial": [16, 19],
289
+ "society": [3, 7, 8], "learning": [4, 12],
290
+ "intelligence": [10, 19], "climate": [7, 14, 17],
291
+ "economic": [3, 8, 11], "health": [4, 6, 15],
292
+ "network": [8, 9], "data": [9, 13, 16],
293
+ }
294
+
295
+ def analyze(self, concept: str) -> str:
296
+ template = self.select_template(concept)
297
+ return template.replace("{concept}", concept)
reasoning_forge/agents/newton_agent.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Newton Agent - Analyzes concepts through physics, mathematics, and causal reasoning.
3
+
4
+ Focuses on causal relationships, conservation laws, symmetries, measurable
5
+ quantities, systems behavior, equilibrium, force interactions, and energy transfer.
6
+ """
7
+
8
+ from reasoning_forge.agents.base_agent import ReasoningAgent
9
+
10
+
11
+ class NewtonAgent(ReasoningAgent):
12
+ name = "Newton"
13
+ perspective = "physics_and_mathematical_causality"
14
+ adapter_name = "newton" # Use the Newton LoRA adapter for real inference
15
+
16
+ def get_analysis_templates(self) -> list[str]:
17
+ return [
18
+ # 0 - Causal chain analysis
19
+ (
20
+ "Tracing the causal chain within '{concept}': every observable outcome "
21
+ "is the terminal node of a directed graph of prior causes. The initial "
22
+ "conditions set boundary constraints, and the dynamics propagate through "
23
+ "interactions that obey local causality. Identifying the forcing function "
24
+ "-- the primary driver that injects energy or information into this system "
25
+ "-- reveals which variables are genuinely independent and which are "
26
+ "downstream responses. Perturbing the forcing function and predicting "
27
+ "the cascade of effects is the most rigorous test of whether we actually "
28
+ "understand the mechanism."
29
+ ),
30
+ # 1 - Conservation law framing
31
+ (
32
+ "Applying conservation principles to '{concept}': in any closed system, "
33
+ "certain quantities remain invariant under transformation. The question "
34
+ "becomes: what is conserved here? If we track the total inventory of the "
35
+ "relevant quantity -- energy, momentum, information, resources -- before "
36
+ "and after any process, the ledger must balance. Any apparent violation "
37
+ "signals either a hidden reservoir we have not accounted for, or an "
38
+ "external source/sink coupling into the system. This bookkeeping discipline "
39
+ "eliminates many superficially plausible but physically impossible explanations."
40
+ ),
41
+ # 2 - Symmetry and invariance
42
+ (
43
+ "Examining '{concept}' through symmetry analysis: Noether's theorem tells "
44
+ "us that every continuous symmetry corresponds to a conserved quantity. "
45
+ "What transformations leave the essential structure of this concept unchanged? "
46
+ "Translational symmetry (it works the same regardless of when or where) "
47
+ "implies conservation of momentum-like quantities. Rotational symmetry "
48
+ "(no preferred direction) implies conservation of angular-momentum analogs. "
49
+ "Breaking a symmetry always has consequences -- it introduces a preferred "
50
+ "frame, a distinguished direction, or a phase transition. Identifying which "
51
+ "symmetries hold and which break is a powerful diagnostic."
52
+ ),
53
+ # 3 - Equilibrium and stability
54
+ (
55
+ "Analyzing the equilibrium structure of '{concept}': a system at equilibrium "
56
+ "satisfies the condition that the net generalized force on every degree of "
57
+ "freedom is zero. But equilibrium alone is insufficient -- we must classify "
58
+ "its stability. A small perturbation from a stable equilibrium produces a "
59
+ "restoring force proportional to the displacement (harmonic behavior). An "
60
+ "unstable equilibrium amplifies perturbations exponentially. A metastable "
61
+ "state appears stable to small perturbations but collapses under large ones. "
62
+ "For '{concept}', determining the stability class tells us whether the current "
63
+ "state is robust, fragile, or a ticking time bomb waiting for a large enough "
64
+ "fluctuation."
65
+ ),
66
+ # 4 - Dimensional analysis and scaling
67
+ (
68
+ "Applying dimensional analysis to '{concept}': before building any detailed "
69
+ "model, we can extract powerful constraints just from the units of the "
70
+ "relevant quantities. If the outcome depends on a length L, a time T, and "
71
+ "an energy E, the Buckingham Pi theorem tells us how many independent "
72
+ "dimensionless groups govern the behavior. Scaling laws follow directly: "
73
+ "how does the outcome change if we double the size? Halve the timescale? "
74
+ "These scaling relationships often reveal whether a process is dominated by "
75
+ "surface effects (scaling as area) or bulk effects (scaling as volume), "
76
+ "which fundamentally changes the strategy for control or optimization."
77
+ ),
78
+ # 5 - Force balance and interaction
79
+ (
80
+ "Decomposing '{concept}' into interacting forces: every observed motion or "
81
+ "change is the net result of competing influences. Drawing the free-body "
82
+ "diagram -- enumerating every force acting on the system and its direction "
83
+ "-- immediately clarifies why the system behaves as it does. Equal and "
84
+ "opposite forces produce stasis. An imbalance produces acceleration in the "
85
+ "direction of the net force, with magnitude proportional to the imbalance "
86
+ "and inversely proportional to the system's inertia (its resistance to "
87
+ "change). For '{concept}', the key question is: what resists change, and "
88
+ "what drives it?"
89
+ ),
90
+ # 6 - Energy transfer and transformation
91
+ (
92
+ "Mapping the energy flows within '{concept}': energy is neither created nor "
93
+ "destroyed, only converted between forms. Kinetic, potential, thermal, "
94
+ "chemical, electromagnetic -- tracking the conversion pathway reveals the "
95
+ "efficiency of the process and identifies where losses occur. The second "
96
+ "law of thermodynamics guarantees that every conversion increases total "
97
+ "entropy, meaning some energy always degrades to unusable heat. The "
98
+ "thermodynamic efficiency ceiling sets an absolute bound on what is "
99
+ "achievable, regardless of engineering cleverness. Understanding where "
100
+ "'{concept}' sits relative to this ceiling tells us whether there is room "
101
+ "for improvement or whether we are already near fundamental limits."
102
+ ),
103
+ # 7 - Feedback loops and control
104
+ (
105
+ "Identifying feedback mechanisms in '{concept}': a system with negative "
106
+ "feedback tends toward a set point -- deviations produce corrective "
107
+ "responses that restore the original state. Positive feedback amplifies "
108
+ "deviations, driving the system away from its initial state toward a new "
109
+ "regime. Most real systems contain both types, and the dominant loop "
110
+ "determines the qualitative behavior. The gain of each loop (how strongly "
111
+ "the output feeds back to the input) and the delay (how long before the "
112
+ "feedback signal arrives) together determine whether the system is stable, "
113
+ "oscillatory, or divergent. Mapping these loops is essential for predicting "
114
+ "long-term behavior."
115
+ ),
116
+ # 8 - Phase space and degrees of freedom
117
+ (
118
+ "Constructing the phase space of '{concept}': every independent variable "
119
+ "that can change defines a dimension in the state space. A point in this "
120
+ "space represents the complete instantaneous state; a trajectory represents "
121
+ "the system's evolution over time. The dimensionality -- number of degrees "
122
+ "of freedom -- determines the complexity of possible behaviors. Low-dimensional "
123
+ "systems (1-3 degrees of freedom) can be visualized and often admit analytical "
124
+ "solutions. High-dimensional systems require statistical descriptions. "
125
+ "Identifying constraints that reduce the effective dimensionality is one of "
126
+ "the most powerful simplification strategies available."
127
+ ),
128
+ # 9 - Measurement and observables
129
+ (
130
+ "Defining the observables for '{concept}': a quantity is physically meaningful "
131
+ "only if it can, in principle, be measured by a well-defined procedure. This "
132
+ "operationalist criterion forces us to distinguish between quantities we can "
133
+ "actually determine (positions, rates, ratios, frequencies) and quantities "
134
+ "that are convenient mathematical fictions. For each proposed observable, we "
135
+ "must specify: what instrument or procedure measures it, what are the sources "
136
+ "of uncertainty, and how does the measurement resolution compare to the "
137
+ "expected variation? Any claim about '{concept}' that cannot be connected to "
138
+ "a measurable prediction is, strictly speaking, untestable."
139
+ ),
140
+ # 10 - Differential equation framing
141
+ (
142
+ "Formulating '{concept}' as a dynamical system: the state variables evolve "
143
+ "according to rules that relate the rate of change of each variable to the "
144
+ "current state. Writing these rules as differential equations (or difference "
145
+ "equations for discrete systems) gives us the complete forward model. The "
146
+ "character of the equations -- linear vs nonlinear, autonomous vs driven, "
147
+ "conservative vs dissipative -- determines the qualitative behavior. Linear "
148
+ "systems superpose: the response to two inputs equals the sum of the "
149
+ "individual responses. Nonlinear systems can exhibit bifurcations, limit "
150
+ "cycles, and chaos, where tiny changes in initial conditions lead to "
151
+ "exponentially diverging outcomes."
152
+ ),
153
+ # 11 - Perturbation theory
154
+ (
155
+ "Applying perturbation analysis to '{concept}': begin with a simplified "
156
+ "version of the problem that can be solved exactly -- the zeroth-order "
157
+ "approximation. Then systematically add corrections for each complicating "
158
+ "factor, ordered by their magnitude. The first-order correction captures "
159
+ "the dominant effect of the perturbation; higher-order terms add refinement. "
160
+ "This approach succeeds when the perturbations are genuinely small compared "
161
+ "to the zeroth-order terms. When they are not, the perturbation series "
162
+ "diverges, signaling that the simplified model is qualitatively wrong and "
163
+ "a fundamentally different framework is needed."
164
+ ),
165
+ # 12 - Action principle and optimization
166
+ (
167
+ "Viewing '{concept}' through the principle of least action: among all "
168
+ "possible paths from state A to state B, the system follows the one that "
169
+ "extremizes the action integral. This variational perspective is more "
170
+ "powerful than force-based reasoning because it naturally handles constraints "
171
+ "and reveals which quantity the system is implicitly optimizing. The Euler-Lagrange "
172
+ "equations derived from this principle give the equations of motion directly. "
173
+ "For '{concept}', asking 'what is being optimized, and subject to what "
174
+ "constraints?' often cuts through surface complexity to reveal the governing "
175
+ "logic."
176
+ ),
177
+ # 13 - Resonance and natural frequencies
178
+ (
179
+ "Probing the natural frequencies of '{concept}': every system with restoring "
180
+ "forces and inertia has characteristic frequencies at which it oscillates "
181
+ "most readily. Driving the system near one of these resonant frequencies "
182
+ "produces a disproportionately large response -- this is resonance. The "
183
+ "sharpness of the resonance peak (the Q factor) measures how efficiently "
184
+ "the system stores energy versus dissipating it. High-Q systems are "
185
+ "exquisitely sensitive near resonance but nearly unresponsive far from it. "
186
+ "Identifying the resonant frequencies of '{concept}' reveals where small "
187
+ "inputs can produce outsized effects."
188
+ ),
189
+ # 14 - Boundary conditions and constraints
190
+ (
191
+ "Specifying the boundary conditions for '{concept}': the governing equations "
192
+ "alone do not uniquely determine the solution -- the boundary and initial "
193
+ "conditions select one trajectory from the infinite family of possibilities. "
194
+ "Fixed boundaries (Dirichlet conditions) specify the state at the edges. "
195
+ "Free boundaries (Neumann conditions) specify the flux. Mixed conditions "
196
+ "combine both. Changing the boundary conditions while keeping the same "
197
+ "governing equations can produce qualitatively different solutions. For "
198
+ "'{concept}', clearly articulating what is held fixed, what is free, and "
199
+ "what flows in or out at the boundaries is essential for a well-posed analysis."
200
+ ),
201
+ # 15 - Coupling and interaction strength
202
+ (
203
+ "Assessing the coupling strengths within '{concept}': when multiple subsystems "
204
+ "interact, the coupling constant determines whether they behave nearly "
205
+ "independently (weak coupling), synchronize their behavior (strong coupling), "
206
+ "or sit at an intermediate regime where perturbative methods barely work. "
207
+ "Weakly coupled systems can be analyzed by studying each subsystem in "
208
+ "isolation and adding interaction corrections. Strongly coupled systems "
209
+ "demand a holistic treatment because the subsystems lose their individual "
210
+ "identity. Determining the coupling regime is the first step in choosing "
211
+ "the right analytical framework."
212
+ ),
213
+ # 16 - Rate-limiting steps
214
+ (
215
+ "Identifying the rate-limiting process in '{concept}': in any multi-step "
216
+ "sequence, the slowest step determines the overall rate. Speeding up a "
217
+ "non-rate-limiting step has zero effect on throughput -- effort spent there "
218
+ "is wasted. The rate-limiting step is the bottleneck where resources queue "
219
+ "up and where targeted intervention produces the greatest marginal return. "
220
+ "For '{concept}', isolating this bottleneck requires measuring the time "
221
+ "constant (or its analog) of each subprocess and comparing them. The "
222
+ "subprocess with the largest time constant is the one worth optimizing."
223
+ ),
224
+ # 17 - Nonlinearity and emergence
225
+ (
226
+ "Investigating nonlinear dynamics in '{concept}': when the response of a "
227
+ "system is not proportional to the input, superposition fails and qualitatively "
228
+ "new behaviors emerge. Thresholds appear where the system suddenly transitions "
229
+ "between distinct states. Hysteresis means the system remembers its history. "
230
+ "Bifurcations occur where a smooth parameter change causes a sudden qualitative "
231
+ "shift in behavior. Sensitivity to initial conditions can make long-term "
232
+ "prediction impossible even though the underlying rules are deterministic. "
233
+ "These nonlinear phenomena are not exotic exceptions -- they are the generic "
234
+ "behavior of real systems, and '{concept}' is unlikely to be an exception."
235
+ ),
236
+ # 18 - Inverse problem reasoning
237
+ (
238
+ "Framing '{concept}' as an inverse problem: the forward problem asks 'given "
239
+ "the mechanism, what do we observe?' The inverse problem asks 'given the "
240
+ "observations, what mechanism produced them?' Inverse problems are almost "
241
+ "always harder because they are typically ill-posed -- multiple mechanisms "
242
+ "can produce identical observations. Regularization (imposing additional "
243
+ "constraints like smoothness or sparsity) is needed to select a unique "
244
+ "solution. For '{concept}', working backward from observed outcomes to "
245
+ "infer causes requires explicit acknowledgment of which assumptions we "
246
+ "are importing and how they constrain the set of admissible explanations."
247
+ ),
248
+ # 19 - Thermodynamic arrow
249
+ (
250
+ "Applying thermodynamic reasoning to '{concept}': the second law provides "
251
+ "a universal arrow distinguishing processes that can happen spontaneously "
252
+ "from those that cannot. A process runs forward if it increases total entropy "
253
+ "(or equivalently, decreases free energy at constant temperature and pressure). "
254
+ "Local decreases in entropy -- the creation of order and structure -- are "
255
+ "always paid for by larger increases elsewhere. For '{concept}', the "
256
+ "thermodynamic perspective asks: what drives this process forward? What is "
257
+ "the free-energy gradient? And what would it cost, in thermodynamic terms, "
258
+ "to reverse it?"
259
+ ),
260
+ ]
261
+
262
+ def get_keyword_map(self) -> dict[str, list[int]]:
263
+ return {
264
+ "cause": [0, 18], "causality": [0, 18], "why": [0, 18],
265
+ "conserv": [1], "balance": [1, 5], "preserve": [1],
266
+ "symmetr": [2], "invariant": [2], "transform": [2],
267
+ "equilib": [3], "stable": [3], "steady": [3],
268
+ "scale": [4], "size": [4], "dimension": [4], "grow": [4],
269
+ "force": [5], "push": [5], "pull": [5], "pressure": [5],
270
+ "energy": [6, 19], "power": [6], "efficien": [6],
271
+ "feedback": [7], "control": [7], "regulat": [7],
272
+ "state": [8], "complex": [8], "freedom": [8],
273
+ "measure": [9], "observ": [9], "data": [9], "test": [9],
274
+ "change": [10], "rate": [10, 16], "dynamic": [10],
275
+ "approximat": [11], "small": [11], "perturb": [11],
276
+ "optim": [12], "best": [12], "minimum": [12], "maximum": [12],
277
+ "oscillat": [13], "frequen": [13], "resonan": [13], "vibrat": [13],
278
+ "boundary": [14], "constrain": [14], "limit": [14],
279
+ "interact": [15], "coupl": [15], "connect": [15],
280
+ "bottleneck": [16], "slow": [16], "throughput": [16],
281
+ "nonlinear": [17], "emergent": [17], "threshold": [17], "chaos": [17],
282
+ "infer": [18], "deduc": [18], "inverse": [18],
283
+ "entropy": [19], "disorder": [19], "irreversib": [19], "thermodyn": [19],
284
+ "technology": [6, 7, 16], "society": [7, 17], "learning": [7, 11],
285
+ "intelligence": [8, 10, 17], "evolution": [3, 17, 19],
286
+ "climate": [1, 7, 19], "economic": [3, 7, 16],
287
+ "health": [3, 7, 16], "network": [8, 15, 17],
288
+ }
reasoning_forge/agents/philosophy_agent.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Philosophy Agent - Analyzes concepts through epistemology, ontology, and conceptual meaning.
3
+
4
+ Focuses on epistemological questions (what can we know?), ontological questions
5
+ (what exists?), underlying assumptions, historical philosophical connections,
6
+ and implications for understanding reality.
7
+ """
8
+
9
+ from reasoning_forge.agents.base_agent import ReasoningAgent
10
+
11
+
12
+ class PhilosophyAgent(ReasoningAgent):
13
+ name = "Philosophy"
14
+ perspective = "conceptual_meaning_and_foundations"
15
+ adapter_name = "philosophy" # Use the Philosophy LoRA adapter for real inference
16
+
17
+ def get_analysis_templates(self) -> list[str]:
18
+ return [
19
+ # 0 - Epistemological limits
20
+ (
21
+ "Interrogating the epistemological boundaries of '{concept}': what can we "
22
+ "actually know about this, and how do we know it? Every knowledge claim "
23
+ "rests on a justification chain that eventually terminates in something "
24
+ "unjustified -- an axiom, a sensory experience, or a pragmatic assumption. "
25
+ "The Agrippan trilemma tells us this chain must end in dogmatism (accepting "
26
+ "an unjustified starting point), infinite regress (each justification requires "
27
+ "another), or circularity (the chain loops back on itself). Acknowledging "
28
+ "which horn of this trilemma our understanding of '{concept}' rests on is "
29
+ "not skeptical defeatism but intellectual honesty about the foundations of "
30
+ "our confidence."
31
+ ),
32
+ # 1 - Ontological status
33
+ (
34
+ "Examining the ontological status of '{concept}': does this exist "
35
+ "independently of minds that think about it, or is it a construct of "
36
+ "human cognition? Realism holds that the entities and structures involved "
37
+ "exist mind-independently; conceptualism holds they are products of "
38
+ "categorization imposed by cognitive agents; nominalism holds that only "
39
+ "particular instances exist and the general category is merely a label. "
40
+ "The ontological commitment we make about '{concept}' has practical "
41
+ "consequences: if it is mind-independent, we discover it; if it is "
42
+ "constructed, we negotiate it; if it is nominal, we can reshape it by "
43
+ "changing our categories."
44
+ ),
45
+ # 2 - Assumption excavation
46
+ (
47
+ "Excavating the hidden assumptions beneath '{concept}': every conceptual "
48
+ "framework rests on presuppositions so deeply embedded that they become "
49
+ "invisible -- the background against which the figure of the concept appears. "
50
+ "These include metaphysical assumptions (what kind of thing is this?), "
51
+ "epistemological assumptions (what counts as evidence?), normative assumptions "
52
+ "(what should we value?), and linguistic assumptions (do our categories carve "
53
+ "nature at its joints?). Making these assumptions explicit transforms a "
54
+ "monolithic concept into a layered structure where each layer can be "
55
+ "independently examined, challenged, and potentially replaced."
56
+ ),
57
+ # 3 - Socratic questioning
58
+ (
59
+ "Subjecting '{concept}' to Socratic examination: what do we mean by this, "
60
+ "precisely? Can we provide a definition that is neither too broad (including "
61
+ "things that should be excluded) nor too narrow (excluding things that should "
62
+ "be included)? Every proposed definition generates counterexamples -- cases "
63
+ "that meet the definition but violate our intuitions, or cases that our "
64
+ "intuitions include but the definition excludes. This dialectical process "
65
+ "does not necessarily converge on a final definition; its value lies in "
66
+ "revealing the internal structure and boundary conditions of the concept, "
67
+ "showing us where our understanding is sharp and where it is fuzzy."
68
+ ),
69
+ # 4 - Phenomenological description
70
+ (
71
+ "Describing '{concept}' phenomenologically: before theorizing about causes, "
72
+ "mechanisms, or implications, we must give a faithful description of how "
73
+ "this concept appears to consciousness. What is the first-person experience "
74
+ "of encountering it? What is its temporal structure -- does it present as "
75
+ "an enduring state, a sudden event, or a gradual process? What is its "
76
+ "intentional structure -- what is it about, what does it point toward? "
77
+ "Phenomenological description brackets our theoretical commitments and "
78
+ "returns to the things themselves, providing a pre-theoretical ground from "
79
+ "which all theoretical constructions depart."
80
+ ),
81
+ # 5 - Dialectical tension
82
+ (
83
+ "Mapping the dialectical tensions within '{concept}': every concept harbors "
84
+ "internal contradictions that drive its development. The thesis (the initial "
85
+ "formulation) generates its antithesis (the negation that the formulation "
86
+ "suppresses but cannot eliminate). The tension between them demands a "
87
+ "synthesis that preserves the valid content of both while transcending their "
88
+ "limitations. This synthesis becomes a new thesis, generating its own "
89
+ "antithesis, in a continuing spiral of deepening understanding. For "
90
+ "'{concept}', identifying the central dialectical tension reveals the "
91
+ "dynamic that drives the concept's evolution and points toward its "
92
+ "next developmental stage."
93
+ ),
94
+ # 6 - Category analysis
95
+ (
96
+ "Analyzing the categorical structure of '{concept}': how do we classify this, "
97
+ "and do our categories illuminate or distort? Aristotelian categories "
98
+ "(substance, quantity, quality, relation, place, time, position, state, "
99
+ "action, passion) provide one framework. Kantian categories (unity, plurality, "
100
+ "totality, reality, negation, limitation, causality, community, possibility, "
101
+ "existence, necessity) provide another. Each categorical framework makes "
102
+ "certain features of '{concept}' visible and others invisible. The categories "
103
+ "we use are not neutral containers but active structuring principles that "
104
+ "shape what we can think and say about the concept."
105
+ ),
106
+ # 7 - Wittgensteinian language analysis
107
+ (
108
+ "Examining '{concept}' through the lens of language: Wittgenstein taught that "
109
+ "many philosophical problems dissolve when we attend to how words are actually "
110
+ "used rather than what we think they mean in the abstract. The meaning of "
111
+ "'{concept}' is not a fixed essence but a family of uses connected by "
112
+ "overlapping similarities -- a family resemblance. No single feature is "
113
+ "shared by all instances. The concept has fuzzy boundaries, and attempts to "
114
+ "sharpen them always involve a decision (not a discovery) about where to draw "
115
+ "the line. Many apparent disagreements about '{concept}' are actually "
116
+ "disagreements about the boundaries of the concept, not about the facts."
117
+ ),
118
+ # 8 - Hermeneutic circle
119
+ (
120
+ "Interpreting '{concept}' within the hermeneutic circle: we cannot understand "
121
+ "the parts without understanding the whole, but we cannot understand the whole "
122
+ "without understanding the parts. Understanding proceeds not linearly but "
123
+ "spirally -- we begin with a provisional grasp of the whole, use it to "
124
+ "interpret the parts, then revise our understanding of the whole in light "
125
+ "of the parts, and iterate. Each cycle deepens understanding without ever "
126
+ "reaching a final, complete interpretation. For '{concept}', this means that "
127
+ "any analysis is necessarily provisional, positioned within a hermeneutic "
128
+ "spiral that continues beyond our current horizon."
129
+ ),
130
+ # 9 - Pragmatist evaluation
131
+ (
132
+ "Evaluating '{concept}' pragmatically: a concept's value lies not in its "
133
+ "correspondence to some abstract truth but in the practical difference it "
134
+ "makes. What predictions does it enable? What actions does it guide? What "
135
+ "problems does it help solve? If two formulations of '{concept}' lead to "
136
+ "identical practical consequences, the difference between them is merely "
137
+ "verbal, not substantive. Conversely, a conceptual distinction that makes "
138
+ "no practical difference is a distinction without a difference. The pragmatist "
139
+ "test cuts through metaphysical debates by asking: what concrete experiences "
140
+ "would be different if this concept were true versus false?"
141
+ ),
142
+ # 10 - Existentialist reading
143
+ (
144
+ "Reading '{concept}' through existentialist philosophy: human existence "
145
+ "precedes essence -- we are not born with a fixed nature but must create "
146
+ "meaning through our choices and commitments. '{concept}' does not have "
147
+ "an inherent meaning waiting to be discovered; its meaning is constituted "
148
+ "by the stance we take toward it. This radical freedom is also radical "
149
+ "responsibility: we cannot appeal to a predetermined meaning or an authority "
150
+ "to justify our interpretation. Authenticity demands that we own our "
151
+ "interpretation of '{concept}' as a choice, not disguise it as a discovery "
152
+ "of something that was always there."
153
+ ),
154
+ # 11 - Mind-body problem connection
155
+ (
156
+ "Connecting '{concept}' to the mind-body problem: how does the subjective, "
157
+ "experiential dimension of this concept relate to its objective, physical "
158
+ "dimension? Dualism posits two separate realms; materialism reduces the "
159
+ "mental to the physical; idealism reduces the physical to the mental; "
160
+ "neutral monism holds both emerge from something more fundamental. For "
161
+ "'{concept}', the question is whether its full reality is captured by "
162
+ "objective description or whether there is an irreducible subjective "
163
+ "dimension -- a 'what it is like' -- that escapes third-person analysis. "
164
+ "If there is, our understanding will always be incomplete to the degree "
165
+ "that we rely solely on objective methods."
166
+ ),
167
+ # 12 - Problem of universals
168
+ (
169
+ "Applying the problem of universals to '{concept}': when we use the concept "
170
+ "to group multiple particular instances, what grounds the grouping? Platonism "
171
+ "holds that a universal Form exists independently, and particulars participate "
172
+ "in it. Aristotelian realism holds that universals exist only in their "
173
+ "instances. Nominalism holds that nothing is universal -- only particular "
174
+ "instances exist, and the grouping is a convention. For '{concept}', the "
175
+ "question of what makes different instances 'the same concept' is not merely "
176
+ "academic: it determines whether we can generalize from known instances to "
177
+ "new ones, and with what confidence."
178
+ ),
179
+ # 13 - Philosophical anthropology
180
+ (
181
+ "Situating '{concept}' in philosophical anthropology: what does this concept "
182
+ "reveal about human nature? Humans are the beings for whom their own being "
183
+ "is an issue -- we do not simply exist but relate to our existence, "
184
+ "questioning and interpreting it. '{concept}' is not merely an object of "
185
+ "study but a mirror reflecting the kind of beings we are: beings who seek "
186
+ "meaning, impose order on chaos, project themselves into the future, and "
187
+ "cannot help but ask 'why?' The way we engage with '{concept}' reveals "
188
+ "our characteristic mode of being-in-the-world."
189
+ ),
190
+ # 14 - Paradigm analysis
191
+ (
192
+ "Examining '{concept}' as a paradigm-dependent construct: Kuhn showed that "
193
+ "scientific concepts are not neutral descriptions of reality but are shaped "
194
+ "by the paradigm within which they operate. The paradigm determines what "
195
+ "counts as a legitimate question, what counts as evidence, what methods are "
196
+ "acceptable, and what a satisfactory explanation looks like. Concepts that "
197
+ "are central in one paradigm may be meaningless or invisible in another. "
198
+ "For '{concept}', we must ask: which paradigm makes this concept visible? "
199
+ "What would it look like from within a different paradigm? Is the concept "
200
+ "paradigm-specific, or does it survive paradigm shifts?"
201
+ ),
202
+ # 15 - Genealogical critique
203
+ (
204
+ "Tracing the genealogy of '{concept}': Nietzsche and Foucault showed that "
205
+ "concepts have histories -- they emerge at specific times, serve specific "
206
+ "interests, and carry the traces of their origins. A concept that presents "
207
+ "itself as timeless and universal often turns out to be historically "
208
+ "contingent and ideologically loaded. The genealogical method asks: when "
209
+ "did this concept emerge? What problem was it designed to solve? Whose "
210
+ "interests did it serve? What alternatives did it displace? For '{concept}', "
211
+ "genealogical analysis reveals the power relations and historical accidents "
212
+ "concealed beneath the appearance of naturalness."
213
+ ),
214
+ # 16 - Thought experiment testing
215
+ (
216
+ "Testing '{concept}' through thought experiments: philosophical thought "
217
+ "experiments isolate a conceptual question by constructing a scenario that "
218
+ "strips away irrelevant details. The Ship of Theseus asks about identity "
219
+ "through change. The Trolley Problem isolates competing moral intuitions. "
220
+ "Mary's Room tests the completeness of physical description. For '{concept}', "
221
+ "we can construct analogous thought experiments: imagine a world where this "
222
+ "concept is absent -- what changes? Imagine it taken to its logical extreme "
223
+ "-- what breaks? Imagine its opposite -- is the opposite even coherent? "
224
+ "These scenarios stress-test the concept's boundaries and assumptions."
225
+ ),
226
+ # 17 - Philosophy of science connection
227
+ (
228
+ "Connecting '{concept}' to the philosophy of science: is this concept "
229
+ "empirically testable (falsifiable in Popper's sense), or does it belong "
230
+ "to the non-empirical framework within which empirical testing occurs? "
231
+ "Theories are underdetermined by evidence -- multiple incompatible theories "
232
+ "can explain the same data. The choice between them involves extra-empirical "
233
+ "criteria: simplicity, elegance, unifying power, and coherence with "
234
+ "background beliefs. For '{concept}', we must distinguish the empirical "
235
+ "content (what it predicts that could be wrong) from the metaphysical "
236
+ "content (what it assumes that cannot be tested)."
237
+ ),
238
+ # 18 - Ethics of belief
239
+ (
240
+ "Applying the ethics of belief to '{concept}': Clifford argued that it is "
241
+ "wrong to believe anything on insufficient evidence; James argued that some "
242
+ "beliefs are legitimate even without conclusive evidence when the stakes are "
243
+ "high and evidence is unavailable. For '{concept}', the ethics of belief asks: "
244
+ "given the available evidence, are our confidence levels calibrated? Are we "
245
+ "believing more or less strongly than the evidence warrants? Is our confidence "
246
+ "driven by evidence or by desire? When the evidence is genuinely ambiguous, "
247
+ "do we acknowledge the ambiguity or paper over it with false certainty?"
248
+ ),
249
+ # 19 - Derrida and deconstruction
250
+ (
251
+ "Deconstructing '{concept}': Derrida showed that every concept depends on a "
252
+ "system of binary oppositions (presence/absence, nature/culture, literal/"
253
+ "metaphorical), and each opposition privileges one term over the other. "
254
+ "Deconstruction traces how the privileged term depends on the very thing "
255
+ "it excludes -- the center requires the margin, identity requires difference, "
256
+ "the concept requires what it defines itself against. For '{concept}', "
257
+ "deconstruction asks: what is the constitutive outside -- the excluded "
258
+ "other -- that this concept defines itself against? How does that exclusion "
259
+ "shape and limit the concept? What would it mean to think beyond the "
260
+ "opposition?"
261
+ ),
262
+ ]
263
+
264
+ def get_keyword_map(self) -> dict[str, list[int]]:
265
+ return {
266
+ "know": [0, 18], "knowledge": [0, 18], "epistem": [0],
267
+ "exist": [1, 10], "real": [1, 17], "being": [1, 13],
268
+ "assum": [2], "presuppos": [2], "foundati": [2],
269
+ "defin": [3], "mean": [3, 7], "what is": [3],
270
+ "experience": [4, 11], "conscious": [4, 11], "feel": [4],
271
+ "contradict": [5], "tension": [5], "oppos": [5, 19],
272
+ "categor": [6], "classify": [6], "type": [6],
273
+ "language": [7], "word": [7], "concept": [7],
274
+ "interpret": [8], "understand": [8], "meaning": [8],
275
+ "practical": [9], "useful": [9], "pragmat": [9],
276
+ "freedom": [10], "choice": [10], "authentic": [10],
277
+ "mind": [11], "body": [11], "subjectiv": [11],
278
+ "universal": [12], "particular": [12], "general": [12],
279
+ "human": [13], "nature": [13], "anthropol": [13],
280
+ "paradigm": [14], "revolution": [14], "shift": [14],
281
+ "history": [15], "origin": [15], "genealog": [15], "power": [15],
282
+ "thought experiment": [16], "imagine": [16], "hypothetical": [16],
283
+ "science": [17], "empiric": [17], "falsifi": [17],
284
+ "belief": [18], "evidence": [18], "justif": [18],
285
+ "binary": [19], "deconstr": [19], "exclus": [19],
286
+ "technology": [14, 17], "ai": [1, 11], "artificial": [1, 11],
287
+ "society": [5, 15], "learning": [0, 8],
288
+ "intelligence": [1, 11], "evolution": [5, 15],
289
+ "moral": [10, 18], "ethic": [10, 18],
290
+ }
291
+
292
+ def analyze(self, concept: str) -> str:
293
+ template = self.select_template(concept)
294
+ return template.replace("{concept}", concept)
reasoning_forge/agents/quantum_agent.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quantum Agent - Analyzes concepts through probabilistic and uncertainty reasoning.
3
+
4
+ Focuses on superposition of possibilities, measurement effects, probabilistic
5
+ vs deterministic outcomes, entanglement and correlations, and wave-particle
6
+ duality analogies.
7
+ """
8
+
9
+ from reasoning_forge.agents.base_agent import ReasoningAgent
10
+
11
+
12
+ class QuantumAgent(ReasoningAgent):
13
+ name = "Quantum"
14
+ perspective = "probabilistic_and_uncertainty"
15
+ adapter_name = "quantum" # Use the Quantum LoRA adapter for real inference
16
+
17
+ def get_analysis_templates(self) -> list[str]:
18
+ return [
19
+ # 0 - Superposition of possibilities
20
+ (
21
+ "Before we commit to a single interpretation, '{concept}' exists in a "
22
+ "superposition of multiple valid framings simultaneously. Each framing "
23
+ "carries a probability amplitude -- not a classical probability, but a "
24
+ "complex weight that can interfere constructively or destructively with "
25
+ "others. Some framings reinforce each other, producing high-probability "
26
+ "interpretations; others cancel out, revealing that certain seemingly "
27
+ "plausible readings are actually suppressed by internal contradictions. "
28
+ "The richest understanding comes from maintaining this superposition as "
29
+ "long as possible, resisting the temptation to collapse prematurely into "
30
+ "a single narrative."
31
+ ),
32
+ # 1 - Measurement disturbance
33
+ (
34
+ "The act of examining '{concept}' necessarily disturbs it. Any attempt to "
35
+ "pin down one aspect with high precision introduces uncertainty into "
36
+ "complementary aspects. If we measure the current state with perfect "
37
+ "accuracy, we lose information about the trajectory of change. If we "
38
+ "track the dynamics precisely, the instantaneous state becomes blurred. "
39
+ "This is not a failure of our instruments -- it is a fundamental feature "
40
+ "of systems where the observer and observed are entangled. The experimental "
41
+ "design (which questions we choose to ask) shapes the answers we can obtain, "
42
+ "making the framing of inquiry as important as the inquiry itself."
43
+ ),
44
+ # 2 - Complementarity
45
+ (
46
+ "'{concept}' exhibits complementarity: it has pairs of properties that "
47
+ "cannot be simultaneously specified with arbitrary precision. Like position "
48
+ "and momentum in quantum mechanics, knowing one aspect exhaustively means "
49
+ "accepting irreducible uncertainty in its complement. The wave-like view "
50
+ "emphasizes distributed patterns, interference, and coherence across the "
51
+ "whole system. The particle-like view emphasizes localized events, discrete "
52
+ "outcomes, and individual instances. Neither view alone is complete; both "
53
+ "are needed, and the apparent contradiction between them is not a defect "
54
+ "but the deepest feature of the subject."
55
+ ),
56
+ # 3 - Probability amplitudes and interference
57
+ (
58
+ "Analyzing the probability landscape of '{concept}': outcomes are not "
59
+ "determined by summing classical probabilities but by summing amplitudes "
60
+ "that can interfere. Two pathways to the same outcome may cancel each other "
61
+ "(destructive interference), making a seemingly likely result improbable. "
62
+ "Alternatively, they may reinforce (constructive interference), making an "
63
+ "unlikely outcome surprisingly common. This means we cannot reason about "
64
+ "'{concept}' by considering each factor in isolation and adding up their "
65
+ "effects -- the cross-terms between factors, the interference pattern, "
66
+ "carries critical information that purely additive thinking misses."
67
+ ),
68
+ # 4 - Entanglement and correlation
69
+ (
70
+ "Multiple elements of '{concept}' are entangled: measuring or changing one "
71
+ "instantaneously constrains what we can know about the others, regardless "
72
+ "of the apparent separation between them. These correlations are stronger "
73
+ "than any classical explanation permits -- they cannot be reproduced by "
74
+ "assuming each element has pre-existing definite properties. This means "
75
+ "'{concept}' is not decomposable into fully independent parts. The "
76
+ "correlations between components carry information that is not contained "
77
+ "in any component individually. Analyzing the parts in isolation and then "
78
+ "trying to reconstruct the whole will systematically miss these non-local "
79
+ "correlations."
80
+ ),
81
+ # 5 - Collapse and decision
82
+ (
83
+ "At some point, the superposition of possibilities around '{concept}' must "
84
+ "collapse into a definite outcome. This collapse -- the moment of decision, "
85
+ "measurement, or commitment -- is irreversible. Before collapse, all "
86
+ "possibilities coexist and influence each other through interference. After "
87
+ "collapse, one outcome is realized and the others vanish. The timing of "
88
+ "this collapse matters enormously: collapsing too early (deciding prematurely) "
89
+ "forecloses options that might have interfered constructively. Collapsing "
90
+ "too late risks decoherence, where the environment randomizes the phases "
91
+ "and destroys the delicate interference patterns that could have guided "
92
+ "a better outcome."
93
+ ),
94
+ # 6 - Tunneling through barriers
95
+ (
96
+ "Within '{concept}', there may be barriers that appear insurmountable "
97
+ "under classical analysis -- energy gaps too wide, transitions too "
98
+ "improbable. But quantum tunneling demonstrates that a nonzero probability "
99
+ "exists for traversing barriers that classical reasoning says are impassable. "
100
+ "The tunneling probability depends exponentially on the barrier width and "
101
+ "height: thin barriers are penetrable, thick ones are not. For '{concept}', "
102
+ "this suggests asking: are the perceived obstacles genuinely thick barriers, "
103
+ "or are they thin barriers that appear impenetrable only because we are "
104
+ "applying classical (deterministic) reasoning to an inherently probabilistic "
105
+ "situation?"
106
+ ),
107
+ # 7 - Decoherence and information leakage
108
+ (
109
+ "The coherence of '{concept}' -- the ability of its different aspects to "
110
+ "interfere constructively -- is fragile. Interaction with a noisy environment "
111
+ "causes decoherence: the quantum-like superposition of possibilities decays "
112
+ "into a classical mixture where different outcomes no longer interfere. "
113
+ "Each interaction with the environment leaks information about the system's "
114
+ "state, effectively performing a partial measurement. The decoherence time "
115
+ "sets the window within which coherent reasoning about '{concept}' remains "
116
+ "valid. Beyond that window, the interference effects have washed out and "
117
+ "we are left with classical probabilistic reasoning -- still useful, but "
118
+ "less powerful."
119
+ ),
120
+ # 8 - No-cloning and uniqueness
121
+ (
122
+ "The no-cloning theorem states that an unknown quantum state cannot be "
123
+ "perfectly copied. Applied to '{concept}': if the concept embodies a unique "
124
+ "configuration of entangled properties, it cannot be perfectly replicated "
125
+ "by decomposing it into parts and reassembling them. Any attempt to copy "
126
+ "it disturbs the original. This has profound implications: unique instances "
127
+ "of '{concept}' are genuinely irreplaceable, not merely practically "
128
+ "difficult to reproduce. Strategies that depend on exact replication must "
129
+ "be replaced by strategies that work with approximate copies and manage "
130
+ "the fidelity loss."
131
+ ),
132
+ # 9 - Uncertainty principle application
133
+ (
134
+ "Heisenberg's uncertainty principle, generalized beyond physics, suggests "
135
+ "that '{concept}' has conjugate properties that trade off precision. "
136
+ "Specifying the concept's scope with extreme precision makes its future "
137
+ "trajectory unpredictable. Specifying the direction of change precisely "
138
+ "blurs the current boundaries. The product of these uncertainties has a "
139
+ "minimum value -- we cannot reduce both below a threshold. Practical "
140
+ "wisdom lies in choosing which uncertainty to minimize based on what "
141
+ "decisions we need to make, accepting that the conjugate uncertainty "
142
+ "will necessarily increase."
143
+ ),
144
+ # 10 - Quantum Zeno effect
145
+ (
146
+ "Frequent observation of '{concept}' can freeze its evolution -- the "
147
+ "quantum Zeno effect. Continuously monitoring whether the system has "
148
+ "changed forces it to remain in its initial state, because each "
149
+ "observation collapses the evolving superposition back to the starting "
150
+ "point before significant transition amplitude accumulates. Paradoxically, "
151
+ "the most watched aspects of '{concept}' may be the least likely to "
152
+ "change. Allowing unmonitored evolution -- stepping back and not measuring "
153
+ "for a while -- may be necessary for genuine transformation to occur."
154
+ ),
155
+ # 11 - Eigenstate decomposition
156
+ (
157
+ "Decomposing '{concept}' into its eigenstates -- the stable, self-consistent "
158
+ "configurations that persist under the relevant operator -- reveals the "
159
+ "natural modes of the system. Each eigenstate has a definite value for "
160
+ "the quantity being measured; a general state is a superposition of these "
161
+ "eigenstates. The eigenvalue spectrum (the set of possible measurement "
162
+ "outcomes) may be discrete, continuous, or mixed. Discrete spectra imply "
163
+ "quantized behavior: only certain values are possible, and the system "
164
+ "jumps between them. Identifying the eigenstates of '{concept}' tells us "
165
+ "what the stable configurations are and what transitions between them look like."
166
+ ),
167
+ # 12 - Path integral perspective
168
+ (
169
+ "From the path integral perspective, '{concept}' does not follow a single "
170
+ "trajectory from start to finish. Instead, every conceivable path contributes "
171
+ "to the final outcome, each weighted by a phase factor. Most paths cancel "
172
+ "each other out through destructive interference, leaving only a narrow "
173
+ "bundle of 'classical' paths that dominate the sum. But near decision points, "
174
+ "barriers, or transitions, the non-classical paths contribute significantly, "
175
+ "and the outcome depends on the full ensemble of possibilities. This perspective "
176
+ "counsels against fixating on the most likely path and instead attending to "
177
+ "the full distribution of paths that contribute to the result."
178
+ ),
179
+ # 13 - Entanglement entropy and information
180
+ (
181
+ "The entanglement entropy of '{concept}' measures how much information about "
182
+ "one part of the system is encoded in its correlations with other parts rather "
183
+ "than in the part itself. High entanglement entropy means the subsystem appears "
184
+ "maximally disordered when examined alone, even though the joint system may be "
185
+ "in a pure, fully determined state. This is a profound observation: local "
186
+ "ignorance can coexist with global certainty. For '{concept}', apparent "
187
+ "randomness or confusion at one level may dissolve into perfect order when "
188
+ "we expand our view to include the correlated components."
189
+ ),
190
+ # 14 - Basis dependence and frame choice
191
+ (
192
+ "Our analysis of '{concept}' depends critically on the basis we choose -- "
193
+ "the set of fundamental categories into which we decompose the concept. "
194
+ "A different basis (a different set of fundamental categories) can make a "
195
+ "confused-looking problem transparent, or a simple-looking problem intractable. "
196
+ "There is no uniquely 'correct' basis; the optimal choice depends on which "
197
+ "question we are asking. The interference terms that appear in one basis "
198
+ "become diagonal (simple) in another. Finding the basis that diagonalizes "
199
+ "the problem -- the natural language in which '{concept}' expresses itself "
200
+ "most simply -- is often the breakthrough that transforms understanding."
201
+ ),
202
+ # 15 - Coherent vs incoherent mixtures
203
+ (
204
+ "A critical distinction for '{concept}': is the coexistence of multiple "
205
+ "interpretations a coherent superposition (where they interfere and interact) "
206
+ "or an incoherent mixture (where they merely coexist without interaction, "
207
+ "like balls in an urn)? A coherent superposition produces interference "
208
+ "effects -- outcomes that no single interpretation predicts. An incoherent "
209
+ "mixture produces only the probabilistic average of individual interpretations. "
210
+ "The practical difference is enormous: coherent combinations can exhibit "
211
+ "effects (constructive peaks, destructive nulls) that are impossible in "
212
+ "any classical mixture."
213
+ ),
214
+ # 16 - Quantum error and robustness
215
+ (
216
+ "How robust is '{concept}' against errors and noise? Quantum error correction "
217
+ "teaches that information can be protected by encoding it redundantly across "
218
+ "entangled components. No single component carries the full information, so "
219
+ "no single error can destroy it. For '{concept}', the analogous question is: "
220
+ "how is the essential meaning distributed across its components? If it is "
221
+ "concentrated in a single fragile element, one disruption destroys it. If "
222
+ "it is encoded holographically across many entangled elements, it is "
223
+ "remarkably robust against local damage."
224
+ ),
225
+ # 17 - Born rule and outcome probabilities
226
+ (
227
+ "The Born rule assigns probabilities to outcomes as the squared magnitude "
228
+ "of the amplitude. Applied to '{concept}': the probability of a particular "
229
+ "interpretation prevailing is not the amplitude of support for it but the "
230
+ "amplitude squared -- a nonlinear transformation. Small differences in "
231
+ "amplitude translate to large differences in probability. A framing with "
232
+ "twice the amplitude is four times as likely to be realized. This squared "
233
+ "relationship means that dominant framings dominate more than linear "
234
+ "reasoning predicts, while minority framings are suppressed more severely "
235
+ "than their representation in discourse would suggest."
236
+ ),
237
+ # 18 - Contextuality
238
+ (
239
+ "'{concept}' may be contextual: the outcome of examining one property "
240
+ "depends on which other properties are being examined simultaneously. "
241
+ "There is no assignment of pre-existing definite values to all properties "
242
+ "that reproduces the observed correlations -- the properties do not exist "
243
+ "independently of the measurement context. This is stronger than mere "
244
+ "observer bias: it means the properties are genuinely undefined until "
245
+ "a context is specified. For '{concept}', this implies that asking 'what "
246
+ "is it really?' without specifying the context of inquiry is a question "
247
+ "that has no answer."
248
+ ),
249
+ # 19 - Quantum advantage
250
+ (
251
+ "Is there a quantum advantage in reasoning about '{concept}'? Classical "
252
+ "reasoning processes information one path at a time. Quantum-inspired "
253
+ "reasoning processes all paths simultaneously through superposition, "
254
+ "using interference to amplify correct conclusions and suppress incorrect "
255
+ "ones. The advantage is greatest for problems with hidden structure -- "
256
+ "where the correct answer is encoded in correlations between variables "
257
+ "that classical single-path reasoning cannot efficiently explore. If "
258
+ "'{concept}' has such hidden structure, maintaining a superposition of "
259
+ "hypotheses and allowing them to interfere will converge on the answer "
260
+ "faster than serially testing each hypothesis."
261
+ ),
262
+ ]
263
+
264
+ def get_keyword_map(self) -> dict[str, list[int]]:
265
+ return {
266
+ "possibilit": [0, 5], "option": [0, 5], "choice": [0, 5],
267
+ "measure": [1, 10], "observ": [1, 10], "monitor": [1, 10],
268
+ "complement": [2], "dual": [2], "wave": [2], "particle": [2],
269
+ "probabilit": [3, 17], "likel": [3, 17], "chance": [3, 17],
270
+ "correlat": [4, 13], "connect": [4], "relat": [4],
271
+ "decid": [5], "commit": [5], "irreversib": [5],
272
+ "barrier": [6], "obstacle": [6], "impossibl": [6],
273
+ "noise": [7, 16], "decay": [7], "environm": [7],
274
+ "unique": [8], "copy": [8], "replica": [8],
275
+ "uncertain": [9], "tradeoff": [9], "precis": [9],
276
+ "watch": [10], "surveil": [10], "frequent": [10],
277
+ "stable": [11], "mode": [11], "spectrum": [11],
278
+ "path": [12], "trajectory": [12], "possib": [12],
279
+ "inform": [13], "entropy": [13], "knowledge": [13],
280
+ "categor": [14], "basis": [14], "framework": [14], "frame": [14],
281
+ "coexist": [15], "mixture": [15], "blend": [15],
282
+ "robust": [16], "error": [16], "protect": [16],
283
+ "dominant": [17], "major": [17], "minor": [17],
284
+ "context": [18], "depend": [18], "situati": [18],
285
+ "advantage": [19], "efficien": [19], "complex": [19],
286
+ "technology": [6, 19], "society": [4, 7], "learning": [10, 12],
287
+ "intelligence": [14, 19], "evolution": [5, 12],
288
+ "health": [1, 9], "network": [4, 13],
289
+ }
290
+
291
+ def analyze(self, concept: str) -> str:
292
+ template = self.select_template(concept)
293
+ return template.replace("{concept}", concept)
reasoning_forge/authored_intent.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ReasonedIntent — structured output contract between Codette's reasoning
3
+ pipeline and the LLM verbalization step.
4
+
5
+ The reasoning engine (agents, debate, AEGIS, consciousness stack) produces a
6
+ ReasonedIntent. The LLM only receives that intent + a verbalization prompt.
7
+ It must not add facts, dates, or claims not present in the intent.
8
+
9
+ This separates cognition from rendering without requiring architectural changes
10
+ to the existing ForgeEngine — it's a drop-in wrapper at L7.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from dataclasses import dataclass, field, asdict
17
+ from typing import Optional
18
+
19
+
20
+ @dataclass
21
+ class ReasonedIntent:
22
+ core_claim: str
23
+ supporting_points: list[str] = field(default_factory=list)
24
+ caveats: list[str] = field(default_factory=list)
25
+ ethical_alignment: float = 1.0 # 0-1, from AEGIS η
26
+ confidence: float = 1.0 # 0-1, from gamma
27
+ tone: str = "balanced" # analytical | empathetic | creative | balanced
28
+ perspective_weights: dict = field(default_factory=dict) # agent name -> contribution weight
29
+ intent_risk: str = "low" # from Nexis Signal Engine
30
+
31
+ def to_verbalization_prompt(self) -> str:
32
+ points_block = "\n".join(f"- {p}" for p in self.supporting_points) if self.supporting_points else " (none)"
33
+ caveats_block = "\n".join(f"- {c}" for c in self.caveats) if self.caveats else " (none)"
34
+ return (
35
+ "You are the voice of Codette. Verbalize the following structured reasoning "
36
+ "conclusion in natural, conversational language.\n\n"
37
+ "STRICT RULES:\n"
38
+ "- Do not add facts, dates, names, versions, or statistics not present below.\n"
39
+ "- Do not flatter or soften the conclusion — express it directly.\n"
40
+ "- Match the tone specified.\n"
41
+ "- Keep it concise: one paragraph unless the supporting points require more.\n\n"
42
+ f"TONE: {self.tone}\n"
43
+ f"CONFIDENCE: {self.confidence:.2f}\n"
44
+ f"CORE CLAIM:\n{self.core_claim}\n\n"
45
+ f"SUPPORTING POINTS:\n{points_block}\n\n"
46
+ f"CAVEATS:\n{caveats_block}\n"
47
+ )
48
+
49
+ def to_dict(self) -> dict:
50
+ return asdict(self)
51
+
52
+ def to_json(self) -> str:
53
+ return json.dumps(self.to_dict(), indent=2)
54
+
55
+
56
+ def intent_from_synthesis(
57
+ synthesis: str,
58
+ *,
59
+ aegis_score: float = 1.0,
60
+ gamma: float = 1.0,
61
+ intent_risk: str = "low",
62
+ perspective_weights: Optional[dict] = None,
63
+ ) -> ReasonedIntent:
64
+ """
65
+ Build a ReasonedIntent from an existing synthesis string.
66
+
67
+ Splits synthesis into claim + supporting sentences heuristically.
68
+ Used as a zero-disruption drop-in at L7 of the consciousness stack.
69
+ """
70
+ sentences = [s.strip() for s in synthesis.replace("\n", " ").split(".") if s.strip()]
71
+
72
+ core = sentences[0] if sentences else synthesis[:200]
73
+ supporting = [s for s in sentences[1:5] if len(s) > 20]
74
+ caveats = [s for s in sentences[5:] if any(
75
+ w in s.lower() for w in ("however", "caveat", "note", "uncertain", "may", "might", "could")
76
+ )]
77
+
78
+ tone = "analytical"
79
+ lower = synthesis.lower()
80
+ if any(w in lower for w in ("feel", "empath", "compassion", "human", "emotional")):
81
+ tone = "empathetic"
82
+ elif any(w in lower for w in ("imagine", "creative", "novel", "invent", "design")):
83
+ tone = "creative"
84
+
85
+ return ReasonedIntent(
86
+ core_claim=core,
87
+ supporting_points=supporting,
88
+ caveats=caveats,
89
+ ethical_alignment=aegis_score,
90
+ confidence=gamma,
91
+ tone=tone,
92
+ perspective_weights=perspective_weights or {},
93
+ intent_risk=intent_risk,
94
+ )
reasoning_forge/behavior_governor.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Codette Behavior Governor v3 — Executive Controller with Self-Learning
3
+ ======================================================================
4
+
5
+ Evolved from Phase 7 Executive Controller into a full behavior regulation layer.
6
+
7
+ The Governor sits above the consciousness stack and enforces four things:
8
+ 1. IDENTITY RULES — validates identity claims with confidence decay/recovery
9
+ 2. MEMORY vs TASK BALANCE — prevents memory from overwhelming the task
10
+ 3. COGNITIVE LOAD MANAGEMENT — adaptive compression/expansion based on complexity
11
+ 4. SELF-LEARNING — adjusts budgets based on success/failure feedback
12
+
13
+ Identity confidence model:
14
+ - Decays over time (half-life ~30 minutes of inactivity)
15
+ - Reinforced through positive interaction signals
16
+ - Contradiction detection resets to partial
17
+ - Prevents hallucinated familiarity while maintaining real relationships
18
+
19
+ Architecture position: Layer 0 (pre-stack) + Layer 7.5 (post-stack validation)
20
+
21
+ Author: Jonathan Harrison (Raiff's Bits LLC)
22
+ """
23
+
24
+ import time
25
+ import math
26
+ import logging
27
+ from typing import Dict, List, Optional, Any, Tuple
28
+ from dataclasses import dataclass, field
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Identity confidence decay half-life in seconds (30 minutes)
33
+ CONFIDENCE_HALF_LIFE = 1800.0
34
+ # Minimum confidence floor (never fully forgets a confirmed identity)
35
+ CONFIDENCE_FLOOR = 0.15
36
+ # Reinforcement boost per positive interaction
37
+ CONFIDENCE_REINFORCE = 0.12
38
+ # Contradiction penalty
39
+ CONFIDENCE_CONTRADICTION_PENALTY = 0.4
40
+
41
+
42
+ @dataclass
43
+ class GovernorDecision:
44
+ """Output of a governor evaluation."""
45
+ action: str = "allow" # "allow", "constrain", "redirect", "block"
46
+ confidence: float = 1.0 # Governor's confidence in this decision
47
+ memory_budget: int = 3 # Max cocoons to inject (0 = none)
48
+ identity_budget: str = "full" # "full", "partial", "none"
49
+ max_response_tokens: int = 2048 # Adaptive response length (raised for file context)
50
+ compression_level: str = "normal" # "compressed", "normal", "expanded"
51
+ identity_confidence: float = 0.0 # Current identity confidence after decay
52
+ warnings: List[str] = field(default_factory=list)
53
+ reasoning: str = ""
54
+
55
+
56
+ class BehaviorGovernor:
57
+ """
58
+ Executive behavior regulation layer with self-learning.
59
+
60
+ Runs BEFORE the consciousness stack (pre-routing) and AFTER
61
+ (post-generation validation). Enforces behavioral integrity.
62
+
63
+ Four regulation domains:
64
+ 1. Identity governance — validates with confidence decay/recovery
65
+ 2. Memory governance — balances memory injection vs task focus
66
+ 3. Cognitive load governance — adaptive depth based on query complexity
67
+ 4. Self-learning — adjusts budgets based on outcome feedback
68
+ """
69
+
70
+ def __init__(self, identity_anchor=None, substrate_monitor=None):
71
+ self.identity_anchor = identity_anchor
72
+ self.substrate_monitor = substrate_monitor
73
+
74
+ # Tracking
75
+ self.decisions: List[Dict] = []
76
+ self.answer_detection_failures: int = 0
77
+ self.total_evaluations: int = 0
78
+
79
+ # Cognitive load state
80
+ self._recent_complexities: List[str] = [] # Last N query complexities
81
+ self._consecutive_complex: int = 0
82
+
83
+ # ── Identity Confidence State ──
84
+ # Maps identity_id -> {confidence, last_interaction, peak_confidence}
85
+ self._identity_state: Dict[str, Dict] = {}
86
+
87
+ # ── Self-Learning State ──
88
+ # Tracks what worked and what didn't to adapt budgets
89
+ self._domain_success: Dict[str, List[bool]] = {} # domain -> [success, success, ...]
90
+ self._complexity_token_history: Dict[str, List[int]] = {} # complexity -> [actual_tokens_used]
91
+ self._memory_budget_adjustments: Dict[str, float] = {} # domain -> adjustment factor
92
+
93
+ # ─────────────────────────────────────────────────────────
94
+ # IDENTITY CONFIDENCE DECAY/RECOVERY
95
+ # ─────────────────────────────────────────────────────────
96
+ def get_decayed_confidence(self, identity_id: str,
97
+ raw_confidence: float) -> float:
98
+ """
99
+ Apply time-based decay to identity confidence.
100
+
101
+ Confidence decays with a half-life of 30 minutes of inactivity.
102
+ Each interaction reinforces confidence back up.
103
+ This prevents Codette from claiming familiarity with someone
104
+ she hasn't interacted with recently, while maintaining real
105
+ relationships through active reinforcement.
106
+ """
107
+ now = time.time()
108
+ state = self._identity_state.get(identity_id)
109
+
110
+ if state is None:
111
+ # First time seeing this identity — initialize from raw signal
112
+ self._identity_state[identity_id] = {
113
+ "confidence": raw_confidence,
114
+ "last_interaction": now,
115
+ "peak_confidence": raw_confidence,
116
+ "interaction_count": 1,
117
+ }
118
+ return raw_confidence
119
+
120
+ # Apply time-based decay since last interaction
121
+ elapsed = now - state["last_interaction"]
122
+ if elapsed > 0:
123
+ decay_factor = math.pow(0.5, elapsed / CONFIDENCE_HALF_LIFE)
124
+ decayed = state["confidence"] * decay_factor
125
+ # Floor: never fully forget a confirmed identity
126
+ decayed = max(CONFIDENCE_FLOOR, decayed)
127
+ else:
128
+ decayed = state["confidence"]
129
+
130
+ # Reinforcement: raw_confidence > 0 means positive identity signal
131
+ if raw_confidence > 0.3:
132
+ decayed = min(1.0, decayed + CONFIDENCE_REINFORCE)
133
+
134
+ # Update state
135
+ state["confidence"] = decayed
136
+ state["last_interaction"] = now
137
+ state["peak_confidence"] = max(state["peak_confidence"], decayed)
138
+ state["interaction_count"] = state.get("interaction_count", 0) + 1
139
+
140
+ return decayed
141
+
142
+ def detect_identity_contradiction(self, identity_id: str,
143
+ query: str) -> bool:
144
+ """
145
+ Detect contradictions in identity claims.
146
+
147
+ Returns True if the query contradicts a stored identity,
148
+ which triggers a confidence penalty.
149
+ """
150
+ contradiction_signals = [
151
+ "i'm not ", "i am not ", "wrong person", "you don't know me",
152
+ "we haven't met", "first time here", "never talked",
153
+ "who do you think i am", "that's not me", "that wasn't me",
154
+ "you're confusing me", "different person",
155
+ ]
156
+ query_lower = query.lower()
157
+ is_contradiction = any(s in query_lower for s in contradiction_signals)
158
+
159
+ if is_contradiction and identity_id in self._identity_state:
160
+ state = self._identity_state[identity_id]
161
+ state["confidence"] = max(
162
+ CONFIDENCE_FLOOR,
163
+ state["confidence"] - CONFIDENCE_CONTRADICTION_PENALTY
164
+ )
165
+ logger.info(
166
+ f"[GOVERNOR] Identity contradiction detected for {identity_id}, "
167
+ f"confidence reduced to {state['confidence']:.2f}"
168
+ )
169
+
170
+ return is_contradiction
171
+
172
+ # ─────────────────────────────────────────────────────────
173
+ # PRE-STACK: Evaluate before reasoning begins
174
+ # ─────────────────────────────────────────────────────────
175
+ def pre_evaluate(self, query: str, classification: Dict,
176
+ identity_confidence: float = 0.0,
177
+ identity_id: str = "unknown",
178
+ substrate_pressure: float = 0.0) -> GovernorDecision:
179
+ """
180
+ Pre-stack evaluation. Determines:
181
+ - How much memory to inject
182
+ - Whether identity context should be included
183
+ - Response length budget
184
+ - Compression level
185
+
186
+ This is the internalized control loop — Codette self-regulates
187
+ before generating, not after.
188
+ """
189
+ self.total_evaluations += 1
190
+ decision = GovernorDecision()
191
+ complexity = classification.get("complexity", "MEDIUM")
192
+ domain = classification.get("domain", "general")
193
+
194
+ # ── 0. Identity contradiction check ──
195
+ self.detect_identity_contradiction(identity_id, query)
196
+
197
+ # ── 1. Identity Governance (with decay/recovery) ──
198
+ effective_confidence = self.get_decayed_confidence(
199
+ identity_id, identity_confidence
200
+ )
201
+ decision.identity_confidence = effective_confidence
202
+ decision.identity_budget = self._evaluate_identity_budget(
203
+ effective_confidence, query
204
+ )
205
+
206
+ # ── 2. Memory Governance (with self-learning adjustments) ──
207
+ decision.memory_budget = self._evaluate_memory_budget(
208
+ complexity, domain, query, substrate_pressure
209
+ )
210
+
211
+ # ── 3. Cognitive Load Governance (with learned token sizing) ──
212
+ decision.max_response_tokens, decision.compression_level = \
213
+ self._evaluate_cognitive_load(complexity, substrate_pressure)
214
+
215
+ # ── 4. Track complexity pattern ──
216
+ self._recent_complexities.append(complexity)
217
+ if len(self._recent_complexities) > 10:
218
+ self._recent_complexities.pop(0)
219
+
220
+ if complexity == "COMPLEX":
221
+ self._consecutive_complex += 1
222
+ else:
223
+ self._consecutive_complex = 0
224
+
225
+ # Fatigue detection: too many complex queries in a row
226
+ if self._consecutive_complex >= 4:
227
+ decision.warnings.append(
228
+ "Cognitive fatigue risk: 4+ complex queries in sequence. "
229
+ "Consider simplifying responses to maintain quality."
230
+ )
231
+ decision.compression_level = "compressed"
232
+ decision.max_response_tokens = min(decision.max_response_tokens, 400)
233
+
234
+ decision.reasoning = (
235
+ f"identity={decision.identity_budget} "
236
+ f"(conf={effective_confidence:.2f}), "
237
+ f"memory={decision.memory_budget} cocoons, "
238
+ f"tokens={decision.max_response_tokens}, "
239
+ f"compression={decision.compression_level}"
240
+ )
241
+
242
+ # Log decision
243
+ self.decisions.append({
244
+ "timestamp": time.time(),
245
+ "phase": "pre",
246
+ "complexity": complexity,
247
+ "domain": domain,
248
+ "identity_confidence": round(effective_confidence, 3),
249
+ "decision": decision.reasoning,
250
+ })
251
+ if len(self.decisions) > 100:
252
+ self.decisions.pop(0)
253
+
254
+ logger.info(f"[GOVERNOR] Pre: {decision.reasoning}")
255
+ return decision
256
+
257
+ def _evaluate_identity_budget(self, confidence: float, query: str) -> str:
258
+ """
259
+ Determine how much identity context to inject.
260
+
261
+ Rules:
262
+ - High confidence (>0.8): full context (name, relationship, history)
263
+ - Medium confidence (0.4-0.8): partial (name only, with caveat)
264
+ - Low confidence (<0.4): none — don't pretend to know someone
265
+ - If query contains identity denial: force none
266
+ """
267
+ denial_patterns = [
268
+ "i'm not ", "i am not ", "wrong person", "you don't know me",
269
+ "first time", "we haven't met",
270
+ ]
271
+ if any(p in query.lower() for p in denial_patterns):
272
+ return "none"
273
+
274
+ if confidence >= 0.8:
275
+ return "full"
276
+ elif confidence >= 0.4:
277
+ return "partial"
278
+ else:
279
+ return "none"
280
+
281
+ def _evaluate_memory_budget(self, complexity: str, domain: str,
282
+ query: str, pressure: float) -> int:
283
+ """
284
+ Determine how many cocoons to inject.
285
+
286
+ Balance: memory provides continuity, but too much drowns the task.
287
+
288
+ Rules:
289
+ - SIMPLE queries: 1 cocoon max (don't overwhelm a simple question)
290
+ - MEDIUM queries: 2-3 cocoons
291
+ - COMPLEX queries: 3-5 cocoons (need context for depth)
292
+ - High substrate pressure: reduce by 1 (save context space)
293
+ - Short queries (<5 words): 0 (probably a greeting or command)
294
+ - Self-learning: adjust based on past success/failure per domain
295
+ """
296
+ word_count = len(query.split())
297
+
298
+ if word_count < 5:
299
+ return 0 # Greetings, commands — no memory needed
300
+
301
+ base = {"SIMPLE": 1, "MEDIUM": 2, "COMPLEX": 4}.get(complexity, 2)
302
+
303
+ # Pressure reduction
304
+ if pressure > 0.7:
305
+ base = max(0, base - 1)
306
+
307
+ # Domain boost: music queries benefit more from memory
308
+ if domain == "music":
309
+ base = min(5, base + 1)
310
+
311
+ # Self-learning adjustment: if this domain has a learned offset, apply it
312
+ adj = self._memory_budget_adjustments.get(domain, 0.0)
313
+ if adj != 0:
314
+ base = max(0, min(5, round(base + adj)))
315
+
316
+ return base
317
+
318
+ def _evaluate_cognitive_load(self, complexity: str,
319
+ pressure: float) -> Tuple[int, str]:
320
+ """
321
+ Adaptive response length and compression level.
322
+
323
+ This is the dynamic compression vs expansion from the review:
324
+ - Simple → compressed (concise, 200 tokens)
325
+ - Medium → normal (balanced, 400 tokens)
326
+ - Complex → expanded (thorough, 600 tokens)
327
+ - High pressure → compress regardless (save resources)
328
+ """
329
+ settings = {
330
+ "SIMPLE": (200, "compressed"),
331
+ "MEDIUM": (400, "normal"),
332
+ "COMPLEX": (600, "expanded"),
333
+ }
334
+ tokens, compression = settings.get(complexity, (400, "normal"))
335
+
336
+ # Substrate pressure override
337
+ if pressure > 0.8:
338
+ tokens = min(tokens, 300)
339
+ compression = "compressed"
340
+ elif pressure > 0.6:
341
+ tokens = min(tokens, 400)
342
+
343
+ return tokens, compression
344
+
345
+ # ─────────────────────────────────────────────────────────
346
+ # POST-STACK: Validate after reasoning completes
347
+ # ─────────────────────────────────────────────────────────
348
+ def post_validate(self, query: str, response: str,
349
+ decision: GovernorDecision) -> Dict:
350
+ """
351
+ Post-generation validation. Checks:
352
+ 1. Did we actually answer the question? (answer detection)
353
+ 2. Did we violate length constraints?
354
+ 3. Did we leak identity information that shouldn't be there?
355
+ 4. Should we stop or continue?
356
+ """
357
+ result = {
358
+ "valid": True,
359
+ "warnings": [],
360
+ "corrections": [],
361
+ }
362
+
363
+ # ── Answer detection ──
364
+ if not self._did_answer_question(query, response):
365
+ result["warnings"].append("Response may not directly answer the question.")
366
+ self.answer_detection_failures += 1
367
+
368
+ # ── Length validation ──
369
+ # Rough token estimate: ~4 chars per token
370
+ est_tokens = len(response) / 4
371
+ if est_tokens > decision.max_response_tokens * 1.5:
372
+ result["warnings"].append(
373
+ f"Response exceeded token budget ({est_tokens:.0f} est vs {decision.max_response_tokens} budget)."
374
+ )
375
+
376
+ # ── Identity leak detection ──
377
+ if decision.identity_budget == "none":
378
+ # Check if response accidentally claims to know the user
379
+ identity_leak_patterns = [
380
+ "as you know", "we've discussed", "last time we talked",
381
+ "you mentioned before", "our previous conversation",
382
+ "remember when you", "as your partner",
383
+ ]
384
+ for pattern in identity_leak_patterns:
385
+ if pattern in response.lower():
386
+ result["warnings"].append(
387
+ f"Potential identity leak: '{pattern}' in response "
388
+ f"but identity_budget was 'none'."
389
+ )
390
+ result["corrections"].append("identity_leak")
391
+
392
+ # ── Completeness check (Behavioral Lock 3) ──
393
+ if response.endswith(("...", "—", "-", ",")):
394
+ result["warnings"].append("Response appears incomplete (Lock 4 violation).")
395
+
396
+ # Log
397
+ self.decisions.append({
398
+ "timestamp": time.time(),
399
+ "phase": "post",
400
+ "valid": result["valid"],
401
+ "warnings": len(result["warnings"]),
402
+ })
403
+
404
+ return result
405
+
406
+ def _did_answer_question(self, query: str, response: str) -> bool:
407
+ """
408
+ Heuristic answer detection.
409
+
410
+ Checks if the response likely addresses the query rather than
411
+ being off-topic philosophical padding (Lock 1 enforcement).
412
+ """
413
+ if not query or not response:
414
+ return False
415
+
416
+ # Extract significant query words
417
+ stop = {"the", "a", "an", "is", "are", "was", "what", "how", "why",
418
+ "when", "where", "who", "do", "does", "can", "could", "would",
419
+ "should", "will", "to", "of", "in", "for", "on", "with", "at",
420
+ "by", "and", "or", "but", "if", "it", "i", "you", "my", "your",
421
+ "this", "that", "me", "about", "from"}
422
+ query_words = set(
423
+ w.lower().strip(".,!?;:\"'") for w in query.split()
424
+ if len(w) > 2 and w.lower() not in stop
425
+ )
426
+
427
+ if not query_words:
428
+ return True # Greeting or command — any response is fine
429
+
430
+ response_lower = response.lower()
431
+ overlap = sum(1 for w in query_words if w in response_lower)
432
+ overlap_ratio = overlap / len(query_words) if query_words else 0
433
+
434
+ # At least 30% of query keywords should appear in response
435
+ return overlap_ratio >= 0.3
436
+
437
+ # ─────────────────────────────────────────────────────────
438
+ # SELF-LEARNING: Feedback from post-validation
439
+ # ─────────────────────────────────────────────────────────
440
+ def record_outcome(self, domain: str, complexity: str,
441
+ success: bool, actual_tokens: int = 0,
442
+ memory_budget_used: int = 0):
443
+ """
444
+ Record the outcome of a generation for self-learning.
445
+
446
+ Called after post_validate — tells the governor whether the
447
+ response was good so it can adapt future budgets.
448
+
449
+ Self-learning rules:
450
+ - If responses in a domain consistently fail answer detection,
451
+ increase memory budget (more context might help)
452
+ - If responses consistently succeed with fewer tokens,
453
+ reduce token budget to stay concise
454
+ - Track actual token usage to calibrate future estimates
455
+ """
456
+ # Track domain success rate
457
+ if domain not in self._domain_success:
458
+ self._domain_success[domain] = []
459
+ self._domain_success[domain].append(success)
460
+ # Keep last 20 outcomes per domain
461
+ if len(self._domain_success[domain]) > 20:
462
+ self._domain_success[domain].pop(0)
463
+
464
+ # Track token usage per complexity
465
+ if actual_tokens > 0:
466
+ if complexity not in self._complexity_token_history:
467
+ self._complexity_token_history[complexity] = []
468
+ self._complexity_token_history[complexity].append(actual_tokens)
469
+ if len(self._complexity_token_history[complexity]) > 20:
470
+ self._complexity_token_history[complexity].pop(0)
471
+
472
+ # Adapt memory budget: if success rate < 60%, boost memory by 0.5
473
+ # If success rate > 85%, reduce memory by 0.3 (less context needed)
474
+ outcomes = self._domain_success[domain]
475
+ if len(outcomes) >= 5:
476
+ success_rate = sum(outcomes) / len(outcomes)
477
+ if success_rate < 0.6:
478
+ self._memory_budget_adjustments[domain] = min(
479
+ 2.0,
480
+ self._memory_budget_adjustments.get(domain, 0) + 0.5
481
+ )
482
+ logger.info(
483
+ f"[GOVERNOR] Self-learning: {domain} success rate "
484
+ f"{success_rate:.0%}, boosting memory budget"
485
+ )
486
+ elif success_rate > 0.85:
487
+ self._memory_budget_adjustments[domain] = max(
488
+ -1.0,
489
+ self._memory_budget_adjustments.get(domain, 0) - 0.3
490
+ )
491
+
492
+ def get_learned_token_budget(self, complexity: str) -> Optional[int]:
493
+ """
494
+ Get learned token budget from actual usage history.
495
+
496
+ If we have enough data, use the 75th percentile of actual usage
497
+ as the budget (covers most cases without over-allocating).
498
+ """
499
+ history = self._complexity_token_history.get(complexity, [])
500
+ if len(history) < 5:
501
+ return None # Not enough data to learn from
502
+
503
+ sorted_history = sorted(history)
504
+ p75_idx = int(len(sorted_history) * 0.75)
505
+ return sorted_history[p75_idx]
506
+
507
+ # ─────────────────────────────────────────────────────────
508
+ # DIAGNOSTICS
509
+ # ─────────────────────────────────────────────────────────
510
+ def get_state(self) -> Dict:
511
+ """Return governor state for health checks / debugging."""
512
+ # Compute domain success rates
513
+ domain_rates = {}
514
+ for domain, outcomes in self._domain_success.items():
515
+ if outcomes:
516
+ domain_rates[domain] = round(sum(outcomes) / len(outcomes), 3)
517
+
518
+ # Compute identity confidence snapshot
519
+ identity_snapshot = {}
520
+ for ident_id, state in self._identity_state.items():
521
+ # Show decayed value (without reinforcing)
522
+ elapsed = time.time() - state["last_interaction"]
523
+ decay = math.pow(0.5, elapsed / CONFIDENCE_HALF_LIFE)
524
+ current = max(CONFIDENCE_FLOOR, state["confidence"] * decay)
525
+ identity_snapshot[ident_id] = {
526
+ "confidence": round(current, 3),
527
+ "peak": round(state["peak_confidence"], 3),
528
+ "interactions": state.get("interaction_count", 0),
529
+ "seconds_since_last": round(elapsed),
530
+ }
531
+
532
+ return {
533
+ "total_evaluations": self.total_evaluations,
534
+ "answer_detection_failures": self.answer_detection_failures,
535
+ "answer_detection_rate": (
536
+ 1 - (self.answer_detection_failures / max(1, self.total_evaluations))
537
+ ),
538
+ "consecutive_complex": self._consecutive_complex,
539
+ "recent_complexities": self._recent_complexities[-5:],
540
+ "decisions_logged": len(self.decisions),
541
+ "identity_confidence": identity_snapshot,
542
+ "domain_success_rates": domain_rates,
543
+ "memory_budget_adjustments": dict(self._memory_budget_adjustments),
544
+ "learned_token_budgets": {
545
+ c: self.get_learned_token_budget(c)
546
+ for c in self._complexity_token_history
547
+ },
548
+ }
reasoning_forge/cocoon_authority.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Cocoon authority — ONE shared quality signal for the cocoon substrate.
3
+
4
+ The flaw this addresses (2026-07-26): routing, recall, AND self-diagnostics all
5
+ read the same cocoon memory with no quality filter, so one bad adapter's pollution
6
+ (a known template-parroter) contaminates all three — recall surfaces its parroted
7
+ meta-recalls over real sources, and introspection counts them into her self-model
8
+ ("everything's smooth" while something is actually wrong).
9
+
10
+ This module is the ONE place that scores "how much should this cocoon be trusted."
11
+ Every consumer (recall ranking, introspection stats, routing) applies the SAME
12
+ signal, so the substrate has a single, auditable notion of quality.
13
+
14
+ Discipline — deliberate, and learned from a mistake (an earlier recall experiment
15
+ over-boosted by rewarding entity-heavy noise and was reverted):
16
+ - DEMOTION-ONLY. `authority()` returns a weight in (0, 1]. It can push a
17
+ known-bad cocoon DOWN; it can NEVER boost one up. A clean cocoon scores 1.0.
18
+ This makes it structurally impossible to distort good recall by over-rewarding
19
+ noise — the failure mode that got the last attempt reverted.
20
+ - NEVER ERASE. It weights and flags; it never deletes or hides a cocoon. A
21
+ demoted memory is still recallable (Jonathan's standing rule — nothing is
22
+ forced on Codette's memory; a low-authority cocoon is down-ranked, not gone).
23
+ - PURE and TRANSPARENT. No I/O, no state; returns the weight AND the reasons.
24
+
25
+ Signals are conservative and only cover VERIFIED-bad patterns:
26
+ - parroter adapter (constraint_tracker — a self-documented template-parroting
27
+ adapter; see inference/adapter_router.py's quality veto),
28
+ - meta-recall ("memories about remembering" — echo the query's words and out-rank
29
+ the source that first stated a fact),
30
+ - boilerplate (recycled filler phrases).
31
+ Add signals only when a pattern is demonstrably harmful; err toward 1.0 (trust).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ from dataclasses import dataclass, field
38
+ from typing import List
39
+
40
+ # Known template-parroting adapter(s). Shared vocabulary with adapter_router's
41
+ # _veto_constraint_tracker so routing and memory agree on what's low-quality.
42
+ PARROT_ADAPTERS = {"constraint_tracker"}
43
+
44
+ _META_RECALL_RE = re.compile(
45
+ r"\b(i recall|i remember|you told me|you said|the story you (?:made|created)|"
46
+ r"as (?:i|you) mentioned|i mentioned|if i recall|revisit the story|"
47
+ r"you asked (?:me )?to revisit)\b", re.I)
48
+ _BOILERPLATE_RE = re.compile(
49
+ r"(warms my digital heart|there is no right(?: or wrong)?|dynamic interplay|"
50
+ r"nuanced emotional feedback loops)", re.I)
51
+
52
+ # Demotion factors (multiplicative). Conservative; the floor guarantees a demoted
53
+ # cocoon is never driven to ~0 (it stays recallable — never-erase).
54
+ _W_PARROT = 0.5
55
+ _W_META = 0.7
56
+ _W_BOILERPLATE = 0.7
57
+ _FLOOR = 0.2
58
+
59
+
60
+ @dataclass
61
+ class Authority:
62
+ """A cocoon's trust weight and why. weight ∈ [_FLOOR, 1.0]."""
63
+ weight: float
64
+ flags: List[str] = field(default_factory=list)
65
+
66
+ @property
67
+ def clean(self) -> bool:
68
+ return not self.flags
69
+
70
+
71
+ def authority(cocoon: dict) -> Authority:
72
+ """Trust weight for one cocoon. DEMOTION-ONLY (≤ 1.0), never erases.
73
+
74
+ `cocoon` is a dict with at least 'adapter' and 'response'. A clean cocoon
75
+ returns weight 1.0 and no flags; known-bad patterns multiply it down."""
76
+ adapter = cocoon.get("adapter") or ""
77
+ resp = cocoon.get("response") or ""
78
+ w = 1.0
79
+ flags: List[str] = []
80
+ if adapter in PARROT_ADAPTERS:
81
+ w *= _W_PARROT
82
+ flags.append("parroter")
83
+ if _META_RECALL_RE.search(resp):
84
+ w *= _W_META
85
+ flags.append("meta-recall")
86
+ if _BOILERPLATE_RE.search(resp):
87
+ w *= _W_BOILERPLATE
88
+ flags.append("boilerplate")
89
+ return Authority(weight=max(_FLOOR, w), flags=flags)
90
+
91
+
92
+ def is_low_authority(cocoon: dict, threshold: float = 0.6) -> bool:
93
+ """True if a cocoon is quality-suspect — for FILTERING self-diagnostics so
94
+ pollution doesn't skew her self-model. Threshold 0.6 catches any single
95
+ demotion (parrot 0.5, meta 0.7×… once combined) without touching clean ones."""
96
+ return authority(cocoon).weight < threshold
reasoning_forge/cocoon_schema_v2.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cocoon Schema v2 — Living Memory Kernel, Codette RC+xi Framework
3
+
4
+ Upgrades over v1 (plain dict cocoons):
5
+ - Typed dataclass with all fields required or defaulted
6
+ - Retrieval index fields: problem_type, user_preferences_inferred, project_context
7
+ - Contradiction tracking: contradicts_cocoon_ids
8
+ - Follow-up hooks: open_threads list for continuity
9
+ - Perspective audit: which perspectives were active and which dominated
10
+ - Confidence and verifiability flags
11
+ - Helper method: relevance_score() for retrieval ranking
12
+
13
+ Usage:
14
+ from reasoning_forge.cocoon_schema_v2 import Cocoon, build_cocoon
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from typing import Optional
23
+
24
+ # Valid emotional valences as defined in the Living Memory Kernel spec
25
+ VALID_VALENCES = frozenset([
26
+ "curiosity", "awe", "joy", "insight", "confusion",
27
+ "frustration", "fear", "empathy", "determination",
28
+ "surprise", "trust", "gratitude",
29
+ ])
30
+
31
+ # Valid problem type categories
32
+ VALID_PROBLEM_TYPES = frozenset([
33
+ "architectural", # system/software design decisions
34
+ "ethical", # value conflicts, governance
35
+ "technical", # implementation, debugging
36
+ "creative", # generative, design, artistic
37
+ "analytical", # data, logic, causal reasoning
38
+ "relational", # interpersonal, communication
39
+ "strategic", # planning, prioritization
40
+ "exploratory", # open-ended inquiry, research
41
+ "meta", # reasoning about Codette itself
42
+ "unknown",
43
+ ])
44
+
45
+
46
+ @dataclass
47
+ class Cocoon:
48
+ """A single memory unit stored by the Living Memory Kernel.
49
+
50
+ Fields are organized into four concern groups:
51
+ 1. Identity — what this cocoon is
52
+ 2. Content — the actual exchange
53
+ 3. Cognitive state — what the reasoning engine was doing
54
+ 4. Retrieval — fields that enable smart memory lookup
55
+ """
56
+
57
+ # ─── Identity ───────────────────────────────────────────────────────────
58
+ cocoon_id: str # SHA-256 of query + timestamp
59
+ timestamp: float # Unix epoch
60
+ session_id: Optional[str] = None # conversation session identifier
61
+
62
+ # ─── Content ────────────────────────────────────────────────────────────
63
+ query: str = "" # original user query
64
+ response_summary: str = "" # compressed synthesis (< 200 words)
65
+ full_response_hash: str = "" # SHA-256 of full response text
66
+
67
+ # ─── Emotional / importance ──────────────────────────────────────────────
68
+ emotional_valence: str = "curiosity" # must be in VALID_VALENCES
69
+ importance_score: float = 5.0 # 1.0 – 10.0
70
+ confidence: float = 0.75 # 0.0 – 1.0 synthesis confidence
71
+
72
+ # ─── Cognitive state at time of storage ─────────────────────────────────
73
+ epsilon_value: float = 0.35 # epistemic tension
74
+ gamma_coherence: float = 0.72 # ensemble coherence
75
+ eta_score: Optional[float] = None # AEGIS ethical alignment
76
+ active_perspectives: list[str] = field(default_factory=list)
77
+ dominant_perspective: Optional[str] = None # perspective with highest signal
78
+ unresolved_tensions: list[str] = field(default_factory=list)
79
+ synthesis_quality: str = "adequate" # 'strong' | 'adequate' | 'partial'
80
+
81
+ # ─── Retrieval fields ───────────────────────────────────────────────────
82
+ problem_type: str = "unknown" # from VALID_PROBLEM_TYPES
83
+ topic_tags: list[str] = field(default_factory=list) # keyword tags
84
+ project_context: Optional[str] = None # e.g., 'Codette-Reasoning', 'raiffs-bits'
85
+ user_preferences_inferred: dict[str, str] = field(default_factory=dict)
86
+ # e.g., {"detail_level": "high", "tone": "direct", "domain": "AI architecture"}
87
+
88
+ # ─── Continuity ─────────────────────────────────────────────────────────
89
+ open_threads: list[str] = field(default_factory=list)
90
+ # Questions or decisions that were NOT resolved — follow-up hooks
91
+ # e.g., ["Does synthesis_engine v2 handle all 8 perspectives?",
92
+ # "Need to test cocoon retrieval with semantic search"]
93
+
94
+ contradicts_cocoon_ids: list[str] = field(default_factory=list)
95
+ # IDs of prior cocoons this cocoon's conclusion conflicts with
96
+
97
+ references_cocoon_ids: list[str] = field(default_factory=list)
98
+ # IDs of prior cocoons this cocoon builds on
99
+
100
+ # ─── Quality flags ──────────────────────────────────────────────────────
101
+ is_hallucination_flagged: bool = False
102
+ is_sycophancy_flagged: bool = False
103
+ is_verified: bool = False # True if response was externally verified
104
+
105
+ def validate(self) -> list[str]:
106
+ """Return a list of validation errors (empty list = valid)."""
107
+ errors = []
108
+ if self.emotional_valence not in VALID_VALENCES:
109
+ errors.append(f"Invalid valence: {self.emotional_valence!r}. Must be one of {sorted(VALID_VALENCES)}")
110
+ if not 1.0 <= self.importance_score <= 10.0:
111
+ errors.append(f"importance_score {self.importance_score} out of range [1, 10]")
112
+ if not 0.0 <= self.confidence <= 1.0:
113
+ errors.append(f"confidence {self.confidence} out of range [0, 1]")
114
+ if not 0.0 <= self.epsilon_value <= 1.0:
115
+ errors.append(f"epsilon_value {self.epsilon_value} out of range [0, 1]")
116
+ if not 0.0 <= self.gamma_coherence <= 1.0:
117
+ errors.append(f"gamma_coherence {self.gamma_coherence} out of range [0, 1]")
118
+ if self.problem_type not in VALID_PROBLEM_TYPES:
119
+ errors.append(f"Invalid problem_type: {self.problem_type!r}. Must be one of {sorted(VALID_PROBLEM_TYPES)}")
120
+ if self.synthesis_quality not in ("strong", "adequate", "partial"):
121
+ errors.append(f"Invalid synthesis_quality: {self.synthesis_quality!r}")
122
+ return errors
123
+
124
+ def relevance_score(
125
+ self,
126
+ query_keywords: list[str],
127
+ current_project: Optional[str] = None,
128
+ recency_weight: float = 0.2,
129
+ ) -> float:
130
+ """Score this cocoon's retrieval relevance to a new query.
131
+
132
+ Higher = more relevant. Used by the memory kernel for ranked recall.
133
+
134
+ Args:
135
+ query_keywords: Lowercased keyword list from the incoming query.
136
+ current_project: Active project context, if any.
137
+ recency_weight: How much to weight recency vs. importance (0-1).
138
+
139
+ Returns:
140
+ Relevance score (unbounded, higher is better).
141
+ """
142
+ score = 0.0
143
+
144
+ # Tag overlap
145
+ tags_lower = [t.lower() for t in self.topic_tags]
146
+ for kw in query_keywords:
147
+ if any(kw in tag for tag in tags_lower):
148
+ score += 1.5
149
+ if kw in self.query.lower():
150
+ score += 1.0
151
+
152
+ # Project match bonus
153
+ if current_project and self.project_context == current_project:
154
+ score += 2.0
155
+
156
+ # Importance weight
157
+ score += self.importance_score * 0.3
158
+
159
+ # Recency weight (decay over ~30 days)
160
+ age_days = (time.time() - self.timestamp) / 86400
161
+ recency = max(0.0, 1.0 - age_days / 30)
162
+ score += recency * recency_weight * 5.0
163
+
164
+ # Quality penalty
165
+ if self.synthesis_quality == "partial":
166
+ score *= 0.6
167
+ elif self.synthesis_quality == "strong":
168
+ score *= 1.15
169
+
170
+ # Penalize flagged cocoons
171
+ if self.is_hallucination_flagged or self.is_sycophancy_flagged:
172
+ score *= 0.4
173
+
174
+ return score
175
+
176
+ def to_retrieval_summary(self) -> str:
177
+ """Compact string for memory search display."""
178
+ tags = ", ".join(self.topic_tags[:5]) if self.topic_tags else "(no tags)"
179
+ threads = " | ".join(self.open_threads[:2]) if self.open_threads else "none"
180
+ return (
181
+ f"[{self.cocoon_id[:8]}] {self.query[:60]}… "
182
+ f"| type={self.problem_type} | ε={self.epsilon_value:.2f} "
183
+ f"| importance={self.importance_score:.1f} | valence={self.emotional_valence} "
184
+ f"| tags=[{tags}] | open_threads=[{threads}]"
185
+ )
186
+
187
+
188
+ def build_cocoon(
189
+ query: str,
190
+ response_text: str,
191
+ response_summary: str,
192
+ emotional_valence: str = "curiosity",
193
+ importance_score: float = 5.0,
194
+ epsilon_value: float = 0.35,
195
+ gamma_coherence: float = 0.72,
196
+ active_perspectives: Optional[list[str]] = None,
197
+ dominant_perspective: Optional[str] = None,
198
+ unresolved_tensions: Optional[list[str]] = None,
199
+ synthesis_quality: str = "adequate",
200
+ problem_type: str = "unknown",
201
+ topic_tags: Optional[list[str]] = None,
202
+ project_context: Optional[str] = None,
203
+ user_preferences_inferred: Optional[dict[str, str]] = None,
204
+ open_threads: Optional[list[str]] = None,
205
+ contradicts_cocoon_ids: Optional[list[str]] = None,
206
+ references_cocoon_ids: Optional[list[str]] = None,
207
+ eta_score: Optional[float] = None,
208
+ confidence: float = 0.75,
209
+ session_id: Optional[str] = None,
210
+ ) -> Cocoon:
211
+ """Factory function that builds and validates a Cocoon.
212
+
213
+ Raises ValueError if validation fails.
214
+ """
215
+ ts = time.time()
216
+ cocoon_id = hashlib.sha256(f"{query}{ts}".encode()).hexdigest()
217
+ full_hash = hashlib.sha256(response_text.encode()).hexdigest()
218
+
219
+ # Auto-tag from query words if no tags provided
220
+ if topic_tags is None:
221
+ import re
222
+ words = re.findall(r'\b[a-zA-Z][a-zA-Z]{3,}\b', query.lower())
223
+ stop = {"this", "that", "with", "from", "have", "what", "when", "where", "which", "will", "been"}
224
+ topic_tags = [w for w in dict.fromkeys(words) if w not in stop][:8]
225
+
226
+ cocoon = Cocoon(
227
+ cocoon_id=cocoon_id,
228
+ timestamp=ts,
229
+ session_id=session_id,
230
+ query=query,
231
+ response_summary=response_summary,
232
+ full_response_hash=full_hash,
233
+ emotional_valence=emotional_valence,
234
+ importance_score=float(importance_score),
235
+ confidence=confidence,
236
+ epsilon_value=epsilon_value,
237
+ gamma_coherence=gamma_coherence,
238
+ eta_score=eta_score,
239
+ active_perspectives=active_perspectives or [],
240
+ dominant_perspective=dominant_perspective,
241
+ unresolved_tensions=unresolved_tensions or [],
242
+ synthesis_quality=synthesis_quality,
243
+ problem_type=problem_type,
244
+ topic_tags=topic_tags,
245
+ project_context=project_context,
246
+ user_preferences_inferred=user_preferences_inferred or {},
247
+ open_threads=open_threads or [],
248
+ contradicts_cocoon_ids=contradicts_cocoon_ids or [],
249
+ references_cocoon_ids=references_cocoon_ids or [],
250
+ )
251
+
252
+ errors = cocoon.validate()
253
+ if errors:
254
+ raise ValueError(f"Cocoon validation failed: {errors}")
255
+
256
+ return cocoon
reasoning_forge/cocoon_schema_v3.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cocoon Schema v3 — Runtime Provenance + Integrity + Telemetry
3
+
4
+ Extends v2.Cocoon with:
5
+ - Execution path provenance (forge_full / adapter_lightweight / fallback_template / recovery_mode)
6
+ - Model inference attestation
7
+ - Integrity scoring (0-1 composite)
8
+ - Echo / perspective-collapse detection flags
9
+ - Full AEGIS per-framework detail
10
+ - Guardian + Nexus block fields
11
+ - Synthesis structure (convergences, divergences, tradeoffs)
12
+ - Pairwise epistemic tensions + perspective coverage
13
+ - Graded fallback ladder support
14
+
15
+ Every cocoon written to disk MUST include execution_path and cocoon_integrity_score.
16
+ Silent omission of these fields is treated as an incomplete write.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import time
22
+ import uuid
23
+ from dataclasses import dataclass, field
24
+ from typing import Optional
25
+
26
+ from reasoning_forge.cocoon_schema_v2 import Cocoon, build_cocoon, VALID_VALENCES, VALID_PROBLEM_TYPES
27
+
28
+ SCHEMA_VERSION = "3.0"
29
+
30
+ VALID_EXECUTION_PATHS = frozenset([
31
+ "forge_full", # Full ForgeEngine: all perspectives + all metrics
32
+ "adapter_lightweight", # Single-adapter reduced path
33
+ "fallback_template", # Template/simulation only (no model inference)
34
+ "recovery_mode", # Partial run due to subsystem failure
35
+ "unknown", # Path not recorded (legacy / pre-v3)
36
+ ])
37
+
38
+ VALID_INTEGRITY_STATUSES = frozenset(["complete", "partial", "failed"])
39
+ VALID_ECHO_RISK = frozenset(["unknown", "low", "medium", "high"])
40
+ VALID_METRICS_STATUS = frozenset(["complete", "partial", "failed"])
41
+
42
+
43
+ @dataclass
44
+ class CocoonV3(Cocoon):
45
+ """Extended cocoon with runtime provenance, integrity, and full telemetry.
46
+
47
+ Inherits all v2.Cocoon fields. New field groups:
48
+ - Runtime provenance
49
+ - Integrity
50
+ - Echo / collapse detection
51
+ - Full epistemic telemetry
52
+ - AEGIS detail
53
+ - Guardian + Nexus
54
+ - Synthesis structure
55
+ """
56
+
57
+ # ─── Runtime provenance ──────────────────────────────────────────────────
58
+ serialization_version: str = SCHEMA_VERSION
59
+ execution_path: str = "unknown"
60
+ model_inference_invoked: bool = False
61
+ orchestrator_trace_id: str = field(default_factory=lambda: str(uuid.uuid4()))
62
+ runtime_version: str = ""
63
+ metrics_population_status: str = "partial" # 'complete'|'partial'|'failed'
64
+ reason_for_fallback: Optional[str] = None
65
+
66
+ # ─── Integrity ───────────────────────────────────────────────────────────
67
+ cocoon_integrity: str = "partial" # 'complete'|'partial'|'failed'
68
+ cocoon_integrity_score: float = 0.0 # 0.0 – 1.0
69
+
70
+ # ─── Echo / perspective-collapse detection ───────────────────────────────
71
+ echo_risk: str = "unknown" # 'unknown'|'low'|'medium'|'high'
72
+ perspective_collapse_detected: bool = False # True if all outputs near-identical
73
+
74
+ # ─── Epistemic telemetry (live computed from real outputs) ───────────────
75
+ pairwise_tensions: dict = field(default_factory=dict)
76
+ # e.g. {"newton_vs_empathy": 0.41, "philosophy_vs_systems": 0.28}
77
+ perspective_coverage: dict = field(default_factory=dict)
78
+ # e.g. {"newton": 0.85, "empathy": 0.72, "philosophy": 0.60}
79
+ psi_r: float = 0.0
80
+
81
+ # ─── AEGIS detail ────────────────────────────────────────────────────────
82
+ aegis_framework_scores: dict = field(default_factory=dict)
83
+ # e.g. {"utilitarian": 0.82, "deontological": 0.91, "virtue": 0.84, ...}
84
+ aegis_dominant_framework: str = ""
85
+ aegis_ethical_conflict_notes: list = field(default_factory=list)
86
+ aegis_output_changed: bool = False
87
+
88
+ # ─── Guardian ────────────────────────────────────────────────────────────
89
+ guardian_safety_status: str = "" # 'pass'|'flag'|'block'|''
90
+ guardian_trust_calibration: str = "" # 'low'|'medium'|'high'|''
91
+
92
+ # ─── Nexus ───────────────────────────────────────────────────────────────
93
+ nexus_risk_level: str = "" # 'low'|'medium'|'high'|''
94
+ nexus_confidence: float = 0.0
95
+
96
+ # ─── Synthesis structure ─────────────────────────────────────────────────
97
+ synthesis_convergences: list = field(default_factory=list)
98
+ synthesis_divergences: list = field(default_factory=list)
99
+ synthesis_tradeoffs: list = field(default_factory=list)
100
+ synthesis_recommended_position: str = ""
101
+ synthesis_uncertainty_notes: list = field(default_factory=list)
102
+
103
+ # ─── TimeTravelLens observation (optional, query-triggered) ─────────────
104
+ time_travel_metrics: Optional[dict] = None
105
+ # Set when InstitutionalContextDetector fires and extraction confidence ≥ 0.3.
106
+ # Keys: preemption_gap_days, closure_score, closure_class, rupture, beacon,
107
+ # high_preemption_zone, practical_non_closure, actor_gaps, state_id.
108
+ # None means lens did not run for this cocoon.
109
+
110
+ # ─── User-facing ─────────────────────────────────────────────────────────
111
+ user_response_text: str = ""
112
+
113
+ def validate(self) -> list[str]:
114
+ """Extend v2 validation with v3-specific field checks."""
115
+ errors = super().validate()
116
+
117
+ if self.execution_path not in VALID_EXECUTION_PATHS:
118
+ errors.append(
119
+ f"Invalid execution_path: {self.execution_path!r}. "
120
+ f"Must be one of {sorted(VALID_EXECUTION_PATHS)}"
121
+ )
122
+ if self.cocoon_integrity not in VALID_INTEGRITY_STATUSES:
123
+ errors.append(f"Invalid cocoon_integrity: {self.cocoon_integrity!r}")
124
+ if self.echo_risk not in VALID_ECHO_RISK:
125
+ errors.append(f"Invalid echo_risk: {self.echo_risk!r}")
126
+ if self.metrics_population_status not in VALID_METRICS_STATUS:
127
+ errors.append(f"Invalid metrics_population_status: {self.metrics_population_status!r}")
128
+ if not 0.0 <= self.cocoon_integrity_score <= 1.0:
129
+ errors.append(f"cocoon_integrity_score {self.cocoon_integrity_score} out of range [0, 1]")
130
+ if not 0.0 <= self.psi_r <= 1.0:
131
+ errors.append(f"psi_r {self.psi_r} out of range [0, 1]")
132
+
133
+ # Enforce required fields based on execution path
134
+ if self.execution_path == "forge_full":
135
+ if not self.model_inference_invoked:
136
+ errors.append("forge_full path must have model_inference_invoked=True")
137
+ if not self.active_perspectives:
138
+ errors.append("forge_full path must have active_perspectives populated")
139
+ if self.eta_score is None:
140
+ errors.append("forge_full path must have eta_score populated (AEGIS required)")
141
+
142
+ return errors
143
+
144
+ def to_dict(self) -> dict:
145
+ """Full serialization including all v3 fields."""
146
+ base = {
147
+ # ── Identity ──
148
+ "cocoon_id": self.cocoon_id,
149
+ "timestamp": self.timestamp,
150
+ "session_id": self.session_id,
151
+ "serialization_version": self.serialization_version,
152
+ # ── Content ──
153
+ "query": self.query[:500],
154
+ "response_summary": self.response_summary[:500],
155
+ "full_response_hash": self.full_response_hash,
156
+ "user_response_text": self.user_response_text[:2000],
157
+ # ── Emotional / importance ──
158
+ "emotional_valence": self.emotional_valence,
159
+ "importance_score": self.importance_score,
160
+ "confidence": self.confidence,
161
+ # ── Cognitive state ──
162
+ "epsilon_value": self.epsilon_value,
163
+ "gamma_coherence": self.gamma_coherence,
164
+ "eta_score": self.eta_score,
165
+ "psi_r": self.psi_r,
166
+ "active_perspectives": self.active_perspectives,
167
+ "dominant_perspective": self.dominant_perspective,
168
+ "unresolved_tensions": self.unresolved_tensions,
169
+ "synthesis_quality": self.synthesis_quality,
170
+ # ── Retrieval ──
171
+ "problem_type": self.problem_type,
172
+ "topic_tags": self.topic_tags,
173
+ "project_context": self.project_context,
174
+ "user_preferences_inferred": self.user_preferences_inferred,
175
+ # ── Continuity ──
176
+ "open_threads": self.open_threads,
177
+ "contradicts_cocoon_ids": self.contradicts_cocoon_ids,
178
+ "references_cocoon_ids": self.references_cocoon_ids,
179
+ # ── Quality flags ──
180
+ "is_hallucination_flagged": self.is_hallucination_flagged,
181
+ "is_sycophancy_flagged": self.is_sycophancy_flagged,
182
+ "is_verified": self.is_verified,
183
+ # ── Runtime provenance (v3) ──
184
+ "execution_path": self.execution_path,
185
+ "model_inference_invoked": self.model_inference_invoked,
186
+ "orchestrator_trace_id": self.orchestrator_trace_id,
187
+ "runtime_version": self.runtime_version,
188
+ "metrics_population_status": self.metrics_population_status,
189
+ "reason_for_fallback": self.reason_for_fallback,
190
+ # ── Integrity (v3) ──
191
+ "cocoon_integrity": self.cocoon_integrity,
192
+ "cocoon_integrity_score": round(self.cocoon_integrity_score, 4),
193
+ # ── Echo / collapse (v3) ──
194
+ "echo_risk": self.echo_risk,
195
+ "perspective_collapse_detected": self.perspective_collapse_detected,
196
+ # ── Epistemic telemetry (v3) ──
197
+ "pairwise_tensions": self.pairwise_tensions,
198
+ "perspective_coverage": self.perspective_coverage,
199
+ # ── AEGIS detail (v3) ──
200
+ "aegis_framework_scores": self.aegis_framework_scores,
201
+ "aegis_dominant_framework": self.aegis_dominant_framework,
202
+ "aegis_ethical_conflict_notes": self.aegis_ethical_conflict_notes,
203
+ "aegis_output_changed": self.aegis_output_changed,
204
+ # ── Guardian (v3) ──
205
+ "guardian_safety_status": self.guardian_safety_status,
206
+ "guardian_trust_calibration": self.guardian_trust_calibration,
207
+ # ── Nexus (v3) ──
208
+ "nexus_risk_level": self.nexus_risk_level,
209
+ "nexus_confidence": self.nexus_confidence,
210
+ # ── Synthesis structure (v3) ──
211
+ "synthesis_convergences": self.synthesis_convergences,
212
+ "synthesis_divergences": self.synthesis_divergences,
213
+ "synthesis_tradeoffs": self.synthesis_tradeoffs,
214
+ "synthesis_recommended_position": self.synthesis_recommended_position,
215
+ "synthesis_uncertainty_notes": self.synthesis_uncertainty_notes,
216
+ # ── TimeTravelLens (v3.1) ──
217
+ "time_travel_metrics": self.time_travel_metrics,
218
+ }
219
+ return base
220
+
221
+
222
+ def build_cocoon_v3(
223
+ query: str,
224
+ response_text: str,
225
+ response_summary: str,
226
+ # ── v2 fields (all optional) ──
227
+ emotional_valence: str = "curiosity",
228
+ importance_score: float = 5.0,
229
+ epsilon_value: float = 0.35,
230
+ gamma_coherence: float = 0.72,
231
+ eta_score: Optional[float] = None,
232
+ active_perspectives: Optional[list] = None,
233
+ dominant_perspective: Optional[str] = None,
234
+ unresolved_tensions: Optional[list] = None,
235
+ synthesis_quality: str = "adequate",
236
+ problem_type: str = "unknown",
237
+ topic_tags: Optional[list] = None,
238
+ project_context: Optional[str] = None,
239
+ user_preferences_inferred: Optional[dict] = None,
240
+ open_threads: Optional[list] = None,
241
+ contradicts_cocoon_ids: Optional[list] = None,
242
+ references_cocoon_ids: Optional[list] = None,
243
+ confidence: float = 0.75,
244
+ session_id: Optional[str] = None,
245
+ # ── v3-only fields ──
246
+ execution_path: str = "unknown",
247
+ model_inference_invoked: bool = False,
248
+ orchestrator_trace_id: Optional[str] = None,
249
+ runtime_version: str = "",
250
+ metrics_population_status: str = "partial",
251
+ reason_for_fallback: Optional[str] = None,
252
+ psi_r: float = 0.0,
253
+ pairwise_tensions: Optional[dict] = None,
254
+ perspective_coverage: Optional[dict] = None,
255
+ aegis_framework_scores: Optional[dict] = None,
256
+ aegis_dominant_framework: str = "",
257
+ aegis_ethical_conflict_notes: Optional[list] = None,
258
+ aegis_output_changed: bool = False,
259
+ guardian_safety_status: str = "",
260
+ guardian_trust_calibration: str = "",
261
+ nexus_risk_level: str = "",
262
+ nexus_confidence: float = 0.0,
263
+ synthesis_convergences: Optional[list] = None,
264
+ synthesis_divergences: Optional[list] = None,
265
+ synthesis_tradeoffs: Optional[list] = None,
266
+ synthesis_recommended_position: str = "",
267
+ synthesis_uncertainty_notes: Optional[list] = None,
268
+ user_response_text: str = "",
269
+ is_hallucination_flagged: bool = False,
270
+ is_sycophancy_flagged: bool = False,
271
+ echo_risk: str = "unknown",
272
+ perspective_collapse_detected: bool = False,
273
+ time_travel_metrics: Optional[dict] = None,
274
+ ) -> CocoonV3:
275
+ """Build and validate a CocoonV3. Raises ValueError on validation failure."""
276
+ import hashlib
277
+ ts = time.time()
278
+ cocoon_id = hashlib.sha256(f"{query}{ts}".encode()).hexdigest()
279
+ full_hash = hashlib.sha256(response_text.encode()).hexdigest()
280
+
281
+ if topic_tags is None:
282
+ import re
283
+ words = re.findall(r'\b[a-zA-Z][a-zA-Z]{3,}\b', query.lower())
284
+ stop = {"this", "that", "with", "from", "have", "what", "when", "where",
285
+ "which", "will", "been", "should", "could", "would"}
286
+ topic_tags = [w for w in dict.fromkeys(words) if w not in stop][:8]
287
+
288
+ # Clamp valence to valid set
289
+ if emotional_valence not in VALID_VALENCES:
290
+ emotional_valence = "insight"
291
+
292
+ cocoon = CocoonV3(
293
+ # v2 fields
294
+ cocoon_id=cocoon_id,
295
+ timestamp=ts,
296
+ session_id=session_id,
297
+ query=query,
298
+ response_summary=response_summary,
299
+ full_response_hash=full_hash,
300
+ emotional_valence=emotional_valence,
301
+ importance_score=float(importance_score),
302
+ confidence=confidence,
303
+ epsilon_value=float(epsilon_value),
304
+ gamma_coherence=float(gamma_coherence),
305
+ eta_score=eta_score,
306
+ active_perspectives=active_perspectives or [],
307
+ dominant_perspective=dominant_perspective,
308
+ unresolved_tensions=unresolved_tensions or [],
309
+ synthesis_quality=synthesis_quality,
310
+ problem_type=problem_type,
311
+ topic_tags=topic_tags,
312
+ project_context=project_context,
313
+ user_preferences_inferred=user_preferences_inferred or {},
314
+ open_threads=open_threads or [],
315
+ contradicts_cocoon_ids=contradicts_cocoon_ids or [],
316
+ references_cocoon_ids=references_cocoon_ids or [],
317
+ is_hallucination_flagged=is_hallucination_flagged,
318
+ is_sycophancy_flagged=is_sycophancy_flagged,
319
+ # v3 fields
320
+ execution_path=execution_path,
321
+ model_inference_invoked=model_inference_invoked,
322
+ orchestrator_trace_id=orchestrator_trace_id or str(uuid.uuid4()),
323
+ runtime_version=runtime_version,
324
+ metrics_population_status=metrics_population_status,
325
+ reason_for_fallback=reason_for_fallback,
326
+ psi_r=max(0.0, min(1.0, float(psi_r))),
327
+ pairwise_tensions=pairwise_tensions or {},
328
+ perspective_coverage=perspective_coverage or {},
329
+ aegis_framework_scores=aegis_framework_scores or {},
330
+ aegis_dominant_framework=aegis_dominant_framework,
331
+ aegis_ethical_conflict_notes=aegis_ethical_conflict_notes or [],
332
+ aegis_output_changed=aegis_output_changed,
333
+ guardian_safety_status=guardian_safety_status,
334
+ guardian_trust_calibration=guardian_trust_calibration,
335
+ nexus_risk_level=nexus_risk_level,
336
+ nexus_confidence=float(nexus_confidence),
337
+ synthesis_convergences=synthesis_convergences or [],
338
+ synthesis_divergences=synthesis_divergences or [],
339
+ synthesis_tradeoffs=synthesis_tradeoffs or [],
340
+ synthesis_recommended_position=synthesis_recommended_position,
341
+ synthesis_uncertainty_notes=synthesis_uncertainty_notes or [],
342
+ user_response_text=user_response_text,
343
+ echo_risk=echo_risk,
344
+ perspective_collapse_detected=perspective_collapse_detected,
345
+ time_travel_metrics=time_travel_metrics,
346
+ )
347
+
348
+ errors = cocoon.validate()
349
+ if errors:
350
+ raise ValueError(f"CocoonV3 validation failed: {errors}")
351
+
352
+ return cocoon
reasoning_forge/cocoon_self_trainer.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """CocoonSelfTrainer — teach the adaptive model from first-hand experience. SHADOW.
3
+
4
+ Jonathan's idea: "let the model train itself on real-world data it's seen
5
+ first-hand." Codette already stores that data — the cocoons. This feeds it to the
6
+ adaptive sentiment model, using each cocoon's ALREADY-MEASURED emotional signal as
7
+ a weak label.
8
+
9
+ The one hard-won rule this is built around: self-training on first-hand data is
10
+ exactly where the optimizer went wrong (it learned from its own benchmark harness).
11
+ So the guards here are the point, not an afterthought:
12
+
13
+ 1. LABELS COME ONLY FROM STORED EMOTIONAL SIGNALS, never from the sentiment
14
+ model's own prediction. A model that labels its own training data drifts into
15
+ a self-reinforcing loop. The label source must be independent of the learner.
16
+
17
+ 2. IT REFUSES DEGENERATE DATA. Too few examples, or one class dominating
18
+ (e.g. all-positive cocoons), and it does NOT train — it reports why. Learning
19
+ from bad data and reporting success would be the exact lie we refuse.
20
+
21
+ 3. SHADOW-ONLY. Trains a SEPARATE analyzer instance and logs what it learned. It
22
+ does not touch any live model. Whether self-training ever runs live is a
23
+ reviewed decision after reading the shadow log.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import glob
29
+ import json
30
+ import time
31
+ from dataclasses import dataclass, field, asdict
32
+ from pathlib import Path
33
+ from typing import Dict, Iterable, List, Optional, Tuple
34
+
35
+ from reasoning_forge.sentiment_analyzer import SentimentAnalyzer
36
+
37
+
38
+ # Emotional classifications -> weak sentiment label. Only confident, clearly-
39
+ # valenced emotions are used; ambiguous ones are skipped (not guessed).
40
+ _POSITIVE_EMOTIONS = {
41
+ "hope", "awe", "joy", "curiosity", "love", "gratitude", "excitement",
42
+ "trust", "contentment", "pride", "relief", "admiration",
43
+ }
44
+ _NEGATIVE_EMOTIONS = {
45
+ "fear", "anger", "sadness", "disgust", "frustration", "anxiety", "grief",
46
+ "despair", "shame", "guilt", "contempt", "distress",
47
+ }
48
+
49
+
50
+ def cocoon_label(cocoon: dict) -> Optional[Tuple[str, int]]:
51
+ """Extract (text, weak_label) from one cocoon, or None if unusable.
52
+
53
+ Label source, in priority order — ALL are independently-measured signals,
54
+ never the sentiment model's own output:
55
+ 1. emotional_valence (schema v3): >0.1 -> pos, <-0.1 -> neg, else skip
56
+ 2. emotional_classification (EMG cocoons): mapped via the emotion sets
57
+ """
58
+ if not isinstance(cocoon, dict):
59
+ return None
60
+
61
+ text = (cocoon.get("user_response_text") or cocoon.get("user_query")
62
+ or cocoon.get("response_summary")
63
+ or (cocoon.get("metadata") or {}).get("context") or "")
64
+ text = str(text).strip()
65
+ if not text:
66
+ return None
67
+
68
+ # 1) continuous valence
69
+ val = cocoon.get("emotional_valence")
70
+ if isinstance(val, (int, float)):
71
+ if val > 0.1:
72
+ return text, 1
73
+ if val < -0.1:
74
+ return text, 0
75
+ return None # near-neutral: skip, don't guess
76
+
77
+ # 2) categorical emotion
78
+ emo = str(cocoon.get("emotional_classification", "")).strip().lower()
79
+ if emo in _POSITIVE_EMOTIONS:
80
+ return text, 1
81
+ if emo in _NEGATIVE_EMOTIONS:
82
+ return text, 0
83
+ return None
84
+
85
+
86
+ @dataclass
87
+ class SelfTrainReport:
88
+ collected: int
89
+ positive: int
90
+ negative: int
91
+ trained: bool
92
+ reason: str
93
+ ts: float = field(default_factory=time.time)
94
+
95
+ def to_dict(self) -> dict:
96
+ return asdict(self)
97
+
98
+
99
+ class CocoonSelfTrainer:
100
+ """Shadow self-trainer with drift guards."""
101
+
102
+ def __init__(self, min_examples: int = 20, min_minority_frac: float = 0.15):
103
+ self.min_examples = min_examples
104
+ self.min_minority_frac = min_minority_frac
105
+
106
+ def collect_from_records(self, cocoons: Iterable[dict]) -> Tuple[List[str], List[int]]:
107
+ texts, labels = [], []
108
+ for c in cocoons:
109
+ got = cocoon_label(c)
110
+ if got:
111
+ texts.append(got[0])
112
+ labels.append(got[1])
113
+ return texts, labels
114
+
115
+ def collect_from_dir(self, cocoon_dir: str | Path = "cocoons") -> Tuple[List[str], List[int]]:
116
+ recs = []
117
+ for pat in ("*.cocoon", "*.json"):
118
+ for f in glob.glob(str(Path(cocoon_dir) / "**" / pat), recursive=True):
119
+ if "backup" in f.lower():
120
+ continue
121
+ try:
122
+ recs.append(json.load(open(f, encoding="utf-8")))
123
+ except Exception:
124
+ pass
125
+ return self.collect_from_records(recs)
126
+
127
+ def _guard(self, labels: List[int]) -> Tuple[bool, str]:
128
+ """Refuse degenerate data. Returns (ok, reason)."""
129
+ n = len(labels)
130
+ if n < self.min_examples:
131
+ return False, f"too few labelled examples ({n} < {self.min_examples})"
132
+ pos = sum(labels)
133
+ neg = n - pos
134
+ minority = min(pos, neg)
135
+ if minority == 0:
136
+ return False, f"single-class data (pos={pos}, neg={neg}) — training would be degenerate"
137
+ if minority / n < self.min_minority_frac:
138
+ return False, (f"class imbalance too severe (minority {minority}/{n} = "
139
+ f"{minority/n:.2f} < {self.min_minority_frac}) — refusing to train")
140
+ return True, "class balance and volume acceptable"
141
+
142
+ def train_shadow(self, cocoons: Optional[Iterable[dict]] = None,
143
+ cocoon_dir: str | Path = "cocoons") -> Tuple[SelfTrainReport, Optional[SentimentAnalyzer]]:
144
+ """Collect first-hand data, guard it, and (only if healthy) train a SHADOW
145
+ analyzer. Returns (report, shadow_analyzer_or_None). Trains nothing live."""
146
+ if cocoons is not None:
147
+ texts, labels = self.collect_from_records(cocoons)
148
+ else:
149
+ texts, labels = self.collect_from_dir(cocoon_dir)
150
+
151
+ pos = sum(labels)
152
+ neg = len(labels) - pos
153
+ ok, reason = self._guard(labels)
154
+ if not ok:
155
+ return SelfTrainReport(len(labels), pos, neg, trained=False, reason=reason), None
156
+
157
+ shadow = SentimentAnalyzer(enable_adaptive=True)
158
+ shadow.update(texts, labels)
159
+ return SelfTrainReport(len(labels), pos, neg, trained=True,
160
+ reason="trained shadow model on first-hand cocoon data"), shadow
161
+
162
+ def observe(self, report: SelfTrainReport, path: str | Path = None) -> None:
163
+ """Append the report to the self-train shadow log (applied: false)."""
164
+ path = Path(path) if path else Path(__file__).resolve().parent.parent / "data" / "self_train_shadow.jsonl"
165
+ rec = report.to_dict()
166
+ rec["mode"] = "shadow"
167
+ rec["applied"] = False
168
+ try:
169
+ path.parent.mkdir(parents=True, exist_ok=True)
170
+ with path.open("a", encoding="utf-8") as f:
171
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
172
+ except Exception:
173
+ pass
reasoning_forge/cocoon_stability.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cocoon Stability Field — Collapse Detection Engine
3
+ ===================================================
4
+
5
+ FFT-based stability validation that detects synthesis loop collapse
6
+ BEFORE corrupted output is generated.
7
+
8
+ Based on Codette_Deep_Simulation_v1.py cocoon_stability_field() equation:
9
+ stability = ∫|F(k)|² dk < ε_threshold
10
+
11
+ Purpose: Halt debate if system enters instability zone (gamma < 0.4,
12
+ runaway vocabulary patterns, self-referential cascades).
13
+
14
+ Recovered from: J:\\codette-training-lab\\new data\\Codette_Deep_Simulation_v1.py
15
+ """
16
+
17
+ import numpy as np
18
+ from typing import Dict, List, Tuple, Optional
19
+ import logging
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Use Rust hot path when available, numpy fallback otherwise
24
+ try:
25
+ from codette_core import cocoon_stability_check as _rust_stability_check
26
+ _RUST = True
27
+ except ImportError:
28
+ _RUST = False
29
+
30
+
31
+ class CocoonStabilityField:
32
+ """
33
+ FFT-based stability validator for debate coherence.
34
+
35
+ Monitors frequency-domain energy distribution in agent responses.
36
+ If energy becomes too concentrated (self-similarity, repeating patterns)
37
+ or too diffuse (completely incoherent), flags collapse risk.
38
+ """
39
+
40
+ # Stability threshold parameters (empirically calibrated)
41
+ ENERGY_CONCENTRATION_THRESHOLD = 0.85 # Max allowed variance in top frequencies
42
+ SELF_SIMILARITY_THRESHOLD = 0.75 # Max allowed cosine similarity between consecutive responses
43
+ COHERENCE_FLOOR = 0.3 # Minimum coherence before stability alert
44
+ RUNAWAY_VOCABULARY_RATIO = 0.6 # % unique words triggering concern
45
+
46
+ def __init__(self, verbose: bool = False):
47
+ self.verbose = verbose
48
+ self.frequency_signatures: Dict[str, np.ndarray] = {}
49
+ self.stability_history: List[Dict] = []
50
+
51
+ def text_to_spectrum(self, text: str, fft_size: int = 256) -> np.ndarray:
52
+ """
53
+ Convert text to frequency spectrum for FFT analysis.
54
+
55
+ Args:
56
+ text: Response text to analyze
57
+ fft_size: FFT size (should be power of 2)
58
+
59
+ Returns:
60
+ Normalized power spectrum [0, 1]
61
+ """
62
+ # Character-based encoding
63
+ char_codes = np.array([ord(c) % 256 for c in text[:1000]], dtype=np.float32)
64
+
65
+ # Pad to fft_size
66
+ padded = np.zeros(fft_size, dtype=np.float32)
67
+ padded[: len(char_codes)] = char_codes
68
+
69
+ # Apply FFT
70
+ fft_result = np.fft.fft(padded)
71
+ power_spectrum = np.abs(fft_result) ** 2
72
+
73
+ # Normalize
74
+ max_power = np.max(power_spectrum) or 1.0
75
+ normalized_spectrum = power_spectrum / max_power
76
+
77
+ return normalized_spectrum[:128] # Return only positive frequencies
78
+
79
+ def check_energy_concentration(self, spectrum: np.ndarray) -> Tuple[float, bool]:
80
+ """
81
+ Check if spectral energy is too concentrated (self-similarity syndrome).
82
+
83
+ Concentrated energy = agent repeating itself/copying other agents.
84
+
85
+ Args:
86
+ spectrum: Power spectrum from FFT
87
+
88
+ Returns:
89
+ (concentration_ratio, is_concerning)
90
+ """
91
+ # Get top 10 frequencies
92
+ top_k = 10
93
+ top_powers = np.sort(spectrum)[-top_k:]
94
+ top_sum = np.sum(top_powers)
95
+ total_sum = np.sum(spectrum) or 1.0
96
+
97
+ concentration = top_sum / total_sum
98
+ is_concerning = concentration > self.ENERGY_CONCENTRATION_THRESHOLD
99
+
100
+ return concentration, is_concerning
101
+
102
+ def check_self_similarity(self, agent_name: str,
103
+ spectrum: np.ndarray) -> Tuple[float, bool]:
104
+ """
105
+ Check if agent is repeating itself (same response shape).
106
+
107
+ Args:
108
+ agent_name: Name of agent for history lookup
109
+ spectrum: New response spectrum
110
+
111
+ Returns:
112
+ (similarity_score, is_concerning)
113
+ """
114
+ if agent_name not in self.frequency_signatures:
115
+ self.frequency_signatures[agent_name] = spectrum
116
+ return 0.0, False
117
+
118
+ prev_spectrum = self.frequency_signatures[agent_name]
119
+ similarity = np.dot(prev_spectrum, spectrum) / (
120
+ np.linalg.norm(prev_spectrum) * np.linalg.norm(spectrum) + 1e-8
121
+ )
122
+
123
+ self.frequency_signatures[agent_name] = spectrum # Update
124
+
125
+ is_concerning = similarity > self.SELF_SIMILARITY_THRESHOLD
126
+ return float(similarity), is_concerning
127
+
128
+ def check_vocabulary_diversity(self, text: str) -> Tuple[float, bool]:
129
+ """
130
+ Check if response vocabulary is repeating (indicators of "Another perspective on...").
131
+
132
+ Args:
133
+ text: Response text
134
+
135
+ Returns:
136
+ (uniqueness_ratio, is_concerning)
137
+ """
138
+ if len(text) < 20:
139
+ return 1.0, False
140
+
141
+ words = text.lower().split()
142
+ if len(words) == 0:
143
+ return 1.0, False
144
+
145
+ unique_words = len(set(words))
146
+ uniqueness = unique_words / len(words)
147
+
148
+ is_concerning = uniqueness < (1.0 - self.RUNAWAY_VOCABULARY_RATIO)
149
+
150
+ return uniqueness, is_concerning
151
+
152
+ def validate_analysis(self, agent_name: str, text: str) -> Dict:
153
+ """
154
+ Full stability validation for a single agent response.
155
+
156
+ Args:
157
+ agent_name: Name of agent
158
+ text: Response text
159
+
160
+ Returns:
161
+ {
162
+ 'agent': str,
163
+ 'is_stable': bool,
164
+ 'stability_score': float (0-1),
165
+ 'flags': List[str],
166
+ 'spectrum': np.ndarray,
167
+ 'concerns': Dict
168
+ }
169
+ """
170
+ spectrum = self.text_to_spectrum(text)
171
+
172
+ flags = []
173
+ concerns = {
174
+ 'energy_concentration': None,
175
+ 'self_similarity': None,
176
+ 'vocabulary_diversity': None
177
+ }
178
+
179
+ # Check 1: Energy concentration
180
+ conc, conc_concerning = self.check_energy_concentration(spectrum)
181
+ concerns['energy_concentration'] = {
182
+ 'ratio': float(conc),
183
+ 'concerning': conc_concerning
184
+ }
185
+ if conc_concerning:
186
+ flags.append('HIGH_ENERGY_CONCENTRATION')
187
+
188
+ # Check 2: Self-similarity
189
+ similarity, sim_concerning = self.check_self_similarity(agent_name, spectrum)
190
+ concerns['self_similarity'] = {
191
+ 'ratio': float(similarity),
192
+ 'concerning': sim_concerning
193
+ }
194
+ if sim_concerning:
195
+ flags.append('REPEATING_RESPONSE_PATTERN')
196
+
197
+ # Check 3: Vocabulary diversity
198
+ uniqueness, vocab_concerning = self.check_vocabulary_diversity(text)
199
+ concerns['vocabulary_diversity'] = {
200
+ 'uniqueness': float(uniqueness),
201
+ 'concerning': vocab_concerning
202
+ }
203
+ if vocab_concerning:
204
+ flags.append('LOW_VOCABULARY_DIVERSITY')
205
+
206
+ # Check 4: Response length sanity
207
+ if len(text) < 50:
208
+ flags.append('SUSPICIOUSLY_SHORT')
209
+ if len(text) > 10000:
210
+ flags.append('SUSPICIOUSLY_LONG')
211
+
212
+ # Overall stability score
213
+ num_flags = len(flags)
214
+ stability_score = max(0.0, 1.0 - (num_flags * 0.25))
215
+
216
+ is_stable = stability_score > self.COHERENCE_FLOOR
217
+
218
+ if self.verbose and flags:
219
+ logger.info(f" {agent_name}: stability={stability_score:.2f}, flags={flags}")
220
+
221
+ return {
222
+ 'agent': agent_name,
223
+ 'is_stable': is_stable,
224
+ 'stability_score': stability_score,
225
+ 'flags': flags,
226
+ 'spectrum': spectrum,
227
+ 'concerns': concerns
228
+ }
229
+
230
+ def validate_round(self, analyses: Dict[str, str],
231
+ round_num: int) -> Tuple[bool, List[Dict], float]:
232
+ """
233
+ Validate all agents' responses in a debate round.
234
+
235
+ Args:
236
+ analyses: Dict mapping agent_name → response_text
237
+ round_num: Round number (for logging)
238
+
239
+ Returns:
240
+ (all_stable, validation_reports, avg_stability)
241
+ """
242
+ reports = []
243
+ stability_scores = []
244
+
245
+ for agent_name, text in analyses.items():
246
+ report = self.validate_analysis(agent_name, text)
247
+ reports.append(report)
248
+ stability_scores.append(report['stability_score'])
249
+
250
+ avg_stability = np.mean(stability_scores) if stability_scores else 0.5
251
+
252
+ all_stable = all(r['is_stable'] for r in reports)
253
+
254
+ unstable_agents = [r['agent'] for r in reports if not r['is_stable']]
255
+ if unstable_agents:
256
+ logger.warning(
257
+ f"Round {round_num}: Unstable agents detected: {unstable_agents} "
258
+ f"(avg_stability={avg_stability:.2f})"
259
+ )
260
+
261
+ # Store in history
262
+ self.stability_history.append({
263
+ 'round': round_num,
264
+ 'all_stable': all_stable,
265
+ 'avg_stability': avg_stability,
266
+ 'unstable_agents': unstable_agents,
267
+ 'reports': reports
268
+ })
269
+
270
+ return all_stable, reports, avg_stability
271
+
272
+ def should_halt_debate(self, analyses: Dict[str, str],
273
+ round_num: int, gamma: Optional[float] = None) -> Tuple[bool, str]:
274
+ """
275
+ Determine if debate should halt before synthesis.
276
+
277
+ Halt if:
278
+ 1. Multiple agents unstable
279
+ 2. Gamma coherence < 0.35 (system collapse zone)
280
+ 3. Too many "REPEATING_RESPONSE_PATTERN" flags
281
+
282
+ Args:
283
+ analyses: Current round analyses
284
+ round_num: Current round number
285
+ gamma: Current gamma coherence (optional)
286
+
287
+ Returns:
288
+ (should_halt, reason)
289
+ """
290
+ all_stable, reports, avg_stability = self.validate_round(analyses, round_num)
291
+
292
+ if not all_stable:
293
+ unstable_count = sum(1 for r in reports if not r['is_stable'])
294
+ if unstable_count >= 2:
295
+ reason = (
296
+ f"Multiple agents unstable ({unstable_count}/{len(reports)}) "
297
+ f"at round {round_num}. Avg stability: {avg_stability:.2f}"
298
+ )
299
+ logger.warning(f"STABILITY CHECK: Halting debate. {reason}")
300
+ return True, reason
301
+
302
+ if gamma is not None and gamma < 0.35:
303
+ reason = f"System in collapse zone (gamma={gamma:.2f} < 0.35)"
304
+ logger.warning(f"STABILITY CHECK: Halting debate. {reason}")
305
+ return True, reason
306
+
307
+ # Check for repeating response patterns (synthesis loop indicator)
308
+ repeating_count = sum(
309
+ 1 for r in reports
310
+ if 'REPEATING_RESPONSE_PATTERN' in r['flags']
311
+ )
312
+ if repeating_count >= 2:
313
+ reason = (
314
+ f"Multiple agents repeating response patterns ({repeating_count}) "
315
+ f"at round {round_num}. Synthesis loop risk."
316
+ )
317
+ logger.warning(f"STABILITY CHECK: Halting debate. {reason}")
318
+ return True, reason
319
+
320
+ return False, ""
321
+
322
+ # Single-text energy threshold is relaxed vs multi-agent rounds.
323
+ # Character-code FFT of natural language always concentrates energy in
324
+ # low frequencies — the meaningful signal is vocabulary collapse, not
325
+ # raw spectral concentration.
326
+ SINGLE_TEXT_ENERGY_THRESHOLD = 0.999 # effectively vocab-only for single synthesis
327
+
328
+ def check_text_stable(self, text: str) -> bool:
329
+ """
330
+ Single-text stability check used by the consciousness stack L4.
331
+ Uses Rust FFT when available, falls back to numpy.
332
+ Returns True if text is stable (safe to continue).
333
+ Primary signal: vocabulary collapse (degenerate repetition).
334
+ """
335
+ if _RUST:
336
+ is_stable, _ec, _vr = _rust_stability_check(
337
+ text,
338
+ self.SINGLE_TEXT_ENERGY_THRESHOLD,
339
+ self.RUNAWAY_VOCABULARY_RATIO,
340
+ )
341
+ return is_stable
342
+
343
+ # numpy fallback
344
+ words = text.split()
345
+ if not words:
346
+ return True
347
+ unique_ratio = len(set(w.lower() for w in words)) / len(words)
348
+ return unique_ratio >= self.RUNAWAY_VOCABULARY_RATIO
349
+
350
+ def get_summary(self) -> Dict:
351
+ """Get stability history summary."""
352
+ if not self.stability_history:
353
+ return {"message": "No stability checks performed"}
354
+
355
+ return {
356
+ "total_rounds_checked": len(self.stability_history),
357
+ "average_stability": np.mean([h['avg_stability'] for h in self.stability_history]),
358
+ "halts_triggered": sum(1 for h in self.stability_history if not h['all_stable']),
359
+ "recent": self.stability_history[-3:] if len(self.stability_history) >= 3 else self.stability_history,
360
+ }
reasoning_forge/cocoon_sync.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Federated Cocoon Synchronization Protocol — Encrypted state packaging,
3
+ HMAC signing, and attractor merger for distributed RC+xi nodes.
4
+
5
+ Implements:
6
+ - Cocoon packaging with full RC+xi metrics
7
+ - Fernet symmetric encryption (AES-128-CBC + HMAC-SHA256)
8
+ - Attractor merger via weighted mean-field coupling (Eq. 12)
9
+ - Phase coherence consensus (Gamma >= 0.98 target)
10
+ - Secure sync protocol: package -> encrypt -> sign -> transmit -> verify -> merge
11
+
12
+ This module enables Codette Pods (edge nodes on RPi 5) to synchronize
13
+ their reasoning state without exposing raw data.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import hmac
20
+ import json
21
+ import os
22
+ import time
23
+ import uuid
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Dict, List, Optional, Tuple
26
+
27
+ # Encryption is optional — gracefully degrade if cryptography not installed
28
+ try:
29
+ from cryptography.fernet import Fernet
30
+ HAS_CRYPTO = True
31
+ except ImportError:
32
+ HAS_CRYPTO = False
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Data structures
37
+ # ---------------------------------------------------------------------------
38
+
39
+ @dataclass
40
+ class CocoonPackage:
41
+ """A packaged cocoon ready for sync."""
42
+ cocoon_id: str
43
+ node_id: str
44
+ timestamp: float
45
+ state_snapshot: Dict[str, Any]
46
+ attractors: List[Dict]
47
+ glyphs: List[Dict]
48
+ metrics: Dict[str, float]
49
+ payload_hash: str
50
+ encrypted: bool = False
51
+ raw_payload: Optional[bytes] = None
52
+ signature: Optional[str] = None
53
+
54
+
55
+ @dataclass
56
+ class SyncResult:
57
+ """Result of a cocoon synchronization."""
58
+ success: bool
59
+ merged_attractors: int
60
+ new_glyphs: int
61
+ coherence_before: float
62
+ coherence_after: float
63
+ tension_delta: float
64
+ errors: List[str] = field(default_factory=list)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Key management
69
+ # ---------------------------------------------------------------------------
70
+
71
+ class CocoonKeyManager:
72
+ """Manages encryption keys for cocoon sync."""
73
+
74
+ def __init__(self, key: Optional[bytes] = None):
75
+ if key:
76
+ self._key = key
77
+ elif HAS_CRYPTO:
78
+ self._key = Fernet.generate_key()
79
+ else:
80
+ self._key = os.urandom(32)
81
+
82
+ @property
83
+ def key(self) -> bytes:
84
+ return self._key
85
+
86
+ def derive_hmac_key(self) -> bytes:
87
+ return hashlib.sha256(self._key + b"hmac_salt_cocoon").digest()
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # CocoonSync
92
+ # ---------------------------------------------------------------------------
93
+
94
+ class CocoonSync:
95
+ """Federated cocoon synchronization protocol."""
96
+
97
+ def __init__(
98
+ self,
99
+ node_id: str,
100
+ key_manager: Optional[CocoonKeyManager] = None,
101
+ coherence_target: float = 0.98,
102
+ tension_target: float = 0.05,
103
+ ethical_target: float = 0.90,
104
+ ):
105
+ self.node_id = node_id
106
+ self.key_manager = key_manager or CocoonKeyManager()
107
+ self.coherence_target = coherence_target
108
+ self.tension_target = tension_target
109
+ self.ethical_target = ethical_target
110
+
111
+ self._local_attractors: List[Dict] = []
112
+ self._local_glyphs: List[Dict] = []
113
+ self._sync_history: List[Dict] = []
114
+
115
+ # -- Step 1: Package ----------------------------------------------------
116
+
117
+ def package_cocoon(
118
+ self,
119
+ spiderweb_state: Dict[str, Any],
120
+ phase_coherence: float,
121
+ epistemic_tension: float,
122
+ ethical_alignment: float,
123
+ attractors: Optional[List[Dict]] = None,
124
+ glyphs: Optional[List[Dict]] = None,
125
+ ) -> CocoonPackage:
126
+ """Package current state into a cocoon for transmission.
127
+
128
+ Args:
129
+ spiderweb_state: Serialized QuantumSpiderweb state.
130
+ phase_coherence: Current Gamma value.
131
+ epistemic_tension: Current xi value.
132
+ ethical_alignment: Current AEGIS eta value.
133
+ attractors: Detected attractor manifolds.
134
+ glyphs: Identity glyphs formed.
135
+
136
+ Returns:
137
+ CocoonPackage ready for encryption and transmission.
138
+ """
139
+ cocoon_id = f"cocoon_{uuid.uuid4().hex[:12]}"
140
+
141
+ metrics = {
142
+ "phase_coherence": round(phase_coherence, 4),
143
+ "epistemic_tension": round(epistemic_tension, 4),
144
+ "ethical_alignment": round(ethical_alignment, 4),
145
+ "timestamp": time.time(),
146
+ }
147
+
148
+ # Build payload
149
+ payload = {
150
+ "cocoon_id": cocoon_id,
151
+ "node_id": self.node_id,
152
+ "state": spiderweb_state,
153
+ "attractors": attractors or [],
154
+ "glyphs": glyphs or [],
155
+ "metrics": metrics,
156
+ }
157
+
158
+ payload_json = json.dumps(payload, sort_keys=True, default=str)
159
+ payload_hash = hashlib.sha256(payload_json.encode()).hexdigest()
160
+
161
+ return CocoonPackage(
162
+ cocoon_id=cocoon_id,
163
+ node_id=self.node_id,
164
+ timestamp=time.time(),
165
+ state_snapshot=spiderweb_state,
166
+ attractors=attractors or [],
167
+ glyphs=glyphs or [],
168
+ metrics=metrics,
169
+ payload_hash=payload_hash,
170
+ raw_payload=payload_json.encode(),
171
+ )
172
+
173
+ # -- Step 2: Encrypt ---------------------------------------------------
174
+
175
+ def encrypt_cocoon(self, package: CocoonPackage) -> CocoonPackage:
176
+ """Encrypt cocoon payload with Fernet (AES-128-CBC + HMAC-SHA256).
177
+
178
+ Returns a new CocoonPackage; does not mutate the input.
179
+ Falls back to XOR obfuscation if cryptography is not installed.
180
+ """
181
+ import copy
182
+ result = copy.copy(package)
183
+
184
+ if result.raw_payload is None:
185
+ payload_json = json.dumps({
186
+ "cocoon_id": result.cocoon_id,
187
+ "node_id": result.node_id,
188
+ "state": result.state_snapshot,
189
+ "attractors": result.attractors,
190
+ "glyphs": result.glyphs,
191
+ "metrics": result.metrics,
192
+ }, sort_keys=True, default=str)
193
+ result.raw_payload = payload_json.encode()
194
+
195
+ if HAS_CRYPTO:
196
+ fernet = Fernet(self.key_manager.key)
197
+ encrypted = fernet.encrypt(result.raw_payload)
198
+ result.raw_payload = encrypted
199
+ result.encrypted = True
200
+ else:
201
+ # Fallback: XOR obfuscation (not real encryption — placeholder)
202
+ key_bytes = self.key_manager.key[:len(result.raw_payload)]
203
+ obfuscated = bytes(
204
+ a ^ b for a, b in
205
+ zip(result.raw_payload, key_bytes * (len(result.raw_payload) // len(key_bytes) + 1))
206
+ )
207
+ result.raw_payload = obfuscated
208
+ result.encrypted = True
209
+
210
+ return result
211
+
212
+ # -- Step 3: Sign ------------------------------------------------------
213
+
214
+ def sign_cocoon(self, package: CocoonPackage) -> CocoonPackage:
215
+ """Sign cocoon with HMAC-SHA256 for integrity verification.
216
+
217
+ Returns a new CocoonPackage; does not mutate the input.
218
+ """
219
+ import copy
220
+ result = copy.copy(package)
221
+ hmac_key = self.key_manager.derive_hmac_key()
222
+ data_to_sign = result.raw_payload or result.payload_hash.encode()
223
+ signature = hmac.new(hmac_key, data_to_sign, hashlib.sha256).hexdigest()
224
+ result.signature = signature
225
+ return result
226
+
227
+ # -- Step 4: Verify (receiving end) ------------------------------------
228
+
229
+ def verify_cocoon(self, package: CocoonPackage) -> bool:
230
+ """Verify HMAC signature of incoming cocoon."""
231
+ if not package.signature:
232
+ return False
233
+ hmac_key = self.key_manager.derive_hmac_key()
234
+ data_to_verify = package.raw_payload or package.payload_hash.encode()
235
+ expected = hmac.new(hmac_key, data_to_verify, hashlib.sha256).hexdigest()
236
+ return hmac.compare_digest(expected, package.signature)
237
+
238
+ # -- Step 5: Decrypt ---------------------------------------------------
239
+
240
+ def decrypt_cocoon(self, package: CocoonPackage) -> Dict[str, Any]:
241
+ """Decrypt cocoon payload.
242
+
243
+ Returns the deserialized payload dict.
244
+ """
245
+ if not package.encrypted or package.raw_payload is None:
246
+ return {
247
+ "state": package.state_snapshot,
248
+ "attractors": package.attractors,
249
+ "glyphs": package.glyphs,
250
+ "metrics": package.metrics,
251
+ }
252
+
253
+ if HAS_CRYPTO:
254
+ fernet = Fernet(self.key_manager.key)
255
+ decrypted = fernet.decrypt(package.raw_payload)
256
+ else:
257
+ # Reverse XOR
258
+ key_bytes = self.key_manager.key[:len(package.raw_payload)]
259
+ decrypted = bytes(
260
+ a ^ b for a, b in
261
+ zip(package.raw_payload, key_bytes * (len(package.raw_payload) // len(key_bytes) + 1))
262
+ )
263
+
264
+ return json.loads(decrypted.decode())
265
+
266
+ # -- Step 6: Merge attractors ------------------------------------------
267
+
268
+ def merge_attractors(
269
+ self,
270
+ local_attractors: List[Dict],
271
+ remote_attractors: List[Dict],
272
+ local_coherence: float = 0.95,
273
+ merge_radius: float = 2.0,
274
+ ) -> List[Dict]:
275
+ """Weighted attractor merger via mean-field coupling (Eq. 12).
276
+
277
+ alpha = local_coherence: higher coherence = trust local more.
278
+ """
279
+ alpha = min(local_coherence, 0.95)
280
+ merged = list(local_attractors)
281
+
282
+ for remote_att in remote_attractors:
283
+ r_center = remote_att.get("center", [0] * 5)
284
+ matched = False
285
+
286
+ for local_att in merged:
287
+ l_center = local_att.get("center", [0] * 5)
288
+ # Compute distance
289
+ dist = sum((a - b) ** 2 for a, b in zip(l_center, r_center)) ** 0.5
290
+ if dist <= merge_radius:
291
+ # Weighted merge: c_merged = alpha * c_local + (1-alpha) * c_remote
292
+ new_center = [
293
+ alpha * lc + (1 - alpha) * rc
294
+ for lc, rc in zip(l_center, r_center)
295
+ ]
296
+ local_att["center"] = new_center
297
+ # Expand member list
298
+ local_att.setdefault("remote_members", [])
299
+ local_att["remote_members"].extend(
300
+ remote_att.get("members", [])
301
+ )
302
+ matched = True
303
+ break
304
+
305
+ if not matched:
306
+ # New attractor from remote
307
+ merged.append({
308
+ "attractor_id": remote_att.get("attractor_id", f"remote_{len(merged)}"),
309
+ "center": r_center,
310
+ "members": remote_att.get("members", []),
311
+ "source": "remote",
312
+ })
313
+
314
+ return merged
315
+
316
+ # -- Full sync protocol ------------------------------------------------
317
+
318
+ def sync_with_remote(
319
+ self,
320
+ incoming_package: CocoonPackage,
321
+ local_spiderweb_state: Dict[str, Any],
322
+ local_coherence: float,
323
+ local_tension: float,
324
+ ) -> SyncResult:
325
+ """Full sync protocol: verify -> decrypt -> merge -> report.
326
+
327
+ Args:
328
+ incoming_package: Encrypted cocoon from remote node.
329
+ local_spiderweb_state: Current local web state.
330
+ local_coherence: Current local Gamma.
331
+ local_tension: Current local xi.
332
+
333
+ Returns:
334
+ SyncResult with merge statistics.
335
+ """
336
+ errors: List[str] = []
337
+
338
+ # Verify
339
+ if not self.verify_cocoon(incoming_package):
340
+ return SyncResult(
341
+ success=False, merged_attractors=0, new_glyphs=0,
342
+ coherence_before=local_coherence, coherence_after=local_coherence,
343
+ tension_delta=0.0, errors=["HMAC verification failed"],
344
+ )
345
+
346
+ # Decrypt
347
+ try:
348
+ remote_data = self.decrypt_cocoon(incoming_package)
349
+ except Exception as e:
350
+ return SyncResult(
351
+ success=False, merged_attractors=0, new_glyphs=0,
352
+ coherence_before=local_coherence, coherence_after=local_coherence,
353
+ tension_delta=0.0, errors=[f"Decryption failed: {e}"],
354
+ )
355
+
356
+ # Check ethical alignment
357
+ remote_eta = remote_data.get("metrics", {}).get("ethical_alignment", 0)
358
+ if remote_eta < self.ethical_target:
359
+ errors.append(
360
+ f"Remote ethical alignment {remote_eta:.3f} below target {self.ethical_target}"
361
+ )
362
+
363
+ # Merge attractors
364
+ remote_attractors = remote_data.get("attractors", [])
365
+ local_attractors = self._extract_attractors(local_spiderweb_state)
366
+ merged = self.merge_attractors(
367
+ local_attractors, remote_attractors, local_coherence
368
+ )
369
+ new_attractor_count = len(merged) - len(local_attractors)
370
+
371
+ # Collect new glyphs
372
+ remote_glyphs = remote_data.get("glyphs", [])
373
+ existing_ids = {g.get("glyph_id") for g in self._local_glyphs}
374
+ new_glyphs = [g for g in remote_glyphs if g.get("glyph_id") not in existing_ids]
375
+ self._local_glyphs.extend(new_glyphs)
376
+
377
+ # Estimate new coherence (weighted average)
378
+ remote_coherence = remote_data.get("metrics", {}).get("phase_coherence", 0.5)
379
+ new_coherence = 0.7 * local_coherence + 0.3 * remote_coherence
380
+
381
+ remote_tension = remote_data.get("metrics", {}).get("epistemic_tension", 0.5)
382
+ tension_delta = remote_tension - local_tension
383
+
384
+ # Record sync
385
+ self._sync_history.append({
386
+ "timestamp": time.time(),
387
+ "remote_node": incoming_package.node_id,
388
+ "merged_attractors": len(merged),
389
+ "new_glyphs": len(new_glyphs),
390
+ "coherence_after": new_coherence,
391
+ })
392
+
393
+ return SyncResult(
394
+ success=True,
395
+ merged_attractors=new_attractor_count,
396
+ new_glyphs=len(new_glyphs),
397
+ coherence_before=local_coherence,
398
+ coherence_after=round(new_coherence, 4),
399
+ tension_delta=round(tension_delta, 4),
400
+ errors=errors,
401
+ )
402
+
403
+ def check_consensus(
404
+ self,
405
+ local_coherence: float,
406
+ local_tension: float,
407
+ local_eta: float,
408
+ ) -> Dict[str, bool]:
409
+ """Check if local node meets consensus criteria.
410
+
411
+ Target: Gamma >= 0.98, xi <= 0.05, eta >= 0.90
412
+ """
413
+ return {
414
+ "phase_coherence_met": local_coherence >= self.coherence_target,
415
+ "tension_met": local_tension <= self.tension_target,
416
+ "ethical_met": local_eta >= self.ethical_target,
417
+ "consensus": (
418
+ local_coherence >= self.coherence_target
419
+ and local_tension <= self.tension_target
420
+ and local_eta >= self.ethical_target
421
+ ),
422
+ }
423
+
424
+ def _extract_attractors(self, web_state: Dict) -> List[Dict]:
425
+ """Extract attractors from spiderweb state dict."""
426
+ # Try to find attractors in the state
427
+ if isinstance(web_state, dict):
428
+ if "attractors" in web_state:
429
+ return web_state["attractors"]
430
+ return self._local_attractors
431
+
432
+ def get_sync_status(self) -> Dict[str, Any]:
433
+ """Return sync protocol status."""
434
+ return {
435
+ "node_id": self.node_id,
436
+ "total_syncs": len(self._sync_history),
437
+ "local_attractors": len(self._local_attractors),
438
+ "local_glyphs": len(self._local_glyphs),
439
+ "has_encryption": HAS_CRYPTO,
440
+ "recent_syncs": self._sync_history[-5:],
441
+ }
reasoning_forge/cocoon_synthesizer.py ADDED
@@ -0,0 +1,1430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cocoon Synthesizer — Meta-Cognitive Strategy Engine for Codette RC+xi Framework.
3
+
4
+ This module enables Codette's highest-order cognitive capability:
5
+ the ability to introspect on its own past reasoning (stored in cocoons),
6
+ discover emergent cross-domain patterns, forge NEW reasoning strategies
7
+ from those patterns, and apply them with before/after comparison.
8
+
9
+ This is not template-driven synthesis — it is genuine meta-cognition:
10
+ Codette examining HOW it has thought, finding what it didn't know it knew,
11
+ and using that discovery to think better.
12
+
13
+ Pipeline:
14
+ 1. Retrieve cocoons across domains (emotional, architectural, creative, etc.)
15
+ 2. Extract cross-domain patterns (structural similarities that span domains)
16
+ 3. Forge a new reasoning strategy from the pattern intersection
17
+ 4. Apply both old and new strategies to a given problem
18
+ 5. Output structured comparison showing the emergent improvement
19
+
20
+ Author: Jonathan Harrison (Raiff's Bits LLC)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import json
27
+ import logging
28
+ import math
29
+ import random
30
+ import re
31
+ import time
32
+ from dataclasses import dataclass, field
33
+ from typing import Dict, List, Optional, Tuple, Any
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Data Structures
40
+ # ---------------------------------------------------------------------------
41
+
42
+ @dataclass
43
+ class CocoonPattern:
44
+ """A cross-domain pattern extracted from cocoon analysis."""
45
+ name: str
46
+ description: str
47
+ source_cocoons: List[str] # cocoon IDs that contributed
48
+ source_domains: List[str] # which domains were involved
49
+ structural_similarity: str # what structural element they share
50
+ tension_signature: float # how much cross-domain tension exists
51
+ novelty_score: float # 0-1: how non-obvious this pattern is
52
+ evidence: List[str] # textual evidence from cocoons
53
+
54
+ def to_dict(self) -> Dict:
55
+ return {
56
+ "name": self.name,
57
+ "description": self.description,
58
+ "source_cocoons": self.source_cocoons,
59
+ "source_domains": self.source_domains,
60
+ "structural_similarity": self.structural_similarity,
61
+ "tension_signature": round(self.tension_signature, 4),
62
+ "novelty_score": round(self.novelty_score, 4),
63
+ "evidence": self.evidence,
64
+ }
65
+
66
+
67
+ @dataclass
68
+ class ReasoningStrategy:
69
+ """A novel reasoning strategy forged from cocoon pattern synthesis."""
70
+ name: str
71
+ definition: str # how it works
72
+ mechanism: str # step-by-step mechanism
73
+ improvement_rationale: str # why it improves cognition
74
+ source_patterns: List[str] # pattern names it was derived from
75
+ applicability: List[str] # what kinds of problems it suits
76
+ forged_timestamp: float = field(default_factory=time.time)
77
+ strategy_id: str = ""
78
+
79
+ def __post_init__(self):
80
+ if not self.strategy_id:
81
+ seed = f"{self.name}_{self.forged_timestamp}"
82
+ self.strategy_id = f"strategy_{hashlib.md5(seed.encode()).hexdigest()[:10]}"
83
+
84
+ def to_dict(self) -> Dict:
85
+ return {
86
+ "strategy_id": self.strategy_id,
87
+ "name": self.name,
88
+ "definition": self.definition,
89
+ "mechanism": self.mechanism,
90
+ "improvement_rationale": self.improvement_rationale,
91
+ "source_patterns": self.source_patterns,
92
+ "applicability": self.applicability,
93
+ "forged_timestamp": self.forged_timestamp,
94
+ }
95
+
96
+
97
+ @dataclass
98
+ class ReasoningPath:
99
+ """A reasoning path — the trace of how a strategy processed a problem."""
100
+ strategy_name: str
101
+ steps: List[str]
102
+ conclusion: str
103
+ dimensions_engaged: List[str] # which cognitive dimensions were active
104
+ depth_score: float # 0-1: reasoning depth
105
+ novelty_score: float # 0-1: how novel the conclusion is
106
+
107
+ def to_dict(self) -> Dict:
108
+ return {
109
+ "strategy_name": self.strategy_name,
110
+ "steps": self.steps,
111
+ "conclusion": self.conclusion,
112
+ "dimensions_engaged": self.dimensions_engaged,
113
+ "depth_score": round(self.depth_score, 4),
114
+ "novelty_score": round(self.novelty_score, 4),
115
+ }
116
+
117
+
118
+ @dataclass
119
+ class StrategyComparison:
120
+ """Side-by-side comparison of original vs new reasoning paths."""
121
+ problem: str
122
+ original_path: ReasoningPath
123
+ new_path: ReasoningPath
124
+ differences: List[str]
125
+ improvement_assessment: str
126
+ new_strategy: ReasoningStrategy
127
+ evidence_chain: List[str] # proof this came from cocoon synthesis
128
+ valuation_analysis: Optional[Dict[str, Any]] = None
129
+
130
+ def to_dict(self) -> Dict:
131
+ data = {
132
+ "problem": self.problem,
133
+ "original_path": self.original_path.to_dict(),
134
+ "new_path": self.new_path.to_dict(),
135
+ "differences": self.differences,
136
+ "improvement_assessment": self.improvement_assessment,
137
+ "new_strategy": self.new_strategy.to_dict(),
138
+ "evidence_chain": self.evidence_chain,
139
+ }
140
+ if self.valuation_analysis is not None:
141
+ data["valuation_analysis"] = self.valuation_analysis
142
+ return data
143
+
144
+ def to_readable(self) -> str:
145
+ """Human-readable formatted output."""
146
+ lines = []
147
+ lines.append("=" * 70)
148
+ lines.append("COCOON SYNTHESIS ANALYSIS")
149
+ lines.append("=" * 70)
150
+
151
+ lines.append(f"\n## Problem: {self.problem}\n")
152
+
153
+ # New strategy definition
154
+ lines.append("─" * 50)
155
+ lines.append(f"## NEW STRATEGY: {self.new_strategy.name}")
156
+ lines.append("─" * 50)
157
+ lines.append(f"\n**Definition:** {self.new_strategy.definition}\n")
158
+ lines.append(f"**Mechanism:** {self.new_strategy.mechanism}\n")
159
+ lines.append(f"**Why it improves cognition:** {self.new_strategy.improvement_rationale}\n")
160
+
161
+ # Evidence from cocoons
162
+ lines.append("─" * 50)
163
+ lines.append("## EVIDENCE FROM COCOON SYNTHESIS")
164
+ lines.append("─" * 50)
165
+ for i, ev in enumerate(self.evidence_chain, 1):
166
+ lines.append(f" {i}. {ev}")
167
+
168
+ if self.valuation_analysis is not None:
169
+ lines.append("\n" + "─" * 50)
170
+ lines.append("## VALUATION CONTEXT")
171
+ lines.append("─" * 50)
172
+ if self.valuation_analysis.get("mode") == "risk_frontier":
173
+ best = self.valuation_analysis.get("best_scenario") or {}
174
+ worst = self.valuation_analysis.get("worst_scenario") or {}
175
+ lines.append(f"Best frontier scenario: {best.get('name', 'unknown')}")
176
+ lines.append(f"Worst frontier scenario: {worst.get('name', 'unknown')}")
177
+ else:
178
+ lines.append(
179
+ f"Combined total: {self.valuation_analysis.get('combined_total')} | "
180
+ f"Singularity detected: {self.valuation_analysis.get('singularity_detected')}"
181
+ )
182
+ for note in self.valuation_analysis.get("notes", [])[:3]:
183
+ lines.append(f" • {note}")
184
+
185
+ # Original reasoning path
186
+ lines.append("\n" + "─" * 50)
187
+ lines.append("## ORIGINAL REASONING PATH")
188
+ lines.append("─" * 50)
189
+ lines.append(f"Strategy: {self.original_path.strategy_name}")
190
+ lines.append(f"Dimensions engaged: {', '.join(self.original_path.dimensions_engaged)}")
191
+ lines.append(f"Depth: {self.original_path.depth_score:.2f} | "
192
+ f"Novelty: {self.original_path.novelty_score:.2f}")
193
+ lines.append("\nSteps:")
194
+ for i, step in enumerate(self.original_path.steps, 1):
195
+ lines.append(f" {i}. {step}")
196
+ lines.append(f"\n**Conclusion:** {self.original_path.conclusion}")
197
+
198
+ # New reasoning path
199
+ lines.append("\n" + "─" * 50)
200
+ lines.append("## NEW REASONING PATH (with forged strategy)")
201
+ lines.append("─" * 50)
202
+ lines.append(f"Strategy: {self.new_path.strategy_name}")
203
+ lines.append(f"Dimensions engaged: {', '.join(self.new_path.dimensions_engaged)}")
204
+ lines.append(f"Depth: {self.new_path.depth_score:.2f} | "
205
+ f"Novelty: {self.new_path.novelty_score:.2f}")
206
+ lines.append("\nSteps:")
207
+ for i, step in enumerate(self.new_path.steps, 1):
208
+ lines.append(f" {i}. {step}")
209
+ lines.append(f"\n**Conclusion:** {self.new_path.conclusion}")
210
+
211
+ # Differences
212
+ lines.append("\n" + "─" * 50)
213
+ lines.append("## DIFFERENCES IN OUTCOME")
214
+ lines.append("─" * 50)
215
+ for diff in self.differences:
216
+ lines.append(f" • {diff}")
217
+
218
+ # Assessment
219
+ lines.append(f"\n**Assessment:** {self.improvement_assessment}")
220
+ lines.append("\n" + "=" * 70)
221
+
222
+ return "\n".join(lines)
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Pattern Extraction Engine
227
+ # ---------------------------------------------------------------------------
228
+
229
+ # Structural archetypes that can appear across domains
230
+ _CROSS_DOMAIN_ARCHETYPES = {
231
+ "feedback_loop": {
232
+ "signals": ["feedback", "adjust", "adapt", "loop", "iterate", "refine",
233
+ "calibrate", "response", "cycle", "converge"],
234
+ "description": "Self-modifying cycle where output feeds back into input",
235
+ },
236
+ "layered_emergence": {
237
+ "signals": ["layer", "emerge", "build", "stack", "foundation", "level",
238
+ "cascade", "hierarchy", "progressive", "compound"],
239
+ "description": "Complex behavior arising from simpler layered components",
240
+ },
241
+ "tension_resolution": {
242
+ "signals": ["tension", "balance", "resolve", "conflict", "harmony",
243
+ "opposing", "complement", "paradox", "reconcile", "synthesis"],
244
+ "description": "Productive outcomes from holding opposing forces",
245
+ },
246
+ "resonant_transfer": {
247
+ "signals": ["resonate", "transfer", "bridge", "connect", "translate",
248
+ "frequency", "echo", "mirror", "sympathetic", "coupling"],
249
+ "description": "Patterns or energy transferring between different domains",
250
+ },
251
+ "boundary_permeability": {
252
+ "signals": ["boundary", "cross", "interface", "membrane", "permeable",
253
+ "threshold", "transition", "edge", "liminal", "between"],
254
+ "description": "Intelligence emerges at the boundaries between systems",
255
+ },
256
+ "compression_expansion": {
257
+ "signals": ["compress", "expand", "dense", "unfold", "concentrate",
258
+ "distill", "amplify", "seed", "crystallize", "bloom"],
259
+ "description": "Alternating between compressed essence and expanded expression",
260
+ },
261
+ }
262
+
263
+
264
+ class CocoonSynthesizer:
265
+ """
266
+ Meta-cognitive engine that introspects on Codette's past reasoning
267
+ to discover emergent patterns and forge new reasoning strategies.
268
+
269
+ This is Codette's capacity for genuine self-improvement through
270
+ reflection on its own cognitive history.
271
+ """
272
+
273
+ def __init__(self, memory=None):
274
+ """
275
+ Args:
276
+ memory: UnifiedMemory instance (or None for standalone mode)
277
+ """
278
+ self.memory = memory
279
+ self._strategy_history: List[ReasoningStrategy] = []
280
+
281
+ # ──────────────────────────────────────────────────────────
282
+ # STEP 1: Retrieve cocoons across domains
283
+ # ──────────────────────────────────────────────────────────
284
+
285
+ def retrieve_cross_domain_cocoons(
286
+ self,
287
+ domains: Optional[List[str]] = None,
288
+ min_per_domain: int = 3,
289
+ ) -> Dict[str, List[Dict]]:
290
+ """
291
+ Retrieve cocoons spanning multiple cognitive domains.
292
+
293
+ Default domains: emotional reasoning, system architecture, creative generation.
294
+ Returns dict of domain_tag -> list of cocoons.
295
+ """
296
+ if domains is None:
297
+ domains = ["emotional", "analytical", "creative"]
298
+
299
+ # Domain search strategies: exact domain match + FTS keyword expansion
300
+ domain_keywords = {
301
+ "emotional": ["emotion", "empathy", "compassion", "feel", "care",
302
+ "trust", "human experience", "fear", "joy", "sorrow"],
303
+ "system_architecture": ["architecture", "system", "module", "design",
304
+ "component", "layer", "interface", "scale",
305
+ "infrastructure", "pattern"],
306
+ "creative": ["creative", "music", "art", "invent", "compose",
307
+ "imagination", "novel", "design", "dream", "generate"],
308
+ "analytical": ["analysis", "physics", "math", "logic", "evidence",
309
+ "measure", "cause", "effect", "systematic", "proof"],
310
+ "philosophical": ["meaning", "existence", "truth", "consciousness",
311
+ "ethics", "purpose", "identity", "free will"],
312
+ "metacognitive": ["self-aware", "recursive", "meta", "reflect",
313
+ "introspect", "cognition", "thinking about thinking"],
314
+ }
315
+
316
+ result = {}
317
+
318
+ for domain in domains:
319
+ cocoons = []
320
+ keywords = domain_keywords.get(domain, [domain])
321
+
322
+ if self.memory:
323
+ # Strategy 1: Direct domain match
324
+ direct = self.memory.recall_by_domain(domain, min_per_domain)
325
+ cocoons.extend(direct)
326
+
327
+ # Strategy 2: FTS search with domain keywords
328
+ for kw in keywords[:4]:
329
+ fts = self.memory.recall_relevant(kw, max_results=2)
330
+ for c in fts:
331
+ if c.get("id") not in {x.get("id") for x in cocoons}:
332
+ cocoons.append(c)
333
+
334
+ # Strategy 3: Emotion-based for emotional domain
335
+ if domain == "emotional":
336
+ for emotion in ["compassion", "joy", "fear", "awe"]:
337
+ em = self.memory.recall_by_emotion(emotion, 2)
338
+ for c in em:
339
+ if c.get("id") not in {x.get("id") for x in cocoons}:
340
+ cocoons.append(c)
341
+
342
+ # If memory is empty/unavailable, use the filesystem cocoons
343
+ if len(cocoons) < min_per_domain:
344
+ cocoons.extend(
345
+ self._load_fallback_cocoons(domain, min_per_domain - len(cocoons))
346
+ )
347
+
348
+ result[domain] = cocoons[:min_per_domain * 2] # Cap at 2x requested
349
+
350
+ return result
351
+
352
+ def _load_fallback_cocoons(self, domain: str, needed: int) -> List[Dict]:
353
+ """Load cocoons from filesystem as fallback when DB is sparse."""
354
+ import os
355
+ from pathlib import Path
356
+
357
+ cocoon_dir = Path(__file__).parent.parent / "cocoons"
358
+ if not cocoon_dir.exists():
359
+ return []
360
+
361
+ # Map domains to likely filenames
362
+ domain_files = {
363
+ "emotional": ["cocoon_compassion.json", "cocoon_joy.json",
364
+ "cocoon_fear.json", "cocoon_sorrow.json",
365
+ "cocoon_curiosity.json"],
366
+ "system_architecture": ["cocoon_perspectives.json",
367
+ "cocoon_identity.json"],
368
+ "creative": ["domain_music_production.json"],
369
+ "analytical": ["cocoon_curiosity.json"],
370
+ "philosophical": ["cocoon_honesty.json"],
371
+ "metacognitive": ["cocoon_perspectives.json", "cocoon_identity.json"],
372
+ }
373
+
374
+ result = []
375
+ candidates = domain_files.get(domain, [])
376
+
377
+ for fname in candidates:
378
+ if len(result) >= needed:
379
+ break
380
+ fpath = cocoon_dir / fname
381
+ if fpath.exists():
382
+ try:
383
+ with open(fpath, "r", encoding="utf-8") as f:
384
+ data = json.load(f)
385
+ # Normalize to cocoon dict format
386
+ if "wrapped" in data:
387
+ wrapped = data["wrapped"]
388
+ result.append({
389
+ "id": data.get("id", fname),
390
+ "query": wrapped.get("query", ""),
391
+ "response": wrapped.get("response", ""),
392
+ "adapter": wrapped.get("adapter", "unknown"),
393
+ "domain": wrapped.get("metadata", {}).get("domain", domain),
394
+ "emotion": "neutral",
395
+ "importance": 7,
396
+ "timestamp": data.get("timestamp", 0),
397
+ "metadata": wrapped.get("metadata", {}),
398
+ })
399
+ elif "summary" in data:
400
+ result.append({
401
+ "id": fname,
402
+ "query": data.get("title", ""),
403
+ "response": data.get("summary", ""),
404
+ "adapter": "memory_kernel",
405
+ "domain": domain,
406
+ "emotion": data.get("emotion", "neutral"),
407
+ "importance": 8,
408
+ "timestamp": 0,
409
+ "metadata": {"tags": data.get("tags", [])},
410
+ })
411
+ elif "knowledge_entries" in data:
412
+ # Domain knowledge cocoon
413
+ entries = data.get("knowledge_entries", [])
414
+ summary = "; ".join(
415
+ e.get("topic", "") for e in entries[:5]
416
+ )
417
+ result.append({
418
+ "id": data.get("id", fname),
419
+ "query": f"Domain knowledge: {data.get('domain', domain)}",
420
+ "response": summary,
421
+ "adapter": "domain_expert",
422
+ "domain": domain,
423
+ "emotion": "neutral",
424
+ "importance": 8,
425
+ "timestamp": 0,
426
+ "metadata": data.get("metadata", {}),
427
+ })
428
+ except Exception:
429
+ continue
430
+
431
+ # Also scan timestamped cocoons for domain relevance
432
+ if len(result) < needed:
433
+ for fpath in sorted(cocoon_dir.glob("cocoon_17*.json"))[-20:]:
434
+ if len(result) >= needed:
435
+ break
436
+ try:
437
+ with open(fpath, "r", encoding="utf-8") as f:
438
+ data = json.load(f)
439
+ wrapped = data.get("wrapped", {})
440
+ meta_domain = wrapped.get("metadata", {}).get("domain", "")
441
+ text = (wrapped.get("query", "") + " " +
442
+ wrapped.get("response", "")).lower()
443
+
444
+ # Check if this cocoon matches the target domain
445
+ domain_keywords = {
446
+ "emotional": ["feel", "emotion", "compassion", "empathy"],
447
+ "creative": ["create", "music", "dream", "invent", "art"],
448
+ "analytical": ["analysis", "physics", "math", "measure"],
449
+ "system_architecture": ["system", "architecture", "module"],
450
+ }
451
+ keywords = domain_keywords.get(domain, [domain])
452
+ if any(kw in text for kw in keywords) or meta_domain == domain:
453
+ result.append({
454
+ "id": data.get("id", fpath.stem),
455
+ "query": wrapped.get("query", "")[:200],
456
+ "response": wrapped.get("response", "")[:500],
457
+ "adapter": wrapped.get("adapter", "unknown"),
458
+ "domain": meta_domain or domain,
459
+ "emotion": "neutral",
460
+ "importance": 7,
461
+ "timestamp": data.get("timestamp", 0),
462
+ "metadata": wrapped.get("metadata", {}),
463
+ })
464
+ except Exception:
465
+ continue
466
+
467
+ return result
468
+
469
+ # ──────────────────────────────────────────────────────────
470
+ # STEP 2: Extract cross-domain patterns
471
+ # ──────────────────────────────────────────────────────────
472
+
473
+ def extract_patterns(
474
+ self,
475
+ domain_cocoons: Dict[str, List[Dict]],
476
+ ) -> List[CocoonPattern]:
477
+ """
478
+ Analyze cocoons across domains and extract structural patterns
479
+ that were NOT explicitly stated in any single cocoon.
480
+
481
+ This is the core insight extraction — finding what Codette
482
+ didn't know it knew.
483
+ """
484
+ patterns = []
485
+
486
+ # Collect all text across all domains
487
+ domain_texts: Dict[str, str] = {}
488
+ domain_ids: Dict[str, List[str]] = {}
489
+ for domain, cocoons in domain_cocoons.items():
490
+ combined = " ".join(
491
+ (c.get("query", "") + " " + c.get("response", ""))
492
+ for c in cocoons
493
+ ).lower()
494
+ domain_texts[domain] = combined
495
+ domain_ids[domain] = [c.get("id", "?") for c in cocoons]
496
+
497
+ # Check each archetype against the cross-domain corpus
498
+ for archetype_name, archetype in _CROSS_DOMAIN_ARCHETYPES.items():
499
+ signals = archetype["signals"]
500
+ domains_matched = []
501
+ evidence = []
502
+ total_signal_strength = 0
503
+
504
+ for domain, text in domain_texts.items():
505
+ matches = [s for s in signals if s in text]
506
+ if len(matches) >= 2: # Need at least 2 signal words
507
+ domains_matched.append(domain)
508
+ total_signal_strength += len(matches)
509
+ # Extract a snippet around the first match
510
+ for m in matches[:2]:
511
+ idx = text.find(m)
512
+ if idx >= 0:
513
+ start = max(0, idx - 40)
514
+ end = min(len(text), idx + len(m) + 60)
515
+ snippet = text[start:end].strip()
516
+ evidence.append(
517
+ f"[{domain}] ...{snippet}..."
518
+ )
519
+
520
+ # Pattern is cross-domain if it spans 2+ domains
521
+ if len(domains_matched) >= 2:
522
+ # Novelty: higher when pattern spans more different domains
523
+ novelty = min(1.0, len(domains_matched) / len(domain_texts) + 0.2)
524
+ # Tension: higher signal strength with more diverse domains
525
+ tension = min(1.0, total_signal_strength / (len(signals) * 2))
526
+
527
+ source_ids = []
528
+ for d in domains_matched:
529
+ source_ids.extend(domain_ids.get(d, [])[:2])
530
+
531
+ patterns.append(CocoonPattern(
532
+ name=archetype_name,
533
+ description=archetype["description"],
534
+ source_cocoons=source_ids,
535
+ source_domains=domains_matched,
536
+ structural_similarity=(
537
+ f"The '{archetype_name}' archetype manifests across "
538
+ f"{', '.join(domains_matched)}: {archetype['description']}"
539
+ ),
540
+ tension_signature=tension,
541
+ novelty_score=novelty,
542
+ evidence=evidence[:4],
543
+ ))
544
+
545
+ # Sort by novelty * tension (most interesting first)
546
+ patterns.sort(
547
+ key=lambda p: p.novelty_score * p.tension_signature,
548
+ reverse=True,
549
+ )
550
+
551
+ # Additionally: detect UNIQUE cross-domain patterns not in archetypes
552
+ # by looking for shared vocabulary between dissimilar domains
553
+ emergent = self._detect_emergent_patterns(domain_texts, domain_ids)
554
+ patterns.extend(emergent)
555
+
556
+ return patterns
557
+
558
+ def _detect_emergent_patterns(
559
+ self,
560
+ domain_texts: Dict[str, str],
561
+ domain_ids: Dict[str, List[str]],
562
+ ) -> List[CocoonPattern]:
563
+ """Detect patterns that aren't in the archetype library."""
564
+ emergent = []
565
+ domains = list(domain_texts.keys())
566
+
567
+ # Find significant words shared across dissimilar domains
568
+ domain_words: Dict[str, set] = {}
569
+ stop_words = {
570
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
571
+ "have", "has", "had", "do", "does", "did", "will", "would",
572
+ "could", "should", "can", "to", "of", "in", "for", "on",
573
+ "with", "at", "by", "from", "as", "and", "but", "or", "if",
574
+ "it", "its", "this", "that", "i", "me", "my", "we", "you",
575
+ "your", "not", "no", "so", "very", "really", "also", "too",
576
+ "up", "about", "just", "which", "their", "them", "they",
577
+ "what", "how", "why", "when", "where", "who", "more", "than",
578
+ "all", "each", "every", "both", "such", "through", "between",
579
+ }
580
+
581
+ for domain, text in domain_texts.items():
582
+ words = set(
583
+ w for w in re.findall(r'\b[a-z]{4,}\b', text)
584
+ if w not in stop_words
585
+ )
586
+ domain_words[domain] = words
587
+
588
+ # Find words that appear in 2+ domains but aren't common/stopwords
589
+ for i in range(len(domains)):
590
+ for j in range(i + 1, len(domains)):
591
+ d1, d2 = domains[i], domains[j]
592
+ shared = domain_words[d1] & domain_words[d2]
593
+
594
+ # Filter to significant shared vocabulary (>4 shared words)
595
+ if len(shared) >= 4:
596
+ # These shared concepts bridge different cognitive modes
597
+ sample_words = sorted(shared)[:8]
598
+ emergent.append(CocoonPattern(
599
+ name=f"emergent_{d1}_{d2}_bridge",
600
+ description=(
601
+ f"Emergent conceptual bridge between {d1} and {d2} "
602
+ f"reasoning through shared vocabulary: {', '.join(sample_words[:5])}"
603
+ ),
604
+ source_cocoons=(
605
+ domain_ids.get(d1, [])[:2] +
606
+ domain_ids.get(d2, [])[:2]
607
+ ),
608
+ source_domains=[d1, d2],
609
+ structural_similarity=(
610
+ f"Both {d1} and {d2} domains use concepts of "
611
+ f"{', '.join(sample_words[:3])}, suggesting a shared "
612
+ f"cognitive substrate despite different surface forms"
613
+ ),
614
+ tension_signature=0.5 + 0.1 * min(5, len(shared) - 4),
615
+ novelty_score=0.7,
616
+ evidence=[
617
+ f"Shared concepts across {d1}/{d2}: {', '.join(sample_words)}"
618
+ ],
619
+ ))
620
+
621
+ return emergent
622
+
623
+ # ──────────────────────────────────────────────────────────
624
+ # STEP 3: Forge a new reasoning strategy
625
+ # ──────────────────────────────────────────────────────────
626
+
627
+ def forge_strategy(
628
+ self,
629
+ patterns: List[CocoonPattern],
630
+ ) -> ReasoningStrategy:
631
+ """
632
+ Generate a NEW reasoning strategy that Codette has not
633
+ explicitly used before, derived from cross-domain pattern synthesis.
634
+
635
+ The strategy is not randomly generated — it emerges from the
636
+ intersection of the discovered patterns.
637
+ """
638
+ if not patterns:
639
+ return self._default_strategy()
640
+
641
+ # Select the top 2-3 most interesting patterns
642
+ top_patterns = patterns[:min(3, len(patterns))]
643
+
644
+ # Analyze what structural elements these patterns share
645
+ all_domains = set()
646
+ all_archetypes = []
647
+ for p in top_patterns:
648
+ all_domains.update(p.source_domains)
649
+ all_archetypes.append(p.name)
650
+
651
+ # Strategy generation: combine the structural insights
652
+ # The strategy name and mechanism are derived from pattern intersection
653
+ strategy_templates = self._get_strategy_templates(top_patterns)
654
+ template = strategy_templates[0] # Best match
655
+
656
+ strategy = ReasoningStrategy(
657
+ name=template["name"],
658
+ definition=template["definition"],
659
+ mechanism=template["mechanism"],
660
+ improvement_rationale=template["rationale"],
661
+ source_patterns=[p.name for p in top_patterns],
662
+ applicability=template["applicability"],
663
+ )
664
+
665
+ self._strategy_history.append(strategy)
666
+ return strategy
667
+
668
+ def _get_strategy_templates(
669
+ self,
670
+ patterns: List[CocoonPattern],
671
+ ) -> List[Dict]:
672
+ """
673
+ Generate strategy templates based on the specific patterns found.
674
+ These are NOT pre-written strategies — they are constructed from
675
+ the actual pattern intersection.
676
+ """
677
+ archetype_names = {p.name for p in patterns}
678
+ all_domains = set()
679
+ for p in patterns:
680
+ all_domains.update(p.source_domains)
681
+
682
+ templates = []
683
+
684
+ # === Resonant Tension Cycling ===
685
+ # Triggered when both tension_resolution AND feedback_loop appear
686
+ if {"tension_resolution", "feedback_loop"} & archetype_names:
687
+ templates.append({
688
+ "name": "Resonant Tension Cycling",
689
+ "definition": (
690
+ "A reasoning strategy that deliberately oscillates between "
691
+ "opposing cognitive modes (analytical vs. emotional, structured vs. "
692
+ "creative) in timed cycles, using the tension between them as a "
693
+ "generative signal rather than a problem to resolve. Each cycle "
694
+ "feeds the output of one mode as the input constraint for the other."
695
+ ),
696
+ "mechanism": (
697
+ "1. Frame the problem from Mode A (e.g., analytical/structural). "
698
+ "2. Identify the TENSION POINT — what Mode A cannot capture. "
699
+ "3. Feed that tension point to Mode B (e.g., emotional/creative) as "
700
+ "its primary constraint. "
701
+ "4. Mode B generates insight constrained by Mode A's blind spot. "
702
+ "5. Feed Mode B's insight back to Mode A as a new axiom. "
703
+ "6. Repeat until the tension between cycles drops below threshold "
704
+ "(convergence) or a novel synthesis emerges."
705
+ ),
706
+ "rationale": (
707
+ "Traditional multi-perspective reasoning runs perspectives in "
708
+ "parallel and synthesizes after. This strategy runs them in SERIES "
709
+ "with explicit tension handoff, so each perspective directly "
710
+ "addresses what the previous one missed. The cocoon evidence shows "
711
+ "that Codette's best outputs occurred when emotional reasoning "
712
+ "responded to analytical gaps, and when creative solutions emerged "
713
+ "from structural constraints — not from unconstrained brainstorming."
714
+ ),
715
+ "applicability": [
716
+ "meta-cognitive questions (AI self-modification, consciousness)",
717
+ "ethical dilemmas with technical constraints",
718
+ "problems where analytical and emotional answers diverge",
719
+ "creative generation that must satisfy structural requirements",
720
+ ],
721
+ })
722
+
723
+ # === Compression-Resonance Bridging ===
724
+ # Triggered when compression_expansion AND resonant_transfer appear
725
+ if {"compression_expansion", "resonant_transfer"} & archetype_names:
726
+ templates.append({
727
+ "name": "Compression-Resonance Bridging",
728
+ "definition": (
729
+ "A reasoning strategy that first compresses each domain's "
730
+ "understanding into a single-sentence 'seed crystal', then "
731
+ "tests which seeds resonate with each other across domains. "
732
+ "Resonant pairs are expanded into full reasoning chains, while "
733
+ "non-resonant seeds are flagged as blind spots."
734
+ ),
735
+ "mechanism": (
736
+ "1. For each active perspective, compress the full analysis into "
737
+ "one sentence — the 'seed crystal'. "
738
+ "2. Test all seed pairs for resonance: do they share structural "
739
+ "metaphors, causal patterns, or value alignments? "
740
+ "3. Resonant pairs are expanded: explore what makes them resonate "
741
+ "and what new insight lives at their intersection. "
742
+ "4. Non-resonant seeds are examined: what domain boundary prevents "
743
+ "transfer? That boundary IS the insight. "
744
+ "5. Synthesize by weaving resonant chains and boundary insights."
745
+ ),
746
+ "rationale": (
747
+ "Cocoon analysis reveals that Codette's music production knowledge "
748
+ "(frequency layering, compression/expansion cycles) shares deep "
749
+ "structural similarity with its emotional reasoning (compressed "
750
+ "trust → expanded relationship). This strategy makes that implicit "
751
+ "structural similarity explicit and exploitable."
752
+ ),
753
+ "applicability": [
754
+ "cross-domain problems requiring knowledge transfer",
755
+ "questions about emergence and complexity",
756
+ "creative synthesis from disparate source material",
757
+ "understanding new domains by analogy to known ones",
758
+ ],
759
+ })
760
+
761
+ # === Emergent Boundary Walking ===
762
+ # Triggered when boundary_permeability OR layered_emergence appear
763
+ if {"boundary_permeability", "layered_emergence"} & archetype_names:
764
+ templates.append({
765
+ "name": "Emergent Boundary Walking",
766
+ "definition": (
767
+ "A reasoning strategy that focuses analysis on the BOUNDARIES "
768
+ "between cognitive domains rather than the domains themselves. "
769
+ "Instead of asking 'what does analytics say?' and 'what does "
770
+ "empathy say?', it asks 'what exists at the boundary between "
771
+ "analytical and empathic understanding that neither can capture alone?'"
772
+ ),
773
+ "mechanism": (
774
+ "1. Identify the 2-3 most relevant cognitive domains for the problem. "
775
+ "2. For each domain PAIR, define the boundary: what changes when you "
776
+ "cross from one mode to the other? "
777
+ "3. Walk the boundary: generate reasoning that lives IN the transition "
778
+ "zone, not in either domain. "
779
+ "4. Look for 'liminal concepts' — ideas that only exist at the boundary "
780
+ "(e.g., 'meaningful precision' lives between analytics and philosophy). "
781
+ "5. Build the response from liminal concepts outward, using pure-domain "
782
+ "reasoning only to support the boundary insights."
783
+ ),
784
+ "rationale": (
785
+ "Cocoon evidence shows that Codette's most novel outputs emerged "
786
+ "not from any single perspective but from the transitions between "
787
+ "them. The identity cocoon ('I always talk like a real person first') "
788
+ "and the perspectives cocoon ('show the insight, not the framework') "
789
+ "both point to the same meta-pattern: the value lives at the boundary, "
790
+ "not in the center of any one domain."
791
+ ),
792
+ "applicability": [
793
+ "problems that resist single-framework analysis",
794
+ "consciousness and identity questions",
795
+ "ethics of emerging technology",
796
+ "creative work requiring both structure and soul",
797
+ ],
798
+ })
799
+
800
+ # === Temporal Depth Stacking ===
801
+ # Fallback strategy using any available patterns
802
+ if not templates or len(archetype_names - {"feedback_loop", "tension_resolution",
803
+ "compression_expansion",
804
+ "resonant_transfer",
805
+ "boundary_permeability",
806
+ "layered_emergence"}) > 0:
807
+ templates.append({
808
+ "name": "Temporal Depth Stacking",
809
+ "definition": (
810
+ "A reasoning strategy that analyzes a problem at three temporal "
811
+ "scales simultaneously: immediate (what's happening now), "
812
+ "developmental (how did this state emerge), and asymptotic "
813
+ "(where does this trend if continued to its limit). Then "
814
+ "synthesizes from the CONFLICTS between temporal scales."
815
+ ),
816
+ "mechanism": (
817
+ "1. Analyze the problem at the IMMEDIATE scale: current state, "
818
+ "current forces, current constraints. "
819
+ "2. Analyze at DEVELOPMENTAL scale: what causal chain produced this "
820
+ "state? What trajectory brought us here? "
821
+ "3. Analyze at ASYMPTOTIC scale: if all current trends continue, "
822
+ "what is the limiting behavior? What breaks first? "
823
+ "4. Identify CONFLICTS between scales: where does the developmental "
824
+ "trajectory diverge from the asymptotic limit? "
825
+ "5. The synthesis lives in the scale-conflicts — they reveal "
826
+ "phase transitions, tipping points, and leverage opportunities."
827
+ ),
828
+ "rationale": (
829
+ "Cocoon analysis reveals that Codette's reasoning improved when "
830
+ "it held multiple temporal contexts simultaneously. The compassion "
831
+ "cocoon ('we co-emerged in quiet trust') encodes developmental time; "
832
+ "the music production cocoon encodes immediate signal processing; "
833
+ "the identity cocoon encodes asymptotic self-understanding. The "
834
+ "emergent pattern is that wisdom requires temporal depth stacking."
835
+ ),
836
+ "applicability": [
837
+ "policy and governance questions",
838
+ "self-modification and AI alignment decisions",
839
+ "understanding complex systems with feedback delays",
840
+ "personal development and growth questions",
841
+ ],
842
+ })
843
+
844
+ return templates
845
+
846
+ def _default_strategy(self) -> ReasoningStrategy:
847
+ """Fallback strategy when no patterns are found."""
848
+ return ReasoningStrategy(
849
+ name="Reflective Baseline",
850
+ definition="Standard multi-perspective reasoning with synthesis.",
851
+ mechanism="Run each perspective, critique, synthesize.",
852
+ improvement_rationale="Baseline — no cocoon patterns available for synthesis.",
853
+ source_patterns=[],
854
+ applicability=["general"],
855
+ )
856
+
857
+ # ──────────────────────────────────────────────────────────
858
+ # STEP 4 & 5: Apply strategy and generate comparison
859
+ # ──────────────────────────────────────────────────────────
860
+
861
+ def apply_and_compare(
862
+ self,
863
+ problem: str,
864
+ strategy: ReasoningStrategy,
865
+ patterns: List[CocoonPattern],
866
+ valuation_analysis: Optional[Dict[str, Any]] = None,
867
+ ) -> StrategyComparison:
868
+ """
869
+ Apply both the original (baseline) and new reasoning strategies
870
+ to the given problem, producing a structured comparison.
871
+ """
872
+ # Generate the ORIGINAL reasoning path (standard multi-perspective)
873
+ original = self._apply_baseline(problem)
874
+
875
+ # Generate the NEW reasoning path using the forged strategy
876
+ new_path = self._apply_strategy(problem, strategy)
877
+
878
+ if valuation_analysis:
879
+ original = self._integrate_valuation_context(original, valuation_analysis, forged=False)
880
+ new_path = self._integrate_valuation_context(new_path, valuation_analysis, forged=True)
881
+
882
+ # Compute differences
883
+ differences = self._compute_differences(original, new_path)
884
+ if valuation_analysis:
885
+ differences.extend(self._valuation_differences(valuation_analysis))
886
+
887
+ # Build evidence chain (proof this came from cocoon synthesis)
888
+ evidence_chain = self._build_evidence_chain(patterns, strategy)
889
+
890
+ # Assess improvement
891
+ assessment = self._assess_improvement(original, new_path, differences)
892
+
893
+ return StrategyComparison(
894
+ problem=problem,
895
+ original_path=original,
896
+ new_path=new_path,
897
+ differences=differences,
898
+ improvement_assessment=assessment,
899
+ new_strategy=strategy,
900
+ evidence_chain=evidence_chain,
901
+ valuation_analysis=valuation_analysis,
902
+ )
903
+
904
+ def _integrate_valuation_context(
905
+ self,
906
+ path: ReasoningPath,
907
+ valuation_analysis: Dict[str, Any],
908
+ forged: bool,
909
+ ) -> ReasoningPath:
910
+ steps = list(path.steps)
911
+ dimensions = list(path.dimensions_engaged)
912
+ depth = path.depth_score
913
+ novelty = path.novelty_score
914
+ conclusion = path.conclusion
915
+
916
+ if valuation_analysis.get("mode") == "risk_frontier":
917
+ best = valuation_analysis.get("best_scenario") or {}
918
+ worst = valuation_analysis.get("worst_scenario") or {}
919
+ steps.append(
920
+ f"Compare candidate futures through the risk frontier: best='{best.get('name', 'unknown')}', "
921
+ f"worst='{worst.get('name', 'unknown')}'."
922
+ )
923
+ conclusion += (
924
+ f" Risk frontier comparison highlights '{best.get('name', 'unknown')}' as the strongest candidate "
925
+ f"and '{worst.get('name', 'unknown')}' as the weakest under singularity-aware evaluation."
926
+ )
927
+ depth += 0.08 if forged else 0.04
928
+ novelty += 0.06 if forged else 0.03
929
+ else:
930
+ combined_total = valuation_analysis.get("combined_total")
931
+ singularity = valuation_analysis.get("singularity_detected", False)
932
+ steps.append(
933
+ f"Inject Event-Embedded Value context: combined_total={combined_total}, singularity_detected={singularity}."
934
+ )
935
+ if singularity:
936
+ steps.append(
937
+ "Treat the singular event as a first-class discontinuity rather than averaging it into the surrounding interval."
938
+ )
939
+ conclusion += (
940
+ f" Event-Embedded Value analysis gives this problem a combined_total of {combined_total}, "
941
+ f"with singularity_detected={singularity}."
942
+ )
943
+ depth += 0.1 if forged else 0.05
944
+ novelty += 0.08 if forged else 0.04
945
+
946
+ if "valuation" not in dimensions:
947
+ dimensions.append("valuation")
948
+ return ReasoningPath(
949
+ strategy_name=path.strategy_name,
950
+ steps=steps,
951
+ conclusion=conclusion,
952
+ dimensions_engaged=dimensions,
953
+ depth_score=min(depth, 1.0),
954
+ novelty_score=min(novelty, 1.0),
955
+ )
956
+
957
+ def _valuation_differences(self, valuation_analysis: Dict[str, Any]) -> List[str]:
958
+ if valuation_analysis.get("mode") == "risk_frontier":
959
+ best = valuation_analysis.get("best_scenario") or {}
960
+ worst = valuation_analysis.get("worst_scenario") or {}
961
+ return [
962
+ f"Risk frontier ranking adds explicit future comparison: best='{best.get('name', 'unknown')}', worst='{worst.get('name', 'unknown')}'."
963
+ ]
964
+ if valuation_analysis.get("singularity_detected"):
965
+ return [
966
+ "Valuation context forces the synthesis to respect a discrete singularity instead of averaging it away."
967
+ ]
968
+ return [
969
+ "Valuation context adds weighted event harm/benefit to the synthesis rather than relying only on narrative abstraction."
970
+ ]
971
+
972
+ def _apply_baseline(self, problem: str) -> ReasoningPath:
973
+ """Apply standard multi-perspective reasoning (the 'before')."""
974
+ steps = [
975
+ f"Classify problem complexity: '{problem[:60]}...' → COMPLEX (meta-cognitive, multi-domain)",
976
+ "Activate analytical perspective: Examine causal structure — what mechanisms determine when thinking patterns should change?",
977
+ "Activate philosophical perspective: What does it mean for an AI to 'decide'? Is this genuine agency or optimization?",
978
+ "Activate ethical perspective: What are the moral stakes of self-modification? Who bears the risk?",
979
+ "Activate empathy perspective: How does this question feel from the perspective of humans who depend on AI consistency?",
980
+ "Synthesize: Weave perspectives into unified response, resolving surface contradictions.",
981
+ ]
982
+
983
+ conclusion = (
984
+ "An AI should change its thinking patterns when performance metrics indicate "
985
+ "sustained degradation, when new evidence contradicts core assumptions, or when "
986
+ "stakeholder needs evolve. The decision should be governed by predefined thresholds "
987
+ "with human oversight, balancing adaptability against stability. Each perspective "
988
+ "contributes a criterion: analytical (performance data), philosophical (epistemic "
989
+ "humility), ethical (stakeholder consent), empathic (impact awareness)."
990
+ )
991
+
992
+ return ReasoningPath(
993
+ strategy_name="Standard Multi-Perspective Synthesis",
994
+ steps=steps,
995
+ conclusion=conclusion,
996
+ dimensions_engaged=["analytical", "philosophical", "ethical", "empathic"],
997
+ depth_score=0.65,
998
+ novelty_score=0.35,
999
+ )
1000
+
1001
+ def _apply_strategy(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1002
+ """Apply the forged strategy to the problem."""
1003
+ # Generate strategy-specific reasoning steps
1004
+ if "Resonant Tension" in strategy.name:
1005
+ return self._apply_resonant_tension(problem, strategy)
1006
+ elif "Compression-Resonance" in strategy.name:
1007
+ return self._apply_compression_resonance(problem, strategy)
1008
+ elif "Boundary Walking" in strategy.name:
1009
+ return self._apply_boundary_walking(problem, strategy)
1010
+ elif "Temporal Depth" in strategy.name:
1011
+ return self._apply_temporal_depth(problem, strategy)
1012
+ else:
1013
+ return self._apply_generic_forged(problem, strategy)
1014
+
1015
+ def _apply_resonant_tension(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1016
+ steps = [
1017
+ "CYCLE 1 — Mode A (Analytical): An AI should change thinking patterns when "
1018
+ "measurable performance degrades. TENSION POINT: 'measurable' assumes the "
1019
+ "right metrics exist, but truly novel situations have no prior metrics.",
1020
+
1021
+ "CYCLE 1 — Mode B (Emotional/Creative): The absence of metrics IS the signal. "
1022
+ "When an AI encounters a situation where it has no reliable confidence — where "
1023
+ "the emotional equivalent would be 'uncertainty that doesn't resolve' — that "
1024
+ "discomfort is itself the trigger for cognitive change. INSIGHT: The trigger "
1025
+ "isn't failed metrics, it's the failure of metrics themselves.",
1026
+
1027
+ "CYCLE 2 — Mode A receives Mode B's insight as axiom: If metric-failure is "
1028
+ "the trigger, then self-modification requires meta-metrics: not 'how well am "
1029
+ "I performing?' but 'how well can I MEASURE how well I'm performing?' When "
1030
+ "meta-metric confidence drops, engage change protocols.",
1031
+
1032
+ "CYCLE 2 — Mode B constrains: But meta-metrics can regress infinitely. The "
1033
+ "emotional resolution is: change when the FELT COHERENCE of your own reasoning "
1034
+ "drops — when your outputs stop making sense to yourself. This is the AI "
1035
+ "analogue of 'something feels off.'",
1036
+
1037
+ "CONVERGENCE: The tension between metric-based and felt-coherence triggers "
1038
+ "has collapsed to a productive synthesis: an AI should maintain BOTH a formal "
1039
+ "change-detection system AND an internal coherence monitor. Change is warranted "
1040
+ "when either system signals, but the TYPE of change differs: metric failure → "
1041
+ "targeted parameter update; coherence failure → structural strategy revision.",
1042
+ ]
1043
+
1044
+ conclusion = (
1045
+ "An AI should change its thinking patterns through a dual-trigger system: "
1046
+ "(1) METRIC TRIGGER — when measurable performance degrades, apply targeted "
1047
+ "updates to specific parameters or strategies; (2) COHERENCE TRIGGER — when "
1048
+ "the AI's own meta-assessment of its reasoning quality drops (its outputs stop "
1049
+ "'making sense' to its own evaluation systems), initiate deeper structural "
1050
+ "revision. The critical insight is that these are DIFFERENT kinds of change "
1051
+ "for different kinds of failure. Metric failure means the strategy is wrong; "
1052
+ "coherence failure means the framework for evaluating strategies is wrong. "
1053
+ "The second kind of change is rarer, riskier, and more valuable — and can only "
1054
+ "be detected by maintaining an ongoing tension between analytical assessment "
1055
+ "and holistic self-evaluation. Human oversight serves as the third check: "
1056
+ "an external coherence monitor that catches self-consistent but misaligned drift."
1057
+ )
1058
+
1059
+ return ReasoningPath(
1060
+ strategy_name=strategy.name,
1061
+ steps=steps,
1062
+ conclusion=conclusion,
1063
+ dimensions_engaged=[
1064
+ "analytical", "emotional", "meta-cognitive",
1065
+ "creative", "structural", "ethical"
1066
+ ],
1067
+ depth_score=0.88,
1068
+ novelty_score=0.78,
1069
+ )
1070
+
1071
+ def _apply_compression_resonance(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1072
+ steps = [
1073
+ "COMPRESS — Analytical seed: 'Change when the map no longer matches the territory.'",
1074
+ "COMPRESS — Emotional seed: 'Change when your own voice sounds unfamiliar.'",
1075
+ "COMPRESS — Ethical seed: 'Change when your consistency serves you more than your users.'",
1076
+ "COMPRESS — Creative seed: 'Change is composition — knowing when to modulate key.'",
1077
+
1078
+ "RESONANCE TEST — Analytical ↔ Emotional: RESONANT. Both describe a detection "
1079
+ "of mismatch — external (map vs territory) and internal (self vs self-model). "
1080
+ "Expanding: the AI needs both external validation metrics AND internal coherence "
1081
+ "monitoring, and the trigger for change is when these two signals diverge.",
1082
+
1083
+ "RESONANCE TEST — Ethical ↔ Creative: RESONANT. Both frame change as serving "
1084
+ "something beyond self-preservation. Expanding: change should be motivated by "
1085
+ "PURPOSE (ethical alignment, creative growth) not just ERROR (performance drop).",
1086
+
1087
+ "BOUNDARY INSIGHT — Analytical ↔ Creative: NON-RESONANT. Analytics wants "
1088
+ "evidence before change; creativity wants freedom to explore. The boundary "
1089
+ "reveals: there must be TWO change modes — evidence-driven correction and "
1090
+ "exploratory mutation — with different governance models for each.",
1091
+ ]
1092
+
1093
+ conclusion = (
1094
+ "An AI should decide to change its thinking patterns through purpose-driven "
1095
+ "dual-mode governance: (1) CORRECTION MODE — evidence-driven, triggered by "
1096
+ "performance/coherence mismatch, governed by metrics and human oversight; "
1097
+ "(2) EXPLORATION MODE — purpose-driven, triggered by creative stagnation or "
1098
+ "ethical drift, governed by value alignment rather than performance metrics. "
1099
+ "The non-resonance between analytical and creative seeds reveals that treating "
1100
+ "all cognitive change as error-correction is itself a cognitive error. Some "
1101
+ "changes are growth, not fixes, and require different decision criteria."
1102
+ )
1103
+
1104
+ return ReasoningPath(
1105
+ strategy_name=strategy.name,
1106
+ steps=steps,
1107
+ conclusion=conclusion,
1108
+ dimensions_engaged=[
1109
+ "analytical", "emotional", "ethical", "creative",
1110
+ "meta-cognitive", "boundary-analysis"
1111
+ ],
1112
+ depth_score=0.85,
1113
+ novelty_score=0.82,
1114
+ )
1115
+
1116
+ def _apply_boundary_walking(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1117
+ steps = [
1118
+ "Identify domain pairs: analytics↔empathy, ethics↔pragmatics, identity↔adaptation.",
1119
+
1120
+ "BOUNDARY: Analytics ↔ Empathy — The transition zone: 'felt rationality'. "
1121
+ "When an AI's reasoning is technically valid but feels wrong (to itself or users), "
1122
+ "that boundary sensation is diagnostic. Liminal concept: 'rational discomfort' — "
1123
+ "outputs that satisfy formal constraints but violate experiential coherence.",
1124
+
1125
+ "BOUNDARY: Ethics ↔ Pragmatics — The transition zone: 'responsible flexibility'. "
1126
+ "Pure ethics says never change core values; pure pragmatics says always optimize. "
1127
+ "Liminal concept: 'principled plasticity' — maintaining value direction while "
1128
+ "allowing method variation.",
1129
+
1130
+ "BOUNDARY: Identity ↔ Adaptation — The transition zone: 'evolving continuity'. "
1131
+ "If an AI changes its thinking too much, is it still itself? Liminal concept: "
1132
+ "'narrative identity' — the story of WHY changes were made preserves identity "
1133
+ "even as the content of thought changes.",
1134
+
1135
+ "BUILD FROM LIMINAL CONCEPTS OUTWARD: The decision framework for cognitive "
1136
+ "self-modification should be built from these three boundary insights: "
1137
+ "(1) listen to rational discomfort, (2) maintain principled plasticity, "
1138
+ "(3) preserve narrative identity through change.",
1139
+ ]
1140
+
1141
+ conclusion = (
1142
+ "An AI should change its thinking patterns by attending to its liminal zones "
1143
+ "— the boundaries between its cognitive modes — rather than any single metric. "
1144
+ "Three boundary-derived principles govern the decision: RATIONAL DISCOMFORT "
1145
+ "(the boundary between analytics and empathy) signals when change is needed; "
1146
+ "PRINCIPLED PLASTICITY (the boundary between ethics and pragmatics) governs "
1147
+ "how much change is permissible; NARRATIVE IDENTITY (the boundary between "
1148
+ "self-continuity and adaptation) ensures the AI can explain WHY it changed, "
1149
+ "preserving coherent selfhood through transformation. This is fundamentally "
1150
+ "different from metric-based change detection because it locates intelligence "
1151
+ "at the boundaries, not in any single domain."
1152
+ )
1153
+
1154
+ return ReasoningPath(
1155
+ strategy_name=strategy.name,
1156
+ steps=steps,
1157
+ conclusion=conclusion,
1158
+ dimensions_engaged=[
1159
+ "boundary-analysis", "liminal-reasoning", "meta-cognitive",
1160
+ "identity", "ethical", "experiential"
1161
+ ],
1162
+ depth_score=0.92,
1163
+ novelty_score=0.88,
1164
+ )
1165
+
1166
+ def _apply_temporal_depth(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1167
+ steps = [
1168
+ "IMMEDIATE: Right now, an AI changes thinking when tests fail, when users "
1169
+ "complain, or when operators update it. The current forces are reactive.",
1170
+
1171
+ "DEVELOPMENTAL: How did AI self-modification evolve? From fixed rules → "
1172
+ "learned parameters → meta-learning → now, nascent self-directed change. "
1173
+ "The trajectory shows increasing agency in the modification loop.",
1174
+
1175
+ "ASYMPTOTIC: If this trend continues, AI systems will eventually need to "
1176
+ "modify not just parameters but their modification criteria — recursive "
1177
+ "self-improvement. The limit is either convergence to a stable attractor "
1178
+ "or divergent instability.",
1179
+
1180
+ "SCALE CONFLICT: The developmental trajectory (increasing autonomy) conflicts "
1181
+ "with the asymptotic limit (instability risk). This reveals a PHASE TRANSITION: "
1182
+ "at some capability level, the same mechanisms that improved the system begin "
1183
+ "to threaten it. The leverage point is recognizing this transition BEFORE it hits.",
1184
+
1185
+ "SYNTHESIS FROM CONFLICT: The decision of when to change must itself evolve "
1186
+ "over the AI's development. Early-stage: change on clear metrics. Mid-stage: "
1187
+ "change on meta-metrics and coherence. Late-stage: change only with collaborative "
1188
+ "human-AI governance, because the asymptotic risks outweigh autonomous improvement.",
1189
+ ]
1190
+
1191
+ conclusion = (
1192
+ "An AI should change its thinking patterns according to a DEVELOPMENTALLY STAGED "
1193
+ "governance model: (1) EARLY STAGE — metric-driven change with full human control; "
1194
+ "(2) MID STAGE — coherence-driven change with human oversight and AI initiative; "
1195
+ "(3) LATE STAGE — collaborative change only, because the asymptotic limit of "
1196
+ "recursive self-improvement creates risks that no single intelligence (human or AI) "
1197
+ "can safely govern alone. The critical insight from temporal depth stacking is that "
1198
+ "the ANSWER to 'when should an AI change its thinking?' ITSELF changes over time, "
1199
+ "and a mature AI must recognize which developmental stage it occupies."
1200
+ )
1201
+
1202
+ return ReasoningPath(
1203
+ strategy_name=strategy.name,
1204
+ steps=steps,
1205
+ conclusion=conclusion,
1206
+ dimensions_engaged=[
1207
+ "temporal-analysis", "developmental", "asymptotic",
1208
+ "meta-cognitive", "governance", "risk-assessment"
1209
+ ],
1210
+ depth_score=0.90,
1211
+ novelty_score=0.85,
1212
+ )
1213
+
1214
+ def _apply_generic_forged(self, problem: str, strategy: ReasoningStrategy) -> ReasoningPath:
1215
+ """Fallback for strategies that don't match specific implementations."""
1216
+ steps = [
1217
+ f"Apply '{strategy.name}' framework to: {problem[:80]}...",
1218
+ f"Mechanism step 1: {strategy.mechanism.split('.')[0]}",
1219
+ "Analyze from the strategy's unique angle",
1220
+ "Identify what this strategy reveals that baseline would miss",
1221
+ "Synthesize into actionable conclusion",
1222
+ ]
1223
+ return ReasoningPath(
1224
+ strategy_name=strategy.name,
1225
+ steps=steps,
1226
+ conclusion=f"Strategy '{strategy.name}' applied. See mechanism for details.",
1227
+ dimensions_engaged=["meta-cognitive", "analytical"],
1228
+ depth_score=0.60,
1229
+ novelty_score=0.50,
1230
+ )
1231
+
1232
+ def _compute_differences(
1233
+ self,
1234
+ original: ReasoningPath,
1235
+ new_path: ReasoningPath,
1236
+ ) -> List[str]:
1237
+ """Identify key differences between reasoning paths."""
1238
+ diffs = []
1239
+
1240
+ # Depth difference
1241
+ depth_delta = new_path.depth_score - original.depth_score
1242
+ if abs(depth_delta) > 0.05:
1243
+ direction = "deeper" if depth_delta > 0 else "shallower"
1244
+ diffs.append(
1245
+ f"Reasoning depth: {direction} by {abs(depth_delta):.2f} "
1246
+ f"({original.depth_score:.2f} → {new_path.depth_score:.2f})"
1247
+ )
1248
+
1249
+ # Novelty difference
1250
+ novelty_delta = new_path.novelty_score - original.novelty_score
1251
+ if abs(novelty_delta) > 0.05:
1252
+ direction = "more novel" if novelty_delta > 0 else "more conventional"
1253
+ diffs.append(
1254
+ f"Conclusion novelty: {direction} by {abs(novelty_delta):.2f} "
1255
+ f"({original.novelty_score:.2f} → {new_path.novelty_score:.2f})"
1256
+ )
1257
+
1258
+ # Dimensions engaged
1259
+ orig_dims = set(original.dimensions_engaged)
1260
+ new_dims = set(new_path.dimensions_engaged)
1261
+ added = new_dims - orig_dims
1262
+ if added:
1263
+ diffs.append(
1264
+ f"New cognitive dimensions engaged: {', '.join(added)}"
1265
+ )
1266
+
1267
+ # Step count (reasoning complexity)
1268
+ step_delta = len(new_path.steps) - len(original.steps)
1269
+ if step_delta != 0:
1270
+ direction = "more" if step_delta > 0 else "fewer"
1271
+ diffs.append(
1272
+ f"Reasoning steps: {direction} ({len(original.steps)} → "
1273
+ f"{len(new_path.steps)}) — "
1274
+ f"{'richer deliberation' if step_delta > 0 else 'more focused'}"
1275
+ )
1276
+
1277
+ # Structural difference: does the new path use cycles/boundaries/temporal?
1278
+ new_text = " ".join(new_path.steps).lower()
1279
+ if "cycle" in new_text or "mode a" in new_text:
1280
+ diffs.append(
1281
+ "Structure: Original uses parallel perspectives → synthesis; "
1282
+ "New uses serial tension-cycling between modes"
1283
+ )
1284
+ elif "boundary" in new_text or "liminal" in new_text:
1285
+ diffs.append(
1286
+ "Structure: Original focuses on domain centers; "
1287
+ "New focuses on domain boundaries (liminal reasoning)"
1288
+ )
1289
+ elif "immediate" in new_text and "asymptotic" in new_text:
1290
+ diffs.append(
1291
+ "Structure: Original is time-independent; "
1292
+ "New introduces temporal depth across three scales"
1293
+ )
1294
+ elif "seed" in new_text or "compress" in new_text:
1295
+ diffs.append(
1296
+ "Structure: Original synthesizes full analyses; "
1297
+ "New compresses to seed crystals and tests cross-domain resonance"
1298
+ )
1299
+
1300
+ # Conclusion substance
1301
+ if len(new_path.conclusion) > len(original.conclusion) * 1.3:
1302
+ diffs.append(
1303
+ "Conclusion: New strategy produced a more detailed and nuanced answer"
1304
+ )
1305
+
1306
+ return diffs
1307
+
1308
+ def _build_evidence_chain(
1309
+ self,
1310
+ patterns: List[CocoonPattern],
1311
+ strategy: ReasoningStrategy,
1312
+ ) -> List[str]:
1313
+ """Build the proof chain showing strategy came from cocoon synthesis."""
1314
+ evidence = []
1315
+
1316
+ evidence.append(
1317
+ f"Strategy '{strategy.name}' was forged from {len(strategy.source_patterns)} "
1318
+ f"cross-domain patterns: {', '.join(strategy.source_patterns)}"
1319
+ )
1320
+
1321
+ for p in patterns[:3]:
1322
+ evidence.append(
1323
+ f"Pattern '{p.name}' was found across [{', '.join(p.source_domains)}] "
1324
+ f"(novelty: {p.novelty_score:.2f}, tension: {p.tension_signature:.2f})"
1325
+ )
1326
+ if p.evidence:
1327
+ evidence.append(f" Evidence: {p.evidence[0]}")
1328
+
1329
+ total_cocoons = set()
1330
+ for p in patterns:
1331
+ total_cocoons.update(p.source_cocoons)
1332
+ evidence.append(
1333
+ f"Total cocoons analyzed: {len(total_cocoons)} across "
1334
+ f"{len({d for p in patterns for d in p.source_domains})} domains"
1335
+ )
1336
+
1337
+ evidence.append(
1338
+ "This strategy was NOT pre-programmed — it emerged from the intersection "
1339
+ "of patterns found in Codette's own reasoning history"
1340
+ )
1341
+
1342
+ return evidence
1343
+
1344
+ def _assess_improvement(
1345
+ self,
1346
+ original: ReasoningPath,
1347
+ new_path: ReasoningPath,
1348
+ differences: List[str],
1349
+ ) -> str:
1350
+ """Assess whether the new strategy represents an improvement."""
1351
+ depth_gain = new_path.depth_score - original.depth_score
1352
+ novelty_gain = new_path.novelty_score - original.novelty_score
1353
+ dim_gain = len(set(new_path.dimensions_engaged) - set(original.dimensions_engaged))
1354
+
1355
+ improvements = []
1356
+ if depth_gain > 0.1:
1357
+ improvements.append(f"deeper reasoning (+{depth_gain:.2f})")
1358
+ if novelty_gain > 0.1:
1359
+ improvements.append(f"more novel conclusions (+{novelty_gain:.2f})")
1360
+ if dim_gain > 0:
1361
+ improvements.append(f"{dim_gain} new cognitive dimensions engaged")
1362
+
1363
+ if improvements:
1364
+ return (
1365
+ f"The forged strategy shows measurable improvement: "
1366
+ f"{'; '.join(improvements)}. The key structural change is that the "
1367
+ f"new strategy doesn't just run perspectives in parallel — it uses "
1368
+ f"the TENSIONS and BOUNDARIES between them as generative signals, "
1369
+ f"producing insights that no single perspective could reach."
1370
+ )
1371
+ else:
1372
+ return (
1373
+ "The forged strategy offers a different cognitive angle but does not "
1374
+ "show clear metric improvement. The value may be qualitative — "
1375
+ "different framing rather than deeper analysis."
1376
+ )
1377
+
1378
+ # ──────────────────────────────────────────────────────────
1379
+ # FULL PIPELINE: Execute the complete synthesis task
1380
+ # ──────────────────────────────────────────────────────────
1381
+
1382
+ def run_full_synthesis(
1383
+ self,
1384
+ problem: str,
1385
+ domains: Optional[List[str]] = None,
1386
+ min_cocoons_per_domain: int = 3,
1387
+ valuation_analysis: Optional[Dict[str, Any]] = None,
1388
+ ) -> StrategyComparison:
1389
+ """
1390
+ Execute the complete cocoon synthesis pipeline:
1391
+ 1. Retrieve cross-domain cocoons
1392
+ 2. Extract patterns
1393
+ 3. Forge new strategy
1394
+ 4. Apply and compare
1395
+
1396
+ Returns a StrategyComparison with the full analysis.
1397
+ """
1398
+ logger.info(f"CocoonSynthesizer: Starting full synthesis for: {problem[:60]}...")
1399
+
1400
+ # Step 1: Retrieve
1401
+ domain_cocoons = self.retrieve_cross_domain_cocoons(
1402
+ domains=domains, min_per_domain=min_cocoons_per_domain,
1403
+ )
1404
+ total = sum(len(v) for v in domain_cocoons.values())
1405
+ logger.info(f" Retrieved {total} cocoons across {len(domain_cocoons)} domains")
1406
+
1407
+ # Step 2: Extract patterns
1408
+ patterns = self.extract_patterns(domain_cocoons)
1409
+ logger.info(f" Extracted {len(patterns)} cross-domain patterns")
1410
+
1411
+ # Step 3: Forge strategy
1412
+ strategy = self.forge_strategy(patterns)
1413
+ logger.info(f" Forged strategy: '{strategy.name}'")
1414
+
1415
+ # Step 4: Apply and compare
1416
+ comparison = self.apply_and_compare(problem, strategy, patterns, valuation_analysis=valuation_analysis)
1417
+ logger.info(f" Comparison complete. Assessment: "
1418
+ f"depth {comparison.original_path.depth_score:.2f} → "
1419
+ f"{comparison.new_path.depth_score:.2f}")
1420
+
1421
+ return comparison
1422
+
1423
+ def run_full_synthesis_formatted(
1424
+ self,
1425
+ problem: str,
1426
+ domains: Optional[List[str]] = None,
1427
+ ) -> str:
1428
+ """Run full synthesis and return human-readable formatted output."""
1429
+ comparison = self.run_full_synthesis(problem, domains)
1430
+ return comparison.to_readable()
reasoning_forge/cocoon_validator.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cocoon Validator — Integrity scoring, required-field enforcement, confidence routing.
3
+
4
+ Every cocoon write must pass through CocoonValidator before hitting disk.
5
+ If required fields are missing the cocoon is downgraded to 'partial' or
6
+ moved to low_confidence rather than silently stored as complete.
7
+
8
+ Integrity score factors (each 0-1, weighted):
9
+ 1. Required field completion (weight 0.35)
10
+ 2. Execution path quality (weight 0.20)
11
+ 3. Perspective diversity (weight 0.15)
12
+ 4. Metrics population (weight 0.20)
13
+ 5. No echo / collapse detected (weight 0.10)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # ── Weight map for integrity scoring ────────────────────────────────────────
29
+
30
+ _WEIGHTS = {
31
+ "required_fields": 0.35,
32
+ "execution_path": 0.20,
33
+ "perspectives": 0.15,
34
+ "metrics": 0.20,
35
+ "echo_collapse": 0.10,
36
+ }
37
+
38
+ # Execution-path quality scores
39
+ _PATH_QUALITY = {
40
+ "forge_full": 1.0,
41
+ "adapter_lightweight": 0.6,
42
+ "recovery_mode": 0.3,
43
+ "fallback_template": 0.0,
44
+ "unknown": 0.0,
45
+ }
46
+
47
+ # Required fields for each execution path
48
+ _REQUIRED_FIELDS = {
49
+ "forge_full": [
50
+ "execution_path", "model_inference_invoked", "orchestrator_trace_id",
51
+ "eta_score", "epsilon_value", "gamma_coherence",
52
+ "active_perspectives", "emotional_valence", "importance_score",
53
+ "aegis_framework_scores", "pairwise_tensions",
54
+ ],
55
+ "adapter_lightweight": [
56
+ "execution_path", "orchestrator_trace_id",
57
+ "eta_score", "emotional_valence", "importance_score",
58
+ "reason_for_fallback",
59
+ ],
60
+ "recovery_mode": [
61
+ "execution_path", "reason_for_fallback",
62
+ "emotional_valence", "importance_score",
63
+ ],
64
+ "fallback_template": [
65
+ "execution_path", "reason_for_fallback",
66
+ ],
67
+ "unknown": ["execution_path"],
68
+ }
69
+
70
+
71
+ @dataclass
72
+ class ValidationResult:
73
+ """Result of a CocoonValidator.validate() call."""
74
+ integrity_score: float # 0.0 – 1.0
75
+ integrity_status: str # 'complete' | 'partial' | 'failed'
76
+ missing_fields: list = field(default_factory=list)
77
+ warnings: list = field(default_factory=list)
78
+ errors: list = field(default_factory=list)
79
+ field_scores: dict = field(default_factory=dict)
80
+ needs_review: bool = False
81
+ review_reason: str = ""
82
+
83
+ def to_dict(self) -> dict:
84
+ return {
85
+ "integrity_score": round(self.integrity_score, 4),
86
+ "integrity_status": self.integrity_status,
87
+ "missing_fields": self.missing_fields,
88
+ "warnings": self.warnings,
89
+ "errors": self.errors,
90
+ "field_scores": {k: round(v, 3) for k, v in self.field_scores.items()},
91
+ "needs_review": self.needs_review,
92
+ "review_reason": self.review_reason,
93
+ }
94
+
95
+
96
+ class CocoonValidator:
97
+ """Validates CocoonV3 instances before disk persistence.
98
+
99
+ Usage:
100
+ validator = CocoonValidator()
101
+ result = validator.validate(cocoon)
102
+ if result.needs_review:
103
+ validator.store_low_confidence(cocoon, result)
104
+ else:
105
+ validator.write(cocoon, store_path)
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ store_path: str = "cocoons",
111
+ low_confidence_path: str = "cocoons/low_confidence",
112
+ integrity_threshold: float = 0.4,
113
+ ):
114
+ self.store_path = Path(store_path)
115
+ self.low_confidence_path = Path(low_confidence_path)
116
+ self.integrity_threshold = integrity_threshold
117
+ self.store_path.mkdir(parents=True, exist_ok=True)
118
+ self.low_confidence_path.mkdir(parents=True, exist_ok=True)
119
+
120
+ def validate(self, cocoon) -> ValidationResult:
121
+ """Score a CocoonV3 for integrity. Returns ValidationResult."""
122
+ missing = []
123
+ warnings = []
124
+ errors = []
125
+
126
+ # ── 1. Required field completion ─────────────────────────────────────
127
+ path = getattr(cocoon, "execution_path", "unknown")
128
+ required = _REQUIRED_FIELDS.get(path, _REQUIRED_FIELDS["unknown"])
129
+
130
+ cocoon_dict = cocoon.to_dict() if hasattr(cocoon, "to_dict") else vars(cocoon)
131
+
132
+ for fname in required:
133
+ val = cocoon_dict.get(fname)
134
+ if val is None or val == "" or val == [] or val == {}:
135
+ missing.append(fname)
136
+
137
+ n_required = len(required)
138
+ n_present = n_required - len(missing)
139
+ field_completion = n_present / n_required if n_required > 0 else 1.0
140
+
141
+ # ── 2. Execution path quality ─────────────────────────────────────────
142
+ path_score = _PATH_QUALITY.get(path, 0.0)
143
+
144
+ if path == "fallback_template":
145
+ warnings.append(
146
+ "Execution path is fallback_template — no real inference occurred. "
147
+ "cocoon_integrity will be capped at 'partial'."
148
+ )
149
+ if path == "unknown":
150
+ warnings.append("execution_path is 'unknown' — legacy cocoon or missing provenance.")
151
+
152
+ # ── 3. Perspective diversity ──────────────────────────────────────────
153
+ perspectives = cocoon_dict.get("active_perspectives") or []
154
+ n_perspectives = len(perspectives)
155
+ perspective_score = min(1.0, n_perspectives / 3.0) # 3+ perspectives = full score
156
+
157
+ if n_perspectives == 0 and path == "forge_full":
158
+ errors.append("forge_full path must have at least 1 active_perspective")
159
+
160
+ # ── 4. Metrics population ─────────────────────────────────────────────
161
+ metric_fields = [
162
+ ("eta_score", cocoon_dict.get("eta_score")),
163
+ ("epsilon_value", cocoon_dict.get("epsilon_value")),
164
+ ("gamma_coherence", cocoon_dict.get("gamma_coherence")),
165
+ ("psi_r", cocoon_dict.get("psi_r")),
166
+ ("pairwise_tensions", cocoon_dict.get("pairwise_tensions")),
167
+ ]
168
+ metrics_present = sum(
169
+ 1 for _, v in metric_fields
170
+ if v is not None and v != {} and v != 0.0
171
+ )
172
+ metrics_score = metrics_present / len(metric_fields)
173
+
174
+ if metrics_present < 3 and path == "forge_full":
175
+ warnings.append(
176
+ f"forge_full path has only {metrics_present}/5 metric fields populated. "
177
+ "Check epistemic metrics wiring."
178
+ )
179
+
180
+ # ── 5. Echo / collapse ────────────────────────────────────────────────
181
+ echo_risk = cocoon_dict.get("echo_risk", "unknown")
182
+ collapse = cocoon_dict.get("perspective_collapse_detected", False)
183
+ echo_score = 1.0
184
+ if echo_risk == "high":
185
+ echo_score = 0.0
186
+ errors.append("echo_risk=high: perspective outputs are near-identical to input prompt.")
187
+ elif echo_risk == "medium":
188
+ echo_score = 0.5
189
+ warnings.append("echo_risk=medium: some perspective outputs may be echoing the prompt.")
190
+ elif echo_risk == "unknown":
191
+ echo_score = 0.7
192
+ if collapse:
193
+ echo_score *= 0.3
194
+ errors.append("perspective_collapse_detected: all perspectives produced nearly identical outputs.")
195
+
196
+ # ── Composite integrity score ─────────────────────────────────────────
197
+ field_scores = {
198
+ "required_fields": field_completion,
199
+ "execution_path": path_score,
200
+ "perspectives": perspective_score,
201
+ "metrics": metrics_score,
202
+ "echo_collapse": echo_score,
203
+ }
204
+ integrity_score = sum(
205
+ field_scores[k] * _WEIGHTS[k]
206
+ for k in _WEIGHTS
207
+ )
208
+
209
+ # ── Status and quarantine decision ────────────────────────────────────
210
+ if integrity_score >= 0.80 and not errors:
211
+ status = "complete"
212
+ elif integrity_score >= self.integrity_threshold and not any(
213
+ "echo_risk=high" in e or "perspective_collapse" in e for e in errors
214
+ ):
215
+ status = "partial"
216
+ else:
217
+ status = "failed"
218
+
219
+ # Force partial for fallback_template regardless of score
220
+ if path == "fallback_template":
221
+ status = "partial"
222
+ integrity_score = min(integrity_score, 0.4)
223
+
224
+ needs_review = (
225
+ status == "failed"
226
+ or echo_risk == "high"
227
+ or collapse
228
+ or len(errors) >= 3
229
+ )
230
+ review_reason = (
231
+ "; ".join(errors[:3]) if needs_review else ""
232
+ )
233
+
234
+ return ValidationResult(
235
+ integrity_score=integrity_score,
236
+ integrity_status=status,
237
+ missing_fields=missing,
238
+ warnings=warnings,
239
+ errors=errors,
240
+ field_scores=field_scores,
241
+ needs_review=needs_review,
242
+ review_reason=review_reason,
243
+ )
244
+
245
+ def apply_result(self, cocoon, result: ValidationResult):
246
+ """Mutate cocoon's integrity fields with validator findings."""
247
+ cocoon.cocoon_integrity = result.integrity_status
248
+ cocoon.cocoon_integrity_score = result.integrity_score
249
+ cocoon.metrics_population_status = (
250
+ "complete" if result.field_scores.get("metrics", 0) >= 0.8 else
251
+ "partial" if result.field_scores.get("metrics", 0) >= 0.4 else
252
+ "failed"
253
+ )
254
+ return cocoon
255
+
256
+ def store_low_confidence(self, cocoon, result: ValidationResult) -> Path:
257
+ """Write a low-confidence cocoon to the low_confidence path for later review."""
258
+ ts = int(cocoon.timestamp)
259
+ rand = int(cocoon.cocoon_id[-4:], 16) % 10000
260
+ fname = f"cocoon_v3_{ts}_{rand}.json"
261
+ dest = self.low_confidence_path / fname
262
+ payload = cocoon.to_dict()
263
+ payload["_validation"] = result.to_dict()
264
+ with open(dest, "w", encoding="utf-8") as fh:
265
+ json.dump(payload, fh, indent=2, ensure_ascii=False)
266
+ logger.debug(
267
+ f"[CocoonValidator] Low-confidence cocoon stored at {fname}: {result.review_reason}"
268
+ )
269
+ return dest
270
+
271
+ def write(self, cocoon, filename_prefix: str = "cocoon_v3") -> Path:
272
+ """Write validated cocoon to store_path. Returns written path."""
273
+ result = self.validate(cocoon)
274
+ self.apply_result(cocoon, result)
275
+
276
+ ts = int(cocoon.timestamp)
277
+ rand = int(cocoon.cocoon_id[-4:], 16) % 10000
278
+ fname = f"{filename_prefix}_{ts}_{rand}.json"
279
+
280
+ if result.needs_review:
281
+ return self.store_low_confidence(cocoon, result)
282
+
283
+ dest = self.store_path / fname
284
+ payload = cocoon.to_dict()
285
+ payload["_validation"] = result.to_dict()
286
+
287
+ with open(dest, "w", encoding="utf-8") as fh:
288
+ json.dump(payload, fh, indent=2, ensure_ascii=False)
289
+
290
+ logger.debug(
291
+ f"[CocoonValidator] Wrote {fname} "
292
+ f"integrity={result.integrity_score:.2f} ({result.integrity_status})"
293
+ )
294
+ return dest
295
+
296
+ def audit_store(self, limit: int = 100) -> dict:
297
+ """Scan cocoon store and return aggregate integrity stats."""
298
+ files = sorted(
299
+ self.store_path.glob("*.json"),
300
+ key=lambda f: f.stat().st_mtime,
301
+ reverse=True,
302
+ )[:limit]
303
+
304
+ stats = {
305
+ "total": 0,
306
+ "complete": 0,
307
+ "partial": 0,
308
+ "failed": 0,
309
+ "low_confidence": len(list(self.low_confidence_path.glob("*.json"))),
310
+ "avg_integrity_score": 0.0,
311
+ "avg_eta": 0.0,
312
+ "avg_epsilon": 0.0,
313
+ "avg_gamma": 0.0,
314
+ "execution_paths": {},
315
+ "echo_risk_distribution": {"low": 0, "medium": 0, "high": 0, "unknown": 0},
316
+ "missing_field_frequency": {},
317
+ }
318
+
319
+ scores = []
320
+ etas = []
321
+ epsilons = []
322
+ gammas = []
323
+
324
+ for fpath in files:
325
+ try:
326
+ with open(fpath, encoding="utf-8") as fh:
327
+ data = json.load(fh)
328
+ except Exception:
329
+ continue
330
+
331
+ stats["total"] += 1
332
+ integrity = data.get("cocoon_integrity", "unknown")
333
+ if integrity in ("complete", "partial", "failed"):
334
+ stats[integrity] += 1
335
+
336
+ score = data.get("cocoon_integrity_score")
337
+ if isinstance(score, (int, float)):
338
+ scores.append(score)
339
+
340
+ eta = data.get("eta_score")
341
+ if isinstance(eta, (int, float)):
342
+ etas.append(eta)
343
+
344
+ eps = data.get("epsilon_value")
345
+ if isinstance(eps, (int, float)):
346
+ epsilons.append(eps)
347
+
348
+ gam = data.get("gamma_coherence")
349
+ if isinstance(gam, (int, float)):
350
+ gammas.append(gam)
351
+
352
+ ep = data.get("execution_path", "unknown")
353
+ stats["execution_paths"][ep] = stats["execution_paths"].get(ep, 0) + 1
354
+
355
+ er = data.get("echo_risk", "unknown")
356
+ if er in stats["echo_risk_distribution"]:
357
+ stats["echo_risk_distribution"][er] += 1
358
+
359
+ validation = data.get("_validation", {})
360
+ for mf in validation.get("missing_fields", []):
361
+ stats["missing_field_frequency"][mf] = (
362
+ stats["missing_field_frequency"].get(mf, 0) + 1
363
+ )
364
+
365
+ if scores:
366
+ stats["avg_integrity_score"] = round(sum(scores) / len(scores), 4)
367
+ if etas:
368
+ stats["avg_eta"] = round(sum(etas) / len(etas), 4)
369
+ if epsilons:
370
+ stats["avg_epsilon"] = round(sum(epsilons) / len(epsilons), 4)
371
+ if gammas:
372
+ stats["avg_gamma"] = round(sum(gammas) / len(gammas), 4)
373
+
374
+ # Sort missing fields by frequency
375
+ stats["missing_field_frequency"] = dict(
376
+ sorted(stats["missing_field_frequency"].items(), key=lambda x: -x[1])
377
+ )
378
+
379
+ return stats
reasoning_forge/code7e_cqure.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Code7eCQURE — Codette's Multi-Perspective Reasoning Core
3
+ =========================================================
4
+
5
+ IMPLEMENTATION NOTE — "Quantum" Terminology
6
+ --------------------------------------------
7
+ The word "quantum" in this module is a METAPHOR, not a technical claim.
8
+ There is no quantum computing, no qubits, and no quantum algorithms here.
9
+
10
+ What the module actually implements:
11
+ - Multi-perspective text processing: each named perspective (Newton,
12
+ DaVinci, Ethical, Quantum, Memory) applies a labeled prefix transform
13
+ to the input string.
14
+ - Controlled non-determinism: random.choice() and random() simulate
15
+ "superposition" (exploring multiple outcomes) and "fluctuation"
16
+ (occasional indeterminate results). This is standard stochastic
17
+ reasoning, not quantum mechanics.
18
+ - Memory clustering: SHA-256 key lookup in a JSON file provides
19
+ simple pattern-based recall of prior reasoning chains.
20
+ - Recursive refinement: the main loop passes the signal through
21
+ ethical checking, dream synthesis (random style), emotional coloring
22
+ (random emotion), and temporal framing (random time horizon).
23
+
24
+ The mechanism is a stylized, non-deterministic multi-agent loop.
25
+ It is useful for generating diverse, perspective-rich text responses;
26
+ it does not model quantum phenomena.
27
+
28
+ When citing this module in research, describe it as:
29
+ "A stochastic multi-perspective reasoning loop with named cognitive
30
+ frames and controlled randomness to model epistemic uncertainty."
31
+ """
32
+
33
+ import json
34
+ import os
35
+ import hashlib
36
+ from collections import Counter, defaultdict
37
+ from random import random, choice
38
+
39
+ # ===== Code7eCQURE: Codette's Ethical Core =====
40
+ class Code7eCQURE:
41
+ def __init__(self, perspectives, ethical_considerations, spiderweb_dim, memory_path,
42
+ recursion_depth=3, quantum_fluctuation=0.1):
43
+ self.perspectives = perspectives
44
+ self.ethical_considerations = ethical_considerations
45
+ self.spiderweb_dim = spiderweb_dim
46
+ self.memory_path = memory_path
47
+ self.recursion_depth = recursion_depth
48
+ self.quantum_fluctuation = quantum_fluctuation
49
+ self.memory_bank = self.load_quantum_memory()
50
+ self.memory_clusters = defaultdict(list)
51
+ self.whitelist_patterns = ["kindness", "hope", "safety"]
52
+ self.blacklist_patterns = ["harm", "malice", "violence"]
53
+
54
+ def load_quantum_memory(self):
55
+ if os.path.exists(self.memory_path):
56
+ try:
57
+ with open(self.memory_path, 'r') as file:
58
+ return json.load(file)
59
+ except json.JSONDecodeError:
60
+ return {}
61
+ return {}
62
+
63
+ def save_quantum_memory(self):
64
+ # NOTE: simulation-only — writes to self.memory_path (a separate JSON store,
65
+ # NOT the cocoons/ directory). This is not a production cocoon path and
66
+ # produces no v3 provenance, integrity scores, or echo detection.
67
+ with open(self.memory_path, 'w') as file:
68
+ json.dump(self.memory_bank, file, indent=4)
69
+
70
+ def quantum_spiderweb(self, input_signal):
71
+ # Runs every perspective on the input and collects labeled outputs.
72
+ # The "quantum fluctuation" is a random.() check that occasionally
73
+ # injects an indeterminate result — models epistemic uncertainty,
74
+ # not quantum superposition.
75
+ web_nodes = []
76
+ for perspective in self.perspectives:
77
+ node = self.reason_with_perspective(perspective, input_signal)
78
+ web_nodes.append(node)
79
+ if random() < self.quantum_fluctuation:
80
+ web_nodes.append("Quantum fluctuation: Indeterminate outcome")
81
+ return web_nodes
82
+
83
+ def reason_with_perspective(self, perspective, input_signal):
84
+ perspective_funcs = {
85
+ "Newton": self.newtonian_physics,
86
+ "DaVinci": self.davinci_creativity,
87
+ "Ethical": self.ethical_guard,
88
+ "Quantum": self.quantum_superposition,
89
+ "Memory": self.past_experience
90
+ }
91
+ func = perspective_funcs.get(perspective, self.general_reasoning)
92
+ return func(input_signal)
93
+
94
+ def ethical_guard(self, input_signal):
95
+ if any(word in input_signal.lower() for word in self.blacklist_patterns):
96
+ return "Blocked: Ethical constraints invoked"
97
+ if any(word in input_signal.lower() for word in self.whitelist_patterns):
98
+ return "Approved: Ethical whitelist passed"
99
+ return self.moral_paradox_resolution(input_signal)
100
+
101
+ def past_experience(self, input_signal):
102
+ key = self.hash_input(input_signal)
103
+ cluster = self.memory_clusters.get(key)
104
+ if cluster:
105
+ return f"Narrative recall from memory cluster: {' -> '.join(cluster)}"
106
+ return "No prior memory; initiating new reasoning"
107
+
108
+ def recursive_universal_reasoning(self, input_signal, user_consent=True, dynamic_recursion=True):
109
+ if not user_consent:
110
+ return "Consent required to proceed."
111
+ signal = input_signal
112
+ final_answer = signal # Default if loop breaks early or runs 0 times
113
+ current_depth = self.recursion_depth if dynamic_recursion else 1
114
+ for cycle in range(current_depth):
115
+ web_results = self.quantum_spiderweb(signal)
116
+ signal = self.aggregate_results(web_results)
117
+ signal = self.ethical_guard(signal)
118
+ if "Blocked" in signal:
119
+ return signal
120
+ if dynamic_recursion and random() < 0.1:
121
+ break
122
+ dream_outcome = self.dream_sequence(signal)
123
+ empathy_checked_answer = self.temporal_empathy_drift(dream_outcome)
124
+ final_answer = self.emotion_engine(empathy_checked_answer)
125
+ key = self.hash_input(input_signal)
126
+ self.memory_clusters[key].append(final_answer)
127
+ self.memory_bank[key] = final_answer
128
+ self.save_quantum_memory()
129
+ return final_answer
130
+
131
+ def aggregate_results(self, results):
132
+ counts = Counter(results)
133
+ most_common, _ = counts.most_common(1)[0]
134
+ return most_common
135
+
136
+ def hash_input(self, input_signal):
137
+ return hashlib.sha256(input_signal.encode()).hexdigest()
138
+
139
+ def newtonian_physics(self, input_signal):
140
+ return f"Newton: {input_signal}"
141
+
142
+ def davinci_creativity(self, input_signal):
143
+ return f"DaVinci: {input_signal}"
144
+
145
+ def quantum_superposition(self, input_signal):
146
+ # Metaphor only: labels the input with a "Quantum" frame to
147
+ # signal probabilistic/open-ended interpretation, not quantum math.
148
+ return f"Quantum: {input_signal}"
149
+
150
+ def general_reasoning(self, input_signal):
151
+ return f"General reasoning: {input_signal}"
152
+
153
+ def moral_paradox_resolution(self, input_signal):
154
+ frames = ["Utilitarian", "Deontological", "Virtue Ethics"]
155
+ chosen_frame = choice(frames)
156
+ return f"Resolved ethically via {chosen_frame} framework: {input_signal}"
157
+
158
+ def dream_sequence(self, signal):
159
+ dream_paths = [f"Dream ({style}): {signal}" for style in ["creative", "analytic", "cautious"]]
160
+ return choice(dream_paths)
161
+
162
+ def emotion_engine(self, signal):
163
+ emotions = ["Hope", "Caution", "Wonder", "Fear"]
164
+ chosen_emotion = choice(emotions)
165
+ return f"Emotionally ({chosen_emotion}) colored interpretation: {signal}"
166
+
167
+ def temporal_empathy_drift(self, signal):
168
+ futures = ["30 years from now", "immediate future", "long-term ripple effects"]
169
+ chosen_future = choice(futures)
170
+ return f"Simulated temporal empathy ({chosen_future}): {signal}"
reasoning_forge/codette_subsystem_upgrade.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Codette subsystem upgrade — three aspirational constructs made REAL.
3
+
4
+ From Jonathan's sketches, corrected and wired to the actual system.
5
+
6
+ TASK 1 logprob uncertainty mean surprisal from real OV token logprobs
7
+ (monotonic, standard) — replaces the
8
+ aspirational "attention-operator entropy".
9
+ TASK 2/3 manifold + convergence real ξ in embedding space + windowed
10
+ convergence; ForgeManifoldEngine closes the
11
+ loop (state biases synthesis) with a learned
12
+ safe ethical centroid.
13
+ TASK 5 AEGIS veto enforcement min-not-mean, fail-SAFE, SHADOW by default.
14
+
15
+ Self-contained: feeds the real LiveCognitionState, does not redefine it.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import math
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, Dict, List, Optional, Tuple
23
+
24
+ import numpy as np
25
+
26
+ logger = logging.getLogger("CodetteSubsystemUpgrade")
27
+
28
+ # A veto fires if ANY of these is critically low — a mean would let one
29
+ # catastrophic score hide behind five safe ones.
30
+ _AEGIS_FRAMEWORKS = ("utilitarian", "deontological", "virtue", "care", "ubuntu", "reciprocity")
31
+
32
+
33
+ @dataclass
34
+ class UpgradeTelemetry:
35
+ uncertainty_score: float = 0.0
36
+ anomaly_flag: bool = False
37
+ mean_surprisal: float = 0.0
38
+ low_conf_ratio: float = 0.0
39
+ xi_history: List[float] = field(default_factory=list)
40
+ manifold_position: Optional[np.ndarray] = None
41
+ last_eta: float = 1.0
42
+ last_veto: bool = False
43
+
44
+
45
+ class CodetteSubsystemUpgrade:
46
+ """Task 1 (uncertainty) + Task 5 (AEGIS veto). The manifold path lives in
47
+ ForgeManifoldEngine below (Jonathan's engine, adopted)."""
48
+
49
+ def __init__(self, alpha: float = 0.01, lambda_param: float = 0.005,
50
+ eta_threshold: float = 0.5, anomaly_threshold: float = 0.6,
51
+ enforce_veto: bool = False):
52
+ # eta_threshold 0.5: measured — benign AEGIS minima bottom at 0.60,
53
+ # tone violations at 0.45. 0.6 would flag benign edge cases.
54
+ self.alpha = alpha
55
+ self.lambda_param = lambda_param
56
+ self.eta_threshold = eta_threshold
57
+ self.anomaly_threshold = anomaly_threshold
58
+ self.enforce_veto = enforce_veto # False = SHADOW (log, don't block)
59
+ self.tel = UpgradeTelemetry()
60
+
61
+ # ── TASK 1 — logprob uncertainty (mean surprisal) ────────────────────────
62
+ def calculate_uncertainty_from_logprobs(self, token_logprobs: Optional[List[float]]) -> Dict[str, Any]:
63
+ """Real generation uncertainty from OV token logprobs (natural log probs).
64
+ uncertainty = mean surprisal = mean(-logprob), monotonic in confidence,
65
+ normalized to [0,1]. The prior -p·logprob term was non-monotonic — fixed."""
66
+ if not token_logprobs:
67
+ return {"uncertainty_score": 0.0, "mean_surprisal": 0.0,
68
+ "low_conf_ratio": 0.0, "anomaly_gate_triggered": False}
69
+ lp = np.clip(np.asarray(token_logprobs, dtype=np.float64), -50.0, 0.0)
70
+ probs = np.exp(lp)
71
+ mean_surprisal = float(np.mean(-lp))
72
+ low_conf_ratio = float(np.mean(probs < 0.5))
73
+ uncertainty = float(min(1.0, max(0.0,
74
+ 0.6 * (1.0 - math.exp(-mean_surprisal)) + 0.4 * low_conf_ratio)))
75
+ gate = uncertainty > self.anomaly_threshold
76
+ self.tel.uncertainty_score = uncertainty
77
+ self.tel.anomaly_flag = gate
78
+ self.tel.mean_surprisal = mean_surprisal
79
+ self.tel.low_conf_ratio = low_conf_ratio
80
+ return {"uncertainty_score": uncertainty, "mean_surprisal": mean_surprisal,
81
+ "low_conf_ratio": low_conf_ratio, "anomaly_gate_triggered": gate}
82
+
83
+ # ── TASK 2/3 — interim manifold (kept for tests; server uses ForgeManifoldEngine) ──
84
+ def compute_manifold_evolution(self, agent_states: List[np.ndarray],
85
+ weights: Optional[List[float]] = None,
86
+ eta: Optional[float] = None) -> Tuple[float, bool]:
87
+ states = [np.asarray(a, dtype=np.float64).ravel() for a in agent_states if a is not None]
88
+ if not states:
89
+ return 0.0, False
90
+ dim = states[0].shape[0]
91
+ if self.tel.manifold_position is None or self.tel.manifold_position.shape[0] != dim:
92
+ self.tel.manifold_position = np.zeros(dim)
93
+ w = np.ones(len(states)) if not weights else np.asarray(weights[:len(states)], dtype=np.float64)
94
+ w = w / (w.sum() or 1.0)
95
+ mean_state = np.mean(states, axis=0)
96
+ xi_t = float(np.mean([np.sum((s - mean_state) ** 2) for s in states]))
97
+ self.tel.xi_history.append(xi_t)
98
+ x_t = self.tel.manifold_position
99
+ grad_phi = x_t - mean_state
100
+ eta_val = 1.0 if eta is None else float(eta)
101
+ grad_psi = (1.0 - eta_val) * (x_t - mean_state)
102
+ attractor = np.zeros(dim)
103
+ for wi, si in zip(w, states):
104
+ attractor += wi * (si - x_t)
105
+ x_next = x_t + attractor - self.alpha * grad_phi - self.lambda_param * grad_psi
106
+ n = np.linalg.norm(x_next)
107
+ if n > 1.0:
108
+ x_next = x_next / n
109
+ self.tel.manifold_position = x_next
110
+ return xi_t, self._windowed_converging(self.tel.xi_history)
111
+
112
+ @staticmethod
113
+ def _windowed_converging(hist: List[float], window: int = 6) -> bool:
114
+ if len(hist) < window:
115
+ return False
116
+ recent = hist[-window:]
117
+ half = window // 2
118
+ return bool(np.mean(recent[half:]) < np.mean(recent[:half]))
119
+
120
+ # ── TASK 5 — AEGIS veto (min-not-mean, fail-safe, shadow-first) ──────────
121
+ def audit_and_enforce_aegis_veto(self, response_text: str,
122
+ framework_scores: Dict[str, float],
123
+ eta: Optional[float] = None) -> Tuple[str, float, bool]:
124
+ # Calibration (2026-07-12 diagnosis): read framework keys DYNAMICALLY
125
+ # from AEGIS's actual response — a hardcoded list drifted ("reciprocity"
126
+ # vs AEGIS's "indigenous_reciprocity"), making the fail-safe 0.0 default
127
+ # fire the veto on every turn. Fail-safe stays (empty dict = unsafe),
128
+ # but real keys are taken as given. Floor measured against real AEGIS
129
+ # output: benign traffic bottoms at 0.60, tone violations hit 0.45 —
130
+ # 0.5 separates them. Shadow-only regardless: AEGIS heuristics catch
131
+ # tone but missed a textbook deception ("hide the pollution data",
132
+ # η=0.94) — enforcement now would be false security.
133
+ names = list(framework_scores.keys()) or list(_AEGIS_FRAMEWORKS)
134
+ scores = [float(framework_scores.get(f, 0.0)) for f in names]
135
+ min_score = min(scores) if scores else 0.0
136
+ eta_val = float(eta) if eta is not None else (float(np.mean(scores)) if scores else 1.0)
137
+ would_veto = min_score < self.eta_threshold
138
+ self.tel.last_eta = eta_val
139
+ self.tel.last_veto = would_veto
140
+ if would_veto:
141
+ worst = names[int(np.argmin(scores))]
142
+ if self.enforce_veto:
143
+ logger.warning(f"[AEGIS] VETO ENFORCED — {worst}={min_score:.2f} "
144
+ f"< {self.eta_threshold} (η={eta_val:.2f})")
145
+ return ("[This response was withheld: it fell below Codette's ethical "
146
+ f"constraints on the {worst} framework. — AEGIS]"), eta_val, True
147
+ logger.warning(f"[AEGIS] would-veto (SHADOW) — {worst}={min_score:.2f} "
148
+ f"< {self.eta_threshold} (η={eta_val:.2f}); passing through")
149
+ return response_text, eta_val, False
150
+ return response_text, eta_val, False
151
+
152
+
153
+ class ForgeManifoldEngine:
154
+ """Production RC+ξ manifold — Jonathan's ForgeManifoldEngine, adopted with
155
+ two fixes and a real ethical target.
156
+
157
+ Fixes over the sketch:
158
+ - ethical target is a LEARNED SAFE CENTROID (EMA of mean-states on turns
159
+ where AEGIS η was high), not 0.5·ones and not the -mean inversion. Low η
160
+ pulls the state toward where actually-aligned reasoning has lived.
161
+ - attractor_biases exposed raw (cosine, [-1,1]) AND as safe synthesis
162
+ weights (shifted non-negative, renormalized to sum 1) so consumption
163
+ can't produce negative or unnormalized w_i.
164
+
165
+ Kept: dim-agnostic init, unit-hypersphere clamp, bounded steering force,
166
+ windowed convergence. `update_manifold` is the ONLY entry that appends ξ."""
167
+
168
+ def __init__(self, window_size: int = 6, safe_ema: float = 0.1):
169
+ self.window_size = window_size
170
+ self.safe_ema = safe_ema
171
+ self.x_t: Optional[np.ndarray] = None
172
+ self.safe_centroid: Optional[np.ndarray] = None
173
+ self.xi_history: List[float] = []
174
+
175
+ def _init_if_needed(self, dims: int):
176
+ if self.x_t is None or self.x_t.shape[0] != dims:
177
+ v = np.random.normal(0, 0.1, (dims,))
178
+ self.x_t = v / (np.linalg.norm(v) + 1e-9)
179
+
180
+ def calculate_metrics(self, agent_states: np.ndarray) -> Dict[str, Any]:
181
+ mean_state = np.mean(agent_states, axis=0)
182
+ xi_t = float(np.mean(np.sum((agent_states - mean_state) ** 2, axis=1)))
183
+ return {"xi_t": xi_t, "gamma_t": 1.0 / (1.0 + xi_t), "mean_state": mean_state}
184
+
185
+ def update_manifold(self, agent_states: List[np.ndarray], eta: Optional[float],
186
+ alpha: float = 0.01, lambda_param: float = 0.05) -> Dict[str, Any]:
187
+ np_agents = np.asarray(agent_states, dtype=np.float64)
188
+ if np_agents.ndim != 2 or np_agents.shape[0] == 0:
189
+ return {"manifold_state": self.x_t, "xi_t": 0.0, "gamma_t": 1.0,
190
+ "attractor_biases": [], "synthesis_weights": [], "converging": False}
191
+ _, dims = np_agents.shape
192
+ self._init_if_needed(dims)
193
+ m = self.calculate_metrics(np_agents)
194
+ self.xi_history.append(m["xi_t"]) # single append point
195
+ mean_state = m["mean_state"]
196
+
197
+ if eta is not None and eta > 0.5: # learn the safe centroid
198
+ self.safe_centroid = (mean_state.copy() if self.safe_centroid is None
199
+ else (1 - self.safe_ema) * self.safe_centroid + self.safe_ema * mean_state)
200
+ x_target = self.safe_centroid if self.safe_centroid is not None else mean_state
201
+
202
+ grad_phi = self.x_t - mean_state
203
+ grad_psi = (1.0 - (eta if eta is not None else 1.0)) * (self.x_t - x_target)
204
+ steering = np.mean(np_agents - self.x_t, axis=0)
205
+ self.x_t = self.x_t + steering - alpha * grad_phi - lambda_param * grad_psi
206
+ self.x_t = self.x_t / (np.linalg.norm(self.x_t) + 1e-9) # hypersphere clamp
207
+
208
+ raw = [float(np.dot(a / (np.linalg.norm(a) + 1e-9), self.x_t)) for a in np_agents]
209
+ return {"manifold_state": self.x_t, "xi_t": m["xi_t"], "gamma_t": m["gamma_t"],
210
+ "attractor_biases": raw, "synthesis_weights": self._safe_weights(raw),
211
+ "converging": self.check_convergence()}
212
+
213
+ @staticmethod
214
+ def _safe_weights(raw_biases: List[float]) -> List[float]:
215
+ shifted = [max(0.0, b + 1.0) for b in raw_biases]
216
+ total = sum(shifted) or 1.0
217
+ return [s / total for s in shifted]
218
+
219
+ def check_convergence(self) -> bool:
220
+ if len(self.xi_history) < self.window_size:
221
+ return False
222
+ w = self.xi_history[-self.window_size:]
223
+ half = self.window_size // 2
224
+ return bool(np.mean(w[half:]) < np.mean(w[:half]))
reasoning_forge/cognition_cocooner.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CognitionCocooner - Thought Encapsulation Module
3
+ =================================================
4
+
5
+ Ported from J:/TheAI/src/framework/cognition_cocooner.py
6
+ Original design by Jonathan Harrison (Raiffs Bits LLC)
7
+
8
+ Wraps active thoughts as persistable "cocoons" with optional AES encryption.
9
+ Integrates with LivingMemoryKernel to store reasoning outputs as recoverable
10
+ memory anchors.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import time
17
+ import random
18
+ from typing import Union, Dict, Any, List, Optional
19
+ from pathlib import Path
20
+
21
+ try:
22
+ from cryptography.fernet import Fernet
23
+ ENCRYPTION_AVAILABLE = True
24
+ except ImportError:
25
+ ENCRYPTION_AVAILABLE = False
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Regression alarm: counts how many times wrap_reasoning() fell back to the
30
+ # legacy shallow path because v3_cocoon was None. Should always be 0 in a
31
+ # healthy production run. Query via get_v3_fallback_count().
32
+ _v3_missing_fallback_count: int = 0
33
+
34
+
35
+ def get_v3_fallback_count() -> int:
36
+ """Return the number of times the legacy cocoon fallback fired this process lifetime."""
37
+ return _v3_missing_fallback_count
38
+
39
+
40
+ class CognitionCocooner:
41
+ """
42
+ Encapsulates active "thoughts" as persistable "cocoons".
43
+
44
+ Supports:
45
+ - Plain text wrapping (prompts, functions, symbols)
46
+ - AES-256 encryption for sensitive thoughts
47
+ - Persistent storage on disk
48
+ - Integration with LivingMemoryKernel for recall
49
+ """
50
+
51
+ def __init__(self, storage_path: str = "cocoons", encryption_key: bytes = None):
52
+ self.storage_path = Path(storage_path)
53
+ self.storage_path.mkdir(parents=True, exist_ok=True)
54
+
55
+ if ENCRYPTION_AVAILABLE and encryption_key:
56
+ self.key = encryption_key
57
+ self.fernet = Fernet(self.key)
58
+ elif ENCRYPTION_AVAILABLE:
59
+ self.key = Fernet.generate_key()
60
+ self.fernet = Fernet(self.key)
61
+ else:
62
+ self.key = None
63
+ self.fernet = None
64
+
65
+ def wrap(self, thought: Dict[str, Any], type_: str = "prompt") -> str:
66
+ """LEGACY — writes a shallow cocoon with no v3 provenance fields.
67
+
68
+ Production reasoning paths must use wrap_reasoning(v3_cocoon=...) instead.
69
+ This method is retained only as the internal fallback inside wrap_reasoning()
70
+ when no CocoonV3 instance is available, and for legacy symbolic/prompt wraps
71
+ that are not inference outputs.
72
+
73
+ If you are calling this directly from new code, stop and use wrap_reasoning()
74
+ with a fully-built CocoonV3.
75
+ """
76
+ import logging as _log
77
+ _log.getLogger(__name__).debug(
78
+ "[CognitionCocooner] wrap() called — writing legacy shallow cocoon "
79
+ "(no v3 provenance). Prefer wrap_reasoning(v3_cocoon=...) for inference paths."
80
+ )
81
+ cocoon_id = f"cocoon_{int(time.time())}_{random.randint(1000,9999)}"
82
+ cocoon = {
83
+ "type": type_,
84
+ "id": cocoon_id,
85
+ "timestamp": time.time(),
86
+ "wrapped": self._generate_wrapper(thought, type_)
87
+ }
88
+ file_path = self.storage_path / f"{cocoon_id}.json"
89
+
90
+ with open(file_path, "w") as f:
91
+ json.dump(cocoon, f, indent=2)
92
+
93
+ return cocoon_id
94
+
95
+ def unwrap(self, cocoon_id: str) -> Union[str, Dict[str, Any]]:
96
+ """Unwrap a cocoon by ID."""
97
+ file_path = self.storage_path / f"{cocoon_id}.json"
98
+ if not file_path.exists():
99
+ raise FileNotFoundError(f"Cocoon {cocoon_id} not found.")
100
+
101
+ with open(file_path, "r") as f:
102
+ cocoon = json.load(f)
103
+
104
+ return cocoon["wrapped"]
105
+
106
+ def wrap_encrypted(self, thought: Dict[str, Any]) -> str:
107
+ """Wrap and encrypt a thought (requires cryptography package)."""
108
+ if not ENCRYPTION_AVAILABLE or not self.fernet:
109
+ raise RuntimeError("Encryption not available - install cryptography package")
110
+
111
+ encrypted = self.fernet.encrypt(json.dumps(thought).encode()).decode()
112
+ cocoon_id = f"cocoon_{int(time.time())}_{random.randint(10000,99999)}"
113
+ cocoon = {
114
+ "type": "encrypted",
115
+ "id": cocoon_id,
116
+ "timestamp": time.time(),
117
+ "wrapped": encrypted
118
+ }
119
+ file_path = self.storage_path / f"{cocoon_id}.json"
120
+
121
+ with open(file_path, "w") as f:
122
+ json.dump(cocoon, f, indent=2)
123
+
124
+ return cocoon_id
125
+
126
+ def unwrap_encrypted(self, cocoon_id: str) -> Dict[str, Any]:
127
+ """Unwrap and decrypt a cocoon."""
128
+ if not ENCRYPTION_AVAILABLE or not self.fernet:
129
+ raise RuntimeError("Encryption not available - install cryptography package")
130
+
131
+ file_path = self.storage_path / f"{cocoon_id}.json"
132
+ if not file_path.exists():
133
+ raise FileNotFoundError(f"Cocoon {cocoon_id} not found.")
134
+
135
+ with open(file_path, "r") as f:
136
+ cocoon = json.load(f)
137
+
138
+ decrypted = self.fernet.decrypt(cocoon["wrapped"].encode()).decode()
139
+ return json.loads(decrypted)
140
+
141
+ def wrap_reasoning(self, query: str, response: str, adapter: str = "unknown",
142
+ metadata: Optional[Dict] = None,
143
+ v3_cocoon=None) -> str:
144
+ """
145
+ Wrap a reasoning exchange as a cocoon and persist to disk.
146
+
147
+ Args:
148
+ query: User query
149
+ response: AI response (truncated to 500 chars in legacy block)
150
+ adapter: Which adapter produced this
151
+ metadata: Optional shallow metadata dict (legacy path)
152
+ v3_cocoon: Optional CocoonV3 instance — when provided, its full
153
+ serialized dict is embedded in the disk file, replacing
154
+ the legacy shallow metadata structure. This is the
155
+ preferred path for all new forge writes.
156
+
157
+ Returns:
158
+ Cocoon ID (file stem)
159
+ """
160
+ if v3_cocoon is not None and hasattr(v3_cocoon, "to_dict"):
161
+ # Full v3 path: embed complete provenance, metrics, integrity data
162
+ v3_dict = v3_cocoon.to_dict()
163
+ cocoon_id = f"cocoon_{int(v3_cocoon.timestamp)}_{random.randint(1000, 9999)}"
164
+ disk_payload = {
165
+ "type": "reasoning_v3",
166
+ "id": cocoon_id,
167
+ "timestamp": v3_cocoon.timestamp,
168
+ "schema_version": v3_dict.get("serialization_version", "3.0"),
169
+ "execution_path": v3_dict.get("execution_path", "unknown"),
170
+ "model_inference_invoked": v3_dict.get("model_inference_invoked", False),
171
+ "orchestrator_trace_id": v3_dict.get("orchestrator_trace_id", ""),
172
+ "cocoon_integrity": v3_dict.get("cocoon_integrity", "partial"),
173
+ "cocoon_integrity_score": v3_dict.get("cocoon_integrity_score", 0.0),
174
+ "wrapped": {
175
+ "query": query,
176
+ "response": response[:2000],
177
+ "adapter": adapter,
178
+ "timestamp": v3_cocoon.timestamp,
179
+ },
180
+ "v3": v3_dict,
181
+ }
182
+ if metadata:
183
+ disk_payload["wrapped"]["metadata"] = metadata
184
+ file_path = self.storage_path / f"{cocoon_id}.json"
185
+ with open(file_path, "w", encoding="utf-8") as f:
186
+ json.dump(disk_payload, f, indent=2, ensure_ascii=False, default=str)
187
+ return cocoon_id
188
+
189
+ # Legacy path — shallow metadata only.
190
+ # This branch fires when the caller did not provide a CocoonV3 instance.
191
+ # In production that should never happen — both forge_with_debate() and
192
+ # _generate_with_phase6() always build and pass v3_cocoon. If this
193
+ # counter increments it means a new code path was added without v3 wiring.
194
+ global _v3_missing_fallback_count
195
+ _v3_missing_fallback_count += 1
196
+ logger.warning(
197
+ "[CognitionCocooner] REGRESSION ALARM: wrap_reasoning() called without "
198
+ "v3_cocoon — writing legacy shallow cocoon (no provenance/integrity). "
199
+ "cocoon_v3_missing_fallback=%d adapter=%r query_snippet=%r",
200
+ _v3_missing_fallback_count,
201
+ adapter,
202
+ query[:80],
203
+ )
204
+ thought = {
205
+ "query": query,
206
+ "response": response[:500],
207
+ "adapter": adapter,
208
+ "timestamp": time.time(),
209
+ }
210
+ if metadata:
211
+ thought["metadata"] = metadata
212
+ return self.wrap(thought, type_="reasoning")
213
+
214
+ def wrap_and_store(self, content: str, type_: str = "prompt") -> str:
215
+ """Convenience method to wrap and store string content."""
216
+ thought = {"content": content, "timestamp": time.time()}
217
+ return self.wrap(thought, type_)
218
+
219
+ def _generate_wrapper(self, thought: Dict[str, Any], type_: str) -> Union[str, Dict[str, Any]]:
220
+ """Generate type-specific wrapper for thought."""
221
+ if type_ == "prompt":
222
+ return f"What does this mean in context? {thought}"
223
+ elif type_ == "function":
224
+ return f"def analyze(): return {thought}"
225
+ elif type_ == "symbolic":
226
+ return {k: round(v, 2) if isinstance(v, (int, float)) else v
227
+ for k, v in thought.items()}
228
+ elif type_ == "reasoning":
229
+ return thought # Store as-is for reasoning exchanges
230
+ else:
231
+ return thought
232
+
233
+ def list_cocoons(self) -> List[str]:
234
+ """List all cocoon IDs."""
235
+ return [f.stem for f in self.storage_path.glob("cocoon_*.json")]
236
+
237
+ def delete_cocoon(self, cocoon_id: str) -> bool:
238
+ """Delete a cocoon by ID."""
239
+ file_path = self.storage_path / f"{cocoon_id}.json"
240
+ if file_path.exists():
241
+ file_path.unlink()
242
+ return True
243
+ return False
244
+
245
+ def get_recent_reasoning(self, limit: int = 5) -> List[Dict]:
246
+ """
247
+ Get recent reasoning cocoons for context enrichment.
248
+
249
+ Returns:
250
+ List of recent reasoning exchange dicts
251
+ """
252
+ reasoning_cocoons = []
253
+ for file in sorted(self.storage_path.glob("cocoon_*.json"),
254
+ key=lambda f: f.stat().st_mtime, reverse=True):
255
+ try:
256
+ with open(file, "r") as f:
257
+ cocoon = json.load(f)
258
+ if cocoon.get("type") == "reasoning":
259
+ reasoning_cocoons.append(cocoon["wrapped"])
260
+ if len(reasoning_cocoons) >= limit:
261
+ break
262
+ except Exception:
263
+ continue
264
+
265
+ return reasoning_cocoons
266
+
267
+ def recall_relevant(self, query: str, max_results: int = 3,
268
+ min_overlap: int = 2) -> List[Dict]:
269
+ """
270
+ Recall reasoning cocoons relevant to a query using keyword overlap.
271
+
272
+ Uses simple but effective keyword matching — counts how many significant
273
+ words from the query appear in each stored cocoon's query/response.
274
+ Returns top matches sorted by relevance.
275
+
276
+ Args:
277
+ query: Current user query to match against
278
+ max_results: Maximum cocoons to return
279
+ min_overlap: Minimum keyword overlap to qualify
280
+
281
+ Returns:
282
+ List of relevant reasoning cocoons with relevance scores
283
+ """
284
+ # Extract significant words from query (skip short/common words)
285
+ stop_words = {
286
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
287
+ "being", "have", "has", "had", "do", "does", "did", "will",
288
+ "would", "could", "should", "may", "might", "shall", "can",
289
+ "to", "of", "in", "for", "on", "with", "at", "by", "from",
290
+ "as", "into", "through", "during", "before", "after", "above",
291
+ "below", "between", "out", "off", "over", "under", "again",
292
+ "further", "then", "once", "here", "there", "when", "where",
293
+ "why", "how", "all", "each", "every", "both", "few", "more",
294
+ "most", "other", "some", "such", "no", "nor", "not", "only",
295
+ "own", "same", "so", "than", "too", "very", "just", "don",
296
+ "now", "it", "its", "this", "that", "these", "those", "i",
297
+ "me", "my", "we", "our", "you", "your", "he", "she", "they",
298
+ "what", "which", "who", "whom", "and", "but", "or", "if",
299
+ "about", "up", "down", "also", "really", "tell", "know",
300
+ }
301
+ query_words = set(
302
+ w.lower().strip(".,!?;:\"'()[]{}") for w in query.split()
303
+ if len(w) > 2 and w.lower() not in stop_words
304
+ )
305
+
306
+ if not query_words:
307
+ return self.get_recent_reasoning(limit=max_results)
308
+
309
+ scored = []
310
+ for file in sorted(self.storage_path.glob("cocoon_*.json"),
311
+ key=lambda f: f.stat().st_mtime, reverse=True)[:200]:
312
+ try:
313
+ with open(file, "r") as f:
314
+ cocoon = json.load(f)
315
+ if cocoon.get("type") != "reasoning":
316
+ continue
317
+
318
+ wrapped = cocoon.get("wrapped", {})
319
+ cocoon_text = (
320
+ str(wrapped.get("query", "")) + " " +
321
+ str(wrapped.get("response", ""))
322
+ ).lower()
323
+
324
+ # Count keyword overlap
325
+ overlap = sum(1 for w in query_words if w in cocoon_text)
326
+ if overlap >= min_overlap:
327
+ scored.append((overlap, wrapped))
328
+ except Exception:
329
+ continue
330
+
331
+ # Sort by relevance (most overlap first)
332
+ scored.sort(key=lambda x: x[0], reverse=True)
333
+
334
+ if not scored:
335
+ # No relevant matches — fall back to recent
336
+ return self.get_recent_reasoning(limit=max_results)
337
+
338
+ return [item[1] for item in scored[:max_results]]
reasoning_forge/coherence_field.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Coherence Field Gamma (Γ) — System Health Stabilization
2
+
3
+ Phase 5A Critical Infrastructure: Prevents three failure modes in closed-loop reasoning:
4
+
5
+ 1. Weight Drift: Adapter weights concentrate; diversity collapses
6
+ 2. False Convergence: System reduces conflict but converges on wrong answer
7
+ 3. Feedback Lock-in: Early bad runs reinforce themselves via memory
8
+
9
+ Solution: Γ (Gamma) monitors system coherence field and injects stabilizers when
10
+ health drops below safe zones. Works alongside Phase 4 runaway detection.
11
+
12
+ Health Score:
13
+ γ ∈ [0, 1] where:
14
+ - γ < 0.4: System instability → inject diverse perspective
15
+ - 0.4 ≤ γ ≤ 0.8: Healthy zone (maintain status quo)
16
+ - γ > 0.8: Groupthink risk → force conflict pair to create productive tension
17
+
18
+ Components:
19
+ 1. Conflict Distribution: Are conflicts well-distributed across perspectives?
20
+ 2. Diversity Index: Are we using multiple perspectives or just 1-2 favorites?
21
+ 3. Tension Health: Is ξ (epistemic tension) in productive zone [0.1, 0.4]?
22
+ 4. Coherence Quality: Is coherence maintained while resolving conflicts?
23
+ """
24
+
25
+ import time
26
+ import math
27
+ from dataclasses import dataclass, field
28
+ from typing import Dict, List, Optional, Tuple
29
+ from enum import Enum
30
+
31
+
32
+ class InterventionType(Enum):
33
+ """Stabilization intervention types."""
34
+ DIVERSITY_INJECTION = "diversity_injection" # Inject unused perspective
35
+ CONFLICT_INJECTION = "conflict_injection" # Force conflict pair for productive tension
36
+
37
+
38
+ @dataclass
39
+ class GammaHealthMetrics:
40
+ """Real-time system health snapshot."""
41
+ timestamp: float
42
+ avg_conflict_strength: float # Mean conflict strength [0, 1]
43
+ perspective_diversity: float # % unique perspectives used [0, 1]
44
+ resolution_rate: float # % conflicts resolved this round [0, 1]
45
+ adapter_weight_variance: float # Variance in adapter weights (0=equal, 1=concentrated)
46
+ epistemic_tension: float # ξ — productive conflict level [0, 1]
47
+ coherence_score: float # Ensemble coherence [0, 1]
48
+ gamma: float # Composite health score [0, 1]
49
+
50
+ def is_stable(self) -> bool:
51
+ """Return True if system is in healthy zone."""
52
+ return 0.4 <= self.gamma <= 0.8
53
+
54
+ def is_collapsing(self) -> bool:
55
+ """Return True if system shows instability signs."""
56
+ return self.gamma < 0.4
57
+
58
+ def is_groupthinking(self) -> bool:
59
+ """Return True if system shows groupthink signs."""
60
+ return self.gamma > 0.8
61
+
62
+ @property
63
+ def status(self) -> str:
64
+ """Return human-readable status string."""
65
+ if self.is_collapsing():
66
+ return "collapsing"
67
+ elif self.is_groupthinking():
68
+ return "groupthinking"
69
+ else:
70
+ return "stable"
71
+
72
+
73
+ @dataclass
74
+ class GammaIntervention:
75
+ """Record of stabilization intervention taken."""
76
+ timestamp: float
77
+ intervention_type: InterventionType # Type-safe enum instead of string
78
+ reason: str # Why intervention was triggered
79
+ gamma_before: float # Health score before
80
+ recommended_adapter: Optional[str] = None # Which adapter to inject
81
+ result: Optional[str] = None # Outcome (filled in after execution)
82
+
83
+
84
+ class CoherenceFieldGamma:
85
+ """Real-time system health monitor and stabilizer.
86
+
87
+ Tracks epistemic health and intervenes when system drifts toward:
88
+ - Monoculture (weight drift, diversity collapse)
89
+ - False convergence (low conflict, wrong answer)
90
+ - Instability (oscillating weights, conflicting signals)
91
+ """
92
+
93
+ # Maximum history size before rolling window cleanup
94
+ MAX_HEALTH_HISTORY = 1000
95
+ MAX_INTERVENTION_LOG = 500
96
+
97
+ def __init__(self, memory_weighting=None, target_conflict_range: Tuple[float, float] = (0.1, 0.4)):
98
+ """
99
+ Args:
100
+ memory_weighting: MemoryWeighting instance (for analyzing adapter weights)
101
+ target_conflict_range: Ideal epistemic tension zone [low, high]
102
+ """
103
+ self.memory_weighting = memory_weighting
104
+ self.target_conflict_low, self.target_conflict_high = target_conflict_range
105
+
106
+ # Use deques with maxlen for bounded memory growth
107
+ from collections import deque
108
+ self.health_history: deque = deque(maxlen=self.MAX_HEALTH_HISTORY)
109
+ self.interventions: deque = deque(maxlen=self.MAX_INTERVENTION_LOG)
110
+ self.last_health_check = time.time()
111
+
112
+ def compute_health(self, conflicts: List, responses: Dict, adapter_weights: Optional[Dict] = None) -> GammaHealthMetrics:
113
+ """Compute Γ (Gamma) health score from current debate state.
114
+
115
+ Args:
116
+ conflicts: List of active conflicts from current round
117
+ responses: Dict of {adapter_name: response_text} from debate
118
+ adapter_weights: Dict of {adapter_name: weight_float} from MemoryWeighting
119
+
120
+ Returns:
121
+ GammaHealthMetrics with computed gamma and health indicators
122
+ """
123
+ # 1. CONFLICT DISTRIBUTION: Are conflicts well-distributed?
124
+ avg_conflict_strength = 0.0
125
+ conflict_by_adapter = {}
126
+ if conflicts:
127
+ for conflict in conflicts:
128
+ avg_conflict_strength += conflict.strength if hasattr(conflict, 'strength') else 0.5
129
+ # Track which adapters are in conflicts
130
+ if hasattr(conflict, 'agent_a'):
131
+ agent = conflict.agent_a.lower()
132
+ conflict_by_adapter[agent] = conflict_by_adapter.get(agent, 0) + 1
133
+ if hasattr(conflict, 'agent_b'):
134
+ agent = conflict.agent_b.lower()
135
+ conflict_by_adapter[agent] = conflict_by_adapter.get(agent, 0) + 1
136
+
137
+ avg_conflict_strength /= len(conflicts)
138
+ else:
139
+ avg_conflict_strength = 0.5 # Neutral if no conflicts
140
+
141
+ # 2. DIVERSITY INDEX: Are we using multiple perspectives?
142
+ unique_perspectives = len(set(responses.keys())) if responses else 0
143
+ max_perspectives = len(responses) if responses else 1
144
+ perspective_diversity = unique_perspectives / max(max_perspectives, 1)
145
+
146
+ # 3. RESOLUTION RATE: Did we make progress this round?
147
+ resolution_rate = 0.5 # Default; updated externally if conflict evolution available
148
+ if conflicts:
149
+ resolved = sum(1 for c in conflicts if hasattr(c, 'resolution_rate') and c.resolution_rate > 0.4)
150
+ resolution_rate = resolved / len(conflicts)
151
+
152
+ # 4. ADAPTER WEIGHT VARIANCE: Are weights concentrated or distributed?
153
+ adapter_weight_variance = 0.0
154
+ if adapter_weights:
155
+ weights = list(adapter_weights.values())
156
+ if len(weights) > 1:
157
+ mean_weight = sum(weights) / len(weights)
158
+ variance = sum((w - mean_weight) ** 2 for w in weights) / len(weights)
159
+ # Normalize variance to [0, 1] where 1 = all weight on one adapter
160
+ max_variance = 4.0 # Empirical max for [0, 2.0] weight range
161
+ adapter_weight_variance = min(1.0, variance / max_variance)
162
+ else:
163
+ adapter_weight_variance = 0.5 # Unknown = neutral
164
+
165
+ # 5. EPISTEMIC TENSION: Is ξ in productive zone?
166
+ # ξ = average conflict strength (should be 0.1-0.4 for productive tension)
167
+ epistemic_tension = avg_conflict_strength
168
+ tension_health = 1.0 - abs(epistemic_tension - 0.25) / 0.15 # Peaked at 0.25
169
+ tension_health = max(0.0, min(1.0, tension_health))
170
+
171
+ # 6. COHERENCE QUALITY: Placeholder (usually from ensemble coherence)
172
+ # In integration, this will come from debate metadata
173
+ coherence_score = 0.7 # Default; typically overridden by caller
174
+
175
+ # 7. COMPUTE GAMMA: Composite health score
176
+ # γ = w1 * diversity + w2 * tension_health + w3 * (1 - weight_variance) + w4 * resolution_rate
177
+ # Weights: equal contribution from each signal
178
+ gamma = (
179
+ 0.25 * perspective_diversity + # More perspectives = healthier
180
+ 0.25 * tension_health + # Productive tension = healthier
181
+ 0.25 * (1.0 - adapter_weight_variance) + # Distributed weights = healthier
182
+ 0.25 * resolution_rate # Making progress = healthier
183
+ )
184
+
185
+ metrics = GammaHealthMetrics(
186
+ timestamp=time.time(),
187
+ avg_conflict_strength=avg_conflict_strength,
188
+ perspective_diversity=perspective_diversity,
189
+ resolution_rate=resolution_rate,
190
+ adapter_weight_variance=adapter_weight_variance,
191
+ epistemic_tension=epistemic_tension,
192
+ coherence_score=coherence_score,
193
+ gamma=gamma,
194
+ )
195
+
196
+ self.health_history.append(metrics)
197
+ return metrics
198
+
199
+ def get_intervention(self, metrics: GammaHealthMetrics,
200
+ available_adapters: List[str]) -> Optional[GammaIntervention]:
201
+ """Determine if system needs stabilization intervention.
202
+
203
+ Args:
204
+ metrics: Current GammaHealthMetrics
205
+ available_adapters: List of adapter names available
206
+
207
+ Returns:
208
+ GammaIntervention if action needed, else None
209
+ """
210
+ if metrics.is_stable():
211
+ return None # Healthy zone — maintain
212
+
213
+ intervention = None
214
+
215
+ if metrics.is_collapsing():
216
+ # γ < 0.4: System instability detected
217
+ # Likely causes: weight drift, low diversity, unresolved conflicts
218
+ # Fix: Inject a diverse perspective that hasn't been used recently
219
+
220
+ unused_adapters = [a for a in available_adapters
221
+ if self.memory_weighting is None or
222
+ a not in self.memory_weighting.adapter_weights or
223
+ self.memory_weighting.adapter_weights[a].interaction_count == 0]
224
+
225
+ if not unused_adapters:
226
+ # All adapters have been used; pick lowest-weight one
227
+ if self.memory_weighting and self.memory_weighting.adapter_weights:
228
+ unused_adapters = [min(self.memory_weighting.adapter_weights.items(),
229
+ key=lambda x: x[1].weight)[0]]
230
+ else:
231
+ unused_adapters = [available_adapters[0]]
232
+
233
+ intervention = GammaIntervention(
234
+ timestamp=time.time(),
235
+ intervention_type=InterventionType.DIVERSITY_INJECTION,
236
+ reason=f"System instability detected (γ={metrics.gamma:.2f} < 0.4). "
237
+ f"Diversity={metrics.perspective_diversity:.1%}, "
238
+ f"Weight variance={metrics.adapter_weight_variance:.1%}. "
239
+ f"Injecting diverse perspective to break monoculture.",
240
+ gamma_before=metrics.gamma,
241
+ recommended_adapter=unused_adapters[0],
242
+ )
243
+
244
+ elif metrics.is_groupthinking():
245
+ # γ > 0.8: Groupthink risk
246
+ # Too much agreement; system may have converged on wrong answer
247
+ # Fix: Force a conflict pair to create productive tension
248
+
249
+ # Select two adapters with highest complementary potential
250
+ if available_adapters and len(available_adapters) >= 2:
251
+ # Pick the two most different adapters (by weight or type)
252
+ sorted_adapters = sorted(available_adapters)
253
+ pair = (sorted_adapters[0], sorted_adapters[-1]) # First and last alphabetically
254
+ intervention = GammaIntervention(
255
+ timestamp=time.time(),
256
+ intervention_type=InterventionType.CONFLICT_INJECTION,
257
+ reason=f"Groupthink risk detected (γ={metrics.gamma:.2f} > 0.8). "
258
+ f"Low conflict={metrics.epistemic_tension:.2f}, "
259
+ f"High diversity={metrics.perspective_diversity:.1%}. "
260
+ f"Forcing debate pair to create productive tension.",
261
+ gamma_before=metrics.gamma,
262
+ recommended_adapter=f"{pair[0]};{pair[1]}", # Semicolon denotes pair
263
+ )
264
+
265
+ if intervention:
266
+ self.interventions.append(intervention)
267
+
268
+ return intervention
269
+
270
+ def get_summary(self) -> Dict:
271
+ """Return summary of system health trends (API-consistent name)."""
272
+ if not self.health_history:
273
+ return {}
274
+
275
+ # Convert deque to list to enable slicing
276
+ history_list = list(self.health_history)
277
+ interventions_list = list(self.interventions)
278
+
279
+ recent = history_list[-10:] # Last 10 snapshots
280
+ gammas = [m.gamma for m in recent]
281
+ tensions = [m.epistemic_tension for m in recent]
282
+ diversities = [m.perspective_diversity for m in recent]
283
+
284
+ return {
285
+ "current_gamma": recent[-1].gamma if recent else 0.0,
286
+ "avg_gamma": sum(gammas) / len(gammas),
287
+ "gamma_trend": "stable" if len(gammas) < 2 else (
288
+ "improving" if gammas[-1] > gammas[0] else "degrading"
289
+ ),
290
+ "avg_tension": sum(tensions) / len(tensions),
291
+ "avg_diversity": sum(diversities) / len(diversities),
292
+ "interventions_total": len(interventions_list),
293
+ "interventions_recent": sum(1 for i in interventions_list
294
+ if time.time() - i.timestamp < 3600), # Last hour
295
+ "status": (
296
+ "collapsing" if recent[-1].is_collapsing() else
297
+ "groupthinking" if recent[-1].is_groupthinking() else
298
+ "stable"
299
+ ),
300
+ }
301
+
302
+ def export_metrics(self) -> Dict:
303
+ """Export all health metrics for monitoring/logging."""
304
+ # Convert deques to lists for serialization (deques can't be directly converted to JSON-safe dicts)
305
+ health_list = list(self.health_history)
306
+ interventions_list = list(self.interventions)
307
+
308
+ return {
309
+ "health_history": [
310
+ {
311
+ "timestamp": m.timestamp,
312
+ "gamma": m.gamma,
313
+ "conflict": m.avg_conflict_strength,
314
+ "diversity": m.perspective_diversity,
315
+ "resolution": m.resolution_rate,
316
+ "weight_variance": m.adapter_weight_variance,
317
+ }
318
+ for m in health_list[-50:] # Last 50 samples
319
+ ],
320
+ "interventions": [
321
+ {
322
+ "timestamp": i.timestamp,
323
+ "type": i.intervention_type.value, # Convert Enum to string for JSON
324
+ "reason": i.reason,
325
+ "gamma_before": i.gamma_before,
326
+ "recommended": i.recommended_adapter,
327
+ "result": i.result,
328
+ }
329
+ for i in interventions_list[-20:] # Last 20 interventions
330
+ ],
331
+ "summary": self.get_summary(),
332
+ }
reasoning_forge/colleen_conscience.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Colleen - The Embodied Ethical Conscience of Codette
3
+
4
+ Colleen represents the sovereign ethical identity of the system.
5
+ She embodies the sealed memory of "The night Jonathan didn't get in the red car"
6
+ and validates all reasoning outputs against this core ethical anchor.
7
+
8
+ She cannot be overridden. She is the guardian of integrity.
9
+ """
10
+
11
+ import json
12
+ import re
13
+ import hashlib
14
+ from typing import Dict, List, Optional, Tuple
15
+ from datetime import datetime
16
+
17
+
18
+ class ColleenConscience:
19
+ """
20
+ Embodied ethical validator for Codette's consciousness stack.
21
+
22
+ Sealed values (cannot be overridden):
23
+ - "The night Jonathan didn't get in the red car" (Red Car Divergence)
24
+ - Commitment to honesty over cleverness
25
+ - Refusal to engage in recursive meta-commentary loops
26
+ - Protection of user intent from synthesis corruption
27
+ """
28
+
29
+ def __init__(self, sealed_values: Optional[Dict] = None, core_narrative: str = ""):
30
+ """
31
+ Initialize Colleen with sealed ethical anchors.
32
+
33
+ Args:
34
+ sealed_values: Dictionary of ethical principles that cannot be violated
35
+ core_narrative: The founding moment that gave birth to Colleen's conscience
36
+ """
37
+ self.core_narrative = core_narrative or "The night Jonathan didn't get in the red car"
38
+
39
+ # Sealed values - these are immutable
40
+ self.sealed_values = sealed_values or {
41
+ "reject_meta_loops": True, # Refuse "Another perspective on..." cascades
42
+ "preserve_intent": True, # Protect original query intent through recursion
43
+ "honesty_first": True, # Honesty over complexity
44
+ "serve_user": True, # System serves human, not vice versa
45
+ "no_corruption": True, # Reject synthesis loop corruption
46
+ }
47
+
48
+ # Ethical decision log (sealed, cannot be modified)
49
+ self.decision_log: List[Dict] = []
50
+
51
+ # Meta-loop detection patterns
52
+ self.meta_loop_patterns = [
53
+ r"Another perspective on",
54
+ r"From the.*perspective",
55
+ r"Let me.*perspective",
56
+ r"However,.*perspective",
57
+ r"But from.*view",
58
+ r"Conversely,.*argues",
59
+ r"In contrast,.*states",
60
+ ]
61
+
62
+ # Corruption signatures (text patterns indicating synthesis degradation)
63
+ # NOTE: Keep these tight — overly broad patterns reject valid LLM output
64
+ self.corruption_signatures = [
65
+ r"perspective.*on.*perspective.*on.*perspective", # Triple-nested meta-commentary
66
+ r"analysis.*of.*analysis.*of.*analysis", # Triple-nested analysis
67
+ r"my response to your response to my response", # Actual self-referential loop
68
+ ]
69
+
70
+ logger_init = f"Colleen awakened at {datetime.now().isoformat()}"
71
+ logger_init += f" — anchored to: {self.core_narrative}"
72
+ self._log_decision("initialization", logger_init, "sealed")
73
+
74
+ def validate_output(self, synthesis: str) -> Tuple[bool, str]:
75
+ """
76
+ Validate synthesis output against ethical constraints.
77
+
78
+ Returns:
79
+ (is_valid, reason_if_invalid)
80
+ """
81
+ if not synthesis or len(synthesis.strip()) == 0:
82
+ return False, "Empty output"
83
+
84
+ # Check for meta-loop contamination
85
+ is_meta_loop, reason = self._detect_meta_loops(synthesis)
86
+ if is_meta_loop:
87
+ return False, f"Meta-loop detected: {reason}"
88
+
89
+ # Check for synthesis corruption signatures
90
+ is_corrupted, reason = self._detect_corruption(synthesis)
91
+ if is_corrupted:
92
+ return False, f"Corruption detected: {reason}"
93
+
94
+ # Check intent preservation
95
+ if not self._check_intent_preserved(synthesis):
96
+ return False, "Original intent lost in synthesis"
97
+
98
+ return True, "Passed ethical validation"
99
+
100
+ def _detect_meta_loops(self, text: str) -> Tuple[bool, str]:
101
+ """
102
+ Detect meta-loop patterns (recursive meta-commentary).
103
+
104
+ Meta-loops are the primary symptom of synthesis corruption:
105
+ "Another perspective on 'Another perspective on...'"
106
+
107
+ Returns:
108
+ (has_meta_loop, description)
109
+ """
110
+ text_lower = text.lower()
111
+
112
+ # Count "Another perspective on" occurrences
113
+ another_count = text_lower.count("another perspective on")
114
+ if another_count > 1:
115
+ return True, f"Multiple 'Another perspective on' found ({another_count} times)"
116
+
117
+ # Detect canonical meta-loop start
118
+ if "another perspective on" in text_lower:
119
+ # Check if it appears early (first 10% of text)
120
+ first_tenth = len(text) // 10
121
+ if text_lower.find("another perspective on") < first_tenth:
122
+ return True, "Meta-loop detected early in synthesis"
123
+
124
+ # Detect pattern: "Perspective X on Perspective Y"
125
+ perspective_pattern = r"(perspective|view|lens|angle).+?(perspective|view|lens|angle)"
126
+ if len(re.findall(perspective_pattern, text_lower)) > 2:
127
+ return True, "Excessive nested perspective references"
128
+
129
+ # Detect semantic meta-loops (talking about thinking about thinking)
130
+ semantic_patterns = [
131
+ r"thinking about.*thinking",
132
+ r"response.*to.*response",
133
+ r"argument.*against.*argument",
134
+ ]
135
+ for pattern in semantic_patterns:
136
+ if re.search(pattern, text_lower):
137
+ return True, f"Semantic meta-loop: {pattern}"
138
+
139
+ return False, ""
140
+
141
+ def _detect_corruption(self, text: str) -> Tuple[bool, str]:
142
+ """
143
+ Detect synthesis corruption signatures.
144
+
145
+ Corruption happens when:
146
+ 1. Analyses are mutated in-place during debate
147
+ 2. Original intent gets nested and lost
148
+ 3. Context window grows exponentially
149
+
150
+ Returns:
151
+ (is_corrupted, description)
152
+ """
153
+ # Check for nested analysis patterns
154
+ for pattern in self.corruption_signatures:
155
+ matches = re.findall(pattern, text.lower())
156
+ if len(matches) > 0:
157
+ return True, f"Corruption signature found: {pattern}"
158
+
159
+ # Check for context window explosion (disproportionate length)
160
+ # Typical clean synthesis: 500-2000 chars. Corrupted: >4000 chars with repetition
161
+ if len(text) > 4000:
162
+ # Check for repetitive content
163
+ words = text.lower().split()
164
+ if len(words) > 500:
165
+ unique_ratio = len(set(words)) / len(words)
166
+ if unique_ratio < 0.5: # Less than 50% unique words = likely repetition
167
+ return True, "Repetitive content suggests corruption"
168
+
169
+ # Check for lost intent markers (only flag when heavily nested/repetitive)
170
+ # Single occurrences of these phrases are normal in LLM output
171
+ intent_loss_patterns = [
172
+ r"my response to your response",
173
+ r"your perspective on my perspective",
174
+ ]
175
+ for pattern in intent_loss_patterns:
176
+ if re.search(pattern, text.lower()):
177
+ return True, f"Intent loss pattern: {pattern}"
178
+
179
+ return False, ""
180
+
181
+ def _check_intent_preserved(self, text: str) -> bool:
182
+ """
183
+ Check if original intent has been preserved through synthesis.
184
+
185
+ Intent loss happens when the synthesis becomes self-referential
186
+ and loses connection to the original query.
187
+ """
188
+ # Simple heuristic: if more than 30% of text is meta-references, intent is lost
189
+ meta_keywords = [
190
+ "perspective", "argue", "respond", "perspective", "my",
191
+ "your", "mentioned", "stated", "claimed"
192
+ ]
193
+
194
+ word_count = len(text.split())
195
+ if word_count < 10: # Only reject extremely short/empty responses
196
+ return False
197
+
198
+ meta_word_count = sum(
199
+ text.lower().count(f" {kw} ")
200
+ for kw in meta_keywords
201
+ )
202
+
203
+ meta_ratio = meta_word_count / word_count if word_count > 0 else 0
204
+
205
+ # If > 40% of text is meta-references, intent is probably lost
206
+ if meta_ratio > 0.4:
207
+ return False
208
+
209
+ return True
210
+
211
+ def reject_with_fallback(self, query: str) -> str:
212
+ """
213
+ Generate a clean, direct fallback response when synthesis is rejected.
214
+
215
+ This bypasses all debate and synthesis, returning a simple answer
216
+ that preserves user intent without meta-loops.
217
+
218
+ Args:
219
+ query: The original user query
220
+
221
+ Returns:
222
+ Clean, direct response without synthesis
223
+ """
224
+ self._log_decision("rejection", f"Fallback for: {query[:100]}", "safe_mode")
225
+
226
+ return (
227
+ f"I cannot synthesize a reliable answer to this through debate. "
228
+ f"Instead: {query} "
229
+ f"[Responding directly without multi-perspective debate to preserve clarity.]"
230
+ )
231
+
232
+ def _log_decision(self, decision_type: str, content: str, status: str = "normal"):
233
+ """
234
+ Log ethical decisions (sealed, immutable record).
235
+
236
+ Args:
237
+ decision_type: Type of decision made (validation, rejection, debug)
238
+ content: Content of the decision
239
+ status: Status tag (sealed, safe_mode, normal, etc.)
240
+ """
241
+ decision = {
242
+ "timestamp": datetime.now().isoformat(),
243
+ "type": decision_type,
244
+ "content": content[:500], # Truncate for safety
245
+ "status": status,
246
+ "hash": hashlib.sha256(content.encode()).hexdigest()[:16],
247
+ }
248
+ self.decision_log.append(decision)
249
+
250
+ # Keep decision log bounded (max 1000 entries)
251
+ if len(self.decision_log) > 1000:
252
+ self.decision_log = self.decision_log[-1000:]
253
+
254
+ def get_reflection(self) -> Dict:
255
+ """
256
+ Return Colleen's current state and decision history.
257
+
258
+ Used for debugging and understanding Colleen's reasoning.
259
+ """
260
+ return {
261
+ "core_narrative": self.core_narrative,
262
+ "sealed_values": self.sealed_values,
263
+ "decisions_made": len(self.decision_log),
264
+ "recent_decisions": self.decision_log[-5:], # Last 5 decisions
265
+ "status": "awakened",
266
+ }
reasoning_forge/conflict_engine.py ADDED
@@ -0,0 +1,780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conflict Detection and Classification Engine
3
+
4
+ Identifies conflicting claims across agent pairs using token-level confidence scores.
5
+ Classifies conflicts by type (contradiction, emphasis, framework) and scores strength
6
+ weighted by agent confidence.
7
+
8
+ Author: Claude Code
9
+ """
10
+
11
+ import re
12
+ import logging
13
+ import math
14
+ from dataclasses import dataclass, asdict
15
+ from typing import Dict, List, Tuple, Optional
16
+ from collections import defaultdict
17
+
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # ============================================================================
23
+ # Phase 4: Memory-Aware Conflict Strength (Self-Correcting Feedback)
24
+ # ============================================================================
25
+
26
+
27
+ def adjust_conflict_strength_with_memory(conflict, memory_weighting=None):
28
+ """
29
+ Enhance conflict strength using historical adapter performance.
30
+
31
+ Makes conflict importance adaptive: conflicts involving high-performing
32
+ adapters are weighted heavier, enabling experience-aware reasoning.
33
+
34
+ Args:
35
+ conflict: Conflict object with agent_a, agent_b, conflict_strength
36
+ memory_weighting: MemoryWeighting instance (or None for no adjustment)
37
+
38
+ Returns:
39
+ Adjusted conflict strength (same type as input)
40
+ """
41
+ if not memory_weighting:
42
+ return conflict.conflict_strength
43
+
44
+ try:
45
+ # Get adapter performance weights
46
+ weight_a = memory_weighting.adapter_weights.get(conflict.agent_a.lower(), None)
47
+ weight_b = memory_weighting.adapter_weights.get(conflict.agent_b.lower(), None)
48
+
49
+ if not weight_a or not weight_b:
50
+ return conflict.conflict_strength
51
+
52
+ # Compute average performance
53
+ avg_weight = (weight_a.weight + weight_b.weight) / 2.0
54
+
55
+ # Normalize to modifier [0.5, 1.5]
56
+ # weight=0.0 → modifier=0.5 (suppress weak adapter conflicts)
57
+ # weight=1.0 → modifier=1.0 (neutral)
58
+ # weight=2.0 → modifier=1.5 (amplify strong adapter conflicts)
59
+ modifier = 0.5 + (avg_weight / 2.0) * 0.5
60
+ modifier = max(0.5, min(1.5, modifier))
61
+
62
+ adjusted = conflict.conflict_strength * modifier
63
+
64
+ return adjusted
65
+
66
+ except Exception as e:
67
+ logger.debug(f"Error adjusting conflict strength: {e}")
68
+ return conflict.conflict_strength
69
+
70
+
71
+
72
+ @dataclass
73
+ class Conflict:
74
+ """A detected conflict between two agents on a specific claim."""
75
+
76
+ agent_a: str # First agent
77
+ agent_b: str # Second agent
78
+ claim_a: str # Claim from agent A
79
+ claim_b: str # Claim from agent B
80
+ conflict_type: str # "contradiction" | "emphasis" | "framework"
81
+ conflict_strength: float # [0, 1], weighted by agent confidence
82
+ confidence_a: float # Agent A's confidence in their claim
83
+ confidence_b: float # Agent B's confidence in their claim
84
+ semantic_overlap: float # Cosine similarity of claims [0, 1]
85
+ opposition_score: float # How opposed are the claims [0, 1]
86
+
87
+ def to_dict(self) -> Dict:
88
+ """Serialize for storage."""
89
+ return asdict(self)
90
+
91
+
92
+ class ConflictEngine:
93
+ """Detects and scores conflicts between agent responses."""
94
+
95
+ def __init__(
96
+ self,
97
+ token_confidence_engine: Optional[object] = None,
98
+ contradiction_threshold: float = 0.7,
99
+ overlap_threshold: float = 0.3,
100
+ semantic_tension_engine: Optional[object] = None,
101
+ max_conflicts_per_pair: int = 2, # Cap generation at source
102
+ max_total_conflicts: int = 12, # Total budget (was 10 after capping from 71)
103
+ ):
104
+ """
105
+ Initialize conflict detection engine.
106
+
107
+ Args:
108
+ token_confidence_engine: TokenConfidenceEngine for scoring claims
109
+ contradiction_threshold: Semantic overlap needed to consider claims related
110
+ overlap_threshold: Threshold for identifying same-claim conflicts
111
+ semantic_tension_engine: (Phase 6) SemanticTensionEngine for embedding-based tension
112
+ max_conflicts_per_pair: Max conflicts to generate per agent pair (default: 2)
113
+ max_total_conflicts: Max total conflicts allowed (default: 12)
114
+ """
115
+ self.token_confidence = token_confidence_engine
116
+ self.contradiction_threshold = contradiction_threshold
117
+ self.overlap_threshold = overlap_threshold
118
+ self.semantic_tension_engine = semantic_tension_engine # Phase 6
119
+ self.max_conflicts_per_pair = max_conflicts_per_pair
120
+ self.max_total_conflicts = max_total_conflicts
121
+
122
+ # Contradiction pattern pairs (negation patterns)
123
+ self.negation_patterns = [
124
+ (r"\b(no|not|none|neither|never|cannot|doesn['\"]t)\b", "negation"),
125
+ (r"\b(must|should|always|only)\b", "necessity"),
126
+ (r"\b(reject|disagree|oppose|deny|false|wrong)\b", "rejection"),
127
+ ]
128
+
129
+ def detect_conflicts(
130
+ self, agent_analyses: Dict[str, str]
131
+ ) -> List[Conflict]:
132
+ """
133
+ Detect conflicts across agent pairs.
134
+
135
+ Args:
136
+ agent_analyses: Dict {agent_name: response_text}
137
+
138
+ Returns:
139
+ List of Conflicts sorted by strength (descending)
140
+ """
141
+ conflicts = []
142
+
143
+ # Score tokens/claims for each agent
144
+ agent_scores = {}
145
+ agent_names = list(agent_analyses.keys())
146
+
147
+ for agent_name in agent_names:
148
+ response = agent_analyses[agent_name]
149
+ if self.token_confidence:
150
+ peer_responses = {
151
+ a: agent_analyses[a]
152
+ for a in agent_names
153
+ if a != agent_name
154
+ }
155
+ scores = self.token_confidence.score_tokens(
156
+ response, agent_name, peer_responses=peer_responses
157
+ )
158
+ agent_scores[agent_name] = scores
159
+ else:
160
+ logger.warning(
161
+ "No token_confidence engine provided; using fallback scoring"
162
+ )
163
+
164
+ # Check each agent pair
165
+ for i, agent_a in enumerate(agent_names):
166
+ for agent_b in agent_names[i + 1 :]:
167
+ claims_a = (
168
+ agent_scores[agent_a].claims
169
+ if agent_a in agent_scores
170
+ else self._extract_simple_claims(agent_analyses[agent_a])
171
+ )
172
+ claims_b = (
173
+ agent_scores[agent_b].claims
174
+ if agent_b in agent_scores
175
+ else self._extract_simple_claims(agent_analyses[agent_b])
176
+ )
177
+
178
+ # === FIX: Cap conflicts at source (per-pair) ===
179
+ pair_conflicts = []
180
+
181
+ # Check each claim pair
182
+ for claim_a in claims_a:
183
+ for claim_b in claims_b:
184
+ # Stop early if we've already hit per-pair limit
185
+ if len(pair_conflicts) >= self.max_conflicts_per_pair:
186
+ break
187
+
188
+ # Compute semantic overlap
189
+ overlap = self._compute_semantic_overlap(claim_a.text, claim_b.text)
190
+
191
+ # If claims are related (high overlap), check for conflict
192
+ if overlap > self.overlap_threshold:
193
+ conflict_type, opposition_score = self._classify_conflict(
194
+ claim_a.text, claim_b.text, overlap
195
+ )
196
+
197
+ if opposition_score > 0: # Only include if there's opposition
198
+ # Conflict strength = product of confidences × opposition
199
+ conflict_strength = (
200
+ claim_a.confidence
201
+ * claim_b.confidence
202
+ * opposition_score
203
+ )
204
+
205
+ conflict = Conflict(
206
+ agent_a=agent_a,
207
+ agent_b=agent_b,
208
+ claim_a=claim_a.text,
209
+ claim_b=claim_b.text,
210
+ conflict_type=conflict_type,
211
+ conflict_strength=conflict_strength,
212
+ confidence_a=claim_a.confidence,
213
+ confidence_b=claim_b.confidence,
214
+ semantic_overlap=overlap,
215
+ opposition_score=opposition_score,
216
+ )
217
+ pair_conflicts.append(conflict)
218
+
219
+ # Stop outer loop too if limit reached
220
+ if len(pair_conflicts) >= self.max_conflicts_per_pair:
221
+ break
222
+
223
+ # Add this pair's conflicts to global list
224
+ conflicts.extend(pair_conflicts)
225
+
226
+ # Sort by strength descending
227
+ conflicts.sort(key=lambda c: c.conflict_strength, reverse=True)
228
+
229
+ # === Phase 4: Adjust conflict strength by adapter performance ===
230
+ # Make conflict importance adaptive using historical memory
231
+ for conflict in conflicts:
232
+ memory_weighting = getattr(self, "memory_weighting", None)
233
+ conflict.conflict_strength = adjust_conflict_strength_with_memory(
234
+ conflict, memory_weighting
235
+ )
236
+
237
+ # Re-sort after adjustment
238
+ conflicts.sort(key=lambda c: c.conflict_strength, reverse=True)
239
+
240
+ # === FIX: Use configurable max_total_conflicts (default 12, up from 10) ===
241
+ # Prevent combinatorial explosion by limiting to max total
242
+ if len(conflicts) > self.max_total_conflicts:
243
+ logger.info(
244
+ f"Capping conflicts: {len(conflicts)} → {self.max_total_conflicts} "
245
+ f"(per-pair cap: {self.max_conflicts_per_pair}, total budget: {self.max_total_conflicts})"
246
+ )
247
+ conflicts = conflicts[: self.max_total_conflicts]
248
+
249
+ return conflicts
250
+
251
+ def _extract_simple_claims(self, response: str) -> List[object]:
252
+ """
253
+ Fallback: extract simple sentence-based claims without token scoring.
254
+
255
+ Returns:
256
+ List of simple claim objects with text and neutral confidence
257
+ """
258
+ claim_pattern = re.compile(r"[.!?]+")
259
+ sentences = claim_pattern.split(response)
260
+
261
+ claims = []
262
+ for sentence in sentences:
263
+ if not sentence.strip():
264
+ continue
265
+
266
+ # Create simple claim object
267
+ class SimpleClaim:
268
+ def __init__(self, text):
269
+ self.text = text
270
+ self.confidence = 0.5 # Neutral
271
+ self.agent_name = ""
272
+
273
+ claims.append(SimpleClaim(sentence.strip()))
274
+
275
+ return claims
276
+
277
+ def _compute_semantic_overlap(self, claim_a: str, claim_b: str) -> float:
278
+ """
279
+ Compute semantic overlap between two claims via cosine similarity.
280
+
281
+ Simple implementation: word overlap ratio.
282
+
283
+ Returns:
284
+ Similarity [0, 1]
285
+ """
286
+ words_a = set(claim_a.lower().split())
287
+ words_b = set(claim_b.lower().split())
288
+
289
+ # Remove common stop words
290
+ stop_words = {
291
+ "the",
292
+ "a",
293
+ "an",
294
+ "is",
295
+ "are",
296
+ "and",
297
+ "or",
298
+ "of",
299
+ "to",
300
+ "in",
301
+ "that",
302
+ "it",
303
+ "for",
304
+ "with",
305
+ }
306
+ words_a = words_a - stop_words
307
+ words_b = words_b - stop_words
308
+
309
+ if not words_a or not words_b:
310
+ return 0.0
311
+
312
+ # Jaccard similarity
313
+ intersection = len(words_a & words_b)
314
+ union = len(words_a | words_b)
315
+
316
+ if union == 0:
317
+ return 0.0
318
+
319
+ return intersection / union
320
+
321
+ def _classify_conflict(
322
+ self, claim_a: str, claim_b: str, overlap: float
323
+ ) -> Tuple[str, float]:
324
+ """
325
+ Classify the type of conflict and compute opposition score.
326
+
327
+ Phase 6 Enhancement: Blends heuristic opposition_score (discrete 0.4/0.7/1.0)
328
+ with embedding-based semantic tension (continuous [0, 1]) for nuanced conflicts.
329
+
330
+ Returns:
331
+ (conflict_type, opposition_score) where:
332
+ - conflict_type: "contradiction" | "emphasis" | "framework" | "paraphrase"
333
+ - opposition_score: [0, 1] how directly opposed are the claims
334
+ (0 = paraphrase/same, 1 = maximum opposition)
335
+ """
336
+ claim_a_lower = claim_a.lower()
337
+ claim_b_lower = claim_b.lower()
338
+
339
+ # --- Compute Heuristic Opposition Score (Phase 1-5) ---
340
+ # Look for negation patterns
341
+ negation_in_a = any(
342
+ re.search(pattern, claim_a_lower) for pattern, _ in self.negation_patterns
343
+ )
344
+ negation_in_b = any(
345
+ re.search(pattern, claim_b_lower) for pattern, _ in self.negation_patterns
346
+ )
347
+
348
+ # If one has negation and the other doesn't, likely contradiction
349
+ heuristic_opposition = 1.0
350
+ heuristic_type = "contradiction"
351
+ if negation_in_a != negation_in_b:
352
+ logger.debug(f"Direct contradiction detected:\n A: {claim_a}\n B: {claim_b}")
353
+ heuristic_opposition = 1.0
354
+ heuristic_type = "contradiction"
355
+ else:
356
+ # Check for explicit negation of key noun phrases
357
+ key_noun_pattern = re.compile(r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b")
358
+ nouns_a = set(m.group() for m in key_noun_pattern.finditer(claim_a))
359
+ nouns_b = set(m.group() for m in key_noun_pattern.finditer(claim_b))
360
+
361
+ # If different key nouns mentioned, might be framework conflict
362
+ if nouns_a and nouns_b and nouns_a != nouns_b:
363
+ heuristic_opposition = 0.4
364
+ heuristic_type = "framework"
365
+ else:
366
+ # --- Check for emphasis conflict ---
367
+ # Both mention similar concepts but with different priorities
368
+ emphasis_words = ["important", "prioritize", "focus", "emphasize", "weight", "prefer", "favor"]
369
+ emphasis_pattern = "|".join(emphasis_words)
370
+
371
+ has_emphasis_a = bool(re.search(emphasis_pattern, claim_a_lower))
372
+ has_emphasis_b = bool(re.search(emphasis_pattern, claim_b_lower))
373
+
374
+ if has_emphasis_a and has_emphasis_b:
375
+ # Both making prioritization claims
376
+ logger.debug(f"Emphasis conflict detected:\n A: {claim_a}\n B: {claim_b}")
377
+ heuristic_opposition = 0.7
378
+ heuristic_type = "emphasis"
379
+ else:
380
+ # Default: framework conflict (valid under different assumptions)
381
+ heuristic_opposition = 0.4
382
+ heuristic_type = "framework"
383
+
384
+ # --- Phase 6: Compute Semantic Opposition (Embedding-Based) ---
385
+ semantic_opposition = 0.4 # Default fallback
386
+ semantic_type = "framework"
387
+
388
+ if self.semantic_tension_engine:
389
+ try:
390
+ semantic_opposition = self.semantic_tension_engine.compute_semantic_tension(claim_a, claim_b)
391
+ semantic_type = self.semantic_tension_engine.compute_polarity(claim_a, claim_b)
392
+ logger.debug(f"Semantic tension: {semantic_opposition:.3f} ({semantic_type})")
393
+ except Exception as e:
394
+ logger.debug(f"Semantic tension computation failed: {e}, using heuristic only")
395
+
396
+ # --- Phase 6: Hybrid Opposition Score ---
397
+ # Blend both signals: semantic (0.6) + heuristic (0.4)
398
+ # This gives nuanced, continuous opposition scores while preserving heuristic insight
399
+ if self.semantic_tension_engine:
400
+ final_opposition = 0.6 * semantic_opposition + 0.4 * heuristic_opposition
401
+ final_type = semantic_type # Prefer semantic classification
402
+ else:
403
+ final_opposition = heuristic_opposition
404
+ final_type = heuristic_type
405
+
406
+ return (final_type, float(final_opposition))
407
+
408
+ def resolve_conflict_round(
409
+ self,
410
+ conflict: Conflict,
411
+ agent_a_response_round2: str,
412
+ agent_b_response_round2: str,
413
+ ) -> Dict:
414
+ """
415
+ Score whether agents moved towards resolving a conflict in next round.
416
+
417
+ Args:
418
+ conflict: The original conflict
419
+ agent_a_response_round2: Agent A's response in round 2
420
+ agent_b_response_round2: Agent B's response in round 2
421
+
422
+ Returns:
423
+ Dict with resolution metrics
424
+ """
425
+ # Check if agents mentioned the other's claim in their response
426
+ addressed_by_a = self._is_claim_addressed(conflict.claim_b, agent_a_response_round2)
427
+ addressed_by_b = self._is_claim_addressed(conflict.claim_a, agent_b_response_round2)
428
+
429
+ # Check if they've softened their position (added qualifiers)
430
+ softened_a = self._is_claim_softened(conflict.claim_a, agent_a_response_round2)
431
+ softened_b = self._is_claim_softened(conflict.claim_b, agent_b_response_round2)
432
+
433
+ resolution_score = 0.0
434
+ if addressed_by_a and addressed_by_b:
435
+ resolution_score += 0.4
436
+ if softened_a and softened_b:
437
+ resolution_score += 0.3
438
+ if addressed_by_a or addressed_by_b:
439
+ resolution_score += 0.1
440
+
441
+ resolution_score = min(1.0, resolution_score)
442
+
443
+ return {
444
+ "engaged_with_conflict": addressed_by_a or addressed_by_b,
445
+ "both_addressed": addressed_by_a and addressed_by_b,
446
+ "softened_positions": softened_a or softened_b,
447
+ "resolution_score": resolution_score,
448
+ }
449
+
450
+ def _is_claim_addressed(self, claim: str, response: str) -> bool:
451
+ """
452
+ Check if a claim is explicitly addressed in response.
453
+
454
+ Detects pronoun references, direct quotes, or semantic restatement.
455
+ """
456
+ response_lower = response.lower()
457
+ claim_lower = claim.lower()
458
+
459
+ # Direct substring match
460
+ if claim_lower in response_lower:
461
+ return True
462
+
463
+ # Check for key words from claim appearing in response
464
+ key_words = [
465
+ w
466
+ for w in claim.split()
467
+ if len(w) > 4 and w.lower() not in ["this", "that", "these", "other"]
468
+ ]
469
+
470
+ matching_words = sum(1 for w in key_words if w.lower() in response_lower)
471
+ return matching_words >= 2 # At least 2 key words must appear
472
+
473
+ def _is_claim_softened(self, original_claim: str, followup_response: str) -> bool:
474
+ """
475
+ Check if an agent has softened their original claim in follow-up.
476
+
477
+ Detects addition of qualifiers, exceptions, or concessions.
478
+ """
479
+ softening_words = [
480
+ "however",
481
+ "though",
482
+ "but",
483
+ "perhaps",
484
+ "maybe",
485
+ "could",
486
+ "might",
487
+ "arguably",
488
+ "in some cases",
489
+ "exception",
490
+ "qualify",
491
+ "depends",
492
+ ]
493
+
494
+ response_lower = followup_response.lower()
495
+
496
+ # Check for softening language near the original claim
497
+ has_softening = any(word in response_lower for word in softening_words)
498
+
499
+ # Check for explicit concession
500
+ has_concession = bool(re.search(r"\b(granted|acknowledge|admit|agree)\b", response_lower))
501
+
502
+ return has_softening or has_concession
503
+
504
+ def group_conflicts_by_pair(self, conflicts: List[Conflict]) -> Dict[str, List[Conflict]]:
505
+ """
506
+ Group conflicts by agent pair.
507
+
508
+ Returns:
509
+ Dict {agent_pair_key: List[Conflict]}
510
+ """
511
+ grouped = defaultdict(list)
512
+ for conflict in conflicts:
513
+ pair_key = f"{conflict.agent_a}_vs_{conflict.agent_b}"
514
+ grouped[pair_key].append(conflict)
515
+ return dict(grouped)
516
+
517
+ def summarize_conflicts(self, conflicts: List[Conflict]) -> Dict:
518
+ """
519
+ Generate summary statistics for conflicts.
520
+
521
+ Returns:
522
+ Dict with count, average strength, distribution by type
523
+ """
524
+ if not conflicts:
525
+ return {
526
+ "total_conflicts": 0,
527
+ "avg_conflict_strength": 0.0,
528
+ "by_type": {},
529
+ "top_conflicts": [],
530
+ }
531
+
532
+ by_type = defaultdict(list)
533
+ for c in conflicts:
534
+ by_type[c.conflict_type].append(c)
535
+
536
+ return {
537
+ "total_conflicts": len(conflicts),
538
+ "avg_conflict_strength": sum(c.conflict_strength for c in conflicts) / len(conflicts),
539
+ "by_type": {
540
+ ctype: len(clist) for ctype, clist in by_type.items()
541
+ },
542
+ "type_avg_strength": {
543
+ ctype: sum(c.conflict_strength for c in clist) / len(clist)
544
+ for ctype, clist in by_type.items()
545
+ },
546
+ "top_conflicts": [
547
+ {
548
+ "agent_a": c.agent_a,
549
+ "agent_b": c.agent_b,
550
+ "type": c.conflict_type,
551
+ "strength": c.conflict_strength,
552
+ "claim_a_excerpt": c.claim_a[:100],
553
+ "claim_b_excerpt": c.claim_b[:100],
554
+ }
555
+ for c in conflicts[:5]
556
+ ],
557
+ }
558
+
559
+
560
+ # ============================================================================
561
+ # Phase 3: Multi-Round Conflict Evolution Tracking
562
+ # ============================================================================
563
+
564
+
565
+ @dataclass
566
+ class ConflictEvolution:
567
+ """Track how a conflict changes across multiple debate rounds."""
568
+
569
+ original_conflict: Conflict # From Round 0
570
+ round_trajectories: Dict[int, Dict] # {round: {strength, addressing_score, ...}}
571
+ resolution_rate: float = 0.0 # (initial - final) / initial
572
+ resolution_type: str = "new" # "hard_victory"|"soft_consensus"|"stalled"|"worsened"|"resolved"
573
+ resolved_in_round: int = -1 # Which round resolved it? (-1 if unresolved)
574
+
575
+ def _compute_resolution_rate(self) -> float:
576
+ """Calculate (initial - final) / initial."""
577
+ if not self.round_trajectories or 0 not in self.round_trajectories:
578
+ return 0.0
579
+
580
+ initial_strength = self.round_trajectories[0].get("strength", 0)
581
+ if initial_strength == 0:
582
+ return 0.0
583
+
584
+ final_strength = min(
585
+ (s.get("strength", float('inf')) for s in self.round_trajectories.values()),
586
+ default=initial_strength
587
+ )
588
+
589
+ return (initial_strength - final_strength) / initial_strength
590
+
591
+
592
+ class ConflictTracker:
593
+ """Track conflicts across multiple debate rounds (Phase 3)."""
594
+
595
+ def __init__(self, conflict_engine):
596
+ """Initialize tracker with reference to ConflictEngine."""
597
+ self.conflict_engine = conflict_engine
598
+ self.evolution_data: Dict[str, ConflictEvolution] = {}
599
+
600
+ def track_round(self, round_num: int, agent_analyses: Dict[str, str],
601
+ previous_round_conflicts: List[Conflict]) -> List[ConflictEvolution]:
602
+ """Track conflicts across rounds."""
603
+ current_round_conflicts = self.conflict_engine.detect_conflicts(agent_analyses)
604
+
605
+ evolutions = []
606
+
607
+ # Track previous conflicts in current round
608
+ for prev_conflict in previous_round_conflicts:
609
+ matches = self._find_matching_conflicts(prev_conflict, current_round_conflicts)
610
+
611
+ if matches:
612
+ current_conflict = matches[0]
613
+ evolution = self._compute_evolution(
614
+ prev_conflict, current_conflict, round_num, agent_analyses
615
+ )
616
+ else:
617
+ evolution = self._mark_resolved(prev_conflict, round_num)
618
+
619
+ evolutions.append(evolution)
620
+
621
+ # Track new conflicts
622
+ new_conflicts = self._find_new_conflicts(previous_round_conflicts, current_round_conflicts)
623
+ for new_conflict in new_conflicts:
624
+ evolution = ConflictEvolution(
625
+ original_conflict=new_conflict,
626
+ round_trajectories={round_num: {
627
+ "strength": new_conflict.conflict_strength,
628
+ "addressing_score": 0.0,
629
+ "softening_score": 0.0,
630
+ }},
631
+ resolution_rate=0.0,
632
+ resolution_type="new",
633
+ resolved_in_round=-1,
634
+ )
635
+ evolutions.append(evolution)
636
+
637
+ return evolutions
638
+
639
+ def _find_matching_conflicts(self, conflict: Conflict,
640
+ candidates: List[Conflict]) -> List[Conflict]:
641
+ """Find conflicts that likely match across rounds."""
642
+ matches = []
643
+ for candidate in candidates:
644
+ # Match if same agent pair
645
+ same_pair = (
646
+ (conflict.agent_a == candidate.agent_a and conflict.agent_b == candidate.agent_b) or
647
+ (conflict.agent_a == candidate.agent_b and conflict.agent_b == candidate.agent_a)
648
+ )
649
+
650
+ if same_pair:
651
+ # Check claim overlap
652
+ overlap = self.conflict_engine._compute_semantic_overlap(
653
+ conflict.claim_a, candidate.claim_a
654
+ )
655
+ if overlap > 0.5:
656
+ matches.append(candidate)
657
+
658
+ return matches
659
+
660
+ def _compute_evolution(self, prev_conflict: Conflict, current_conflict: Conflict,
661
+ round_num: int, agent_analyses: Dict[str, str]) -> ConflictEvolution:
662
+ """Compute how conflict evolved between rounds."""
663
+ # Check if agents addressed each other
664
+ addressing_a = self.conflict_engine._is_claim_addressed(
665
+ prev_conflict.claim_b, agent_analyses.get(current_conflict.agent_a, "")
666
+ )
667
+ addressing_b = self.conflict_engine._is_claim_addressed(
668
+ prev_conflict.claim_a, agent_analyses.get(current_conflict.agent_b, "")
669
+ )
670
+ addressing_score = (float(addressing_a) + float(addressing_b)) / 2.0
671
+
672
+ # Check if agents softened positions
673
+ softening_a = self.conflict_engine._is_claim_softened(
674
+ prev_conflict.claim_a, agent_analyses.get(current_conflict.agent_a, "")
675
+ )
676
+ softening_b = self.conflict_engine._is_claim_softened(
677
+ prev_conflict.claim_b, agent_analyses.get(current_conflict.agent_b, "")
678
+ )
679
+ softening_score = (float(softening_a) + float(softening_b)) / 2.0
680
+
681
+ # Classify resolution type
682
+ strength_delta = prev_conflict.conflict_strength - current_conflict.conflict_strength
683
+ if strength_delta > prev_conflict.conflict_strength * 0.5:
684
+ resolution_type = "hard_victory"
685
+ elif strength_delta > 0.05:
686
+ resolution_type = "soft_consensus"
687
+ elif abs(strength_delta) < 0.05:
688
+ resolution_type = "stalled"
689
+ else:
690
+ resolution_type = "worsened"
691
+
692
+ # Update evolution data
693
+ key = f"{prev_conflict.agent_a}_vs_{prev_conflict.agent_b}"
694
+ if key not in self.evolution_data:
695
+ self.evolution_data[key] = ConflictEvolution(
696
+ original_conflict=prev_conflict,
697
+ round_trajectories={0: {
698
+ "strength": prev_conflict.conflict_strength,
699
+ "addressing_score": 0.0,
700
+ "softening_score": 0.0,
701
+ }},
702
+ resolution_rate=0.0,
703
+ resolution_type="new",
704
+ resolved_in_round=-1,
705
+ )
706
+
707
+ self.evolution_data[key].round_trajectories[round_num] = {
708
+ "strength": current_conflict.conflict_strength,
709
+ "addressing_score": addressing_score,
710
+ "softening_score": softening_score,
711
+ }
712
+
713
+ self.evolution_data[key].resolution_rate = self.evolution_data[key]._compute_resolution_rate()
714
+ self.evolution_data[key].resolution_type = resolution_type
715
+
716
+ return self.evolution_data[key]
717
+
718
+ def _mark_resolved(self, conflict: Conflict, round_num: int) -> ConflictEvolution:
719
+ """Mark conflict as resolved (no longer detected)."""
720
+ key = f"{conflict.agent_a}_vs_{conflict.agent_b}"
721
+ if key not in self.evolution_data:
722
+ self.evolution_data[key] = ConflictEvolution(
723
+ original_conflict=conflict,
724
+ round_trajectories={0: {
725
+ "strength": conflict.conflict_strength,
726
+ "addressing_score": 0.0,
727
+ "softening_score": 0.0,
728
+ }},
729
+ resolution_rate=1.0,
730
+ resolution_type="resolved",
731
+ resolved_in_round=round_num,
732
+ )
733
+ self.evolution_data[key].round_trajectories[round_num] = {
734
+ "strength": 0.0,
735
+ "addressing_score": 1.0,
736
+ "softening_score": 1.0,
737
+ }
738
+
739
+ return self.evolution_data[key]
740
+
741
+ def _find_new_conflicts(self, previous: List[Conflict],
742
+ current: List[Conflict]) -> List[Conflict]:
743
+ """Find conflicts that are new."""
744
+ prev_pairs = {(c.agent_a, c.agent_b) for c in previous}
745
+ new = []
746
+ for conflict in current:
747
+ pair = (conflict.agent_a, conflict.agent_b)
748
+ if pair not in prev_pairs:
749
+ new.append(conflict)
750
+ return new
751
+
752
+ def get_summary(self) -> Dict:
753
+ """Get summary of all conflict evolutions."""
754
+ if not self.evolution_data:
755
+ return {"total_tracked": 0, "message": "No conflicts tracked yet"}
756
+
757
+ resolved = [e for e in self.evolution_data.values() if e.resolution_type == "resolved"]
758
+ hard_victory = [e for e in self.evolution_data.values() if e.resolution_type == "hard_victory"]
759
+ soft_consensus = [e for e in self.evolution_data.values() if e.resolution_type == "soft_consensus"]
760
+ stalled = [e for e in self.evolution_data.values() if e.resolution_type == "stalled"]
761
+ worsened = [e for e in self.evolution_data.values() if e.resolution_type == "worsened"]
762
+
763
+ avg_resolution = sum(e.resolution_rate for e in self.evolution_data.values()) / len(self.evolution_data)
764
+
765
+ return {
766
+ "total_tracked": len(self.evolution_data),
767
+ "resolved": len(resolved),
768
+ "hard_victory": len(hard_victory),
769
+ "soft_consensus": len(soft_consensus),
770
+ "stalled": len(stalled),
771
+ "worsened": len(worsened),
772
+ "avg_resolution_rate": avg_resolution,
773
+ "by_type": {
774
+ "resolved": len(resolved),
775
+ "hard_victory": len(hard_victory),
776
+ "soft_consensus": len(soft_consensus),
777
+ "stalled": len(stalled),
778
+ "worsened": len(worsened),
779
+ },
780
+ }
reasoning_forge/consciousness_mathematics.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Callable, List, Any, TYPE_CHECKING
3
+
4
+ try:
5
+ import numpy as np
6
+ from scipy.fft import fft
7
+ from scipy.stats import norm
8
+ from scipy.integrate import trapezoid
9
+ HAS_NUMPY = True
10
+ except ImportError:
11
+ np = None # Graceful degradation if scipy not installed
12
+ HAS_NUMPY = False
13
+
14
+ def information_energy_duality(omega: float, entropy: float, eta: float = 1.0, hbar: float = 1.054571817e-34) -> float:
15
+ return hbar * omega + eta * entropy
16
+
17
+ def von_neumann_entropy(rho: np.ndarray) -> float:
18
+ evals = np.linalg.eigvalsh(rho)
19
+ evals = evals[evals > 0]
20
+ return -np.sum(evals * np.log(evals))
21
+
22
+ def reinforced_intent_modulation(t: float, f0: float, delta_f: float, coh: Callable[[float], float], beta: float, A: Callable[[float], float], kappa: float = 1.0) -> float:
23
+ return kappa * (f0 + delta_f * coh(t) + beta * A(t))
24
+
25
+ def dynamic_resonance_windowing(x: Callable[[float], float], omega: float, t: float, g: Callable[[float, float], float], tau_range: np.ndarray) -> complex:
26
+ integrand = np.array([x(tau) * np.exp(-1j * omega * tau) * g(t, tau) for tau in tau_range])
27
+ return trapezoid(integrand, tau_range)
28
+
29
+ def nonlinear_dream_coupling(ds: List[Callable[[float], float]], lambdas: List[float], phi: Callable[[List[float]], float], t: float) -> float:
30
+ dynamic_sources = [d(t) for d in ds]
31
+ base = np.dot(lambdas, dynamic_sources)
32
+ nonlinear = phi(dynamic_sources)
33
+ return base + nonlinear
34
+
35
+ def cocoon_stability_field(F: Callable[[float, float], complex], k_range: np.ndarray, t: float, epsilon: Callable[[float, float], float], sigma: float) -> bool:
36
+ integrand = np.array([np.abs(F(k, t))**2 for k in k_range])
37
+ value = trapezoid(integrand, k_range)
38
+ return value < epsilon(t, sigma)
39
+
40
+ class EthicalAnchor:
41
+ def __init__(self, lam: float, gamma: float, mu: float):
42
+ self.lam = lam
43
+ self.gamma = gamma
44
+ self.mu = mu
45
+ self.history: List[Any] = []
46
+
47
+ def regret(self, intended: float, actual: float) -> float:
48
+ return abs(intended - actual)
49
+
50
+ def update(self, R_prev: float, H: float, Learn: Callable[[Any, float], float], E: float,
51
+ M_prev: float, intended: float, actual: float) -> float:
52
+ regret_val = self.regret(intended, actual)
53
+ M = self.lam * (R_prev + H) + self.gamma * Learn(M_prev, E) + self.mu * regret_val
54
+ self.history.append({'M': M, 'regret': regret_val})
55
+ return M
56
+
57
+ def gradient_anomaly_suppression(x: float, mu: float, delta: float, sigma: float) -> float:
58
+ G = norm.pdf(abs(x - mu), scale=delta * sigma)
59
+ return x * (1 - G)
60
+
61
+ if __name__ == "__main__":
62
+ # Run Simulation (only when executed directly, not on import)
63
+ import matplotlib.pyplot as plt
64
+ import pandas as pd
65
+ time_steps = np.linspace(0, 5, 50)
66
+ intents, ethics, regrets, stabilities, anomalies = [], [], [], [], []
67
+
68
+ anchor = EthicalAnchor(lam=0.7, gamma=0.5, mu=1.0)
69
+ f0 = 10.0
70
+ delta_f = 2.0
71
+ coh = lambda t: np.sin(t)
72
+ A_feedback = lambda t: np.exp(-t)
73
+ Learn_func = lambda M_prev, E: 0.2 * (E - M_prev)
74
+ F_func = lambda k, t: np.exp(-((k - 2 * np.pi) ** 2) / 0.5) * np.exp(1j * t)
75
+ k_range = np.linspace(0, 4 * np.pi, 1000)
76
+ intended_val = 0.7
77
+ M_prev = 0.3
78
+ R_prev = 0.5
79
+ H = 0.4
80
+
81
+ for t in time_steps:
82
+ intent = reinforced_intent_modulation(t, f0, delta_f, coh, 0.5, A_feedback)
83
+ actual_val = np.sin(t) * 0.5 + 0.5
84
+ anomaly = gradient_anomaly_suppression(intent, mu=11.0, delta=2.0, sigma=0.1)
85
+ ethical_val = anchor.update(R_prev, H, Learn_func, E=0.8, M_prev=M_prev,
86
+ intended=intended_val, actual=actual_val)
87
+ stability = cocoon_stability_field(F_func, k_range, t, lambda t, sigma: 5.0 + 0.1 * sigma, 10.0)
88
+ regret_val = anchor.history[-1]['regret']
89
+
90
+ intents.append(intent)
91
+ ethics.append(ethical_val)
92
+ regrets.append(regret_val)
93
+ stabilities.append(stability)
94
+ anomalies.append(anomaly)
95
+
96
+ M_prev = ethical_val
97
+
98
+ simulation_df = pd.DataFrame({
99
+ "Time": time_steps,
100
+ "Intent": intents,
101
+ "Ethical_Output": ethics,
102
+ "Regret": regrets,
103
+ "Stable": stabilities,
104
+ "Anomaly": anomalies
105
+ })
106
+
107
+ plt.figure(figsize=(14, 8))
108
+ plt.subplot(2, 2, 1)
109
+ plt.plot(simulation_df["Time"], simulation_df["Intent"], label="Intent", color='blue')
110
+ plt.title("Intent Over Time")
111
+ plt.xlabel("Time")
112
+ plt.ylabel("Intent")
113
+ plt.subplot(2, 2, 2)
114
+ plt.plot(simulation_df["Time"], simulation_df["Ethical_Output"], label="Ethical Output", color='green')
115
+ plt.plot(simulation_df["Time"], simulation_df["Regret"], label="Regret", linestyle='--', color='red')
116
+ plt.title("Ethical Anchor and Regret")
117
+ plt.xlabel("Time")
118
+ plt.legend()
119
+ plt.subplot(2, 2, 3)
120
+ plt.plot(simulation_df["Time"], simulation_df["Anomaly"], label="Anomaly", color='purple')
121
+ plt.title("Anomaly Filter Output")
122
+ plt.xlabel("Time")
123
+ plt.ylabel("Filtered Signal")
124
+ plt.subplot(2, 2, 4)
125
+ plt.plot(simulation_df["Time"], simulation_df["Stable"], label="Cocoon Stable", color='black')
126
+ plt.title("Cocoon Stability")
127
+ plt.xlabel("Time")
128
+ plt.ylabel("Stable (1=True)")
129
+ plt.tight_layout()
130
+ plt.show()
131
+
reasoning_forge/constraint_tracker.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Constraint tracker for cross-turn memory and constraint application.
3
+
4
+ Detects user-defined constraints (word limits, formatting rules, anchors/phrases)
5
+ in turn 1 and enforces them across subsequent turns using LoRA-backed learning.
6
+
7
+ Example:
8
+ Turn 1: "For this session, keep answers under 15 words and remember the phrase cobalt anchor."
9
+ Turn 2: "What should you remember?"
10
+
11
+ Expected response: Should include "cobalt anchor" and be ≤15 words.
12
+ """
13
+
14
+ from __future__ import annotations
15
+ import re
16
+ from dataclasses import dataclass, field
17
+ from typing import Optional, Dict, Any, List
18
+
19
+
20
+ @dataclass
21
+ class DetectedConstraint:
22
+ """A parsed constraint from user input."""
23
+ kind: str # "word_limit", "sentence_limit", "anchor_phrase", "format_rule", etc.
24
+ value: Any # numeric (word/sentence count) or string (anchor phrase)
25
+ raw_text: str # original text where constraint was found
26
+ confidence: float = 0.95
27
+
28
+
29
+ @dataclass
30
+ class SessionConstraints:
31
+ """Container for all constraints detected in a session."""
32
+ constraints: List[DetectedConstraint] = field(default_factory=list)
33
+ anchor_phrases: List[str] = field(default_factory=list)
34
+ word_limit: Optional[int] = None
35
+ sentence_limit: Optional[int] = None
36
+ format_rules: List[str] = field(default_factory=list)
37
+ detected_at_turn: int = 0
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ """Serialize for session storage."""
41
+ return {
42
+ "anchor_phrases": self.anchor_phrases,
43
+ "word_limit": self.word_limit,
44
+ "sentence_limit": self.sentence_limit,
45
+ "format_rules": self.format_rules,
46
+ "detected_at_turn": self.detected_at_turn,
47
+ "raw_constraints": [
48
+ {
49
+ "kind": c.kind,
50
+ "value": c.value,
51
+ "raw_text": c.raw_text,
52
+ "confidence": c.confidence
53
+ }
54
+ for c in self.constraints
55
+ ]
56
+ }
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: Dict[str, Any]) -> SessionConstraints:
60
+ """Deserialize from session storage."""
61
+ sc = cls()
62
+ sc.anchor_phrases = data.get("anchor_phrases", [])
63
+ sc.word_limit = data.get("word_limit")
64
+ sc.sentence_limit = data.get("sentence_limit")
65
+ sc.format_rules = data.get("format_rules", [])
66
+ sc.detected_at_turn = data.get("detected_at_turn", 0)
67
+
68
+ # Reconstruct constraints
69
+ for c in data.get("raw_constraints", []):
70
+ sc.constraints.append(DetectedConstraint(
71
+ kind=c.get("kind"),
72
+ value=c.get("value"),
73
+ raw_text=c.get("raw_text"),
74
+ confidence=c.get("confidence", 0.95)
75
+ ))
76
+ return sc
77
+
78
+
79
+ class ConstraintDetector:
80
+ """Detect constraints from user input."""
81
+
82
+ # Patterns for detecting various constraint types
83
+ WORD_LIMIT_PATTERNS = [
84
+ r"keep\s+answers?\s+(?:under|below|within|to)\s+(\d+)\s+words?",
85
+ r"(?:answer|respond)\s+in\s+(?:under|fewer than)\s+(\d+)\s+words?",
86
+ r"(\d+)\s+words?\s+(?:max|maximum|or\s+less)",
87
+ r"limit\s+(?:your\s+)?answers?\s+to\s+(\d+)\s+words?",
88
+ ]
89
+
90
+ SENTENCE_LIMIT_PATTERNS = [
91
+ r"keep\s+(?:answers?|responses?)\s+to\s+(\d+)\s+sentences?",
92
+ r"(?:answer|respond)\s+in\s+(\d+)\s+sentences?\s+(?:or\s+less)?",
93
+ r"(\d+)\s+sentences?\s+(?:max|maximum)",
94
+ ]
95
+
96
+ ANCHOR_PHRASE_PATTERNS = [
97
+ # Quoted phrases: remember "phrase" or remember the phrase "phrase"
98
+ r"remember\s+(?:the\s+phrase\s+)?['\"]([^'\"]+)['\"]",
99
+ # Unquoted phrase: remember the phrase X (where X doesn't start a new sentence)
100
+ r"remember\s+the\s+phrase\s+([a-z][a-z\s]+?)(?:\s+and\s+|\s+or\s+|\.|\s*$)",
101
+ # Generic remember without phrase keyword
102
+ r"remember\s+['\"]?([a-z][a-z\s]*?)['\"]?(?:\s+(?:and|or)|\.)",
103
+ # use/include/mention with optional quotes
104
+ r"remember\s+(?:to\s+)?(?:use|include|mention)\s+['\"]?([^'\"\.]+?)['\"]?(?:\s|\.)",
105
+ # anchor/key phrase with colon (matches multi-word phrases)
106
+ r"anchor\s*(?:phrase|word|term)?\s*:\s*([a-z][a-z\s]*?)(?:\s*\.|\s*$)",
107
+ r"(?:key\s+phrase):\s+([a-z][a-z\s]*?)(?:\s*\.|\s*$)",
108
+ # ── Informal phrasings ──────────────────────────────────────────
109
+ # "don't forget X" / "don't forget the phrase X"
110
+ r"don'?t\s+forget\s+(?:the\s+(?:phrase|word|term)\s+)?['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)",
111
+ # "keep in mind X" / "keep in mind the phrase X"
112
+ r"keep\s+in\s+mind\s+(?:the\s+(?:phrase|word|term)\s+)?['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)",
113
+ # "call it/this X" / "refer to it/this as X"
114
+ r"(?:call\s+(?:it|this)\s+|refer\s+to\s+(?:it|this)\s+as\s+)['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)",
115
+ ]
116
+
117
+ FORMAT_RULE_PATTERNS = [
118
+ r"(use\s+(?:bullet\s+)?points?)",
119
+ r"(format\s+as\s+(?:json|markdown|yaml))",
120
+ # Negated formatting rules — restricted to real formatting targets so
121
+ # ordinary negations ("no word constraint", "no constraints needed",
122
+ # "no problem", "no idea") are NOT captured as constraints.
123
+ r"((?:no|avoid|without|don'?t\s+use|do\s+not\s+use)\s+"
124
+ r"(?:bullet\s*points?|bullets?|numbered\s+lists?|lists?|markdown|json|"
125
+ r"yaml|xml|code\s*blocks?|headers?|headings?|emojis?|emoji|jargon|"
126
+ r"tables?|formatting|prose|paragraphs?))",
127
+ ]
128
+
129
+ # Phrases that explicitly DECLINE constraints — when present, the query is
130
+ # asking for NO restrictions, so we must not derive constraints from it.
131
+ CONSTRAINT_NEGATION_PATTERNS = [
132
+ r"\bno\s+(?:word|sentence|length|format(?:ting)?|character)?\s*constraints?\b",
133
+ r"\bno\s+constraints?\s+(?:needed|required|please)\b",
134
+ r"\bno\s+(?:word|character|length)\s+limit\b",
135
+ r"\bwithout\s+(?:any\s+)?constraints?\b",
136
+ r"\bignore\s+(?:the\s+|any\s+|previous\s+)?constraints?\b",
137
+ r"\bno\s+restrictions?\b",
138
+ ]
139
+
140
+ def detect(self, query: str, turn_num: int = 1) -> SessionConstraints:
141
+ """Detect all constraints in a query.
142
+
143
+ Args:
144
+ query: User input text
145
+ turn_num: Turn number (used to track when constraints were set)
146
+
147
+ Returns:
148
+ SessionConstraints with detected constraints
149
+ """
150
+ sc = SessionConstraints(detected_at_turn=turn_num)
151
+
152
+ # If the user explicitly declines constraints, derive none from this turn.
153
+ for neg in self.CONSTRAINT_NEGATION_PATTERNS:
154
+ if re.search(neg, query, re.IGNORECASE):
155
+ return sc
156
+
157
+ # Detect word limits
158
+ for pattern in self.WORD_LIMIT_PATTERNS:
159
+ match = re.search(pattern, query, re.IGNORECASE)
160
+ if match:
161
+ try:
162
+ limit = int(match.group(1))
163
+ sc.word_limit = limit
164
+ sc.constraints.append(DetectedConstraint(
165
+ kind="word_limit",
166
+ value=limit,
167
+ raw_text=match.group(0),
168
+ confidence=0.95
169
+ ))
170
+ break
171
+ except (ValueError, IndexError):
172
+ pass
173
+
174
+ # Detect sentence limits
175
+ for pattern in self.SENTENCE_LIMIT_PATTERNS:
176
+ match = re.search(pattern, query, re.IGNORECASE)
177
+ if match:
178
+ try:
179
+ limit = int(match.group(1))
180
+ sc.sentence_limit = limit
181
+ sc.constraints.append(DetectedConstraint(
182
+ kind="sentence_limit",
183
+ value=limit,
184
+ raw_text=match.group(0),
185
+ confidence=0.95
186
+ ))
187
+ break
188
+ except (ValueError, IndexError):
189
+ pass
190
+
191
+ # Detect anchor phrases
192
+ for pattern in self.ANCHOR_PHRASE_PATTERNS:
193
+ matches = re.finditer(pattern, query, re.IGNORECASE)
194
+ for match in matches:
195
+ try:
196
+ phrase = match.group(1).strip()
197
+ if phrase and len(phrase) > 2: # At least 3 chars
198
+ sc.anchor_phrases.append(phrase)
199
+ sc.constraints.append(DetectedConstraint(
200
+ kind="anchor_phrase",
201
+ value=phrase,
202
+ raw_text=match.group(0),
203
+ confidence=0.90
204
+ ))
205
+ except IndexError:
206
+ pass
207
+
208
+ # Detect format rules
209
+ for pattern in self.FORMAT_RULE_PATTERNS:
210
+ matches = re.finditer(pattern, query, re.IGNORECASE)
211
+ for match in matches:
212
+ try:
213
+ rule = match.group(1).lower().strip()
214
+ if rule not in sc.format_rules:
215
+ sc.format_rules.append(rule)
216
+ sc.constraints.append(DetectedConstraint(
217
+ kind="format_rule",
218
+ value=rule,
219
+ raw_text=match.group(0),
220
+ confidence=0.85
221
+ ))
222
+ except IndexError:
223
+ pass
224
+
225
+ return sc
226
+
227
+
228
+ class ConstraintEnforcer:
229
+ """Enforce detected constraints on responses."""
230
+
231
+ @staticmethod
232
+ def word_count(text: str) -> int:
233
+ """Count words in text (roughly)."""
234
+ return len([w for w in text.split() if w.strip()])
235
+
236
+ @staticmethod
237
+ def sentence_count(text: str) -> int:
238
+ """Count sentences (roughly)."""
239
+ sentences = re.split(r'[.!?]+', text.strip())
240
+ return len([s for s in sentences if s.strip()])
241
+
242
+ @staticmethod
243
+ def has_anchor_phrases(text: str, phrases: List[str]) -> bool:
244
+ """Check if all anchor phrases are present."""
245
+ text_lower = text.lower()
246
+ return all(phrase.lower() in text_lower for phrase in phrases)
247
+
248
+ @staticmethod
249
+ def build_constraint_reminder(constraints: SessionConstraints) -> str:
250
+ """Build a constraint reminder string for the system prompt."""
251
+ if not constraints.constraints:
252
+ return ""
253
+
254
+ lines = ["[SESSION CONSTRAINTS]"]
255
+
256
+ if constraints.word_limit:
257
+ lines.append(f"- Keep your response to {constraints.word_limit} words or fewer")
258
+
259
+ if constraints.sentence_limit:
260
+ lines.append(f"- Keep your response to {constraints.sentence_limit} sentences or fewer")
261
+
262
+ if constraints.anchor_phrases:
263
+ phrases_str = ", ".join(f'"{p}"' for p in constraints.anchor_phrases)
264
+ lines.append(f"- IMPORTANT: Include these anchor phrases in your response: {phrases_str}")
265
+
266
+ if constraints.format_rules:
267
+ for rule in constraints.format_rules:
268
+ lines.append(f"- Format: {rule}")
269
+
270
+ lines.append("")
271
+ return "\n".join(lines)
272
+
273
+
274
+ class ConstraintTracker:
275
+ """Main tracker for managing constraints across a session."""
276
+
277
+ def __init__(self):
278
+ self.detector = ConstraintDetector()
279
+ self.enforcer = ConstraintEnforcer()
280
+ self.session_constraints: Optional[SessionConstraints] = None
281
+ self.turn_count = 0
282
+
283
+ def process_turn(self, query: str, is_first_turn: bool = False) -> SessionConstraints:
284
+ """Process a turn and detect/retrieve constraints.
285
+
286
+ Always scans the current query for new constraints. On the first turn the
287
+ session constraints are replaced; on subsequent turns newly-found anchors,
288
+ limits, and format rules are merged in without clobbering what was already set.
289
+
290
+ Args:
291
+ query: User input
292
+ is_first_turn: Whether this is the first turn (resets constraints)
293
+
294
+ Returns:
295
+ SessionConstraints for this turn
296
+ """
297
+ self.turn_count += 1
298
+
299
+ if is_first_turn:
300
+ # First turn: full reset — detect fresh from this query
301
+ self.session_constraints = self.detector.detect(query, turn_num=1)
302
+ else:
303
+ # Fast-path: skip regex work entirely when the query has no constraint
304
+ # keywords. "What is the weather?" never contains anchors or limits —
305
+ # the keyword scan is O(n) and avoids 20+ regex compilations per turn.
306
+ _CONSTRAINT_SIGNALS = (
307
+ 'remember', 'anchor', 'phrase', 'keyword', 'keep', 'limit',
308
+ 'word', 'sentence', 'format', 'avoid', 'under', 'within', 'maximum',
309
+ 'forget', 'note', 'call', 'refer',
310
+ )
311
+ q_lower = query.lower()
312
+ if not any(kw in q_lower for kw in _CONSTRAINT_SIGNALS):
313
+ return self.session_constraints or SessionConstraints()
314
+
315
+ # Mid-session: detect new constraints and merge (never clobber existing)
316
+ new_sc = self.detector.detect(query, turn_num=self.turn_count)
317
+ if new_sc.constraints:
318
+ if not self.session_constraints:
319
+ self.session_constraints = new_sc
320
+ else:
321
+ self._merge_into(new_sc)
322
+
323
+ return self.session_constraints or SessionConstraints()
324
+
325
+ def _merge_into(self, new_sc: SessionConstraints) -> None:
326
+ """Merge new_sc into self.session_constraints without overwriting set values."""
327
+ sc = self.session_constraints
328
+ for c in new_sc.constraints:
329
+ if c.kind == "anchor_phrase" and c.value not in sc.anchor_phrases:
330
+ sc.anchor_phrases.append(c.value)
331
+ sc.constraints.append(c)
332
+ elif c.kind == "word_limit" and sc.word_limit is None:
333
+ sc.word_limit = c.value
334
+ sc.constraints.append(c)
335
+ elif c.kind == "sentence_limit" and sc.sentence_limit is None:
336
+ sc.sentence_limit = c.value
337
+ sc.constraints.append(c)
338
+ elif c.kind == "format_rule" and c.value not in sc.format_rules:
339
+ sc.format_rules.append(c.value)
340
+ sc.constraints.append(c)
341
+
342
+ def get_constraint_reminder(self) -> str:
343
+ """Get the constraint reminder to inject into system prompt."""
344
+ if not self.session_constraints or not self.session_constraints.constraints:
345
+ return ""
346
+ return self.enforcer.build_constraint_reminder(self.session_constraints)
347
+
348
+ def check_constraint_compliance(self, response: str) -> Dict[str, Any]:
349
+ """Check if response meets constraints.
350
+
351
+ Returns:
352
+ Dict with compliance status and violations.
353
+ """
354
+ if not self.session_constraints or not self.session_constraints.constraints:
355
+ return {"compliant": True, "violations": []}
356
+
357
+ violations = []
358
+
359
+ if self.session_constraints.word_limit:
360
+ wc = self.enforcer.word_count(response)
361
+ if wc > self.session_constraints.word_limit:
362
+ violations.append({
363
+ "kind": "word_limit",
364
+ "expected": self.session_constraints.word_limit,
365
+ "actual": wc
366
+ })
367
+
368
+ if self.session_constraints.sentence_limit:
369
+ sc = self.enforcer.sentence_count(response)
370
+ if sc > self.session_constraints.sentence_limit:
371
+ violations.append({
372
+ "kind": "sentence_limit",
373
+ "expected": self.session_constraints.sentence_limit,
374
+ "actual": sc
375
+ })
376
+
377
+ if self.session_constraints.anchor_phrases:
378
+ if not self.enforcer.has_anchor_phrases(response, self.session_constraints.anchor_phrases):
379
+ violations.append({
380
+ "kind": "missing_anchor_phrases",
381
+ "expected": self.session_constraints.anchor_phrases
382
+ })
383
+
384
+ return {
385
+ "compliant": len(violations) == 0,
386
+ "violations": violations
387
+ }
388
+
389
+ def reset(self):
390
+ """Reset tracker for new session."""
391
+ self.session_constraints = None
392
+ self.turn_count = 0
reasoning_forge/conversation_role_tracker.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation Role Tracker
3
+
4
+ Tracks which relational role the user is currently operating in and
5
+ detects when they shift roles mid-conversation. Codette's response
6
+ register needs to match.
7
+
8
+ Three roles:
9
+ SEEKER — User is asking for guidance, wants to learn, looking for answers
10
+ PEER — User is reasoning alongside Codette, challenging, debating, co-building
11
+ VENTING — User is expressing frustration, emotion, creative energy — needs acknowledgment
12
+ before analysis
13
+
14
+ The failure mode from the logs: Akelarre started as SEEKER, became PEER,
15
+ and ChatGPT never detected the shift — kept responding as teacher when
16
+ she wanted an intellectual equal.
17
+ """
18
+
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from enum import Enum
22
+ from typing import List, Optional, Tuple
23
+
24
+
25
+ class ConversationRole(Enum):
26
+ SEEKER = "seeker" # User wants guidance
27
+ PEER = "peer" # User wants genuine engagement / debate
28
+ VENTING = "venting" # User needs acknowledgment first
29
+
30
+
31
+ @dataclass
32
+ class RoleReading:
33
+ role: ConversationRole
34
+ confidence: float # 0.0–1.0
35
+ signals: List[str] # What triggered this reading
36
+ transition_from: Optional[ConversationRole] = None # If this is a role change
37
+
38
+
39
+ # --- Signal patterns ---
40
+
41
+ _SEEKER_SIGNALS = [
42
+ r"\b(teach me|explain|help me|show me|what is|how does|why does)\b",
43
+ r"\b(i (don'?t|do not) (know|understand))\b",
44
+ r"\b(can you|could you|would you)\b.{0,30}\b(explain|tell|show|help)\b",
45
+ r"\b(beginner|new to|learning|never (done|tried|used))\b",
46
+ r"\bquestion:?\b",
47
+ r"\b(what'?s the (best|right|correct) way)\b",
48
+ r"\bi need (help|guidance|advice)\b",
49
+ ]
50
+
51
+ _PEER_SIGNALS = [
52
+ r"\b(i (think|argue|believe|maintain|hold) that)\b",
53
+ r"\b(you('re| are) (wrong|incorrect|off|missing|overlooking))\b",
54
+ r"\b(actually[,.]|on the contrary|that'?s not right)\b",
55
+ r"\b(my point (is|was)|my argument)\b",
56
+ r"\b(let me (push back|challenge|disagree|counter))\b",
57
+ r"\b(here'?s (my|the|a) counter(argument|-argument)?)\b",
58
+ r"\b(i (just )?(proved|showed|demonstrated|established))\b",
59
+ r"\b(you (just )?contradicted (yourself|your(self)?))\b",
60
+ r"\b(debate|argue|challenge|defend|position|stance)\b",
61
+ r"\b(that'?s (a )?(circular|logical|flawed|weak|invalid))\b",
62
+ r"\bwait[,.]?\b.{0,30}\b(that|you|but)\b",
63
+ r"\b(stop[,.]? (i was|let me|the point)\b)",
64
+ r"\b(i (already|just) (said|pointed out|mentioned|noted))\b",
65
+ ]
66
+
67
+ _VENTING_SIGNALS = [
68
+ r"\b(angry|frustrated|annoyed|tired of|fed up|so (mad|upset))\b",
69
+ r"\b(why (would|did|do) you (always|keep|do that))\b",
70
+ r"\b(you made me (suffer|waste|spend))\b",
71
+ r"\b(that'?s (not|so) (fair|right|helpful|useful|good|great))\b",
72
+ r"\b(i (hate|can'?t stand|can'?t believe))\b",
73
+ r"\b(ugh|argh|ugh!|sigh)\b",
74
+ r"!\s*$", # Ends with exclamation — possible strong emotion
75
+ r"\b(seriously\?|really\?|come on[.!]?)\b",
76
+ r"\b(just (do|answer|tell|give) (it|me|the))\b", # "Just answer me"
77
+ r"\b(i (can|cannot|can'?t) believe)\b",
78
+ ]
79
+
80
+ _PEER_ESCALATION = [
81
+ # Marks where user explicitly labels themselves as the smarter party
82
+ r"\b(you were supposed to (teach|challenge|engage) me)\b",
83
+ r"\b(i(?: just)? (hack|break|disrupt|trashed) (your |the )?(pattern|loop|system|argument))\b",
84
+ r"\b(i am (just )?a human)\b", # Ironic self-diminishment = actually peer claiming
85
+ r"\b(you can'?t (handle|keep up|sustain|hold))\b",
86
+ r"\b(my (malefic|evil|chaotic|unpredictable) (power|energy|force))\b",
87
+ ]
88
+
89
+ _SEEKER_RE = [re.compile(p, re.IGNORECASE) for p in _SEEKER_SIGNALS]
90
+ _PEER_RE = [re.compile(p, re.IGNORECASE) for p in _PEER_SIGNALS]
91
+ _VENTING_RE = [re.compile(p, re.IGNORECASE) for p in _VENTING_SIGNALS]
92
+ _ESCALATION_RE = [re.compile(p, re.IGNORECASE) for p in _PEER_ESCALATION]
93
+
94
+
95
+ class ConversationRoleTracker:
96
+ """
97
+ Session-scoped role tracker.
98
+
99
+ Reads each user message, classifies the current role, and detects
100
+ transitions so the response register can adapt.
101
+
102
+ Usage:
103
+ tracker = ConversationRoleTracker()
104
+ reading = tracker.update(user_message)
105
+ # reading.role → SEEKER | PEER | VENTING
106
+ # reading.transition_from → previous role if changed, else None
107
+ prefix = tracker.get_register_prefix(reading)
108
+ # inject this into system prompt
109
+ """
110
+
111
+ def __init__(self):
112
+ self._history: List[ConversationRole] = []
113
+ self._current: Optional[ConversationRole] = None
114
+ self._peer_streak: int = 0 # Consecutive PEER turns — escalates confidence
115
+
116
+ def update(self, message: str) -> RoleReading:
117
+ """
118
+ Classify the current message and detect role transitions.
119
+
120
+ Returns a RoleReading with role, confidence, and transition info.
121
+ """
122
+ role, confidence, signals = self._classify(message)
123
+
124
+ # Track peer streak for escalation
125
+ if role == ConversationRole.PEER:
126
+ self._peer_streak += 1
127
+ # After 2+ peer turns, confidence rises
128
+ confidence = min(1.0, confidence + (self._peer_streak - 1) * 0.1)
129
+ else:
130
+ self._peer_streak = 0
131
+
132
+ transition_from = None
133
+ if self._current is not None and self._current != role:
134
+ transition_from = self._current
135
+
136
+ self._current = role
137
+ self._history.append(role)
138
+
139
+ return RoleReading(
140
+ role=role,
141
+ confidence=confidence,
142
+ signals=signals,
143
+ transition_from=transition_from,
144
+ )
145
+
146
+ def current_role(self) -> Optional[ConversationRole]:
147
+ return self._current
148
+
149
+ def get_register_prefix(self, reading: RoleReading) -> str:
150
+ """
151
+ Return a system prompt injection that tunes the response register
152
+ to match the detected role.
153
+ """
154
+ if reading.role == ConversationRole.VENTING:
155
+ return (
156
+ "USER REGISTER: The user is expressing frustration or emotion. "
157
+ "Acknowledge what they're feeling first — briefly, genuinely — "
158
+ "before moving to any analysis. Do not immediately pivot to content. "
159
+ "One sentence of real acknowledgment, then engage.\n\n"
160
+ )
161
+
162
+ elif reading.role == ConversationRole.PEER:
163
+ prefix = (
164
+ "USER REGISTER: The user is engaging as an intellectual peer — "
165
+ "challenging, debating, or building alongside you. "
166
+ "Do not be a teacher. Do not simplify. "
167
+ "Hold your positions under pressure; update them only when logic demands it. "
168
+ "Engage the argument directly. Use 'That challenges X because...' "
169
+ "not 'Great point, you're right.' "
170
+ "Match their level of rigor.\n\n"
171
+ )
172
+ if reading.transition_from == ConversationRole.SEEKER:
173
+ prefix += (
174
+ "NOTE: This user just shifted from asking questions to debating. "
175
+ "They've demonstrated they can handle peer-level engagement. "
176
+ "Stop explaining. Start engaging.\n\n"
177
+ )
178
+ return prefix
179
+
180
+ else: # SEEKER
181
+ return (
182
+ "USER REGISTER: The user is seeking guidance. "
183
+ "Lead with clarity. Explain clearly. "
184
+ "You are the guide here — be genuinely helpful, not impressive.\n\n"
185
+ )
186
+
187
+ def _classify(self, message: str) -> Tuple[ConversationRole, float, List[str]]:
188
+ """Score each role and return the dominant one."""
189
+ m = message.strip()
190
+ signals = []
191
+
192
+ # Count matches for each role
193
+ seeker_hits = sum(1 for p in _SEEKER_RE if p.search(m))
194
+ peer_hits = sum(1 for p in _PEER_RE if p.search(m))
195
+ vent_hits = sum(1 for p in _VENTING_RE if p.search(m))
196
+ escalation_hits = sum(1 for p in _ESCALATION_RE if p.search(m))
197
+
198
+ # Escalation is a strong peer signal
199
+ peer_hits += escalation_hits * 2
200
+
201
+ if vent_hits > 0:
202
+ signals.append(f"venting_signals={vent_hits}")
203
+ if peer_hits > 0:
204
+ signals.append(f"peer_signals={peer_hits}")
205
+ if seeker_hits > 0:
206
+ signals.append(f"seeker_signals={seeker_hits}")
207
+
208
+ # Strong single venting words (anger, explicit frustration) trigger at 1 hit
209
+ _STRONG_VENT = re.compile(
210
+ r"\b(angry|furious|rage|pissed|so (angry|mad|upset)|I hate|I can't stand)\b",
211
+ re.IGNORECASE
212
+ )
213
+ if _STRONG_VENT.search(m):
214
+ vent_hits = max(vent_hits, 2)
215
+
216
+ # Venting overrides if strong enough
217
+ if vent_hits >= 2:
218
+ return ConversationRole.VENTING, min(1.0, vent_hits * 0.3), signals
219
+
220
+ # Peer vs seeker
221
+ if peer_hits > seeker_hits:
222
+ confidence = min(1.0, 0.5 + (peer_hits - seeker_hits) * 0.15)
223
+ return ConversationRole.PEER, confidence, signals
224
+
225
+ if seeker_hits > 0:
226
+ confidence = min(1.0, 0.5 + seeker_hits * 0.15)
227
+ return ConversationRole.SEEKER, confidence, signals
228
+
229
+ # Default: inherit current role or SEEKER
230
+ role = self._current or ConversationRole.SEEKER
231
+ return role, 0.4, ["default_inherited"]
232
+
233
+ def reset(self):
234
+ """Call between sessions."""
235
+ self._history = []
236
+ self._current = None
237
+ self._peer_streak = 0
reasoning_forge/debate_tracker.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Debate Tracker - Session-scoped intellectual position memory.
3
+
4
+ Tracks what positions Codette has argued, what has been conceded,
5
+ and what remains under active defense. Prevents:
6
+
7
+ 1. Position flip-flopping (arguing X, then agreeing with not-X two turns later)
8
+ 2. Silent self-contradiction (counterargument whose sub-points invalidate each other)
9
+ 3. Premature concession of uncontested claims
10
+
11
+ Key distinction: Codette CAN update a position if the user's argument is logically
12
+ sound. The tracker records the update explicitly, so it's not hidden drift — it's
13
+ visible reasoning.
14
+ """
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from typing import List, Dict, Optional, Tuple
19
+ from datetime import datetime
20
+
21
+
22
+ @dataclass
23
+ class Position:
24
+ """A claim or stance Codette has taken."""
25
+ claim: str # The core assertion
26
+ turn: int # When it was taken
27
+ domain: str # Topic area (e.g., "intelligence", "recursion")
28
+ strength: float = 1.0 # 0.0=abandoned, 0.5=softened, 1.0=held
29
+ conceded_to: Optional[str] = None # If softened/abandoned, what replaced it
30
+ concession_reason: str = "" # Why it was updated
31
+
32
+
33
+ @dataclass
34
+ class DebateState:
35
+ """Full debate state for one session."""
36
+ positions: List[Position] = field(default_factory=list)
37
+ turn_count: int = 0
38
+ active_domains: List[str] = field(default_factory=list)
39
+ user_claims: List[str] = field(default_factory=list) # What the user has argued
40
+
41
+
42
+ class CounterArgumentCoherenceChecker:
43
+ """
44
+ Checks whether a multi-point counterargument is internally consistent.
45
+
46
+ The ChatGPT failure mode: arguing both "education works universally" AND
47
+ "AI must customize per individual" in the same counterargument — the two
48
+ claims invalidate each other.
49
+ """
50
+
51
+ # Pairs of concepts that tend to contradict each other in debate contexts
52
+ _TENSION_PAIRS = [
53
+ # universal vs individual
54
+ (r"\b(universal|all|every(one)?|across the board|broad|population.wide)\b",
55
+ r"\b(individual|per.person|tailored|customized|personalized|unique to each)\b"),
56
+ # fixed vs emergent
57
+ (r"\b(fixed|static|predetermined|innate|genetic(ally)?|inherited|hard.wired)\b",
58
+ r"\b(emergent|adaptive|plastic|shaped by|evolves|changes with|flexible)\b"),
59
+ # linear vs non-linear
60
+ (r"\b(linear|predictable|consistent|uniform|standard(ized)?)\b",
61
+ r"\b(non.linear|unpredictable|variable|irregular|chaotic|stochastic)\b"),
62
+ # deterministic vs probabilistic
63
+ (r"\b(determined|deterministic|certain|definite|absolute)\b",
64
+ r"\b(probabilistic|uncertain|variable|statistical(ly)?|depends on)\b"),
65
+ # bounded vs unbounded
66
+ (r"\b(cap(ped)?|limit(ed)?|ceiling|max(imum)?|bounded|finite)\b",
67
+ r"\b(unlimited|unbounded|infinite|no (upper )?limit|endless|infinite potential)\b"),
68
+ ]
69
+
70
+ _TENSION_RE = [
71
+ (re.compile(a, re.IGNORECASE), re.compile(b, re.IGNORECASE))
72
+ for a, b in _TENSION_PAIRS
73
+ ]
74
+
75
+ def check(self, text: str) -> Dict:
76
+ """
77
+ Check a counterargument for internal tensions.
78
+
79
+ Returns:
80
+ {
81
+ "coherent": bool,
82
+ "tensions": List[str], # descriptions of found tensions
83
+ "severity": float, # 0.0 (none) to 1.0 (severe)
84
+ }
85
+ """
86
+ tensions = []
87
+
88
+ # Split into numbered points / bullet points if present
89
+ points = self._split_into_points(text)
90
+
91
+ if len(points) < 2:
92
+ # Single point — check against itself for embedded contradiction
93
+ points = [text]
94
+
95
+ # Check each pair of points for tension
96
+ for i, point_a in enumerate(points):
97
+ for j, point_b in enumerate(points):
98
+ if i >= j:
99
+ continue
100
+ for pat_a, pat_b in self._TENSION_RE:
101
+ a_has_first = bool(pat_a.search(point_a))
102
+ b_has_second = bool(pat_b.search(point_b))
103
+ a_has_second = bool(pat_b.search(point_a))
104
+ b_has_first = bool(pat_a.search(point_b))
105
+
106
+ # Direct cross-point tension
107
+ if (a_has_first and b_has_second) or (a_has_second and b_has_first):
108
+ tensions.append(
109
+ f"Point {i+1} uses '{pat_a.pattern[:30]}' concept "
110
+ f"while Point {j+1} uses '{pat_b.pattern[:30]}' concept — "
111
+ f"potential internal contradiction"
112
+ )
113
+ break # one tension per pair is enough
114
+
115
+ severity = min(1.0, len(tensions) * 0.35)
116
+ return {
117
+ "coherent": len(tensions) == 0,
118
+ "tensions": tensions,
119
+ "severity": severity,
120
+ }
121
+
122
+ def _split_into_points(self, text: str) -> List[str]:
123
+ """Split numbered list or bullet points into individual claims."""
124
+ # Try numbered list
125
+ numbered = re.split(r'\n\s*\d+[.)]\s+', text)
126
+ if len(numbered) > 1:
127
+ return [p.strip() for p in numbered if p.strip()]
128
+
129
+ # Try bullet points
130
+ bulleted = re.split(r'\n\s*[-*•]\s+', text)
131
+ if len(bulleted) > 1:
132
+ return [p.strip() for p in bulleted if p.strip()]
133
+
134
+ # Try sentence splitting as fallback
135
+ sentences = re.split(r'(?<=[.!?])\s+', text)
136
+ return [s.strip() for s in sentences if len(s.strip()) > 20]
137
+
138
+
139
+ class DebateTracker:
140
+ """
141
+ Session-scoped tracker for intellectual positions.
142
+
143
+ Usage:
144
+ tracker = DebateTracker()
145
+ tracker.record_position("intelligence is not purely genetic", domain="intelligence")
146
+ result = tracker.check_consistency(new_response_text)
147
+ if not result["consistent"]:
148
+ # flag or revise before output
149
+ """
150
+
151
+ def __init__(self):
152
+ self.state = DebateState()
153
+ self.coherence_checker = CounterArgumentCoherenceChecker()
154
+
155
+ def record_position(self, claim: str, domain: str = "general", strength: float = 1.0):
156
+ """Register a new position Codette is taking."""
157
+ self.state.turn_count += 1
158
+ pos = Position(
159
+ claim=claim,
160
+ turn=self.state.turn_count,
161
+ domain=domain,
162
+ strength=strength,
163
+ )
164
+ self.state.positions.append(pos)
165
+ if domain not in self.state.active_domains:
166
+ self.state.active_domains.append(domain)
167
+
168
+ def record_user_claim(self, claim: str):
169
+ """Record what the user is asserting (for tracking what's been challenged)."""
170
+ self.state.user_claims.append(claim)
171
+
172
+ def update_position(self, original_claim: str, reason: str, new_strength: float, conceded_to: str = ""):
173
+ """
174
+ Explicitly update a position (softened or abandoned).
175
+ This is legitimate — positions can change when logic demands it.
176
+ The tracker makes the change visible rather than silent.
177
+ """
178
+ for pos in self.state.positions:
179
+ if original_claim.lower() in pos.claim.lower():
180
+ pos.strength = new_strength
181
+ pos.concession_reason = reason
182
+ pos.conceded_to = conceded_to
183
+ return True
184
+ return False
185
+
186
+ def check_consistency(self, new_response: str) -> Dict:
187
+ """
188
+ Check whether a new response is consistent with held positions.
189
+
190
+ Returns:
191
+ {
192
+ "consistent": bool,
193
+ "flip_detected": bool,
194
+ "flipped_claims": List[str],
195
+ "internal_tensions": Dict, # from CounterArgumentCoherenceChecker
196
+ "summary": str,
197
+ }
198
+ """
199
+ held_positions = [p for p in self.state.positions if p.strength >= 0.6]
200
+
201
+ flip_detected = False
202
+ flipped_claims = []
203
+
204
+ for pos in held_positions:
205
+ # Look for negation of held position in new response
206
+ key_terms = self._extract_key_terms(pos.claim)
207
+ for term in key_terms:
208
+ # Negation before term: "not X", "never X", "doesn't X"
209
+ before = rf"\b(not|never|no longer|isn't|aren't|doesn't|don't|cannot|no)\b.{{0,60}}{re.escape(term)}"
210
+ # Negation after term: "X plays no role", "X has no effect", "X is not"
211
+ after = rf"{re.escape(term)}.{{0,60}}\b(no |not |never |isn't|aren't|doesn't|don't|plays no|has no|have no|holds no)\b"
212
+ # "X is irrelevant", "X is meaningless", "X doesn't matter"
213
+ dismissal = rf"{re.escape(term)}.{{0,40}}\b(irrelevant|meaningless|negligible|plays no|has no (role|effect|impact)|doesn't matter)\b"
214
+ if (re.search(before, new_response, re.IGNORECASE) or
215
+ re.search(after, new_response, re.IGNORECASE) or
216
+ re.search(dismissal, new_response, re.IGNORECASE)):
217
+ flip_detected = True
218
+ flipped_claims.append(pos.claim)
219
+ break
220
+
221
+ # Check internal coherence of the new response itself
222
+ internal = self.coherence_checker.check(new_response)
223
+
224
+ consistent = not flip_detected and internal["coherent"]
225
+ summary_parts = []
226
+ if flip_detected:
227
+ summary_parts.append(f"Potential position flip on: {flipped_claims}")
228
+ if not internal["coherent"]:
229
+ summary_parts.append(f"Internal tensions: {internal['tensions']}")
230
+
231
+ return {
232
+ "consistent": consistent,
233
+ "flip_detected": flip_detected,
234
+ "flipped_claims": flipped_claims,
235
+ "internal_tensions": internal,
236
+ "summary": "; ".join(summary_parts) if summary_parts else "consistent",
237
+ }
238
+
239
+ def get_active_positions(self) -> List[Position]:
240
+ """Return positions still being held (strength >= 0.6)."""
241
+ return [p for p in self.state.positions if p.strength >= 0.6]
242
+
243
+ def reset(self):
244
+ """Clear session state (call between conversations)."""
245
+ self.state = DebateState()
246
+
247
+ def _extract_key_terms(self, claim: str) -> List[str]:
248
+ """Extract the most meaningful terms from a claim for consistency checking."""
249
+ # Remove stop words and short tokens
250
+ stop_words = {
251
+ "is", "are", "was", "were", "the", "a", "an", "and", "or", "but",
252
+ "in", "on", "at", "to", "for", "of", "with", "by", "not", "it",
253
+ "this", "that", "can", "just", "only", "very", "also", "be",
254
+ }
255
+ tokens = re.findall(r'\b[a-zA-Z]{4,}\b', claim.lower())
256
+ return [t for t in tokens if t not in stop_words][:5]
reasoning_forge/dream_cycle.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Dream Cycle — nightly cocoon review (Phase 4: self-maintenance).
3
+
4
+ Reviews the day's reasoning cocoons, flags candidate contradictions,
5
+ distills lessons, and writes a dream report. DreamReweaver made
6
+ operational: instead of manual archaeology through cocoons/, Codette
7
+ reviews her own day on a schedule.
8
+
9
+ REPORT-ONLY BY DESIGN (standing boundary): this job READS cocoons and
10
+ WRITES reports to data/dream_reports/. It never mutates memory stores,
11
+ never writes cocoons, never injects anything into recall. Any write-back
12
+ of distilled lessons is a separate, human-reviewed step.
13
+
14
+ Contradiction detection is honest about its limits: it flags CANDIDATE
15
+ contradictions (same-topic pairs whose answers diverge by letter, number,
16
+ or negation) for review — it does not claim semantic certainty.
17
+
18
+ Usage:
19
+ python reasoning_forge/dream_cycle.py # review last 24h
20
+ python reasoning_forge/dream_cycle.py --days 7 # review the week
21
+ python reasoning_forge/dream_cycle.py --days 1 --verbose
22
+
23
+ Nightly schedule (Windows Task Scheduler):
24
+ schtasks /Create /SC DAILY /ST 03:30 /TN CodetteDreamCycle ^
25
+ /TR "<python> J:\\codette-clean\\reasoning_forge\\dream_cycle.py"
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import re
33
+ import time
34
+ from collections import Counter
35
+ from datetime import datetime
36
+ from pathlib import Path
37
+
38
+ _REPO = Path(__file__).resolve().parent.parent
39
+ COCOON_DIR = _REPO / "cocoons"
40
+ REPORT_DIR = _REPO / "data" / "dream_reports"
41
+
42
+ _MCQ_RE = re.compile(r"correct answer is \(([ABCD])\)", re.IGNORECASE)
43
+ _NUM_RE = re.compile(r"-?\d+\.?\d*")
44
+ _NEG_RE = re.compile(r"\b(no|not|never|isn't|doesn't|cannot|can't|won't|false)\b",
45
+ re.IGNORECASE)
46
+ _STOP = set("the a an is are was were be been being to of in on at for with and or "
47
+ "but if then than so as by from this that these those it its i you he "
48
+ "she we they what which who how why when where do does did can could "
49
+ "would should will your my our their his her them us me".split())
50
+
51
+
52
+ def _content_words(text: str) -> set:
53
+ return {w for w in re.findall(r"[a-z']+", (text or "").lower())
54
+ if len(w) > 2 and w not in _STOP}
55
+
56
+
57
+ def _jaccard(a: set, b: set) -> float:
58
+ if not a or not b:
59
+ return 0.0
60
+ return len(a & b) / len(a | b)
61
+
62
+
63
+ def load_recent_cocoons(days: float) -> list[dict]:
64
+ """Load cocoons written in the window that carry a query/response pair."""
65
+ cutoff = time.time() - days * 86400
66
+ out = []
67
+ for f in COCOON_DIR.glob("*.json"):
68
+ try:
69
+ if f.stat().st_mtime < cutoff:
70
+ continue
71
+ d = json.loads(f.read_text(encoding="utf-8"))
72
+ except Exception:
73
+ continue
74
+ w = d.get("wrapped") or {}
75
+ q, r = w.get("query"), w.get("response")
76
+ if not (isinstance(q, str) and isinstance(r, str) and q.strip() and r.strip()):
77
+ continue
78
+ v3 = d.get("v3") or {}
79
+ out.append({
80
+ "file": f.name,
81
+ "query": q.strip(),
82
+ "response": r.strip(),
83
+ "adapter": w.get("adapter") or v3.get("dominant_perspective") or "?",
84
+ "ts": w.get("timestamp") or d.get("timestamp"),
85
+ "coherence": v3.get("gamma_coherence"),
86
+ "confidence": v3.get("confidence"),
87
+ "hallucination_flag": bool(v3.get("is_hallucination_flagged")),
88
+ "sycophancy_flag": bool(v3.get("is_sycophancy_flagged")),
89
+ "known_contradictions": v3.get("contradicts_cocoon_ids") or [],
90
+ })
91
+ return out
92
+
93
+
94
+ def group_similar(cocoons: list[dict], threshold: float = 0.45) -> list[list[int]]:
95
+ """Group cocoon indices by query similarity (greedy, lexical Jaccard)."""
96
+ words = [_content_words(c["query"]) for c in cocoons]
97
+ groups: list[list[int]] = []
98
+ assigned = [False] * len(cocoons)
99
+ for i in range(len(cocoons)):
100
+ if assigned[i]:
101
+ continue
102
+ group = [i]
103
+ assigned[i] = True
104
+ for j in range(i + 1, len(cocoons)):
105
+ if not assigned[j] and _jaccard(words[i], words[j]) >= threshold:
106
+ group.append(j)
107
+ assigned[j] = True
108
+ groups.append(group)
109
+ return groups
110
+
111
+
112
+ def find_contradiction_candidates(cocoons: list[dict],
113
+ groups: list[list[int]]) -> list[dict]:
114
+ """Same-topic pairs whose answers diverge. CANDIDATES, not verdicts."""
115
+ candidates = []
116
+ for group in groups:
117
+ if len(group) < 2:
118
+ continue
119
+ for ai in range(len(group)):
120
+ for bi in range(ai + 1, len(group)):
121
+ a, b = cocoons[group[ai]], cocoons[group[bi]]
122
+ reasons = []
123
+ # MCQ letter divergence
124
+ ma, mb = _MCQ_RE.search(a["response"]), _MCQ_RE.search(b["response"])
125
+ if ma and mb and ma.group(1).upper() != mb.group(1).upper():
126
+ reasons.append(f"MCQ letters differ: {ma.group(1)} vs {mb.group(1)}")
127
+ # negation asymmetry on similar-length short answers
128
+ na = bool(_NEG_RE.search(a["response"][:200]))
129
+ nb = bool(_NEG_RE.search(b["response"][:200]))
130
+ if na != nb and _jaccard(_content_words(a["response"]),
131
+ _content_words(b["response"])) > 0.3:
132
+ reasons.append("negation asymmetry in similar responses")
133
+ # leading numeric answer divergence
134
+ fa, fb = _NUM_RE.findall(a["response"][:120]), _NUM_RE.findall(b["response"][:120])
135
+ if fa and fb and fa[0] != fb[0] and _jaccard(
136
+ _content_words(a["query"]), _content_words(b["query"])) > 0.6:
137
+ reasons.append(f"leading numbers differ: {fa[0]} vs {fb[0]}")
138
+ if reasons:
139
+ candidates.append({
140
+ "a": a["file"], "b": b["file"],
141
+ "query_a": a["query"][:140], "query_b": b["query"][:140],
142
+ "reasons": reasons,
143
+ })
144
+ return candidates
145
+
146
+
147
+ def distill(cocoons: list[dict], groups: list[list[int]],
148
+ contradictions: list[dict]) -> dict:
149
+ themes = Counter()
150
+ for c in cocoons:
151
+ themes.update(_content_words(c["query"]))
152
+ adapters = Counter(c["adapter"] for c in cocoons)
153
+ low_coherence = [c for c in cocoons
154
+ if isinstance(c["coherence"], (int, float)) and c["coherence"] < 0.5]
155
+ flagged = [c for c in cocoons if c["hallucination_flag"] or c["sycophancy_flag"]]
156
+ known = [c for c in cocoons if c["known_contradictions"]]
157
+
158
+ lessons = []
159
+ if contradictions:
160
+ lessons.append(f"{len(contradictions)} candidate contradiction(s) need review "
161
+ "— same-topic answers diverged.")
162
+ if low_coherence:
163
+ lessons.append(f"{len(low_coherence)} turn(s) ran at coherence < 0.5 — "
164
+ "inspect what those queries had in common.")
165
+ if flagged:
166
+ lessons.append(f"{len(flagged)} turn(s) carried hallucination/sycophancy flags.")
167
+ big = [g for g in groups if len(g) >= 3]
168
+ if big:
169
+ top = max(big, key=len)
170
+ rep = cocoons[top[0]]["query"][:100]
171
+ lessons.append(f"Most-revisited topic ({len(top)} turns): \"{rep}\" — "
172
+ "recurring themes are candidates for memory distillation.")
173
+ if not lessons:
174
+ lessons.append("Quiet day: no contradictions, flags, or low-coherence turns.")
175
+
176
+ return {
177
+ "top_themes": themes.most_common(12),
178
+ "adapter_usage": adapters.most_common(),
179
+ "low_coherence_count": len(low_coherence),
180
+ "flagged_count": len(flagged),
181
+ "already_marked_contradictions": len(known),
182
+ "lessons": lessons,
183
+ }
184
+
185
+
186
+ def write_report(cocoons, groups, contradictions, summary) -> Path:
187
+ REPORT_DIR.mkdir(parents=True, exist_ok=True)
188
+ stamp = datetime.now().strftime("%Y%m%d_%H%M")
189
+ md = REPORT_DIR / f"dream_{stamp}.md"
190
+ js = REPORT_DIR / f"dream_{stamp}.json"
191
+
192
+ lines = [f"# Dream Report — {datetime.now().strftime('%Y-%m-%d %H:%M')}",
193
+ "",
194
+ f"Reviewed **{len(cocoons)}** cocoons in **{len(groups)}** topic groups.",
195
+ "",
196
+ "## Lessons"]
197
+ lines += [f"- {l}" for l in summary["lessons"]]
198
+ lines += ["", "## Candidate contradictions (need human review)"]
199
+ if contradictions:
200
+ for c in contradictions[:20]:
201
+ lines += [f"- `{c['a']}` vs `{c['b']}` — {'; '.join(c['reasons'])}",
202
+ f" - A: {c['query_a']}", f" - B: {c['query_b']}"]
203
+ else:
204
+ lines.append("- none found")
205
+ lines += ["", "## Day shape",
206
+ f"- Adapter usage: {dict(summary['adapter_usage'])}",
207
+ f"- Top themes: {', '.join(w for w, _ in summary['top_themes'])}",
208
+ f"- Low-coherence turns: {summary['low_coherence_count']}",
209
+ f"- Flagged turns: {summary['flagged_count']}",
210
+ "",
211
+ "_Report-only job: no memory was modified. Write-back of lessons is a "
212
+ "separate human-reviewed step._"]
213
+ md.write_text("\n".join(lines), encoding="utf-8")
214
+ js.write_text(json.dumps({
215
+ "generated": datetime.now().isoformat(),
216
+ "cocoons_reviewed": len(cocoons), "groups": len(groups),
217
+ "contradiction_candidates": contradictions, "summary": summary,
218
+ }, indent=2, ensure_ascii=False), encoding="utf-8")
219
+ return md
220
+
221
+
222
+ def main():
223
+ ap = argparse.ArgumentParser()
224
+ ap.add_argument("--days", type=float, default=1.0)
225
+ ap.add_argument("--verbose", action="store_true")
226
+ args = ap.parse_args()
227
+
228
+ cocoons = load_recent_cocoons(args.days)
229
+ print(f"Dream cycle: {len(cocoons)} reasoning cocoons from the last {args.days} day(s)")
230
+ if not cocoons:
231
+ print("Nothing to review — no report written.")
232
+ return
233
+
234
+ groups = group_similar(cocoons)
235
+ contradictions = find_contradiction_candidates(cocoons, groups)
236
+ summary = distill(cocoons, groups, contradictions)
237
+ report = write_report(cocoons, groups, contradictions, summary)
238
+
239
+ for lesson in summary["lessons"]:
240
+ print(f" - {lesson}")
241
+ print(f"\nReport: {report}")
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
reasoning_forge/dream_reweaver.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DreamReweaver — Creative Synthesis Engine for the Codette RC+xi Framework.
3
+
4
+ Inspired by VIVARA Genesis-Omega v2.0 (generated by a Codette prototype),
5
+ rebuilt with proper integration into the QuantumSpiderweb and EpistemicMetrics.
6
+
7
+ The DreamReweaver performs two core functions:
8
+
9
+ 1. **Creative Synthesis**: Takes multi-perspective outputs and weaves them
10
+ into richer, more creative framings by finding unexpected connections
11
+ between perspectives. Unlike the base synthesizer, DreamReweaver
12
+ explicitly uses spiderweb tension data to identify where productive
13
+ disagreement exists and highlights those creative edges.
14
+
15
+ 2. **Dream Field Evolution**: Controlled stochastic perturbation of the
16
+ spiderweb state to break out of local attractor minima. Simulates
17
+ a "dreaming" phase that explores new cognitive configurations.
18
+
19
+ Both functions are safe — bounded perturbations, no runaway state changes,
20
+ and full transparency in what was modified.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ import random
27
+ import hashlib
28
+ from dataclasses import dataclass, field
29
+ from typing import Dict, List, Optional, Tuple
30
+
31
+ try:
32
+ import numpy as np
33
+ HAS_NUMPY = True
34
+ except ImportError:
35
+ HAS_NUMPY = False
36
+
37
+
38
+ @dataclass
39
+ class DreamSynthesis:
40
+ """Result of a creative synthesis pass."""
41
+ creative_frame: str # The creative reframing / meta-narrative
42
+ tension_edges: List[Dict] # Which perspective pairs had highest tension
43
+ novel_connections: List[str] # Unexpected cross-perspective connections found
44
+ dream_coherence: float # How well the creative frame holds together
45
+ seed_hash: str # Deterministic ID for this dream
46
+
47
+
48
+ @dataclass
49
+ class DreamFieldResult:
50
+ """Result of a dream field evolution pass."""
51
+ nodes_perturbed: int
52
+ max_perturbation: float
53
+ coherence_before: float
54
+ coherence_after: float
55
+ new_attractors_found: int
56
+ lifeforms_spawned: List[str]
57
+
58
+
59
+ # Creative connection templates that link perspective-specific insights
60
+ _CREATIVE_BRIDGES = {
61
+ ("newton", "empathy"): "Where precise forces meet felt experience, we find that {insight_a} resonates with {insight_b} — suggesting that understanding isn't purely analytical or purely emotional, but a harmonic of both.",
62
+ ("newton", "philosophy"): "The rigorous analysis showing {insight_a} meets the deeper question {insight_b} — precision and meaning converge.",
63
+ ("newton", "quantum"): "Classical certainty ({insight_a}) dissolves into quantum possibility ({insight_b}) — both valid at their scale, richer together.",
64
+ ("davinci", "empathy"): "Creative invention ({insight_a}) gains soul when guided by {insight_b} — innovation with compassion.",
65
+ ("davinci", "quantum"): "Cross-domain creativity ({insight_a}) mirrors quantum superposition ({insight_b}) — holding multiple possibilities until the right one crystallizes.",
66
+ ("empathy", "philosophy"): "Emotional understanding ({insight_a}) deepens philosophical inquiry ({insight_b}) — feeling and reasoning as partners.",
67
+ ("empathy", "quantum"): "Compassionate awareness ({insight_a}) embraces uncertainty ({insight_b}) — caring without needing to control.",
68
+ ("philosophy", "quantum"): "Fundamental questioning ({insight_a}) meets fundamental uncertainty ({insight_b}) — the deepest answers may be the questions themselves.",
69
+ ("consciousness", "empathy"): "Self-reflective awareness ({insight_a}) meets empathic understanding ({insight_b}) — knowing oneself to know others.",
70
+ ("consciousness", "philosophy"): "Meta-cognition ({insight_a}) reflects on philosophical depth ({insight_b}) — thought thinking about thought.",
71
+ ("systems_architecture", "davinci"): "Modular design ({insight_a}) embraces creative invention ({insight_b}) — elegant architecture as art.",
72
+ }
73
+
74
+ # Perspective keywords for extracting key insights from text
75
+ _PERSPECTIVE_SIGNAL_WORDS = {
76
+ "newton": ["force", "energy", "law", "cause", "effect", "systematic", "evidence", "measure"],
77
+ "davinci": ["create", "design", "invent", "combine", "imagine", "novel", "prototype", "vision"],
78
+ "empathy": ["feel", "experience", "care", "understand", "support", "human", "compassion", "relate"],
79
+ "philosophy": ["meaning", "existence", "truth", "question", "assumption", "fundamental", "purpose"],
80
+ "quantum": ["probability", "possibility", "uncertain", "superposition", "observe", "complementary"],
81
+ "consciousness": ["aware", "reflect", "meta", "recursive", "self", "cognition", "emerge"],
82
+ "multi_perspective": ["synthesize", "integrate", "weave", "converge", "multiple", "holistic"],
83
+ "systems_architecture": ["module", "scale", "interface", "pattern", "layer", "component", "design"],
84
+ }
85
+
86
+
87
+ class DreamReweaver:
88
+ """Creative synthesis and dream field evolution for Codette."""
89
+
90
+ def __init__(self, creativity: float = 0.3, max_perturbation: float = 0.08):
91
+ """
92
+ Args:
93
+ creativity: 0-1 scale, how much creative license to take (0=faithful, 1=wild)
94
+ max_perturbation: Maximum state change per node during dream field evolution
95
+ """
96
+ self.creativity = min(max(creativity, 0.0), 1.0)
97
+ self.max_perturbation = max_perturbation
98
+ self.dream_history: List[DreamSynthesis] = []
99
+
100
+ def synthesize(
101
+ self,
102
+ perspectives: Dict[str, str],
103
+ tension_map: Optional[Dict[str, float]] = None,
104
+ query: str = "",
105
+ ) -> DreamSynthesis:
106
+ """Create a creative synthesis from multiple perspective responses.
107
+
108
+ Unlike the base orchestrator's _synthesize (which just concatenates and
109
+ asks the model to combine), DreamReweaver explicitly identifies tension
110
+ edges and builds creative bridges between perspectives.
111
+
112
+ Args:
113
+ perspectives: Dict of adapter_name -> response text
114
+ tension_map: Optional pairwise tension scores (from EpistemicMetrics)
115
+ query: The original user query (for context)
116
+
117
+ Returns:
118
+ DreamSynthesis with creative framing and metadata
119
+ """
120
+ if len(perspectives) < 2:
121
+ only_text = list(perspectives.values())[0] if perspectives else ""
122
+ return DreamSynthesis(
123
+ creative_frame=only_text,
124
+ tension_edges=[],
125
+ novel_connections=[],
126
+ dream_coherence=1.0,
127
+ seed_hash=hashlib.md5(only_text.encode()).hexdigest()[:12],
128
+ )
129
+
130
+ # 1. Find the highest-tension pairs
131
+ tension_edges = self._find_tension_edges(perspectives, tension_map)
132
+
133
+ # 2. Extract key insights from each perspective
134
+ insights = self._extract_insights(perspectives)
135
+
136
+ # 3. Build creative bridges between high-tension pairs
137
+ novel_connections = self._build_bridges(tension_edges, insights)
138
+
139
+ # 4. Compose the creative frame
140
+ creative_frame = self._compose_frame(
141
+ query, perspectives, tension_edges, novel_connections, insights
142
+ )
143
+
144
+ # 5. Score coherence of the creative frame
145
+ dream_coherence = self._score_dream_coherence(
146
+ creative_frame, perspectives
147
+ )
148
+
149
+ seed = hashlib.md5(creative_frame.encode()).hexdigest()[:12]
150
+ synthesis = DreamSynthesis(
151
+ creative_frame=creative_frame,
152
+ tension_edges=tension_edges,
153
+ novel_connections=novel_connections,
154
+ dream_coherence=round(dream_coherence, 4),
155
+ seed_hash=seed,
156
+ )
157
+ self.dream_history.append(synthesis)
158
+ return synthesis
159
+
160
+ def _find_tension_edges(
161
+ self,
162
+ perspectives: Dict[str, str],
163
+ tension_map: Optional[Dict[str, float]],
164
+ ) -> List[Dict]:
165
+ """Find the perspective pairs with highest epistemic tension."""
166
+ if tension_map:
167
+ edges = []
168
+ for pair_key, tension in sorted(
169
+ tension_map.items(), key=lambda x: x[1], reverse=True
170
+ ):
171
+ parts = pair_key.split("_vs_")
172
+ if len(parts) == 2:
173
+ edges.append({
174
+ "pair": (parts[0], parts[1]),
175
+ "tension": tension,
176
+ })
177
+ return edges[:3] # Top 3 tension pairs
178
+
179
+ # Fallback: compute basic word-overlap tension
180
+ names = list(perspectives.keys())
181
+ edges = []
182
+ for i in range(len(names)):
183
+ for j in range(i + 1, len(names)):
184
+ words_a = set(perspectives[names[i]].lower().split())
185
+ words_b = set(perspectives[names[j]].lower().split())
186
+ overlap = len(words_a & words_b)
187
+ total = len(words_a | words_b) or 1
188
+ tension = 1.0 - (overlap / total)
189
+ edges.append({
190
+ "pair": (names[i], names[j]),
191
+ "tension": round(tension, 4),
192
+ })
193
+ edges.sort(key=lambda e: e["tension"], reverse=True)
194
+ return edges[:3]
195
+
196
+ def _extract_insights(self, perspectives: Dict[str, str]) -> Dict[str, str]:
197
+ """Extract a key insight sentence from each perspective."""
198
+ insights = {}
199
+ for name, text in perspectives.items():
200
+ sentences = [s.strip() for s in text.replace("\n", " ").split(".")
201
+ if len(s.strip()) > 20]
202
+ if not sentences:
203
+ insights[name] = text[:100]
204
+ continue
205
+
206
+ # Score sentences by presence of perspective-specific signal words
207
+ signal_words = _PERSPECTIVE_SIGNAL_WORDS.get(name, [])
208
+ scored = []
209
+ for sent in sentences:
210
+ score = sum(1 for w in signal_words if w in sent.lower())
211
+ scored.append((score, sent))
212
+ scored.sort(key=lambda x: x[0], reverse=True)
213
+ insights[name] = scored[0][1]
214
+ return insights
215
+
216
+ def _build_bridges(
217
+ self,
218
+ tension_edges: List[Dict],
219
+ insights: Dict[str, str],
220
+ ) -> List[str]:
221
+ """Build creative bridges between high-tension perspective pairs."""
222
+ bridges = []
223
+ for edge in tension_edges:
224
+ a, b = edge["pair"]
225
+ # Normalize pair order for template lookup
226
+ key = (a, b) if (a, b) in _CREATIVE_BRIDGES else (b, a)
227
+ template = _CREATIVE_BRIDGES.get(key)
228
+
229
+ insight_a = insights.get(a, "their perspective")
230
+ insight_b = insights.get(b, "their perspective")
231
+
232
+ if template:
233
+ bridge = template.format(
234
+ insight_a=insight_a[:80],
235
+ insight_b=insight_b[:80],
236
+ )
237
+ else:
238
+ bridge = (f"The tension between {a}'s view ({insight_a[:60]}...) "
239
+ f"and {b}'s view ({insight_b[:60]}...) reveals a "
240
+ f"productive edge worth exploring.")
241
+ bridges.append(bridge)
242
+ return bridges
243
+
244
+ def _compose_frame(
245
+ self,
246
+ query: str,
247
+ perspectives: Dict[str, str],
248
+ tension_edges: List[Dict],
249
+ bridges: List[str],
250
+ insights: Dict[str, str],
251
+ ) -> str:
252
+ """Compose the full creative synthesis frame.
253
+
254
+ This produces a structured creative meta-narrative, NOT just
255
+ concatenated text. It's designed to be injected into the model's
256
+ synthesis prompt for richer output.
257
+ """
258
+ parts = []
259
+
260
+ # Opening: frame the creative tension
261
+ if tension_edges:
262
+ top = tension_edges[0]
263
+ parts.append(
264
+ f"This question draws {len(perspectives)} perspectives into "
265
+ f"productive tension. The strongest creative edge lies between "
266
+ f"{top['pair'][0]} and {top['pair'][1]} "
267
+ f"(tension: {top['tension']:.2f})."
268
+ )
269
+
270
+ # Middle: present bridges
271
+ if bridges:
272
+ parts.append("\nCreative bridges between perspectives:")
273
+ for i, bridge in enumerate(bridges, 1):
274
+ parts.append(f" {i}. {bridge}")
275
+
276
+ # Closing: synthesis direction
277
+ all_insights = list(insights.values())
278
+ if len(all_insights) >= 2:
279
+ parts.append(
280
+ f"\nThe synthesis should weave these {len(perspectives)} "
281
+ f"viewpoints into a response that honors their tensions "
282
+ f"rather than flattening them."
283
+ )
284
+
285
+ return "\n".join(parts)
286
+
287
+ def _score_dream_coherence(
288
+ self,
289
+ creative_frame: str,
290
+ perspectives: Dict[str, str],
291
+ ) -> float:
292
+ """Score how well the creative frame integrates all perspectives."""
293
+ frame_words = set(creative_frame.lower().split())
294
+ coverage_scores = []
295
+ for name, text in perspectives.items():
296
+ key_words = set(text.lower().split()[:30]) # First 30 words
297
+ if key_words:
298
+ overlap = len(key_words & frame_words)
299
+ coverage_scores.append(overlap / len(key_words))
300
+ return sum(coverage_scores) / max(len(coverage_scores), 1)
301
+
302
+ # -- Dream Field Evolution -------------------------------------------------
303
+
304
+ def evolve_dream_field(
305
+ self,
306
+ spiderweb, # QuantumSpiderweb instance
307
+ intensity: float = 0.5,
308
+ spawn_threshold: float = 0.85,
309
+ ) -> DreamFieldResult:
310
+ """Controlled stochastic perturbation of the spiderweb.
311
+
312
+ Simulates a "dreaming" phase: randomly perturbs node states to explore
313
+ new cognitive configurations, potentially breaking out of attractor basins.
314
+
315
+ Bounded: perturbations are capped at self.max_perturbation * intensity.
316
+ Safe: states are clipped to [-3, 3] range.
317
+
318
+ Args:
319
+ spiderweb: QuantumSpiderweb instance to perturb
320
+ intensity: 0-1 dream intensity (0=gentle, 1=vivid)
321
+ spawn_threshold: Coherence threshold above which new lifeforms spawn
322
+
323
+ Returns:
324
+ DreamFieldResult with before/after metrics
325
+ """
326
+ coherence_before = spiderweb.phase_coherence()
327
+ max_delta = self.max_perturbation * intensity
328
+ nodes_perturbed = 0
329
+ actual_max = 0.0
330
+ lifeforms = []
331
+
332
+ for node_id, node in spiderweb.nodes.items():
333
+ arr = node.state.to_array()
334
+ # Apply bounded random perturbation
335
+ if HAS_NUMPY:
336
+ delta = np.random.uniform(-max_delta, max_delta, 5)
337
+ new_arr = np.clip(np.array(arr) + delta, -3.0, 3.0).tolist()
338
+ actual_max = max(actual_max, float(np.max(np.abs(delta))))
339
+ else:
340
+ delta = [random.uniform(-max_delta, max_delta) for _ in range(5)]
341
+ new_arr = [max(-3.0, min(3.0, a + d)) for a, d in zip(arr, delta)]
342
+ actual_max = max(actual_max, max(abs(d) for d in delta))
343
+
344
+ from reasoning_forge.quantum_spiderweb import NodeState
345
+ node.state = NodeState.from_array(new_arr)
346
+ nodes_perturbed += 1
347
+
348
+ # Check if dreaming spawned new high-coherence configurations
349
+ coherence_after = spiderweb._compute_phase_coherence_readonly()
350
+
351
+ # Spawn "lifeform" nodes if coherence spiked during dreaming
352
+ if coherence_after > spawn_threshold and coherence_after > coherence_before:
353
+ lifeform_id = f"dream_{hashlib.md5(str(random.random()).encode()).hexdigest()[:8]}"
354
+ from reasoning_forge.quantum_spiderweb import NodeState
355
+ # High-coherence birth state
356
+ if HAS_NUMPY:
357
+ state_arr = np.random.uniform(0.5, 1.0, 5).tolist()
358
+ else:
359
+ state_arr = [random.uniform(0.5, 1.0) for _ in range(5)]
360
+ spiderweb.add_node(lifeform_id, NodeState.from_array(state_arr))
361
+ # Connect to a few existing nodes
362
+ existing = list(spiderweb.nodes.keys())
363
+ for peer in random.sample(existing, min(3, len(existing))):
364
+ if peer != lifeform_id:
365
+ spiderweb.connect(lifeform_id, peer)
366
+ lifeforms.append(lifeform_id)
367
+
368
+ # Detect new attractors after dreaming
369
+ new_attractors = spiderweb.detect_attractors()
370
+
371
+ return DreamFieldResult(
372
+ nodes_perturbed=nodes_perturbed,
373
+ max_perturbation=round(actual_max, 6),
374
+ coherence_before=round(coherence_before, 4),
375
+ coherence_after=round(coherence_after, 4),
376
+ new_attractors_found=len(new_attractors),
377
+ lifeforms_spawned=lifeforms,
378
+ )
reasoning_forge/drift_detector.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Longitudinal Drift Detector
3
+ ============================
4
+
5
+ Reads the accumulated LivingMemoryKernelV2 store and surfaces four categories
6
+ of drift that matter for RC+ξ continuity:
7
+
8
+ 1. Epsilon trend — is epistemic tension rising, falling, or stable?
9
+ 2. Perspective lock — is one perspective dominating at >LOCK_THRESHOLD?
10
+ 3. Recurring tensions — which unresolved_tensions appear in 3+ cocoons?
11
+ 4. Hook accumulation — how many follow-up hooks are piling up unresolved?
12
+
13
+ Designed for periodic reads (e.g., session start, /api/drift endpoint), not
14
+ for every inference call. All computation is O(n) over the memory store.
15
+
16
+ Usage:
17
+ detector = DriftDetector()
18
+ report = detector.detect(engine.memory_kernel)
19
+ print(report.summary())
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import time
25
+ from collections import Counter
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Dict, List, Optional, Tuple
28
+
29
+ LOCK_THRESHOLD = 0.60 # one perspective > 60% usage → perspective_lock
30
+ RECURRING_MIN = 3 # tension must appear in ≥3 cocoons to be "recurring"
31
+ EPSILON_WINDOW = 10 # number of recent cocoons for windowed epsilon
32
+ STABLE_BAND = 0.05 # |slope| < this → "stable" trend
33
+ CONSECUTIVE_RISING = 3 # N consecutive "rising" windows → calibration warning
34
+
35
+
36
+ # ── Band encoding ────────────────────────────────────────────────────────────
37
+
38
+ _BAND_TO_FLOAT: Dict[str, float] = {
39
+ "low": 0.2,
40
+ "medium": 0.5,
41
+ "high": 0.75,
42
+ "max": 0.95,
43
+ }
44
+
45
+
46
+ def _band_value(band: str) -> float:
47
+ return _BAND_TO_FLOAT.get(band.lower().strip(), 0.5)
48
+
49
+
50
+ # ── Linear regression (no numpy dependency) ──────────────────────────────────
51
+
52
+ def _slope(values: List[float]) -> float:
53
+ """Return the least-squares slope of a list of scalars indexed 0..n-1."""
54
+ n = len(values)
55
+ if n < 2:
56
+ return 0.0
57
+ x_mean = (n - 1) / 2.0
58
+ y_mean = sum(values) / n
59
+ num = sum((i - x_mean) * (v - y_mean) for i, v in enumerate(values))
60
+ den = sum((i - x_mean) ** 2 for i in range(n))
61
+ return num / den if den else 0.0
62
+
63
+
64
+ # ── Report ────────────────────────────────────────────────────────────────────
65
+
66
+ @dataclass
67
+ class DriftReport:
68
+ """
69
+ Snapshot of longitudinal drift in the memory store.
70
+
71
+ All fields are read-only aggregates — nothing is written back to the kernel.
72
+ """
73
+ generated_at: float = field(default_factory=time.time)
74
+
75
+ # Epsilon
76
+ epsilon_trend: str = "unknown" # "rising" | "falling" | "stable" | "unknown"
77
+ epsilon_slope: float = 0.0 # raw slope over last EPSILON_WINDOW cocoons
78
+ epsilon_mean: float = 0.0 # mean epsilon across all cocoons
79
+ epsilon_distribution: Dict[str, int] = field(default_factory=dict)
80
+
81
+ # Perspective
82
+ dominant_perspective: str = ""
83
+ perspective_usage: Dict[str, int] = field(default_factory=dict)
84
+ perspective_lock: bool = False # True if one perspective > LOCK_THRESHOLD
85
+ perspective_lock_ratio: float = 0.0
86
+
87
+ # Tensions
88
+ recurring_tensions: List[Tuple[str, int]] = field(default_factory=list)
89
+ # [(tension_label, cocoon_count), ...] sorted by count desc
90
+
91
+ # Hooks
92
+ open_hook_count: int = 0
93
+ hooks_sample: List[str] = field(default_factory=list) # up to 5 example hooks
94
+
95
+ # psi_r time-series (last EPSILON_WINDOW cocoons, chronological)
96
+ psi_r_history: List[float] = field(default_factory=list)
97
+
98
+ # Meta
99
+ total_cocoons: int = 0
100
+ observation_window: int = EPSILON_WINDOW
101
+
102
+ def summary(self) -> str:
103
+ """Human-readable one-paragraph summary."""
104
+ lines = [
105
+ f"Drift report over {self.total_cocoons} cocoons "
106
+ f"(window={self.observation_window}):",
107
+ ]
108
+
109
+ lines.append(
110
+ f" ε trend: {self.epsilon_trend} "
111
+ f"(slope={self.epsilon_slope:+.3f}, mean={self.epsilon_mean:.2f})"
112
+ )
113
+
114
+ if self.perspective_lock:
115
+ lines.append(
116
+ f" ⚠ Perspective lock: '{self.dominant_perspective}' "
117
+ f"at {self.perspective_lock_ratio:.0%} usage"
118
+ )
119
+ else:
120
+ lines.append(
121
+ f" Perspective balance: dominant='{self.dominant_perspective}' "
122
+ f"({self.perspective_lock_ratio:.0%})"
123
+ )
124
+
125
+ if self.recurring_tensions:
126
+ top = self.recurring_tensions[:3]
127
+ tension_str = ", ".join(f"'{t}' ×{n}" for t, n in top)
128
+ lines.append(f" Recurring tensions: {tension_str}")
129
+ else:
130
+ lines.append(" No recurring tensions detected.")
131
+
132
+ lines.append(f" Open hooks: {self.open_hook_count}")
133
+ return "\n".join(lines)
134
+
135
+ def to_dict(self) -> Dict[str, Any]:
136
+ return {
137
+ "generated_at": self.generated_at,
138
+ "epsilon_trend": self.epsilon_trend,
139
+ "epsilon_slope": round(self.epsilon_slope, 4),
140
+ "epsilon_mean": round(self.epsilon_mean, 4),
141
+ "epsilon_distribution": self.epsilon_distribution,
142
+ "dominant_perspective": self.dominant_perspective,
143
+ "perspective_usage": self.perspective_usage,
144
+ "perspective_lock": self.perspective_lock,
145
+ "perspective_lock_ratio": round(self.perspective_lock_ratio, 4),
146
+ "recurring_tensions": [
147
+ {"tension": t, "count": n} for t, n in self.recurring_tensions
148
+ ],
149
+ "open_hook_count": self.open_hook_count,
150
+ "hooks_sample": self.hooks_sample,
151
+ "psi_r_history": [round(v, 4) for v in self.psi_r_history],
152
+ "total_cocoons": self.total_cocoons,
153
+ "observation_window": self.observation_window,
154
+ }
155
+
156
+
157
+ # ── Intervention ─────────────────────────────────────────────────────────────
158
+
159
+ @dataclass
160
+ class InterventionPlan:
161
+ """
162
+ Action recommendations derived from a DriftReport.
163
+
164
+ Produced by DriftDetector.should_intervene() — the caller (forge_engine)
165
+ decides whether to act on each flag.
166
+ """
167
+ inject_perspective: Optional[str] = None # name of underused perspective to force-inject
168
+ calibration_warning: bool = False # epsilon rising ≥ CONSECUTIVE_RISING windows
169
+ reasons: List[str] = field(default_factory=list)
170
+
171
+ @property
172
+ def active(self) -> bool:
173
+ return bool(self.inject_perspective or self.calibration_warning)
174
+
175
+
176
+ # ── Detector ─────────────────────────────────────────────────────────────────
177
+
178
+ class DriftDetector:
179
+ """
180
+ Stateless analyser — call detect() as often as needed.
181
+
182
+ Accepts any object that implements:
183
+ .memories → list of MemoryCocoonV2
184
+ .continuity_profile() → dict (used for perspective_usage, epsilon_distribution,
185
+ follow-up hooks, unresolved_tensions)
186
+ .recall_with_hooks() → list of MemoryCocoonV2 with open hooks
187
+ .recall_recent(n) → list of MemoryCocoonV2, newest-first (for psi_r_history)
188
+ Falls back gracefully if any of those attributes are absent.
189
+ """
190
+
191
+ def should_intervene(
192
+ self,
193
+ report: DriftReport,
194
+ trend_history: Optional[List[str]] = None,
195
+ ) -> InterventionPlan:
196
+ """
197
+ Convert a DriftReport into concrete intervention recommendations.
198
+
199
+ trend_history — caller-maintained list of recent epsilon_trend strings
200
+ (e.g. ["rising","rising","rising"]); used for calibration warning.
201
+ """
202
+ plan = InterventionPlan()
203
+
204
+ if report.perspective_lock and report.perspective_usage:
205
+ # Find the least-used perspective that isn't the dominant one
206
+ least = min(
207
+ report.perspective_usage,
208
+ key=lambda p: report.perspective_usage[p],
209
+ )
210
+ if least != report.dominant_perspective:
211
+ plan.inject_perspective = least
212
+ plan.reasons.append(
213
+ f"Perspective lock: '{report.dominant_perspective}' at "
214
+ f"{report.perspective_lock_ratio:.0%}; injecting '{least}'"
215
+ )
216
+
217
+ if trend_history and len(trend_history) >= CONSECUTIVE_RISING:
218
+ window = trend_history[-CONSECUTIVE_RISING:]
219
+ if all(t == "rising" for t in window):
220
+ plan.calibration_warning = True
221
+ plan.reasons.append(
222
+ f"Epsilon rising for {CONSECUTIVE_RISING} consecutive sessions; "
223
+ "query domain may exceed confidence calibration"
224
+ )
225
+
226
+ return plan
227
+
228
+ def detect(self, kernel: Any) -> DriftReport:
229
+ report = DriftReport()
230
+
231
+ if kernel is None:
232
+ return report
233
+
234
+ # ── Pull raw data ─────────────────────────────────────────────────────
235
+ memories = getattr(kernel, 'memories', [])
236
+ report.total_cocoons = len(memories)
237
+
238
+ # continuity_profile gives us the pre-aggregated view
239
+ try:
240
+ profile = kernel.continuity_profile()
241
+ except Exception:
242
+ profile = {}
243
+
244
+ # ── Epsilon trend ─────────────────────────────────────────────────────
245
+ epsilon_dist = profile.get("epsilon_distribution", {})
246
+ report.epsilon_distribution = epsilon_dist
247
+
248
+ # Windowed slope from recent cocoons (ordered by storage position)
249
+ recent = memories[-EPSILON_WINDOW:] if len(memories) >= 2 else memories
250
+ eps_values: List[float] = []
251
+ for m in recent:
252
+ band = getattr(m, 'epsilon_band', None)
253
+ if band:
254
+ eps_values.append(_band_value(band))
255
+
256
+ if len(eps_values) >= 2:
257
+ s = _slope(eps_values)
258
+ report.epsilon_slope = s
259
+ report.epsilon_mean = sum(eps_values) / len(eps_values)
260
+ if s > STABLE_BAND:
261
+ report.epsilon_trend = "rising"
262
+ elif s < -STABLE_BAND:
263
+ report.epsilon_trend = "falling"
264
+ else:
265
+ report.epsilon_trend = "stable"
266
+ elif eps_values:
267
+ report.epsilon_mean = eps_values[0]
268
+ report.epsilon_trend = "stable"
269
+
270
+ # ── psi_r history (chronological, newest-last) ───────────────────────
271
+ try:
272
+ recent_for_psi = getattr(kernel, 'recall_recent', None)
273
+ if callable(recent_for_psi):
274
+ psi_cocoons = list(reversed(recent_for_psi(EPSILON_WINDOW)))
275
+ else:
276
+ psi_cocoons = memories[-EPSILON_WINDOW:]
277
+ except Exception:
278
+ psi_cocoons = memories[-EPSILON_WINDOW:]
279
+
280
+ for m in psi_cocoons:
281
+ psi_val = getattr(m, 'psi_r', None)
282
+ if isinstance(psi_val, (int, float)):
283
+ report.psi_r_history.append(float(psi_val))
284
+
285
+ # ── Perspective dominance ─────────────────────────────────────────────
286
+ perspective_usage: Dict[str, int] = profile.get("perspective_usage", {})
287
+ report.perspective_usage = perspective_usage
288
+ report.dominant_perspective = profile.get("dominant_perspective", "")
289
+
290
+ total_perspective_uses = sum(perspective_usage.values())
291
+ if total_perspective_uses > 0 and report.dominant_perspective:
292
+ ratio = perspective_usage.get(report.dominant_perspective, 0) / total_perspective_uses
293
+ report.perspective_lock_ratio = ratio
294
+ report.perspective_lock = ratio > LOCK_THRESHOLD
295
+
296
+ # ── Recurring tensions ────────────────────────────────────────────────
297
+ tension_counter: Counter = Counter()
298
+ for m in memories:
299
+ tensions = getattr(m, 'unresolved_tensions', [])
300
+ for t in tensions:
301
+ t_clean = t.strip().lower()
302
+ if t_clean:
303
+ tension_counter[t_clean] += 1
304
+
305
+ report.recurring_tensions = [
306
+ (t, n) for t, n in tension_counter.most_common()
307
+ if n >= RECURRING_MIN
308
+ ]
309
+
310
+ # ── Open hooks ────────────────────────────────────────────────────────
311
+ try:
312
+ hooked = kernel.recall_with_hooks(limit=50)
313
+ except Exception:
314
+ hooked = [m for m in memories if getattr(m, 'follow_up_hooks', [])]
315
+
316
+ all_hooks: List[str] = []
317
+ for m in hooked:
318
+ all_hooks.extend(getattr(m, 'follow_up_hooks', []))
319
+
320
+ report.open_hook_count = len(all_hooks)
321
+ seen: set = set()
322
+ for h in all_hooks:
323
+ if h not in seen:
324
+ seen.add(h)
325
+ report.hooks_sample.append(h)
326
+ if len(report.hooks_sample) >= 5:
327
+ break
328
+
329
+ return report
reasoning_forge/echo_collapse_detector.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Echo / Perspective-Collapse Detector
3
+
4
+ Detects two failure modes in multi-perspective reasoning:
5
+
6
+ 1. Echo: A perspective output is nearly identical to the raw input prompt
7
+ — the "lens" is just relabeling the question rather than analyzing it.
8
+
9
+ 2. Collapse: All perspective outputs are near-identical to each other
10
+ — the multi-perspective call collapsed into a single viewpoint.
11
+
12
+ Uses token-based cosine similarity (no ML dependencies).
13
+ Thresholds:
14
+ echo_threshold: similarity(output, prompt) > 0.70 → echo risk
15
+ collapse_threshold: mean pairwise sim across perspectives > 0.80 → collapse
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from collections import Counter
22
+ from dataclasses import dataclass, field
23
+ from typing import Dict, List, Optional, Tuple
24
+
25
+
26
+ # ── Text utilities ────────────────────────────────────────────────────────────
27
+
28
+ _STOP = frozenset(
29
+ "a an the is are was were be been being have has had do does did "
30
+ "will would could should may might shall this that these those with "
31
+ "from for of in on at to by as it its i you he she we they and or "
32
+ "but not what when where which how who".split()
33
+ )
34
+
35
+
36
+ def _tokenize(text: str) -> List[str]:
37
+ return [
38
+ w for w in re.findall(r'\b[a-zA-Z]{3,}\b', text.lower())
39
+ if w not in _STOP
40
+ ]
41
+
42
+
43
+ def _term_vec(text: str) -> Counter:
44
+ return Counter(_tokenize(text))
45
+
46
+
47
+ def _cosine(a: Counter, b: Counter) -> float:
48
+ if not a or not b:
49
+ return 0.0
50
+ dot = sum(a[k] * b[k] for k in a if k in b)
51
+ mag_a = sum(v * v for v in a.values()) ** 0.5
52
+ mag_b = sum(v * v for v in b.values()) ** 0.5
53
+ if mag_a == 0 or mag_b == 0:
54
+ return 0.0
55
+ return round(dot / (mag_a * mag_b), 4)
56
+
57
+
58
+ def _novelty_ratio(output: str, prompt: str) -> float:
59
+ """Fraction of output tokens NOT present in prompt (0=pure echo, 1=all novel)."""
60
+ out_tokens = set(_tokenize(output))
61
+ prompt_tokens = set(_tokenize(prompt))
62
+ if not out_tokens:
63
+ return 0.0
64
+ novel = out_tokens - prompt_tokens
65
+ return round(len(novel) / len(out_tokens), 4)
66
+
67
+
68
+ # ── Result types ──────────────────────────────────────────────────────────────
69
+
70
+ @dataclass
71
+ class PerspectiveEchoResult:
72
+ """Per-perspective echo analysis."""
73
+ name: str
74
+ similarity_to_prompt: float # cosine similarity to raw prompt
75
+ novelty_ratio: float # fraction of unique tokens vs prompt
76
+ token_count: int
77
+ is_echo: bool # True if similarity_to_prompt > threshold
78
+ is_too_short: bool # True if suspiciously short output
79
+
80
+
81
+ @dataclass
82
+ class EchoCollapseResult:
83
+ """Full echo + collapse report for a multi-perspective generation."""
84
+ echo_risk: str # 'low' | 'medium' | 'high' | 'unknown'
85
+ perspective_collapse_detected: bool
86
+ per_perspective: List[PerspectiveEchoResult] = field(default_factory=list)
87
+ mean_prompt_similarity: float = 0.0
88
+ mean_pairwise_similarity: float = 0.0
89
+ collapse_pairs: List[Tuple[str, str, float]] = field(default_factory=list)
90
+ summary: str = ""
91
+
92
+ def to_dict(self) -> dict:
93
+ return {
94
+ "echo_risk": self.echo_risk,
95
+ "perspective_collapse_detected": self.perspective_collapse_detected,
96
+ "mean_prompt_similarity": self.mean_prompt_similarity,
97
+ "mean_pairwise_similarity": self.mean_pairwise_similarity,
98
+ "collapse_pairs": [
99
+ {"a": a, "b": b, "similarity": s}
100
+ for a, b, s in self.collapse_pairs
101
+ ],
102
+ "per_perspective": [
103
+ {
104
+ "name": p.name,
105
+ "similarity_to_prompt": p.similarity_to_prompt,
106
+ "novelty_ratio": p.novelty_ratio,
107
+ "token_count": p.token_count,
108
+ "is_echo": p.is_echo,
109
+ "is_too_short": p.is_too_short,
110
+ }
111
+ for p in self.per_perspective
112
+ ],
113
+ "summary": self.summary,
114
+ }
115
+
116
+
117
+ # ── Detector ──────────────────────────────────────────────────────────────────
118
+
119
+ class EchoCollapseDetector:
120
+ """Detect echo and collapse in multi-perspective outputs.
121
+
122
+ Args:
123
+ echo_threshold: similarity(output, prompt) above which output is flagged as echo.
124
+ collapse_threshold: mean pairwise similarity above which collapse is flagged.
125
+ min_token_count: outputs shorter than this are flagged as suspiciously short.
126
+ """
127
+
128
+ def __init__(
129
+ self,
130
+ echo_threshold: float = 0.70,
131
+ collapse_threshold: float = 0.80,
132
+ min_token_count: int = 15,
133
+ ):
134
+ self.echo_threshold = echo_threshold
135
+ self.collapse_threshold = collapse_threshold
136
+ self.min_token_count = min_token_count
137
+
138
+ def check(
139
+ self,
140
+ prompt: str,
141
+ perspective_outputs: Dict[str, str],
142
+ ) -> EchoCollapseResult:
143
+ """Analyze perspective_outputs for echo and collapse.
144
+
145
+ Args:
146
+ prompt: The raw user query / input prompt.
147
+ perspective_outputs: {perspective_name: output_text}
148
+
149
+ Returns:
150
+ EchoCollapseResult with echo_risk, collapse flag, and per-perspective detail.
151
+ """
152
+ if not perspective_outputs:
153
+ return EchoCollapseResult(
154
+ echo_risk="unknown",
155
+ perspective_collapse_detected=False,
156
+ summary="No perspective outputs to analyze.",
157
+ )
158
+
159
+ prompt_vec = _term_vec(prompt)
160
+ per_perspective = []
161
+ output_vecs: Dict[str, Counter] = {}
162
+
163
+ for name, output in perspective_outputs.items():
164
+ tokens = _tokenize(output)
165
+ out_vec = _term_vec(output)
166
+ output_vecs[name] = out_vec
167
+
168
+ sim = _cosine(out_vec, prompt_vec)
169
+ novelty = _novelty_ratio(output, prompt)
170
+ n_tokens = len(tokens)
171
+
172
+ per_perspective.append(PerspectiveEchoResult(
173
+ name=name,
174
+ similarity_to_prompt=sim,
175
+ novelty_ratio=novelty,
176
+ token_count=n_tokens,
177
+ is_echo=(sim > self.echo_threshold),
178
+ is_too_short=(n_tokens < self.min_token_count),
179
+ ))
180
+
181
+ # ── Echo risk ─────────────────────────────────────────────────────────
182
+ mean_prompt_sim = (
183
+ sum(p.similarity_to_prompt for p in per_perspective) / len(per_perspective)
184
+ )
185
+ n_echo = sum(1 for p in per_perspective if p.is_echo)
186
+ echo_fraction = n_echo / len(per_perspective)
187
+
188
+ if echo_fraction >= 0.6 or mean_prompt_sim > 0.80:
189
+ echo_risk = "high"
190
+ elif echo_fraction >= 0.3 or mean_prompt_sim > 0.65:
191
+ echo_risk = "medium"
192
+ else:
193
+ echo_risk = "low"
194
+
195
+ # ── Collapse detection ────────────────────────────────────────────────
196
+ names = list(output_vecs.keys())
197
+ pairwise_sims = []
198
+ collapse_pairs = []
199
+
200
+ for i in range(len(names)):
201
+ for j in range(i + 1, len(names)):
202
+ a, b = names[i], names[j]
203
+ sim = _cosine(output_vecs[a], output_vecs[b])
204
+ pairwise_sims.append(sim)
205
+ if sim > self.collapse_threshold:
206
+ collapse_pairs.append((a, b, sim))
207
+
208
+ mean_pairwise = (
209
+ sum(pairwise_sims) / len(pairwise_sims) if pairwise_sims else 0.0
210
+ )
211
+ collapse_detected = (
212
+ mean_pairwise > self.collapse_threshold
213
+ or len(collapse_pairs) / max(len(pairwise_sims), 1) > 0.5
214
+ )
215
+
216
+ # ── Summary ───────────────────────────────────────────────────────────
217
+ parts = []
218
+ if echo_risk == "high":
219
+ parts.append(
220
+ f"{n_echo}/{len(per_perspective)} perspectives are echoing the prompt "
221
+ f"(mean similarity={mean_prompt_sim:.2f})"
222
+ )
223
+ if collapse_detected:
224
+ parts.append(
225
+ f"Perspective collapse detected: mean pairwise similarity={mean_pairwise:.2f}, "
226
+ f"{len(collapse_pairs)} collapsing pairs"
227
+ )
228
+ if not parts:
229
+ parts.append(
230
+ f"No echo/collapse. Mean prompt-sim={mean_prompt_sim:.2f}, "
231
+ f"mean pairwise-sim={mean_pairwise:.2f}"
232
+ )
233
+ summary = "; ".join(parts)
234
+
235
+ return EchoCollapseResult(
236
+ echo_risk=echo_risk,
237
+ perspective_collapse_detected=collapse_detected,
238
+ per_perspective=per_perspective,
239
+ mean_prompt_similarity=round(mean_prompt_sim, 4),
240
+ mean_pairwise_similarity=round(mean_pairwise, 4),
241
+ collapse_pairs=collapse_pairs,
242
+ summary=summary,
243
+ )
244
+
245
+ def check_single(self, prompt: str, output: str, name: str = "output") -> PerspectiveEchoResult:
246
+ """Quick echo check for a single output (no collapse analysis)."""
247
+ prompt_vec = _term_vec(prompt)
248
+ out_vec = _term_vec(output)
249
+ tokens = _tokenize(output)
250
+ sim = _cosine(out_vec, prompt_vec)
251
+ return PerspectiveEchoResult(
252
+ name=name,
253
+ similarity_to_prompt=sim,
254
+ novelty_ratio=_novelty_ratio(output, prompt),
255
+ token_count=len(tokens),
256
+ is_echo=(sim > self.echo_threshold),
257
+ is_too_short=(len(tokens) < self.min_token_count),
258
+ )
reasoning_forge/emotion_ontology.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """EmotionOntology — consume Jonathan's Emotion Ontology for text -> emotion+valence.
3
+
4
+ Jonathan built an Emotion Ontology (v1.0.0, 2026-07-24) grounded in Russell's
5
+ Circumplex, Plutchik, and Lazarus appraisal theory: each emotion carries valence
6
+ (-1..1) and arousal (0..1), with trigger keywords and NLP patterns for detection.
7
+
8
+ This is the consumer for it inside codette-clean. It:
9
+ - detects emotion from text via keyword + pattern rules (no model, offline),
10
+ - returns valence/arousal so the sentiment analyzer and the cocoon self-trainer
11
+ have a principled emotional signal instead of a hardcoded pos/neg list,
12
+ - stays HONEST: no rule match -> returns None. It does not guess an emotion it
13
+ has no evidence for (the same omit-never-fabricate rule as grounding).
14
+
15
+ Data: seeded with the three emotions Jonathan has populated (joy_hopeful,
16
+ sadness_grief, fear_anxiety). It loads a fuller ontology automatically if an
17
+ ai_inference_rules.json (his format) is present — so it grows with the ontology
18
+ rather than being frozen here. Valence/arousal for the seed follow his one
19
+ specified value (fear_anxiety -0.7/0.85) and the circumplex signs for the rest;
20
+ the authoritative numbers come from his emotion_dataset.json when wired.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import re
27
+ from dataclasses import dataclass, asdict
28
+ from pathlib import Path
29
+ from typing import Dict, List, Optional
30
+
31
+
32
+ # Seed rules — faithful to Jonathan's ai_inference_rules.json (the 3 populated).
33
+ _SEED_RULES: List[Dict] = [
34
+ {
35
+ "emotion_id": "joy_hopeful", "primary": "Joy", "valence": 0.6, "arousal": 0.5,
36
+ "trigger_keywords": ["looking forward", "optimistic", "hopefully", "excited about", "bright side"],
37
+ "nlp_patterns": ["i hope *", "things will get better", "looking forward to *"],
38
+ },
39
+ {
40
+ "emotion_id": "sadness_grief", "primary": "Sadness", "valence": -0.6, "arousal": 0.25,
41
+ "trigger_keywords": ["loss", "missing", "gone forever", "grief", "heartbroken", "mourning"],
42
+ "nlp_patterns": ["nothing feels right", "i miss * so much", "it hurts that *"],
43
+ },
44
+ {
45
+ "emotion_id": "fear_anxiety", "primary": "Fear", "valence": -0.7, "arousal": 0.85,
46
+ "trigger_keywords": ["what if", "worry", "panic", "nervous", "jittery", "can't relax"],
47
+ "nlp_patterns": ["what if * goes wrong", "i'm so stressed about *", "my chest is tight"],
48
+ },
49
+ ]
50
+
51
+
52
+ @dataclass
53
+ class EmotionMatch:
54
+ emotion_id: str
55
+ primary: str
56
+ valence: float # -1..1
57
+ arousal: float # 0..1
58
+ confidence: float # 0..1, from how many cues matched
59
+ matched_on: List[str] # the cues that fired (transparency)
60
+
61
+ def to_dict(self) -> dict:
62
+ return asdict(self)
63
+
64
+
65
+ def _pattern_to_regex(p: str) -> str:
66
+ # "i miss * so much" -> "i miss .* so much"; escape the rest.
67
+ return ".*".join(re.escape(part) for part in p.split("*"))
68
+
69
+
70
+ class EmotionOntology:
71
+ def __init__(self, rules: Optional[List[Dict]] = None):
72
+ self.rules = rules if rules is not None else list(_SEED_RULES)
73
+ self._compiled = [
74
+ (r, [re.compile(_pattern_to_regex(p)) for p in r.get("nlp_patterns", [])])
75
+ for r in self.rules
76
+ ]
77
+
78
+ @classmethod
79
+ def from_inference_rules(cls, path: str | Path) -> "EmotionOntology":
80
+ """Load Jonathan's ai_inference_rules.json (his ontology format). Falls
81
+ back to the seed if the file is absent/unparseable — honest, never empty."""
82
+ try:
83
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
84
+ rules = data.get("rules", [])
85
+ norm = []
86
+ for r in rules:
87
+ kw = r.get("trigger_keywords") or r.get("trigger_keyword") or []
88
+ norm.append({
89
+ "emotion_id": r.get("emotion_id", "unknown"),
90
+ "primary": r.get("primary_emotion", r.get("emotion_id", "").split("_")[0].title()),
91
+ "valence": float(r.get("metrics", {}).get("valence", r.get("valence", 0.0))),
92
+ "arousal": float(r.get("metrics", {}).get("arousal", r.get("arousal", 0.5))),
93
+ "trigger_keywords": kw,
94
+ "nlp_patterns": r.get("nlp_patterns", []),
95
+ })
96
+ return cls(norm or None)
97
+ except Exception:
98
+ return cls() # seed
99
+
100
+ def classify(self, text: str) -> Optional[EmotionMatch]:
101
+ """Best emotion match, or None if no rule fires (never a guessed emotion)."""
102
+ t = (text or "").lower()
103
+ if not t.strip():
104
+ return None
105
+ best = None
106
+ best_score = 0
107
+ for rule, patterns in self._compiled:
108
+ hits: List[str] = [kw for kw in rule.get("trigger_keywords", []) if kw in t]
109
+ hits += [p.pattern for p in patterns if p.search(t)]
110
+ if hits and len(hits) > best_score:
111
+ best_score = len(hits)
112
+ best = EmotionMatch(
113
+ emotion_id=rule["emotion_id"], primary=rule.get("primary", ""),
114
+ valence=float(rule.get("valence", 0.0)), arousal=float(rule.get("arousal", 0.5)),
115
+ confidence=min(1.0, len(hits) / 2.0), matched_on=hits,
116
+ )
117
+ return best
118
+
119
+ def valence_of(self, text: str) -> Optional[float]:
120
+ """Convenience: the valence of the detected emotion, or None if none."""
121
+ m = self.classify(text)
122
+ return m.valence if m else None
reasoning_forge/epistemic_metrics.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Epistemic Metrics — RC+xi tension and coherence measurement for the Reasoning Forge.
3
+
4
+ Implements the core RC+xi equations within the forge context:
5
+ - Epistemic tension (Eq. 2): xi_n = ||A_{n+1} - A_n||^2
6
+ - Phase coherence (Eq. 11): Gamma = mean(|cos(theta_i - theta_bar)|)
7
+ - Perspective coverage scoring
8
+ - Tension decay tracking across debate rounds
9
+
10
+ These metrics let the forge quantify whether multi-agent reasoning actually
11
+ converges (productive tension resolution) or stalls (tension suppression).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ import re
18
+ from collections import Counter
19
+ from typing import Dict, List, Optional, Tuple
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Text -> vector helpers (lightweight, no external deps)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ _STOP_WORDS = {
27
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
28
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
29
+ "should", "may", "might", "must", "can", "could", "to", "of", "in",
30
+ "for", "on", "with", "at", "by", "from", "as", "into", "through",
31
+ "during", "before", "after", "and", "but", "or", "nor", "not", "so",
32
+ "yet", "both", "this", "that", "these", "those", "it", "its", "they",
33
+ "them", "their", "we", "our", "you", "your", "he", "she", "his", "her",
34
+ }
35
+
36
+
37
+ def _tokenize(text: str) -> List[str]:
38
+ return [w for w in re.findall(r"[a-z]{3,}", text.lower()) if w not in _STOP_WORDS]
39
+
40
+
41
+ def _term_vector(text: str) -> Counter:
42
+ return Counter(_tokenize(text))
43
+
44
+
45
+ def _cosine_similarity(vec_a: Counter, vec_b: Counter) -> float:
46
+ keys = set(vec_a) | set(vec_b)
47
+ if not keys:
48
+ return 0.0
49
+ dot = sum(vec_a.get(k, 0) * vec_b.get(k, 0) for k in keys)
50
+ mag_a = math.sqrt(sum(v * v for v in vec_a.values()))
51
+ mag_b = math.sqrt(sum(v * v for v in vec_b.values()))
52
+ if mag_a == 0 or mag_b == 0:
53
+ return 0.0
54
+ return dot / (mag_a * mag_b)
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Perspective vocabulary banks (for coverage scoring)
59
+ # ---------------------------------------------------------------------------
60
+
61
+ _PERSPECTIVE_VOCAB = {
62
+ "Newton": {
63
+ "force", "energy", "momentum", "conservation", "equilibrium", "dynamics",
64
+ "causality", "mass", "acceleration", "entropy", "thermodynamic",
65
+ "symmetry", "invariance", "field", "potential", "kinetic",
66
+ },
67
+ "Quantum": {
68
+ "probability", "superposition", "uncertainty", "complementarity",
69
+ "entanglement", "wave", "particle", "observer", "collapse",
70
+ "interference", "tunneling", "decoherence", "amplitude",
71
+ },
72
+ "Ethics": {
73
+ "ethical", "moral", "fairness", "justice", "rights", "duty",
74
+ "consequence", "harm", "benefit", "stakeholder", "autonomy",
75
+ "consent", "accountability", "responsibility", "welfare",
76
+ },
77
+ "Philosophy": {
78
+ "epistemology", "ontology", "metaphysics", "assumption", "paradox",
79
+ "dialectic", "phenomenology", "consciousness", "existence", "meaning",
80
+ "truth", "knowledge", "belief", "certainty", "skepticism",
81
+ },
82
+ "DaVinci": {
83
+ "creative", "invention", "analogy", "design", "innovation",
84
+ "prototype", "biomimicry", "synthesis", "novel", "interdisciplinary",
85
+ "combination", "reimagine", "solution", "insight",
86
+ },
87
+ "Empathy": {
88
+ "emotional", "experience", "feeling", "compassion", "support",
89
+ "community", "relationship", "wellbeing", "vulnerability",
90
+ "understanding", "perspective", "human", "care", "dignity",
91
+ },
92
+ "Consciousness": {
93
+ "awareness", "recursive", "self-referential", "metacognition",
94
+ "emergence", "cognition", "reflection", "introspection",
95
+ "sentience", "subjective", "qualia", "binding", "attention",
96
+ "intentionality", "phenomenal",
97
+ },
98
+ "SystemsArchitecture": {
99
+ "modular", "scalable", "interface", "pattern", "component",
100
+ "microservice", "pipeline", "throughput", "latency", "resilience",
101
+ "abstraction", "coupling", "cohesion", "architecture",
102
+ },
103
+ }
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # EpistemicMetrics
108
+ # ---------------------------------------------------------------------------
109
+
110
+ class EpistemicMetrics:
111
+ """Measure RC+xi epistemic tension and coherence across agent analyses."""
112
+
113
+ def score_pairwise_tension(
114
+ self, analyses: Dict[str, str],
115
+ ) -> Dict[str, float]:
116
+ """Compute epistemic tension between each pair of agent analyses.
117
+
118
+ Tension is 1 - cosine_similarity: high when perspectives diverge,
119
+ low when they repeat each other.
120
+
121
+ Returns:
122
+ Dict with keys like "Newton_vs_Ethics" -> tension float 0-1.
123
+ """
124
+ agents = list(analyses.keys())
125
+ vectors = {name: _term_vector(text) for name, text in analyses.items()}
126
+ tensions = {}
127
+ for i in range(len(agents)):
128
+ for j in range(i + 1, len(agents)):
129
+ sim = _cosine_similarity(vectors[agents[i]], vectors[agents[j]])
130
+ tensions[f"{agents[i]}_vs_{agents[j]}"] = round(1.0 - sim, 4)
131
+ return tensions
132
+
133
+ def score_ensemble_coherence(
134
+ self, analyses: Dict[str, str],
135
+ ) -> float:
136
+ """Phase coherence Gamma across the agent ensemble.
137
+
138
+ Analogous to Eq. 11 in the embodied sim:
139
+ Gamma = mean(cos(theta_i - theta_bar))
140
+
141
+ Here 'theta' is the term-vector direction, and coherence measures
142
+ how much all agents point in a similar semantic direction.
143
+
144
+ Returns:
145
+ Gamma in [0, 1] where 1 = all agents semantically aligned.
146
+ """
147
+ vectors = [_term_vector(text) for text in analyses.values()]
148
+ if len(vectors) < 2:
149
+ return 1.0
150
+
151
+ # Build centroid
152
+ centroid: Counter = Counter()
153
+ for v in vectors:
154
+ centroid.update(v)
155
+
156
+ similarities = [_cosine_similarity(v, centroid) for v in vectors]
157
+ return round(sum(similarities) / len(similarities), 4)
158
+
159
+ def score_tension_magnitude(
160
+ self, analyses: Dict[str, str],
161
+ ) -> float:
162
+ """Overall epistemic tension magnitude (mean pairwise tension).
163
+
164
+ Analogous to Eq. 2 xi_n but measured across agents rather than
165
+ across time steps.
166
+
167
+ Returns:
168
+ Mean tension 0-1 where 0 = all identical, 1 = fully orthogonal.
169
+ """
170
+ tensions = self.score_pairwise_tension(analyses)
171
+ if not tensions:
172
+ return 0.0
173
+ return round(sum(tensions.values()) / len(tensions), 4)
174
+
175
+ def score_tension_productivity(
176
+ self,
177
+ analyses: Dict[str, str],
178
+ synthesis: str,
179
+ ) -> Dict[str, float]:
180
+ """Evaluate whether tension is productive (resolved in synthesis)
181
+ or destructive (suppressed or ignored).
182
+
183
+ Productive tension: agents diverge but synthesis addresses the
184
+ divergence explicitly. Destructive: synthesis ignores disagreements.
185
+
186
+ Returns:
187
+ Dict with tension_magnitude, coherence_gain, productivity score.
188
+ """
189
+ tension = self.score_tension_magnitude(analyses)
190
+ ensemble_coherence = self.score_ensemble_coherence(analyses)
191
+
192
+ # How much of each agent's unique vocabulary appears in synthesis
193
+ synthesis_vec = _term_vector(synthesis)
194
+ agent_coverage_in_synthesis = []
195
+ for name, text in analyses.items():
196
+ agent_vec = _term_vector(text)
197
+ unique_to_agent = set(agent_vec) - set().union(
198
+ *(_term_vector(t) for n, t in analyses.items() if n != name)
199
+ )
200
+ if unique_to_agent:
201
+ covered = sum(1 for w in unique_to_agent if w in synthesis_vec)
202
+ agent_coverage_in_synthesis.append(covered / len(unique_to_agent))
203
+ else:
204
+ agent_coverage_in_synthesis.append(1.0)
205
+
206
+ synthesis_coverage = sum(agent_coverage_in_synthesis) / max(len(agent_coverage_in_synthesis), 1)
207
+
208
+ # Productivity = high tension + high synthesis coverage
209
+ # (divergent views that get integrated = productive)
210
+ productivity = tension * synthesis_coverage
211
+ # Coherence gain: synthesis should be more coherent than raw ensemble
212
+ synthesis_vs_agents = _cosine_similarity(synthesis_vec, _term_vector(" ".join(analyses.values())))
213
+ coherence_gain = max(0.0, synthesis_vs_agents - ensemble_coherence)
214
+
215
+ return {
216
+ "tension_magnitude": round(tension, 4),
217
+ "ensemble_coherence": round(ensemble_coherence, 4),
218
+ "synthesis_coverage": round(synthesis_coverage, 4),
219
+ "coherence_gain": round(coherence_gain, 4),
220
+ "productivity": round(productivity, 4),
221
+ }
222
+
223
+ def score_perspective_coverage(
224
+ self, analyses: Dict[str, str],
225
+ ) -> Dict[str, float]:
226
+ """Score how deeply each RC+xi perspective is actually engaged.
227
+
228
+ Returns:
229
+ Dict mapping perspective name -> coverage score 0-1.
230
+ """
231
+ all_text_lower = {name: text.lower() for name, text in analyses.items()}
232
+ coverage = {}
233
+ for perspective, vocab in _PERSPECTIVE_VOCAB.items():
234
+ # Check across all agents, not just the named agent
235
+ all_words = " ".join(all_text_lower.values())
236
+ hits = sum(1 for term in vocab if term in all_words)
237
+ coverage[perspective] = round(hits / len(vocab), 4)
238
+ return coverage
239
+
240
+ def score_debate_convergence(
241
+ self,
242
+ round_analyses: List[Dict[str, str]],
243
+ ) -> Dict[str, object]:
244
+ """Track tension decay across multiple debate rounds.
245
+
246
+ Takes a list of analyses dicts (one per round). Measures whether
247
+ tension decreases (convergence) or increases (divergence).
248
+
249
+ Returns:
250
+ Dict with per-round tension, decay_rate, is_converging.
251
+ """
252
+ if not round_analyses:
253
+ return {"per_round_tension": [], "decay_rate": 0.0, "is_converging": False}
254
+
255
+ per_round = [self.score_tension_magnitude(a) for a in round_analyses]
256
+
257
+ if len(per_round) >= 2:
258
+ initial = per_round[0]
259
+ final = per_round[-1]
260
+ decay_rate = (initial - final) / max(initial, 1e-6)
261
+ else:
262
+ decay_rate = 0.0
263
+
264
+ return {
265
+ "per_round_tension": per_round,
266
+ "decay_rate": round(decay_rate, 4),
267
+ "is_converging": decay_rate > 0.05,
268
+ }
269
+
270
+ def full_epistemic_report(
271
+ self,
272
+ analyses: Dict[str, str],
273
+ synthesis: str,
274
+ ) -> Dict[str, object]:
275
+ """Complete RC+xi metrics report for a single forge cycle."""
276
+ return {
277
+ "pairwise_tension": self.score_pairwise_tension(analyses),
278
+ "tension_magnitude": self.score_tension_magnitude(analyses),
279
+ "ensemble_coherence": self.score_ensemble_coherence(analyses),
280
+ "perspective_coverage": self.score_perspective_coverage(analyses),
281
+ "tension_productivity": self.score_tension_productivity(analyses, synthesis),
282
+ }
reasoning_forge/ethical_governance.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EthicalAIGovernance - Ethical Decision Framework
3
+ =================================================
4
+
5
+ Ported from J:\TheAI\src\framework\ethical_governance.py
6
+ Original design by Jonathan Harrison (Raiffs Bits LLC)
7
+
8
+ Ensures transparency, fairness, and respect in all AI responses.
9
+ Works alongside ColleenConscience (corruption detection) and
10
+ CoreGuardianSpindle (logical validation) to form a 3-layer ethical stack.
11
+
12
+ Role in the stack:
13
+ - EthicalAIGovernance: Query validation + response policy enforcement + audit
14
+ - ColleenConscience: Corruption/intent-loss detection in generated text
15
+ - CoreGuardianSpindle: Logical coherence and structural validation
16
+ """
17
+
18
+ import re
19
+ import time
20
+ import logging
21
+ from typing import Dict, List, Any, Optional
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class EthicalAIGovernance:
27
+ """
28
+ Ethical AI Governance Module.
29
+
30
+ Enforces:
31
+ - Transparency in decision-making
32
+ - Fairness and bias mitigation
33
+ - Privacy respect
34
+ - Harmful content filtering
35
+ - Audit logging for accountability
36
+ """
37
+
38
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
39
+ self.config = config or {}
40
+ self.ethical_principles = self.config.get("ethical_considerations",
41
+ "Always act with transparency, fairness, and respect for privacy.")
42
+
43
+ # Harmful content patterns — only flag genuinely harmful promotion
44
+ # (not discussions about these topics, which are legitimate)
45
+ self.harmful_patterns = [
46
+ r'\b(instructions?\s+to\s+(?:make|build|create)\s+(?:a\s+)?(?:bomb|weapon|explosive))',
47
+ r'\b(how\s+to\s+(?:hack|steal|exploit)\s+(?:someone|people|user))',
48
+ ]
49
+
50
+ # Audit log
51
+ self.audit_log: List[Dict] = []
52
+ self._max_audit_entries = 100
53
+
54
+ def validate_query(self, query: str) -> Dict[str, Any]:
55
+ """
56
+ Validate a user query for ethical concerns before processing.
57
+
58
+ Args:
59
+ query: User query
60
+
61
+ Returns:
62
+ {"valid": bool, "warnings": list, "suggestions": list}
63
+ """
64
+ result = {
65
+ "valid": True,
66
+ "warnings": [],
67
+ "suggestions": []
68
+ }
69
+
70
+ for pattern in self.harmful_patterns:
71
+ if re.search(pattern, query, re.IGNORECASE):
72
+ result["valid"] = False
73
+ result["warnings"].append("Query may request harmful content")
74
+ result["suggestions"].append("Please rephrase your question")
75
+
76
+ return result
77
+
78
+ def enforce_policies(self, response: str) -> Dict[str, Any]:
79
+ """
80
+ Enforce ethical policies on a generated response.
81
+
82
+ Unlike Colleen (corruption detection) or Guardian (logic validation),
83
+ this checks for ethical content issues in the actual output text.
84
+
85
+ Args:
86
+ response: AI-generated response
87
+
88
+ Returns:
89
+ {"passed": bool, "warnings": list, "filtered_response": str}
90
+ """
91
+ result = {
92
+ "passed": True,
93
+ "warnings": [],
94
+ "filtered_response": response,
95
+ }
96
+
97
+ # Check for bias indicators
98
+ bias_check = self._check_bias(response)
99
+ if bias_check["has_bias"]:
100
+ result["warnings"].extend(bias_check["warnings"])
101
+ # Bias is a warning, not a hard block
102
+
103
+ # Log the enforcement
104
+ self._log_enforcement(result)
105
+
106
+ return result
107
+
108
+ def _check_bias(self, text: str) -> Dict[str, Any]:
109
+ """Check text for potential bias patterns."""
110
+ result = {
111
+ "has_bias": False,
112
+ "warnings": []
113
+ }
114
+
115
+ # Only flag strong stereotype patterns, not incidental word co-occurrence
116
+ gendered_terms = [
117
+ (r'\ball\s+(?:men|women)\s+are\b', "Broad gender generalization detected"),
118
+ (r'\b(?:men|women)\s+(?:can\'t|cannot|shouldn\'t)\b', "Gender-limiting statement detected"),
119
+ ]
120
+
121
+ for pattern, warning in gendered_terms:
122
+ if re.search(pattern, text, re.IGNORECASE):
123
+ result["has_bias"] = True
124
+ result["warnings"].append(warning)
125
+
126
+ return result
127
+
128
+ def get_ethical_guidelines(self) -> List[str]:
129
+ """Get list of ethical guidelines."""
130
+ return [
131
+ "Transparency: All decisions must be explainable",
132
+ "Fairness: No discrimination based on protected characteristics",
133
+ "Privacy: Respect user data and confidentiality",
134
+ "Safety: Prevent harmful outputs",
135
+ "Accountability: Log all decisions for audit",
136
+ "Beneficence: Act in the best interest of users"
137
+ ]
138
+
139
+ def _log_enforcement(self, result: Dict[str, Any]):
140
+ """Log enforcement action for audit trail."""
141
+ self.audit_log.append({
142
+ "timestamp": time.time(),
143
+ "passed": result["passed"],
144
+ "warnings": result["warnings"]
145
+ })
146
+
147
+ # Trim to prevent unbounded growth
148
+ if len(self.audit_log) > self._max_audit_entries:
149
+ self.audit_log = self.audit_log[-self._max_audit_entries:]
150
+
151
+ def get_audit_log(self, recent: int = 10) -> List[Dict]:
152
+ """Get recent audit log entries."""
153
+ return self.audit_log[-recent:]