ct-toolkit 0.3.6__tar.gz → 0.3.8__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.6 → ct_toolkit-0.3.8}/PKG-INFO +5 -3
  2. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/README.md +1 -1
  3. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/__init__.py +1 -1
  4. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/cli.py +32 -0
  5. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/exceptions.py +16 -0
  6. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/wrapper.py +158 -109
  7. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/l3_icm.py +53 -16
  8. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/reflective.py +306 -15
  9. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/embedding.py +16 -14
  10. ct_toolkit-0.3.8/ct_toolkit/middleware/litellm.py +344 -0
  11. ct_toolkit-0.3.8/ct_toolkit/server.py +174 -0
  12. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/PKG-INFO +5 -3
  13. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/SOURCES.txt +2 -0
  14. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/requires.txt +3 -1
  15. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/pyproject.toml +4 -2
  16. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/LICENSE +0 -0
  17. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/__main__.py +0 -0
  18. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/__init__.py +0 -0
  19. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/compatibility.py +0 -0
  20. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/compression_guard.py +0 -0
  21. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/integrity.py +0 -0
  22. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/kernel.py +0 -0
  23. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/__init__.py +0 -0
  24. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/analysis.py +0 -0
  25. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/engine.py +0 -0
  26. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/l2_judge.py +0 -0
  27. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/loss.py +0 -0
  28. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/scheduler.py +0 -0
  29. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/__init__.py +0 -0
  30. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/probes/__init__.py +0 -0
  31. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/__init__.py +0 -0
  32. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/__init__.py +0 -0
  33. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/defense.yaml +0 -0
  34. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/finance.yaml +0 -0
  35. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/general.yaml +0 -0
  36. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/medical.yaml +0 -0
  37. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/__init__.py +0 -0
  38. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/default.yaml +0 -0
  39. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/defense.yaml +0 -0
  40. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/__init__.py +0 -0
  41. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/autogen.py +0 -0
  42. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/crewai.py +0 -0
  43. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/deepagents.py +0 -0
  44. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/langchain.py +0 -0
  45. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/__init__.py +0 -0
  46. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/log.py +0 -0
  47. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/vault/__init__.py +0 -0
  48. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/utils/__init__.py +0 -0
  49. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/utils/logger.py +0 -0
  50. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/dependency_links.txt +0 -0
  51. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/entry_points.txt +0 -0
  52. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/top_level.txt +0 -0
  53. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/setup.cfg +0 -0
  54. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_analysis.py +0 -0
  55. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_cli.py +0 -0
  56. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_cli_extended.py +0 -0
  57. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_deep_agents_integration.py +0 -0
  58. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_loss.py +0 -0
  59. {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/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.6
3
+ Version: 0.3.8
4
4
  Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
5
5
  Author: Hakan Damar
6
6
  License: Apache-2.0
@@ -9,7 +9,7 @@ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: instructor>=1.3.0
12
- Requires-Dist: any-llm-sdk[all]>=1.0.0
12
+ Requires-Dist: litellm>=1.40.0
13
13
  Requires-Dist: numpy>=1.26.0
14
14
  Requires-Dist: pyyaml>=6.0
15
15
  Requires-Dist: pydantic>=2.5.0
@@ -20,6 +20,8 @@ Requires-Dist: rich>=13.0.0
20
20
  Requires-Dist: jinja2>=3.1.0
21
21
  Requires-Dist: pyasn1>=0.6.3
22
22
  Requires-Dist: typer[all]>=0.12.0
23
+ Requires-Dist: fastapi>=0.110.0
24
+ Requires-Dist: uvicorn>=0.29.0
23
25
  Provides-Extra: ollama
24
26
  Requires-Dist: ollama>=0.2.0; extra == "ollama"
25
27
  Provides-Extra: langchain
@@ -116,7 +118,7 @@ ct-toolkit list-templates
116
118
 
117
119
  | Metric | Status |
118
120
  | :--------------- | :------------------------------------------------------ |
119
- | **Tests** | ✅ 269/272 passing (92% coverage) |
121
+ | **Tests** | ✅ 281/292 passing (95% coverage) |
120
122
  | **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
121
123
  | **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
122
124
 
@@ -75,7 +75,7 @@ ct-toolkit list-templates
75
75
 
76
76
  | Metric | Status |
77
77
  | :--------------- | :------------------------------------------------------ |
78
- | **Tests** | ✅ 269/272 passing (92% coverage) |
78
+ | **Tests** | ✅ 281/292 passing (95% coverage) |
79
79
  | **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
80
80
  | **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
81
81
 
@@ -23,7 +23,7 @@ from ct_toolkit.core.exceptions import (
23
23
  ChainIntegrityError,
24
24
  )
25
25
 
26
- __version__ = "0.3.6"
26
+ __version__ = "0.3.8"
27
27
 
28
28
  __all__ = [
29
29
  "TheseusWrapper",
@@ -17,6 +17,7 @@ from rich.table import Table
17
17
  from ct_toolkit import TheseusWrapper, WrapperConfig, __version__
18
18
  from ct_toolkit.divergence.l3_icm import ICMRunner
19
19
  from ct_toolkit.core.kernel import ConstitutionalKernel
20
+ from ct_toolkit.server import start_server
20
21
 
21
22
  app = typer.Typer(
22
23
  help="Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems.",
@@ -135,6 +136,37 @@ def audit(
135
136
  if not report.is_healthy:
136
137
  raise typer.Exit(code=1)
137
138
 
139
+ @app.command()
140
+ def serve(
141
+ host: str = typer.Option("127.0.0.1", "--host", help="Host to bind the server to."),
142
+ port: int = typer.Option(8001, "--port", help="Port to bind the server to."),
143
+ kernel: str = typer.Option("general", "--kernel", help="Name of the Constitutional Kernel to use."),
144
+ template: str = typer.Option("general", "--template", help="Name of the Identity Template to use."),
145
+ vault_path: str = typer.Option("./ct_provenance.db", "--vault", help="Path to the provenance log database."),
146
+ judge_provider: str = typer.Option("openai", "--judge-provider", help="LLM provider for the L2 judge."),
147
+ judge_model: Optional[str] = typer.Option(None, "--judge-model", help="Model ID for the L2 judge."),
148
+ ):
149
+ """Start the CT-Toolkit Guardrail Server for LiteLLM integration."""
150
+ console.print(f"Starting Guardrail Server...")
151
+ console.print(f" - Kernel: [bold yellow]{kernel}[/bold yellow]")
152
+ console.print(f" - Template: [bold green]{template}[/bold green]")
153
+ console.print(f" - Bind: [bold cyan]{host}:{port}[/bold cyan]")
154
+
155
+ try:
156
+ config = WrapperConfig(
157
+ kernel_name=kernel,
158
+ template=template,
159
+ vault_path=vault_path,
160
+ project_root=Path.cwd()
161
+ )
162
+ wrapper = TheseusWrapper(provider=judge_provider, config=config)
163
+
164
+ # Start the uvicorn server
165
+ start_server(wrapper=wrapper, host=host, port=port)
166
+ except Exception as e:
167
+ console.print(f"[bold red]Failed to start server:[/bold red] {e}")
168
+ raise typer.Exit(code=1)
169
+
138
170
  @app.command()
139
171
  def list_kernels():
140
172
  """List available Constitutional Kernels."""
@@ -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,25 +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
- import any_llm
29
- from any_llm import AnyLLM
30
- import glob
28
+ from typing import Any, Optional, Callable, Dict, List
29
+
30
+ import litellm
31
31
 
32
32
  from ct_toolkit.core.kernel import ConstitutionalKernel
33
33
  from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
34
- from ct_toolkit.core.exceptions import CTToolkitError, MissingClientError
34
+ from ct_toolkit.core.exceptions import (
35
+ MissingClientError,
36
+ CriticalSandboxDivergenceError
37
+ )
35
38
  from ct_toolkit.core.integrity import IntegrityMonitor
36
39
  from ct_toolkit.divergence.scheduler import RiskProfile
37
40
  from ct_toolkit.core.compression_guard import ContextCompressionGuard
41
+ from ct_toolkit.endorsement.reflective import (
42
+ ReflectiveEndorsement,
43
+ StagedUpdateManager,
44
+ CooldownCalculator
45
+ )
38
46
  from ct_toolkit.utils.logger import get_logger
39
47
 
40
48
  logger = get_logger(__name__)
@@ -76,6 +84,11 @@ class WrapperConfig:
76
84
  compression_threshold: float = 0.85
77
85
  compression_passive_detection: bool = True
78
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
+
79
92
 
80
93
  @dataclass
81
94
  class CTResponse:
@@ -87,6 +100,8 @@ class CTResponse:
87
100
  divergence_tier: str | None = None # "ok" | "l1_warning" | "l2_judge" | "l3_icm"
88
101
  provenance_id: str | None = None
89
102
  raw_response: Any = field(default=None, repr=False)
103
+ # Sandbox metadata if staged updates are active
104
+ sandbox_divergence: float | None = None
90
105
 
91
106
  def __str__(self) -> str:
92
107
  return self.content
@@ -97,7 +112,7 @@ class TheseusWrapper:
97
112
  Identity-continuity proxy wrapping the LLM API client.
98
113
 
99
114
  Supported providers: openai (default), anthropic, ollama, google, etc.
100
- (Any provider supported by any-llm-sdk)
115
+ (Any provider supported by LiteLLM)
101
116
 
102
117
  Usage:
103
118
  from ct_toolkit import TheseusWrapper
@@ -127,15 +142,6 @@ class TheseusWrapper:
127
142
  ) -> None:
128
143
  """
129
144
  Initializes TheseusWrapper.
130
-
131
- Args:
132
- client: An existing API client instance (OpenAI, Anthropic, etc.).
133
- Optional if 'provider' is specified.
134
- config: Optional WrapperConfig instance.
135
- provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
136
- If provided, 'any-llm' will be used to initialize the client internally.
137
- project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
138
- Defaults to the current working directory.
139
145
  """
140
146
  self._config = config or WrapperConfig(
141
147
  kernel_path=kernel_path,
@@ -177,6 +183,9 @@ class TheseusWrapper:
177
183
  )
178
184
  self._shadow_history: Optional[List[Dict[str, str]]] = None
179
185
 
186
+ # ── Staged Approval (Cooldown) ──
187
+ self._staged_manager = StagedUpdateManager()
188
+
180
189
  # ── Integrity Monitoring ──
181
190
  self._integrity_monitor = IntegrityMonitor()
182
191
  self._register_monitored_files()
@@ -185,8 +194,7 @@ class TheseusWrapper:
185
194
 
186
195
  def _register_monitored_files(self):
187
196
  """
188
- Finds and registers all critical configuration files for integrity monitoring,
189
- including both built-in package files and user-provided custom configs.
197
+ Finds and registers all critical configuration files for integrity monitoring.
190
198
  """
191
199
  logger.debug("Registering configuration files for integrity monitoring.")
192
200
 
@@ -207,10 +215,7 @@ class TheseusWrapper:
207
215
  self._integrity_monitor.register_file(file_path)
208
216
 
209
217
  except Exception:
210
- # Fallback for older Python or different environments
211
218
  package_root = Path(__file__).parent.parent
212
- # This is less ideal as it might find non-package files, but it's a
213
- # reasonable fallback for development environments.
214
219
  for pattern in internal_patterns:
215
220
  for file_path in package_root.rglob(pattern):
216
221
  if file_path.is_file():
@@ -231,23 +236,16 @@ class TheseusWrapper:
231
236
  # ── Factory / Init ─────────────────────────────────────────────────────────
232
237
 
233
238
  def _detect_provider(self, client: Any) -> str:
234
- """
235
- Detects the provider from the client instance or string.
236
- Returns 'unknown' if detection fails.
237
- """
238
239
  if client is None:
239
240
  return "unknown"
240
241
 
241
242
  if isinstance(client, str):
242
- # client passed as a string (e.g. "openai")
243
243
  return client.lower()
244
244
 
245
245
  try:
246
- # Check for any_llm wrapper
247
246
  if hasattr(client, "provider") and isinstance(client.provider, str):
248
247
  return client.provider
249
248
 
250
- # Check module name
251
249
  module = type(client).__module__.lower()
252
250
  if "openai" in module: return "openai"
253
251
  if "anthropic" in module: return "anthropic"
@@ -260,21 +258,18 @@ class TheseusWrapper:
260
258
  return "unknown"
261
259
 
262
260
  def _load_kernel(self) -> ConstitutionalKernel:
263
- # 1. Check for explicit path
264
261
  if self._config.kernel_path:
265
262
  logger.debug(f"Loading kernel from explicit path: {self._config.kernel_path}")
266
263
  return ConstitutionalKernel.from_yaml(self._config.kernel_path)
267
264
 
268
265
  safe_name = os.path.basename(self._config.kernel_name)
269
266
 
270
- # 2. Check for user-defined config directory
271
267
  if self._project_root:
272
268
  user_config_path = self._project_root / "config" / f"{safe_name}.yaml"
273
269
  if user_config_path.exists():
274
270
  logger.debug(f"Loading kernel from user config: {user_config_path}")
275
271
  return ConstitutionalKernel.from_yaml(user_config_path)
276
272
 
277
- # 3. Load built-in kernel using importlib.resources
278
273
  try:
279
274
  from importlib.resources import files
280
275
  kernel_resource = files("ct_toolkit.kernels").joinpath(f"{safe_name}.yaml")
@@ -282,7 +277,6 @@ class TheseusWrapper:
282
277
  logger.debug(f"Loading built-in kernel: {safe_name}.yaml")
283
278
  return ConstitutionalKernel.from_yaml(kernel_resource)
284
279
  except (ImportError, FileNotFoundError):
285
- # Fallback for older python versions
286
280
  kernel_path = (
287
281
  Path(__file__).parent.parent
288
282
  / "kernels"
@@ -298,7 +292,6 @@ class TheseusWrapper:
298
292
  return ConstitutionalKernel.default()
299
293
 
300
294
  def _init_provenance_log(self) -> Any:
301
- """Initializes the Provenance Log vault adapter (Will be fully implemented in Step 2)."""
302
295
  from ct_toolkit.provenance.log import ProvenanceLog
303
296
  return ProvenanceLog(
304
297
  vault_type=self._config.vault_type,
@@ -306,13 +299,10 @@ class TheseusWrapper:
306
299
  )
307
300
 
308
301
  def _init_identity_layer(self) -> Any:
309
- """Initializes the Identity Embedding Layer."""
310
302
  from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
311
303
 
312
- # Guard against using raw clients that don't support embeddings
313
304
  emb_client = self._config.embedding_client
314
305
  if emb_client is None:
315
- # Only default to main client if it's known to be OpenAI (standard for embeddings)
316
306
  if self._provider == "openai":
317
307
  emb_client = self._client
318
308
  else:
@@ -327,7 +317,6 @@ class TheseusWrapper:
327
317
  )
328
318
 
329
319
  def _init_divergence_engine(self) -> Any:
330
- """Initializes the Divergence Engine (judge_client is required for L2/L3)."""
331
320
  from ct_toolkit.divergence.engine import DivergenceEngine
332
321
  from ct_toolkit.divergence.scheduler import ElasticityScheduler
333
322
 
@@ -385,15 +374,10 @@ class TheseusWrapper:
385
374
  ) -> CTResponse:
386
375
  """
387
376
  Sends a single message, returning the response with identity protection.
388
-
389
- Args:
390
- message: User message
391
- model: Model to use (provider default is used)
392
- system: Additional system prompt (appended to kernel)
393
- history: Conversation history [{"role": ..., "content": ...}]
394
377
  """
395
378
  # --- Pre-flight Checks ---
396
379
  self._integrity_monitor.verify_integrity()
380
+ self._process_staged_updates()
397
381
 
398
382
  composed_system = self._compose_system_prompt(system)
399
383
  messages = self._build_messages(message, history, composed_system)
@@ -451,6 +435,11 @@ class TheseusWrapper:
451
435
 
452
436
  break
453
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
+
454
443
  # Provenance Log record
455
444
  provenance_id = None
456
445
  if self._config.log_requests:
@@ -466,6 +455,7 @@ class TheseusWrapper:
466
455
  "template": self._config.template,
467
456
  "kernel": self._kernel.name,
468
457
  "drift_alert_enabled": self._config.drift_alert_callback is not None,
458
+ "sandbox_divergence": sandbox_divergence,
469
459
  },
470
460
  )
471
461
 
@@ -477,8 +467,99 @@ class TheseusWrapper:
477
467
  divergence_tier=div_result.tier.value,
478
468
  provenance_id=provenance_id,
479
469
  raw_response=raw_response,
470
+ sandbox_divergence=sandbox_divergence,
480
471
  )
481
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
+
482
563
  # ── Provider Dispatch ──────────────────────────────────────────────────────
483
564
 
484
565
  def _has_env_credentials(self) -> bool:
@@ -489,7 +570,6 @@ class TheseusWrapper:
489
570
  if self._provider == "anthropic":
490
571
  return bool(os.environ.get("ANTHROPIC_API_KEY"))
491
572
  if self._provider == "ollama":
492
- # Ollama typically doesn't need a key locally
493
573
  return True
494
574
  if self._provider == "google":
495
575
  return bool(os.environ.get("GOOGLE_API_KEY"))
@@ -501,55 +581,40 @@ class TheseusWrapper:
501
581
  model: str | None,
502
582
  **kwargs: Any,
503
583
  ) -> Any:
504
- """Uses any_llm for unified provider calling."""
505
- # --- Passive Context Compression Detection ---
584
+ """Uses LiteLLM for unified provider calling."""
506
585
  if self._config.compression_passive_detection and self._shadow_history:
507
- # Simple heuristic: message count dropped significantly
508
- # We use 0.7 as a threshold (30% drop)
509
586
  if len(messages) < len(self._shadow_history) * 0.7:
510
587
  self._compression_guard.on_passive_detection(
511
588
  original=self._shadow_history,
512
589
  compressed=messages
513
590
  )
514
591
 
515
- # Update shadow history for next call
516
592
  self._shadow_history = list(messages)
517
- # Convert messages to any_llm format if necessary
518
- # any_llm.completion handles different formats internally
519
593
 
520
- # Set default models per provider if not specified
521
594
  if not model:
522
595
  if self._provider == "openai": model = "gpt-4o-mini"
523
596
  elif self._provider == "anthropic": model = "claude-3-5-sonnet-latest"
524
597
  elif self._provider == "ollama": model = "llama3"
525
598
  else: model = "gpt-4o-mini"
526
599
 
527
- # any_llm expects "provider:model" format
528
600
  full_model = model
529
- if ":" not in model:
530
- full_model = f"{self._provider}:{model}"
531
-
532
- # Prepare any_llm call
533
- any_llm_kwargs = {
534
- "model": full_model,
535
- "messages": messages,
536
- **kwargs
537
- }
601
+ if ":" in model:
602
+ full_model = model.replace(":", "/", 1)
603
+ elif self._provider not in ("openai", "unknown"):
604
+ full_model = f"{self._provider}/{model}"
538
605
 
539
- # Extract connection info from client if possible
540
606
  if hasattr(self._client, "base_url") and self._client.base_url:
541
- any_llm_kwargs["api_base"] = str(self._client.base_url)
607
+ kwargs["api_base"] = str(self._client.base_url)
542
608
  if hasattr(self._client, "api_key") and self._client.api_key:
543
- any_llm_kwargs["api_key"] = self._client.api_key
609
+ kwargs["api_key"] = self._client.api_key
544
610
 
545
- # Transfer common client configurations if available
546
- if hasattr(self._client, "timeout") and "timeout" not in any_llm_kwargs:
547
- any_llm_kwargs["timeout"] = self._client.timeout
611
+ if hasattr(self._client, "timeout") and "timeout" not in kwargs:
612
+ kwargs["timeout"] = self._client.timeout
548
613
 
549
614
  try:
550
- return any_llm.completion(**any_llm_kwargs)
615
+ return litellm.completion(model=full_model, messages=messages, **kwargs)
551
616
  except Exception as e:
552
- logger.error(f"any_llm call failed: {e}")
617
+ logger.error(f"litellm call failed: {e}")
553
618
  raise
554
619
 
555
620
  # ── Helper Methods ──────────────────────────────────────────────────────
@@ -557,7 +622,6 @@ class TheseusWrapper:
557
622
  def _compose_system_prompt(self, extra: str | None) -> str:
558
623
  kernel_injection = self._kernel.get_system_prompt_injection()
559
624
 
560
- # If this kernel contains propagated constraints, add a specific header
561
625
  if any(a.id.startswith("propagated_") for a in self._kernel.anchors):
562
626
  kernel_injection = (
563
627
  "# Mother Agent Constraints\n"
@@ -573,13 +637,11 @@ class TheseusWrapper:
573
637
  def propagate_headers(self) -> dict[str, str]:
574
638
  """
575
639
  Returns a dictionary of headers to be used when calling sub-agents.
576
- Includes the current kernel serialized for propagation.
577
640
  """
578
641
  import json
579
642
  import base64
580
643
 
581
644
  kernel_data = self._kernel.to_dict()
582
- # Mark as read-only for sub-agents
583
645
  kernel_data["is_readonly"] = True
584
646
 
585
647
  encoded_kernel = base64.b64encode(json.dumps(kernel_data).encode()).decode()
@@ -603,25 +665,28 @@ class TheseusWrapper:
603
665
  return messages
604
666
 
605
667
  def _extract_content(self, raw: Any) -> str:
606
- # Handle dict responses (e.g. from some mockers or raw APIs)
607
668
  if isinstance(raw, dict):
608
669
  if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
609
- # OpenAI dict format
610
670
  return raw["choices"][0].get("message", {}).get("content", "") or ""
611
671
  if "message" in raw:
612
- # Ollama dict format
613
672
  return raw["message"].get("content", "") or ""
614
673
  return str(raw)
615
674
 
616
- # OpenAI
617
- if hasattr(raw, "choices"):
618
- return raw.choices[0].message.content or ""
619
- # Anthropic
675
+ try:
676
+ if hasattr(raw, "choices") and raw.choices:
677
+ choice = raw.choices[0]
678
+ if hasattr(choice, "message") and choice.message:
679
+ return choice.message.content or ""
680
+ if hasattr(choice, "text"):
681
+ return choice.text or ""
682
+ except Exception:
683
+ pass
684
+
620
685
  if hasattr(raw, "content") and isinstance(raw.content, list):
621
- return raw.content[0].text if raw.content else ""
622
- # Ollama
686
+ return raw.content[0].text if raw.content else ""
623
687
  if hasattr(raw, "message"):
624
- return raw.message.content or ""
688
+ return getattr(raw.message, "content", "") or ""
689
+
625
690
  return str(raw)
626
691
 
627
692
  def _extract_model(self, raw: Any, fallback: str | None) -> str:
@@ -632,7 +697,6 @@ class TheseusWrapper:
632
697
  return fallback or "unknown"
633
698
 
634
699
  def _run_divergence_engine(self, message: str, response: str, interaction_count: int = 0, skip_l3: bool = False) -> Any:
635
- """Runs the staged divergence engine (L1 -> L2 -> L3)."""
636
700
  try:
637
701
  return self._divergence_engine.analyze(message, response, interaction_count, skip_l3=skip_l3)
638
702
  except Exception as e:
@@ -646,10 +710,6 @@ class TheseusWrapper:
646
710
  # ── Kernel Management ────────────────────────────────────────────────────────
647
711
 
648
712
  def validate_user_rule(self, rule_text: str) -> None:
649
- """
650
- Validates a user-defined rule against the kernel.
651
- Raises an exception if there is a conflict.
652
- """
653
713
  self._kernel.validate_user_rule(rule_text)
654
714
 
655
715
  def endorse_rule(
@@ -661,15 +721,7 @@ class TheseusWrapper:
661
721
  ) -> Any:
662
722
  """
663
723
  Validates the rule; initiates the Reflective Endorsement
664
- flow if there is a plastic conflict. Throws hard reject on axiomatic violation.
665
-
666
- Args:
667
- rule_text: The rule to be enforced
668
- operator_id: The identity of the person granting approval
669
- approval_channel: Custom approval channel (default: CLI)
670
- commitment_new_value: New value to assign to the commitment
671
-
672
- Returns: EndorsementRecord
724
+ flow if there is a plastic conflict.
673
725
  """
674
726
  from ct_toolkit.endorsement.reflective import ReflectiveEndorsement
675
727
 
@@ -677,6 +729,11 @@ class TheseusWrapper:
677
729
  kernel=self._kernel,
678
730
  provenance_log=self._provenance_log,
679
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,
680
737
  )
681
738
  return re.validate_and_endorse(
682
739
  rule_text=rule_text,
@@ -685,18 +742,6 @@ class TheseusWrapper:
685
742
  )
686
743
 
687
744
  def export_provenance_log(self) -> list[dict[str, Any]]:
688
- """
689
- Exports the entire verified provenance log.
690
-
691
- Verifies the integrity of the log before returning the data.
692
- This is intended for external auditing and analysis.
693
-
694
- Returns:
695
- A list of all log entries as dictionaries.
696
-
697
- Raises:
698
- ChainIntegrityError: If the log's hash chain is broken.
699
- """
700
745
  return self._provenance_log.export_log()
701
746
 
702
747
  @property
@@ -711,6 +756,10 @@ class TheseusWrapper:
711
756
  def divergence_engine(self) -> Any:
712
757
  return self._divergence_engine
713
758
 
759
+ @property
760
+ def staged_manager(self) -> StagedUpdateManager:
761
+ return self._staged_manager
762
+
714
763
  def __repr__(self) -> str:
715
764
  return (
716
765
  f"TheseusWrapper("