hipcortex 0.5.0__tar.gz → 0.5.2__tar.gz

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 (49) hide show
  1. {hipcortex-0.5.0 → hipcortex-0.5.2}/PKG-INFO +6 -2
  2. {hipcortex-0.5.0 → hipcortex-0.5.2}/README.md +5 -1
  3. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/__init__.py +15 -15
  4. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/cli.py +1 -1
  5. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/client.py +26 -5
  6. hipcortex-0.5.2/hipcortex/install/mcp_server.py +794 -0
  7. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/PKG-INFO +6 -2
  8. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/SOURCES.txt +2 -0
  9. {hipcortex-0.5.0 → hipcortex-0.5.2}/pyproject.toml +2 -2
  10. hipcortex-0.5.2/tests/test_can_execute.py +33 -0
  11. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/adapters/__init__.py +0 -0
  12. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/adapters/autogen.py +0 -0
  13. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/adapters/common.py +0 -0
  14. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/adapters/crewai.py +0 -0
  15. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/async_client.py +0 -0
  16. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/config.py +0 -0
  17. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/daemon.py +0 -0
  18. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/doctor.py +0 -0
  19. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/framework_scaffold.py +0 -0
  20. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/indexer.py +0 -0
  21. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/SKILL.md +0 -0
  22. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/__init__.py +0 -0
  23. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/autogen.py.tmpl +0 -0
  24. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/crewai.py.tmpl +0 -0
  25. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/dspy.py.tmpl +0 -0
  26. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/langchain.py.tmpl +0 -0
  27. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/llamaindex.py.tmpl +0 -0
  28. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/n8n_curl.sh.tmpl +0 -0
  29. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install/templates/pydantic_ai.py.tmpl +0 -0
  30. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/install_hosts.py +0 -0
  31. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/langchain_memory.py +0 -0
  32. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex/llamaindex_storage.py +0 -0
  33. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/dependency_links.txt +0 -0
  34. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/entry_points.txt +0 -0
  35. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/requires.txt +0 -0
  36. {hipcortex-0.5.0 → hipcortex-0.5.2}/hipcortex.egg-info/top_level.txt +0 -0
  37. {hipcortex-0.5.0 → hipcortex-0.5.2}/setup.cfg +0 -0
  38. {hipcortex-0.5.0 → hipcortex-0.5.2}/setup.py +0 -0
  39. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_adapters_common.py +0 -0
  40. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_async_client.py +0 -0
  41. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_async_langchain.py +0 -0
  42. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_cli_install.py +0 -0
  43. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_client_config.py +0 -0
  44. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_config.py +0 -0
  45. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_daemon.py +0 -0
  46. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_doctor.py +0 -0
  47. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_harness_with_loop.py +0 -0
  48. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_host_install_e2e.py +0 -0
  49. {hipcortex-0.5.0 → hipcortex-0.5.2}/tests/test_reflect_predict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hipcortex
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Persistent causal memory for AI agents -- LangChain, LlamaIndex, AutoGen, CrewAI
5
5
  Author: HipCortex Contributors
6
6
  License: Apache-2.0
@@ -42,7 +42,7 @@ Requires-Dist: mem0ai>=0.1; extra == "all"
42
42
  Requires-Dist: tabulate>=0.9; extra == "all"
43
43
  Dynamic: requires-python
44
44
 
45
- # HipCortex Python SDK (`v0.5.0`)
45
+ # HipCortex Python SDK (`v0.5.2`)
46
46
 
47
47
  **Persistent causal topological memory, recursive Bayesian world-model prediction (`/worldmodel/rollout`), and automatic FSM skill compilation for autonomous AI agents.**
48
48
 
@@ -55,8 +55,12 @@ Runs against the standalone local Rust binary (`http://127.0.0.1:3030`) with `0.
55
55
  ```bash
56
56
  pip install hipcortex
57
57
  hipcortex doctor # verify server health + version after install
58
+ hipcortex install # wizard: MCP hosts + copies ~/.hipcortex-mcp/server.py (18 tools)
59
+ hipcortex channels # honesty matrix (native / mcp / framework / guide)
58
60
  ```
59
61
 
62
+ **Version matrix:** product pip/Cargo/MCP `serverInfo` = **0.5.2**; VS Code VSIX = **0.5.7** ([release](https://github.com/farmountain/HipCortex/releases/download/v0.5.7/hipcortex-memory-0.5.7.vsix)). See [docs/channels.md](../../docs/channels.md).
63
+
60
64
  ---
61
65
 
62
66
  ## ⚡ Quick Start: Multi-Tier Memory & World Model Prediction
@@ -1,4 +1,4 @@
1
- # HipCortex Python SDK (`v0.5.0`)
1
+ # HipCortex Python SDK (`v0.5.2`)
2
2
 
3
3
  **Persistent causal topological memory, recursive Bayesian world-model prediction (`/worldmodel/rollout`), and automatic FSM skill compilation for autonomous AI agents.**
4
4
 
@@ -11,8 +11,12 @@ Runs against the standalone local Rust binary (`http://127.0.0.1:3030`) with `0.
11
11
  ```bash
12
12
  pip install hipcortex
13
13
  hipcortex doctor # verify server health + version after install
14
+ hipcortex install # wizard: MCP hosts + copies ~/.hipcortex-mcp/server.py (18 tools)
15
+ hipcortex channels # honesty matrix (native / mcp / framework / guide)
14
16
  ```
15
17
 
18
+ **Version matrix:** product pip/Cargo/MCP `serverInfo` = **0.5.2**; VS Code VSIX = **0.5.7** ([release](https://github.com/farmountain/HipCortex/releases/download/v0.5.7/hipcortex-memory-0.5.7.vsix)). See [docs/channels.md](../../docs/channels.md).
19
+
16
20
  ---
17
21
 
18
22
  ## ⚡ Quick Start: Multi-Tier Memory & World Model Prediction
@@ -1,15 +1,15 @@
1
- """HipCortex Python SDK — AI memory engine client."""
2
-
3
- from .client import HipCortexClient
4
- from .async_client import AsyncHipCortexClient
5
- from .langchain_memory import HipCortexMemory, AsyncHipCortexMemory
6
- from .llamaindex_storage import HipCortexStorageContext
7
-
8
- __version__ = "0.5.0"
9
- __all__ = [
10
- "HipCortexClient",
11
- "AsyncHipCortexClient",
12
- "HipCortexMemory",
13
- "AsyncHipCortexMemory",
14
- "HipCortexStorageContext",
15
- ]
1
+ """HipCortex Python SDK - AI memory engine client."""
2
+
3
+ from .client import HipCortexClient
4
+ from .async_client import AsyncHipCortexClient
5
+ from .langchain_memory import HipCortexMemory, AsyncHipCortexMemory
6
+ from .llamaindex_storage import HipCortexStorageContext
7
+
8
+ __version__ = "0.5.2"
9
+ __all__ = [
10
+ "HipCortexClient",
11
+ "AsyncHipCortexClient",
12
+ "HipCortexMemory",
13
+ "AsyncHipCortexMemory",
14
+ "HipCortexStorageContext",
15
+ ]
@@ -1380,7 +1380,7 @@ def _fallback_channels() -> list[dict]:
1380
1380
  {"id": "claude-code", "name": "Claude Code", "status": "native", "install": "hipcortex install"},
1381
1381
  {"id": "cursor", "name": "Cursor", "status": "mcp", "install": "hipcortex install"},
1382
1382
  {"id": "windsurf", "name": "Windsurf", "status": "mcp", "install": "hipcortex install"},
1383
- {"id": "vscode-extension", "name": "VS Code VSIX", "status": "native", "install": "hipcortex-memory-0.5.5.vsix"},
1383
+ {"id": "vscode-extension", "name": "VS Code VSIX", "status": "native", "install": "hipcortex-memory-0.5.7.vsix"},
1384
1384
  {"id": "vscode-mcp", "name": "VS Code MCP", "status": "mcp", "install": "hipcortex install"},
1385
1385
  {"id": "cline", "name": "Cline", "status": "mcp", "install": "hipcortex install"},
1386
1386
  {"id": "roocode", "name": "RooCode", "status": "mcp", "install": "hipcortex install"},
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import time
6
7
  from typing import Any, Dict, List, Optional
7
8
 
@@ -567,20 +568,40 @@ class HipCortexClient:
567
568
  return (time.perf_counter() - t0) * 1000
568
569
 
569
570
  def can_execute(self, action: str = "rollout") -> bool:
570
- """Evaluate capability gates and self-model execution readiness."""
571
+ """Query SelfModel gate: GET /self/can-execute?operation=...
572
+
573
+ Falls back to /health ok only if self route is missing (404).
574
+ """
571
575
  try:
572
- status = self.health()
573
- return status.get("status") == "ok"
576
+ resp = self._session.get(
577
+ f"{self.base_url}/self/can-execute",
578
+ params={"operation": action},
579
+ timeout=self.timeout,
580
+ )
581
+ if resp.status_code == 200:
582
+ body = resp.json()
583
+ if "should_execute" in body:
584
+ return bool(body["should_execute"])
585
+ return False
586
+ if resp.status_code == 404:
587
+ status = self.health()
588
+ return status.get("status") == "ok"
589
+ return False
574
590
  except Exception:
575
591
  return False
576
592
 
577
593
  def rollout(
578
594
  self,
579
- initial_state: Dict[str, Any],
595
+ initial_state: Any,
580
596
  actions: List[str],
581
597
  max_depth: int = 5,
582
598
  ) -> Dict[str, Any]:
583
- """Predict next state trajectory via multi-step Monte Carlo Tree Search (/worldmodel/rollout)."""
599
+ """Multi-step predict via POST /worldmodel/rollout {initial_state, actions}.
600
+
601
+ ``initial_state`` may be a string or JSON-serializable object (stringified).
602
+ """
603
+ if not isinstance(initial_state, str):
604
+ initial_state = json.dumps(initial_state)
584
605
  payload = {
585
606
  "initial_state": initial_state,
586
607
  "actions": actions,
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env python3
2
+ """HipCortex MCP Server — Model Context Protocol (2024-11-05) over stdio.
3
+
4
+ Exposes HipCortex memory as tools for Cursor, Claude Code, Windsurf, Zed AI,
5
+ and any MCP-compatible AI coding assistant.
6
+
7
+ Protocol: JSON-RPC 2.0 over stdin/stdout
8
+ Dependencies: Python 3.9+ stdlib + requests (pip install requests)
9
+
10
+ Usage:
11
+ python server.py
12
+
13
+ Environment variables:
14
+ HIPCORTEX_URL Base URL (default: http://localhost:3030)
15
+ HIPCORTEX_API_KEY Optional X-Api-Key for managed SaaS tiers
16
+ HIPCORTEX_TIMEOUT Request timeout in seconds (default: 10)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ import urllib.parse
25
+ from typing import Any
26
+
27
+ import requests
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Configuration
31
+ # ---------------------------------------------------------------------------
32
+
33
+ HIPCORTEX_URL = os.getenv("HIPCORTEX_URL", "http://localhost:3030").rstrip("/")
34
+ API_KEY = os.getenv("HIPCORTEX_API_KEY", "")
35
+ TIMEOUT = int(os.getenv("HIPCORTEX_TIMEOUT", "10"))
36
+
37
+ # Session harness state (one MCP process lifetime). Soft substrate-first nudge:
38
+ # prefer get_live_beliefs / reflect before search_memory. Never hard-blocks.
39
+ # Disable: HIPCORTEX_HARNESS_SOFT=0
40
+ _live_beliefs_seen = False
41
+
42
+ _HARNESS_SEARCH_WARN = (
43
+ "[harness] Prefer get_live_beliefs FIRST before search (substrate-first). "
44
+ "Continuing with search..."
45
+ )
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # HTTP helpers
49
+ # ---------------------------------------------------------------------------
50
+
51
+ def _headers() -> dict:
52
+ h = {"Content-Type": "application/json"}
53
+ if API_KEY:
54
+ h["X-Api-Key"] = API_KEY
55
+ return h
56
+
57
+ def _get(path: str) -> dict:
58
+ resp = requests.get(f"{HIPCORTEX_URL}{path}", headers=_headers(), timeout=TIMEOUT)
59
+ resp.raise_for_status()
60
+ return resp.json()
61
+
62
+ def _post(path: str, body: dict) -> dict:
63
+ resp = requests.post(f"{HIPCORTEX_URL}{path}", json=body, headers=_headers(), timeout=TIMEOUT)
64
+ resp.raise_for_status()
65
+ return resp.json()
66
+
67
+ def _delete(path: str) -> dict:
68
+ resp = requests.delete(f"{HIPCORTEX_URL}{path}", headers=_headers(), timeout=TIMEOUT)
69
+ resp.raise_for_status()
70
+ return resp.json()
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # MCP tool definitions
74
+ # ---------------------------------------------------------------------------
75
+
76
+ TOOLS = [
77
+ {
78
+ "name": "add_memory",
79
+ "description": (
80
+ "Store a memory record in HipCortex. "
81
+ "Use to remember decisions, code patterns, bug fixes, architectural choices, "
82
+ "or any context that should persist across sessions."
83
+ ),
84
+ "inputSchema": {
85
+ "type": "object",
86
+ "required": ["actor", "action", "target"],
87
+ "properties": {
88
+ "actor": {
89
+ "type": "string",
90
+ "description": "Scope identifier — use project name or 'global' (e.g. 'my-app', 'user-42')",
91
+ },
92
+ "action": {
93
+ "type": "string",
94
+ "description": "What happened (e.g. 'decided', 'implemented', 'fixed', 'noted')",
95
+ },
96
+ "target": {
97
+ "type": "string",
98
+ "description": "The content to remember — be specific and self-contained",
99
+ },
100
+ "record_type": {
101
+ "type": "string",
102
+ "enum": ["Temporal", "Symbolic", "Procedural", "Reflexion", "Perception", "Episodic", "Semantic", "LongTerm", "ShortTerm", "Working", "Reflexive", "Perceptual"],
103
+ "default": "Temporal",
104
+ },
105
+ "ttl_seconds": {
106
+ "type": "integer",
107
+ "description": "Auto-expire after N seconds (omit for permanent memory)",
108
+ },
109
+ },
110
+ },
111
+ },
112
+ {
113
+ "name": "search_memory",
114
+ "description": (
115
+ "Search stored memories by keyword. "
116
+ "Use before starting a task to recall relevant past decisions or context. "
117
+ "Prefer get_live_beliefs first for project-state questions "
118
+ "(substrate-first soft harness may warn if search is used first). "
119
+ "Search remains available."
120
+ ),
121
+ "inputSchema": {
122
+ "type": "object",
123
+ "required": ["query"],
124
+ "properties": {
125
+ "query": {"type": "string", "description": "What to search for"},
126
+ "actor": {"type": "string", "description": "Filter by actor/scope (optional)"},
127
+ "limit": {"type": "integer", "default": 10},
128
+ },
129
+ },
130
+ },
131
+ {
132
+ "name": "forget_actor",
133
+ "description": "Delete all memories for an actor (GDPR right-to-forget / fresh start).",
134
+ "inputSchema": {
135
+ "type": "object",
136
+ "required": ["actor"],
137
+ "properties": {
138
+ "actor": {"type": "string", "description": "Actor whose memories to delete"},
139
+ },
140
+ },
141
+ },
142
+ {
143
+ "name": "get_stats",
144
+ "description": "Get memory store statistics: total records, types, unique actors.",
145
+ "inputSchema": {"type": "object", "properties": {}},
146
+ },
147
+ {
148
+ "name": "search_code",
149
+ "description": (
150
+ "Search the HipCortex code knowledge graph for relevant symbols, functions, classes. "
151
+ "Use this BEFORE reading files — it returns targeted symbol info in ~100 tokens instead of "
152
+ "reading entire files (~10k tokens). Works after running 'hipcortex index' on the codebase."
153
+ ),
154
+ "inputSchema": {
155
+ "type": "object",
156
+ "required": ["query"],
157
+ "properties": {
158
+ "query": {
159
+ "type": "string",
160
+ "description": "Symbol name, function, class, or concept to find in the codebase",
161
+ },
162
+ "limit": {
163
+ "type": "integer",
164
+ "default": 5,
165
+ "description": "Max symbols to return",
166
+ },
167
+ },
168
+ },
169
+ },
170
+ {
171
+ "name": "link_memories",
172
+ "description": "Create a directed graph edge between two memory records in the CausalTopoGraph. Use to model causal, temporal, or semantic relationships between memories.",
173
+ "inputSchema": {
174
+ "type": "object",
175
+ "required": ["source_id", "target_id"],
176
+ "properties": {
177
+ "source_id": {"type": "string", "description": "UUID of the source memory record"},
178
+ "target_id": {"type": "string", "description": "UUID of the target memory record"},
179
+ "relation": {"type": "string", "default": "related", "description": "Edge label (e.g. \"caused\", \"follows\", \"related\")"},
180
+ },
181
+ },
182
+ },
183
+ {
184
+ "name": "get_neighbors",
185
+ "description": "Return memory records directly linked to a seed record via the CausalTopoGraph. Use to explore local context around a known memory.",
186
+ "inputSchema": {
187
+ "type": "object",
188
+ "required": ["record_id"],
189
+ "properties": {
190
+ "record_id": {"type": "string", "description": "UUID of the seed memory record"},
191
+ "limit": {"type": "integer", "default": 10, "description": "Max neighbors to return"},
192
+ },
193
+ },
194
+ },
195
+ {
196
+ "name": "search_related",
197
+ "description": "PPR-ranked related memory search. Uses Personalized PageRank (alpha=0.85, 20 rounds) to surface the most contextually relevant memories seeded from a given record. Call after search_memory to expand context graph-first.",
198
+ "inputSchema": {
199
+ "type": "object",
200
+ "required": ["seed_id"],
201
+ "properties": {
202
+ "seed_id": {"type": "string", "description": "UUID of the seed memory record"},
203
+ "limit": {"type": "integer", "default": 10, "description": "Max results to return"},
204
+ },
205
+ },
206
+ },
207
+ {
208
+ "name": "graph_ppr",
209
+ "description": (
210
+ "Personalized PageRank over the live CausalTopoGraph substrate. "
211
+ "Use for topology-first exploration of symbolic graph nodes (not only memory UUIDs). "
212
+ "Prefer after link_memories or deconstruct_hypothesis."
213
+ ),
214
+ "inputSchema": {
215
+ "type": "object",
216
+ "properties": {
217
+ "seed": {"type": "string", "description": "Optional seed node symbolic_id"},
218
+ "limit": {"type": "integer", "default": 10},
219
+ },
220
+ },
221
+ },
222
+ {
223
+ "name": "deconstruct_hypothesis",
224
+ "description": (
225
+ "Parse free text into candidate causal nodes/edges (rule-based; optional llm_json). "
226
+ "Use before applying structure to the topo graph via apply_hypothesis."
227
+ ),
228
+ "inputSchema": {
229
+ "type": "object",
230
+ "required": ["text"],
231
+ "properties": {
232
+ "text": {"type": "string", "description": "Hypothesis text, e.g. 'rain causes floods'"},
233
+ "llm_json": {
234
+ "type": "string",
235
+ "description": "Optional LLM JSON: {\"edges\":[{\"from\",\"to\",\"relation\"?}]}",
236
+ },
237
+ "apply": {
238
+ "type": "boolean",
239
+ "default": False,
240
+ "description": "If true, also apply edges to live topo graph (POST /topo/apply-hyp)",
241
+ },
242
+ },
243
+ },
244
+ },
245
+ {
246
+ "name": "check_topo_edge",
247
+ "description": (
248
+ "Check whether adding a causal edge from→to would contradict the CausalTopoGraph "
249
+ "(cycle or high-strength reverse)."
250
+ ),
251
+ "inputSchema": {
252
+ "type": "object",
253
+ "required": ["from", "to"],
254
+ "properties": {
255
+ "from": {"type": "string"},
256
+ "to": {"type": "string"},
257
+ },
258
+ },
259
+ },
260
+ {
261
+ "name": "rollout",
262
+ "description": (
263
+ "World-model multi-step rollout. Default Dirichlet MAP after observe; "
264
+ "mode=mcts uses goal-shaped UCB1 search. Requires prior worldmodel observations "
265
+ "or mode=mcts with observed transitions."
266
+ ),
267
+ "inputSchema": {
268
+ "type": "object",
269
+ "required": ["initial_state"],
270
+ "properties": {
271
+ "initial_state": {"type": "string"},
272
+ "actions": {
273
+ "type": "array",
274
+ "items": {"type": "string"},
275
+ "description": "Required unless mode=mcts",
276
+ },
277
+ "mode": {
278
+ "type": "string",
279
+ "enum": ["dirichlet", "mcts", "ensemble"],
280
+ "default": "dirichlet",
281
+ },
282
+ "goal_state": {"type": "string", "description": "Goal for MCTS reward shaping"},
283
+ "iterations": {"type": "integer", "default": 50},
284
+ "max_depth": {"type": "integer", "default": 3},
285
+ },
286
+ },
287
+ },
288
+ {
289
+ "name": "can_execute",
290
+ "description": "Query SelfModel gate: should the agent execute this operation?",
291
+ "inputSchema": {
292
+ "type": "object",
293
+ "required": ["operation"],
294
+ "properties": {
295
+ "operation": {
296
+ "type": "string",
297
+ "description": "Operation name (e.g. add_memory, rollout, search_memory)",
298
+ },
299
+ },
300
+ },
301
+ },
302
+ {
303
+ "name": "delete_memory",
304
+ "description": "Delete a single memory record by UUID. Use for targeted removal without forgetting an entire actor.",
305
+ "inputSchema": {
306
+ "type": "object",
307
+ "required": ["record_id"],
308
+ "properties": {
309
+ "record_id": {"type": "string", "description": "UUID of the memory record to delete"},
310
+ },
311
+ },
312
+ },
313
+ {
314
+ "name": "get_live_beliefs",
315
+ "description": (
316
+ "Get the latest unique beliefs per actor+action pair. "
317
+ "Returns the most recent value for each fact the system has observed. "
318
+ "Per Claude Agent Harness: call this FIRST before any project-state question."
319
+ ),
320
+ "inputSchema": {
321
+ "type": "object",
322
+ "properties": {
323
+ "actor": {"type": "string", "description": "Filter to a specific actor (optional)"},
324
+ "limit": {"type": "integer", "default": 20, "description": "Max beliefs to return"},
325
+ },
326
+ },
327
+ },
328
+ {
329
+ "name": "purge_expired",
330
+ "description": "Trigger purge of all TTL-expired memory records from the store.",
331
+ "inputSchema": {"type": "object", "properties": {}},
332
+ },
333
+ {
334
+ "name": "reflect",
335
+ "description": (
336
+ "Substrate-first CoT / Bayesian hypothesis sampling via AureusBridge. "
337
+ "POST /memory/reflect — search memory context, sample hypotheses with confidence. "
338
+ "Use for uncertain architectural decisions or counterfactual reasoning before final answer."
339
+ ),
340
+ "inputSchema": {
341
+ "type": "object",
342
+ "required": ["query"],
343
+ "properties": {
344
+ "query": {
345
+ "type": "string",
346
+ "description": "What to reflect on (e.g. 'Postgres vs RocksDB for session store')",
347
+ },
348
+ },
349
+ },
350
+ },
351
+ {
352
+ "name": "predict",
353
+ "description": (
354
+ "World-model single-step prediction P(s'|s,a). "
355
+ "POST /worldmodel/predict with state + action; returns next-state probabilities + entropy."
356
+ ),
357
+ "inputSchema": {
358
+ "type": "object",
359
+ "required": ["state", "action"],
360
+ "properties": {
361
+ "state": {
362
+ "type": "string",
363
+ "description": "Current world-model state label",
364
+ },
365
+ "action": {
366
+ "type": "string",
367
+ "description": "Action applied from that state",
368
+ },
369
+ },
370
+ },
371
+ },
372
+ ]
373
+
374
+ # ---------------------------------------------------------------------------
375
+ # Tool execution
376
+ # ---------------------------------------------------------------------------
377
+
378
+ def handle_add_memory(args: dict) -> str:
379
+ body = {"actor": args["actor"], "action": args["action"], "target": args["target"]}
380
+ if "record_type" in args:
381
+ body["record_type"] = args["record_type"]
382
+ if "ttl_seconds" in args:
383
+ body["ttl_seconds"] = args["ttl_seconds"]
384
+ result = _post("/memory/add", body)
385
+ return f"✓ Memory stored (id: {result.get('record_id', 'unknown')})\n [{args['action']}] {args['target']}"
386
+
387
+
388
+ def handle_search_memory(args: dict) -> str:
389
+ body = {"query": args["query"], "limit": args.get("limit", 10)}
390
+ result = _post("/memory/search", body)
391
+ search_results = result.get("results", [])
392
+
393
+ if search_results:
394
+ lines = [
395
+ f"• [{r.get('record', {}).get('action', '?')}] {r.get('record', {}).get('target', '')} "
396
+ f"(actor: {r.get('record', {}).get('actor', '?')}, score: {r.get('score', 0):.2f})"
397
+ for r in search_results
398
+ ]
399
+ return f"Found {len(lines)} result(s):\n" + "\n".join(lines)
400
+
401
+ # Fallback: keyword query
402
+ params: dict = {"limit": args.get("limit", 10)}
403
+ if "actor" in args:
404
+ params["actor"] = args["actor"]
405
+ qs = urllib.parse.urlencode(params)
406
+ result2 = _get(f"/memory/query?{qs}")
407
+ records = result2.get("records", [])
408
+ if not records:
409
+ return "No memories found."
410
+ lines = [
411
+ f"• [{r.get('action', '?')}] {r.get('target', '')} (actor: {r.get('actor', '?')})"
412
+ for r in records
413
+ ]
414
+ return f"Found {len(lines)} record(s):\n" + "\n".join(lines)
415
+
416
+
417
+ def handle_forget_actor(args: dict) -> str:
418
+ actor = args["actor"]
419
+ result = _delete(f"/memory/forget/{actor}")
420
+ deleted = result.get("records_deleted", 0)
421
+ symbolic = result.get("symbolic_nodes_deleted", 0)
422
+ return f"✓ Deleted {deleted} records and {symbolic} symbolic nodes for '{actor}'."
423
+
424
+
425
+ def handle_get_stats(_args: dict) -> str:
426
+ result = _get("/stats")
427
+ total = result.get("total_records", 0)
428
+ actors = result.get("unique_actors", 0)
429
+ by_type = result.get("by_type", {})
430
+ metered = result.get("metering_enabled", False)
431
+ lines = [
432
+ "HipCortex memory store:",
433
+ f" Total records: {total}",
434
+ f" Unique actors: {actors}",
435
+ f" Metering: {'enabled' if metered else 'open mode'}",
436
+ ]
437
+ if by_type:
438
+ lines.append(" By type:")
439
+ for t, count in sorted(by_type.items()):
440
+ lines.append(f" {t}: {count}")
441
+ return "\n".join(lines)
442
+
443
+
444
+ def handle_search_code(args: dict) -> str:
445
+ query = args.get("query", "")
446
+ limit = args.get("limit", 5)
447
+ try:
448
+ resp = requests.get(
449
+ f"{HIPCORTEX_URL}/graph/search",
450
+ params={"q": query, "limit": limit},
451
+ headers=_headers(),
452
+ timeout=TIMEOUT,
453
+ )
454
+ resp.raise_for_status()
455
+ data = resp.json()
456
+ nodes = data.get("nodes", [])
457
+ if not nodes:
458
+ return (
459
+ f"No code symbols found for '{query}'. "
460
+ "Run 'hipcortex index .' to index the codebase first."
461
+ )
462
+ lines = []
463
+ for n in nodes:
464
+ props = n.get("properties", {})
465
+ label = n.get("label", "?")
466
+ line = props.get("line", "?")
467
+ sig = props.get("signature", props.get("name", label))
468
+ fpath = props.get("file", "")
469
+ lines.append(f"• {sig} [{fpath}:{line}]")
470
+ return f"Code symbols matching '{query}':\n" + "\n".join(lines)
471
+ except Exception as e:
472
+ return f"Error searching code graph: {e}. Run 'hipcortex index .' first."
473
+
474
+
475
+ def handle_link_memories(args: dict) -> str:
476
+ body = {
477
+ "source_id": args["source_id"],
478
+ "target_id": args["target_id"],
479
+ "relation": args.get("relation", "related"),
480
+ }
481
+ result = _post("/memory/link", body)
482
+ ok = result.get("success", False)
483
+ rel = result.get("relation", "related")
484
+ return (
485
+ f"✓ Link created: {args['source_id']} --[{rel}]--> {args['target_id']}"
486
+ if ok else f"✗ Link failed: {result}"
487
+ )
488
+
489
+
490
+ def handle_get_neighbors(args: dict) -> str:
491
+ record_id = args["record_id"]
492
+ limit = args.get("limit", 10)
493
+ qs = urllib.parse.urlencode({"limit": limit})
494
+ result = _get(f"/memory/neighbors/{record_id}?{qs}")
495
+ records = result.get("records", [])
496
+ if not records:
497
+ return f"No neighbors found for record {record_id}."
498
+ lines = [
499
+ f"• [{r.get('action', '?')}] {r.get('target', '')} (actor: {r.get('actor', '?')}, id: {r.get('id', '?')})"
500
+ for r in records
501
+ ]
502
+ return f"{len(lines)} neighbor(s) of {record_id}:\n" + "\n".join(lines)
503
+
504
+
505
+ def handle_search_related(args: dict) -> str:
506
+ seed_id = args["seed_id"]
507
+ limit = args.get("limit", 10)
508
+ qs = urllib.parse.urlencode({"seed_id": seed_id, "limit": limit})
509
+ result = _get(f"/memory/search/related?{qs}")
510
+ results = result.get("results", [])
511
+ if not results:
512
+ return f"No related memories found for seed {seed_id}. Ensure the record is linked to others."
513
+ lines = [
514
+ f"• [{r.get('record', {}).get('action', '?')}] {r.get('record', {}).get('target', '')} "
515
+ f"(score: {r.get('score', 0):.3f})"
516
+ for r in results
517
+ ]
518
+ return f"{len(lines)} PPR-related result(s):\n" + "\n".join(lines)
519
+
520
+
521
+ def handle_delete_memory(args: dict) -> str:
522
+ record_id = args["record_id"]
523
+ result = _delete(f"/memory/{record_id}")
524
+ ok = result.get("success", False)
525
+ return (
526
+ f"✓ Deleted record {record_id}."
527
+ if ok else f"✗ Delete failed: {result}"
528
+ )
529
+
530
+
531
+ def handle_get_live_beliefs(args: dict) -> str:
532
+ params: dict = {"limit": args.get("limit", 20)}
533
+ if "actor" in args:
534
+ params["actor"] = args["actor"]
535
+ qs = urllib.parse.urlencode(params)
536
+ result = _get(f"/memory/live_beliefs?{qs}")
537
+ beliefs = result.get("beliefs", [])
538
+ if not beliefs:
539
+ return "No live beliefs found."
540
+ lines = [
541
+ f"• [{b.get('action', '?')}] {b.get('target', '')} (actor: {b.get('actor', '?')})"
542
+ for b in beliefs
543
+ ]
544
+ return f"Live beliefs ({result.get('total', len(beliefs))}):\n" + "\n".join(lines)
545
+
546
+
547
+ def handle_purge_expired(_args: dict) -> str:
548
+ result = _post("/memory/consolidate", {"dry_run": False})
549
+ deleted = result.get("deleted", 0)
550
+ return f"✓ Purge completed. Deleted {deleted} expired records."
551
+
552
+
553
+ def handle_reflect(args: dict) -> str:
554
+ query = args["query"]
555
+ result = _post("/memory/reflect", {"query": query})
556
+ if "error" in result and "hypothesis" not in result:
557
+ return f"✗ Reflect failed: {result.get('error')}"
558
+ hyp = result.get("hypothesis", "")
559
+ conf = result.get("confidence", 0)
560
+ evidence = result.get("evidence", [])
561
+ loops = result.get("loops_run", 0)
562
+ llm = result.get("llm_available", False)
563
+ fallback = result.get("is_fallback", False)
564
+ lines = [
565
+ f"Reflect on: {query}",
566
+ f" Hypothesis: {hyp}",
567
+ f" Confidence: {conf}",
568
+ f" Loops run: {loops}",
569
+ f" LLM: {'available' if llm else 'unavailable'}",
570
+ f" Fallback: {fallback}",
571
+ ]
572
+ if evidence:
573
+ lines.append(" Evidence:")
574
+ for e in evidence if isinstance(evidence, list) else [evidence]:
575
+ lines.append(f" • {e}")
576
+ return "\n".join(lines)
577
+
578
+
579
+ def handle_predict(args: dict) -> str:
580
+ state = args["state"]
581
+ action = args["action"]
582
+ result = _post("/worldmodel/predict", {"state": state, "action": action})
583
+ if "error" in result and "probabilities" not in result:
584
+ return f"✗ Predict failed: {result.get('error')}"
585
+ probs = result.get("probabilities", {})
586
+ entropy = result.get("entropy", 0)
587
+ obs = result.get("observation_count", 0)
588
+ from_state = result.get("from_state", state)
589
+ lines = [
590
+ f"Predict P(s'|{from_state}, {result.get('action', action)}):",
591
+ f" Entropy: {entropy}",
592
+ f" Observation count: {obs}",
593
+ ]
594
+ if probs:
595
+ lines.append(" Next-state probabilities:")
596
+ if isinstance(probs, dict):
597
+ for s, p in sorted(probs.items(), key=lambda x: -float(x[1]) if x[1] is not None else 0):
598
+ lines.append(f" • {s}: {p}")
599
+ else:
600
+ lines.append(f" {probs}")
601
+ else:
602
+ lines.append(" (no probability mass — observe transitions first)")
603
+ return "\n".join(lines)
604
+
605
+
606
+ def handle_graph_ppr(args: dict) -> str:
607
+ params: dict = {"limit": args.get("limit", 10)}
608
+ if args.get("seed"):
609
+ params["seed"] = args["seed"]
610
+ qs = urllib.parse.urlencode(params)
611
+ result = _get(f"/topo/ppr?{qs}")
612
+ results = result.get("results", [])
613
+ if not results:
614
+ return f"No PPR results (seed={args.get('seed')}). Link nodes or apply a hypothesis first."
615
+ lines = [f"• {r.get('id')} (score: {r.get('score', 0):.4f})" for r in results]
616
+ return f"Topo PPR ({len(lines)}):\n" + "\n".join(lines)
617
+
618
+
619
+ def handle_deconstruct_hypothesis(args: dict) -> str:
620
+ body: dict = {"text": args["text"]}
621
+ if args.get("llm_json"):
622
+ body["llm_json"] = args["llm_json"]
623
+ apply = bool(args.get("apply", False))
624
+ path = "/topo/apply-hyp" if apply else "/topo/deconstruct"
625
+ result = _post(path, body)
626
+ if not result.get("success", True) and result.get("error"):
627
+ return f"✗ Deconstruct failed: {result.get('error')}"
628
+ edges = result.get("edges") or result.get("hypothesis", {}).get("edges") or []
629
+ nodes = result.get("nodes") or result.get("hypothesis", {}).get("nodes") or []
630
+ lines = [f"Nodes: {', '.join(nodes) if nodes else '(none)'}"]
631
+ if edges:
632
+ lines.append("Edges:")
633
+ for e in edges:
634
+ if isinstance(e, dict):
635
+ lines.append(
636
+ f" • {e.get('from')} --[{e.get('relation', '?')}]--> {e.get('to')} "
637
+ f"(conf={e.get('confidence', '?')})"
638
+ )
639
+ else:
640
+ lines.append(f" • {e}")
641
+ if apply:
642
+ lines.append(
643
+ f"Applied: nodes+={result.get('added_nodes', 0)} edges+={result.get('added_edges', 0)}"
644
+ )
645
+ if result.get("skipped"):
646
+ lines.append(f"Skipped: {result.get('skipped')}")
647
+ return "\n".join(lines)
648
+
649
+
650
+ def handle_check_topo_edge(args: dict) -> str:
651
+ result = _post("/topo/check-edge", {"from": args["from"], "to": args["to"]})
652
+ if result.get("error") and not result.get("success", True):
653
+ return f"✗ check failed: {result.get('error')}"
654
+ bad = result.get("would_contradict", False)
655
+ report = result.get("report")
656
+ if bad:
657
+ reason = (report or {}).get("reason", "contradiction") if isinstance(report, dict) else "contradiction"
658
+ return f"Would contradict: {args['from']} → {args['to']} ({reason})"
659
+ return f"OK to add: {args['from']} → {args['to']} (no contradiction detected)"
660
+
661
+
662
+ def handle_rollout(args: dict) -> str:
663
+ body: dict = {
664
+ "initial_state": args["initial_state"],
665
+ "mode": args.get("mode", "dirichlet"),
666
+ }
667
+ if "actions" in args:
668
+ body["actions"] = args["actions"]
669
+ if "goal_state" in args:
670
+ body["goal_state"] = args["goal_state"]
671
+ if "iterations" in args:
672
+ body["iterations"] = args["iterations"]
673
+ if "max_depth" in args:
674
+ body["max_depth"] = args["max_depth"]
675
+ result = _post("/worldmodel/rollout", body)
676
+ if result.get("error") and "predicted_state" not in result:
677
+ return f"✗ Rollout failed: {result.get('error')}"
678
+ lines = [
679
+ f"Rollout mode={result.get('mode', body.get('mode'))}",
680
+ f" initial: {result.get('initial_state', args['initial_state'])}",
681
+ f" predicted: {result.get('predicted_state', '?')}",
682
+ f" confidence: {result.get('confidence', '?')}",
683
+ ]
684
+ if result.get("best_action"):
685
+ lines.append(f" best_action: {result.get('best_action')}")
686
+ if result.get("goal_state"):
687
+ lines.append(f" goal: {result.get('goal_state')}")
688
+ return "\n".join(lines)
689
+
690
+
691
+ def handle_can_execute(args: dict) -> str:
692
+ op = args["operation"]
693
+ qs = urllib.parse.urlencode({"operation": op})
694
+ result = _get(f"/self/can-execute?{qs}")
695
+ if result.get("error") and "should_execute" not in result:
696
+ return f"✗ can_execute failed: {result.get('error')}"
697
+ ok = result.get("should_execute")
698
+ conf = result.get("confidence", "?")
699
+ rat = result.get("rationale", "")
700
+ return f"can_execute({op}) = {ok} (confidence={conf})\n {rat}".rstrip()
701
+
702
+
703
+ def dispatch_tool(name: str, args: dict) -> str:
704
+ global _live_beliefs_seen
705
+ handlers = {
706
+ "add_memory": handle_add_memory,
707
+ "search_memory": handle_search_memory,
708
+ "forget_actor": handle_forget_actor,
709
+ "get_stats": handle_get_stats,
710
+ "search_code": handle_search_code,
711
+ "link_memories": handle_link_memories,
712
+ "get_neighbors": handle_get_neighbors,
713
+ "search_related": handle_search_related,
714
+ "graph_ppr": handle_graph_ppr,
715
+ "deconstruct_hypothesis": handle_deconstruct_hypothesis,
716
+ "check_topo_edge": handle_check_topo_edge,
717
+ "rollout": handle_rollout,
718
+ "can_execute": handle_can_execute,
719
+ "delete_memory": handle_delete_memory,
720
+ "get_live_beliefs": handle_get_live_beliefs,
721
+ "purge_expired": handle_purge_expired,
722
+ "reflect": handle_reflect,
723
+ "predict": handle_predict,
724
+ }
725
+ handler = handlers.get(name)
726
+ if handler is None:
727
+ raise ValueError(f"Unknown tool: {name}")
728
+ result = handler(args)
729
+ if name in ("get_live_beliefs", "reflect"):
730
+ _live_beliefs_seen = True
731
+ elif name == "search_memory":
732
+ soft_on = os.getenv("HIPCORTEX_HARNESS_SOFT", "1") != "0"
733
+ if soft_on and not _live_beliefs_seen:
734
+ result = _HARNESS_SEARCH_WARN + "\n" + result
735
+ return result
736
+
737
+ # ---------------------------------------------------------------------------
738
+ # JSON-RPC 2.0 transport
739
+ # ---------------------------------------------------------------------------
740
+
741
+ def respond(id_: Any, result: Any = None, error: Any = None) -> None:
742
+ msg: dict = {"jsonrpc": "2.0", "id": id_}
743
+ if error is not None:
744
+ msg["error"] = {"code": -32000, "message": str(error)}
745
+ else:
746
+ msg["result"] = result
747
+ sys.stdout.write(json.dumps(msg) + "\n")
748
+ sys.stdout.flush()
749
+
750
+
751
+ def main() -> None:
752
+ for raw_line in sys.stdin:
753
+ line = raw_line.strip()
754
+ if not line:
755
+ continue
756
+ try:
757
+ req = json.loads(line)
758
+ except json.JSONDecodeError as e:
759
+ respond(None, error=f"JSON parse error: {e}")
760
+ continue
761
+
762
+ method = req.get("method", "")
763
+ id_ = req.get("id")
764
+ params = req.get("params", {})
765
+
766
+ if method == "initialize":
767
+ respond(id_, {
768
+ "protocolVersion": "2024-11-05",
769
+ "capabilities": {"tools": {}},
770
+ "serverInfo": {"name": "hipcortex", "version": "0.5.2"},
771
+ })
772
+ elif method == "initialized":
773
+ pass # notification — no response
774
+ elif method == "tools/list":
775
+ respond(id_, {"tools": TOOLS})
776
+ elif method == "tools/call":
777
+ tool_name = params.get("name", "")
778
+ tool_args = params.get("arguments", {})
779
+ try:
780
+ content = dispatch_tool(tool_name, tool_args)
781
+ respond(id_, {"content": [{"type": "text", "text": content}]})
782
+ except requests.RequestException as e:
783
+ respond(id_, error=f"HipCortex server error: {e}")
784
+ except Exception as e:
785
+ respond(id_, error=str(e))
786
+ elif method == "ping":
787
+ respond(id_, {})
788
+ else:
789
+ if id_ is not None:
790
+ respond(id_, error=f"Unknown method: {method}")
791
+
792
+
793
+ if __name__ == "__main__":
794
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hipcortex
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Persistent causal memory for AI agents -- LangChain, LlamaIndex, AutoGen, CrewAI
5
5
  Author: HipCortex Contributors
6
6
  License: Apache-2.0
@@ -42,7 +42,7 @@ Requires-Dist: mem0ai>=0.1; extra == "all"
42
42
  Requires-Dist: tabulate>=0.9; extra == "all"
43
43
  Dynamic: requires-python
44
44
 
45
- # HipCortex Python SDK (`v0.5.0`)
45
+ # HipCortex Python SDK (`v0.5.2`)
46
46
 
47
47
  **Persistent causal topological memory, recursive Bayesian world-model prediction (`/worldmodel/rollout`), and automatic FSM skill compilation for autonomous AI agents.**
48
48
 
@@ -55,8 +55,12 @@ Runs against the standalone local Rust binary (`http://127.0.0.1:3030`) with `0.
55
55
  ```bash
56
56
  pip install hipcortex
57
57
  hipcortex doctor # verify server health + version after install
58
+ hipcortex install # wizard: MCP hosts + copies ~/.hipcortex-mcp/server.py (18 tools)
59
+ hipcortex channels # honesty matrix (native / mcp / framework / guide)
58
60
  ```
59
61
 
62
+ **Version matrix:** product pip/Cargo/MCP `serverInfo` = **0.5.2**; VS Code VSIX = **0.5.7** ([release](https://github.com/farmountain/HipCortex/releases/download/v0.5.7/hipcortex-memory-0.5.7.vsix)). See [docs/channels.md](../../docs/channels.md).
63
+
60
64
  ---
61
65
 
62
66
  ## ⚡ Quick Start: Multi-Tier Memory & World Model Prediction
@@ -25,6 +25,7 @@ hipcortex/adapters/common.py
25
25
  hipcortex/adapters/crewai.py
26
26
  hipcortex/install/SKILL.md
27
27
  hipcortex/install/__init__.py
28
+ hipcortex/install/mcp_server.py
28
29
  hipcortex/install/templates/autogen.py.tmpl
29
30
  hipcortex/install/templates/crewai.py.tmpl
30
31
  hipcortex/install/templates/dspy.py.tmpl
@@ -35,6 +36,7 @@ hipcortex/install/templates/pydantic_ai.py.tmpl
35
36
  tests/test_adapters_common.py
36
37
  tests/test_async_client.py
37
38
  tests/test_async_langchain.py
39
+ tests/test_can_execute.py
38
40
  tests/test_cli_install.py
39
41
  tests/test_client_config.py
40
42
  tests/test_config.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hipcortex"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Persistent causal memory for AI agents -- LangChain, LlamaIndex, AutoGen, CrewAI"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -44,7 +44,7 @@ where = ["."]
44
44
  include = ["hipcortex*"]
45
45
 
46
46
  [tool.setuptools.package-data]
47
- "hipcortex.install" = ["SKILL.md", "templates/*.tmpl"]
47
+ "hipcortex.install" = ["SKILL.md", "templates/*.tmpl", "mcp_server.py"]
48
48
 
49
49
  [tool.pytest.ini_options]
50
50
  asyncio_mode = "auto"
@@ -0,0 +1,33 @@
1
+ """can_execute uses /self/can-execute when available."""
2
+ from unittest.mock import MagicMock
3
+
4
+ from hipcortex.client import HipCortexClient
5
+
6
+
7
+ def test_can_execute_uses_self_route():
8
+ client = HipCortexClient(base_url="http://127.0.0.1:3030")
9
+ mock_resp = MagicMock()
10
+ mock_resp.status_code = 200
11
+ mock_resp.json.return_value = {
12
+ "operation": "rollout",
13
+ "should_execute": True,
14
+ "confidence": 0.9,
15
+ }
16
+ client._session = MagicMock()
17
+ client._session.get.return_value = mock_resp
18
+
19
+ assert client.can_execute("rollout") is True
20
+ client._session.get.assert_called_once()
21
+ args, kwargs = client._session.get.call_args
22
+ assert args[0].endswith("/self/can-execute")
23
+ assert kwargs["params"]["operation"] == "rollout"
24
+
25
+
26
+ def test_can_execute_false_when_gate_rejects():
27
+ client = HipCortexClient(base_url="http://127.0.0.1:3030")
28
+ mock_resp = MagicMock()
29
+ mock_resp.status_code = 200
30
+ mock_resp.json.return_value = {"should_execute": False, "rationale": "busy"}
31
+ client._session = MagicMock()
32
+ client._session.get.return_value = mock_resp
33
+ assert client.can_execute("heavy_op") is False
File without changes
File without changes
File without changes
File without changes
File without changes