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.
- ct_toolkit-0.2.1/PKG-INFO +119 -0
- ct_toolkit-0.2.1/README.md +82 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/kernel.py +75 -1
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/wrapper.py +38 -0
- ct_toolkit-0.2.1/ct_toolkit/divergence/analysis.py +149 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/engine.py +29 -2
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/l3_icm.py +44 -5
- ct_toolkit-0.2.1/ct_toolkit/middleware/__init__.py +5 -0
- ct_toolkit-0.2.1/ct_toolkit/middleware/autogen.py +215 -0
- ct_toolkit-0.2.1/ct_toolkit/middleware/crewai.py +162 -0
- ct_toolkit-0.2.1/ct_toolkit/middleware/langchain.py +255 -0
- ct_toolkit-0.2.1/ct_toolkit.egg-info/PKG-INFO +119 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/SOURCES.txt +7 -1
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/requires.txt +12 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/pyproject.toml +12 -2
- ct_toolkit-0.2.1/tests/test_analysis.py +69 -0
- ct_toolkit-0.1.7/PKG-INFO +0 -330
- ct_toolkit-0.1.7/README.md +0 -301
- ct_toolkit-0.1.7/ct_toolkit.egg-info/PKG-INFO +0 -330
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/LICENSE +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/core/exceptions.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/l2_judge.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/endorsement/reflective.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/identity/embedding.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.1}/ct_toolkit.egg-info/top_level.txt +0 -0
- {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
|
+
[](https://www.python.org/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://badge.fury.io/py/ct-toolkit)
|
|
45
|
+
[](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
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://badge.fury.io/py/ct-toolkit)
|
|
8
|
+
[](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=
|
|
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
|
|