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,1049 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S7 MCP Server — DevTorch primitives exposed via JSON-RPC 2.0 / MCP protocol.
|
|
3
|
+
|
|
4
|
+
Supports two transports:
|
|
5
|
+
- If the `mcp` SDK is installed: uses mcp.server.Server + stdio_server
|
|
6
|
+
- Fallback: raw JSON-RPC 2.0 line-by-line stdio loop
|
|
7
|
+
|
|
8
|
+
Tools exposed:
|
|
9
|
+
devtorch_commit — Commit a reasoning milestone to .GCC/
|
|
10
|
+
devtorch_sensitivity_add — Record a sensitivity signal
|
|
11
|
+
devtorch_context — Retrieve bounded context bundle
|
|
12
|
+
devtorch_theta_read — Read coordination vector Θ
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
# FastAPI / Starlette imports — only needed for SSE transport, but must be at
|
|
23
|
+
# module level so that FastAPI can resolve string annotations (PEP 563) when
|
|
24
|
+
# building the route dependency graph.
|
|
25
|
+
try:
|
|
26
|
+
from fastapi import FastAPI as _FastAPI, Request as _Request, HTTPException as _HTTPException
|
|
27
|
+
from fastapi.responses import StreamingResponse as _StreamingResponse, Response as _Response
|
|
28
|
+
_FASTAPI_AVAILABLE = True
|
|
29
|
+
except ImportError: # pragma: no cover
|
|
30
|
+
_FASTAPI_AVAILABLE = False
|
|
31
|
+
_FastAPI = None # type: ignore[assignment,misc]
|
|
32
|
+
_Request = None # type: ignore[assignment]
|
|
33
|
+
_HTTPException = None # type: ignore[assignment]
|
|
34
|
+
_StreamingResponse = None # type: ignore[assignment]
|
|
35
|
+
_Response = None # type: ignore[assignment]
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Tool definitions (shared between MCP-SDK path and fallback path)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
TOOLS: List[Dict[str, Any]] = [
|
|
42
|
+
{
|
|
43
|
+
"name": "devtorch_commit",
|
|
44
|
+
"description": "Commit a reasoning milestone to the .GCC/ store",
|
|
45
|
+
"inputSchema": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"message": {"type": "string", "description": "Commit message"},
|
|
49
|
+
"branch": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Branch name (optional)",
|
|
52
|
+
},
|
|
53
|
+
"concepts": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Comma-separated concept tags (optional, e.g. auth,security)",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
"required": ["message"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "devtorch_sensitivity_add",
|
|
63
|
+
"description": "Record what this decision depends on (sensitivity signal)",
|
|
64
|
+
"inputSchema": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"concept": {"type": "string", "description": "Target concept"},
|
|
68
|
+
"signal": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Signal / counterfactual text",
|
|
71
|
+
},
|
|
72
|
+
"confidence": {
|
|
73
|
+
"type": "number",
|
|
74
|
+
"description": "Confidence in [0, 1]",
|
|
75
|
+
},
|
|
76
|
+
"disclosure": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Disclosure level: PUBLIC, PROTECTED (default), or PRIVATE",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"required": ["concept", "signal", "confidence"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "devtorch_context",
|
|
86
|
+
"description": "Retrieve bounded context bundle from .GCC/ (use this at session start)",
|
|
87
|
+
"inputSchema": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"k": {
|
|
91
|
+
"type": "integer",
|
|
92
|
+
"description": "Token budget (default 8000)",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "devtorch_theta_read",
|
|
99
|
+
"description": "Read current coordination vector Θ (shared sensitivity state)",
|
|
100
|
+
"inputSchema": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"concepts": {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"items": {"type": "string"},
|
|
106
|
+
"description": "Filter to specific concepts (optional)",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "devtorch_reasoning_query",
|
|
113
|
+
"description": "Query an agent's reasoning about a specific concept (use at session start to understand prior decisions)",
|
|
114
|
+
"inputSchema": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"concept": {"type": "string", "description": "Concept to query (e.g. auth, schema, payments)"},
|
|
118
|
+
"agent_id": {"type": "string", "description": "Filter to a specific agent ID (optional)"},
|
|
119
|
+
"scope": {"type": "string", "description": "branch | session | org (default: branch)"},
|
|
120
|
+
"k": {"type": "integer", "description": "Token budget (default 4000)"},
|
|
121
|
+
},
|
|
122
|
+
"required": ["concept"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "devtorch_divergence_detect",
|
|
127
|
+
"description": "Detect reasoning divergence between agents for a concept",
|
|
128
|
+
"inputSchema": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"concept": {"type": "string"},
|
|
132
|
+
},
|
|
133
|
+
"required": ["concept"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "devtorch_consolidation_status",
|
|
138
|
+
"description": "Get consolidation record status for a concept",
|
|
139
|
+
"inputSchema": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"concept": {"type": "string"},
|
|
143
|
+
},
|
|
144
|
+
"required": ["concept"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "devtorch_hitl_respond",
|
|
149
|
+
"description": "Provide a human or agent resolution to a pending HITL consolidation",
|
|
150
|
+
"inputSchema": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"properties": {
|
|
153
|
+
"record_id": {"type": "string", "description": "ConsolidationRecord ID"},
|
|
154
|
+
"author": {"type": "string", "description": "Responder name or agent_id"},
|
|
155
|
+
"comment": {"type": "string", "description": "Explanation or reasoning"},
|
|
156
|
+
"decision": {"type": "string", "description": "The consolidated decision text"},
|
|
157
|
+
},
|
|
158
|
+
"required": ["record_id", "author", "decision"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "devtorch_reasoning_plus_config",
|
|
163
|
+
"description": "Read current Reasoning Plus settings and their source",
|
|
164
|
+
"inputSchema": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "devtorch_reasoning_plus_set",
|
|
171
|
+
"description": "Update Reasoning Plus settings for this project",
|
|
172
|
+
"inputSchema": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"properties": {
|
|
175
|
+
"enabled": {"type": "boolean", "description": "Enable or disable Reasoning Plus"},
|
|
176
|
+
"smart_top_n": {"type": "integer", "description": "Number of smart-context files to include"},
|
|
177
|
+
"smart_max_lines": {"type": "integer", "description": "Max lines per smart-context file"},
|
|
178
|
+
"context_k": {"type": "integer", "description": "Context token budget (used by benchmark runners)"},
|
|
179
|
+
"require_thinking": {"type": "boolean", "description": "Require <thinking> block in responses"},
|
|
180
|
+
"max_keywords": {"type": "integer", "description": "Max keywords for smart context extraction"},
|
|
181
|
+
"cache_ttl_secs": {"type": "integer", "description": "Smart context cache TTL in seconds"},
|
|
182
|
+
"learning_enabled": {"type": "boolean", "description": "Enable or disable Reasoning Plus Learning"},
|
|
183
|
+
"learning_top_n": {"type": "integer", "description": "Number of learnings to inject"},
|
|
184
|
+
"learning_max_lines": {"type": "integer", "description": "Max lines for the injected learnings block"},
|
|
185
|
+
"learning_embedding_backend": {"type": "string", "description": "Embedding backend for relevance: keyword, sentence_transformers, openai"},
|
|
186
|
+
"learning_embedding_model": {"type": "string", "description": "Model name for sentence_transformers or openai backend"},
|
|
187
|
+
"learning_extraction_strategy": {"type": "string", "description": "Learning extraction strategy: rule or llm"},
|
|
188
|
+
"learning_relevance_cache_ttl": {"type": "integer", "description": "Relevance cache TTL in seconds"},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "devtorch_reasoning_plus_learning_query",
|
|
194
|
+
"description": "Query relevant learnings from the Reasoning Plus Learning store",
|
|
195
|
+
"inputSchema": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"properties": {
|
|
198
|
+
"context": {"type": "string", "description": "Current task or prompt context to match learnings against"},
|
|
199
|
+
"top_n": {"type": "integer", "description": "Maximum number of learnings to return (default: project setting)"},
|
|
200
|
+
"session_id": {"type": "string", "description": "Optional session filter"},
|
|
201
|
+
"state_hash": {"type": "string", "description": "Optional workspace state hash for freshness boost"},
|
|
202
|
+
},
|
|
203
|
+
"required": ["context"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "devtorch_reasoning_plus_learning_purge",
|
|
208
|
+
"description": "Purge stale or deprecated learnings from the Reasoning Plus Learning store",
|
|
209
|
+
"inputSchema": {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"properties": {
|
|
212
|
+
"scope": {"type": "string", "description": "Scope to purge: stale, deprecated, or all"},
|
|
213
|
+
"session_id": {"type": "string", "description": "Optional session filter"},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "devtorch_reasoning_plus_learning_feedback",
|
|
219
|
+
"description": "Apply outcome feedback to a learning (success, failure, or stale)",
|
|
220
|
+
"inputSchema": {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"learning_id": {"type": "string", "description": "Learning ID to update"},
|
|
224
|
+
"outcome": {"type": "string", "description": "Outcome: success, failure, or stale"},
|
|
225
|
+
},
|
|
226
|
+
"required": ["learning_id", "outcome"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "devtorch_topics_list",
|
|
231
|
+
"description": "List all topics with captured reasoning",
|
|
232
|
+
"inputSchema": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"all": {"type": "boolean", "description": "Include topics with no data (default: false)"}
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"name": "devtorch_reasoning_history",
|
|
241
|
+
"description": "Get reasoning history for a topic (expands to all concepts) or a single concept",
|
|
242
|
+
"inputSchema": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"topic": {"type": "string", "description": "Topic name (expands to multiple concepts)"},
|
|
246
|
+
"concept": {"type": "string", "description": "Single concept name (alternative to topic)"},
|
|
247
|
+
"max_entries": {"type": "integer", "description": "Max total entries across all concepts (default: 50)"},
|
|
248
|
+
"agent_id": {"type": "string", "description": "Filter by agent ID"}
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "devtorch_concepts_list",
|
|
254
|
+
"description": "List all concepts from all sources (I3, theta, sensitivity, learnings, commits)",
|
|
255
|
+
"inputSchema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"learned_only": {"type": "boolean", "description": "Only concepts from learning interactions"}
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "devtorch_concepts_related",
|
|
264
|
+
"description": "List concepts related to a given topic",
|
|
265
|
+
"inputSchema": {
|
|
266
|
+
"type": "object",
|
|
267
|
+
"properties": {
|
|
268
|
+
"topic": {"type": "string"}
|
|
269
|
+
},
|
|
270
|
+
"required": ["topic"],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "devtorch_sync_pull",
|
|
275
|
+
"description": "Pull delta from server — returns items the client doesn't have based on sync state",
|
|
276
|
+
"inputSchema": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"properties": {
|
|
279
|
+
"sync_state": {
|
|
280
|
+
"type": "object",
|
|
281
|
+
"description": "Client's current sync state (what it already has)",
|
|
282
|
+
},
|
|
283
|
+
"chunk_size": {
|
|
284
|
+
"type": "integer",
|
|
285
|
+
"description": "Max items per data type in response (default: 500)",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "devtorch_sync_push",
|
|
292
|
+
"description": "Push delta to server — sends items the server doesn't have",
|
|
293
|
+
"inputSchema": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"properties": {
|
|
296
|
+
"bundle": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": "Base64-encoded gzip-compressed delta bundle",
|
|
299
|
+
},
|
|
300
|
+
"batch_id": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Batch identifier for chunking",
|
|
303
|
+
},
|
|
304
|
+
"is_last": {
|
|
305
|
+
"type": "boolean",
|
|
306
|
+
"description": "True if this is the final batch (default: true)",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Tool implementations
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _tool_devtorch_commit(repo: Any, args: dict) -> str:
|
|
319
|
+
message = args.get("message", "")
|
|
320
|
+
concepts_raw = args.get("concepts", "")
|
|
321
|
+
concepts = [c.strip() for c in concepts_raw.split(",") if c.strip()] if concepts_raw else None
|
|
322
|
+
repo.commit(message=message, concepts=concepts)
|
|
323
|
+
parts = [f"Committed to .GCC/"]
|
|
324
|
+
if concepts:
|
|
325
|
+
parts.append(f"[concepts: {', '.join(concepts)}]")
|
|
326
|
+
return " ".join(parts)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _tool_devtorch_sensitivity_add(repo: Any, args: dict) -> str:
|
|
330
|
+
from devtorch_core.sensitivity import SensitivityEvent
|
|
331
|
+
|
|
332
|
+
concept = args["concept"]
|
|
333
|
+
signal = args["signal"]
|
|
334
|
+
confidence = float(args["confidence"])
|
|
335
|
+
disclosure = args.get("disclosure", "PROTECTED").upper()
|
|
336
|
+
if disclosure not in ("PUBLIC", "PROTECTED", "PRIVATE"):
|
|
337
|
+
disclosure = "PROTECTED"
|
|
338
|
+
|
|
339
|
+
ev = SensitivityEvent(
|
|
340
|
+
source_node="mcp-client",
|
|
341
|
+
target_concept=concept,
|
|
342
|
+
counterfactuals=signal,
|
|
343
|
+
confidence=confidence,
|
|
344
|
+
disclosure_level=disclosure,
|
|
345
|
+
message=signal,
|
|
346
|
+
)
|
|
347
|
+
repo.add_sensitivity(ev)
|
|
348
|
+
return f"Sensitivity recorded: {concept} @ {confidence}"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _tool_devtorch_context(repo: Any, args: dict) -> str:
|
|
352
|
+
k = int(args.get("k", 8000))
|
|
353
|
+
bundle = repo.context_bundle(
|
|
354
|
+
k_tokens=k, policy={"sensitivity_policy": "default"}
|
|
355
|
+
)
|
|
356
|
+
# bundle is a dict; format it as a readable string
|
|
357
|
+
artifacts = bundle.get("artifacts", [])
|
|
358
|
+
if not artifacts:
|
|
359
|
+
return f"Context bundle (budget={k} tokens): no artifacts found."
|
|
360
|
+
|
|
361
|
+
lines = [f"Context bundle (budget={k} tokens, {len(artifacts)} artifacts):"]
|
|
362
|
+
for art in artifacts:
|
|
363
|
+
# Support both "name" (test mock) and "path" (real GCCRepository output)
|
|
364
|
+
name = art.get("name", art.get("path", art.get("id", "unknown")))
|
|
365
|
+
reason = art.get("reason", "")
|
|
366
|
+
# Support both "tokens" (test mock) and "approx_tokens" (real GCCRepository output)
|
|
367
|
+
tokens = art.get("tokens", art.get("approx_tokens", "?"))
|
|
368
|
+
lines.append(f" - {name} ({tokens} tokens): {reason}")
|
|
369
|
+
return "\n".join(lines)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _tool_devtorch_theta_read(repo: Any, args: dict) -> str:
|
|
373
|
+
theta = repo.get_theta().get("coordination_vector", {})
|
|
374
|
+
concepts = args.get("concepts")
|
|
375
|
+
if concepts:
|
|
376
|
+
theta = {k: v for k, v in theta.items() if k in concepts}
|
|
377
|
+
return json.dumps(theta, indent=2)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _tool_devtorch_reasoning_query(repo: Any, args: dict) -> str:
|
|
381
|
+
from devtorch_core.reasoning import ReasoningStore
|
|
382
|
+
|
|
383
|
+
concept = args["concept"]
|
|
384
|
+
agent_id = args.get("agent_id")
|
|
385
|
+
scope = args.get("scope", "branch")
|
|
386
|
+
k = int(args.get("k", 4000))
|
|
387
|
+
|
|
388
|
+
store = ReasoningStore(repo)
|
|
389
|
+
entries = store.query(concept=concept, agent_id=agent_id, scope=scope, k_tokens=k)
|
|
390
|
+
|
|
391
|
+
if not entries:
|
|
392
|
+
return f"No reasoning entries found for concept '{concept}'."
|
|
393
|
+
|
|
394
|
+
lines = [f"Reasoning for '{concept}' ({len(entries)} entries, budget={k} tokens):"]
|
|
395
|
+
for e in entries:
|
|
396
|
+
lines.append(
|
|
397
|
+
f"\n [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]"
|
|
398
|
+
)
|
|
399
|
+
lines.append(f" Reasoning: {e.reasoning_text}")
|
|
400
|
+
if e.decision_text:
|
|
401
|
+
lines.append(f" Decision: {e.decision_text}")
|
|
402
|
+
return "\n".join(lines)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _tool_devtorch_divergence_detect(repo: Any, args: dict) -> str:
|
|
406
|
+
from devtorch_core.divergence import DivergenceDetector
|
|
407
|
+
concept = args["concept"]
|
|
408
|
+
signals = DivergenceDetector(repo).detect(concept=concept)
|
|
409
|
+
if not signals:
|
|
410
|
+
return f"No divergence detected for concept '{concept}'."
|
|
411
|
+
lines = [f"Divergence signals for '{concept}' ({len(signals)} signals):"]
|
|
412
|
+
for s in signals:
|
|
413
|
+
lines.append(f" [{s.signal_type}/{s.severity}] {s.description}")
|
|
414
|
+
return "\n".join(lines)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _tool_devtorch_consolidation_status(repo: Any, args: dict) -> str:
|
|
418
|
+
from devtorch_core.consolidation import ConsolidationWorkflow
|
|
419
|
+
concept = args["concept"]
|
|
420
|
+
records = ConsolidationWorkflow(repo).list_for_concept(concept)
|
|
421
|
+
if not records:
|
|
422
|
+
return f"No consolidation records found for concept '{concept}'."
|
|
423
|
+
r = records[0] # most recent
|
|
424
|
+
return (
|
|
425
|
+
f"Consolidation '{r.record_id[:8]}' — concept='{r.concept}' state={r.state}\n"
|
|
426
|
+
f" Decision: {r.decision or '(pending)'}\n"
|
|
427
|
+
f" Trace: {r.decision_trace[:200]}\n"
|
|
428
|
+
f" Agents: {', '.join(r.agents_involved)}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _tool_devtorch_hitl_respond(repo: Any, args: dict) -> str:
|
|
433
|
+
from devtorch_core.consolidation import ConsolidationWorkflow
|
|
434
|
+
record_id = args["record_id"]
|
|
435
|
+
author = args["author"]
|
|
436
|
+
comment = args.get("comment", "")
|
|
437
|
+
decision = args["decision"]
|
|
438
|
+
workflow = ConsolidationWorkflow(repo)
|
|
439
|
+
record = workflow.add_human_comment(
|
|
440
|
+
record_id=record_id,
|
|
441
|
+
author=author,
|
|
442
|
+
comment=comment or decision,
|
|
443
|
+
decision=decision,
|
|
444
|
+
)
|
|
445
|
+
return f"Recorded: consolidation '{record_id[:8]}' resolved by '{author}'. State: {record.state}."
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _tool_devtorch_reasoning_plus_config(repo: Any, args: dict) -> str:
|
|
449
|
+
from devtorch_core.reasoning_plus import SettingsStore
|
|
450
|
+
config, source = SettingsStore(repo.gcc_dir).load()
|
|
451
|
+
return json.dumps(
|
|
452
|
+
{
|
|
453
|
+
"source": source,
|
|
454
|
+
"config": config.to_dict(),
|
|
455
|
+
},
|
|
456
|
+
indent=2,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _tool_devtorch_reasoning_plus_set(repo: Any, args: dict) -> str:
|
|
461
|
+
from devtorch_core.reasoning_plus import ReasoningPlusConfig, SettingsStore
|
|
462
|
+
config, _ = SettingsStore(repo.gcc_dir).load()
|
|
463
|
+
overrides = {k: v for k, v in args.items() if v is not None and k in config.to_dict()}
|
|
464
|
+
config = config.with_values(**overrides)
|
|
465
|
+
SettingsStore(repo.gcc_dir).save_project(config)
|
|
466
|
+
return f"Reasoning Plus settings updated: {json.dumps(config.to_dict(), indent=2)}"
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _tool_devtorch_reasoning_plus_learning_query(repo: Any, args: dict) -> str:
|
|
470
|
+
from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
|
|
471
|
+
config, _ = SettingsStore(repo.gcc_dir).load()
|
|
472
|
+
drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
|
|
473
|
+
context = args["context"]
|
|
474
|
+
top_n = args.get("top_n")
|
|
475
|
+
state_hash = args.get("state_hash")
|
|
476
|
+
learnings = drpl.get_relevant_learnings(context, top_n=top_n, state_hash=state_hash)
|
|
477
|
+
session_id = args.get("session_id")
|
|
478
|
+
if session_id:
|
|
479
|
+
learnings = [l for l in learnings if session_id in l.meta.get("session_id", "") or not session_id]
|
|
480
|
+
result = [
|
|
481
|
+
{
|
|
482
|
+
"id": l.id,
|
|
483
|
+
"type": l.type,
|
|
484
|
+
"content": l.short_form(max_chars=300),
|
|
485
|
+
"confidence": l.confidence,
|
|
486
|
+
"validity": l.validity,
|
|
487
|
+
"state_hash": l.state_hash,
|
|
488
|
+
"trigger_concepts": l.trigger_concepts,
|
|
489
|
+
}
|
|
490
|
+
for l in learnings
|
|
491
|
+
]
|
|
492
|
+
return json.dumps({"count": len(result), "learnings": result}, indent=2)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _tool_devtorch_reasoning_plus_learning_purge(repo: Any, args: dict) -> str:
|
|
496
|
+
from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
|
|
497
|
+
config, _ = SettingsStore(repo.gcc_dir).load()
|
|
498
|
+
drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
|
|
499
|
+
scope = args.get("scope", "stale").lower()
|
|
500
|
+
session_id = args.get("session_id")
|
|
501
|
+
if scope not in ("stale", "deprecated", "all"):
|
|
502
|
+
raise ValueError(f"Invalid purge scope: {scope}")
|
|
503
|
+
target_validity = None if scope == "all" else scope
|
|
504
|
+
learnings = drpl._store.list(validity=target_validity)
|
|
505
|
+
if session_id:
|
|
506
|
+
learnings = [l for l in learnings if any(
|
|
507
|
+
drpl._recorder.get(cid) and drpl._recorder.get(cid).session_id == session_id
|
|
508
|
+
for cid in l.source_reasoning_ids
|
|
509
|
+
)]
|
|
510
|
+
count = 0
|
|
511
|
+
for learning in learnings:
|
|
512
|
+
drpl._store.invalidate(learning.id, reason="purged")
|
|
513
|
+
count += 1
|
|
514
|
+
return f"Purged {count} learning(s) (scope={scope})."
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _tool_devtorch_reasoning_plus_learning_feedback(repo: Any, args: dict) -> str:
|
|
518
|
+
from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
|
|
519
|
+
config, _ = SettingsStore(repo.gcc_dir).load()
|
|
520
|
+
drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
|
|
521
|
+
learning_id = args["learning_id"]
|
|
522
|
+
outcome = args["outcome"]
|
|
523
|
+
updated = drpl.apply_feedback(learning_id, outcome)
|
|
524
|
+
if updated is None:
|
|
525
|
+
return f"Learning {learning_id} not found."
|
|
526
|
+
return json.dumps(
|
|
527
|
+
{
|
|
528
|
+
"id": updated.id,
|
|
529
|
+
"validity": updated.validity,
|
|
530
|
+
"confidence": updated.confidence,
|
|
531
|
+
},
|
|
532
|
+
indent=2,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _tool_devtorch_topics_list(repo: Any, args: dict) -> str:
|
|
537
|
+
from devtorch_core.topics import TopicStore
|
|
538
|
+
show_all = args.get("all", False)
|
|
539
|
+
store = TopicStore(repo.gcc_dir)
|
|
540
|
+
topics = store.list_topics(with_data_only=not show_all)
|
|
541
|
+
if not topics:
|
|
542
|
+
return "No topics found."
|
|
543
|
+
lines = []
|
|
544
|
+
for t in topics:
|
|
545
|
+
tag = f" [{t['source']}]" if t["source"] != "default" else ""
|
|
546
|
+
lines.append(f" {t['name']:<20} ({len(t['concepts'])} concepts: {', '.join(t['concepts'])}){tag}")
|
|
547
|
+
header = "Topics" + (" (full taxonomy)" if show_all else " with captured reasoning") + ":\n"
|
|
548
|
+
return header + "\n".join(lines)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _tool_devtorch_reasoning_history(repo: Any, args: dict) -> str:
|
|
552
|
+
from devtorch_core.reasoning import ReasoningStore
|
|
553
|
+
from devtorch_core.topics import TopicStore
|
|
554
|
+
topic = args.get("topic")
|
|
555
|
+
concept = args.get("concept")
|
|
556
|
+
max_entries = int(args.get("max_entries", 50))
|
|
557
|
+
agent_id = args.get("agent_id")
|
|
558
|
+
|
|
559
|
+
if not topic and not concept:
|
|
560
|
+
return "Provide either 'topic' or 'concept'."
|
|
561
|
+
|
|
562
|
+
store = ReasoningStore(repo)
|
|
563
|
+
|
|
564
|
+
if concept:
|
|
565
|
+
entries = store.query(concept=concept, agent_id=agent_id, k_tokens=4000)
|
|
566
|
+
return _format_reasoning_entries(concept, entries, max_entries)
|
|
567
|
+
|
|
568
|
+
if topic:
|
|
569
|
+
topic_store = TopicStore(repo.gcc_dir)
|
|
570
|
+
concepts = topic_store.get_concepts(topic)
|
|
571
|
+
if not concepts:
|
|
572
|
+
return f"Topic '{topic}' not found."
|
|
573
|
+
all_lines = [f"Reasoning history for topic '{topic}' ({len(concepts)} concepts):\n"]
|
|
574
|
+
total = 0
|
|
575
|
+
for c in concepts:
|
|
576
|
+
entries = store.query(concept=c, agent_id=agent_id, k_tokens=4000)
|
|
577
|
+
if entries:
|
|
578
|
+
all_lines.append(_format_reasoning_entries(c, entries, max_entries - total))
|
|
579
|
+
total += len(entries)
|
|
580
|
+
if total >= max_entries:
|
|
581
|
+
break
|
|
582
|
+
all_lines.append(f"\nTotal: {total} reasoning entries across {len(concepts)} concepts (capped at {max_entries}).")
|
|
583
|
+
return "\n".join(all_lines)
|
|
584
|
+
|
|
585
|
+
return "No results."
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _format_reasoning_entries(label: str, entries: list, max_entries: int) -> str:
|
|
589
|
+
if not entries:
|
|
590
|
+
return f"\n═══ {label} (0 entries) ═══\n No reasoning found."
|
|
591
|
+
shown = entries[:max_entries] if max_entries > 0 else entries
|
|
592
|
+
lines = [f"\n═══ {label} ({len(entries)} entries, showing {len(shown)}) ═══"]
|
|
593
|
+
for e in shown:
|
|
594
|
+
lines.append(f" [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]")
|
|
595
|
+
lines.append(f" Reasoning: {e.reasoning_text}")
|
|
596
|
+
if e.decision_text:
|
|
597
|
+
lines.append(f" Decision: {e.decision_text}")
|
|
598
|
+
return "\n".join(lines)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _tool_devtorch_concepts_list(repo: Any, args: dict) -> str:
|
|
602
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
603
|
+
learned_only = args.get("learned_only", False)
|
|
604
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
605
|
+
if learned_only:
|
|
606
|
+
learned = catalog.enumerate_learned()
|
|
607
|
+
if not learned:
|
|
608
|
+
return "No concepts with learning data found."
|
|
609
|
+
lines = ["Concepts learned from interactions:\n"]
|
|
610
|
+
for cname, stats in sorted(learned.items()):
|
|
611
|
+
lines.append(
|
|
612
|
+
f" {cname:<16} {stats['learning_count']} learnings, "
|
|
613
|
+
f"{stats['active']} active, "
|
|
614
|
+
f"{stats['success_rate']:.0%} success, "
|
|
615
|
+
f"conf={stats['avg_confidence']:.2f}"
|
|
616
|
+
)
|
|
617
|
+
return "\n".join(lines)
|
|
618
|
+
else:
|
|
619
|
+
all_concepts = catalog.enumerate_all()
|
|
620
|
+
if not all_concepts:
|
|
621
|
+
return "No concepts found."
|
|
622
|
+
lines = [f"All concepts ({len(all_concepts)} from multiple sources):\n"]
|
|
623
|
+
for cname, sources in all_concepts.items():
|
|
624
|
+
lines.append(f" {cname:<16} {', '.join(sources)}")
|
|
625
|
+
return "\n".join(lines)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _tool_devtorch_concepts_related(repo: Any, args: dict) -> str:
|
|
629
|
+
from devtorch_core.concept_catalog import ConceptCatalog
|
|
630
|
+
topic = args["topic"]
|
|
631
|
+
catalog = ConceptCatalog(repo.gcc_dir)
|
|
632
|
+
result = catalog.concepts_for_topic(topic, hide_empty=True)
|
|
633
|
+
concepts = result.get("concepts", [])
|
|
634
|
+
taxonomy_only = result.get("taxonomy_only", [])
|
|
635
|
+
if not concepts and not taxonomy_only:
|
|
636
|
+
return f"Topic '{topic}' not found or has no concepts."
|
|
637
|
+
lines = [f"Concepts related to topic '{topic}':\n"]
|
|
638
|
+
for c in concepts:
|
|
639
|
+
parts = [f" {c['name']:<16}"]
|
|
640
|
+
parts.append(f"sources: {', '.join(c['sources'])}")
|
|
641
|
+
if "learning_count" in c:
|
|
642
|
+
parts.append(f"{c['learning_count']} learnings, {c['success_rate']:.0%} success")
|
|
643
|
+
lines.append(" — ".join(parts))
|
|
644
|
+
if taxonomy_only:
|
|
645
|
+
lines.append(f"\n Also in taxonomy (no data): {', '.join(taxonomy_only)}")
|
|
646
|
+
lines.append(f"\n{len(concepts)} concept(s) with data. Use 'devtorch reasoning history --topic {topic}' for full history.")
|
|
647
|
+
return "\n".join(lines)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _tool_devtorch_sync_pull(repo: Any, args: dict) -> str:
|
|
651
|
+
import json as _json
|
|
652
|
+
from devtorch_core.cloud.sync_bundle import build_remote_delta, build_server_state, compress
|
|
653
|
+
client_state = args.get("sync_state", {})
|
|
654
|
+
delta = build_remote_delta(repo.gcc_dir, client_state)
|
|
655
|
+
server_state = build_server_state(repo.gcc_dir)
|
|
656
|
+
compressed = compress({
|
|
657
|
+
"delta": delta,
|
|
658
|
+
"server_state": server_state,
|
|
659
|
+
"counts": {k: len(v) if isinstance(v, list) else len(v.get("coordination_vector", {})) if isinstance(v, dict) else 0 for k, v in delta.items()},
|
|
660
|
+
})
|
|
661
|
+
return _json.dumps({"bundle": compressed, "server_state": server_state})
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _tool_devtorch_sync_push(repo: Any, args: dict) -> str:
|
|
665
|
+
import json as _json
|
|
666
|
+
from devtorch_core.cloud.sync_bundle import decompress, merge_push, build_server_state
|
|
667
|
+
from devtorch_core.cloud.sync_conflicts import ConflictLog
|
|
668
|
+
encoded = args.get("bundle", "")
|
|
669
|
+
if not encoded:
|
|
670
|
+
return _json.dumps({"error": "No bundle provided"})
|
|
671
|
+
delta = decompress(encoded)
|
|
672
|
+
conflict_log = ConflictLog(repo.gcc_dir)
|
|
673
|
+
result = merge_push(repo.gcc_dir, delta, conflict_log)
|
|
674
|
+
server_state = build_server_state(repo.gcc_dir)
|
|
675
|
+
return _json.dumps({
|
|
676
|
+
"counts": result["counts"],
|
|
677
|
+
"conflicts": result["conflicts"],
|
|
678
|
+
"server_state": server_state,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _dispatch_tool(repo: Any, name: str, args: dict) -> str:
|
|
683
|
+
if name == "devtorch_commit":
|
|
684
|
+
return _tool_devtorch_commit(repo, args)
|
|
685
|
+
elif name == "devtorch_sensitivity_add":
|
|
686
|
+
return _tool_devtorch_sensitivity_add(repo, args)
|
|
687
|
+
elif name == "devtorch_context":
|
|
688
|
+
return _tool_devtorch_context(repo, args)
|
|
689
|
+
elif name == "devtorch_theta_read":
|
|
690
|
+
return _tool_devtorch_theta_read(repo, args)
|
|
691
|
+
elif name == "devtorch_reasoning_query":
|
|
692
|
+
return _tool_devtorch_reasoning_query(repo, args)
|
|
693
|
+
elif name == "devtorch_divergence_detect":
|
|
694
|
+
return _tool_devtorch_divergence_detect(repo, args)
|
|
695
|
+
elif name == "devtorch_consolidation_status":
|
|
696
|
+
return _tool_devtorch_consolidation_status(repo, args)
|
|
697
|
+
elif name == "devtorch_hitl_respond":
|
|
698
|
+
return _tool_devtorch_hitl_respond(repo, args)
|
|
699
|
+
elif name == "devtorch_reasoning_plus_config":
|
|
700
|
+
return _tool_devtorch_reasoning_plus_config(repo, args)
|
|
701
|
+
elif name == "devtorch_reasoning_plus_set":
|
|
702
|
+
return _tool_devtorch_reasoning_plus_set(repo, args)
|
|
703
|
+
elif name == "devtorch_reasoning_plus_learning_query":
|
|
704
|
+
return _tool_devtorch_reasoning_plus_learning_query(repo, args)
|
|
705
|
+
elif name == "devtorch_reasoning_plus_learning_purge":
|
|
706
|
+
return _tool_devtorch_reasoning_plus_learning_purge(repo, args)
|
|
707
|
+
elif name == "devtorch_reasoning_plus_learning_feedback":
|
|
708
|
+
return _tool_devtorch_reasoning_plus_learning_feedback(repo, args)
|
|
709
|
+
elif name == "devtorch_topics_list":
|
|
710
|
+
return _tool_devtorch_topics_list(repo, args)
|
|
711
|
+
elif name == "devtorch_reasoning_history":
|
|
712
|
+
return _tool_devtorch_reasoning_history(repo, args)
|
|
713
|
+
elif name == "devtorch_concepts_list":
|
|
714
|
+
return _tool_devtorch_concepts_list(repo, args)
|
|
715
|
+
elif name == "devtorch_concepts_related":
|
|
716
|
+
return _tool_devtorch_concepts_related(repo, args)
|
|
717
|
+
elif name == "devtorch_sync_pull":
|
|
718
|
+
return _tool_devtorch_sync_pull(repo, args)
|
|
719
|
+
elif name == "devtorch_sync_push":
|
|
720
|
+
return _tool_devtorch_sync_push(repo, args)
|
|
721
|
+
else:
|
|
722
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# MCP SDK path
|
|
727
|
+
# ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _run_with_sdk(repo: Any) -> None:
|
|
731
|
+
"""Run using the official mcp Python SDK."""
|
|
732
|
+
import asyncio
|
|
733
|
+
from mcp.server import Server
|
|
734
|
+
from mcp.server.stdio import stdio_server
|
|
735
|
+
import mcp.types as types
|
|
736
|
+
|
|
737
|
+
server = Server("devtorch")
|
|
738
|
+
|
|
739
|
+
@server.list_tools() # type: ignore[misc]
|
|
740
|
+
async def list_tools() -> List[types.Tool]:
|
|
741
|
+
result = []
|
|
742
|
+
for t in TOOLS:
|
|
743
|
+
result.append(
|
|
744
|
+
types.Tool(
|
|
745
|
+
name=t["name"],
|
|
746
|
+
description=t["description"],
|
|
747
|
+
inputSchema=t["inputSchema"],
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
return result
|
|
751
|
+
|
|
752
|
+
@server.call_tool() # type: ignore[misc]
|
|
753
|
+
async def call_tool(
|
|
754
|
+
name: str, arguments: dict
|
|
755
|
+
) -> List[types.TextContent]:
|
|
756
|
+
try:
|
|
757
|
+
text = _dispatch_tool(repo, name, arguments or {})
|
|
758
|
+
except Exception as exc:
|
|
759
|
+
text = f"Error: {exc}"
|
|
760
|
+
return [types.TextContent(type="text", text=text)]
|
|
761
|
+
|
|
762
|
+
async def _run() -> None:
|
|
763
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
764
|
+
await server.run(
|
|
765
|
+
read_stream,
|
|
766
|
+
write_stream,
|
|
767
|
+
server.create_initialization_options(),
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
asyncio.run(_run())
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ---------------------------------------------------------------------------
|
|
774
|
+
# Minimal JSON-RPC 2.0 fallback
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
_JSONRPC_VERSION = "2.0"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _make_response(id: Any, result: Any) -> dict:
|
|
781
|
+
return {"jsonrpc": _JSONRPC_VERSION, "id": id, "result": result}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _make_error(id: Any, code: int, message: str) -> dict:
|
|
785
|
+
return {
|
|
786
|
+
"jsonrpc": _JSONRPC_VERSION,
|
|
787
|
+
"id": id,
|
|
788
|
+
"error": {"code": code, "message": message},
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _handle_request(repo: Any, req: dict) -> Optional[dict]:
|
|
793
|
+
"""Dispatch a single JSON-RPC request and return the response dict (or None for notifications)."""
|
|
794
|
+
req_id = req.get("id")
|
|
795
|
+
method = req.get("method", "")
|
|
796
|
+
params = req.get("params") or {}
|
|
797
|
+
|
|
798
|
+
# Notifications (no id) — process but no response
|
|
799
|
+
is_notification = "id" not in req
|
|
800
|
+
|
|
801
|
+
if method == "initialize":
|
|
802
|
+
result = {
|
|
803
|
+
"protocolVersion": "2024-11-05",
|
|
804
|
+
"capabilities": {"tools": {}},
|
|
805
|
+
"serverInfo": {"name": "devtorch", "version": "0.7.0"},
|
|
806
|
+
}
|
|
807
|
+
if is_notification:
|
|
808
|
+
return None
|
|
809
|
+
return _make_response(req_id, result)
|
|
810
|
+
|
|
811
|
+
elif method == "notifications/initialized":
|
|
812
|
+
return None # no response for notifications
|
|
813
|
+
|
|
814
|
+
elif method == "tools/list":
|
|
815
|
+
result = {"tools": TOOLS}
|
|
816
|
+
if is_notification:
|
|
817
|
+
return None
|
|
818
|
+
return _make_response(req_id, result)
|
|
819
|
+
|
|
820
|
+
elif method == "tools/call":
|
|
821
|
+
name = params.get("name", "")
|
|
822
|
+
arguments = params.get("arguments") or {}
|
|
823
|
+
try:
|
|
824
|
+
text = _dispatch_tool(repo, name, arguments)
|
|
825
|
+
result = {
|
|
826
|
+
"content": [{"type": "text", "text": text}],
|
|
827
|
+
"isError": False,
|
|
828
|
+
}
|
|
829
|
+
except Exception as exc:
|
|
830
|
+
result = {
|
|
831
|
+
"content": [{"type": "text", "text": f"Error: {exc}"}],
|
|
832
|
+
"isError": True,
|
|
833
|
+
}
|
|
834
|
+
if is_notification:
|
|
835
|
+
return None
|
|
836
|
+
return _make_response(req_id, result)
|
|
837
|
+
|
|
838
|
+
elif method == "ping":
|
|
839
|
+
if is_notification:
|
|
840
|
+
return None
|
|
841
|
+
return _make_response(req_id, {})
|
|
842
|
+
|
|
843
|
+
else:
|
|
844
|
+
if is_notification:
|
|
845
|
+
return None
|
|
846
|
+
return _make_error(req_id, -32601, f"Method not found: {method}")
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _run_raw_stdio(repo: Any) -> None:
|
|
850
|
+
"""Minimal JSON-RPC 2.0 stdio loop (fallback when mcp SDK not installed)."""
|
|
851
|
+
for raw_line in sys.stdin:
|
|
852
|
+
raw_line = raw_line.strip()
|
|
853
|
+
if not raw_line:
|
|
854
|
+
continue
|
|
855
|
+
try:
|
|
856
|
+
req = json.loads(raw_line)
|
|
857
|
+
except json.JSONDecodeError as exc:
|
|
858
|
+
resp = _make_error(None, -32700, f"Parse error: {exc}")
|
|
859
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
860
|
+
sys.stdout.flush()
|
|
861
|
+
continue
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
resp = _handle_request(repo, req)
|
|
865
|
+
except Exception as exc:
|
|
866
|
+
resp = _make_error(req.get("id"), -32603, f"Internal error: {exc}")
|
|
867
|
+
|
|
868
|
+
if resp is not None:
|
|
869
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
870
|
+
sys.stdout.flush()
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
# ---------------------------------------------------------------------------
|
|
874
|
+
# Server entry points
|
|
875
|
+
# ---------------------------------------------------------------------------
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def run_server(repo: Any) -> None:
|
|
879
|
+
"""Run the MCP server using the best available transport."""
|
|
880
|
+
try:
|
|
881
|
+
import mcp # noqa: F401
|
|
882
|
+
_run_with_sdk(repo)
|
|
883
|
+
except ImportError:
|
|
884
|
+
_run_raw_stdio(repo)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def find_gcc_repo() -> Optional[Any]:
|
|
888
|
+
"""
|
|
889
|
+
Find an initialised .GCC/ repository.
|
|
890
|
+
|
|
891
|
+
Search order:
|
|
892
|
+
1. DEVTORCH_GCC_PATH env var — explicit project root (used by IDE MCP configs)
|
|
893
|
+
2. Walk upward from cwd
|
|
894
|
+
"""
|
|
895
|
+
import os
|
|
896
|
+
from devtorch_core import GCCRepository
|
|
897
|
+
|
|
898
|
+
explicit = os.environ.get("DEVTORCH_GCC_PATH", "").strip()
|
|
899
|
+
if explicit:
|
|
900
|
+
r = GCCRepository.at(Path(explicit))
|
|
901
|
+
if r.is_initialized():
|
|
902
|
+
return r
|
|
903
|
+
|
|
904
|
+
for parent in [Path.cwd()] + list(Path.cwd().parents):
|
|
905
|
+
r = GCCRepository.at(parent)
|
|
906
|
+
if r.is_initialized():
|
|
907
|
+
return r
|
|
908
|
+
return None
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def main() -> None:
|
|
912
|
+
"""Entry point: devtorch mcp-server"""
|
|
913
|
+
gcc = find_gcc_repo()
|
|
914
|
+
if gcc is None:
|
|
915
|
+
print(
|
|
916
|
+
"error: no .GCC/ found in current directory or parents. "
|
|
917
|
+
"Set DEVTORCH_GCC_PATH=/path/to/project to point to your project root.",
|
|
918
|
+
file=sys.stderr,
|
|
919
|
+
)
|
|
920
|
+
sys.exit(1)
|
|
921
|
+
run_server(gcc)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
# ---------------------------------------------------------------------------
|
|
925
|
+
# SSE transport (cloud MCP server)
|
|
926
|
+
# ---------------------------------------------------------------------------
|
|
927
|
+
|
|
928
|
+
import asyncio as _asyncio
|
|
929
|
+
import uuid as _uuid_mod
|
|
930
|
+
import json as _json_mod
|
|
931
|
+
from typing import Callable as _Callable
|
|
932
|
+
|
|
933
|
+
# Global session registry: session_id → (asyncio.Queue, GCCRepository)
|
|
934
|
+
_session_queues: dict[str, tuple[_asyncio.Queue, Any]] = {}
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def build_sse_app(
|
|
938
|
+
repo_factory: _Callable[[str, str], Any],
|
|
939
|
+
auth_backend: Any = None,
|
|
940
|
+
) -> Any:
|
|
941
|
+
"""Build the FastAPI app for the cloud SSE MCP server."""
|
|
942
|
+
# Use module-level _FastAPI / _Request etc. so FastAPI can resolve the
|
|
943
|
+
# string annotations produced by `from __future__ import annotations`.
|
|
944
|
+
app = _FastAPI(title="DevTorch Cloud MCP Server")
|
|
945
|
+
|
|
946
|
+
if auth_backend is not None:
|
|
947
|
+
from devtorch_core.mcp.auth import APIKeyMiddleware, OAuthMiddleware
|
|
948
|
+
import os
|
|
949
|
+
jwks_urls = [u for u in os.environ.get("DEVTORCH_JWKS_URLS", "").split(",") if u]
|
|
950
|
+
if jwks_urls:
|
|
951
|
+
app.add_middleware(OAuthMiddleware, jwks_urls=jwks_urls)
|
|
952
|
+
app.add_middleware(APIKeyMiddleware, backend=auth_backend)
|
|
953
|
+
|
|
954
|
+
@app.get("/{org_id}/{repo_id}/sse")
|
|
955
|
+
async def sse_endpoint(org_id: str, repo_id: str, request: _Request):
|
|
956
|
+
session_id = str(_uuid_mod.uuid4())
|
|
957
|
+
queue: _asyncio.Queue = _asyncio.Queue()
|
|
958
|
+
repo = repo_factory(org_id, repo_id)
|
|
959
|
+
_session_queues[session_id] = (queue, repo)
|
|
960
|
+
|
|
961
|
+
base_url = str(request.base_url).rstrip("/")
|
|
962
|
+
messages_url = f"{base_url}/{org_id}/{repo_id}/messages?sessionId={session_id}"
|
|
963
|
+
|
|
964
|
+
async def event_stream():
|
|
965
|
+
try:
|
|
966
|
+
yield f"event: endpoint\ndata: {messages_url}\n\n"
|
|
967
|
+
while True:
|
|
968
|
+
try:
|
|
969
|
+
item = await _asyncio.wait_for(queue.get(), timeout=30)
|
|
970
|
+
yield f"event: message\ndata: {_json_mod.dumps(item)}\n\n"
|
|
971
|
+
except _asyncio.TimeoutError:
|
|
972
|
+
yield ": keepalive\n\n"
|
|
973
|
+
finally:
|
|
974
|
+
_session_queues.pop(session_id, None)
|
|
975
|
+
|
|
976
|
+
return _StreamingResponse(
|
|
977
|
+
event_stream(),
|
|
978
|
+
media_type="text/event-stream",
|
|
979
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
@app.post("/{org_id}/{repo_id}/messages")
|
|
983
|
+
async def messages_endpoint(org_id: str, repo_id: str, sessionId: str, request: _Request):
|
|
984
|
+
entry = _session_queues.get(sessionId)
|
|
985
|
+
if entry is None:
|
|
986
|
+
raise _HTTPException(status_code=404, detail="Session not found")
|
|
987
|
+
|
|
988
|
+
queue, repo = entry
|
|
989
|
+
body = await request.json()
|
|
990
|
+
|
|
991
|
+
if not isinstance(body, dict):
|
|
992
|
+
raise _HTTPException(status_code=400, detail="Request body must be a JSON object")
|
|
993
|
+
|
|
994
|
+
try:
|
|
995
|
+
response = _handle_request(repo, body)
|
|
996
|
+
except Exception as exc:
|
|
997
|
+
response = _make_error(body.get("id"), -32603, str(exc))
|
|
998
|
+
|
|
999
|
+
if response is not None:
|
|
1000
|
+
await queue.put(response)
|
|
1001
|
+
|
|
1002
|
+
return _Response(status_code=202)
|
|
1003
|
+
|
|
1004
|
+
@app.get("/health")
|
|
1005
|
+
async def health():
|
|
1006
|
+
return {"status": "ok"}
|
|
1007
|
+
|
|
1008
|
+
@app.get("/readyz")
|
|
1009
|
+
async def readyz():
|
|
1010
|
+
return {"status": "ready"}
|
|
1011
|
+
|
|
1012
|
+
@app.post("/github/webhook")
|
|
1013
|
+
async def github_webhook(request: _Request) -> _Response:
|
|
1014
|
+
import os
|
|
1015
|
+
secret = os.environ.get("GITHUB_APP_WEBHOOK_SECRET", "").encode()
|
|
1016
|
+
if not secret:
|
|
1017
|
+
return _Response(status_code=501, content=b"GitHub App not configured")
|
|
1018
|
+
|
|
1019
|
+
body = await request.body()
|
|
1020
|
+
signature = request.headers.get("X-Hub-Signature-256", "")
|
|
1021
|
+
event_type = request.headers.get("X-GitHub-Event", "")
|
|
1022
|
+
|
|
1023
|
+
if auth_backend is None:
|
|
1024
|
+
return _Response(status_code=503, content=b"No auth backend configured")
|
|
1025
|
+
|
|
1026
|
+
from devtorch_core.github.app import GitHubAppHandler
|
|
1027
|
+
handler = GitHubAppHandler(backend=auth_backend, webhook_secret=secret)
|
|
1028
|
+
result = handler.handle_webhook(body=body, signature=signature, event_type=event_type)
|
|
1029
|
+
|
|
1030
|
+
if result["status"] == "forbidden":
|
|
1031
|
+
return _Response(status_code=403, content=b"Forbidden")
|
|
1032
|
+
return _Response(
|
|
1033
|
+
status_code=200,
|
|
1034
|
+
content=json.dumps(result).encode(),
|
|
1035
|
+
media_type="application/json",
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
return app
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _run_with_sse(
|
|
1042
|
+
repo_factory: _Callable[[str, str], Any],
|
|
1043
|
+
auth_backend: Any = None,
|
|
1044
|
+
host: str = "0.0.0.0",
|
|
1045
|
+
port: int = 8080,
|
|
1046
|
+
) -> None:
|
|
1047
|
+
import uvicorn
|
|
1048
|
+
app = build_sse_app(repo_factory=repo_factory, auth_backend=auth_backend)
|
|
1049
|
+
uvicorn.run(app, host=host, port=port)
|