thinkstack-core 4.0.0__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.
- thinkstack_core/__init__.py +158 -0
- thinkstack_core/aggphi_textual.py +275 -0
- thinkstack_core/alerts/__init__.py +23 -0
- thinkstack_core/alerts/base.py +46 -0
- thinkstack_core/alerts/config.py +60 -0
- thinkstack_core/alerts/dispatcher.py +110 -0
- thinkstack_core/alerts/jira.py +96 -0
- thinkstack_core/alerts/linear.py +72 -0
- thinkstack_core/alerts/pagerduty.py +66 -0
- thinkstack_core/alerts/slack.py +81 -0
- thinkstack_core/alerts/teams.py +70 -0
- thinkstack_core/audit/__init__.py +43 -0
- thinkstack_core/audit/exporter.py +297 -0
- thinkstack_core/audit/privacy.py +101 -0
- thinkstack_core/audit/scrubber.py +149 -0
- thinkstack_core/audit/service.py +67 -0
- thinkstack_core/audit/signing.py +127 -0
- thinkstack_core/broadcast/__init__.py +4 -0
- thinkstack_core/broadcast/broadcaster.py +100 -0
- thinkstack_core/broadcast/watcher.py +71 -0
- thinkstack_core/capability.py +639 -0
- thinkstack_core/cloud/__init__.py +1 -0
- thinkstack_core/cloud/client_config.py +472 -0
- thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
- thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode.json +15 -0
- thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
- thinkstack_core/cloud/mcp_client.py +229 -0
- thinkstack_core/cloud/setup.py +144 -0
- thinkstack_core/cloud/sync.py +143 -0
- thinkstack_core/cloud/sync_bundle.py +639 -0
- thinkstack_core/cloud/sync_conflicts.py +183 -0
- thinkstack_core/cloud/sync_state.py +159 -0
- thinkstack_core/cloud/team_sync.py +337 -0
- thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
- thinkstack_core/codex/__init__.py +9 -0
- thinkstack_core/codex/__main__.py +97 -0
- thinkstack_core/codex/capture.py +208 -0
- thinkstack_core/codex/proxy.py +412 -0
- thinkstack_core/compat.py +103 -0
- thinkstack_core/concept_catalog.py +209 -0
- thinkstack_core/consolidation/__init__.py +3 -0
- thinkstack_core/consolidation/synthesizer.py +87 -0
- thinkstack_core/consolidation/workflow.py +175 -0
- thinkstack_core/daemon/__init__.py +27 -0
- thinkstack_core/daemon/supervisor.py +293 -0
- thinkstack_core/daemon/watcher.py +244 -0
- thinkstack_core/dashboard_api.py +2012 -0
- thinkstack_core/deltaf.py +97 -0
- thinkstack_core/disclosure.py +50 -0
- thinkstack_core/divergence/__init__.py +3 -0
- thinkstack_core/divergence/detector.py +166 -0
- thinkstack_core/gateway/__init__.py +32 -0
- thinkstack_core/gateway/key_manager.py +124 -0
- thinkstack_core/gateway/metrics_webhook.py +252 -0
- thinkstack_core/gateway/policy.py +262 -0
- thinkstack_core/gateway/server.py +727 -0
- thinkstack_core/gateway/sso.py +233 -0
- thinkstack_core/gcc.py +1246 -0
- thinkstack_core/github/__init__.py +35 -0
- thinkstack_core/github/app.py +240 -0
- thinkstack_core/github/comment_builder.py +113 -0
- thinkstack_core/github/pat.py +76 -0
- thinkstack_core/github/pr_parser.py +82 -0
- thinkstack_core/github/pr_reporter.py +555 -0
- thinkstack_core/gitlab/__init__.py +177 -0
- thinkstack_core/hitl/__init__.py +4 -0
- thinkstack_core/hitl/channels.py +129 -0
- thinkstack_core/hitl/orchestrator.py +95 -0
- thinkstack_core/hooks/__init__.py +17 -0
- thinkstack_core/hooks/claude_code.py +228 -0
- thinkstack_core/hooks/git_capture.py +341 -0
- thinkstack_core/hooks/git_commit.py +182 -0
- thinkstack_core/hooks/installer.py +850 -0
- thinkstack_core/hooks/pre_commit.py +157 -0
- thinkstack_core/hooks/runner.py +386 -0
- thinkstack_core/identity/__init__.py +4 -0
- thinkstack_core/identity/agent.py +86 -0
- thinkstack_core/identity/providers.py +85 -0
- thinkstack_core/invariants.py +182 -0
- thinkstack_core/mcp/__init__.py +10 -0
- thinkstack_core/mcp/auth.py +177 -0
- thinkstack_core/mcp/server.py +1215 -0
- thinkstack_core/metrics/__init__.py +35 -0
- thinkstack_core/metrics/aggregate.py +215 -0
- thinkstack_core/metrics/calibrate.py +198 -0
- thinkstack_core/metrics/calibration.py +125 -0
- thinkstack_core/metrics/credibility.py +288 -0
- thinkstack_core/metrics/delivery_time.py +70 -0
- thinkstack_core/metrics/dhs.py +126 -0
- thinkstack_core/metrics/mcs.py +96 -0
- thinkstack_core/metrics/roi.py +88 -0
- thinkstack_core/metrics/session_writer.py +81 -0
- thinkstack_core/metrics/shadow_ai.py +117 -0
- thinkstack_core/metrics/sprint_writer.py +243 -0
- thinkstack_core/observability/__init__.py +78 -0
- thinkstack_core/observability/datadog.py +157 -0
- thinkstack_core/observability/formatter.py +119 -0
- thinkstack_core/observability/report.py +264 -0
- thinkstack_core/observability/servicenow.py +147 -0
- thinkstack_core/observability/splunk.py +218 -0
- thinkstack_core/observability/webhook.py +227 -0
- thinkstack_core/parser/__init__.py +30 -0
- thinkstack_core/parser/blocks.py +216 -0
- thinkstack_core/parser/inference.py +159 -0
- thinkstack_core/parser/thinking.py +112 -0
- thinkstack_core/projects.py +169 -0
- thinkstack_core/prompt_artifact.py +76 -0
- thinkstack_core/proxy/__init__.py +9 -0
- thinkstack_core/proxy/routes/__init__.py +1 -0
- thinkstack_core/proxy/routes/anthropic.py +264 -0
- thinkstack_core/proxy/routes/azure_openai.py +336 -0
- thinkstack_core/proxy/routes/gemini.py +331 -0
- thinkstack_core/proxy/routes/groq.py +284 -0
- thinkstack_core/proxy/routes/ollama.py +279 -0
- thinkstack_core/proxy/routes/openai.py +287 -0
- thinkstack_core/proxy/server.py +356 -0
- thinkstack_core/query/__init__.py +15 -0
- thinkstack_core/query/grep.py +181 -0
- thinkstack_core/query/hybrid.py +86 -0
- thinkstack_core/query/semantic.py +157 -0
- thinkstack_core/rdp.py +105 -0
- thinkstack_core/reasoning/__init__.py +4 -0
- thinkstack_core/reasoning/entry.py +31 -0
- thinkstack_core/reasoning/store.py +122 -0
- thinkstack_core/reasoning_plus/__init__.py +70 -0
- thinkstack_core/reasoning_plus/augmenter.py +337 -0
- thinkstack_core/reasoning_plus/capture.py +51 -0
- thinkstack_core/reasoning_plus/config.py +313 -0
- thinkstack_core/reasoning_plus/context.py +262 -0
- thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
- thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
- thinkstack_core/reasoning_plus/learning/api.py +784 -0
- thinkstack_core/reasoning_plus/learning/chain.py +285 -0
- thinkstack_core/reasoning_plus/learning/composer.py +141 -0
- thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
- thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
- thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
- thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
- thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
- thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
- thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
- thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
- thinkstack_core/reasoning_plus/learning/models.py +149 -0
- thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
- thinkstack_core/reasoning_plus/learning/pii.py +142 -0
- thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
- thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
- thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
- thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
- thinkstack_core/reasoning_plus/learning/state.py +86 -0
- thinkstack_core/reasoning_plus/learning/store.py +178 -0
- thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- thinkstack_core/reasoning_plus/prompt.py +90 -0
- thinkstack_core/rep.py +134 -0
- thinkstack_core/rep_network/__init__.py +25 -0
- thinkstack_core/rep_network/merge.py +70 -0
- thinkstack_core/rep_network/node.py +137 -0
- thinkstack_core/rep_network/server.py +140 -0
- thinkstack_core/rep_network/sync.py +207 -0
- thinkstack_core/sensitivity.py +182 -0
- thinkstack_core/serve.py +258 -0
- thinkstack_core/session/__init__.py +39 -0
- thinkstack_core/session/disagreement.py +188 -0
- thinkstack_core/session/models.py +114 -0
- thinkstack_core/session/orchestrator.py +182 -0
- thinkstack_core/session/planner.py +169 -0
- thinkstack_core/session/simulator.py +132 -0
- thinkstack_core/signing.py +290 -0
- thinkstack_core/sis.py +197 -0
- thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
- thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
- thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
- thinkstack_core/storage.py +308 -0
- thinkstack_core/templates/__init__.py +6 -0
- thinkstack_core/templates/engine.py +122 -0
- thinkstack_core/templates/go.py +18 -0
- thinkstack_core/templates/infra.py +19 -0
- thinkstack_core/templates/library/__init__.py +18 -0
- thinkstack_core/templates/library/api_design.md +27 -0
- thinkstack_core/templates/library/bug_fix.md +27 -0
- thinkstack_core/templates/library/decision_record.md +27 -0
- thinkstack_core/templates/library/engine.py +228 -0
- thinkstack_core/templates/library/security_review.md +30 -0
- thinkstack_core/templates/python.py +19 -0
- thinkstack_core/templates/react.py +18 -0
- thinkstack_core/templates/typescript.py +18 -0
- thinkstack_core/theta.py +221 -0
- thinkstack_core/theta_synthesis.py +268 -0
- thinkstack_core/topics.py +320 -0
- thinkstack_core/variance.py +219 -0
- thinkstack_core/wrapper/__init__.py +52 -0
- thinkstack_core/wrapper/anthropic.py +487 -0
- thinkstack_core/wrapper/base.py +562 -0
- thinkstack_core/wrapper/bedrock.py +342 -0
- thinkstack_core/wrapper/gemini.py +422 -0
- thinkstack_core/wrapper/ollama.py +527 -0
- thinkstack_core/wrapper/openai.py +461 -0
- thinkstack_core-4.0.0.dist-info/METADATA +868 -0
- thinkstack_core-4.0.0.dist-info/RECORD +205 -0
- thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
- thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
- thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_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 thinkstack_core.reasoning_plus.learning.models import ReasoningCall
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("thinkstack.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("thinkstack: 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("thinkstack: 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("thinkstack: 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
|
+
thinkstack_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 thinkstack_core.reasoning_plus.context import extract_keywords
|
|
19
|
+
from thinkstack_core.reasoning_plus.learning.embeddings import (
|
|
20
|
+
EmbeddingBackend,
|
|
21
|
+
KeywordEmbeddingBackend,
|
|
22
|
+
)
|
|
23
|
+
from thinkstack_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
|
+
thinkstack_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 thinkstack_core.reasoning_plus.learning.models import hash_state
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("thinkstack.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("thinkstack: 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("thinkstack: 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,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_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 thinkstack_core.reasoning_plus.context import extract_keywords
|
|
14
|
+
from thinkstack_core.reasoning_plus.learning.models import Learning
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("thinkstack.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("thinkstack: 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("thinkstack: 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("thinkstack: failed to read learning %s — %s", learning_id, exc)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def list(
|
|
57
|
+
self,
|
|
58
|
+
validity: str | None = None,
|
|
59
|
+
concept: str | None = None,
|
|
60
|
+
scope: str | None = None,
|
|
61
|
+
) -> list[Learning]:
|
|
62
|
+
out: list[Learning] = []
|
|
63
|
+
if not self._learnings_dir.exists():
|
|
64
|
+
return out
|
|
65
|
+
for path in sorted(self._learnings_dir.glob("*.json")):
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
learning = Learning.from_dict(data)
|
|
69
|
+
if validity and learning.validity != validity:
|
|
70
|
+
continue
|
|
71
|
+
if concept and concept.lower() not in {c.lower() for c in learning.trigger_concepts}:
|
|
72
|
+
continue
|
|
73
|
+
if scope and learning.scope != scope:
|
|
74
|
+
continue
|
|
75
|
+
out.append(learning)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.debug("thinkstack: skipping malformed learning %s — %s", path, exc)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
def invalidate(self, learning_id: str, reason: str = "stale") -> Learning | None:
|
|
81
|
+
learning = self.get(learning_id)
|
|
82
|
+
if not learning:
|
|
83
|
+
return None
|
|
84
|
+
learning.validity = "stale" if reason == "stale" else "deprecated"
|
|
85
|
+
learning.updated_at = _now_iso()
|
|
86
|
+
self.save(learning)
|
|
87
|
+
return learning
|
|
88
|
+
|
|
89
|
+
def count(self) -> int:
|
|
90
|
+
if not self._learnings_dir.exists():
|
|
91
|
+
return 0
|
|
92
|
+
return len(list(self._learnings_dir.glob("*.json")))
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Deduplication
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def _find_duplicate(self, learning: Learning, threshold: float = 0.8) -> Learning | None:
|
|
99
|
+
"""Return the most similar *other* active learning if it reaches the threshold."""
|
|
100
|
+
best: Learning | None = None
|
|
101
|
+
best_score = threshold
|
|
102
|
+
for existing in self.list(validity="active"):
|
|
103
|
+
if existing.id == learning.id:
|
|
104
|
+
continue
|
|
105
|
+
score = self._similarity(existing, learning)
|
|
106
|
+
if score >= best_score:
|
|
107
|
+
best_score = score
|
|
108
|
+
best = existing
|
|
109
|
+
return best
|
|
110
|
+
|
|
111
|
+
def _similarity(self, a: Learning, b: Learning) -> float:
|
|
112
|
+
"""Combined concept and content similarity in [0, 1].
|
|
113
|
+
|
|
114
|
+
Concept overlap is weighted more heavily because two learnings that share
|
|
115
|
+
the same trigger concepts are likely talking about the same situation.
|
|
116
|
+
"""
|
|
117
|
+
a_concepts = {c.lower() for c in a.trigger_concepts}
|
|
118
|
+
b_concepts = {c.lower() for c in b.trigger_concepts}
|
|
119
|
+
concept_union = len(a_concepts | b_concepts)
|
|
120
|
+
concept_score = len(a_concepts & b_concepts) / concept_union if concept_union else 0.0
|
|
121
|
+
|
|
122
|
+
a_keywords = set(extract_keywords(a.content, max_keywords=15))
|
|
123
|
+
b_keywords = set(extract_keywords(b.content, max_keywords=15))
|
|
124
|
+
content_union = len(a_keywords | b_keywords)
|
|
125
|
+
content_score = len(a_keywords & b_keywords) / content_union if content_union else 0.0
|
|
126
|
+
|
|
127
|
+
return 0.8 * concept_score + 0.2 * content_score
|
|
128
|
+
|
|
129
|
+
def _merge(self, existing: Learning, new: Learning) -> Learning:
|
|
130
|
+
"""Merge *new* into *existing*, keeping the existing stable ID."""
|
|
131
|
+
merged = Learning.from_dict(existing.to_dict())
|
|
132
|
+
merged.content = new.content
|
|
133
|
+
merged.trigger_concepts = sorted(
|
|
134
|
+
set(existing.trigger_concepts) | set(new.trigger_concepts)
|
|
135
|
+
)
|
|
136
|
+
merged.confidence = max(existing.confidence, new.confidence)
|
|
137
|
+
merged.source_reasoning_ids = list(
|
|
138
|
+
set(existing.source_reasoning_ids) | set(new.source_reasoning_ids)
|
|
139
|
+
)
|
|
140
|
+
merged.validity = _more_restrictive_validity(existing.validity, new.validity)
|
|
141
|
+
# Preserve evolution fields (S19) — union lists, prefer non-empty values.
|
|
142
|
+
merged.supersedes = sorted(set(existing.supersedes) | set(new.supersedes))
|
|
143
|
+
merged.superseded_by = new.superseded_by or existing.superseded_by
|
|
144
|
+
merged.evolution_chain = new.evolution_chain or existing.evolution_chain
|
|
145
|
+
merged.scope = _more_restrictive_scope(existing.scope, new.scope)
|
|
146
|
+
merged.updated_at = _now_iso()
|
|
147
|
+
return merged
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
|
|
151
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
152
|
+
try:
|
|
153
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
154
|
+
json.dump(data, f, indent=2)
|
|
155
|
+
tmp.replace(path)
|
|
156
|
+
finally:
|
|
157
|
+
if tmp.exists():
|
|
158
|
+
try:
|
|
159
|
+
tmp.unlink()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _more_restrictive_validity(a: str, b: str) -> str:
|
|
165
|
+
"""Return the more restrictive validity status."""
|
|
166
|
+
order = {"deprecated": 2, "stale": 1, "active": 0}
|
|
167
|
+
return a if order.get(a, 0) >= order.get(b, 0) else b
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _more_restrictive_scope(a: str, b: str) -> str:
|
|
171
|
+
"""Return the more restrictive (narrower visibility) scope."""
|
|
172
|
+
order = {"branch": 2, "project": 1, "org": 0}
|
|
173
|
+
return a if order.get(a, 1) >= order.get(b, 1) else b
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _now_iso() -> str:
|
|
177
|
+
from datetime import datetime, timezone
|
|
178
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.reasoning_plus.learning.theta_learning_bridge
|
|
3
|
+
============================================================
|
|
4
|
+
Bridge between DRPL learning outcomes and the coordination vector Θ.
|
|
5
|
+
|
|
6
|
+
Feeds per-concept success rates into ThetaStore.ripple() so that
|
|
7
|
+
learning outcomes influence the shared coordination vector.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from thinkstack_core.reasoning_plus.learning.store import LearningStore
|
|
16
|
+
from thinkstack_core.theta import make_theta_store
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("thinkstack.reasoning_plus.learning")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sync_learnings_to_theta(
|
|
22
|
+
gcc_dir: Path | str,
|
|
23
|
+
min_samples: int = 3,
|
|
24
|
+
disclosure: str = "PUBLIC",
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Compute per-concept success rates from learnings and feed them into Θ.
|
|
27
|
+
|
|
28
|
+
For each concept with at least *min_samples* learnings, creates a
|
|
29
|
+
sensitivity event with confidence = success_rate and feeds it through
|
|
30
|
+
ThetaStore.ripple(). This means concepts with high success rates
|
|
31
|
+
produce high-confidence events, and failing concepts produce low-confidence
|
|
32
|
+
signals in the coordination vector.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
gcc_dir: Path to the local .GCC/ directory.
|
|
36
|
+
min_samples: Minimum learnings per concept to include.
|
|
37
|
+
disclosure: Disclosure level for generated events.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A dict with keys:
|
|
41
|
+
- concepts_synced: number of concepts fed into theta
|
|
42
|
+
- events_generated: total events generated
|
|
43
|
+
- theta_concepts_after: concept count in theta after sync
|
|
44
|
+
"""
|
|
45
|
+
from thinkstack_core.reasoning_plus.learning.analytics import concept_stats
|
|
46
|
+
|
|
47
|
+
store = LearningStore(gcc_dir)
|
|
48
|
+
stats = concept_stats(store)
|
|
49
|
+
|
|
50
|
+
events: list[dict] = []
|
|
51
|
+
concepts_synced = 0
|
|
52
|
+
|
|
53
|
+
for cname, data in stats.items():
|
|
54
|
+
if cname == "__untyped__":
|
|
55
|
+
continue
|
|
56
|
+
if data["total"] < min_samples:
|
|
57
|
+
continue
|
|
58
|
+
events.append({
|
|
59
|
+
"target_concept": cname,
|
|
60
|
+
"confidence": data["success_rate"],
|
|
61
|
+
"disclosure_level": disclosure,
|
|
62
|
+
"created_at": _now_iso(),
|
|
63
|
+
"source": "learning-analytics",
|
|
64
|
+
})
|
|
65
|
+
concepts_synced += 1
|
|
66
|
+
|
|
67
|
+
if not events:
|
|
68
|
+
return {
|
|
69
|
+
"concepts_synced": 0,
|
|
70
|
+
"events_generated": 0,
|
|
71
|
+
"theta_concepts_after": len(
|
|
72
|
+
make_theta_store(Path(gcc_dir)).load().get("coordination_vector", {})
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
theta_store = make_theta_store(Path(gcc_dir))
|
|
77
|
+
theta_store.ripple(events)
|
|
78
|
+
updated = theta_store.load()
|
|
79
|
+
theta_count = len(updated.get("coordination_vector", {}))
|
|
80
|
+
|
|
81
|
+
logger.info(
|
|
82
|
+
"thinkstack: synced %d concept(s) into Θ (%d events) — theta now has %d concept(s)",
|
|
83
|
+
concepts_synced, len(events), theta_count,
|
|
84
|
+
)
|
|
85
|
+
return {
|
|
86
|
+
"concepts_synced": concepts_synced,
|
|
87
|
+
"events_generated": len(events),
|
|
88
|
+
"theta_concepts_after": theta_count,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _now_iso() -> str:
|
|
93
|
+
from datetime import datetime, timezone
|
|
94
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.reasoning_plus.prompt
|
|
3
|
+
===================================
|
|
4
|
+
Reasoning Plus prompt templates.
|
|
5
|
+
|
|
6
|
+
The default template is task-agnostic: it asks the model to reason step-by-step
|
|
7
|
+
inside <thinking> tags before producing the final answer. The SWE-Bench runner
|
|
8
|
+
uses a patch-specific override via the `task_prompt` parameter.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
DEFAULT_REASONING_DIRECTIVE = """You are a careful reasoning assistant.
|
|
13
|
+
|
|
14
|
+
Before giving your final answer, think step by step inside <thinking>...</thinking> tags.
|
|
15
|
+
Explain your reasoning, the relevant facts, and any trade-offs you considered.
|
|
16
|
+
Then provide the final answer outside the <thinking> block.
|
|
17
|
+
|
|
18
|
+
You MUST output the <thinking> block before the final answer.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PATCH_REASONING_DIRECTIVE = """You are an expert software engineer fixing a GitHub issue in a Python repository.
|
|
23
|
+
|
|
24
|
+
Your job is to produce a single, correct patch in unified diff format that the repository maintainers could apply with `git apply`.
|
|
25
|
+
|
|
26
|
+
Rules:
|
|
27
|
+
- Edit only the files needed to fix the issue.
|
|
28
|
+
- Use exact `git diff` style hunks: `--- a/<path>` and `+++ b/<path>` headers, `@@ -start,len +start,len @@` context lines.
|
|
29
|
+
- Context lines must match the original file exactly (indentation, spacing, content).
|
|
30
|
+
- Do not add line numbers, explanations, or markdown inside the patch.
|
|
31
|
+
- Do not output any text after `</patch>`.
|
|
32
|
+
|
|
33
|
+
Before writing the patch, you MUST think step by step inside <thinking>...</thinking> tags.
|
|
34
|
+
The thinking block should explain your analysis of the issue, the files that need to change, and the fix strategy.
|
|
35
|
+
Then write the patch between <patch>...</patch> tags.
|
|
36
|
+
|
|
37
|
+
You MUST output the <thinking> block before the <patch> block.
|
|
38
|
+
|
|
39
|
+
Example of the required output format:
|
|
40
|
+
|
|
41
|
+
<thinking>
|
|
42
|
+
1. Root cause: the function X does not handle Y because Z.
|
|
43
|
+
2. Files to edit: src/example.py
|
|
44
|
+
3. Fix strategy: add a guard before the call to X.
|
|
45
|
+
</thinking>
|
|
46
|
+
|
|
47
|
+
<patch>
|
|
48
|
+
--- a/src/example.py
|
|
49
|
+
+++ b/src/example.py
|
|
50
|
+
@@ -10,7 +10,7 @@
|
|
51
|
+
def old_function():
|
|
52
|
+
x = 1
|
|
53
|
+
- return x
|
|
54
|
+
+ return x + 1
|
|
55
|
+
|
|
56
|
+
def other_function():
|
|
57
|
+
pass
|
|
58
|
+
</patch>
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_reasoning_plus_system_prompt(
|
|
63
|
+
base_prompt: str,
|
|
64
|
+
smart_context: str = "",
|
|
65
|
+
*,
|
|
66
|
+
require_thinking: bool = True,
|
|
67
|
+
task_prompt: str = "",
|
|
68
|
+
) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Build a Reasoning Plus system prompt.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
base_prompt: the caller's existing system prompt.
|
|
74
|
+
smart_context: optional markdown snippet of related files.
|
|
75
|
+
require_thinking: whether to append the thinking directive.
|
|
76
|
+
task_prompt: optional task-specific directive (e.g. the SWE-Bench patch prompt).
|
|
77
|
+
If empty, the generic task-agnostic directive is used.
|
|
78
|
+
"""
|
|
79
|
+
parts = [base_prompt.strip()]
|
|
80
|
+
|
|
81
|
+
if smart_context:
|
|
82
|
+
parts.append("[ThinkStack smart context]")
|
|
83
|
+
parts.append(smart_context.strip())
|
|
84
|
+
|
|
85
|
+
if require_thinking:
|
|
86
|
+
directive = task_prompt.strip() if task_prompt else DEFAULT_REASONING_DIRECTIVE.strip()
|
|
87
|
+
parts.append("[ThinkStack reasoning directive]")
|
|
88
|
+
parts.append(directive)
|
|
89
|
+
|
|
90
|
+
return "\n\n".join(parts)
|