devtorch-core 3.0.1__py3-none-any.whl

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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,562 @@
1
+ """
2
+ devtorch_core.wrapper.base
3
+ ==========================
4
+ Core infrastructure shared by all LLM SDK wrappers:
5
+
6
+ - RACPInjector — builds the RACP system-prompt prefix for every LLM call.
7
+ - CaptureOrchestrator — fires devtorch commit / sensitivity primitives after
8
+ an LLM response is received.
9
+ - Helpers: is_disabled(), find_gcc_repo().
10
+
11
+ All code must degrade gracefully. If .GCC/ is missing, if devtorch_core
12
+ methods fail, or if parser imports fail, a warning is logged and the caller
13
+ never sees an exception.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ import time
21
+ import uuid
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ logger = logging.getLogger("devtorch.wrapper")
26
+
27
+ DEVTORCH_DISABLE_ENV = "DEVTORCH_DISABLE"
28
+ CONTEXT_CACHE_TTL_SECS = 60
29
+
30
+
31
+ def _reasoning_text(thinking_blocks: list) -> str:
32
+ """Extract text from a list of ThinkingBlock objects."""
33
+ if not thinking_blocks:
34
+ return ""
35
+ return "\n\n".join(getattr(tb, "text", str(tb)) for tb in thinking_blocks)
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # RACP prefix builder
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class RACPInjector:
43
+ """Builds the injected prefix for every LLM call."""
44
+
45
+ def __init__(self, gcc_repo: Any) -> None:
46
+ self._repo = gcc_repo
47
+ self._cache: dict = {} # keys: "bundle" (dict), "ts" (float)
48
+ self._augmenter: Any | None = None
49
+
50
+ # ------------------------------------------------------------------
51
+ # Public
52
+ # ------------------------------------------------------------------
53
+
54
+ def build_system_prefix(self, prompt_text: str | None = None) -> str:
55
+ """
56
+ Return a string to prepend to the system prompt composed of:
57
+ 1. RACP system prompt from .GCC/prompts/ (if set).
58
+ 2. Θ summary — top-10 concepts by mean_confidence.
59
+ 3. Context bundle summary — artifact names and reasons.
60
+ 4. Reasoning Plus smart context + reasoning directive (if enabled).
61
+
62
+ Returns an empty string on any error so callers can safely
63
+ concatenate without conditional checks.
64
+ """
65
+ prefix, _ = self.build_system_prefix_with_provenance(
66
+ prompt_text=prompt_text,
67
+ )
68
+ return prefix
69
+
70
+ def build_system_prefix_with_provenance(
71
+ self,
72
+ prompt_text: str | None = None,
73
+ *,
74
+ session_id: str = "",
75
+ call_id: str = "",
76
+ model_name: str = "",
77
+ ) -> tuple[str, list[str]]:
78
+ """
79
+ Build the system prefix and also inject relevant learnings.
80
+
81
+ Returns (prefix, injected_learning_ids). When *call_id* is
82
+ provided, a provenance record is written so that the observed
83
+ outcome can later be attributed to the injected learnings.
84
+
85
+ Architecture: pure RACP builds the base prefix first (system prompt
86
+ + Theta + context bundle). Reasoning Plus decorates that base
87
+ with smart context, reasoning directive, and DRPL learnings when
88
+ enabled. The augmentation never replaces RACP — it wraps it.
89
+ """
90
+ # ---- Layer 1: pure RACP (always runs, is the protocol) ----------
91
+ base_parts = self._build_racp_parts()
92
+
93
+ learning_ids: list[str] = []
94
+
95
+ # ---- Layer 2: Reasoning Plus decorates RACP (optional) ----------
96
+ if prompt_text and not is_disabled():
97
+ try:
98
+ augmenter = self._get_reasoning_plus_augmenter()
99
+ if augmenter and augmenter.should_augment():
100
+ augmented, learning_ids = augmenter.build_system_prompt_with_learning_context(
101
+ base_parts,
102
+ prompt_text_for_context=prompt_text,
103
+ call_id=call_id,
104
+ session_id=session_id,
105
+ model_name=model_name,
106
+ )
107
+ return augmented, learning_ids
108
+ except Exception as exc:
109
+ logger.warning("devtorch: Reasoning Plus injection failed — %s", exc)
110
+
111
+ return base_parts, learning_ids
112
+
113
+ def invalidate_cache(self) -> None:
114
+ """Invalidate context bundle cache and reasoning-plus smart context cache."""
115
+ self._cache = {}
116
+ if self._augmenter is not None:
117
+ try:
118
+ self._augmenter.invalidate_cache()
119
+ except Exception:
120
+ pass
121
+
122
+ # ------------------------------------------------------------------
123
+ # Private
124
+ # ------------------------------------------------------------------
125
+
126
+ def _build_racp_parts(self) -> str:
127
+ """Build the classic RACP system-prefix (prompt + Θ + context bundle)."""
128
+ parts: list[str] = []
129
+
130
+ # 1. RACP system prompt artifact
131
+ try:
132
+ racp_prompt = self._repo.prompt_get()
133
+ if racp_prompt:
134
+ parts.append(racp_prompt.strip())
135
+ except Exception as exc:
136
+ logger.warning("devtorch: prompt_get failed — %s", exc)
137
+
138
+ # 2. Θ summary
139
+ try:
140
+ theta_text = self._theta_summary()
141
+ if theta_text:
142
+ parts.append(theta_text)
143
+ except Exception as exc:
144
+ logger.warning("devtorch: theta_summary failed — %s", exc)
145
+
146
+ # 3. Context bundle summary
147
+ try:
148
+ bundle = self._get_context_bundle()
149
+ artifacts = bundle.get("artifacts", [])
150
+ if artifacts:
151
+ lines = ["[DevTorch context bundle]"]
152
+ for art in artifacts:
153
+ path = art.get("path", "?")
154
+ reason = art.get("reason", "")
155
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
156
+ parts.append("\n".join(lines))
157
+ except Exception as exc:
158
+ logger.warning("devtorch: context_bundle summary failed — %s", exc)
159
+
160
+ return "\n\n".join(parts)
161
+
162
+ # ------------------------------------------------------------------
163
+ # Private helpers
164
+ # ------------------------------------------------------------------
165
+
166
+ def _get_context_bundle(self) -> dict:
167
+ """Return cached context bundle (CONTEXT_CACHE_TTL_SECS TTL)."""
168
+ now = time.monotonic()
169
+ if self._cache and (now - self._cache.get("ts", 0)) < CONTEXT_CACHE_TTL_SECS:
170
+ return self._cache["bundle"]
171
+
172
+ # Respect an override set by the caller (e.g. the benchmark runner).
173
+ k_tokens = getattr(self._repo, "_context_k_override", None) or 8000
174
+ bundle = self._repo.context_bundle(
175
+ k_tokens=k_tokens,
176
+ policy={"sensitivity_policy": "default"},
177
+ )
178
+ self._cache = {"bundle": bundle, "ts": now}
179
+ return bundle
180
+
181
+ def _theta_summary(self) -> str:
182
+ """Format top-10 Θ entries as compact text (sorted by mean_confidence desc)."""
183
+ theta = self._repo.get_theta()
184
+ cv = theta.get("coordination_vector", {})
185
+ if not cv:
186
+ return ""
187
+
188
+ # Sort by mean_confidence descending; take top 10
189
+ def _mean_conf(entry: Any) -> float:
190
+ if isinstance(entry, dict):
191
+ return float(entry.get("mean_confidence", 0.0))
192
+ return 0.0
193
+
194
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
195
+ if not top:
196
+ return ""
197
+
198
+ lines = ["[DevTorch Θ — top concepts]"]
199
+ for concept, data in top:
200
+ conf = _mean_conf(data)
201
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
202
+ return "\n".join(lines)
203
+
204
+ def _get_reasoning_plus_augmenter(self) -> Any | None:
205
+ """Lazy-initialize the Reasoning Plus augmenter for this repo."""
206
+ if self._augmenter is None and self._repo is not None:
207
+ try:
208
+ from devtorch_core.reasoning_plus.augmenter import ReasoningPlusAugmenter
209
+
210
+ repo_path = self._repo.gcc_dir.parent
211
+ self._augmenter = ReasoningPlusAugmenter(repo_path, gcc_repo=self._repo)
212
+ except Exception as exc:
213
+ logger.warning("devtorch: failed to load Reasoning Plus augmenter — %s", exc)
214
+ self._augmenter = None
215
+ return self._augmenter
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Capture orchestrator
220
+ # ---------------------------------------------------------------------------
221
+
222
+ class CaptureOrchestrator:
223
+ """Fires devtorch primitives after an LLM response is received."""
224
+
225
+ def __init__(self, gcc_repo: Any) -> None:
226
+ self._repo = gcc_repo
227
+ self._learning: Any | None = None
228
+ self._bundle_tokens_cache: Optional[int] = None
229
+
230
+ # ------------------------------------------------------------------
231
+ # Public
232
+ # ------------------------------------------------------------------
233
+
234
+ def record_learning_call(
235
+ self,
236
+ *,
237
+ call_type: str,
238
+ reasoning: str,
239
+ session_id: str = "",
240
+ name: str = "",
241
+ input: str = "",
242
+ output: str = "",
243
+ outcome: str = "unknown",
244
+ sensitivity: str = "PUBLIC",
245
+ call_id: str = "",
246
+ concepts: list[str] | None = None,
247
+ ) -> None:
248
+ """
249
+ Record a reasoning call in the Reasoning Plus Learning (DRPL) layer.
250
+
251
+ This is the hook for automatic outcome detection: wrappers can call it
252
+ with ``outcome="success"`` or ``outcome="failure"`` after an LLM/tool
253
+ call completes.
254
+
255
+ If a *call_id* is provided, it is reused for the call record and any
256
+ provenance feedback for learnings that were injected into this call.
257
+ The *concepts* parameter tags the call with RACP concepts so that
258
+ extracted learnings carry semantic context for relevance scoring.
259
+ """
260
+ if not self._repo:
261
+ return
262
+ try:
263
+ if self._learning is None:
264
+ from devtorch_core.reasoning_plus.learning import ReasoningPlusLearning
265
+ from devtorch_core.reasoning_plus.config import SettingsStore
266
+
267
+ config, _ = SettingsStore(self._repo.gcc_dir).load()
268
+ self._learning = ReasoningPlusLearning(
269
+ gcc_dir=self._repo.gcc_dir,
270
+ repo_path=self._repo.gcc_dir.parent,
271
+ config=config,
272
+ )
273
+
274
+ recorded_id = self._learning.record_call(
275
+ call_type=call_type,
276
+ reasoning=reasoning,
277
+ session_id=session_id,
278
+ name=name,
279
+ input=input,
280
+ output=output,
281
+ outcome=outcome,
282
+ sensitivity=sensitivity,
283
+ call_id=call_id or None,
284
+ concepts=concepts,
285
+ )
286
+ self._learning.extract_learnings(recorded_id)
287
+ if call_id and outcome in ("success", "failure", "stale"):
288
+ self._learning.record_injection_outcome(call_id, outcome)
289
+ except Exception as exc:
290
+ logger.warning("devtorch: learning call recording failed — %s", exc)
291
+
292
+ def capture(
293
+ self,
294
+ response_text: str,
295
+ thinking_blocks: list,
296
+ session_id: str,
297
+ *,
298
+ prompt_text: str = "",
299
+ prompt_tokens: int = 0,
300
+ completion_tokens: int = 0,
301
+ bundle_tokens: int = 0,
302
+ ) -> None:
303
+ """
304
+ Parse *response_text* and fire devtorch primitives:
305
+
306
+ 1. If thinking blocks present → commit with conf=1.0.
307
+ 2. Extract RACP blocks from text → commit + sensitivity_add per block.
308
+ 3. If nothing found → infer_sensitivity + extract_decision_summary
309
+ → commit with conf=0.5.
310
+ 4. Write session metrics (tokens, developer_id) for this session.
311
+
312
+ All errors are caught and logged — never raised.
313
+ """
314
+ try:
315
+ self._do_capture(response_text, thinking_blocks, session_id)
316
+ except Exception as exc:
317
+ logger.warning("devtorch: capture error (session=%s) — %s", session_id, exc)
318
+
319
+ try:
320
+ self._write_session_metrics(
321
+ session_id,
322
+ prompt_tokens=prompt_tokens,
323
+ completion_tokens=completion_tokens,
324
+ bundle_tokens=bundle_tokens,
325
+ )
326
+ except Exception as exc:
327
+ logger.warning("devtorch: session metrics write failed (session=%s) — %s", session_id, exc)
328
+
329
+ # ------------------------------------------------------------------
330
+ # Internal capture pipeline
331
+ # ------------------------------------------------------------------
332
+
333
+ def _do_capture(
334
+ self,
335
+ response_text: str,
336
+ thinking_blocks: list,
337
+ session_id: str,
338
+ ) -> None:
339
+ captured_something = False
340
+
341
+ # --- 1. Native thinking blocks → commit at conf=1.0 ---
342
+ if thinking_blocks:
343
+ try:
344
+ from devtorch_core.parser.thinking import thinking_to_commit_message
345
+ msg = thinking_to_commit_message(thinking_blocks)
346
+ self._do_commit(f"[session={session_id}] {msg}")
347
+ captured_something = True
348
+ except Exception as exc:
349
+ logger.warning("devtorch: thinking capture failed — %s", exc)
350
+
351
+ # --- 2. RACP structured blocks (primary protocol) ---
352
+ commits: list = []
353
+ sensitivities: list = []
354
+ try:
355
+ from devtorch_core.parser.blocks import extract_racp_blocks
356
+ commits, sensitivities = extract_racp_blocks(response_text)
357
+ except Exception as exc:
358
+ logger.warning("devtorch: extract_racp_blocks failed — %s", exc)
359
+
360
+ for cb in commits:
361
+ self._do_commit(cb.message, concepts=cb.concepts)
362
+ captured_something = True
363
+
364
+ for sb in sensitivities:
365
+ self._do_sensitivity(
366
+ concept=sb.concept,
367
+ signal=sb.signal,
368
+ confidence=sb.confidence,
369
+ disclosure=sb.disclosure,
370
+ )
371
+ captured_something = True
372
+
373
+ # --- 3. Inline <thinking> blocks (Reasoning Plus augmentation) ---
374
+ try:
375
+ from devtorch_core.reasoning_plus.capture import (
376
+ commit_reasoning,
377
+ extract_inline_thinking,
378
+ )
379
+
380
+ inline_thinking = extract_inline_thinking(response_text)
381
+ if inline_thinking:
382
+ combined = "\n\n".join(inline_thinking)
383
+ commit_reasoning(self._repo, combined, session_id)
384
+ captured_something = True
385
+ except Exception as exc:
386
+ logger.warning("devtorch: inline thinking capture failed — %s", exc)
387
+
388
+ # --- 4. Fallback: infer + decision summary ---
389
+ if not captured_something and response_text:
390
+ summary = ""
391
+ try:
392
+ from devtorch_core.parser.inference import extract_decision_summary
393
+ summary = extract_decision_summary(response_text)
394
+ except Exception:
395
+ # inference module may not exist yet
396
+ summary = response_text[:200]
397
+
398
+ try:
399
+ from devtorch_core.parser.inference import infer_sensitivity
400
+ inferred = infer_sensitivity(response_text)
401
+ for item in inferred:
402
+ concept = getattr(item, "concept", str(item))
403
+ signal = getattr(item, "signal", "inferred")
404
+ confidence = float(getattr(item, "confidence", 0.5))
405
+ disclosure = getattr(item, "disclosure", "PROTECTED")
406
+ self._do_sensitivity(concept, signal, confidence, disclosure)
407
+ except Exception as exc:
408
+ logger.warning("devtorch: infer_sensitivity failed — %s", exc)
409
+
410
+ commit_msg = (
411
+ f"[session={session_id}] auto(conf=0.5): {summary[:180]}"
412
+ if summary
413
+ else f"[session={session_id}] auto(conf=0.5): response captured"
414
+ )
415
+ self._do_commit(commit_msg)
416
+
417
+ # ------------------------------------------------------------------
418
+ # Primitives — each catches its own errors
419
+ # ------------------------------------------------------------------
420
+
421
+ def _do_commit(self, message: str, concepts: list[str] | None = None) -> None:
422
+ try:
423
+ self._repo.commit(message=message, concepts=concepts)
424
+ except Exception as exc:
425
+ logger.warning("devtorch commit failed: %s", exc)
426
+
427
+ def _do_sensitivity(
428
+ self,
429
+ concept: str,
430
+ signal: str,
431
+ confidence: float,
432
+ disclosure: str = "PROTECTED",
433
+ ) -> None:
434
+ try:
435
+ from devtorch_core.sensitivity import SensitivityEvent
436
+
437
+ ev = SensitivityEvent(
438
+ source_node="llm_wrapper",
439
+ target_concept=concept,
440
+ counterfactuals="",
441
+ confidence=confidence,
442
+ disclosure_level=disclosure,
443
+ message=signal[:200],
444
+ )
445
+ self._repo.add_sensitivity(ev)
446
+ except Exception as exc:
447
+ logger.warning("devtorch sensitivity add failed: %s", exc)
448
+
449
+ def _estimate_bundle_tokens(self) -> int:
450
+ """Estimate the current context bundle size in tokens.
451
+
452
+ This is used when the wrapper did not provide an explicit bundle token
453
+ count. It is cached per CaptureOrchestrator instance because the
454
+ context bundle changes slowly relative to the LLM call rate.
455
+ """
456
+ if self._bundle_tokens_cache is not None:
457
+ return self._bundle_tokens_cache
458
+ try:
459
+ if self._repo is None:
460
+ self._bundle_tokens_cache = 0
461
+ else:
462
+ bundle = self._repo.context_bundle(k_tokens=8000)
463
+ self._bundle_tokens_cache = int(bundle.get("approx_tokens_used", 0))
464
+ except Exception as exc:
465
+ logger.debug("devtorch: bundle token estimation failed — %s", exc)
466
+ self._bundle_tokens_cache = 0
467
+ return self._bundle_tokens_cache
468
+
469
+ def _write_session_metrics(
470
+ self,
471
+ session_id: str,
472
+ *,
473
+ prompt_tokens: int = 0,
474
+ completion_tokens: int = 0,
475
+ bundle_tokens: int = 0,
476
+ ) -> None:
477
+ """Write/update session metrics for this wrapper session."""
478
+ from devtorch_core.metrics.session_writer import write_session_metrics
479
+
480
+ tokens_used = prompt_tokens + completion_tokens
481
+ if bundle_tokens <= 0:
482
+ bundle_tokens = self._estimate_bundle_tokens()
483
+ full_history_tokens = max(tokens_used, bundle_tokens)
484
+ tokens_saved = max(0, full_history_tokens - bundle_tokens)
485
+ latency_speedup_factor = (
486
+ full_history_tokens / bundle_tokens if bundle_tokens > 0 else 1.0
487
+ )
488
+
489
+ write_session_metrics(
490
+ self._repo.gcc_dir,
491
+ session_id,
492
+ {
493
+ "tokens_used": tokens_used,
494
+ "tokens_saved": tokens_saved,
495
+ "bundle_tokens": bundle_tokens,
496
+ "full_history_tokens": full_history_tokens,
497
+ "latency_speedup_factor": round(latency_speedup_factor, 4),
498
+ "developer_id": _developer_id(),
499
+ },
500
+ )
501
+
502
+
503
+ # ---------------------------------------------------------------------------
504
+ # Module-level helpers
505
+ # ---------------------------------------------------------------------------
506
+
507
+ def _developer_id() -> str:
508
+ """Return a developer identifier for the current session.
509
+
510
+ Order of preference:
511
+ 1. DEVTORCH_DEVELOPER_ID environment variable.
512
+ 2. git config user.email in the current repository.
513
+ 3. "unknown".
514
+ """
515
+ env_id = os.environ.get("DEVTORCH_DEVELOPER_ID", "").strip()
516
+ if env_id:
517
+ return env_id
518
+
519
+ try:
520
+ import subprocess
521
+ result = subprocess.run(
522
+ ["git", "config", "user.email"],
523
+ capture_output=True,
524
+ text=True,
525
+ timeout=2,
526
+ check=False,
527
+ )
528
+ email = result.stdout.strip()
529
+ if email:
530
+ return email
531
+ except Exception:
532
+ pass
533
+
534
+ return "unknown"
535
+
536
+ def is_disabled() -> bool:
537
+ """Return True if the DEVTORCH_DISABLE env var is set to a truthy value."""
538
+ return os.environ.get(DEVTORCH_DISABLE_ENV, "").strip().lower() in (
539
+ "1", "true", "yes"
540
+ )
541
+
542
+
543
+ def find_gcc_repo() -> Any | None:
544
+ """
545
+ Walk up from cwd looking for an initialised .GCC/ directory.
546
+ Returns a GCCRepository instance or None if none found or if
547
+ devtorch_core is not importable.
548
+ """
549
+ try:
550
+ from devtorch_core import GCCRepository
551
+ except ImportError:
552
+ logger.warning("devtorch: devtorch_core not importable — wrapper disabled")
553
+ return None
554
+
555
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
556
+ try:
557
+ candidate = GCCRepository.at(parent)
558
+ if candidate.is_initialized():
559
+ return candidate
560
+ except Exception:
561
+ continue
562
+ return None