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,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_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 devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
16
|
+
from devtorch_core.theta import make_theta_store
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("devtorch.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 devtorch_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
|
+
"devtorch: 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
|
+
devtorch_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("[DevTorch 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("[DevTorch reasoning directive]")
|
|
88
|
+
parts.append(directive)
|
|
89
|
+
|
|
90
|
+
return "\n\n".join(parts)
|
devtorch_core/rep.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint 5 – REP (Reasoning Exchange Protocol) envelope schema and local transport.
|
|
3
|
+
|
|
4
|
+
Message envelope: agent_id, round, trust, payload hashes. Optional signatures.
|
|
5
|
+
Local transport: file-based ledger with deterministic ordering for record/replay.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
REP_VERSION = "rep-v1"
|
|
17
|
+
REP_LEDGER_NAME = "rep_ledger.jsonl"
|
|
18
|
+
REP_DIR_NAME = "rep"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _hash_payload(payload: Any) -> str:
|
|
22
|
+
"""SHA-256 hex digest of JSON-serialized payload."""
|
|
23
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class REPEnvelope:
|
|
28
|
+
"""REP message envelope for sensitivity exchange."""
|
|
29
|
+
agent_id: str
|
|
30
|
+
round_id: int
|
|
31
|
+
trust: float # [0, 1] optional trust score
|
|
32
|
+
payload_hash: str
|
|
33
|
+
payload_type: str = "sensitivity" # sensitivity | control | ack
|
|
34
|
+
payload_ref: Optional[str] = None # optional pointer to full payload
|
|
35
|
+
signature: Optional[str] = None # optional crypto signature
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
d = {
|
|
39
|
+
"version": REP_VERSION,
|
|
40
|
+
"agent_id": self.agent_id,
|
|
41
|
+
"round_id": self.round_id,
|
|
42
|
+
"trust": self.trust,
|
|
43
|
+
"payload_hash": self.payload_hash,
|
|
44
|
+
"payload_type": self.payload_type,
|
|
45
|
+
}
|
|
46
|
+
if self.payload_ref is not None:
|
|
47
|
+
d["payload_ref"] = self.payload_ref
|
|
48
|
+
if self.signature is not None:
|
|
49
|
+
d["signature"] = self.signature
|
|
50
|
+
return d
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, d: dict) -> "REPEnvelope":
|
|
54
|
+
return cls(
|
|
55
|
+
agent_id=str(d["agent_id"]),
|
|
56
|
+
round_id=int(d["round_id"]),
|
|
57
|
+
trust=float(d.get("trust", 0.5)),
|
|
58
|
+
payload_hash=str(d["payload_hash"]),
|
|
59
|
+
payload_type=str(d.get("payload_type", "sensitivity")),
|
|
60
|
+
payload_ref=d.get("payload_ref"),
|
|
61
|
+
signature=d.get("signature"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class REPLedger:
|
|
66
|
+
"""
|
|
67
|
+
Append-only REP ledger for local transport. Deterministic ordering by
|
|
68
|
+
sequence id and timestamp for record/replay.
|
|
69
|
+
"""
|
|
70
|
+
def __init__(self, gcc_dir: Path) -> None:
|
|
71
|
+
self.rep_dir = gcc_dir / REP_DIR_NAME
|
|
72
|
+
self.ledger_path = self.rep_dir / REP_LEDGER_NAME
|
|
73
|
+
|
|
74
|
+
def ensure_dir(self) -> None:
|
|
75
|
+
self.rep_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
def append(self, envelope: REPEnvelope, timestamp: str, sequence_id: Optional[int] = None) -> int:
|
|
78
|
+
"""Append envelope to ledger; return sequence id (1-based)."""
|
|
79
|
+
self.ensure_dir()
|
|
80
|
+
if sequence_id is None:
|
|
81
|
+
sequence_id = self._next_sequence_id()
|
|
82
|
+
record = {
|
|
83
|
+
"sequence_id": sequence_id,
|
|
84
|
+
"timestamp": timestamp,
|
|
85
|
+
"envelope": envelope.to_dict(),
|
|
86
|
+
}
|
|
87
|
+
with self.ledger_path.open("a", encoding="utf-8") as f:
|
|
88
|
+
f.write(json.dumps(record, sort_keys=True) + "\n")
|
|
89
|
+
return sequence_id
|
|
90
|
+
|
|
91
|
+
def _next_sequence_id(self) -> int:
|
|
92
|
+
if not self.ledger_path.exists():
|
|
93
|
+
return 1
|
|
94
|
+
last = 0
|
|
95
|
+
with self.ledger_path.open("r", encoding="utf-8") as f:
|
|
96
|
+
for line in f:
|
|
97
|
+
line = line.strip()
|
|
98
|
+
if not line:
|
|
99
|
+
continue
|
|
100
|
+
try:
|
|
101
|
+
rec = json.loads(line)
|
|
102
|
+
last = max(last, rec.get("sequence_id", 0))
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
continue
|
|
105
|
+
return last + 1
|
|
106
|
+
|
|
107
|
+
def read_all(self) -> List[dict]:
|
|
108
|
+
"""Read all records in deterministic order (sequence_id, timestamp)."""
|
|
109
|
+
if not self.ledger_path.exists():
|
|
110
|
+
return []
|
|
111
|
+
records = []
|
|
112
|
+
with self.ledger_path.open("r", encoding="utf-8") as f:
|
|
113
|
+
for line in f:
|
|
114
|
+
line = line.strip()
|
|
115
|
+
if not line:
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
records.append(json.loads(line))
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
continue
|
|
121
|
+
records.sort(key=lambda r: (r.get("sequence_id", 0), r.get("timestamp", "")))
|
|
122
|
+
return records
|
|
123
|
+
|
|
124
|
+
def replay_checksum(self) -> str:
|
|
125
|
+
"""Deterministic checksum of ledger for replay verification."""
|
|
126
|
+
records = self.read_all()
|
|
127
|
+
content = json.dumps(records, sort_keys=True)
|
|
128
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_envelope(agent_id: str, round_id: int, payload: Any, trust: float = 0.5) -> REPEnvelope:
|
|
132
|
+
"""Build REP envelope from payload (hash stored; payload not stored in envelope)."""
|
|
133
|
+
payload_hash = _hash_payload(payload)
|
|
134
|
+
return REPEnvelope(agent_id=agent_id, round_id=round_id, trust=trust, payload_hash=payload_hash)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.rep_network — S15 multi-node REP ledger synchronization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from devtorch_core.rep_network.node import NodeIdentity, NodeRegistry, PeerEntry
|
|
6
|
+
from devtorch_core.rep_network.sync import (
|
|
7
|
+
SyncResult,
|
|
8
|
+
pull_from_peer,
|
|
9
|
+
push_to_peer,
|
|
10
|
+
sync_all_peers,
|
|
11
|
+
sync_with_peer,
|
|
12
|
+
)
|
|
13
|
+
from devtorch_core.rep_network.merge import merge_theta
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"NodeIdentity",
|
|
17
|
+
"PeerEntry",
|
|
18
|
+
"NodeRegistry",
|
|
19
|
+
"SyncResult",
|
|
20
|
+
"push_to_peer",
|
|
21
|
+
"pull_from_peer",
|
|
22
|
+
"sync_with_peer",
|
|
23
|
+
"sync_all_peers",
|
|
24
|
+
"merge_theta",
|
|
25
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
theta.json merge function for REP network synchronisation.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
_LEVEL_RANK = {"PUBLIC": 0, "PROTECTED": 1, "PRIVATE": 2}
|
|
8
|
+
_RANK_LEVEL = {0: "PUBLIC", 1: "PROTECTED", 2: "PRIVATE"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def merge_theta(local: dict, remote: dict) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Merge two theta.json dicts (remote into local) and return the merged result.
|
|
14
|
+
|
|
15
|
+
Merge rules per concept:
|
|
16
|
+
- event_count: sum of both
|
|
17
|
+
- mean_confidence: weighted mean (weighted by event_count from each side)
|
|
18
|
+
- disclosure_level: max (most restrictive) of both sides
|
|
19
|
+
- last_updated: take the more recent of both (ISO string comparison)
|
|
20
|
+
|
|
21
|
+
The returned dict has the same shape as ThetaStore.load() output:
|
|
22
|
+
{
|
|
23
|
+
"version": 1,
|
|
24
|
+
"updated_at": <now ISO UTC>,
|
|
25
|
+
"coordination_vector": { concept: { ... } }
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
import datetime as _dt
|
|
29
|
+
|
|
30
|
+
local_cv = local.get("coordination_vector", {})
|
|
31
|
+
remote_cv = remote.get("coordination_vector", {})
|
|
32
|
+
all_concepts = set(local_cv) | set(remote_cv)
|
|
33
|
+
|
|
34
|
+
merged_cv: Dict[str, dict] = {}
|
|
35
|
+
for concept in all_concepts:
|
|
36
|
+
l = local_cv.get(concept, {})
|
|
37
|
+
r = remote_cv.get(concept, {})
|
|
38
|
+
|
|
39
|
+
l_count = int(l.get("event_count", 0))
|
|
40
|
+
r_count = int(r.get("event_count", 0))
|
|
41
|
+
total = l_count + r_count
|
|
42
|
+
|
|
43
|
+
if total == 0:
|
|
44
|
+
merged_conf = 0.0
|
|
45
|
+
else:
|
|
46
|
+
l_conf = float(l.get("mean_confidence", 0.0))
|
|
47
|
+
r_conf = float(r.get("mean_confidence", 0.0))
|
|
48
|
+
merged_conf = round((l_conf * l_count + r_conf * r_count) / total, 6)
|
|
49
|
+
|
|
50
|
+
l_rank = _LEVEL_RANK.get(l.get("disclosure_level", "PUBLIC"), 0)
|
|
51
|
+
r_rank = _LEVEL_RANK.get(r.get("disclosure_level", "PUBLIC"), 0)
|
|
52
|
+
merged_level = _RANK_LEVEL[max(l_rank, r_rank)]
|
|
53
|
+
|
|
54
|
+
l_ts = l.get("last_updated", "")
|
|
55
|
+
r_ts = r.get("last_updated", "")
|
|
56
|
+
merged_ts = l_ts if l_ts >= r_ts else r_ts
|
|
57
|
+
|
|
58
|
+
merged_cv[concept] = {
|
|
59
|
+
"event_count": total,
|
|
60
|
+
"mean_confidence": merged_conf,
|
|
61
|
+
"disclosure_level": merged_level,
|
|
62
|
+
"last_updated": merged_ts,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
66
|
+
return {
|
|
67
|
+
"version": local.get("version", 1),
|
|
68
|
+
"updated_at": now,
|
|
69
|
+
"coordination_vector": merged_cv,
|
|
70
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REP network node identity and peer discovery.
|
|
3
|
+
|
|
4
|
+
Nodes are identified by a UUID generated once and stored in .GCC/rep/node_id.
|
|
5
|
+
Peer discovery is file-based (no mDNS/DNS-SD in S15): peers are listed in
|
|
6
|
+
.GCC/rep/peers.json as [{id: str, url: str, name: str}].
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import socket
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class NodeIdentity:
|
|
22
|
+
"""Identity of this node."""
|
|
23
|
+
node_id: str
|
|
24
|
+
name: str
|
|
25
|
+
url: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PeerEntry:
|
|
30
|
+
"""A known remote peer."""
|
|
31
|
+
node_id: str
|
|
32
|
+
name: str
|
|
33
|
+
url: str
|
|
34
|
+
last_seen: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NodeRegistry:
|
|
38
|
+
"""Manages node identity and peer list backed by .GCC/rep/ files."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, gcc_root: str) -> None:
|
|
41
|
+
self._rep_dir = Path(gcc_root) / "rep"
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Identity
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def get_or_create_identity(self, url: str = "") -> NodeIdentity:
|
|
48
|
+
"""Read .GCC/rep/node_id; create and persist one if missing."""
|
|
49
|
+
self._rep_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
node_id_path = self._rep_dir / "node_id"
|
|
51
|
+
if node_id_path.exists():
|
|
52
|
+
try:
|
|
53
|
+
node_id = node_id_path.read_text(encoding="utf-8").strip()
|
|
54
|
+
except OSError:
|
|
55
|
+
node_id = str(uuid.uuid4())
|
|
56
|
+
node_id_path.write_text(node_id, encoding="utf-8")
|
|
57
|
+
else:
|
|
58
|
+
node_id = str(uuid.uuid4())
|
|
59
|
+
node_id_path.write_text(node_id, encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
name = socket.gethostname()
|
|
62
|
+
return NodeIdentity(node_id=node_id, name=name, url=url)
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Peers
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _peers_path(self) -> Path:
|
|
69
|
+
return self._rep_dir / "peers.json"
|
|
70
|
+
|
|
71
|
+
def list_peers(self) -> List[PeerEntry]:
|
|
72
|
+
"""Read .GCC/rep/peers.json; return empty list if missing or corrupt."""
|
|
73
|
+
path = self._peers_path()
|
|
74
|
+
if not path.exists():
|
|
75
|
+
return []
|
|
76
|
+
try:
|
|
77
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
78
|
+
if not isinstance(raw, list):
|
|
79
|
+
return []
|
|
80
|
+
peers = []
|
|
81
|
+
for item in raw:
|
|
82
|
+
if not isinstance(item, dict):
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
peers.append(PeerEntry(
|
|
86
|
+
node_id=str(item["node_id"]),
|
|
87
|
+
name=str(item.get("name", "")),
|
|
88
|
+
url=str(item.get("url", "")),
|
|
89
|
+
last_seen=str(item.get("last_seen", "")),
|
|
90
|
+
))
|
|
91
|
+
except (KeyError, TypeError):
|
|
92
|
+
continue
|
|
93
|
+
return peers
|
|
94
|
+
except (json.JSONDecodeError, OSError):
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
def _save_peers(self, peers: List[PeerEntry]) -> None:
|
|
98
|
+
self._rep_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
data = [
|
|
100
|
+
{
|
|
101
|
+
"node_id": p.node_id,
|
|
102
|
+
"name": p.name,
|
|
103
|
+
"url": p.url,
|
|
104
|
+
"last_seen": p.last_seen,
|
|
105
|
+
}
|
|
106
|
+
for p in peers
|
|
107
|
+
]
|
|
108
|
+
self._peers_path().write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
def add_peer(self, peer: PeerEntry) -> None:
|
|
111
|
+
"""Append peer; replace existing entry with the same node_id."""
|
|
112
|
+
peers = self.list_peers()
|
|
113
|
+
peers = [p for p in peers if p.node_id != peer.node_id]
|
|
114
|
+
peers.append(peer)
|
|
115
|
+
self._save_peers(peers)
|
|
116
|
+
|
|
117
|
+
def remove_peer(self, node_id: str) -> None:
|
|
118
|
+
"""Remove peer by node_id; no-op if not found."""
|
|
119
|
+
peers = [p for p in self.list_peers() if p.node_id != node_id]
|
|
120
|
+
self._save_peers(peers)
|
|
121
|
+
|
|
122
|
+
def update_peer_last_seen(self, node_id: str) -> None:
|
|
123
|
+
"""Set last_seen to current UTC ISO timestamp for the given peer."""
|
|
124
|
+
peers = self.list_peers()
|
|
125
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
126
|
+
updated = []
|
|
127
|
+
for p in peers:
|
|
128
|
+
if p.node_id == node_id:
|
|
129
|
+
updated.append(PeerEntry(
|
|
130
|
+
node_id=p.node_id,
|
|
131
|
+
name=p.name,
|
|
132
|
+
url=p.url,
|
|
133
|
+
last_seen=ts,
|
|
134
|
+
))
|
|
135
|
+
else:
|
|
136
|
+
updated.append(p)
|
|
137
|
+
self._save_peers(updated)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI server exposing REP ledger to peers.
|
|
3
|
+
Runs on a different port from the proxy (default 8767).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from devtorch_core.rep import REPEnvelope, REPLedger
|
|
11
|
+
from devtorch_core.rep_network.node import NodeRegistry, PeerEntry
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from fastapi import Body, FastAPI
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
HAS_FASTAPI = True
|
|
18
|
+
|
|
19
|
+
class PushEntriesRequest(BaseModel):
|
|
20
|
+
entries: List[Dict[str, Any]]
|
|
21
|
+
sender_id: str = ""
|
|
22
|
+
|
|
23
|
+
class PeerEntryModel(BaseModel):
|
|
24
|
+
node_id: str
|
|
25
|
+
name: str = ""
|
|
26
|
+
url: str = ""
|
|
27
|
+
last_seen: str = ""
|
|
28
|
+
|
|
29
|
+
except ImportError:
|
|
30
|
+
HAS_FASTAPI = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_rep_server_app(ledger: REPLedger, registry: NodeRegistry) -> "FastAPI":
|
|
34
|
+
"""Build and return a FastAPI application exposing the REP ledger.
|
|
35
|
+
|
|
36
|
+
Raises RuntimeError if FastAPI is not installed.
|
|
37
|
+
"""
|
|
38
|
+
if not HAS_FASTAPI:
|
|
39
|
+
raise RuntimeError("FastAPI is not installed; cannot create REP server app.")
|
|
40
|
+
|
|
41
|
+
app = FastAPI(title="DevTorch REP Network Node", version="0.1.0")
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Helpers
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
from devtorch_core.rep_network.sync import _envelope_ids_in_ledger, _persist_pulled_id
|
|
48
|
+
|
|
49
|
+
def _existing_envelope_ids() -> set:
|
|
50
|
+
return _envelope_ids_in_ledger(ledger)
|
|
51
|
+
|
|
52
|
+
def _get_node_id() -> str:
|
|
53
|
+
try:
|
|
54
|
+
identity = registry.get_or_create_identity()
|
|
55
|
+
return identity.node_id
|
|
56
|
+
except Exception: # noqa: BLE001
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# Routes
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
@app.get("/rep/entries")
|
|
64
|
+
def get_entries(since: int = 0) -> Dict[str, Any]:
|
|
65
|
+
"""Return ledger entries with sequence_id > since."""
|
|
66
|
+
all_records = ledger.read_all()
|
|
67
|
+
filtered = [r for r in all_records if r.get("sequence_id", 0) > since]
|
|
68
|
+
entries = [r.get("envelope", {}) for r in filtered]
|
|
69
|
+
return {
|
|
70
|
+
"node_id": _get_node_id(),
|
|
71
|
+
"entries": entries,
|
|
72
|
+
"count": len(entries),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@app.post("/rep/entries")
|
|
76
|
+
def post_entries(request: PushEntriesRequest = Body(...)) -> Dict[str, Any]:
|
|
77
|
+
"""Accept pushed entries; skip duplicates by envelope_id."""
|
|
78
|
+
existing_ids = _existing_envelope_ids()
|
|
79
|
+
accepted = 0
|
|
80
|
+
duplicate = 0
|
|
81
|
+
import datetime as _dt
|
|
82
|
+
|
|
83
|
+
for entry_dict in request.entries:
|
|
84
|
+
eid = entry_dict.get("envelope_id")
|
|
85
|
+
if eid and eid in existing_ids:
|
|
86
|
+
duplicate += 1
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
envelope = REPEnvelope.from_dict(entry_dict)
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
continue
|
|
92
|
+
ts = entry_dict.get("timestamp", _dt.datetime.now(_dt.timezone.utc).isoformat())
|
|
93
|
+
ledger.append(envelope, timestamp=ts)
|
|
94
|
+
if eid:
|
|
95
|
+
existing_ids.add(eid)
|
|
96
|
+
_persist_pulled_id(ledger, eid)
|
|
97
|
+
accepted += 1
|
|
98
|
+
|
|
99
|
+
# Update last_seen for sender
|
|
100
|
+
if request.sender_id:
|
|
101
|
+
try:
|
|
102
|
+
registry.update_peer_last_seen(request.sender_id)
|
|
103
|
+
except Exception: # noqa: BLE001
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
return {"accepted": accepted, "duplicate": duplicate}
|
|
107
|
+
|
|
108
|
+
@app.get("/rep/peers")
|
|
109
|
+
def get_peers() -> List[Dict[str, Any]]:
|
|
110
|
+
peers = registry.list_peers()
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
"node_id": p.node_id,
|
|
114
|
+
"name": p.name,
|
|
115
|
+
"url": p.url,
|
|
116
|
+
"last_seen": p.last_seen,
|
|
117
|
+
}
|
|
118
|
+
for p in peers
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
@app.post("/rep/peers")
|
|
122
|
+
def post_peer(body: PeerEntryModel = Body(...)) -> Dict[str, str]:
|
|
123
|
+
peer = PeerEntry(
|
|
124
|
+
node_id=body.node_id,
|
|
125
|
+
name=body.name,
|
|
126
|
+
url=body.url,
|
|
127
|
+
last_seen=body.last_seen,
|
|
128
|
+
)
|
|
129
|
+
registry.add_peer(peer)
|
|
130
|
+
return {"status": "ok"}
|
|
131
|
+
|
|
132
|
+
@app.get("/health")
|
|
133
|
+
def health() -> Dict[str, Any]:
|
|
134
|
+
return {
|
|
135
|
+
"status": "ok",
|
|
136
|
+
"node_id": _get_node_id(),
|
|
137
|
+
"entry_count": len(ledger.read_all()),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return app
|