sid-harness 0.3.0__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 (45) hide show
  1. sid_harness/__init__.py +13 -0
  2. sid_harness/__main__.py +4 -0
  3. sid_harness/_native.py +1639 -0
  4. sid_harness/brave_proof.py +246 -0
  5. sid_harness/bridge.py +851 -0
  6. sid_harness/cli.py +937 -0
  7. sid_harness/client.py +161 -0
  8. sid_harness/companion.py +151 -0
  9. sid_harness/config.py +68 -0
  10. sid_harness/context/__init__.py +40 -0
  11. sid_harness/context/index.py +267 -0
  12. sid_harness/context/retrieval.py +74 -0
  13. sid_harness/editor.py +231 -0
  14. sid_harness/gateway.py +106 -0
  15. sid_harness/git_write.py +598 -0
  16. sid_harness/local_workspace.py +421 -0
  17. sid_harness/loop.py +394 -0
  18. sid_harness/lsp.py +598 -0
  19. sid_harness/policy/__init__.py +44 -0
  20. sid_harness/policy/repo_policy.py +626 -0
  21. sid_harness/receipt.py +142 -0
  22. sid_harness/runner.py +468 -0
  23. sid_harness/selftest.py +149 -0
  24. sid_harness/session/__init__.py +33 -0
  25. sid_harness/session/bounds.py +140 -0
  26. sid_harness/session/diff_gate.py +93 -0
  27. sid_harness/session/session.py +427 -0
  28. sid_harness/session/store.py +88 -0
  29. sid_harness/tools/__init__.py +52 -0
  30. sid_harness/tools/mcp_client.py +285 -0
  31. sid_harness/tools/plan.py +138 -0
  32. sid_harness/tools/registry.py +159 -0
  33. sid_harness/tools/skills.py +185 -0
  34. sid_harness/tools/subagents.py +112 -0
  35. sid_harness/tui.py +96 -0
  36. sid_harness/workspace.py +107 -0
  37. sid_harness-0.3.0.data/data/share/sid-harness/editors/README.md +46 -0
  38. sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/README.md +32 -0
  39. sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/extension.js +395 -0
  40. sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/package.json +120 -0
  41. sid_harness-0.3.0.dist-info/METADATA +229 -0
  42. sid_harness-0.3.0.dist-info/RECORD +45 -0
  43. sid_harness-0.3.0.dist-info/WHEEL +5 -0
  44. sid_harness-0.3.0.dist-info/entry_points.txt +2 -0
  45. sid_harness-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,285 @@
1
+ """Minimal MCP (Model Context Protocol) client over stdio and HTTP (SUB-56).
2
+
3
+ JSON-RPC 2.0. The stdio transport uses the spec's newline-delimited framing
4
+ (one JSON-RPC message per line); the HTTP transport POSTs a single JSON-RPC
5
+ message and reads the JSON response. Standard library only.
6
+
7
+ This is a client, not an engine: the tools it exposes are registered into the
8
+ SID ``ToolRegistry`` (``registry.py``) and settle through the same loop.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ import urllib.error
16
+ import urllib.request
17
+ from typing import Any
18
+
19
+ DEFAULT_PROTOCOL_VERSION = "2024-11-05"
20
+
21
+
22
+ class MCPError(RuntimeError):
23
+ """An MCP call could not be completed (transport, protocol, or server error)."""
24
+
25
+
26
+ class StdioTransport:
27
+ """Newline-delimited JSON-RPC over a child process's stdin/stdout."""
28
+
29
+ def __init__(
30
+ self,
31
+ command: str,
32
+ args: list[str] | None = None,
33
+ *,
34
+ cwd: str | None = None,
35
+ env: dict[str, str] | None = None,
36
+ ) -> None:
37
+ try:
38
+ self._proc = subprocess.Popen(
39
+ [command, *(args or [])],
40
+ stdin=subprocess.PIPE,
41
+ stdout=subprocess.PIPE,
42
+ stderr=subprocess.PIPE,
43
+ text=True,
44
+ encoding="utf-8",
45
+ bufsize=1,
46
+ cwd=cwd,
47
+ env=env,
48
+ )
49
+ except OSError as exc:
50
+ raise MCPError(f"could not start MCP stdio server {command!r}: {exc}") from exc
51
+
52
+ def request(self, message: dict[str, Any]) -> dict[str, Any]:
53
+ self._write(message)
54
+ assert self._proc.stdout is not None
55
+ while True:
56
+ line = self._proc.stdout.readline()
57
+ if not line:
58
+ raise MCPError("MCP stdio server closed the stream before replying")
59
+ line = line.strip()
60
+ if not line:
61
+ continue
62
+ try:
63
+ payload = json.loads(line)
64
+ except json.JSONDecodeError:
65
+ continue
66
+ if isinstance(payload, dict) and payload.get("id") == message.get("id"):
67
+ return payload
68
+
69
+ def notify(self, message: dict[str, Any]) -> None:
70
+ self._write(message)
71
+
72
+ def _write(self, message: dict[str, Any]) -> None:
73
+ if self._proc.stdin is None:
74
+ raise MCPError("MCP stdio server stdin is unavailable")
75
+ self._proc.stdin.write(json.dumps(message) + "\n")
76
+ self._proc.stdin.flush()
77
+
78
+ def close(self) -> None:
79
+ if self._proc.stdin is not None:
80
+ try:
81
+ self._proc.stdin.close()
82
+ except OSError:
83
+ pass
84
+ try:
85
+ self._proc.terminate()
86
+ self._proc.wait(timeout=5)
87
+ except Exception: # pragma: no cover - best-effort teardown
88
+ try:
89
+ self._proc.kill()
90
+ except Exception:
91
+ pass
92
+
93
+
94
+ class HTTPTransport:
95
+ """JSON-RPC over HTTP POST (one request, one JSON response)."""
96
+
97
+ def __init__(
98
+ self,
99
+ url: str,
100
+ *,
101
+ headers: dict[str, str] | None = None,
102
+ timeout: float = 30.0,
103
+ ) -> None:
104
+ self.url = url
105
+ self.headers = {"Content-Type": "application/json", **(headers or {})}
106
+ self.timeout = timeout
107
+
108
+ def request(self, message: dict[str, Any]) -> dict[str, Any]:
109
+ data = json.dumps(message).encode("utf-8")
110
+ req = urllib.request.Request(
111
+ self.url, data=data, headers=self.headers, method="POST"
112
+ )
113
+ try:
114
+ with urllib.request.urlopen(req, timeout=self.timeout) as response:
115
+ body = response.read().decode("utf-8")
116
+ except urllib.error.HTTPError as exc:
117
+ detail = exc.read().decode("utf-8", "replace")
118
+ raise MCPError(f"MCP HTTP {exc.code}: {detail[:300]}") from exc
119
+ except urllib.error.URLError as exc:
120
+ raise MCPError(f"MCP endpoint unreachable at {self.url}: {exc.reason}") from exc
121
+ try:
122
+ parsed = json.loads(body)
123
+ except json.JSONDecodeError as exc:
124
+ raise MCPError("MCP endpoint returned a non-JSON response") from exc
125
+ if not isinstance(parsed, dict):
126
+ raise MCPError("MCP endpoint returned an unexpected response shape")
127
+ return parsed
128
+
129
+ def notify(self, message: dict[str, Any]) -> None:
130
+ data = json.dumps(message).encode("utf-8")
131
+ req = urllib.request.Request(
132
+ self.url, data=data, headers=self.headers, method="POST"
133
+ )
134
+ try:
135
+ with urllib.request.urlopen(req, timeout=self.timeout):
136
+ pass
137
+ except urllib.error.HTTPError as exc:
138
+ detail = exc.read().decode("utf-8", "replace")
139
+ raise MCPError(f"MCP HTTP {exc.code}: {detail[:300]}") from exc
140
+ except urllib.error.URLError as exc:
141
+ raise MCPError(f"MCP endpoint unreachable at {self.url}: {exc.reason}") from exc
142
+
143
+ def close(self) -> None: # nothing persistent to release
144
+ return None
145
+
146
+
147
+ class MCPClient:
148
+ """A small MCP client: initialize, list tools, call a tool."""
149
+
150
+ def __init__(
151
+ self,
152
+ transport: Any,
153
+ *,
154
+ client_name: str = "sid-harness",
155
+ client_version: str = "0.1.0",
156
+ protocol_version: str = DEFAULT_PROTOCOL_VERSION,
157
+ ) -> None:
158
+ self._transport = transport
159
+ self._client_name = client_name
160
+ self._client_version = client_version
161
+ self._protocol_version = protocol_version
162
+ self._request_id = 0
163
+ self.initialized = False
164
+
165
+ def _next_id(self) -> int:
166
+ self._request_id += 1
167
+ return self._request_id
168
+
169
+ def _request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
170
+ message: dict[str, Any] = {
171
+ "jsonrpc": "2.0",
172
+ "id": self._next_id(),
173
+ "method": method,
174
+ }
175
+ if params is not None:
176
+ message["params"] = params
177
+ response = self._transport.request(message)
178
+ error = response.get("error")
179
+ if error:
180
+ raise MCPError(f"MCP {method} failed: {error}")
181
+ result = response.get("result")
182
+ return result if isinstance(result, dict) else {}
183
+
184
+ def _notify(self, method: str, params: dict[str, Any] | None = None) -> None:
185
+ message: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
186
+ if params is not None:
187
+ message["params"] = params
188
+ self._transport.notify(message)
189
+
190
+ def initialize(self) -> dict[str, Any]:
191
+ result = self._request(
192
+ "initialize",
193
+ {
194
+ "protocolVersion": self._protocol_version,
195
+ "capabilities": {},
196
+ "clientInfo": {"name": self._client_name, "version": self._client_version},
197
+ },
198
+ )
199
+ self._notify("notifications/initialized")
200
+ self.initialized = True
201
+ return result
202
+
203
+ def list_tools(self) -> list[dict[str, Any]]:
204
+ result = self._request("tools/list", {})
205
+ tools = result.get("tools")
206
+ if not isinstance(tools, list):
207
+ raise MCPError("MCP tools/list returned an unexpected shape")
208
+ return tools
209
+
210
+ def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
211
+ return self._request("tools/call", {"name": name, "arguments": arguments or {}})
212
+
213
+ def close(self) -> None:
214
+ self._transport.close()
215
+
216
+
217
+ def _tool_schema(tool: dict[str, Any], *, exposed_name: str) -> dict[str, Any]:
218
+ return {
219
+ "type": "function",
220
+ "function": {
221
+ "name": exposed_name,
222
+ "description": tool.get("description") or f"MCP tool {exposed_name}",
223
+ "parameters": tool.get("inputSchema")
224
+ or {"type": "object", "properties": {}, "additionalProperties": False},
225
+ },
226
+ }
227
+
228
+
229
+ def mcp_tool_schemas(
230
+ tools: list[dict[str, Any]], *, prefix: str | None = None
231
+ ) -> list[dict[str, Any]]:
232
+ """Convert MCP tool descriptors to the loop's function-schema shape."""
233
+ schemas: list[dict[str, Any]] = []
234
+ for tool in tools:
235
+ name = str(tool.get("name") or "").strip()
236
+ if not name:
237
+ continue
238
+ exposed = f"{prefix}_{name}" if prefix else name
239
+ schemas.append(_tool_schema(tool, exposed_name=exposed))
240
+ return schemas
241
+
242
+
243
+ def register_mcp_tools(
244
+ registry: Any,
245
+ client: MCPClient,
246
+ *,
247
+ prefix: str = "mcp",
248
+ mutating: bool = True,
249
+ tools: list[dict[str, Any]] | None = None,
250
+ ) -> list[str]:
251
+ """Register an MCP server's tools into a ``ToolRegistry``.
252
+
253
+ MCP tools default to ``mutating=True``: the harness cannot prove a remote
254
+ tool is read-only, so the plan gate guards them like a workspace edit.
255
+ """
256
+ listed = tools if tools is not None else client.list_tools()
257
+ registered: list[str] = []
258
+ for tool in listed:
259
+ name = str(tool.get("name") or "").strip()
260
+ if not name:
261
+ continue
262
+ exposed = f"{prefix}_{name}" if prefix else name
263
+
264
+ def handler(arguments: dict[str, Any], _name: str = name) -> dict[str, Any]:
265
+ return client.call_tool(_name, arguments)
266
+
267
+ registry.register(
268
+ _tool_schema(tool, exposed_name=exposed),
269
+ handler,
270
+ source="mcp",
271
+ mutating=mutating,
272
+ )
273
+ registered.append(exposed)
274
+ return registered
275
+
276
+
277
+ __all__ = [
278
+ "DEFAULT_PROTOCOL_VERSION",
279
+ "HTTPTransport",
280
+ "MCPClient",
281
+ "MCPError",
282
+ "StdioTransport",
283
+ "mcp_tool_schemas",
284
+ "register_mcp_tools",
285
+ ]
@@ -0,0 +1,138 @@
1
+ """Plan/todo layer for the SID tool loop (SUB-56).
2
+
3
+ The gate emits a plan before edits and refuses workspace edits until that plan
4
+ is approved. ``PlanGate`` is consumed by ``ToolRegistry.dispatch``
5
+ (``registry.py``); ``TodoList`` is a small, honest todo store for the session.
6
+ No vendor is named here and no execution path is added — this only decides
7
+ whether the shared loop's mutating tools may run.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ TODO_STATUSES = ("pending", "in_progress", "completed", "cancelled")
17
+
18
+
19
+ class PlanError(RuntimeError):
20
+ """A plan or todo action was invalid."""
21
+
22
+
23
+ @dataclass
24
+ class Todo:
25
+ id: str
26
+ text: str
27
+ status: str = "pending"
28
+
29
+ def as_dict(self) -> dict[str, str]:
30
+ return {"id": self.id, "text": self.text, "status": self.status}
31
+
32
+
33
+ class TodoList:
34
+ """Ordered todos with a bounded status vocabulary."""
35
+
36
+ def __init__(self) -> None:
37
+ self._items: list[Todo] = []
38
+ self._seq = 0
39
+
40
+ def add(self, text: str) -> Todo:
41
+ if not isinstance(text, str) or not text.strip():
42
+ raise PlanError("todo text must be a non-empty string")
43
+ self._seq += 1
44
+ todo = Todo(id=f"t{self._seq}", text=text.strip())
45
+ self._items.append(todo)
46
+ return todo
47
+
48
+ def update(self, todo_id: str, status: str) -> Todo:
49
+ if status not in TODO_STATUSES:
50
+ raise PlanError(
51
+ f"unknown todo status {status!r}; expected one of {', '.join(TODO_STATUSES)}"
52
+ )
53
+ for todo in self._items:
54
+ if todo.id == todo_id:
55
+ todo.status = status
56
+ return todo
57
+ raise PlanError(f"unknown todo {todo_id!r}")
58
+
59
+ def items(self) -> list[Todo]:
60
+ return list(self._items)
61
+
62
+ def open_items(self) -> list[Todo]:
63
+ return [todo for todo in self._items if todo.status in ("pending", "in_progress")]
64
+
65
+ def as_dicts(self) -> list[dict[str, str]]:
66
+ return [todo.as_dict() for todo in self._items]
67
+
68
+
69
+ class PlanGate:
70
+ """Refuse mutating tool calls until a plan has been emitted and approved."""
71
+
72
+ def __init__(self, *, require_plan: bool = True) -> None:
73
+ self.require_plan = require_plan
74
+ self._plan: list[str] | None = None
75
+ self._approved = False
76
+ self._rejected_reason: str | None = None
77
+ self.todos = TodoList()
78
+
79
+ @property
80
+ def plan(self) -> list[str] | None:
81
+ return list(self._plan) if self._plan is not None else None
82
+
83
+ @property
84
+ def approved(self) -> bool:
85
+ return self._approved
86
+
87
+ def set_plan(self, steps: Iterable[str]) -> dict[str, Any]:
88
+ cleaned = [str(step).strip() for step in steps if str(step).strip()]
89
+ if not cleaned:
90
+ raise PlanError("a plan needs at least one step")
91
+ self._plan = cleaned
92
+ self._approved = False
93
+ self._rejected_reason = None
94
+ return self.snapshot()
95
+
96
+ def approve(self) -> dict[str, Any]:
97
+ if self._plan is None:
98
+ raise PlanError("no plan to approve; call set_plan first")
99
+ self._approved = True
100
+ self._rejected_reason = None
101
+ return self.snapshot()
102
+
103
+ def reject(self, reason: str) -> dict[str, Any]:
104
+ self._approved = False
105
+ self._rejected_reason = reason.strip() or "plan rejected"
106
+ return self.snapshot()
107
+
108
+ def authorize(self, tool_name: str) -> bool:
109
+ if not self.require_plan:
110
+ return True
111
+ return self._approved
112
+
113
+ def refusal(self, tool_name: str) -> str:
114
+ if self._plan is None:
115
+ return (
116
+ f"plan gate: {tool_name} is refused until a plan is emitted and "
117
+ "approved (call set_plan, then approve)."
118
+ )
119
+ if self._rejected_reason:
120
+ return (
121
+ f"plan gate: {tool_name} is refused — the plan was rejected "
122
+ f"({self._rejected_reason})."
123
+ )
124
+ return (
125
+ f"plan gate: {tool_name} is refused until the current plan is "
126
+ "approved (call approve)."
127
+ )
128
+
129
+ def snapshot(self) -> dict[str, Any]:
130
+ return {
131
+ "plan": self.plan,
132
+ "approved": self._approved,
133
+ "rejected_reason": self._rejected_reason,
134
+ "todos": self.todos.as_dicts(),
135
+ }
136
+
137
+
138
+ __all__ = ["PlanError", "PlanGate", "Todo", "TodoList", "TODO_STATUSES"]
@@ -0,0 +1,159 @@
1
+ """Additive tool registry over the shared SID native loop contract.
2
+
3
+ The loop's tool contract lives in the gateway's ``app.native_agent`` — the four
4
+ base tools, ``_safe_path``, and ``_tool_result``. This module EXTENDS that
5
+ contract; it never forks it:
6
+
7
+ - ``ToolRegistry.schemas()`` returns the base schemas UNCHANGED (identity-equal
8
+ to ``native_agent.TOOLS``) followed by registered extension schemas.
9
+ - ``ToolRegistry.dispatch()`` routes every base tool name through
10
+ ``native_agent._tool_result`` — the one implementation — and only handles
11
+ extension names itself. There is no second execution engine and no second
12
+ verify path.
13
+ - A registered name may not shadow a base tool: the safety and execution
14
+ contract has exactly one owner.
15
+
16
+ ``PlanGate`` (``plan.py``) plugs into ``dispatch`` to refuse workspace edits
17
+ until a plan is approved.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Callable
23
+ from dataclasses import dataclass
24
+ from typing import Any
25
+
26
+ from sid_harness.gateway import native_agent
27
+
28
+ BASE_TOOL_NAMES: tuple[str, ...] = tuple(
29
+ schema["function"]["name"] for schema in native_agent.TOOLS
30
+ )
31
+ BASE_TOOL_SCHEMAS: tuple[dict[str, Any], ...] = tuple(native_agent.TOOLS)
32
+ # Every base tool that mutates the workspace, sourced from the shared loop so a
33
+ # new mutating tool can never be added without being gated here too.
34
+ MUTATING_BASE_TOOLS = frozenset(
35
+ name for name in BASE_TOOL_NAMES if name in native_agent.MUTATING_TOOL_NAMES
36
+ )
37
+
38
+ ToolHandler = Callable[[dict[str, Any]], Any]
39
+
40
+
41
+ class ToolRegistrationError(ValueError):
42
+ """A tool could not be registered without breaking the shared contract."""
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class RegisteredTool:
47
+ name: str
48
+ schema: dict[str, Any]
49
+ handler: ToolHandler
50
+ source: str
51
+ mutating: bool = False
52
+
53
+
54
+ class ToolRegistry:
55
+ """Base SID tools plus registered extensions, dispatched through one seam."""
56
+
57
+ def __init__(self, *, gate: Any | None = None) -> None:
58
+ self._gate = gate
59
+ self._tools: dict[str, RegisteredTool] = {}
60
+
61
+ def set_gate(self, gate: Any | None) -> None:
62
+ self._gate = gate
63
+
64
+ @property
65
+ def gate(self) -> Any | None:
66
+ return self._gate
67
+
68
+ def register(
69
+ self,
70
+ schema: dict[str, Any],
71
+ handler: ToolHandler,
72
+ *,
73
+ source: str = "extension",
74
+ mutating: bool = False,
75
+ ) -> RegisteredTool:
76
+ try:
77
+ name = schema["function"]["name"]
78
+ except (KeyError, TypeError) as exc:
79
+ raise ToolRegistrationError(
80
+ "tool schema must be an OpenAI-compatible function schema"
81
+ ) from exc
82
+ if not isinstance(name, str) or not name:
83
+ raise ToolRegistrationError("tool schema requires a non-empty function name")
84
+ if name in BASE_TOOL_NAMES:
85
+ raise ToolRegistrationError(
86
+ f"{name!r} shadows a base SID tool; the base contract has one owner"
87
+ )
88
+ if name in self._tools:
89
+ raise ToolRegistrationError(f"tool {name!r} is already registered")
90
+ if not callable(handler):
91
+ raise ToolRegistrationError("tool handler must be callable")
92
+ tool = RegisteredTool(
93
+ name=name,
94
+ schema=schema,
95
+ handler=handler,
96
+ source=source,
97
+ mutating=mutating,
98
+ )
99
+ self._tools[name] = tool
100
+ return tool
101
+
102
+ def schemas(self) -> list[dict[str, Any]]:
103
+ """Base schemas (unchanged) followed by extension schemas."""
104
+ return [*native_agent.TOOLS, *(tool.schema for tool in self._tools.values())]
105
+
106
+ def names(self) -> list[str]:
107
+ return [*BASE_TOOL_NAMES, *self._tools]
108
+
109
+ def extension_names(self) -> list[str]:
110
+ return list(self._tools)
111
+
112
+ def is_mutating(self, name: str) -> bool:
113
+ if name in MUTATING_BASE_TOOLS:
114
+ return True
115
+ tool = self._tools.get(name)
116
+ return bool(tool and tool.mutating)
117
+
118
+ def dispatch(
119
+ self,
120
+ name: str,
121
+ arguments: Any,
122
+ files: dict[str, str],
123
+ run_tests: Any,
124
+ ) -> Any:
125
+ """Route one tool call: base tools to the shared implementation, else here."""
126
+ if name in BASE_TOOL_NAMES:
127
+ if self.is_mutating(name) and not self._authorized(name):
128
+ return {"error": self._refusal(name)}
129
+ return native_agent._tool_result(name, arguments, files, run_tests)
130
+ tool = self._tools.get(name)
131
+ if tool is None:
132
+ return {"error": f"unknown tool: {name}"}
133
+ if tool.mutating and not self._authorized(name):
134
+ return {"error": self._refusal(name)}
135
+ try:
136
+ return tool.handler(arguments if isinstance(arguments, dict) else {})
137
+ except Exception as exc:
138
+ return {"error": f"tool failed: {type(exc).__name__}: {exc}"}
139
+
140
+ def _authorized(self, name: str) -> bool:
141
+ if self._gate is None:
142
+ return True
143
+ return bool(self._gate.authorize(name))
144
+
145
+ def _refusal(self, name: str) -> str:
146
+ if self._gate is None:
147
+ return f"tool {name} is not authorized"
148
+ return str(self._gate.refusal(name))
149
+
150
+
151
+ __all__ = [
152
+ "BASE_TOOL_NAMES",
153
+ "BASE_TOOL_SCHEMAS",
154
+ "MUTATING_BASE_TOOLS",
155
+ "RegisteredTool",
156
+ "ToolHandler",
157
+ "ToolRegistrationError",
158
+ "ToolRegistry",
159
+ ]