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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- 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
|