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,116 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.models
3
+ ===========================================
4
+ Data models for Reasoning Plus Learning (DRPL).
5
+
6
+ A ReasoningCall is a single recorded step (LLM, tool, memory, or action).
7
+ A Learning is a distilled, reusable insight derived from one or more calls.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import uuid
14
+ from dataclasses import asdict, dataclass, field, fields
15
+ from datetime import datetime, timezone
16
+ from typing import Any
17
+
18
+
19
+ VALID_CALL_TYPES = ("llm", "tool", "memory", "action")
20
+ VALID_OUTCOMES = ("success", "failure", "partial", "unknown")
21
+ VALID_LEARNING_TYPES = ("insight", "correction", "pattern", "avoid", "confirm")
22
+ VALID_VALIDITIES = ("active", "stale", "deprecated")
23
+ VALID_DISCLOSURE_LEVELS = ("PUBLIC", "PROTECTED", "PRIVATE")
24
+
25
+
26
+ def _now_iso() -> str:
27
+ return datetime.now(timezone.utc).isoformat()
28
+
29
+
30
+ def _make_id(prefix: str = "drpl") -> str:
31
+ return f"{prefix}_{uuid.uuid4().hex[:16]}"
32
+
33
+
34
+ @dataclass
35
+ class ReasoningCall:
36
+ """One recorded reasoning step."""
37
+
38
+ reasoning: str
39
+ call_type: str = "llm"
40
+ name: str = ""
41
+ input: str = ""
42
+ output: str = ""
43
+ outcome: str = "unknown"
44
+ state_hash: str = ""
45
+ concepts: list[str] = field(default_factory=list)
46
+ confidence: float = 1.0
47
+ sensitivity: str = "PUBLIC"
48
+ session_id: str = ""
49
+ timestamp: str = field(default_factory=_now_iso)
50
+ id: str = field(default_factory=lambda: _make_id("call"))
51
+ meta: dict[str, Any] = field(default_factory=dict)
52
+
53
+ def __post_init__(self) -> None:
54
+ if self.call_type not in VALID_CALL_TYPES:
55
+ raise ValueError(f"Invalid call_type: {self.call_type}")
56
+ if self.outcome not in VALID_OUTCOMES:
57
+ raise ValueError(f"Invalid outcome: {self.outcome}")
58
+ if self.sensitivity not in VALID_DISCLOSURE_LEVELS:
59
+ raise ValueError(f"Invalid sensitivity: {self.sensitivity}")
60
+ self.confidence = max(0.0, min(1.0, float(self.confidence)))
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ return asdict(self)
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict[str, Any]) -> "ReasoningCall":
67
+ valid = {f.name for f in fields(cls)}
68
+ return cls(**{k: v for k, v in data.items() if k in valid})
69
+
70
+
71
+ @dataclass
72
+ class Learning:
73
+ """A distilled, reusable learning from one or more reasoning calls."""
74
+
75
+ content: str
76
+ type: str = "insight"
77
+ trigger_concepts: list[str] = field(default_factory=list)
78
+ state_hash: str = ""
79
+ validity: str = "active"
80
+ confidence: float = 1.0
81
+ source_reasoning_ids: list[str] = field(default_factory=list)
82
+ sensitivity: str = "PUBLIC"
83
+ created_at: str = field(default_factory=_now_iso)
84
+ updated_at: str = field(default_factory=_now_iso)
85
+ id: str = field(default_factory=lambda: _make_id("learn"))
86
+ meta: dict[str, Any] = field(default_factory=dict)
87
+
88
+ def __post_init__(self) -> None:
89
+ if self.type not in VALID_LEARNING_TYPES:
90
+ raise ValueError(f"Invalid learning type: {self.type}")
91
+ if self.validity not in VALID_VALIDITIES:
92
+ raise ValueError(f"Invalid validity: {self.validity}")
93
+ if self.sensitivity not in VALID_DISCLOSURE_LEVELS:
94
+ raise ValueError(f"Invalid sensitivity: {self.sensitivity}")
95
+ self.confidence = max(0.0, min(1.0, float(self.confidence)))
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ return asdict(self)
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, Any]) -> "Learning":
102
+ valid = {f.name for f in fields(cls)}
103
+ return cls(**{k: v for k, v in data.items() if k in valid})
104
+
105
+ def short_form(self, max_chars: int = 200) -> str:
106
+ """Compact, prompt-ready representation."""
107
+ text = self.content.strip().replace("\n", " ")
108
+ if len(text) > max_chars:
109
+ text = text[: max_chars - 3].rstrip() + "..."
110
+ return text
111
+
112
+
113
+ def hash_state(*items: str) -> str:
114
+ """Stable hash for a set of state tokens."""
115
+ joined = "|".join(sorted(items))
116
+ return hashlib.sha256(joined.encode("utf-8")).hexdigest()[:16]
@@ -0,0 +1,126 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.provenance
3
+ ===============================================
4
+ Provenance tracking for injected learnings.
5
+
6
+ Records which Learning records were injected into each LLM/tool call so that
7
+ observed outcomes can be attributed back to the learnings that influenced the
8
+ call.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ from dataclasses import asdict, dataclass, field, fields
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from devtorch_core.reasoning_plus.learning.models import _make_id, _now_iso
20
+
21
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
22
+
23
+
24
+ @dataclass
25
+ class ProvenanceRecord:
26
+ """Which learnings were injected into a single call, and what the outcome was."""
27
+
28
+ call_id: str
29
+ learning_ids: list[str] = field(default_factory=list)
30
+ session_id: str = ""
31
+ prompt_hash: str = ""
32
+ prompt_excerpt: str = ""
33
+ model_name: str = ""
34
+ outcome: str | None = None
35
+ injected_at: str = field(default_factory=_now_iso)
36
+ updated_at: str = field(default_factory=_now_iso)
37
+ id: str = field(default_factory=lambda: _make_id("prov"))
38
+ meta: dict[str, Any] = field(default_factory=dict)
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ return asdict(self)
42
+
43
+ @classmethod
44
+ def from_dict(cls, data: dict[str, Any]) -> "ProvenanceRecord":
45
+ valid = {f.name for f in fields(cls)}
46
+ return cls(**{k: v for k, v in data.items() if k in valid})
47
+
48
+
49
+ class ProvenanceStore:
50
+ """Persist ProvenanceRecord records under .GCC/reasoning_learnings/provenance/."""
51
+
52
+ def __init__(self, gcc_dir: Path | str) -> None:
53
+ self._gcc_dir = Path(gcc_dir)
54
+ self._provenance_dir = self._gcc_dir / "reasoning_learnings" / "provenance"
55
+
56
+ def save(self, record: ProvenanceRecord) -> Path:
57
+ self._provenance_dir.mkdir(parents=True, exist_ok=True)
58
+ path = self._provenance_dir / f"{record.id}.json"
59
+ _atomic_json_write(path, record.to_dict())
60
+ return path
61
+
62
+ def get(self, record_id: str) -> ProvenanceRecord | None:
63
+ path = self._provenance_dir / f"{record_id}.json"
64
+ if not path.exists():
65
+ return None
66
+ try:
67
+ data = json.loads(path.read_text(encoding="utf-8"))
68
+ return ProvenanceRecord.from_dict(data)
69
+ except Exception as exc:
70
+ logger.warning("devtorch: failed to read provenance %s — %s", record_id, exc)
71
+ return None
72
+
73
+ def get_by_call(self, call_id: str) -> ProvenanceRecord | None:
74
+ for record in self.list():
75
+ if record.call_id == call_id:
76
+ return record
77
+ return None
78
+
79
+ def list(self, outcome: str | None = None, learning_id: str | None = None) -> list[ProvenanceRecord]:
80
+ out: list[ProvenanceRecord] = []
81
+ if not self._provenance_dir.exists():
82
+ return out
83
+ for path in sorted(self._provenance_dir.glob("*.json")):
84
+ try:
85
+ data = json.loads(path.read_text(encoding="utf-8"))
86
+ record = ProvenanceRecord.from_dict(data)
87
+ if outcome and record.outcome != outcome:
88
+ continue
89
+ if learning_id and learning_id not in record.learning_ids:
90
+ continue
91
+ out.append(record)
92
+ except Exception as exc:
93
+ logger.debug("devtorch: skipping malformed provenance %s — %s", path, exc)
94
+ return out
95
+
96
+ def update_outcome(self, call_id: str, outcome: str) -> ProvenanceRecord | None:
97
+ record = self.get_by_call(call_id)
98
+ if not record:
99
+ return None
100
+ record.outcome = outcome
101
+ record.updated_at = _now_iso()
102
+ self.save(record)
103
+ return record
104
+
105
+ def count(self) -> int:
106
+ if not self._provenance_dir.exists():
107
+ return 0
108
+ return len(list(self._provenance_dir.glob("*.json")))
109
+
110
+
111
+ def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
112
+ tmp = path.with_suffix(path.suffix + ".tmp")
113
+ try:
114
+ with open(tmp, "w", encoding="utf-8") as f:
115
+ json.dump(data, f, indent=2)
116
+ tmp.replace(path)
117
+ finally:
118
+ if tmp.exists():
119
+ try:
120
+ tmp.unlink()
121
+ except Exception:
122
+ pass
123
+
124
+
125
+ def _hash_prompt(prompt_text: str) -> str:
126
+ return hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()[:16]
@@ -0,0 +1,81 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.recorder
3
+ ==============================================
4
+ Record reasoning calls (LLM, tool, memory, action) to .GCC/.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from devtorch_core.reasoning_plus.learning.models import ReasoningCall
14
+
15
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
16
+
17
+
18
+ class CallRecorder:
19
+ """Persist ReasoningCall records under .GCC/reasoning_learnings/calls/."""
20
+
21
+ def __init__(self, gcc_dir: Path | str) -> None:
22
+ self._gcc_dir = Path(gcc_dir)
23
+ self._calls_dir = self._gcc_dir / "reasoning_learnings" / "calls"
24
+
25
+ def record(self, call: ReasoningCall) -> Path:
26
+ """Write a call to disk and return the path."""
27
+ self._calls_dir.mkdir(parents=True, exist_ok=True)
28
+ path = self._calls_dir / f"{call.id}.json"
29
+ try:
30
+ _atomic_json_write(path, call.to_dict())
31
+ except Exception as exc:
32
+ logger.warning("devtorch: failed to record reasoning call — %s", exc)
33
+ return path
34
+
35
+ def get(self, call_id: str) -> ReasoningCall | None:
36
+ path = self._calls_dir / f"{call_id}.json"
37
+ if not path.exists():
38
+ return None
39
+ try:
40
+ data = json.loads(path.read_text(encoding="utf-8"))
41
+ return ReasoningCall.from_dict(data)
42
+ except Exception as exc:
43
+ logger.warning("devtorch: failed to read reasoning call %s — %s", call_id, exc)
44
+ return None
45
+
46
+ def list(self, session_id: str | None = None, call_type: str | None = None) -> list[ReasoningCall]:
47
+ """List recorded calls, optionally filtered by session or type."""
48
+ out: list[ReasoningCall] = []
49
+ if not self._calls_dir.exists():
50
+ return out
51
+ for path in sorted(self._calls_dir.glob("*.json")):
52
+ try:
53
+ data = json.loads(path.read_text(encoding="utf-8"))
54
+ call = ReasoningCall.from_dict(data)
55
+ if session_id and call.session_id != session_id:
56
+ continue
57
+ if call_type and call.call_type != call_type:
58
+ continue
59
+ out.append(call)
60
+ except Exception as exc:
61
+ logger.debug("devtorch: skipping malformed call record %s — %s", path, exc)
62
+ return out
63
+
64
+ def count(self) -> int:
65
+ if not self._calls_dir.exists():
66
+ return 0
67
+ return len(list(self._calls_dir.glob("*.json")))
68
+
69
+
70
+ def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
71
+ tmp = path.with_suffix(path.suffix + ".tmp")
72
+ try:
73
+ with open(tmp, "w", encoding="utf-8") as f:
74
+ json.dump(data, f, indent=2)
75
+ tmp.replace(path)
76
+ finally:
77
+ if tmp.exists():
78
+ try:
79
+ tmp.unlink()
80
+ except Exception:
81
+ pass
@@ -0,0 +1,122 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.relevance
3
+ =================================================
4
+ Rank learnings by relevance to the current context and state.
5
+
6
+ Scoring factors (two-dimensional):
7
+ 1. Semantic similarity between context and learning content (embedding)
8
+ 2. Concept overlap between context keywords and learning trigger_concepts
9
+ 3. Confidence
10
+ 4. State freshness (matching state_hash boosts score)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import time
16
+ from typing import Any
17
+
18
+ from devtorch_core.reasoning_plus.context import extract_keywords
19
+ from devtorch_core.reasoning_plus.learning.embeddings import (
20
+ EmbeddingBackend,
21
+ KeywordEmbeddingBackend,
22
+ )
23
+ from devtorch_core.reasoning_plus.learning.models import Learning
24
+
25
+
26
+ class RelevanceEngine:
27
+ """Score and rank learnings for a given context using an embedding backend."""
28
+
29
+ def __init__(self, backend: EmbeddingBackend | None = None, cache_ttl: float = 0) -> None:
30
+ self._backend = backend or KeywordEmbeddingBackend()
31
+ self._cache_ttl = cache_ttl
32
+ self._cache: dict[str, tuple[list[Learning], float]] = {}
33
+
34
+ def rank(
35
+ self,
36
+ context: str,
37
+ learnings: list[Learning],
38
+ current_state_hash: str | None = None,
39
+ top_n: int = 3,
40
+ ) -> list[Learning]:
41
+ """
42
+ Return the top-N active learnings ranked by relevance.
43
+
44
+ Scoring factors:
45
+ - semantic similarity between context and learning content
46
+ - concept overlap between context keywords and learning trigger_concepts
47
+ - confidence
48
+ - state freshness (matching state_hash boosts score)
49
+
50
+ Results are cached in memory when ``cache_ttl`` > 0 so repeated
51
+ similar prompts in the same session avoid recomputing embeddings.
52
+ """
53
+ cache_key = self._cache_key(context, learnings, current_state_hash, top_n)
54
+ if self._cache_ttl and cache_key:
55
+ cached = self._cache.get(cache_key)
56
+ if cached and (time.monotonic() - cached[1]) < self._cache_ttl:
57
+ return cached[0]
58
+
59
+ context_embedding = self._backend.encode(context)
60
+ context_keywords = set(extract_keywords(context, max_keywords=30))
61
+ scored: list[tuple[float, Learning]] = []
62
+
63
+ for learning in learnings:
64
+ if learning.validity != "active":
65
+ continue
66
+ score = self._score(
67
+ learning, context_embedding, current_state_hash, context_keywords,
68
+ )
69
+ scored.append((score, learning))
70
+
71
+ scored.sort(key=lambda x: x[0], reverse=True)
72
+ result = [learning for _, learning in scored[:top_n]]
73
+
74
+ if self._cache_ttl and cache_key:
75
+ self._cache[cache_key] = (result, time.monotonic())
76
+ return result
77
+
78
+ def _cache_key(
79
+ self,
80
+ context: str,
81
+ learnings: list[Learning],
82
+ current_state_hash: str | None,
83
+ top_n: int,
84
+ ) -> str:
85
+ """Stable key for the relevance cache."""
86
+ context_hash = hashlib.sha256(context.encode("utf-8")).hexdigest()[:16]
87
+ learning_ids = sorted({l.id for l in learnings})
88
+ learning_hash = hashlib.sha256(
89
+ "|".join(learning_ids).encode("utf-8")
90
+ ).hexdigest()[:16]
91
+ return f"{context_hash}:{current_state_hash or ''}:{learning_hash}:{top_n}"
92
+
93
+ def _score(
94
+ self,
95
+ learning: Learning,
96
+ context_embedding: Any,
97
+ current_state_hash: str | None,
98
+ context_keywords: set[str] | None = None,
99
+ ) -> float:
100
+ learning_embedding = self._backend.encode(learning.content)
101
+ semantic_score = self._backend.similarity(context_embedding, learning_embedding)
102
+
103
+ confidence_weight = learning.confidence
104
+
105
+ state_bonus = 0.0
106
+ if current_state_hash and learning.state_hash == current_state_hash:
107
+ state_bonus = 0.2
108
+
109
+ # Concept overlap: how much the learning's trigger_concepts overlap
110
+ # with keywords extracted from the current prompt context.
111
+ concept_bonus = 0.0
112
+ if context_keywords and learning.trigger_concepts:
113
+ learning_concepts = {c.lower() for c in learning.trigger_concepts}
114
+ overlap = len(context_keywords & learning_concepts)
115
+ if overlap > 0:
116
+ concept_bonus = min(0.3, overlap * 0.1)
117
+
118
+ return semantic_score + confidence_weight * 0.5 + state_bonus + concept_bonus
119
+
120
+ def invalidate_cache(self) -> None:
121
+ """Clear the relevance cache."""
122
+ self._cache.clear()
@@ -0,0 +1,86 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.state
3
+ ===========================================
4
+ Workspace state hashing and staleness detection for learnings.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+
13
+ from devtorch_core.reasoning_plus.learning.models import hash_state
14
+
15
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
16
+
17
+
18
+ def workspace_state_hash(repo_path: Path | str, file_paths: Iterable[str] | None = None) -> str:
19
+ """
20
+ Return a short hash representing the current state of the workspace.
21
+
22
+ Strategy:
23
+ - If inside a git repo, include the current HEAD commit hash.
24
+ - For any provided file_paths, include the current file content hash.
25
+ - If not a git repo and no file paths, fall back to a hash of the directory path.
26
+ """
27
+ repo_path = Path(repo_path)
28
+ tokens: list[str] = []
29
+
30
+ head = _git_head(repo_path)
31
+ if head:
32
+ tokens.append(f"head:{head}")
33
+
34
+ for fp in file_paths or []:
35
+ content_hash = _file_content_hash(repo_path / fp)
36
+ if content_hash:
37
+ tokens.append(f"{fp}:{content_hash}")
38
+
39
+ if not tokens:
40
+ tokens.append(str(repo_path.resolve()))
41
+
42
+ return hash_state(*tokens)
43
+
44
+
45
+ def _git_head(repo_path: Path) -> str | None:
46
+ try:
47
+ result = subprocess.run(
48
+ ["git", "rev-parse", "HEAD"],
49
+ cwd=repo_path,
50
+ capture_output=True,
51
+ text=True,
52
+ errors="ignore",
53
+ timeout=5,
54
+ )
55
+ if result.returncode == 0:
56
+ return result.stdout.strip()
57
+ except Exception as exc:
58
+ logger.debug("devtorch: could not read git HEAD — %s", exc)
59
+ return None
60
+
61
+
62
+ def _file_content_hash(path: Path) -> str | None:
63
+ if not path.exists():
64
+ return None
65
+ try:
66
+ return hash_state(path.read_text(encoding="utf-8", errors="ignore"))
67
+ except Exception as exc:
68
+ logger.debug("devtorch: could not hash %s — %s", path, exc)
69
+ return None
70
+
71
+
72
+ def affected_by_state_change(repo_path: Path | str, learning_file_paths: Iterable[str], changed_paths: Iterable[str]) -> bool:
73
+ """
74
+ Return True if a learning should be re-evaluated because the workspace changed.
75
+
76
+ A learning is affected when any of the paths it references overlap with a changed path.
77
+ """
78
+ changed = set(changed_paths)
79
+ for fp in learning_file_paths:
80
+ if fp in changed:
81
+ return True
82
+ # Also invalidate if a parent directory was changed
83
+ for changed_path in changed:
84
+ if changed_path.startswith(fp) or fp.startswith(changed_path):
85
+ return True
86
+ return False
@@ -0,0 +1,160 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.store
3
+ ===========================================
4
+ Persistence layer for distilled learnings.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from devtorch_core.reasoning_plus.context import extract_keywords
14
+ from devtorch_core.reasoning_plus.learning.models import Learning
15
+
16
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
17
+
18
+
19
+ class LearningStore:
20
+ """Persist Learning records under .GCC/reasoning_learnings/learnings/."""
21
+
22
+ def __init__(self, gcc_dir: Path | str) -> None:
23
+ self._gcc_dir = Path(gcc_dir)
24
+ self._learnings_dir = self._gcc_dir / "reasoning_learnings" / "learnings"
25
+
26
+ def save(self, learning: Learning, deduplicate: bool = True) -> Path:
27
+ self._learnings_dir.mkdir(parents=True, exist_ok=True)
28
+ if deduplicate and learning.validity == "active":
29
+ duplicate = self._find_duplicate(learning)
30
+ if duplicate:
31
+ merged = self._merge(duplicate, learning)
32
+ path = self._learnings_dir / f"{merged.id}.json"
33
+ try:
34
+ _atomic_json_write(path, merged.to_dict())
35
+ except Exception as exc:
36
+ logger.warning("devtorch: failed to save merged learning — %s", exc)
37
+ return path
38
+ path = self._learnings_dir / f"{learning.id}.json"
39
+ try:
40
+ _atomic_json_write(path, learning.to_dict())
41
+ except Exception as exc:
42
+ logger.warning("devtorch: failed to save learning — %s", exc)
43
+ return path
44
+
45
+ def get(self, learning_id: str) -> Learning | None:
46
+ path = self._learnings_dir / f"{learning_id}.json"
47
+ if not path.exists():
48
+ return None
49
+ try:
50
+ data = json.loads(path.read_text(encoding="utf-8"))
51
+ return Learning.from_dict(data)
52
+ except Exception as exc:
53
+ logger.warning("devtorch: failed to read learning %s — %s", learning_id, exc)
54
+ return None
55
+
56
+ def list(self, validity: str | None = None, concept: str | None = None) -> list[Learning]:
57
+ out: list[Learning] = []
58
+ if not self._learnings_dir.exists():
59
+ return out
60
+ for path in sorted(self._learnings_dir.glob("*.json")):
61
+ try:
62
+ data = json.loads(path.read_text(encoding="utf-8"))
63
+ learning = Learning.from_dict(data)
64
+ if validity and learning.validity != validity:
65
+ continue
66
+ if concept and concept.lower() not in {c.lower() for c in learning.trigger_concepts}:
67
+ continue
68
+ out.append(learning)
69
+ except Exception as exc:
70
+ logger.debug("devtorch: skipping malformed learning %s — %s", path, exc)
71
+ return out
72
+
73
+ def invalidate(self, learning_id: str, reason: str = "stale") -> Learning | None:
74
+ learning = self.get(learning_id)
75
+ if not learning:
76
+ return None
77
+ learning.validity = "stale" if reason == "stale" else "deprecated"
78
+ learning.updated_at = _now_iso()
79
+ self.save(learning)
80
+ return learning
81
+
82
+ def count(self) -> int:
83
+ if not self._learnings_dir.exists():
84
+ return 0
85
+ return len(list(self._learnings_dir.glob("*.json")))
86
+
87
+ # ------------------------------------------------------------------
88
+ # Deduplication
89
+ # ------------------------------------------------------------------
90
+
91
+ def _find_duplicate(self, learning: Learning, threshold: float = 0.8) -> Learning | None:
92
+ """Return the most similar *other* active learning if it reaches the threshold."""
93
+ best: Learning | None = None
94
+ best_score = threshold
95
+ for existing in self.list(validity="active"):
96
+ if existing.id == learning.id:
97
+ continue
98
+ score = self._similarity(existing, learning)
99
+ if score >= best_score:
100
+ best_score = score
101
+ best = existing
102
+ return best
103
+
104
+ def _similarity(self, a: Learning, b: Learning) -> float:
105
+ """Combined concept and content similarity in [0, 1].
106
+
107
+ Concept overlap is weighted more heavily because two learnings that share
108
+ the same trigger concepts are likely talking about the same situation.
109
+ """
110
+ a_concepts = {c.lower() for c in a.trigger_concepts}
111
+ b_concepts = {c.lower() for c in b.trigger_concepts}
112
+ concept_union = len(a_concepts | b_concepts)
113
+ concept_score = len(a_concepts & b_concepts) / concept_union if concept_union else 0.0
114
+
115
+ a_keywords = set(extract_keywords(a.content, max_keywords=15))
116
+ b_keywords = set(extract_keywords(b.content, max_keywords=15))
117
+ content_union = len(a_keywords | b_keywords)
118
+ content_score = len(a_keywords & b_keywords) / content_union if content_union else 0.0
119
+
120
+ return 0.8 * concept_score + 0.2 * content_score
121
+
122
+ def _merge(self, existing: Learning, new: Learning) -> Learning:
123
+ """Merge *new* into *existing*, keeping the existing stable ID."""
124
+ merged = Learning.from_dict(existing.to_dict())
125
+ merged.content = new.content
126
+ merged.trigger_concepts = sorted(
127
+ set(existing.trigger_concepts) | set(new.trigger_concepts)
128
+ )
129
+ merged.confidence = max(existing.confidence, new.confidence)
130
+ merged.source_reasoning_ids = list(
131
+ set(existing.source_reasoning_ids) | set(new.source_reasoning_ids)
132
+ )
133
+ merged.validity = _more_restrictive_validity(existing.validity, new.validity)
134
+ merged.updated_at = _now_iso()
135
+ return merged
136
+
137
+
138
+ def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
139
+ tmp = path.with_suffix(path.suffix + ".tmp")
140
+ try:
141
+ with open(tmp, "w", encoding="utf-8") as f:
142
+ json.dump(data, f, indent=2)
143
+ tmp.replace(path)
144
+ finally:
145
+ if tmp.exists():
146
+ try:
147
+ tmp.unlink()
148
+ except Exception:
149
+ pass
150
+
151
+
152
+ def _more_restrictive_validity(a: str, b: str) -> str:
153
+ """Return the more restrictive validity status."""
154
+ order = {"deprecated": 2, "stale": 1, "active": 0}
155
+ return a if order.get(a, 0) >= order.get(b, 0) else b
156
+
157
+
158
+ def _now_iso() -> str:
159
+ from datetime import datetime, timezone
160
+ return datetime.now(timezone.utc).isoformat()