ctxora 6.2.0

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/README.vi.md +441 -0
  4. package/bin/ctxora.mjs +147 -0
  5. package/package.json +45 -0
  6. package/pyproject.toml +59 -0
  7. package/src/chunking/compressor.py +104 -0
  8. package/src/chunking/treesitter_chunker.py +240 -0
  9. package/src/compact/anthropic.py +98 -0
  10. package/src/compact/gemini.py +88 -0
  11. package/src/compact/handoff.py +179 -0
  12. package/src/compact/openai.py +318 -0
  13. package/src/compact/summarizer.py +186 -0
  14. package/src/context/assembler.py +298 -0
  15. package/src/context/budgeting.py +137 -0
  16. package/src/context/sanitizer.py +23 -0
  17. package/src/evaluation/__init__.py +1 -0
  18. package/src/evaluation/gates.py +172 -0
  19. package/src/evaluation/metrics.py +41 -0
  20. package/src/harness_context/__init__.py +5 -0
  21. package/src/harness_context/adapters/__init__.py +1 -0
  22. package/src/harness_context/adapters/clients/__init__.py +4 -0
  23. package/src/harness_context/adapters/clients/formatters.py +47 -0
  24. package/src/harness_context/adapters/clients/profiles.py +29 -0
  25. package/src/harness_context/adapters/ecc/__init__.py +4 -0
  26. package/src/harness_context/adapters/ecc/detection.py +41 -0
  27. package/src/harness_context/adapters/ecc/mapping.py +32 -0
  28. package/src/harness_context/adapters/ecc/memory_reader.py +162 -0
  29. package/src/harness_context/adapters/ecc/provenance.py +16 -0
  30. package/src/harness_context/api/__init__.py +1 -0
  31. package/src/harness_context/api/v2/__init__.py +12 -0
  32. package/src/harness_context/api/v2/contracts.py +119 -0
  33. package/src/harness_context/api/v2/diagnostics.py +13 -0
  34. package/src/harness_context/api/v2/enums.py +17 -0
  35. package/src/harness_context/api/v2/errors.py +32 -0
  36. package/src/harness_context/api/v2/models.py +4 -0
  37. package/src/harness_context/api/v2/requests.py +17 -0
  38. package/src/harness_context/api/v2/responses.py +22 -0
  39. package/src/harness_context/application/__init__.py +3 -0
  40. package/src/harness_context/application/container.py +31 -0
  41. package/src/harness_context/application/context_service.py +51 -0
  42. package/src/harness_context/application/ecc_service.py +7 -0
  43. package/src/harness_context/application/handoff_service.py +11 -0
  44. package/src/harness_context/application/memory_service.py +9 -0
  45. package/src/harness_context/application/protocols.py +46 -0
  46. package/src/harness_context/application/refresh_service.py +25 -0
  47. package/src/harness_context/application/retrieval_service.py +22 -0
  48. package/src/harness_context/application/services.py +4 -0
  49. package/src/harness_context/application/workspace_service.py +18 -0
  50. package/src/harness_context/bootstrap.py +47 -0
  51. package/src/harness_context/branding.py +16 -0
  52. package/src/harness_context/cli/__init__.py +1 -0
  53. package/src/harness_context/cli/app.py +239 -0
  54. package/src/harness_context/cli/exit_codes.py +25 -0
  55. package/src/harness_context/domain/__init__.py +9 -0
  56. package/src/harness_context/domain/cag.py +18 -0
  57. package/src/harness_context/domain/chunking.py +17 -0
  58. package/src/harness_context/domain/planning.py +30 -0
  59. package/src/harness_context/domain/ports.py +24 -0
  60. package/src/harness_context/domain/retrieval.py +46 -0
  61. package/src/harness_context/engine.py +10 -0
  62. package/src/harness_context/free_tools.py +143 -0
  63. package/src/harness_context/infrastructure/__init__.py +10 -0
  64. package/src/harness_context/infrastructure/graph.py +26 -0
  65. package/src/harness_context/infrastructure/indexes.py +33 -0
  66. package/src/harness_context/infrastructure/local_engine.py +296 -0
  67. package/src/harness_context/infrastructure/parsing.py +38 -0
  68. package/src/harness_context/infrastructure/scanning.py +51 -0
  69. package/src/harness_context/installer/__init__.py +4 -0
  70. package/src/harness_context/installer/models.py +22 -0
  71. package/src/harness_context/installer/service.py +168 -0
  72. package/src/harness_context/mcp/__init__.py +3 -0
  73. package/src/harness_context/mcp/capabilities.py +11 -0
  74. package/src/harness_context/mcp/errors.py +8 -0
  75. package/src/harness_context/mcp/lifecycle.py +72 -0
  76. package/src/harness_context/mcp/middleware.py +57 -0
  77. package/src/harness_context/mcp/server.py +3 -0
  78. package/src/harness_context/mcp/tool_handlers/__init__.py +7 -0
  79. package/src/harness_context/mcp/tool_handlers/context.py +16 -0
  80. package/src/harness_context/mcp/tool_handlers/ecc.py +8 -0
  81. package/src/harness_context/mcp/tool_handlers/handoffs.py +20 -0
  82. package/src/harness_context/mcp/tool_handlers/memory.py +16 -0
  83. package/src/harness_context/mcp/tool_handlers/workspace.py +12 -0
  84. package/src/harness_context/mcp/tools.py +15 -0
  85. package/src/harness_context/observability/__init__.py +6 -0
  86. package/src/harness_context/observability/events.py +25 -0
  87. package/src/harness_context/observability/metrics.py +20 -0
  88. package/src/harness_context/paths.py +35 -0
  89. package/src/harness_context/runtime.py +127 -0
  90. package/src/harness_context/schemas.py +38 -0
  91. package/src/harness_context/security/__init__.py +3 -0
  92. package/src/harness_context/security/secret_patterns.py +15 -0
  93. package/src/harness_context/server.py +1077 -0
  94. package/src/harness_context/storage/__init__.py +6 -0
  95. package/src/harness_context/storage/migrations.py +24 -0
  96. package/src/harness_context/storage/pins.py +10 -0
  97. package/src/harness_context/storage/snapshots.py +149 -0
  98. package/src/harness_context/tokenize.py +12 -0
  99. package/src/harness_context/topology.py +65 -0
  100. package/src/harness_context/watcher/__init__.py +3 -0
  101. package/src/harness_context/watcher/service.py +32 -0
  102. package/src/harness_context/workspace/__init__.py +13 -0
  103. package/src/harness_context/workspace/identity.py +9 -0
  104. package/src/harness_context/workspace/lock.py +24 -0
  105. package/src/harness_context/workspace/policy.py +3 -0
  106. package/src/harness_context/workspace/roots.py +84 -0
  107. package/src/harness_context/workspace/state.py +35 -0
  108. package/src/memory/episodic.py +257 -0
  109. package/src/memory/vector_store.py +104 -0
  110. package/src/retrieval/bm25.py +23 -0
  111. package/src/retrieval/cache.py +76 -0
  112. package/src/retrieval/embeddings.py +75 -0
  113. package/src/retrieval/graph.py +45 -0
  114. package/src/retrieval/reranker.py +78 -0
  115. package/src/retrieval/tokenize.py +11 -0
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Mutation:
9
+ operation: str
10
+ path: str
11
+ before: Any
12
+ after: Any
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MutationPlan:
17
+ profile: str
18
+ config_path: str
19
+ mutations: tuple[Mutation, ...]
20
+
21
+ def to_dict(self) -> dict[str, Any]:
22
+ return asdict(self)
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from harness_context.adapters.clients import ClientProfile, get_profile
12
+ from harness_context.branding import CLI_NAME
13
+ from harness_context.installer.models import Mutation, MutationPlan
14
+ from harness_context.paths import workspace_data_dirs, workspace_installer_dir
15
+
16
+
17
+ class ClientInstaller:
18
+ def __init__(self, workspace: str | Path):
19
+ self.workspace = Path(workspace).expanduser().resolve()
20
+ self.ownership_path = workspace_installer_dir(self.workspace) / "ownership.json"
21
+
22
+ def plan(self, profile_name: str, config_path: str | Path | None = None, uninstall: bool = False) -> MutationPlan:
23
+ profile = get_profile(profile_name)
24
+ path = self._config_path(profile, config_path)
25
+ current = self._read(profile, path)
26
+ before = self._owned_value(profile, current)
27
+ manifest = self._load_manifest().get(profile.name, {})
28
+ after = manifest.get("previous") if uninstall else self._server_config()
29
+ if uninstall and manifest and before != manifest.get("installed"):
30
+ return MutationPlan(profile.name, str(path), (Mutation("conflict", self._mutation_path(profile), before, after),))
31
+ operation = "remove" if uninstall and after is None else ("restore" if uninstall else ("replace" if before is not None else "add"))
32
+ mutations = () if before == after else (Mutation(operation, self._mutation_path(profile), before, after),)
33
+ return MutationPlan(profile.name, str(path), mutations)
34
+
35
+ def install(self, profile_name: str, config_path: str | Path | None = None, dry_run: bool = False) -> MutationPlan:
36
+ profile = get_profile(profile_name)
37
+ path = self._config_path(profile, config_path)
38
+ plan = self.plan(profile_name, path)
39
+ if dry_run or not plan.mutations:
40
+ return plan
41
+ current = self._read(profile, path)
42
+ updated = self._set_owned(profile, current, self._server_config())
43
+ self._backup(path)
44
+ self._write(profile, path, updated)
45
+ manifest = self._load_manifest()
46
+ manifest[profile.name] = {
47
+ "config_path": str(path),
48
+ "server_key": profile.server_key,
49
+ "previous": self._owned_value(profile, current),
50
+ "installed": self._owned_value(profile, updated),
51
+ }
52
+ self._write_json(self.ownership_path, manifest)
53
+ return plan
54
+
55
+ def uninstall(self, profile_name: str, config_path: str | Path | None = None, dry_run: bool = False, delete_data: bool = False) -> MutationPlan:
56
+ profile = get_profile(profile_name)
57
+ manifest = self._load_manifest()
58
+ owned = manifest.get(profile.name, {})
59
+ path = self._config_path(profile, config_path or owned.get("config_path"))
60
+ plan = self.plan(profile_name, path, uninstall=True)
61
+ conflict = any(mutation.operation == "conflict" for mutation in plan.mutations)
62
+ if not dry_run and plan.mutations and not conflict:
63
+ current = self._read(profile, path)
64
+ self._backup(path)
65
+ self._write(profile, path, self._set_owned(profile, current, owned.get("previous")))
66
+ if not dry_run and not conflict:
67
+ manifest.pop(profile.name, None)
68
+ if manifest:
69
+ self._write_json(self.ownership_path, manifest)
70
+ else:
71
+ self.ownership_path.unlink(missing_ok=True)
72
+ if delete_data:
73
+ for data_dir in workspace_data_dirs(self.workspace):
74
+ shutil.rmtree(data_dir, ignore_errors=True)
75
+ return plan
76
+
77
+ def _server_config(self) -> dict[str, Any]:
78
+ command = os.environ.get("CTXORA_MCP_COMMAND", CLI_NAME)
79
+ prefix = json.loads(os.environ.get("CTXORA_MCP_ARGS_PREFIX", "[]"))
80
+ if not isinstance(prefix, list) or not all(isinstance(item, str) for item in prefix):
81
+ raise ValueError("CTXORA_MCP_ARGS_PREFIX must be a JSON array of strings")
82
+ return {
83
+ "command": command,
84
+ "args": [*prefix, "run", "--workspace", str(self.workspace), "--transport", "stdio"],
85
+ "env": {"CTXORA_ALLOWED_ROOTS": str(self.workspace), "PYTHONUTF8": "1"},
86
+ }
87
+
88
+ @staticmethod
89
+ def _mutation_path(profile: ClientProfile) -> str:
90
+ return f"mcp_servers.{profile.server_key}" if profile.config_format == "toml" else f"mcpServers.{profile.server_key}"
91
+
92
+ @staticmethod
93
+ def _config_path(profile: ClientProfile, config_path: str | Path | None) -> Path:
94
+ return Path(config_path or profile.default_config).expanduser().resolve()
95
+
96
+ @staticmethod
97
+ def _read(profile: ClientProfile, path: Path) -> Any:
98
+ if not path.exists():
99
+ return "" if profile.config_format == "toml" else {}
100
+ text = path.read_text("utf-8")
101
+ return text if profile.config_format == "toml" else json.loads(text or "{}")
102
+
103
+ @staticmethod
104
+ def _owned_value(profile: ClientProfile, current: Any) -> Any:
105
+ if profile.config_format == "json":
106
+ return current.get("mcpServers", {}).get(profile.server_key)
107
+ match = re.search(ClientInstaller._toml_pattern(profile), current)
108
+ return match.group(0).rstrip() if match else None
109
+
110
+ def _set_owned(self, profile: ClientProfile, current: Any, value: Any) -> Any:
111
+ if profile.config_format == "json":
112
+ updated = json.loads(json.dumps(current))
113
+ servers = updated.setdefault("mcpServers", {})
114
+ if value is None:
115
+ servers.pop(profile.server_key, None)
116
+ if not servers:
117
+ updated.pop("mcpServers", None)
118
+ else:
119
+ servers[profile.server_key] = value
120
+ return updated
121
+ pattern = self._toml_pattern(profile)
122
+ cleaned = re.sub(pattern, "", current).rstrip()
123
+ if value is None:
124
+ return cleaned + ("\n" if cleaned else "")
125
+ if isinstance(value, str):
126
+ return (cleaned + "\n\n" if cleaned else "") + value.rstrip() + "\n"
127
+ block = [f"[mcp_servers.{profile.server_key}]", f'command = {json.dumps(value["command"])}', f'args = {json.dumps(value["args"])}', ""]
128
+ block.append(f"[mcp_servers.{profile.server_key}.env]")
129
+ block.extend(f"{key} = {json.dumps(item)}" for key, item in sorted(value["env"].items()))
130
+ return (cleaned + "\n\n" if cleaned else "") + "\n".join(block) + "\n"
131
+
132
+ @staticmethod
133
+ def _toml_pattern(profile: ClientProfile) -> str:
134
+ key = re.escape(profile.server_key)
135
+ return rf"(?ms)^\[mcp_servers\.{key}\]\n.*?(?=^\[(?!mcp_servers\.{key}(?:\.|\]))|\Z)"
136
+
137
+ def _load_manifest(self) -> dict[str, Any]:
138
+ if not self.ownership_path.exists():
139
+ return {}
140
+ return json.loads(self.ownership_path.read_text("utf-8"))
141
+
142
+ @staticmethod
143
+ def _backup(path: Path) -> None:
144
+ if not path.exists():
145
+ return
146
+ backup = path.with_suffix(path.suffix + ".ctxora.bak")
147
+ backup.parent.mkdir(parents=True, exist_ok=True)
148
+ with tempfile.NamedTemporaryFile(dir=backup.parent, delete=False) as handle:
149
+ handle.write(path.read_bytes())
150
+ temporary = Path(handle.name)
151
+ os.replace(temporary, backup)
152
+
153
+ @staticmethod
154
+ def _write(profile: ClientProfile, path: Path, value: Any) -> None:
155
+ text = value if profile.config_format == "toml" else json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
156
+ ClientInstaller._write_text(path, text)
157
+
158
+ @staticmethod
159
+ def _write_json(path: Path, value: Any) -> None:
160
+ ClientInstaller._write_text(path, json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
161
+
162
+ @staticmethod
163
+ def _write_text(path: Path, text: str) -> None:
164
+ path.parent.mkdir(parents=True, exist_ok=True)
165
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
166
+ handle.write(text)
167
+ temporary = Path(handle.name)
168
+ os.replace(temporary, path)
@@ -0,0 +1,3 @@
1
+ from harness_context.mcp.server import create_mcp_server
2
+
3
+ __all__ = ["create_mcp_server"]
@@ -0,0 +1,11 @@
1
+ from harness_context.branding import MCP_SERVER_NAME
2
+
3
+ SERVER_NAME = MCP_SERVER_NAME
4
+
5
+ TOOL_NAMES = frozenset({
6
+ "register_workspace", "refresh_workspace", "plan_context", "retrieve_context",
7
+ "prepare_context", "context_stats", "invalidate_context", "memory_save",
8
+ "memory_search", "memory_delete", "memory_list", "handoff_conversation",
9
+ "restore_conversation_handoff", "list_conversation_handoffs",
10
+ "delete_conversation_handoff", "purge_expired_handoffs", "ecc_status", "ecc_search",
11
+ })
@@ -0,0 +1,8 @@
1
+ from harness_context.api.v2.errors import ErrorResponse, map_exception
2
+
3
+
4
+ def error_payload(error: Exception) -> dict:
5
+ return map_exception(error).to_dict()
6
+
7
+
8
+ __all__ = ["ErrorResponse", "error_payload", "map_exception"]
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from harness_context.mcp.capabilities import SERVER_NAME
9
+ from harness_context.mcp.middleware import RequestMiddleware
10
+ from harness_context.mcp.tools import register_tools
11
+ from harness_context.observability import LocalMetrics, StructuredEventSink
12
+
13
+
14
+ class ServerLifecycle:
15
+ def __init__(self):
16
+ self._condition = threading.Condition()
17
+ self._ready = False
18
+ self._draining = False
19
+ self._active_requests = 0
20
+
21
+ def mark_ready(self) -> None:
22
+ with self._condition:
23
+ self._ready = True
24
+
25
+ def begin_request(self) -> bool:
26
+ with self._condition:
27
+ if not self._ready or self._draining:
28
+ return False
29
+ self._active_requests += 1
30
+ return True
31
+
32
+ def end_request(self) -> None:
33
+ with self._condition:
34
+ self._active_requests = max(0, self._active_requests - 1)
35
+ self._condition.notify_all()
36
+
37
+ def drain(self, timeout: float = 10.0) -> bool:
38
+ deadline = time.monotonic() + timeout
39
+ with self._condition:
40
+ self._draining = True
41
+ while self._active_requests and time.monotonic() < deadline:
42
+ self._condition.wait(max(0, deadline - time.monotonic()))
43
+ return self._active_requests == 0
44
+
45
+ def status(self) -> dict[str, object]:
46
+ with self._condition:
47
+ return {"ready": self._ready and not self._draining,
48
+ "draining": self._draining, "active_requests": self._active_requests}
49
+
50
+
51
+ def create_mcp_server(container, default_workspace_id: str, lifecycle=None,
52
+ events=None, metrics=None) -> FastMCP:
53
+ server = FastMCP(SERVER_NAME)
54
+ register_tools(server, container, default_workspace_id)
55
+ lifecycle = lifecycle or ServerLifecycle()
56
+ events = events or StructuredEventSink(container.snapshots.state_dir / "events.jsonl")
57
+ metrics = metrics or LocalMetrics()
58
+ middleware = RequestMiddleware(lifecycle, events, metrics)
59
+ target = getattr(server, "middleware", None)
60
+ if target is None:
61
+ target = getattr(getattr(server, "_mcp_server", None), "middleware", None)
62
+ if target is not None:
63
+ target.append(middleware)
64
+ else:
65
+ for tool in server._tool_manager._tools.values():
66
+ tool.fn = middleware.wrap(tool.name, tool.fn)
67
+ lifecycle.mark_ready()
68
+ server.ctxora_lifecycle = lifecycle
69
+ server.ctxora_metrics = metrics
70
+ server.harness_lifecycle = lifecycle
71
+ server.harness_metrics = metrics
72
+ return server
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from functools import wraps
6
+
7
+
8
+ class RequestMiddleware:
9
+ def __init__(self, lifecycle, events, metrics):
10
+ self.lifecycle = lifecycle
11
+ self.events = events
12
+ self.metrics = metrics
13
+
14
+ async def __call__(self, context, call_next):
15
+ request_id = uuid.uuid4().hex
16
+ method = str(getattr(context, "method", "unknown"))
17
+ started = time.monotonic()
18
+ if not self.lifecycle.begin_request():
19
+ self._complete(request_id, method, "rejected", started)
20
+ raise RuntimeError("server is draining")
21
+ try:
22
+ result = await call_next(context)
23
+ except Exception:
24
+ self._complete(request_id, method, "error", started)
25
+ raise
26
+ else:
27
+ self._complete(request_id, method, "ok", started)
28
+ return result
29
+ finally:
30
+ self.lifecycle.end_request()
31
+
32
+ def wrap(self, method: str, function):
33
+ @wraps(function)
34
+ def tracked(*args, **kwargs):
35
+ request_id = uuid.uuid4().hex
36
+ started = time.monotonic()
37
+ if not self.lifecycle.begin_request():
38
+ self._complete(request_id, method, "rejected", started)
39
+ raise RuntimeError("server is draining")
40
+ try:
41
+ result = function(*args, **kwargs)
42
+ except Exception:
43
+ self._complete(request_id, method, "error", started)
44
+ raise
45
+ else:
46
+ self._complete(request_id, method, "ok", started)
47
+ return result
48
+ finally:
49
+ self.lifecycle.end_request()
50
+
51
+ return tracked
52
+
53
+ def _complete(self, request_id: str, method: str, outcome: str, started: float) -> None:
54
+ duration = int((time.monotonic() - started) * 1000)
55
+ self.metrics.record_request(outcome, duration)
56
+ self.events.emit("request.completed", request_id=request_id, method=method,
57
+ outcome=outcome, duration_ms=duration)
@@ -0,0 +1,3 @@
1
+ from harness_context.mcp.lifecycle import create_mcp_server
2
+
3
+ __all__ = ["create_mcp_server"]
@@ -0,0 +1,7 @@
1
+ from harness_context.mcp.tool_handlers.context import register_context_tools
2
+ from harness_context.mcp.tool_handlers.ecc import register_ecc_tools
3
+ from harness_context.mcp.tool_handlers.handoffs import register_handoff_tools
4
+ from harness_context.mcp.tool_handlers.memory import register_memory_tools
5
+ from harness_context.mcp.tool_handlers.workspace import register_workspace_tools
6
+
7
+ __all__ = ["register_context_tools", "register_ecc_tools", "register_handoff_tools", "register_memory_tools", "register_workspace_tools"]
@@ -0,0 +1,16 @@
1
+ def register_context_tools(mcp, container, default_workspace_id: str) -> None:
2
+ @mcp.tool()
3
+ def plan_context(workspace_id: str, query: str, available_input_tokens: int, strategy_override: str = "") -> dict:
4
+ return container.retrieval.plan(workspace_id, query, available_input_tokens, strategy_override)
5
+
6
+ @mcp.tool()
7
+ def retrieve_context(workspace_id: str, query: str, top_k: int = 12, graph_expand: bool = True, token_budget: int = 4_000) -> dict:
8
+ return container.retrieval.retrieve(workspace_id, query, top_k, graph_expand, token_budget)
9
+
10
+ @mcp.tool()
11
+ def prepare_context(workspace_id: str, query: str, available_input_tokens: int, preferred_strategy: str = "auto", include_memory: bool = True, include_handoff: bool = False, handoff_id: str = "", include_ecc: bool = False, freshness: str = "current", deadline_ms: int = 5_000) -> dict:
12
+ return container.context.prepare_values(workspace_id=workspace_id, query=query, available_input_tokens=available_input_tokens, preferred_strategy=preferred_strategy, include_memory=include_memory, include_handoff=include_handoff, handoff_id=handoff_id, include_ecc=include_ecc, freshness=freshness, deadline_ms=deadline_ms)
13
+
14
+ @mcp.tool()
15
+ def context_stats(workspace_id: str = default_workspace_id) -> dict:
16
+ return container.retrieval.stats(workspace_id)
@@ -0,0 +1,8 @@
1
+ def register_ecc_tools(mcp, container) -> None:
2
+ @mcp.tool()
3
+ def ecc_status() -> dict:
4
+ return container.ecc_queries.status()
5
+
6
+ @mcp.tool()
7
+ def ecc_search(query: str, limit: int = 4) -> dict:
8
+ return container.ecc_queries.search(query, limit)
@@ -0,0 +1,20 @@
1
+ def register_handoff_tools(mcp, container) -> None:
2
+ @mcp.tool()
3
+ def handoff_conversation(workspace_id: str, messages_json: str, threshold_tokens: int = 30_000, label: str = "", retention_seconds: int = 604_800, consent: bool = False) -> dict:
4
+ return container.handoff.prepare(workspace_id, messages_json, threshold_tokens, label, retention_seconds, consent)
5
+
6
+ @mcp.tool()
7
+ def restore_conversation_handoff(workspace_id: str, handoff_id: str) -> dict:
8
+ return container.handoff.restore(workspace_id, handoff_id)
9
+
10
+ @mcp.tool()
11
+ def list_conversation_handoffs(workspace_id: str, limit: int = 30) -> list[dict]:
12
+ return container.handoff.list(workspace_id, limit)
13
+
14
+ @mcp.tool()
15
+ def delete_conversation_handoff(workspace_id: str, handoff_id: str) -> dict:
16
+ return container.handoff.delete(workspace_id, handoff_id)
17
+
18
+ @mcp.tool()
19
+ def purge_expired_handoffs() -> dict:
20
+ return container.handoff.purge()
@@ -0,0 +1,16 @@
1
+ def register_memory_tools(mcp, container) -> None:
2
+ @mcp.tool()
3
+ def memory_save(workspace_id: str, key: str, value: str, mtype: str = "semantic", tags: str = "", scope: str = "workspace", source: str = "user", confidence: float = 1.0, expires_at: float | None = None) -> dict:
4
+ return container.memories.save(workspace_id, key, value, mtype, tags, scope, source, confidence, expires_at)
5
+
6
+ @mcp.tool()
7
+ def memory_search(workspace_id: str, query: str, mtype: str = "", top_k: int = 5, min_sim: float = 0.12) -> list[dict]:
8
+ return container.memories.search(workspace_id, query, mtype, top_k, min_sim)
9
+
10
+ @mcp.tool()
11
+ def memory_delete(workspace_id: str, key: str, mtype: str = "semantic") -> dict:
12
+ return container.memories.delete(workspace_id, key, mtype)
13
+
14
+ @mcp.tool()
15
+ def memory_list(workspace_id: str, mtype: str = "", limit: int = 30) -> list[str]:
16
+ return container.memories.list(workspace_id, mtype, limit)
@@ -0,0 +1,12 @@
1
+ def register_workspace_tools(mcp, container, default_workspace_id: str) -> None:
2
+ @mcp.tool()
3
+ def register_workspace(workspace_id: str, roots: list[str], initial_refresh: bool = False) -> dict:
4
+ return container.workspace.register(workspace_id, roots, initial_refresh)
5
+
6
+ @mcp.tool()
7
+ def refresh_workspace(workspace_id: str = default_workspace_id, paths: list[str] | None = None) -> dict:
8
+ return container.refresh.execute(workspace_id, paths)
9
+
10
+ @mcp.tool()
11
+ def invalidate_context(workspace_id: str, target: str) -> dict:
12
+ return container.refresh.invalidate(workspace_id, target)
@@ -0,0 +1,15 @@
1
+ from harness_context.mcp.tool_handlers import (
2
+ register_context_tools,
3
+ register_ecc_tools,
4
+ register_handoff_tools,
5
+ register_memory_tools,
6
+ register_workspace_tools,
7
+ )
8
+
9
+
10
+ def register_tools(mcp, container, default_workspace_id: str) -> None:
11
+ register_workspace_tools(mcp, container, default_workspace_id)
12
+ register_context_tools(mcp, container, default_workspace_id)
13
+ register_memory_tools(mcp, container)
14
+ register_handoff_tools(mcp, container)
15
+ register_ecc_tools(mcp, container)
@@ -0,0 +1,6 @@
1
+ from harness_context.observability.events import StructuredEventSink
2
+ from harness_context.observability.metrics import LocalMetrics
3
+
4
+ __all__ = ["LocalMetrics", "StructuredEventSink"]
5
+
6
+ __all__ = ["LocalMetrics", "StructuredEventSink"]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import threading
5
+ import time
6
+ from pathlib import Path
7
+ from typing import ClassVar
8
+
9
+
10
+ class StructuredEventSink:
11
+ _FIELDS: ClassVar = {"request_id", "method", "outcome", "duration_ms"}
12
+
13
+ def __init__(self, path: str | Path):
14
+ self.path = Path(path)
15
+ self._lock = threading.Lock()
16
+
17
+ def emit(self, event: str, **fields: object) -> dict[str, object]:
18
+ unknown = set(fields) - self._FIELDS
19
+ if unknown:
20
+ raise ValueError(f"unsupported event fields: {sorted(unknown)}")
21
+ payload = {"event": event, "timestamp": round(time.time(), 3), **fields}
22
+ self.path.parent.mkdir(parents=True, exist_ok=True)
23
+ with self._lock, self.path.open("a", encoding="utf-8") as handle:
24
+ handle.write(json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n")
25
+ return payload
@@ -0,0 +1,20 @@
1
+ import threading
2
+ from collections import Counter
3
+
4
+
5
+ class LocalMetrics:
6
+ def __init__(self):
7
+ self._values: Counter[str] = Counter()
8
+ self._lock = threading.Lock()
9
+
10
+ def record_request(self, outcome: str, duration_ms: int) -> None:
11
+ if outcome not in {"ok", "error", "rejected"}:
12
+ raise ValueError("unsupported request outcome")
13
+ with self._lock:
14
+ self._values["requests_total"] += 1
15
+ self._values[f"requests_{outcome}"] += 1
16
+ self._values["request_duration_ms_total"] += max(0, int(duration_ms))
17
+
18
+ def snapshot(self) -> dict[str, int]:
19
+ with self._lock:
20
+ return dict(sorted(self._values.items()))
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from harness_context.branding import LEGACY_WORKSPACE_DIR_NAME, WORKSPACE_DIR_NAME
6
+
7
+
8
+ def workspace_data_dir(workspace: str | Path) -> Path:
9
+ return Path(workspace).resolve() / WORKSPACE_DIR_NAME
10
+
11
+
12
+ def legacy_workspace_data_dir(workspace: str | Path) -> Path:
13
+ return Path(workspace).resolve() / LEGACY_WORKSPACE_DIR_NAME
14
+
15
+
16
+ def workspace_state_dir(workspace: str | Path) -> Path:
17
+ preferred = workspace_data_dir(workspace) / "state"
18
+ legacy = legacy_workspace_data_dir(workspace) / "state"
19
+ return preferred if preferred.exists() or not legacy.exists() else legacy
20
+
21
+
22
+ def workspace_runtime_dir(workspace: str | Path) -> Path:
23
+ preferred = workspace_data_dir(workspace)
24
+ legacy = legacy_workspace_data_dir(workspace)
25
+ return preferred if (preferred / "run.pid").exists() or not (legacy / "run.pid").exists() else legacy
26
+
27
+
28
+ def workspace_installer_dir(workspace: str | Path) -> Path:
29
+ preferred = workspace_data_dir(workspace) / "installer"
30
+ legacy = legacy_workspace_data_dir(workspace) / "installer"
31
+ return preferred if preferred.exists() or not legacy.exists() else legacy
32
+
33
+
34
+ def workspace_data_dirs(workspace: str | Path) -> tuple[Path, Path]:
35
+ return workspace_data_dir(workspace), legacy_workspace_data_dir(workspace)