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.
Files changed (59) hide show
  1. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/PKG-INFO +5 -4
  2. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/README.md +4 -3
  3. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/__init__.py +1 -1
  4. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/exceptions.py +16 -0
  5. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/wrapper.py +134 -88
  6. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/l3_icm.py +34 -0
  7. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/reflective.py +306 -15
  8. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/PKG-INFO +5 -4
  9. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/pyproject.toml +1 -1
  10. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/LICENSE +0 -0
  11. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/__main__.py +0 -0
  12. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/cli.py +0 -0
  13. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/__init__.py +0 -0
  14. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/compatibility.py +0 -0
  15. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/compression_guard.py +0 -0
  16. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/integrity.py +0 -0
  17. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/core/kernel.py +0 -0
  18. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/__init__.py +0 -0
  19. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/analysis.py +0 -0
  20. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/engine.py +0 -0
  21. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/l2_judge.py +0 -0
  22. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/loss.py +0 -0
  23. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/divergence/scheduler.py +0 -0
  24. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/__init__.py +0 -0
  25. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/endorsement/probes/__init__.py +0 -0
  26. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/__init__.py +0 -0
  27. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/embedding.py +0 -0
  28. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/__init__.py +0 -0
  29. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/defense.yaml +0 -0
  30. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/finance.yaml +0 -0
  31. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/general.yaml +0 -0
  32. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/identity/templates/medical.yaml +0 -0
  33. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/__init__.py +0 -0
  34. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/default.yaml +0 -0
  35. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/kernels/defense.yaml +0 -0
  36. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/__init__.py +0 -0
  37. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/autogen.py +0 -0
  38. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/crewai.py +0 -0
  39. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/deepagents.py +0 -0
  40. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/langchain.py +0 -0
  41. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/middleware/litellm.py +0 -0
  42. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/__init__.py +0 -0
  43. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/log.py +0 -0
  44. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/provenance/vault/__init__.py +0 -0
  45. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/server.py +0 -0
  46. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/utils/__init__.py +0 -0
  47. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit/utils/logger.py +0 -0
  48. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/SOURCES.txt +0 -0
  49. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/dependency_links.txt +0 -0
  50. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/entry_points.txt +0 -0
  51. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/requires.txt +0 -0
  52. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/ct_toolkit.egg-info/top_level.txt +0 -0
  53. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/setup.cfg +0 -0
  54. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_analysis.py +0 -0
  55. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_cli.py +0 -0
  56. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_cli_extended.py +0 -0
  57. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_deep_agents_integration.py +0 -0
  58. {ct_toolkit-0.3.7 → ct_toolkit-0.3.9}/tests/test_loss.py +0 -0
  59. {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.7
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
- - **Passive Context Compression Detection**: Automatically detects silent provider-side history compression (e.g., OpenAI/Anthropic) via history shrinkage heuristics.
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** | ✅ 281/292 passing (95% coverage) |
122
- | **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
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
- - **Passive Context Compression Detection**: Automatically detects silent provider-side history compression (e.g., OpenAI/Anthropic) via history shrinkage heuristics.
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** | ✅ 281/292 passing (95% coverage) |
79
- | **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
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).
@@ -23,7 +23,7 @@ from ct_toolkit.core.exceptions import (
23
23
  ChainIntegrityError,
24
24
  )
25
25
 
26
- __version__ = "0.3.7"
26
+ __version__ = "0.3.9"
27
27
 
28
28
  __all__ = [
29
29
  "TheseusWrapper",
@@ -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, Iterator, Optional, Callable, Dict
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 CTToolkitError, MissingClientError
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
- # Fallback raw extraction for edge cases
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"): # Ollama generic style
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. Throws hard reject on axiomatic violation.
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: