openplan-mcp 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. openplan/__init__.py +8 -0
  2. openplan/__main__.py +17 -0
  3. openplan/cli.py +264 -0
  4. openplan/config.py +67 -0
  5. openplan/core/__init__.py +0 -0
  6. openplan/core/activation.py +295 -0
  7. openplan/core/analytics.py +156 -0
  8. openplan/core/bandit.py +73 -0
  9. openplan/core/conditions.py +49 -0
  10. openplan/core/costs.py +92 -0
  11. openplan/core/embedding.py +359 -0
  12. openplan/core/errors.py +102 -0
  13. openplan/core/estimator.py +74 -0
  14. openplan/core/export.py +284 -0
  15. openplan/core/goals.py +55 -0
  16. openplan/core/graph.py +551 -0
  17. openplan/core/graph_ops.py +61 -0
  18. openplan/core/ids.py +45 -0
  19. openplan/core/insight_propagation.py +101 -0
  20. openplan/core/learning.py +152 -0
  21. openplan/core/learnings.py +76 -0
  22. openplan/core/maintenance.py +71 -0
  23. openplan/core/planner.py +506 -0
  24. openplan/core/planning.py +277 -0
  25. openplan/core/read.py +588 -0
  26. openplan/core/reasoning.py +57 -0
  27. openplan/core/recommend.py +318 -0
  28. openplan/core/resolve.py +76 -0
  29. openplan/core/retro.py +82 -0
  30. openplan/core/self_tune.py +181 -0
  31. openplan/core/simulate.py +83 -0
  32. openplan/core/state.py +272 -0
  33. openplan/core/telemetry.py +282 -0
  34. openplan/core/transaction.py +54 -0
  35. openplan/core/tree.py +194 -0
  36. openplan/core/utils.py +7 -0
  37. openplan/db/__init__.py +0 -0
  38. openplan/db/connection.py +16 -0
  39. openplan/db/schema.py +192 -0
  40. openplan/handler_utils.py +213 -0
  41. openplan/handlers/__init__.py +15 -0
  42. openplan/handlers/act_handler.py +251 -0
  43. openplan/handlers/complete_handler.py +121 -0
  44. openplan/handlers/export_handler.py +19 -0
  45. openplan/handlers/init_handler.py +20 -0
  46. openplan/handlers/recommend_handler.py +150 -0
  47. openplan/handlers/start_handler.py +20 -0
  48. openplan/server.py +315 -0
  49. openplan/tools/__init__.py +0 -0
  50. openplan/tools/definitions.py +126 -0
  51. openplan_mcp-0.8.2.dist-info/METADATA +116 -0
  52. openplan_mcp-0.8.2.dist-info/RECORD +56 -0
  53. openplan_mcp-0.8.2.dist-info/WHEEL +5 -0
  54. openplan_mcp-0.8.2.dist-info/entry_points.txt +2 -0
  55. openplan_mcp-0.8.2.dist-info/licenses/LICENSE +21 -0
  56. openplan_mcp-0.8.2.dist-info/top_level.txt +1 -0
openplan/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
2
+
3
+ try:
4
+ __version__ = _pkg_version("openplan")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.8.2"
7
+
8
+ VERSION = __version__
openplan/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+
6
+ def main() -> None:
7
+ if len(sys.argv) >= 2 and sys.argv[1] in ("auth", "subscribe", "status", "--help", "-h"):
8
+ from openplan.cli import main as cli_main
9
+ cli_main()
10
+ else:
11
+ from openplan.server import main as server_main
12
+ import anyio
13
+ anyio.run(server_main)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
openplan/cli.py ADDED
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "openplan"
10
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
11
+
12
+
13
+ def _load_credentials() -> dict[str, Any]:
14
+ if CREDENTIALS_FILE.exists():
15
+ try:
16
+ return json.loads(CREDENTIALS_FILE.read_text())
17
+ except (json.JSONDecodeError, OSError):
18
+ pass
19
+ return {}
20
+
21
+
22
+ def _save_credentials(data: dict[str, Any]) -> None:
23
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
24
+ CREDENTIALS_FILE.write_text(json.dumps(data, indent=2))
25
+ CREDENTIALS_FILE.chmod(0o600)
26
+
27
+
28
+ def _get_api_key() -> str | None:
29
+ if key := os.environ.get("OPENPLAN_API_KEY"):
30
+ return key
31
+ creds = _load_credentials()
32
+ return creds.get("api_key")
33
+
34
+
35
+ def cmd_auth_login(args: list[str]) -> None:
36
+ """Authenticate with GitHub via device code flow."""
37
+ api_url = os.environ.get("OPENPLAN_API_URL", "https://api.openplan.cc")
38
+
39
+ print("Open the following URL in your browser and enter the code shown.")
40
+ print()
41
+
42
+ try:
43
+ import httpx
44
+ resp = httpx.post(f"{api_url}/oauth/authorize", json={
45
+ "client_id": "openplan-cli",
46
+ "scope": "user:email",
47
+ }, timeout=10)
48
+ resp.raise_for_status()
49
+ data = resp.json()
50
+ except Exception as e:
51
+ print(f"Failed to start authentication: {e}")
52
+ sys.exit(1)
53
+
54
+ user_code = data["user_code"]
55
+ verification_uri = data.get("verification_uri", "https://github.com/login/device")
56
+ device_code = data["device_code"]
57
+ interval = data.get("interval", 5)
58
+
59
+ print(f" {verification_uri}")
60
+ print(f" Enter code: {user_code}")
61
+ print()
62
+
63
+ import time
64
+ try:
65
+ import httpx
66
+ while True:
67
+ time.sleep(interval)
68
+ resp = httpx.post(f"{api_url}/oauth/token", json={
69
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
70
+ "device_code": device_code,
71
+ "client_id": "openplan-cli",
72
+ }, timeout=10)
73
+ token_data = resp.json()
74
+ if resp.status_code == 200:
75
+ access_token = token_data["access_token"]
76
+ refresh_token = token_data.get("refresh_token", "")
77
+
78
+ # Exchange access token for an API key
79
+ key_resp = httpx.post(f"{api_url}/api/keys", headers={
80
+ "Authorization": f"Bearer {access_token}",
81
+ }, json={"tier": "pro"}, timeout=10)
82
+ key_resp.raise_for_status()
83
+ api_key = key_resp.json()["api_key"]
84
+
85
+ _save_credentials({
86
+ "api_key": api_key,
87
+ "refresh_token": refresh_token,
88
+ "tier": "pro",
89
+ })
90
+ print("✓ Authentication complete")
91
+ print("✓ Pro tier enabled")
92
+ print(f" Config: {CREDENTIALS_FILE}")
93
+ return
94
+ elif token_data.get("error") == "authorization_pending":
95
+ continue
96
+ elif token_data.get("error") == "slow_down":
97
+ interval += 5
98
+ continue
99
+ elif token_data.get("error") == "access_denied":
100
+ print("Authentication cancelled.")
101
+ sys.exit(1)
102
+ elif token_data.get("error") == "expired_token":
103
+ print("Session expired. Try again.")
104
+ sys.exit(1)
105
+ else:
106
+ print(f"Unexpected error: {token_data}")
107
+ sys.exit(1)
108
+ except KeyboardInterrupt:
109
+ print("\nCancelled.")
110
+ sys.exit(1)
111
+
112
+
113
+ def cmd_auth_logout(args: list[str]) -> None:
114
+ """Remove stored credentials."""
115
+ if CREDENTIALS_FILE.exists():
116
+ CREDENTIALS_FILE.unlink()
117
+ api_url = os.environ.get("OPENPLAN_API_URL", "https://api.openplan.cc")
118
+ key = _get_api_key()
119
+ if key:
120
+ try:
121
+ import httpx
122
+ httpx.post(f"{api_url}/api/keys/revoke", json={"api_key": key}, timeout=10)
123
+ except Exception:
124
+ pass
125
+ print("✓ Credentials removed")
126
+ else:
127
+ print("No credentials found")
128
+
129
+
130
+ def cmd_auth_status(args: list[str]) -> None:
131
+ """Show authentication status."""
132
+ creds = _load_credentials()
133
+ env_key = os.environ.get("OPENPLAN_API_KEY")
134
+
135
+ if env_key:
136
+ print("Authentication: API key from OPENPLAN_API_KEY env var")
137
+ elif creds.get("api_key"):
138
+ print("Authentication: stored credentials")
139
+ print(f" Tier: {creds.get('tier', 'unknown')}")
140
+ print(f" Config: {CREDENTIALS_FILE}")
141
+ else:
142
+ print("Authentication: none (free tier)")
143
+ print(" Run 'openplan auth login' to authenticate with GitHub")
144
+
145
+
146
+ def cmd_subscribe(args: list[str]) -> None:
147
+ """Open Stripe Checkout for Pro subscription."""
148
+ api_key = _get_api_key()
149
+ if not api_key:
150
+ print("You need to authenticate first.")
151
+ print("Run: openplan auth login")
152
+ sys.exit(1)
153
+
154
+ api_url = os.environ.get("OPENPLAN_API_URL", "https://api.openplan.cc")
155
+ plan = args[0] if args else "pro"
156
+
157
+ try:
158
+ import httpx
159
+ resp = httpx.post(f"{api_url}/checkout", json={
160
+ "plan": plan,
161
+ "api_key": api_key,
162
+ }, timeout=10)
163
+ resp.raise_for_status()
164
+ checkout_url = resp.json()["checkout_url"]
165
+ except Exception as e:
166
+ print(f"Failed to create checkout session: {e}")
167
+ sys.exit(1)
168
+
169
+ import webbrowser
170
+ webbrowser.open(checkout_url)
171
+ print(f"Opened browser for Stripe Checkout ({plan} plan)")
172
+ print("Complete payment in the browser.")
173
+
174
+ import time
175
+ deadline = time.time() + 300 # 5 minute timeout
176
+ poll_count = 0
177
+ try:
178
+ while time.time() < deadline:
179
+ time.sleep(3)
180
+ poll_count += 1
181
+ if poll_count % 5 == 0:
182
+ print(f" Waiting for payment... ({int(deadline - time.time())}s remaining)")
183
+ try:
184
+ resp = httpx.get(f"{api_url}/api/subscription/status", headers={
185
+ "Authorization": f"Bearer {api_key}",
186
+ }, timeout=10)
187
+ data = resp.json()
188
+ if data.get("status") == "active":
189
+ print("✓ Subscription active")
190
+ print(f" Tier: {data.get('tier', 'pro')}")
191
+ return
192
+ except Exception:
193
+ pass
194
+ print("Timed out waiting for subscription. Complete payment at the Stripe page.")
195
+ except KeyboardInterrupt:
196
+ print("\nCancelled.")
197
+
198
+
199
+ def cmd_status(args: list[str]) -> None:
200
+ """Show OpenPlan status and sync info."""
201
+ api_key = _get_api_key()
202
+ api_url = os.environ.get("OPENPLAN_API_URL", "https://api.openplan.cc")
203
+
204
+ creds = _load_credentials()
205
+ tier = creds.get("tier", "free") if not os.environ.get("OPENPLAN_API_KEY") else "pro (env)"
206
+
207
+ from openplan import VERSION
208
+ print(f"OpenPlan v{VERSION}")
209
+ print(f"Tier: {tier}")
210
+ print()
211
+
212
+ if api_key:
213
+ try:
214
+ import httpx
215
+ resp = httpx.get(f"{api_url}/api/keys/usage", headers={
216
+ "Authorization": f"Bearer {api_key}",
217
+ }, timeout=10)
218
+ data = resp.json()
219
+ print(f"Events synced: {data.get('event_count', '?')}")
220
+ print(f"Sync rate: {data.get('rate_limit', '?')}/min")
221
+ except Exception:
222
+ print("API: unreachable")
223
+ else:
224
+ print("Free tier (anonymous)")
225
+ print("Run 'openplan auth login' to enable Pro features")
226
+
227
+
228
+ def main() -> None:
229
+ import sys
230
+ if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h"):
231
+ print("OpenPlan — Waze for AI agents planning")
232
+ print()
233
+ print("Usage:")
234
+ print(" openplan Start MCP server")
235
+ print(" openplan auth login Authenticate with GitHub")
236
+ print(" openplan auth logout Remove credentials")
237
+ print(" openplan auth status Show authentication state")
238
+ print(" openplan subscribe [plan] Start Pro subscription")
239
+ print(" openplan status Show OpenPlan status")
240
+ print()
241
+ print("Docs: https://github.com/anomalyco/opencode")
242
+ return
243
+
244
+ cmd = sys.argv[1]
245
+ if cmd == "auth":
246
+ if len(sys.argv) < 3:
247
+ print("Usage: openplan auth <login|logout|status>")
248
+ return
249
+ sub = sys.argv[2]
250
+ if sub == "login":
251
+ cmd_auth_login(sys.argv[3:])
252
+ elif sub == "logout":
253
+ cmd_auth_logout(sys.argv[3:])
254
+ elif sub == "status":
255
+ cmd_auth_status(sys.argv[3:])
256
+ else:
257
+ print(f"Unknown auth command: {sub}")
258
+ elif cmd == "subscribe":
259
+ cmd_subscribe(sys.argv[2:])
260
+ elif cmd == "status":
261
+ cmd_status(sys.argv[2:])
262
+ else:
263
+ print(f"Unknown command: {cmd}")
264
+ sys.exit(1)
openplan/config.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import os
6
+ from typing import Any
7
+
8
+ CONFIG_SCHEMA: dict[str, dict[str, Any]] = {
9
+ "db_path": {"type": str, "default": "openplan.db"},
10
+ "stale_days": {"type": int, "default": 2, "min": 1, "max": 365},
11
+ "wip_limit": {"type": int, "default": 20, "min": 1, "max": 1000},
12
+ "activation_threshold": {"type": float, "default": 0.5, "min": 0.0, "max": 1.0},
13
+ "plan_limit": {"type": int, "default": 10, "min": 1, "max": 100},
14
+ "expansion_limit": {"type": int, "default": 5, "min": 1, "max": 50},
15
+ "avg_edge_cost": {"type": float, "default": 5000.0, "min": 100, "max": 1000000},
16
+ "heuristic_scale": {"type": float, "default": 1.0, "min": 0.1, "max": 10.0},
17
+ "reverse_penalty": {"type": float, "default": 3.0, "min": 1.0, "max": 100.0},
18
+ "risk_adjustment": {"type": str, "default": "probability", "values": ["none", "probability", "variance"]},
19
+ "tune_interval": {"type": int, "default": 10, "min": 0, "max": 1000},
20
+ "activation_weights": {"type": dict, "default": {"in_degree": 0.33, "frontier": 0.24, "recency": 0.19, "boost": 0.09, "visit": 0.1, "novelty": 0.05}},
21
+ "learning": {"type": dict, "default": {}},
22
+ "embedding": {"type": dict, "default": {}},
23
+ "page_rank": {"type": dict, "default": {}},
24
+ "stale_branch_hours": {"type": int, "default": 24, "min": 1, "max": 720},
25
+ "recommend_weights": {"type": dict, "default": {}},
26
+ "maintenance_interval_minutes": {"type": int, "default": 5, "min": 1, "max": 1440},
27
+ "adaptive_weights": {"type": dict, "default": {}},
28
+ "insight_similarity_threshold": {"type": float, "default": 0.7, "min": 0.0, "max": 1.0},
29
+ "api_url": {"type": str, "default": ""},
30
+ }
31
+
32
+
33
+ def _validate_config(raw: dict[str, Any]) -> dict[str, Any]:
34
+ validated: dict[str, Any] = {}
35
+ for key, value in raw.items():
36
+ spec = CONFIG_SCHEMA.get(key)
37
+ if not spec:
38
+ raise ValueError(f"Unknown config key: '{key}'")
39
+ expected_type = spec["type"]
40
+ if not isinstance(value, expected_type):
41
+ raise TypeError(f"Config '{key}' must be {expected_type.__name__}, got {type(value).__name__}")
42
+ if expected_type in (int, float) and "min" in spec and value < spec["min"]:
43
+ raise ValueError(f"Config '{key}' must be >= {spec['min']}, got {value}")
44
+ if expected_type in (int, float) and "max" in spec and value > spec["max"]:
45
+ raise ValueError(f"Config '{key}' must be <= {spec['max']}, got {value}")
46
+ if "values" in spec and value not in spec["values"]:
47
+ raise ValueError(f"Config '{key}' must be one of {spec['values']}, got '{value}'")
48
+ validated[key] = value
49
+ return validated
50
+
51
+
52
+ def load_config(config_path: str | None = None) -> dict[str, Any]:
53
+ cfg: dict[str, Any] = {}
54
+ for key, spec in CONFIG_SCHEMA.items():
55
+ cfg[key] = copy.deepcopy(spec["default"])
56
+ cfg["db_path"] = os.environ.get("OPENPLAN_DB_PATH", cfg.get("db_path", "openplan.db"))
57
+ if config_path and os.path.exists(config_path):
58
+ with open(config_path) as f:
59
+ raw = json.load(f)
60
+ validated = _validate_config(raw)
61
+ for key in list(cfg.keys()):
62
+ if key in validated and isinstance(validated[key], dict) and isinstance(cfg[key], dict):
63
+ merged = dict(cfg[key])
64
+ merged.update(validated[key])
65
+ validated[key] = merged
66
+ cfg.update(validated)
67
+ return cfg
File without changes
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ import threading
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+
10
+ class ActivationContext:
11
+ def __init__(self) -> None:
12
+ self.cache: dict[str, float] = {}
13
+ self.dirty: set[str] = set()
14
+ self.cache_order: list[str] = []
15
+ self.visiting: set[str] = set()
16
+ self.max_in_degree: int = 1
17
+ self.validation_counter: int = 0
18
+ self.VALIDATE_EVERY: int = 100
19
+ self.max_in_degree_initialized: bool = False
20
+ self.lock = threading.RLock()
21
+
22
+ def increment_max_in_degree(self, target_id: str, conn: sqlite3.Connection) -> None:
23
+ with self.lock:
24
+ cnt = conn.execute(
25
+ "SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ?", (target_id,)
26
+ ).fetchone()["cnt"]
27
+ if cnt > self.max_in_degree:
28
+ self.max_in_degree = cnt
29
+ self.validation_counter += 1
30
+ if self.validation_counter >= self.VALIDATE_EVERY:
31
+ self._validate_max_in_degree(conn)
32
+
33
+ def _validate_max_in_degree(self, conn: sqlite3.Connection) -> None:
34
+ row = conn.execute(
35
+ "SELECT COUNT(*) AS cnt FROM edges GROUP BY target_id ORDER BY cnt DESC LIMIT 1"
36
+ ).fetchone()
37
+ self.max_in_degree = row["cnt"] if row else 1
38
+ self.validation_counter = 0
39
+
40
+ def _get_max_in_degree(self, conn: sqlite3.Connection) -> int:
41
+ if not self.max_in_degree_initialized:
42
+ self._validate_max_in_degree(conn)
43
+ self.max_in_degree_initialized = True
44
+ return self.max_in_degree
45
+
46
+ def _get_outgoing_count(self, state_id: str, conn: sqlite3.Connection) -> int:
47
+ row = conn.execute(
48
+ "SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ?", (state_id,)
49
+ ).fetchone()
50
+ return row["cnt"]
51
+
52
+ def _recompute_cache_order(self, conn: sqlite3.Connection) -> None:
53
+ self.cache_order = sorted(
54
+ self.dirty,
55
+ key=lambda s: (
56
+ self._get_outgoing_count(s, conn),
57
+ -conn.execute(
58
+ "SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ?", (s,)
59
+ ).fetchone()["cnt"],
60
+ s,
61
+ ),
62
+ )
63
+
64
+ def mark_dirty(self, state_id: str, conn: sqlite3.Connection) -> None:
65
+ with self.lock:
66
+ self.dirty.add(state_id)
67
+ preds = conn.execute(
68
+ "SELECT DISTINCT source_id FROM edges WHERE target_id = ?", (state_id,)
69
+ ).fetchall()
70
+ for p in preds:
71
+ self.dirty.add(p["source_id"])
72
+ self._recompute_cache_order(conn)
73
+
74
+ def _compute_frontier_ratio(self, state_id: str, conn: sqlite3.Connection, config: dict[str, Any]) -> float:
75
+ threshold = config.get("activation_threshold", 0.5)
76
+ edges = conn.execute(
77
+ "SELECT target_id FROM edges WHERE source_id = ?", (state_id,)
78
+ ).fetchall()
79
+ if not edges:
80
+ return 0.0
81
+ active = 0
82
+ for e in edges:
83
+ target_act = self.cache.get(e["target_id"], 0.0)
84
+ if target_act > threshold:
85
+ active += 1
86
+ return active / len(edges)
87
+
88
+ def _compute_in_degree_ratio(self, state_id: str, conn: sqlite3.Connection) -> float:
89
+ row = conn.execute(
90
+ "SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ?", (state_id,)
91
+ ).fetchone()
92
+ cnt = row["cnt"]
93
+ max_in = self._get_max_in_degree(conn)
94
+ return min(cnt / max_in, 1.0) if max_in > 0 else 0.0
95
+
96
+ def _compute_recency(self, state_id: str, conn: sqlite3.Connection, stale_days: int) -> float:
97
+ row = conn.execute(
98
+ "SELECT updated_at FROM nodes WHERE id = ?", (state_id,)
99
+ ).fetchone()
100
+ if not row:
101
+ return 0.0
102
+ updated = row["updated_at"]
103
+ try:
104
+ updated_dt = datetime.fromisoformat(updated.replace("Z", "+00:00"))
105
+ except (ValueError, TypeError):
106
+ return 0.0
107
+ now = datetime.now(timezone.utc)
108
+ if updated_dt.tzinfo is None:
109
+ updated_dt = updated_dt.replace(tzinfo=timezone.utc)
110
+ delta = now - updated_dt
111
+ days = delta.total_seconds() / 86400.0
112
+ return 1.0 - min(days / stale_days, 1.0)
113
+
114
+ def _compute_agent_boost(self, state_id: str, conn: sqlite3.Connection) -> float:
115
+ row = conn.execute(
116
+ "SELECT props FROM nodes WHERE id = ?", (state_id,)
117
+ ).fetchone()
118
+ if not row:
119
+ return 0.5
120
+ try:
121
+ props = json.loads(row["props"])
122
+ except (json.JSONDecodeError, TypeError):
123
+ return 0.5
124
+ if props.get("boost") is True:
125
+ boosted_at = props.get("boosted_at")
126
+ if boosted_at:
127
+ try:
128
+ bt = datetime.fromisoformat(boosted_at)
129
+ now = datetime.now(timezone.utc)
130
+ if bt.tzinfo is None:
131
+ bt = bt.replace(tzinfo=timezone.utc)
132
+ hours = (now - bt).total_seconds() / 3600.0
133
+ if hours > 24:
134
+ return 0.5
135
+ except (ValueError, TypeError):
136
+ pass
137
+ return 1.0
138
+ return 0.5
139
+
140
+ def _compute_visit_ratio(self, state_id: str, conn: sqlite3.Connection) -> float:
141
+ row = conn.execute("SELECT props FROM nodes WHERE id = ?", (state_id,)).fetchone()
142
+ if not row:
143
+ return 0.0
144
+ try:
145
+ props = json.loads(row["props"])
146
+ except (json.JSONDecodeError, TypeError):
147
+ return 0.0
148
+ my_visits = props.get("visit_count", 0)
149
+ if my_visits == 0:
150
+ return 0.0
151
+ project_row = conn.execute("SELECT project FROM nodes WHERE id = ?", (state_id,)).fetchone()
152
+ if not project_row:
153
+ return 0.0
154
+ max_row = conn.execute(
155
+ "SELECT MAX(json_extract(props, '$.visit_count')) AS max_visits FROM nodes WHERE project = ?",
156
+ (project_row["project"],),
157
+ ).fetchone()
158
+ max_visits = max_row["max_visits"] if max_row and max_row["max_visits"] else 0
159
+ return min(my_visits / max_visits, 1.0) if max_visits > 0 else 0.0
160
+
161
+ def _compute_novelty_ratio(self, state_id: str, conn: sqlite3.Connection) -> float:
162
+ out_count = conn.execute(
163
+ "SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ?", (state_id,)
164
+ ).fetchone()["cnt"]
165
+ return 1.0 / (out_count + 1)
166
+
167
+ def _compute_activation(self, state_id: str, conn: sqlite3.Connection, config: dict[str, Any]) -> float:
168
+ import hashlib
169
+ weights = dict(config.get("activation_weights", {}))
170
+ w_row = conn.execute(
171
+ "SELECT value FROM meta WHERE key = 'self_tuning:weight_config'"
172
+ ).fetchone()
173
+ if w_row:
174
+ try:
175
+ w_override = json.loads(w_row["value"])
176
+ if isinstance(w_override, dict):
177
+ weights = w_override
178
+ except (json.JSONDecodeError, TypeError):
179
+ pass
180
+ w_in = weights.get("in_degree", 0.35)
181
+ w_frontier = weights.get("frontier", 0.25)
182
+ w_recency = weights.get("recency", 0.2)
183
+ w_boost = weights.get("boost", 0.1)
184
+ w_visit = weights.get("visit", 0.0)
185
+ w_novelty = weights.get("novelty", 0.0)
186
+ in_degree_ratio = self._compute_in_degree_ratio(state_id, conn)
187
+ frontier_ratio = self._compute_frontier_ratio(state_id, conn, config)
188
+ stale_days = config.get("stale_days", 2)
189
+ recency = self._compute_recency(state_id, conn, stale_days)
190
+ agent_boost = self._compute_agent_boost(state_id, conn)
191
+ visit_ratio = self._compute_visit_ratio(state_id, conn)
192
+ novelty = self._compute_novelty_ratio(state_id, conn)
193
+ raw = w_in * in_degree_ratio + w_frontier * frontier_ratio + w_recency * recency + w_boost * agent_boost + w_visit * visit_ratio + w_novelty * novelty
194
+ digest = hashlib.sha256(state_id.encode()).digest()[:4]
195
+ tiebreaker = int.from_bytes(digest, "big") / 1e16
196
+ return raw + tiebreaker
197
+
198
+ def _precompute_targets(self, state_id: str, conn: sqlite3.Connection, config: dict[str, Any]) -> None:
199
+ stack = [(state_id, 0)]
200
+ order: list[str] = []
201
+ while stack:
202
+ sid, idx = stack[-1]
203
+ if idx == 0:
204
+ if sid in self.visiting:
205
+ stack.pop()
206
+ continue
207
+ self.visiting.add(sid)
208
+ order.append(sid)
209
+ targets = conn.execute(
210
+ "SELECT target_id FROM edges WHERE source_id = ?", (sid,)
211
+ ).fetchall()
212
+ if idx < len(targets):
213
+ tid = targets[idx]["target_id"]
214
+ stack[-1] = (sid, idx + 1)
215
+ if tid not in self.cache and tid not in self.visiting:
216
+ stack.append((tid, 0))
217
+ else:
218
+ stack.pop()
219
+ self.visiting.discard(sid)
220
+ for sid in reversed(order):
221
+ if sid not in self.cache:
222
+ act = self._compute_activation(sid, conn, config)
223
+ self.cache[sid] = act
224
+ threshold = config.get("activation_threshold", 0.5)
225
+ frontier = 1 if self._is_frontier(sid, act, conn, threshold) else 0
226
+ conn.execute(
227
+ "UPDATE nodes SET activation = ?, frontier = ? WHERE id = ?",
228
+ (act, frontier, sid),
229
+ )
230
+
231
+ def _is_frontier(self, state_id: str, activation: float, conn: sqlite3.Connection, threshold: float) -> bool:
232
+ if activation <= threshold:
233
+ return False
234
+ cnt = conn.execute(
235
+ "SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ?", (state_id,)
236
+ ).fetchone()["cnt"]
237
+ return cnt > 0
238
+
239
+ def recompute_all_dirty(self, conn: sqlite3.Connection, config: dict[str, Any]) -> None:
240
+ with self.lock:
241
+ self._recompute_all_dirty_locked(conn, config)
242
+
243
+ def _recompute_all_dirty_locked(self, conn: sqlite3.Connection, config: dict[str, Any]) -> None:
244
+ threshold = config.get("activation_threshold", 0.5)
245
+ for sid in self.cache_order:
246
+ if sid in self.dirty:
247
+ act = self._compute_activation(sid, conn, config)
248
+ self.cache[sid] = act
249
+ self.dirty.discard(sid)
250
+ frontier = 1 if self._is_frontier(sid, act, conn, threshold) else 0
251
+ conn.execute(
252
+ "UPDATE nodes SET activation = ?, frontier = ? WHERE id = ?",
253
+ (act, frontier, sid),
254
+ )
255
+
256
+ def get_activation(self, state_id: str, conn: sqlite3.Connection, config: dict[str, Any]) -> float:
257
+ with self.lock:
258
+ if self.dirty:
259
+ self._recompute_all_dirty_locked(conn, config)
260
+ if state_id not in self.cache:
261
+ self._precompute_targets(state_id, conn, config)
262
+ return self.cache[state_id]
263
+
264
+ def reset(self) -> None:
265
+ with self.lock:
266
+ self.cache.clear()
267
+ self.dirty.clear()
268
+ self.cache_order.clear()
269
+ self.visiting.clear()
270
+ self.max_in_degree = 1
271
+ self.validation_counter = 0
272
+ self.max_in_degree_initialized = False
273
+
274
+
275
+ _default_ctx = ActivationContext()
276
+
277
+
278
+ def get_activation(state_id: str, conn: sqlite3.Connection, config: dict[str, Any], ctx: ActivationContext | None = None) -> float:
279
+ return (ctx or _default_ctx).get_activation(state_id, conn, config)
280
+
281
+
282
+ def mark_dirty(state_id: str, conn: sqlite3.Connection, ctx: ActivationContext | None = None) -> None:
283
+ (ctx or _default_ctx).mark_dirty(state_id, conn)
284
+
285
+
286
+ def recompute_all_dirty(conn: sqlite3.Connection, config: dict[str, Any], ctx: ActivationContext | None = None) -> None:
287
+ (ctx or _default_ctx).recompute_all_dirty(conn, config)
288
+
289
+
290
+ def increment_max_in_degree(target_id: str, conn: sqlite3.Connection, ctx: ActivationContext | None = None) -> None:
291
+ (ctx or _default_ctx).increment_max_in_degree(target_id, conn)
292
+
293
+
294
+ def reset_cache(ctx: ActivationContext | None = None) -> None:
295
+ (ctx or _default_ctx).reset()