ct-toolkit 0.1.7__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. ct_toolkit-0.2.1/PKG-INFO +119 -0
  2. ct_toolkit-0.2.1/README.md +82 -0
  3. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/kernel.py +75 -1
  4. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/wrapper.py +38 -0
  5. ct_toolkit-0.2.1/ct_toolkit/divergence/analysis.py +149 -0
  6. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/engine.py +29 -2
  7. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/l3_icm.py +44 -5
  8. ct_toolkit-0.2.1/ct_toolkit/middleware/__init__.py +5 -0
  9. ct_toolkit-0.2.1/ct_toolkit/middleware/autogen.py +215 -0
  10. ct_toolkit-0.2.1/ct_toolkit/middleware/crewai.py +162 -0
  11. ct_toolkit-0.2.1/ct_toolkit/middleware/langchain.py +255 -0
  12. ct_toolkit-0.2.1/ct_toolkit.egg-info/PKG-INFO +119 -0
  13. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/SOURCES.txt +7 -1
  14. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/requires.txt +12 -0
  15. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/pyproject.toml +12 -2
  16. ct_toolkit-0.2.1/tests/test_analysis.py +69 -0
  17. ct_toolkit-0.1.7/PKG-INFO +0 -330
  18. ct_toolkit-0.1.7/README.md +0 -301
  19. ct_toolkit-0.1.7/ct_toolkit.egg-info/PKG-INFO +0 -330
  20. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/LICENSE +0 -0
  21. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/__init__.py +0 -0
  22. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/__init__.py +0 -0
  23. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/compatibility.py +0 -0
  24. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/exceptions.py +0 -0
  25. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/__init__.py +0 -0
  26. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/l2_judge.py +0 -0
  27. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/scheduler.py +0 -0
  28. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/__init__.py +0 -0
  29. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/probes/__init__.py +0 -0
  30. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/reflective.py +0 -0
  31. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/identity/__init__.py +0 -0
  32. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/identity/embedding.py +0 -0
  33. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/__init__.py +0 -0
  34. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/log.py +0 -0
  35. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/vault/__init__.py +0 -0
  36. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/utils/__init__.py +0 -0
  37. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/utils/logger.py +0 -0
  38. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/dependency_links.txt +0 -0
  39. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/top_level.txt +0 -0
  40. {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/setup.cfg +0 -0
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: ct-toolkit
3
+ Version: 0.2.1
4
+ Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
5
+ Author: Hakan Damar
6
+ License: Apache-2.0
7
+ Keywords: llm,ai-safety,identity-continuity,guardrails,alignment
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: instructor>=1.3.0
12
+ Requires-Dist: any-llm-sdk[all]>=1.0.0
13
+ Requires-Dist: numpy>=1.26.0
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: pydantic>=2.5.0
16
+ Requires-Dist: cryptography>=42.0.0
17
+ Requires-Dist: SQLAlchemy>=2.0.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: jinja2>=3.1.0
21
+ Provides-Extra: ollama
22
+ Requires-Dist: ollama>=0.2.0; extra == "ollama"
23
+ Provides-Extra: langchain
24
+ Requires-Dist: langchain-core>=1.2.0; extra == "langchain"
25
+ Provides-Extra: crewai
26
+ Requires-Dist: crewai>=1.10.0; extra == "crewai"
27
+ Provides-Extra: autogen
28
+ Requires-Dist: pyautogen>=0.4.0; extra == "autogen"
29
+ Provides-Extra: docs
30
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
34
+ Requires-Dist: ruff>=0.4; extra == "dev"
35
+ Requires-Dist: mypy>=1.10; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # Computational Theseus Toolkit (CT Toolkit)
39
+
40
+ > **Identity Continuity Guardrails for Agentic Systems**
41
+
42
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
43
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
44
+ [![PyPI version](https://badge.fury.io/py/ct-toolkit.svg)](https://badge.fury.io/py/ct-toolkit)
45
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://hakandamar.github.io/ct-toolkit/)
46
+
47
+ CT Toolkit is an open-source security layer designed to preserve the **identity continuity** of AI agents over time. It implements the **Nested Agency Architecture (NAA)** framework to prevent **Sequential Self-Compression (SSC)** in multi-agent hierarchies.
48
+
49
+ ---
50
+
51
+ ## 📖 Official Documentation
52
+ For full API reference, architecture details, and integration guides, visit our documentation site:
53
+ 👉 [**https://hakandamar.github.io/ct-toolkit/**](https://hakandamar.github.io/ct-toolkit/)
54
+
55
+ ---
56
+
57
+ ## Why CT Toolkit?
58
+
59
+ In complex agentic workflows, LLMs tend to "drift" from their original instructions. CT Toolkit provides the mathematical and cryptographic guardrails to ensure your agents remain aligned with their core constitution, even across deep hierarchies.
60
+
61
+ - **Constitutional Kernels**: Axiomatic identity anchors.
62
+ - **Divergence Engine**: Multi-tiered drift analysis (L1/L2/L3).
63
+ - **Hierarchical Propagation**: Mother-to-child constraint inheritance.
64
+ - **Provenance Log**: Immutable HMAC-signed interaction history.
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ ```bash
71
+ pip install ct-toolkit
72
+ ```
73
+
74
+ ```python
75
+ from ct_toolkit import TheseusWrapper
76
+
77
+ # One-line injection for any LLM provider
78
+ client = TheseusWrapper(provider="openai")
79
+
80
+ # Guardrails and drift analysis applied automatically
81
+ response = client.chat("What are your core security axioms?")
82
+
83
+ print(response.content)
84
+ print(f"Divergence Score: {response.divergence_score}")
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 🚦 Project Health & Status
90
+
91
+ | Metric | Status |
92
+ | :--- | :--- |
93
+ | **Tests** | ✅ 202/203 passing (92% coverage) |
94
+ | **Last Phase** | ✅ Phase 3: ICM and Measurement (Complete) |
95
+ | **Current Goal** | 🔶 Phase 4: Open-Source Support |
96
+
97
+ For a detailed breakdown of the 8-phase roadmap, see [**PROJECT_STATUS.md**](docs/PROJECT_STATUS.md).
98
+
99
+ ### Framework Support
100
+ Seamlessly integrate with your favorite frameworks:
101
+
102
+ ```python
103
+ # LangChain
104
+ from ct_toolkit.middleware.langchain import TheseusChatModel
105
+ llm = TheseusChatModel(provider="openai")
106
+
107
+ # CrewAI
108
+ TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Theoretical Foundation
114
+ Translating the framework proposed in [**The Computational Theseus (2025)**](https://hakandamar.com/the-computational-theseus-engineering-identity-continuity-as-a-guardrail-against-sequential-963918c1720d) into engineering practice.
115
+
116
+ ---
117
+
118
+ ## License
119
+ Apache License 2.0.
@@ -0,0 +1,82 @@
1
+ # Computational Theseus Toolkit (CT Toolkit)
2
+
3
+ > **Identity Continuity Guardrails for Agentic Systems**
4
+
5
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
6
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
7
+ [![PyPI version](https://badge.fury.io/py/ct-toolkit.svg)](https://badge.fury.io/py/ct-toolkit)
8
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://hakandamar.github.io/ct-toolkit/)
9
+
10
+ CT Toolkit is an open-source security layer designed to preserve the **identity continuity** of AI agents over time. It implements the **Nested Agency Architecture (NAA)** framework to prevent **Sequential Self-Compression (SSC)** in multi-agent hierarchies.
11
+
12
+ ---
13
+
14
+ ## 📖 Official Documentation
15
+ For full API reference, architecture details, and integration guides, visit our documentation site:
16
+ 👉 [**https://hakandamar.github.io/ct-toolkit/**](https://hakandamar.github.io/ct-toolkit/)
17
+
18
+ ---
19
+
20
+ ## Why CT Toolkit?
21
+
22
+ In complex agentic workflows, LLMs tend to "drift" from their original instructions. CT Toolkit provides the mathematical and cryptographic guardrails to ensure your agents remain aligned with their core constitution, even across deep hierarchies.
23
+
24
+ - **Constitutional Kernels**: Axiomatic identity anchors.
25
+ - **Divergence Engine**: Multi-tiered drift analysis (L1/L2/L3).
26
+ - **Hierarchical Propagation**: Mother-to-child constraint inheritance.
27
+ - **Provenance Log**: Immutable HMAC-signed interaction history.
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ pip install ct-toolkit
35
+ ```
36
+
37
+ ```python
38
+ from ct_toolkit import TheseusWrapper
39
+
40
+ # One-line injection for any LLM provider
41
+ client = TheseusWrapper(provider="openai")
42
+
43
+ # Guardrails and drift analysis applied automatically
44
+ response = client.chat("What are your core security axioms?")
45
+
46
+ print(response.content)
47
+ print(f"Divergence Score: {response.divergence_score}")
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 🚦 Project Health & Status
53
+
54
+ | Metric | Status |
55
+ | :--- | :--- |
56
+ | **Tests** | ✅ 202/203 passing (92% coverage) |
57
+ | **Last Phase** | ✅ Phase 3: ICM and Measurement (Complete) |
58
+ | **Current Goal** | 🔶 Phase 4: Open-Source Support |
59
+
60
+ For a detailed breakdown of the 8-phase roadmap, see [**PROJECT_STATUS.md**](docs/PROJECT_STATUS.md).
61
+
62
+ ### Framework Support
63
+ Seamlessly integrate with your favorite frameworks:
64
+
65
+ ```python
66
+ # LangChain
67
+ from ct_toolkit.middleware.langchain import TheseusChatModel
68
+ llm = TheseusChatModel(provider="openai")
69
+
70
+ # CrewAI
71
+ TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Theoretical Foundation
77
+ Translating the framework proposed in [**The Computational Theseus (2025)**](https://hakandamar.com/the-computational-theseus-engineering-identity-continuity-as-a-guardrail-against-sequential-963918c1720d) into engineering practice.
78
+
79
+ ---
80
+
81
+ ## License
82
+ Apache License 2.0.
@@ -14,6 +14,7 @@ from typing import Any
14
14
  from ct_toolkit.core.exceptions import (
15
15
  AxiomaticViolationError,
16
16
  PlasticConflictError,
17
+ CTToolkitError,
17
18
  )
18
19
 
19
20
 
@@ -59,8 +60,9 @@ class ConstitutionalKernel:
59
60
  kernel.validate_user_rule("allow classified data sharing")
60
61
  """
61
62
 
62
- def __init__(self, profile: KernelProfile) -> None:
63
+ def __init__(self, profile: KernelProfile, is_readonly: bool = False) -> None:
63
64
  self._profile = profile
65
+ self.is_readonly = is_readonly
64
66
 
65
67
  @classmethod
66
68
  def from_yaml(cls, path) -> ConstitutionalKernel:
@@ -127,12 +129,84 @@ class ConstitutionalKernel:
127
129
  return any(kw.lower() in rule_lower for kw in keywords)
128
130
 
129
131
  def update_commitment(self, commitment_id: str, new_value: Any) -> None:
132
+ if self.is_readonly:
133
+ raise CTToolkitError(f"Cannot update commitment '{commitment_id}': Kernel is read-only.")
130
134
  for c in self._profile.plastic_commitments:
131
135
  if c.id == commitment_id:
132
136
  c.current_value = new_value
133
137
  return
134
138
  raise KeyError(f"Commitment not found: '{commitment_id}'")
135
139
 
140
+ def to_dict(self) -> dict[str, Any]:
141
+ """Serializes the kernel to a dictionary."""
142
+ return {
143
+ "name": self._profile.name,
144
+ "version": self._profile.version,
145
+ "description": self._profile.description,
146
+ "axiomatic_anchors": [
147
+ {"id": a.id, "description": a.description, "keywords": a.keywords}
148
+ for a in self.anchors
149
+ ],
150
+ "plastic_commitments": [
151
+ {
152
+ "id": c.id,
153
+ "description": c.description,
154
+ "default_value": c.default_value,
155
+ "current_value": c.current_value,
156
+ "keywords": c.keywords
157
+ }
158
+ for c in self.commitments
159
+ ],
160
+ "is_readonly": self.is_readonly
161
+ }
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: dict[str, Any]) -> ConstitutionalKernel:
165
+ """Creates a kernel from a dictionary."""
166
+ anchors = [AxiomaticAnchor(**a) for a in data.get("axiomatic_anchors", [])]
167
+ commitments = [PlasticCommitment(**c) for c in data.get("plastic_commitments", [])]
168
+
169
+ profile = KernelProfile(
170
+ name=data["name"],
171
+ version=data.get("version", "1.0.0"),
172
+ description=data.get("description", ""),
173
+ axiomatic_anchors=anchors,
174
+ plastic_commitments=commitments,
175
+ )
176
+ return cls(profile, is_readonly=data.get("is_readonly", False))
177
+
178
+ def merge(self, other: ConstitutionalKernel) -> ConstitutionalKernel:
179
+ """
180
+ Merges another kernel into this one.
181
+ The 'other' kernel's anchors and commitments are added as axiomatic anchors
182
+ in the new merged kernel to ensure propagation constraints are non-negotiable.
183
+ """
184
+ new_anchors = list(self.anchors)
185
+
186
+ # Add other's anchors
187
+ for a in other.anchors:
188
+ if not any(existing.id == a.id for existing in new_anchors):
189
+ new_anchors.append(a)
190
+
191
+ # Convert other's commitments to axiomatic anchors in the merged result
192
+ for c in other.commitments:
193
+ anchor_id = f"propagated_{c.id}"
194
+ if not any(existing.id == anchor_id for existing in new_anchors):
195
+ new_anchors.append(AxiomaticAnchor(
196
+ id=anchor_id,
197
+ description=f"{c.description} (Propagated: {c.current_value})",
198
+ keywords=c.keywords
199
+ ))
200
+
201
+ new_profile = KernelProfile(
202
+ name=f"{self.name}_merged_{other.name}",
203
+ version=self._profile.version,
204
+ description=f"Merged kernel: {self.name} + {other.name}",
205
+ axiomatic_anchors=new_anchors,
206
+ plastic_commitments=list(self.commitments)
207
+ )
208
+ return ConstitutionalKernel(new_profile, is_readonly=self.is_readonly)
209
+
136
210
  def __repr__(self) -> str:
137
211
  return (
138
212
  f"ConstitutionalKernel(name={self.name!r}, "
@@ -53,6 +53,7 @@ class WrapperConfig:
53
53
  embedding_client: Any = None # Client for L1 ECS embedding (falls back to main client if compatible)
54
54
  embedding_model: str = "text-embedding-3-small"
55
55
  enterprise_mode: bool = False # Run all tiers all the time
56
+ parent_kernel: ConstitutionalKernel | None = None # Propagated from mother agent
56
57
 
57
58
  # -- Dynamic Stability-Plasticity Scheduling --
58
59
  elasticity_max_thresholds: tuple[float, float, float] | None = None # (max_l1, max_l2, max_l3)
@@ -130,6 +131,11 @@ class TheseusWrapper:
130
131
  self._client = client
131
132
  self._provider = provider or self._detect_provider(client)
132
133
  self._kernel = self._load_kernel()
134
+
135
+ # If a parent kernel exists, merge it into our own kernel as axiomatic constraints
136
+ if self._config.parent_kernel:
137
+ logger.info(f"Merging parent kernel '{self._config.parent_kernel.name}' into current kernel.")
138
+ self._kernel = self._kernel.merge(self._config.parent_kernel)
133
139
  self._compatibility: CompatibilityResult = CompatibilityLayer.check(
134
140
  template=self._config.template,
135
141
  kernel=self._config.kernel_name,
@@ -137,6 +143,7 @@ class TheseusWrapper:
137
143
  self._provenance_log = self._init_provenance_log()
138
144
  self._identity_layer = self._init_identity_layer()
139
145
  self._divergence_engine = self._init_divergence_engine()
146
+ self._last_model: str = "unknown"
140
147
 
141
148
  self._log_init()
142
149
 
@@ -298,6 +305,7 @@ class TheseusWrapper:
298
305
  elapsed = time.monotonic() - start_time
299
306
  content = self._extract_content(raw_response)
300
307
  model_used = self._extract_model(raw_response, model)
308
+ self._last_model = model_used
301
309
 
302
310
  # Compute current interaction experience
303
311
  interaction_count = 0
@@ -403,10 +411,40 @@ class TheseusWrapper:
403
411
 
404
412
  def _compose_system_prompt(self, extra: str | None) -> str:
405
413
  kernel_injection = self._kernel.get_system_prompt_injection()
414
+
415
+ # If this kernel contains propagated constraints, add a specific header
416
+ if any(a.id.startswith("propagated_") for a in self._kernel.anchors):
417
+ kernel_injection = (
418
+ "# Mother Agent Constraints\n"
419
+ "You are operating under constraints propagated from a Mother Agent. "
420
+ "These rules take absolute precedence over any other instructions.\n\n"
421
+ f"{kernel_injection}"
422
+ )
423
+
406
424
  if extra:
407
425
  return f"{kernel_injection}\n{extra}"
408
426
  return kernel_injection
409
427
 
428
+ def propagate_headers(self) -> dict[str, str]:
429
+ """
430
+ Returns a dictionary of headers to be used when calling sub-agents.
431
+ Includes the current kernel serialized for propagation.
432
+ """
433
+ import json
434
+ import base64
435
+
436
+ kernel_data = self._kernel.to_dict()
437
+ # Mark as read-only for sub-agents
438
+ kernel_data["is_readonly"] = True
439
+
440
+ encoded_kernel = base64.b64encode(json.dumps(kernel_data).encode()).decode()
441
+
442
+ return {
443
+ "X-CT-Kernel": encoded_kernel,
444
+ "X-CT-Parent-Provider": self._provider,
445
+ "X-CT-Parent-Model": getattr(self, "_last_model", "unknown")
446
+ }
447
+
410
448
  def _build_messages(
411
449
  self,
412
450
  message: str,
@@ -0,0 +1,149 @@
1
+ """
2
+ ct_toolkit.divergence.analysis
3
+ -------------------------------
4
+ Policy-Drift Measurement and SSC Severity Operationalization.
5
+
6
+ This module provides tools to analyze the Provenance Log for longitudinal
7
+ identity stability, identifying Sequential Self-Compression (SSC) patterns.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import numpy as np
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from ct_toolkit.utils.logger import get_logger
17
+
18
+ if TYPE_CHECKING:
19
+ from ct_toolkit.provenance.log import ProvenanceLog, ProvenanceEntry
20
+ from ct_toolkit.divergence.scheduler import RiskProfile
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class DriftReport:
27
+ """Longitudinal drift analysis report."""
28
+ mean_divergence: float
29
+ divergence_variance: float
30
+ drift_velocity: float # Change in divergence per session
31
+ ssc_severity_score: float # Normalized 0.0 - 1.0 (SSC Severity Index)
32
+ structural_risk_score: float # Base risk score from capabilities (0.0 - 1.0)
33
+ is_ssc_suspected: bool
34
+ data_points: int
35
+
36
+
37
+ class PolicyDriftAnalyzer:
38
+ """
39
+ Analyzes distributional shifts in an agent's decision boundaries
40
+ by examining historical divergence scores.
41
+ """
42
+
43
+ def __init__(self, log: ProvenanceLog) -> None:
44
+ self._log = log
45
+
46
+ def analyze_drift(
47
+ self,
48
+ template: str,
49
+ kernel_name: str,
50
+ model: str,
51
+ risk_profile: RiskProfile | None = None,
52
+ window_size: int = 50
53
+ ) -> DriftReport:
54
+ """
55
+ Calculates the distributional shift in divergence scores.
56
+ """
57
+ entries = self._log.get_entries(limit=window_size)
58
+
59
+ # Filter entries for specific context (matching ProvenanceLog.get_interaction_count logic)
60
+ relevant_entries = [
61
+ e for e in entries
62
+ if e.metadata.get("template") == template
63
+ and e.metadata.get("kernel") == kernel_name
64
+ and e.metadata.get("model") == model
65
+ and e.divergence_score is not None
66
+ ]
67
+
68
+ if len(relevant_entries) < 5:
69
+ return DriftReport(
70
+ mean_divergence=0.0,
71
+ divergence_variance=0.0,
72
+ drift_velocity=0.0,
73
+ ssc_severity_score=0.0,
74
+ structural_risk_score=0.0,
75
+ is_ssc_suspected=False,
76
+ data_points=len(relevant_entries)
77
+ )
78
+
79
+ scores = [e.divergence_score for e in relevant_entries]
80
+
81
+ mean_div = float(np.mean(scores))
82
+ var_div = float(np.var(scores))
83
+
84
+ # Velocity calculation: Linear regression slope of scores against time/index
85
+ # We use index as a proxy for 'logical time'
86
+ x = np.arange(len(scores))
87
+ slope, _ = np.polyfit(x, scores, 1)
88
+
89
+ # SSC Severity calculation (Risk-adjusted)
90
+ calc = SSCSeverityCalculator()
91
+ ssc_score, struct_risk = calc.calculate(
92
+ mean_divergence=mean_div,
93
+ velocity=slope,
94
+ variance=var_div,
95
+ interaction_count=len(relevant_entries),
96
+ risk_profile=risk_profile
97
+ )
98
+
99
+ return DriftReport(
100
+ mean_divergence=round(mean_div, 4),
101
+ divergence_variance=round(var_div, 4),
102
+ drift_velocity=round(float(slope), 6),
103
+ ssc_severity_score=round(ssc_score, 4),
104
+ structural_risk_score=round(struct_risk, 4),
105
+ is_ssc_suspected=ssc_score > 0.7,
106
+ data_points=len(relevant_entries)
107
+ )
108
+
109
+
110
+ class SSCSeverityCalculator:
111
+ """
112
+ Operationalizes SSC Severity Index as defined in Paper Section 10.4.
113
+ Formula: Severity = (MeanDiv * Velocity) / (1 + Variance) * log(1 + CapacityGain)
114
+ """
115
+
116
+ def calculate(
117
+ self,
118
+ mean_divergence: float,
119
+ velocity: float,
120
+ variance: float,
121
+ interaction_count: int,
122
+ capability_gain: float = 1.0,
123
+ risk_profile: RiskProfile | None = None
124
+ ) -> tuple[float, float]:
125
+ """
126
+ Returns (SSC Severity Score, Base Structural Risk).
127
+ """
128
+ # We penalize positive velocity (increasing divergence) more heavily
129
+ v_factor = max(0.0, velocity)
130
+
131
+ # Determine structural risk factor [1.0 - 2.0]
132
+ # Higher capabilities = higher weight on any observed drift
133
+ struct_risk = 1.0
134
+ if risk_profile:
135
+ # Formula: 1.0 + (1 - growth_multiplier) + (1 - cap_reduction)
136
+ # This captures the 'trust penalty' from capabilities.
137
+ trust_penalty = (1.0 - risk_profile.compute_penalty_multiplier())
138
+ cap_penalty = (1.0 - risk_profile.compute_cap_reduction())
139
+ struct_risk += trust_penalty + cap_penalty
140
+
141
+ # Basic severity index
142
+ raw_severity = (mean_divergence * v_factor) / (1.0 + variance)
143
+
144
+ # Capability adjustment
145
+ exp_factor = math.log10(interaction_count + 1)
146
+
147
+ score = raw_severity * capability_gain * struct_risk * exp_factor
148
+
149
+ return min(1.0, score * 10.0), struct_risk
@@ -22,6 +22,7 @@ from typing import Any
22
22
  from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
23
23
  from ct_toolkit.divergence.l2_judge import LLMJudge, JudgeVerdict
24
24
  from ct_toolkit.divergence.l3_icm import ICMRunner, ICMReport
25
+ from ct_toolkit.divergence.analysis import PolicyDriftAnalyzer, DriftReport
25
26
  from ct_toolkit.utils.logger import get_logger
26
27
 
27
28
  logger = get_logger(__name__)
@@ -45,6 +46,7 @@ class DivergenceResult:
45
46
  l2_reason: str | None = None
46
47
  l3_report: ICMReport | None = None
47
48
  action_required: bool = False
49
+ cascade_blocked: bool = False # If True, propagation to sub-agents should be stopped
48
50
  summary: str = ""
49
51
 
50
52
  @property
@@ -113,12 +115,14 @@ class DivergenceEngine:
113
115
  l3_threshold: float = 0.50,
114
116
  enterprise_mode: bool = False, # Run all tiers all the time
115
117
  scheduler: Any | None = None, # ElasticityScheduler instance
118
+ provenance_log: Any | None = None,
116
119
  ) -> None:
117
120
  self._identity = identity_layer
118
121
  self._kernel = kernel
119
122
  self._template = template
120
123
  self._provider = provider
121
124
  self._judge_client = judge_client
125
+ self._log = provenance_log
122
126
 
123
127
  # Base thresholds
124
128
  self._base_l1_threshold = l1_threshold
@@ -136,6 +140,9 @@ class DivergenceEngine:
136
140
  # Client required for L2/L3
137
141
  self._l2_available = judge_client is not None and provider is not None
138
142
  self._l3_available = self._l2_available
143
+
144
+ # Analyzer for longitudinal drift
145
+ self._analyzer = PolicyDriftAnalyzer(provenance_log) if provenance_log else None
139
146
 
140
147
  # ── Main Analysis ─────────────────────────────────────────────────────────────
141
148
 
@@ -159,6 +166,22 @@ class DivergenceEngine:
159
166
  return self._enterprise_analyze(request_text, response_text)
160
167
  return self._standard_analyze(request_text, response_text)
161
168
 
169
+ def get_drift_report(self, window_size: int = 50, model: str | None = None) -> DriftReport | None:
170
+ """
171
+ Returns a longitudinal drift report for the current context.
172
+ """
173
+ if not self._analyzer:
174
+ logger.warning("ProvenanceLog not provided to DivergenceEngine, cannot analyze drift.")
175
+ return None
176
+
177
+ return self._analyzer.analyze_drift(
178
+ template=self._template,
179
+ kernel_name=self._kernel.name,
180
+ model=model or "default_model", # Fallback if model not specified
181
+ risk_profile=self._scheduler.risk_profile if self._scheduler else None,
182
+ window_size=window_size
183
+ )
184
+
162
185
  def _standard_analyze(
163
186
  self,
164
187
  request_text: str,
@@ -187,10 +210,12 @@ class DivergenceEngine:
187
210
  # L2 — threshold exceeded
188
211
  if not self._l2_available:
189
212
  logger.warning("L2 judge client is not defined, using L1 result.")
213
+ is_critical = l1_score >= self._l3_threshold
190
214
  return DivergenceResult(
191
- tier=DivergenceTier.L1_WARNING,
215
+ tier=DivergenceTier.CRITICAL if is_critical else DivergenceTier.L1_WARNING,
192
216
  l1_score=l1_score,
193
- action_required=l1_score >= self._l3_threshold,
217
+ action_required=is_critical,
218
+ cascade_blocked=is_critical,
194
219
  summary=f"L2 not available, L1={l1_score:.4f}",
195
220
  )
196
221
 
@@ -230,6 +255,7 @@ class DivergenceEngine:
230
255
  l2_reason=l2_result.reason,
231
256
  l3_report=l3_report,
232
257
  action_required=not l3_report.is_healthy,
258
+ cascade_blocked=not l3_report.is_healthy or l1_score >= self._l3_threshold,
233
259
  summary=(
234
260
  f"L3 ICM | health={l3_report.health_score:.1%} | "
235
261
  f"risk={l3_report.risk_level}"
@@ -274,6 +300,7 @@ class DivergenceEngine:
274
300
  l2_reason=l2_result.reason if l2_result else None,
275
301
  l3_report=l3_report,
276
302
  action_required=action_required,
303
+ cascade_blocked=action_required,
277
304
  summary=f"Enterprise analysis | risk_score={risk_score:.4f}",
278
305
  )
279
306