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,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REP ledger push/pull synchronization.
|
|
3
|
+
|
|
4
|
+
Protocol:
|
|
5
|
+
PUSH: POST /rep/entries body: {entries: [...REPEnvelope dicts...], sender_id: str}
|
|
6
|
+
PULL: GET /rep/entries?since=<index_or_0> → {entries: [...], node_id: str}
|
|
7
|
+
|
|
8
|
+
Sync is one-directional per call (push OR pull). Full sync = push then pull.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import List
|
|
19
|
+
|
|
20
|
+
from devtorch_core.rep import REPEnvelope, REPLedger
|
|
21
|
+
from devtorch_core.rep_network.node import NodeRegistry, PeerEntry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SyncResult:
|
|
26
|
+
"""Result of a push/pull sync operation with a single peer."""
|
|
27
|
+
peer_node_id: str
|
|
28
|
+
peer_url: str
|
|
29
|
+
pushed_count: int = 0
|
|
30
|
+
pulled_count: int = 0
|
|
31
|
+
errors: List[str] = field(default_factory=list)
|
|
32
|
+
success: bool = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Internal helpers
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _timestamp() -> str:
|
|
40
|
+
return datetime.now(timezone.utc).isoformat()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_entries_from(ledger: REPLedger, since_index: int) -> List[dict]:
|
|
44
|
+
"""Return ledger records (as dicts) with sequence_id > since_index."""
|
|
45
|
+
all_records = ledger.read_all()
|
|
46
|
+
return [r for r in all_records if r.get("sequence_id", 0) > since_index]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pulled_ids_path(ledger: REPLedger):
|
|
50
|
+
return ledger.rep_dir / "pulled_ids.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _envelope_ids_in_ledger(ledger: REPLedger) -> set:
|
|
54
|
+
"""Return set of envelope_ids already pulled (persisted sidecar + ledger records)."""
|
|
55
|
+
ids: set = set()
|
|
56
|
+
# Read from sidecar file (persists across calls)
|
|
57
|
+
p = _pulled_ids_path(ledger)
|
|
58
|
+
if p.exists():
|
|
59
|
+
try:
|
|
60
|
+
ids = set(json.loads(p.read_text(encoding="utf-8")))
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
# Also check envelope.payload_hash in stored records for additional dedup
|
|
64
|
+
for record in ledger.read_all():
|
|
65
|
+
env = record.get("envelope", {})
|
|
66
|
+
eid = env.get("envelope_id")
|
|
67
|
+
if eid:
|
|
68
|
+
ids.add(eid)
|
|
69
|
+
return ids
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _persist_pulled_id(ledger: REPLedger, eid: str) -> None:
|
|
73
|
+
"""Persist an envelope_id to the sidecar so future pulls skip it."""
|
|
74
|
+
p = _pulled_ids_path(ledger)
|
|
75
|
+
try:
|
|
76
|
+
existing: set = set(json.loads(p.read_text(encoding="utf-8"))) if p.exists() else set()
|
|
77
|
+
existing.add(eid)
|
|
78
|
+
p.write_text(json.dumps(list(existing)), encoding="utf-8")
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _post_json(url: str, payload: dict, timeout: int = 10) -> dict:
|
|
84
|
+
data = json.dumps(payload).encode("utf-8")
|
|
85
|
+
req = urllib.request.Request(
|
|
86
|
+
url,
|
|
87
|
+
data=data,
|
|
88
|
+
headers={"Content-Type": "application/json"},
|
|
89
|
+
method="POST",
|
|
90
|
+
)
|
|
91
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
92
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_json(url: str, timeout: int = 10) -> dict:
|
|
96
|
+
req = urllib.request.Request(url, method="GET")
|
|
97
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
98
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Public API
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def push_to_peer(
|
|
106
|
+
ledger: REPLedger,
|
|
107
|
+
peer: PeerEntry,
|
|
108
|
+
since_index: int = 0,
|
|
109
|
+
) -> SyncResult:
|
|
110
|
+
"""POST entries from ledger to peer. Never raises."""
|
|
111
|
+
result = SyncResult(peer_node_id=peer.node_id, peer_url=peer.url)
|
|
112
|
+
try:
|
|
113
|
+
records = _read_entries_from(ledger, since_index)
|
|
114
|
+
entries = [r.get("envelope", {}) for r in records]
|
|
115
|
+
if not entries:
|
|
116
|
+
result.pushed_count = 0
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
identity_path = ledger.rep_dir / "node_id"
|
|
120
|
+
sender_id = identity_path.read_text().strip() if identity_path.exists() else ""
|
|
121
|
+
|
|
122
|
+
url = f"{peer.url.rstrip('/')}/rep/entries"
|
|
123
|
+
_post_json(url, {"entries": entries, "sender_id": sender_id})
|
|
124
|
+
result.pushed_count = len(entries)
|
|
125
|
+
except Exception as exc: # noqa: BLE001
|
|
126
|
+
result.success = False
|
|
127
|
+
result.errors.append(str(exc))
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pull_from_peer(
|
|
132
|
+
ledger: REPLedger,
|
|
133
|
+
peer: PeerEntry,
|
|
134
|
+
since_index: int = 0,
|
|
135
|
+
) -> SyncResult:
|
|
136
|
+
"""GET entries from peer and append new ones to ledger. Never raises."""
|
|
137
|
+
result = SyncResult(peer_node_id=peer.node_id, peer_url=peer.url)
|
|
138
|
+
try:
|
|
139
|
+
url = f"{peer.url.rstrip('/')}/rep/entries?since={since_index}"
|
|
140
|
+
response = _get_json(url)
|
|
141
|
+
entries = response.get("entries", [])
|
|
142
|
+
if not entries:
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
existing_ids = _envelope_ids_in_ledger(ledger)
|
|
146
|
+
pulled = 0
|
|
147
|
+
for entry_dict in entries:
|
|
148
|
+
eid = entry_dict.get("envelope_id")
|
|
149
|
+
if eid and eid in existing_ids:
|
|
150
|
+
continue
|
|
151
|
+
# Build envelope from the dict; tolerate extra keys
|
|
152
|
+
try:
|
|
153
|
+
envelope = REPEnvelope.from_dict(entry_dict)
|
|
154
|
+
except Exception: # noqa: BLE001
|
|
155
|
+
continue
|
|
156
|
+
ts = entry_dict.get("timestamp", _timestamp())
|
|
157
|
+
ledger.append(envelope, timestamp=ts)
|
|
158
|
+
if eid:
|
|
159
|
+
existing_ids.add(eid)
|
|
160
|
+
_persist_pulled_id(ledger, eid)
|
|
161
|
+
pulled += 1
|
|
162
|
+
|
|
163
|
+
result.pulled_count = pulled
|
|
164
|
+
except Exception as exc: # noqa: BLE001
|
|
165
|
+
result.success = False
|
|
166
|
+
result.errors.append(str(exc))
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def sync_with_peer(ledger: REPLedger, peer: PeerEntry) -> SyncResult:
|
|
171
|
+
"""Push then pull with a single peer; merge into one SyncResult."""
|
|
172
|
+
push_result = push_to_peer(ledger, peer)
|
|
173
|
+
pull_result = pull_from_peer(ledger, peer)
|
|
174
|
+
|
|
175
|
+
merged = SyncResult(
|
|
176
|
+
peer_node_id=peer.node_id,
|
|
177
|
+
peer_url=peer.url,
|
|
178
|
+
pushed_count=push_result.pushed_count,
|
|
179
|
+
pulled_count=pull_result.pulled_count,
|
|
180
|
+
errors=push_result.errors + pull_result.errors,
|
|
181
|
+
success=push_result.success and pull_result.success,
|
|
182
|
+
)
|
|
183
|
+
return merged
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def sync_all_peers(
|
|
187
|
+
ledger: REPLedger,
|
|
188
|
+
registry: NodeRegistry,
|
|
189
|
+
) -> List[SyncResult]:
|
|
190
|
+
"""Sync with every peer in registry. Never raises."""
|
|
191
|
+
results: List[SyncResult] = []
|
|
192
|
+
try:
|
|
193
|
+
peers = registry.list_peers()
|
|
194
|
+
except Exception: # noqa: BLE001
|
|
195
|
+
return results
|
|
196
|
+
|
|
197
|
+
for peer in peers:
|
|
198
|
+
try:
|
|
199
|
+
results.append(sync_with_peer(ledger, peer))
|
|
200
|
+
except Exception as exc: # noqa: BLE001
|
|
201
|
+
results.append(SyncResult(
|
|
202
|
+
peer_node_id=peer.node_id,
|
|
203
|
+
peer_url=peer.url,
|
|
204
|
+
success=False,
|
|
205
|
+
errors=[str(exc)],
|
|
206
|
+
))
|
|
207
|
+
return results
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint 3 – Sensitivity schema and storage.
|
|
3
|
+
|
|
4
|
+
SensitivityEvent captures a single sensitivity observation emitted by a
|
|
5
|
+
reasoning node, including an optional numerical value, confidence, and a
|
|
6
|
+
disclosure level (PUBLIC / PROTECTED / PRIVATE).
|
|
7
|
+
|
|
8
|
+
SensitivityStore persists events as an append-only JSONL file under
|
|
9
|
+
`.GCC/sensitivities/events.jsonl` and maintains a concept-keyed index at
|
|
10
|
+
`.GCC/sensitivities/index.json` so callers can filter without full scans.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses
|
|
16
|
+
import datetime as _dt
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DISCLOSURE_LEVELS = ("PUBLIC", "PROTECTED", "PRIVATE")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass
|
|
27
|
+
class SensitivityEvent:
|
|
28
|
+
"""
|
|
29
|
+
A single sensitivity observation.
|
|
30
|
+
|
|
31
|
+
Fields
|
|
32
|
+
------
|
|
33
|
+
source_node : Identifier of the reasoning node that emitted this event.
|
|
34
|
+
target_concept : The concept/variable whose sensitivity is being reported.
|
|
35
|
+
counterfactuals : Free-text description of the counterfactual question.
|
|
36
|
+
confidence : Confidence in the sensitivity estimate; float in [0, 1].
|
|
37
|
+
disclosure_level : One of PUBLIC, PROTECTED, PRIVATE.
|
|
38
|
+
numerical_value : Optional point-estimate of the sensitivity magnitude.
|
|
39
|
+
uncertainty : Optional ±uncertainty for numerical_value.
|
|
40
|
+
message : Human-readable annotation.
|
|
41
|
+
event_id : UUID assigned on creation.
|
|
42
|
+
created_at : ISO-8601 UTC timestamp.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
source_node: str
|
|
46
|
+
target_concept: str
|
|
47
|
+
counterfactuals: str
|
|
48
|
+
confidence: float
|
|
49
|
+
disclosure_level: str
|
|
50
|
+
numerical_value: Optional[float] = None
|
|
51
|
+
uncertainty: Optional[float] = None
|
|
52
|
+
message: str = ""
|
|
53
|
+
event_id: str = dataclasses.field(
|
|
54
|
+
default_factory=lambda: str(uuid.uuid4())
|
|
55
|
+
)
|
|
56
|
+
created_at: str = dataclasses.field(
|
|
57
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
58
|
+
)
|
|
59
|
+
agent_id: Optional[str] = None
|
|
60
|
+
session_id: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if self.disclosure_level not in DISCLOSURE_LEVELS:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"disclosure_level must be one of {DISCLOSURE_LEVELS}, "
|
|
66
|
+
f"got '{self.disclosure_level}'"
|
|
67
|
+
)
|
|
68
|
+
if not (0.0 <= self.confidence <= 1.0):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"confidence must be in [0, 1], got {self.confidence}"
|
|
71
|
+
)
|
|
72
|
+
if not self.source_node.strip():
|
|
73
|
+
raise ValueError("source_node must not be empty.")
|
|
74
|
+
if not self.target_concept.strip():
|
|
75
|
+
raise ValueError("target_concept must not be empty.")
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict:
|
|
78
|
+
return dataclasses.asdict(self)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, d: dict) -> "SensitivityEvent":
|
|
82
|
+
known = {f.name for f in dataclasses.fields(cls)}
|
|
83
|
+
return cls(**{k: v for k, v in d.items() if k in known})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SensitivityStore:
|
|
87
|
+
"""
|
|
88
|
+
Append-only storage for SensitivityEvent objects.
|
|
89
|
+
|
|
90
|
+
Layout (relative to the `.GCC/sensitivities/` directory):
|
|
91
|
+
events.jsonl – one JSON object per line, ordered by insertion.
|
|
92
|
+
index.json – concept_name → [event_id, …], rebuilt on every write.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
EVENTS_FILE = "events.jsonl"
|
|
96
|
+
INDEX_FILE = "index.json"
|
|
97
|
+
|
|
98
|
+
def __init__(self, sensitivities_dir: Path) -> None:
|
|
99
|
+
self.dir = sensitivities_dir
|
|
100
|
+
|
|
101
|
+
def _ensure_dir(self) -> None:
|
|
102
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def events_path(self) -> Path:
|
|
106
|
+
return self.dir / self.EVENTS_FILE
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def index_path(self) -> Path:
|
|
110
|
+
return self.dir / self.INDEX_FILE
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Internal index helpers
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def _load_index(self) -> dict:
|
|
117
|
+
if not self.index_path.exists():
|
|
118
|
+
return {}
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(self.index_path.read_text(encoding="utf-8"))
|
|
121
|
+
except (json.JSONDecodeError, OSError):
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
def _save_index(self, index: dict) -> None:
|
|
125
|
+
self.index_path.write_text(
|
|
126
|
+
json.dumps(index, indent=2, sort_keys=True) + "\n",
|
|
127
|
+
encoding="utf-8",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Public API
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def add(self, event: SensitivityEvent) -> str:
|
|
135
|
+
"""Persist *event* and update the concept index. Returns event_id."""
|
|
136
|
+
self._ensure_dir()
|
|
137
|
+
with self.events_path.open("a", encoding="utf-8") as fh:
|
|
138
|
+
fh.write(json.dumps(event.to_dict(), sort_keys=True) + "\n")
|
|
139
|
+
|
|
140
|
+
index = self._load_index()
|
|
141
|
+
index.setdefault(event.target_concept, []).append(event.event_id)
|
|
142
|
+
self._save_index(index)
|
|
143
|
+
return event.event_id
|
|
144
|
+
|
|
145
|
+
def list_all(self) -> List[SensitivityEvent]:
|
|
146
|
+
"""Return all stored events in insertion order."""
|
|
147
|
+
if not self.events_path.exists():
|
|
148
|
+
return []
|
|
149
|
+
events: List[SensitivityEvent] = []
|
|
150
|
+
with self.events_path.open("r", encoding="utf-8") as fh:
|
|
151
|
+
for raw in fh:
|
|
152
|
+
line = raw.strip()
|
|
153
|
+
if not line:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
events.append(SensitivityEvent.from_dict(json.loads(line)))
|
|
157
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
158
|
+
pass
|
|
159
|
+
return events
|
|
160
|
+
|
|
161
|
+
def filter(
|
|
162
|
+
self,
|
|
163
|
+
concept: Optional[str] = None,
|
|
164
|
+
disclosure_level: Optional[str] = None,
|
|
165
|
+
source_node: Optional[str] = None,
|
|
166
|
+
) -> List[SensitivityEvent]:
|
|
167
|
+
"""
|
|
168
|
+
Return events that match *all* provided filter criteria.
|
|
169
|
+
Any criterion that is None is treated as a wildcard.
|
|
170
|
+
"""
|
|
171
|
+
events = self.list_all()
|
|
172
|
+
if concept is not None:
|
|
173
|
+
events = [e for e in events if e.target_concept == concept]
|
|
174
|
+
if disclosure_level is not None:
|
|
175
|
+
events = [e for e in events if e.disclosure_level == disclosure_level]
|
|
176
|
+
if source_node is not None:
|
|
177
|
+
events = [e for e in events if e.source_node == source_node]
|
|
178
|
+
return events
|
|
179
|
+
|
|
180
|
+
def concepts(self) -> List[str]:
|
|
181
|
+
"""Return all distinct concept names seen so far."""
|
|
182
|
+
return sorted(self._load_index().keys())
|
devtorch_core/serve.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint 25 — Unified local dev server: `devtorch serve`.
|
|
3
|
+
|
|
4
|
+
Combines the existing FastAPI apps into a single local server:
|
|
5
|
+
- Proxy (mounted at /proxy)
|
|
6
|
+
- Dashboard (mounted at /dashboard)
|
|
7
|
+
- Broadcast (mounted at /broadcast)
|
|
8
|
+
- MCP (spawned as a stdio subprocess)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from devtorch_core.serve import serve_all
|
|
12
|
+
serve_all(host="127.0.0.1", port=8765)
|
|
13
|
+
|
|
14
|
+
Or via the CLI helper (intended for integration in devtorch_cli/main.py):
|
|
15
|
+
from devtorch_core.serve import cmd_serve
|
|
16
|
+
cmd_serve(args)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
from devtorch_core.broadcast.broadcaster import create_broadcast_app, EventBroadcaster
|
|
27
|
+
from devtorch_core.dashboard_api import create_app as create_dashboard_app
|
|
28
|
+
from devtorch_core.proxy.server import create_app as create_proxy_app
|
|
29
|
+
from devtorch_core.proxy.server import find_gcc_repo as proxy_find_gcc_repo
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from fastapi import FastAPI
|
|
33
|
+
import uvicorn
|
|
34
|
+
|
|
35
|
+
_HAS_FASTAPI = True
|
|
36
|
+
except ImportError: # pragma: no cover
|
|
37
|
+
_HAS_FASTAPI = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
41
|
+
DEFAULT_PORT = 8765
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# GCC repository resolution
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_gcc_repo(gcc_path: Optional[str] = None) -> Optional[Any]:
|
|
50
|
+
"""
|
|
51
|
+
Resolve an initialised GCCRepository.
|
|
52
|
+
|
|
53
|
+
Search order:
|
|
54
|
+
1. Explicit `gcc_path` argument.
|
|
55
|
+
2. DEVTORCH_GCC_PATH environment variable.
|
|
56
|
+
3. Walk upward from cwd (proxy server discovery).
|
|
57
|
+
"""
|
|
58
|
+
from devtorch_core import GCCRepository
|
|
59
|
+
|
|
60
|
+
candidates: list[str] = []
|
|
61
|
+
if gcc_path is not None:
|
|
62
|
+
candidates.append(str(gcc_path))
|
|
63
|
+
explicit = os.environ.get("DEVTORCH_GCC_PATH", "").strip()
|
|
64
|
+
if explicit:
|
|
65
|
+
candidates.append(explicit)
|
|
66
|
+
|
|
67
|
+
for candidate in candidates:
|
|
68
|
+
try:
|
|
69
|
+
repo = GCCRepository.at(Path(candidate))
|
|
70
|
+
if repo.is_initialized():
|
|
71
|
+
return repo
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
return proxy_find_gcc_repo()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _repo_path(repo: Optional[Any]) -> Optional[Path]:
|
|
79
|
+
"""Best-effort extraction of a Path from a GCCRepository-like object."""
|
|
80
|
+
if repo is None:
|
|
81
|
+
return None
|
|
82
|
+
if hasattr(repo, "root"):
|
|
83
|
+
return Path(repo.root)
|
|
84
|
+
if hasattr(repo, "path"):
|
|
85
|
+
return Path(repo.path)
|
|
86
|
+
return Path(repo)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# App factory
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_app(
|
|
95
|
+
gcc_repo: Optional[Any] = None,
|
|
96
|
+
broadcaster: Optional[EventBroadcaster] = None,
|
|
97
|
+
mcp_process: Optional[subprocess.Popen] = None,
|
|
98
|
+
) -> "FastAPI":
|
|
99
|
+
"""
|
|
100
|
+
Build a single FastAPI app combining proxy, dashboard, and broadcast.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
gcc_repo:
|
|
105
|
+
An initialised GCCRepository used by the proxy and dashboard.
|
|
106
|
+
broadcaster:
|
|
107
|
+
Optional EventBroadcaster instance; a fresh one is created if None.
|
|
108
|
+
mcp_process:
|
|
109
|
+
Optional subprocess.Popen handle for the stdio MCP server. If provided,
|
|
110
|
+
the unified /health endpoint reports its runtime status.
|
|
111
|
+
"""
|
|
112
|
+
if not _HAS_FASTAPI:
|
|
113
|
+
raise ImportError(
|
|
114
|
+
"FastAPI and uvicorn are required for the unified server. "
|
|
115
|
+
"Install them with: pip install fastapi uvicorn"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
app = FastAPI(
|
|
119
|
+
title="DevTorch Unified Local Server",
|
|
120
|
+
version="0.1.0",
|
|
121
|
+
description=(
|
|
122
|
+
"Local dev server that unifies the DevTorch proxy, dashboard API, "
|
|
123
|
+
"SSE broadcaster, and MCP server into a single process."
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Proxy app (mounted at /proxy)
|
|
128
|
+
proxy_app = create_proxy_app(gcc_repo)
|
|
129
|
+
app.mount("/proxy", proxy_app)
|
|
130
|
+
|
|
131
|
+
# Dashboard app (mounted at /dashboard)
|
|
132
|
+
dashboard_path = _repo_path(gcc_repo)
|
|
133
|
+
dashboard_app = create_dashboard_app(dashboard_path)
|
|
134
|
+
app.mount("/dashboard", dashboard_app)
|
|
135
|
+
|
|
136
|
+
# Broadcast app (mounted at /broadcast)
|
|
137
|
+
broadcast_broadcaster = broadcaster or EventBroadcaster()
|
|
138
|
+
broadcast_app = create_broadcast_app(broadcast_broadcaster)
|
|
139
|
+
if broadcast_app is not None:
|
|
140
|
+
app.mount("/broadcast", broadcast_app)
|
|
141
|
+
|
|
142
|
+
# Keep the MCP subprocess handle accessible to the health endpoint.
|
|
143
|
+
if mcp_process is not None:
|
|
144
|
+
app.state.mcp_process = mcp_process
|
|
145
|
+
|
|
146
|
+
@app.get("/health")
|
|
147
|
+
def health() -> dict:
|
|
148
|
+
"""Aggregate health check for all mounted sub-services."""
|
|
149
|
+
mcp_status = "not_started"
|
|
150
|
+
mcp_proc = getattr(app.state, "mcp_process", None)
|
|
151
|
+
if mcp_proc is not None:
|
|
152
|
+
try:
|
|
153
|
+
mcp_status = "running" if mcp_proc.poll() is None else "stopped"
|
|
154
|
+
except Exception:
|
|
155
|
+
mcp_status = "unknown"
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"status": "ok",
|
|
159
|
+
"proxy": "ok",
|
|
160
|
+
"dashboard": "ok",
|
|
161
|
+
"broadcast": "ok",
|
|
162
|
+
"mcp": mcp_status,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return app
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Server runner
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def serve_all(
|
|
174
|
+
host: str = DEFAULT_HOST,
|
|
175
|
+
port: int = DEFAULT_PORT,
|
|
176
|
+
gcc_path: Optional[str] = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Start the unified FastAPI app and spawn the stdio MCP server as a subprocess.
|
|
180
|
+
|
|
181
|
+
The MCP server is launched with `python -m devtorch_core.mcp.server` using
|
|
182
|
+
the same interpreter and inherits the current environment. If a GCC repo
|
|
183
|
+
is resolved, DEVTORCH_GCC_PATH is set so the MCP server can find it.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
host:
|
|
188
|
+
Host interface to bind (default 127.0.0.1).
|
|
189
|
+
port:
|
|
190
|
+
Port to bind (default 8765).
|
|
191
|
+
gcc_path:
|
|
192
|
+
Optional project root containing an initialised `.GCC/` directory.
|
|
193
|
+
"""
|
|
194
|
+
if not _HAS_FASTAPI:
|
|
195
|
+
raise ImportError(
|
|
196
|
+
"FastAPI and uvicorn are required for the unified server. "
|
|
197
|
+
"Install them with: pip install fastapi uvicorn"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
repo = _resolve_gcc_repo(gcc_path)
|
|
201
|
+
|
|
202
|
+
env = dict(os.environ)
|
|
203
|
+
if repo is not None:
|
|
204
|
+
repo_path = _repo_path(repo)
|
|
205
|
+
if repo_path is not None:
|
|
206
|
+
env["DEVTORCH_GCC_PATH"] = str(repo_path)
|
|
207
|
+
elif gcc_path is not None:
|
|
208
|
+
env["DEVTORCH_GCC_PATH"] = str(gcc_path)
|
|
209
|
+
|
|
210
|
+
mcp_cmd = [sys.executable, "-m", "devtorch_core.mcp.server"]
|
|
211
|
+
mcp_process = subprocess.Popen(
|
|
212
|
+
mcp_cmd,
|
|
213
|
+
stdin=subprocess.PIPE,
|
|
214
|
+
stdout=subprocess.PIPE,
|
|
215
|
+
stderr=subprocess.PIPE,
|
|
216
|
+
env=env,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
app = build_app(gcc_repo=repo, mcp_process=mcp_process)
|
|
220
|
+
|
|
221
|
+
print(f"DevTorch unified server listening on http://{host}:{port}")
|
|
222
|
+
print(f" Proxy: http://{host}:{port}/proxy")
|
|
223
|
+
print(f" Dashboard: http://{host}:{port}/dashboard")
|
|
224
|
+
print(f" Broadcast: http://{host}:{port}/broadcast")
|
|
225
|
+
print(f" MCP (stdio): spawned pid={mcp_process.pid}")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
229
|
+
finally:
|
|
230
|
+
if mcp_process.poll() is None:
|
|
231
|
+
mcp_process.terminate()
|
|
232
|
+
try:
|
|
233
|
+
mcp_process.wait(timeout=5)
|
|
234
|
+
except subprocess.TimeoutExpired:
|
|
235
|
+
mcp_process.kill()
|
|
236
|
+
mcp_process.wait()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# CLI helper (for later integration in devtorch_cli/main.py)
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_serve(args: Any) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Adapter to call `serve_all` from the CLI argument parser.
|
|
247
|
+
|
|
248
|
+
Expected attributes on `args`:
|
|
249
|
+
- host (str)
|
|
250
|
+
- port (int)
|
|
251
|
+
- gcc_path (Optional[str])
|
|
252
|
+
Missing attributes fall back to defaults.
|
|
253
|
+
"""
|
|
254
|
+
host = getattr(args, "host", DEFAULT_HOST)
|
|
255
|
+
port = int(getattr(args, "port", DEFAULT_PORT))
|
|
256
|
+
gcc_path = getattr(args, "gcc_path", None)
|
|
257
|
+
serve_all(host=host, port=port, gcc_path=gcc_path)
|
|
258
|
+
return 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Multi-agent session support: planning, disagreement resolution, and what-if simulation."""
|
|
2
|
+
|
|
3
|
+
from .models import (
|
|
4
|
+
ACTIVE,
|
|
5
|
+
COMPLETED,
|
|
6
|
+
FAILED,
|
|
7
|
+
PENDING,
|
|
8
|
+
PAUSED,
|
|
9
|
+
PLANNING,
|
|
10
|
+
AgentAssignment,
|
|
11
|
+
Session,
|
|
12
|
+
SessionStatus,
|
|
13
|
+
Subtask,
|
|
14
|
+
is_terminal_status,
|
|
15
|
+
)
|
|
16
|
+
from .orchestrator import SessionOrchestrator
|
|
17
|
+
from .planner import SubtaskPlanner
|
|
18
|
+
from .disagreement import DisagreementRecord, DisagreementResolver
|
|
19
|
+
from .simulator import SimulationResult, WhatIfSimulator
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"SessionStatus",
|
|
23
|
+
"PENDING",
|
|
24
|
+
"PLANNING",
|
|
25
|
+
"ACTIVE",
|
|
26
|
+
"PAUSED",
|
|
27
|
+
"COMPLETED",
|
|
28
|
+
"FAILED",
|
|
29
|
+
"AgentAssignment",
|
|
30
|
+
"Subtask",
|
|
31
|
+
"Session",
|
|
32
|
+
"is_terminal_status",
|
|
33
|
+
"SessionOrchestrator",
|
|
34
|
+
"SubtaskPlanner",
|
|
35
|
+
"DisagreementRecord",
|
|
36
|
+
"DisagreementResolver",
|
|
37
|
+
"SimulationResult",
|
|
38
|
+
"WhatIfSimulator",
|
|
39
|
+
]
|