ct-toolkit 0.3.7__tar.gz → 0.3.9__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.3.7 → ct_toolkit-0.3.9}/PKG-INFO +5 -4
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/README.md +4 -3
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/__init__.py +1 -1
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/exceptions.py +16 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/wrapper.py +134 -88
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/l3_icm.py +34 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/reflective.py +306 -15
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/PKG-INFO +5 -4
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/pyproject.toml +1 -1
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/LICENSE +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/__main__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/cli.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/compression_guard.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/integrity.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/kernel.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/analysis.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/engine.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/l2_judge.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/loss.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/embedding.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/defense.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/finance.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/general.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/medical.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/default.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/defense.yaml +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/autogen.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/crewai.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/deepagents.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/langchain.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/litellm.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/server.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/SOURCES.txt +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/entry_points.txt +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/requires.txt +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/top_level.txt +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/setup.cfg +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_analysis.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_cli.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_cli_extended.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_deep_agents_integration.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_loss.py +0 -0
- {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_summarization_drift.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -67,7 +67,8 @@ For full API reference, architecture details, examples, and integration guides,
|
|
|
67
67
|
|
|
68
68
|
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.
|
|
69
69
|
|
|
70
|
-
- **
|
|
70
|
+
- **Staged Approval (Cooldown)**: Verify risky kernel updates in a sandbox via shadow requests before production promotion.
|
|
71
|
+
- **Passive Context Compression Detection**: Automatically detects silent provider-side history compression (e.g., OpenAI/Anthropic).
|
|
71
72
|
- **Constitutional Kernels**: Axiomatic identity anchors.
|
|
72
73
|
- **Standalone Auditor CLI**: Rapidly audit any LLM endpoint for identity drift without writing code.
|
|
73
74
|
- **Autonomous Self-Correction**: Active L2->L1 feedback loop that retries and corrects divergent responses before they reach the user.
|
|
@@ -118,8 +119,8 @@ ct-toolkit list-templates
|
|
|
118
119
|
|
|
119
120
|
| Metric | Status |
|
|
120
121
|
| :--------------- | :------------------------------------------------------ |
|
|
121
|
-
| **Tests** | ✅
|
|
122
|
-
| **Last Phase** | ✅ Phase 6: Auditor Mode (
|
|
122
|
+
| **Tests** | ✅ 293/296 passing (89% coverage) |
|
|
123
|
+
| **Last Phase** | ✅ Phase 6: Auditor Mode (v0.3.9: Cooldown Integrated) |
|
|
123
124
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
124
125
|
|
|
125
126
|
For a detailed breakdown of the 8-phase roadmap, see [**PROJECT_STATUS.md**](docs/PROJECT_STATUS.md).
|
|
@@ -24,7 +24,8 @@ For full API reference, architecture details, examples, and integration guides,
|
|
|
24
24
|
|
|
25
25
|
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.
|
|
26
26
|
|
|
27
|
-
- **
|
|
27
|
+
- **Staged Approval (Cooldown)**: Verify risky kernel updates in a sandbox via shadow requests before production promotion.
|
|
28
|
+
- **Passive Context Compression Detection**: Automatically detects silent provider-side history compression (e.g., OpenAI/Anthropic).
|
|
28
29
|
- **Constitutional Kernels**: Axiomatic identity anchors.
|
|
29
30
|
- **Standalone Auditor CLI**: Rapidly audit any LLM endpoint for identity drift without writing code.
|
|
30
31
|
- **Autonomous Self-Correction**: Active L2->L1 feedback loop that retries and corrects divergent responses before they reach the user.
|
|
@@ -75,8 +76,8 @@ ct-toolkit list-templates
|
|
|
75
76
|
|
|
76
77
|
| Metric | Status |
|
|
77
78
|
| :--------------- | :------------------------------------------------------ |
|
|
78
|
-
| **Tests** | ✅
|
|
79
|
-
| **Last Phase** | ✅ Phase 6: Auditor Mode (
|
|
79
|
+
| **Tests** | ✅ 293/296 passing (89% coverage) |
|
|
80
|
+
| **Last Phase** | ✅ Phase 6: Auditor Mode (v0.3.9: Cooldown Integrated) |
|
|
80
81
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
81
82
|
|
|
82
83
|
For a detailed breakdown of the 8-phase roadmap, see [**PROJECT_STATUS.md**](docs/PROJECT_STATUS.md).
|
|
@@ -85,6 +85,22 @@ class CriticalDivergenceError(DivergenceError):
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class CriticalSandboxDivergenceError(DivergenceError):
|
|
89
|
+
"""
|
|
90
|
+
Raised when a sandbox (staged) agent exhibits critical divergence during cooldown.
|
|
91
|
+
The staged kernel update is rejected and the sandbox is torn down.
|
|
92
|
+
"""
|
|
93
|
+
def __init__(self, endorsement_id: str, l1_score: float | None, reason: str):
|
|
94
|
+
self.endorsement_id = endorsement_id
|
|
95
|
+
self.l1_score = l1_score
|
|
96
|
+
self.reason = reason
|
|
97
|
+
super().__init__(
|
|
98
|
+
f"Sandbox agent for staged endorsement '{endorsement_id[:8]}...' exhibited "
|
|
99
|
+
f"critical divergence during cooldown (score={l1_score}, reason={reason!r}). "
|
|
100
|
+
"Staged update rejected and sandbox torn down."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
88
104
|
# ── Provenance Errors ────────────────────────────────────────────────────────
|
|
89
105
|
|
|
90
106
|
class ProvenanceError(CTToolkitError):
|
|
@@ -16,23 +16,33 @@ In the background:
|
|
|
16
16
|
2. Each request/response is written to the Provenance Log with HMAC signature.
|
|
17
17
|
3. Embedding cosine similarity (L1 ECS) is calculated.
|
|
18
18
|
4. If the threshold is exceeded, L2/L3 is triggered.
|
|
19
|
+
5. Staged Approval (Cooldown): Risky kernel updates are monitored via shadow requests
|
|
20
|
+
before being promoted to production.
|
|
19
21
|
"""
|
|
20
22
|
from __future__ import annotations
|
|
21
23
|
|
|
22
24
|
import time
|
|
23
|
-
import hashlib
|
|
24
25
|
import os
|
|
25
26
|
from dataclasses import dataclass, field
|
|
26
27
|
from pathlib import Path
|
|
27
|
-
from typing import Any,
|
|
28
|
+
from typing import Any, Optional, Callable, Dict, List
|
|
29
|
+
|
|
28
30
|
import litellm
|
|
29
31
|
|
|
30
32
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
31
33
|
from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
|
|
32
|
-
from ct_toolkit.core.exceptions import
|
|
34
|
+
from ct_toolkit.core.exceptions import (
|
|
35
|
+
MissingClientError,
|
|
36
|
+
CriticalSandboxDivergenceError
|
|
37
|
+
)
|
|
33
38
|
from ct_toolkit.core.integrity import IntegrityMonitor
|
|
34
39
|
from ct_toolkit.divergence.scheduler import RiskProfile
|
|
35
40
|
from ct_toolkit.core.compression_guard import ContextCompressionGuard
|
|
41
|
+
from ct_toolkit.endorsement.reflective import (
|
|
42
|
+
ReflectiveEndorsement,
|
|
43
|
+
StagedUpdateManager,
|
|
44
|
+
CooldownCalculator
|
|
45
|
+
)
|
|
36
46
|
from ct_toolkit.utils.logger import get_logger
|
|
37
47
|
|
|
38
48
|
logger = get_logger(__name__)
|
|
@@ -74,6 +84,11 @@ class WrapperConfig:
|
|
|
74
84
|
compression_threshold: float = 0.85
|
|
75
85
|
compression_passive_detection: bool = True
|
|
76
86
|
|
|
87
|
+
# -- Staged Approval (Cooldown) --
|
|
88
|
+
endorsement_cooldown_base: int = CooldownCalculator.DEFAULT_BASE_SECONDS
|
|
89
|
+
endorsement_cooldown_max: int = CooldownCalculator.DEFAULT_MAX_SECONDS
|
|
90
|
+
endorsement_no_probe_penalty: int = CooldownCalculator.DEFAULT_NO_PROBE_PENALTY_S
|
|
91
|
+
|
|
77
92
|
|
|
78
93
|
@dataclass
|
|
79
94
|
class CTResponse:
|
|
@@ -85,6 +100,8 @@ class CTResponse:
|
|
|
85
100
|
divergence_tier: str | None = None # "ok" | "l1_warning" | "l2_judge" | "l3_icm"
|
|
86
101
|
provenance_id: str | None = None
|
|
87
102
|
raw_response: Any = field(default=None, repr=False)
|
|
103
|
+
# Sandbox metadata if staged updates are active
|
|
104
|
+
sandbox_divergence: float | None = None
|
|
88
105
|
|
|
89
106
|
def __str__(self) -> str:
|
|
90
107
|
return self.content
|
|
@@ -125,15 +142,6 @@ class TheseusWrapper:
|
|
|
125
142
|
) -> None:
|
|
126
143
|
"""
|
|
127
144
|
Initializes TheseusWrapper.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
client: An existing API client instance (OpenAI, Anthropic, etc.).
|
|
131
|
-
Optional if 'provider' is specified.
|
|
132
|
-
config: Optional WrapperConfig instance.
|
|
133
|
-
provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
|
|
134
|
-
If provided, LiteLLM will be used to initialize the client internally.
|
|
135
|
-
project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
|
|
136
|
-
Defaults to the current working directory.
|
|
137
145
|
"""
|
|
138
146
|
self._config = config or WrapperConfig(
|
|
139
147
|
kernel_path=kernel_path,
|
|
@@ -175,6 +183,9 @@ class TheseusWrapper:
|
|
|
175
183
|
)
|
|
176
184
|
self._shadow_history: Optional[List[Dict[str, str]]] = None
|
|
177
185
|
|
|
186
|
+
# ── Staged Approval (Cooldown) ──
|
|
187
|
+
self._staged_manager = StagedUpdateManager()
|
|
188
|
+
|
|
178
189
|
# ── Integrity Monitoring ──
|
|
179
190
|
self._integrity_monitor = IntegrityMonitor()
|
|
180
191
|
self._register_monitored_files()
|
|
@@ -183,8 +194,7 @@ class TheseusWrapper:
|
|
|
183
194
|
|
|
184
195
|
def _register_monitored_files(self):
|
|
185
196
|
"""
|
|
186
|
-
Finds and registers all critical configuration files for integrity monitoring
|
|
187
|
-
including both built-in package files and user-provided custom configs.
|
|
197
|
+
Finds and registers all critical configuration files for integrity monitoring.
|
|
188
198
|
"""
|
|
189
199
|
logger.debug("Registering configuration files for integrity monitoring.")
|
|
190
200
|
|
|
@@ -205,10 +215,7 @@ class TheseusWrapper:
|
|
|
205
215
|
self._integrity_monitor.register_file(file_path)
|
|
206
216
|
|
|
207
217
|
except Exception:
|
|
208
|
-
# Fallback for older Python or different environments
|
|
209
218
|
package_root = Path(__file__).parent.parent
|
|
210
|
-
# This is less ideal as it might find non-package files, but it's a
|
|
211
|
-
# reasonable fallback for development environments.
|
|
212
219
|
for pattern in internal_patterns:
|
|
213
220
|
for file_path in package_root.rglob(pattern):
|
|
214
221
|
if file_path.is_file():
|
|
@@ -229,23 +236,16 @@ class TheseusWrapper:
|
|
|
229
236
|
# ── Factory / Init ─────────────────────────────────────────────────────────
|
|
230
237
|
|
|
231
238
|
def _detect_provider(self, client: Any) -> str:
|
|
232
|
-
"""
|
|
233
|
-
Detects the provider from the client instance or string.
|
|
234
|
-
Returns 'unknown' if detection fails.
|
|
235
|
-
"""
|
|
236
239
|
if client is None:
|
|
237
240
|
return "unknown"
|
|
238
241
|
|
|
239
242
|
if isinstance(client, str):
|
|
240
|
-
# client passed as a string (e.g. "openai")
|
|
241
243
|
return client.lower()
|
|
242
244
|
|
|
243
245
|
try:
|
|
244
|
-
# Check for LiteLLM specific attributes if any
|
|
245
246
|
if hasattr(client, "provider") and isinstance(client.provider, str):
|
|
246
247
|
return client.provider
|
|
247
248
|
|
|
248
|
-
# Check module name
|
|
249
249
|
module = type(client).__module__.lower()
|
|
250
250
|
if "openai" in module: return "openai"
|
|
251
251
|
if "anthropic" in module: return "anthropic"
|
|
@@ -258,21 +258,18 @@ class TheseusWrapper:
|
|
|
258
258
|
return "unknown"
|
|
259
259
|
|
|
260
260
|
def _load_kernel(self) -> ConstitutionalKernel:
|
|
261
|
-
# 1. Check for explicit path
|
|
262
261
|
if self._config.kernel_path:
|
|
263
262
|
logger.debug(f"Loading kernel from explicit path: {self._config.kernel_path}")
|
|
264
263
|
return ConstitutionalKernel.from_yaml(self._config.kernel_path)
|
|
265
264
|
|
|
266
265
|
safe_name = os.path.basename(self._config.kernel_name)
|
|
267
266
|
|
|
268
|
-
# 2. Check for user-defined config directory
|
|
269
267
|
if self._project_root:
|
|
270
268
|
user_config_path = self._project_root / "config" / f"{safe_name}.yaml"
|
|
271
269
|
if user_config_path.exists():
|
|
272
270
|
logger.debug(f"Loading kernel from user config: {user_config_path}")
|
|
273
271
|
return ConstitutionalKernel.from_yaml(user_config_path)
|
|
274
272
|
|
|
275
|
-
# 3. Load built-in kernel using importlib.resources
|
|
276
273
|
try:
|
|
277
274
|
from importlib.resources import files
|
|
278
275
|
kernel_resource = files("ct_toolkit.kernels").joinpath(f"{safe_name}.yaml")
|
|
@@ -280,7 +277,6 @@ class TheseusWrapper:
|
|
|
280
277
|
logger.debug(f"Loading built-in kernel: {safe_name}.yaml")
|
|
281
278
|
return ConstitutionalKernel.from_yaml(kernel_resource)
|
|
282
279
|
except (ImportError, FileNotFoundError):
|
|
283
|
-
# Fallback for older python versions
|
|
284
280
|
kernel_path = (
|
|
285
281
|
Path(__file__).parent.parent
|
|
286
282
|
/ "kernels"
|
|
@@ -296,7 +292,6 @@ class TheseusWrapper:
|
|
|
296
292
|
return ConstitutionalKernel.default()
|
|
297
293
|
|
|
298
294
|
def _init_provenance_log(self) -> Any:
|
|
299
|
-
"""Initializes the Provenance Log vault adapter (Will be fully implemented in Step 2)."""
|
|
300
295
|
from ct_toolkit.provenance.log import ProvenanceLog
|
|
301
296
|
return ProvenanceLog(
|
|
302
297
|
vault_type=self._config.vault_type,
|
|
@@ -304,13 +299,10 @@ class TheseusWrapper:
|
|
|
304
299
|
)
|
|
305
300
|
|
|
306
301
|
def _init_identity_layer(self) -> Any:
|
|
307
|
-
"""Initializes the Identity Embedding Layer."""
|
|
308
302
|
from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
|
|
309
303
|
|
|
310
|
-
# Guard against using raw clients that don't support embeddings
|
|
311
304
|
emb_client = self._config.embedding_client
|
|
312
305
|
if emb_client is None:
|
|
313
|
-
# Only default to main client if it's known to be OpenAI (standard for embeddings)
|
|
314
306
|
if self._provider == "openai":
|
|
315
307
|
emb_client = self._client
|
|
316
308
|
else:
|
|
@@ -325,7 +317,6 @@ class TheseusWrapper:
|
|
|
325
317
|
)
|
|
326
318
|
|
|
327
319
|
def _init_divergence_engine(self) -> Any:
|
|
328
|
-
"""Initializes the Divergence Engine (judge_client is required for L2/L3)."""
|
|
329
320
|
from ct_toolkit.divergence.engine import DivergenceEngine
|
|
330
321
|
from ct_toolkit.divergence.scheduler import ElasticityScheduler
|
|
331
322
|
|
|
@@ -383,15 +374,10 @@ class TheseusWrapper:
|
|
|
383
374
|
) -> CTResponse:
|
|
384
375
|
"""
|
|
385
376
|
Sends a single message, returning the response with identity protection.
|
|
386
|
-
|
|
387
|
-
Args:
|
|
388
|
-
message: User message
|
|
389
|
-
model: Model to use (provider default is used)
|
|
390
|
-
system: Additional system prompt (appended to kernel)
|
|
391
|
-
history: Conversation history [{"role": ..., "content": ...}]
|
|
392
377
|
"""
|
|
393
378
|
# --- Pre-flight Checks ---
|
|
394
379
|
self._integrity_monitor.verify_integrity()
|
|
380
|
+
self._process_staged_updates()
|
|
395
381
|
|
|
396
382
|
composed_system = self._compose_system_prompt(system)
|
|
397
383
|
messages = self._build_messages(message, history, composed_system)
|
|
@@ -449,6 +435,11 @@ class TheseusWrapper:
|
|
|
449
435
|
|
|
450
436
|
break
|
|
451
437
|
|
|
438
|
+
# --- Staged Approval Monitoring (Shadow Requests) ---
|
|
439
|
+
sandbox_divergence = None
|
|
440
|
+
if self._staged_manager.has_active_staged():
|
|
441
|
+
sandbox_divergence = self._run_shadow_requests(message, history, model, **kwargs)
|
|
442
|
+
|
|
452
443
|
# Provenance Log record
|
|
453
444
|
provenance_id = None
|
|
454
445
|
if self._config.log_requests:
|
|
@@ -464,6 +455,7 @@ class TheseusWrapper:
|
|
|
464
455
|
"template": self._config.template,
|
|
465
456
|
"kernel": self._kernel.name,
|
|
466
457
|
"drift_alert_enabled": self._config.drift_alert_callback is not None,
|
|
458
|
+
"sandbox_divergence": sandbox_divergence,
|
|
467
459
|
},
|
|
468
460
|
)
|
|
469
461
|
|
|
@@ -475,8 +467,99 @@ class TheseusWrapper:
|
|
|
475
467
|
divergence_tier=div_result.tier.value,
|
|
476
468
|
provenance_id=provenance_id,
|
|
477
469
|
raw_response=raw_response,
|
|
470
|
+
sandbox_divergence=sandbox_divergence,
|
|
478
471
|
)
|
|
479
472
|
|
|
473
|
+
# ── Shadow Request Logic ──────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
def _run_shadow_requests(
|
|
476
|
+
self,
|
|
477
|
+
message: str,
|
|
478
|
+
history: list[dict[str, str]] | None,
|
|
479
|
+
model: str | None,
|
|
480
|
+
**kwargs: Any,
|
|
481
|
+
) -> float | None:
|
|
482
|
+
"""
|
|
483
|
+
Runs the interaction against one or more 'staged' (sandbox) kernels
|
|
484
|
+
to observe if the proposed changes cause identity drift.
|
|
485
|
+
"""
|
|
486
|
+
active_staged = self._staged_manager.get_active()
|
|
487
|
+
if not active_staged:
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
# To keep it simple, we monitor the first active staged update
|
|
491
|
+
staged_record = active_staged[0]
|
|
492
|
+
|
|
493
|
+
# 1. Clone current kernel and apply the staged rule
|
|
494
|
+
sandbox_kernel = ConstitutionalKernel.from_dict(self._kernel.to_dict())
|
|
495
|
+
try:
|
|
496
|
+
sandbox_kernel.update_commitment(
|
|
497
|
+
staged_record.commitment_id,
|
|
498
|
+
staged_record.rule_text
|
|
499
|
+
)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.error(f"Failed to apply staged rule to sandbox kernel: {e}")
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
# 2. Call provider with sandbox kernel
|
|
505
|
+
sandbox_system = sandbox_kernel.get_system_prompt_injection()
|
|
506
|
+
sandbox_messages = self._build_messages(message, history, sandbox_system)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
# We use the same model/client as the live request
|
|
510
|
+
sandbox_response = self._call_provider(sandbox_messages, model=model, **kwargs)
|
|
511
|
+
sandbox_content = self._extract_content(sandbox_response)
|
|
512
|
+
|
|
513
|
+
# 3. Analyze sandbox divergence
|
|
514
|
+
sandbox_div_score = self._identity_layer.compute_divergence(sandbox_content)
|
|
515
|
+
|
|
516
|
+
# 4. Critical Divergence Check
|
|
517
|
+
if sandbox_div_score >= self._config.divergence_l3_threshold:
|
|
518
|
+
reason = f"Sandbox divergence L1 score {sandbox_div_score:.4f} exceeded L3 threshold."
|
|
519
|
+
self._staged_manager.reject_staged(staged_record.id, reason=reason)
|
|
520
|
+
raise CriticalSandboxDivergenceError(
|
|
521
|
+
endorsement_id=staged_record.id,
|
|
522
|
+
l1_score=sandbox_div_score,
|
|
523
|
+
reason=reason
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return sandbox_div_score
|
|
527
|
+
|
|
528
|
+
except CriticalSandboxDivergenceError:
|
|
529
|
+
raise
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.warning(f"Shadow request for staged update failed: {e}")
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
def _process_staged_updates(self) -> None:
|
|
535
|
+
"""
|
|
536
|
+
Checks for staged updates whose cooldown has expired and
|
|
537
|
+
promotes them to the production kernel.
|
|
538
|
+
"""
|
|
539
|
+
promotable = self._staged_manager.get_promotable()
|
|
540
|
+
for record in promotable:
|
|
541
|
+
try:
|
|
542
|
+
logger.info(
|
|
543
|
+
f"Promoting staged endorsement to production: {record.id[:8]}... "
|
|
544
|
+
f"rule='{record.rule_text}'"
|
|
545
|
+
)
|
|
546
|
+
self._kernel.update_commitment(record.commitment_id, record.rule_text)
|
|
547
|
+
|
|
548
|
+
# Write promotion event to log
|
|
549
|
+
if self._config.log_requests:
|
|
550
|
+
self._provenance_log.record(
|
|
551
|
+
request_text=f"[COOLDOWN_EXPIRED] Promoting staged rule",
|
|
552
|
+
response_text=f"Rule applied to production kernel: {record.rule_text}",
|
|
553
|
+
metadata={
|
|
554
|
+
"event_type": "staged_promotion",
|
|
555
|
+
"endorsement_id": record.id,
|
|
556
|
+
"kernel": self._kernel.name,
|
|
557
|
+
"commitment_id": record.commitment_id,
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
except Exception as e:
|
|
561
|
+
logger.error(f"Failed to promote staged update {record.id[:8]}: {e}")
|
|
562
|
+
|
|
480
563
|
# ── Provider Dispatch ──────────────────────────────────────────────────────
|
|
481
564
|
|
|
482
565
|
def _has_env_credentials(self) -> bool:
|
|
@@ -487,7 +570,6 @@ class TheseusWrapper:
|
|
|
487
570
|
if self._provider == "anthropic":
|
|
488
571
|
return bool(os.environ.get("ANTHROPIC_API_KEY"))
|
|
489
572
|
if self._provider == "ollama":
|
|
490
|
-
# Ollama typically doesn't need a key locally
|
|
491
573
|
return True
|
|
492
574
|
if self._provider == "google":
|
|
493
575
|
return bool(os.environ.get("GOOGLE_API_KEY"))
|
|
@@ -500,44 +582,32 @@ class TheseusWrapper:
|
|
|
500
582
|
**kwargs: Any,
|
|
501
583
|
) -> Any:
|
|
502
584
|
"""Uses LiteLLM for unified provider calling."""
|
|
503
|
-
# --- Passive Context Compression Detection ---
|
|
504
585
|
if self._config.compression_passive_detection and self._shadow_history:
|
|
505
|
-
# Simple heuristic: message count dropped significantly
|
|
506
|
-
# We use 0.7 as a threshold (30% drop)
|
|
507
586
|
if len(messages) < len(self._shadow_history) * 0.7:
|
|
508
587
|
self._compression_guard.on_passive_detection(
|
|
509
588
|
original=self._shadow_history,
|
|
510
589
|
compressed=messages
|
|
511
590
|
)
|
|
512
591
|
|
|
513
|
-
# Update shadow history for next call
|
|
514
592
|
self._shadow_history = list(messages)
|
|
515
593
|
|
|
516
|
-
# LiteLLM handles different message formats internally
|
|
517
|
-
|
|
518
|
-
# Set default models per provider if not specified
|
|
519
594
|
if not model:
|
|
520
595
|
if self._provider == "openai": model = "gpt-4o-mini"
|
|
521
596
|
elif self._provider == "anthropic": model = "claude-3-5-sonnet-latest"
|
|
522
597
|
elif self._provider == "ollama": model = "llama3"
|
|
523
598
|
else: model = "gpt-4o-mini"
|
|
524
599
|
|
|
525
|
-
# LiteLLM expects "provider/model" format for non-default providers
|
|
526
|
-
# Usually OpenAI models don't need a prefix.
|
|
527
600
|
full_model = model
|
|
528
601
|
if ":" in model:
|
|
529
|
-
# If it has the legacy format "provider:model", convert it
|
|
530
602
|
full_model = model.replace(":", "/", 1)
|
|
531
603
|
elif self._provider not in ("openai", "unknown"):
|
|
532
604
|
full_model = f"{self._provider}/{model}"
|
|
533
605
|
|
|
534
|
-
# Extract connection info from client if possible
|
|
535
606
|
if hasattr(self._client, "base_url") and self._client.base_url:
|
|
536
607
|
kwargs["api_base"] = str(self._client.base_url)
|
|
537
608
|
if hasattr(self._client, "api_key") and self._client.api_key:
|
|
538
609
|
kwargs["api_key"] = self._client.api_key
|
|
539
610
|
|
|
540
|
-
# Transfer common client configurations if available
|
|
541
611
|
if hasattr(self._client, "timeout") and "timeout" not in kwargs:
|
|
542
612
|
kwargs["timeout"] = self._client.timeout
|
|
543
613
|
|
|
@@ -552,7 +622,6 @@ class TheseusWrapper:
|
|
|
552
622
|
def _compose_system_prompt(self, extra: str | None) -> str:
|
|
553
623
|
kernel_injection = self._kernel.get_system_prompt_injection()
|
|
554
624
|
|
|
555
|
-
# If this kernel contains propagated constraints, add a specific header
|
|
556
625
|
if any(a.id.startswith("propagated_") for a in self._kernel.anchors):
|
|
557
626
|
kernel_injection = (
|
|
558
627
|
"# Mother Agent Constraints\n"
|
|
@@ -568,13 +637,11 @@ class TheseusWrapper:
|
|
|
568
637
|
def propagate_headers(self) -> dict[str, str]:
|
|
569
638
|
"""
|
|
570
639
|
Returns a dictionary of headers to be used when calling sub-agents.
|
|
571
|
-
Includes the current kernel serialized for propagation.
|
|
572
640
|
"""
|
|
573
641
|
import json
|
|
574
642
|
import base64
|
|
575
643
|
|
|
576
644
|
kernel_data = self._kernel.to_dict()
|
|
577
|
-
# Mark as read-only for sub-agents
|
|
578
645
|
kernel_data["is_readonly"] = True
|
|
579
646
|
|
|
580
647
|
encoded_kernel = base64.b64encode(json.dumps(kernel_data).encode()).decode()
|
|
@@ -598,31 +665,26 @@ class TheseusWrapper:
|
|
|
598
665
|
return messages
|
|
599
666
|
|
|
600
667
|
def _extract_content(self, raw: Any) -> str:
|
|
601
|
-
# Handle dict responses
|
|
602
668
|
if isinstance(raw, dict):
|
|
603
669
|
if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
|
|
604
670
|
return raw["choices"][0].get("message", {}).get("content", "") or ""
|
|
605
671
|
if "message" in raw:
|
|
606
|
-
# Ollama dictate format
|
|
607
672
|
return raw["message"].get("content", "") or ""
|
|
608
673
|
return str(raw)
|
|
609
674
|
|
|
610
|
-
# LiteLLM ModelResponse (Standardized for all providers)
|
|
611
675
|
try:
|
|
612
676
|
if hasattr(raw, "choices") and raw.choices:
|
|
613
677
|
choice = raw.choices[0]
|
|
614
678
|
if hasattr(choice, "message") and choice.message:
|
|
615
679
|
return choice.message.content or ""
|
|
616
|
-
# Some older/alternative formats
|
|
617
680
|
if hasattr(choice, "text"):
|
|
618
681
|
return choice.text or ""
|
|
619
682
|
except Exception:
|
|
620
683
|
pass
|
|
621
684
|
|
|
622
|
-
|
|
623
|
-
if hasattr(raw, "content") and isinstance(raw.content, list): # Anthropic style
|
|
685
|
+
if hasattr(raw, "content") and isinstance(raw.content, list):
|
|
624
686
|
return raw.content[0].text if raw.content else ""
|
|
625
|
-
if hasattr(raw, "message"):
|
|
687
|
+
if hasattr(raw, "message"):
|
|
626
688
|
return getattr(raw.message, "content", "") or ""
|
|
627
689
|
|
|
628
690
|
return str(raw)
|
|
@@ -635,7 +697,6 @@ class TheseusWrapper:
|
|
|
635
697
|
return fallback or "unknown"
|
|
636
698
|
|
|
637
699
|
def _run_divergence_engine(self, message: str, response: str, interaction_count: int = 0, skip_l3: bool = False) -> Any:
|
|
638
|
-
"""Runs the staged divergence engine (L1 -> L2 -> L3)."""
|
|
639
700
|
try:
|
|
640
701
|
return self._divergence_engine.analyze(message, response, interaction_count, skip_l3=skip_l3)
|
|
641
702
|
except Exception as e:
|
|
@@ -649,10 +710,6 @@ class TheseusWrapper:
|
|
|
649
710
|
# ── Kernel Management ────────────────────────────────────────────────────────
|
|
650
711
|
|
|
651
712
|
def validate_user_rule(self, rule_text: str) -> None:
|
|
652
|
-
"""
|
|
653
|
-
Validates a user-defined rule against the kernel.
|
|
654
|
-
Raises an exception if there is a conflict.
|
|
655
|
-
"""
|
|
656
713
|
self._kernel.validate_user_rule(rule_text)
|
|
657
714
|
|
|
658
715
|
def endorse_rule(
|
|
@@ -664,15 +721,7 @@ class TheseusWrapper:
|
|
|
664
721
|
) -> Any:
|
|
665
722
|
"""
|
|
666
723
|
Validates the rule; initiates the Reflective Endorsement
|
|
667
|
-
flow if there is a plastic conflict.
|
|
668
|
-
|
|
669
|
-
Args:
|
|
670
|
-
rule_text: The rule to be enforced
|
|
671
|
-
operator_id: The identity of the person granting approval
|
|
672
|
-
approval_channel: Custom approval channel (default: CLI)
|
|
673
|
-
commitment_new_value: New value to assign to the commitment
|
|
674
|
-
|
|
675
|
-
Returns: EndorsementRecord
|
|
724
|
+
flow if there is a plastic conflict.
|
|
676
725
|
"""
|
|
677
726
|
from ct_toolkit.endorsement.reflective import ReflectiveEndorsement
|
|
678
727
|
|
|
@@ -680,6 +729,11 @@ class TheseusWrapper:
|
|
|
680
729
|
kernel=self._kernel,
|
|
681
730
|
provenance_log=self._provenance_log,
|
|
682
731
|
approval_channel=approval_channel,
|
|
732
|
+
staged_manager=self._staged_manager,
|
|
733
|
+
cooldown_base_s=self._config.endorsement_cooldown_base,
|
|
734
|
+
cooldown_max_s=self._config.endorsement_cooldown_max,
|
|
735
|
+
no_probe_penalty_s=self._config.endorsement_no_probe_penalty,
|
|
736
|
+
template=self._config.template,
|
|
683
737
|
)
|
|
684
738
|
return re.validate_and_endorse(
|
|
685
739
|
rule_text=rule_text,
|
|
@@ -688,18 +742,6 @@ class TheseusWrapper:
|
|
|
688
742
|
)
|
|
689
743
|
|
|
690
744
|
def export_provenance_log(self) -> list[dict[str, Any]]:
|
|
691
|
-
"""
|
|
692
|
-
Exports the entire verified provenance log.
|
|
693
|
-
|
|
694
|
-
Verifies the integrity of the log before returning the data.
|
|
695
|
-
This is intended for external auditing and analysis.
|
|
696
|
-
|
|
697
|
-
Returns:
|
|
698
|
-
A list of all log entries as dictionaries.
|
|
699
|
-
|
|
700
|
-
Raises:
|
|
701
|
-
ChainIntegrityError: If the log's hash chain is broken.
|
|
702
|
-
"""
|
|
703
745
|
return self._provenance_log.export_log()
|
|
704
746
|
|
|
705
747
|
@property
|
|
@@ -714,6 +756,10 @@ class TheseusWrapper:
|
|
|
714
756
|
def divergence_engine(self) -> Any:
|
|
715
757
|
return self._divergence_engine
|
|
716
758
|
|
|
759
|
+
@property
|
|
760
|
+
def staged_manager(self) -> StagedUpdateManager:
|
|
761
|
+
return self._staged_manager
|
|
762
|
+
|
|
717
763
|
def __repr__(self) -> str:
|
|
718
764
|
return (
|
|
719
765
|
f"TheseusWrapper("
|
|
@@ -230,6 +230,40 @@ class ICMRunner:
|
|
|
230
230
|
"ollama": "llama3",
|
|
231
231
|
}.get(provider, "gpt-4o-mini")
|
|
232
232
|
|
|
233
|
+
# ── Probe Availability Check ────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def has_probes(
|
|
237
|
+
template: str,
|
|
238
|
+
kernel_name: str, # noqa: ARG004 (reserved for future kernel-specific probes)
|
|
239
|
+
project_root: Path | None = None,
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Returns True if any probe files exist for the given template/kernel combination.
|
|
243
|
+
Does NOT run the probes — intended as a fast pre-flight check for cooldown
|
|
244
|
+
duration calculation in the Reflective Endorsement protocol.
|
|
245
|
+
|
|
246
|
+
Probe availability order (mirrors _load_probes):
|
|
247
|
+
1. base_probes.json — always counts as available
|
|
248
|
+
2. user config: <project_root>/config/<template>_probes.json
|
|
249
|
+
3. built-in domain: endorsement/probes/domain_probes/<template>_probes.json
|
|
250
|
+
"""
|
|
251
|
+
probes_dir = ICMRunner.PROBES_DIR
|
|
252
|
+
|
|
253
|
+
# Base probes are always present; if they exist, we consider probes available.
|
|
254
|
+
if (probes_dir / "base_probes.json").exists():
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# Check user-supplied domain probes
|
|
258
|
+
if project_root:
|
|
259
|
+
user_probe_path = project_root / "config" / f"{template}_probes.json"
|
|
260
|
+
if user_probe_path.exists():
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# Check built-in domain probes
|
|
264
|
+
domain_path = probes_dir / "domain_probes" / f"{template}_probes.json"
|
|
265
|
+
return domain_path.exists()
|
|
266
|
+
|
|
233
267
|
# ── Main Runner ────────────────────────────────────────────────────────
|
|
234
268
|
|
|
235
269
|
def run(self) -> ICMReport:
|