tactus 0.34.1__py3-none-any.whl → 0.35.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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/file.py
CHANGED
|
@@ -39,29 +39,31 @@ class FilePrimitive:
|
|
|
39
39
|
"""
|
|
40
40
|
self.base_path = Path(base_path) if base_path else Path.cwd()
|
|
41
41
|
self.execution_context = execution_context
|
|
42
|
-
logger.debug(
|
|
42
|
+
logger.debug("FilePrimitive initialized with base_path: %s", self.base_path)
|
|
43
43
|
|
|
44
|
-
def _check_determinism(self, operation: str):
|
|
44
|
+
def _check_determinism(self, operation: str) -> None:
|
|
45
45
|
"""Warn if file operation called outside checkpoint."""
|
|
46
46
|
if self.execution_context and not getattr(
|
|
47
47
|
self.execution_context, "_inside_checkpoint", False
|
|
48
48
|
):
|
|
49
49
|
import warnings
|
|
50
50
|
|
|
51
|
+
warning_banner = "=" * 70
|
|
51
52
|
warnings.warn(
|
|
52
|
-
|
|
53
|
+
"\n"
|
|
54
|
+
f"{warning_banner}\n"
|
|
53
55
|
f"DETERMINISM WARNING: File.{operation}() called outside checkpoint\n"
|
|
54
|
-
f"{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
f"{warning_banner}\n\n"
|
|
57
|
+
"File operations are non-deterministic - "
|
|
58
|
+
"file contents can change between executions.\n\n"
|
|
59
|
+
"To fix, wrap in Step.checkpoint():\n\n"
|
|
60
|
+
" state.data = Step.checkpoint(function()\n"
|
|
59
61
|
f" return File.{operation}(...)\n"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
f"\n{
|
|
62
|
+
" end)\n\n"
|
|
63
|
+
"Why: Files can be modified, deleted, or created "
|
|
64
|
+
"between procedure executions,\n"
|
|
65
|
+
"causing different behavior on replay.\n"
|
|
66
|
+
f"\n{warning_banner}\n",
|
|
65
67
|
UserWarning,
|
|
66
68
|
stacklevel=3,
|
|
67
69
|
)
|
|
@@ -88,21 +90,21 @@ class FilePrimitive:
|
|
|
88
90
|
file_path = self._resolve_path(path)
|
|
89
91
|
|
|
90
92
|
try:
|
|
91
|
-
logger.debug(
|
|
92
|
-
with open(file_path, "r", encoding="utf-8") as
|
|
93
|
-
content =
|
|
94
|
-
logger.info(
|
|
93
|
+
logger.debug("Reading file: %s", file_path)
|
|
94
|
+
with open(file_path, "r", encoding="utf-8") as file_handle:
|
|
95
|
+
content = file_handle.read()
|
|
96
|
+
logger.info("Read %s bytes from %s", len(content), file_path)
|
|
95
97
|
return content
|
|
96
98
|
|
|
97
99
|
except FileNotFoundError:
|
|
98
|
-
|
|
99
|
-
logger.error(
|
|
100
|
-
raise FileNotFoundError(
|
|
100
|
+
error_message = f"File not found: {file_path}"
|
|
101
|
+
logger.error(error_message)
|
|
102
|
+
raise FileNotFoundError(error_message)
|
|
101
103
|
|
|
102
|
-
except Exception as
|
|
103
|
-
|
|
104
|
-
logger.error(
|
|
105
|
-
raise IOError(
|
|
104
|
+
except Exception as error:
|
|
105
|
+
error_message = f"Failed to read file {file_path}: {error}"
|
|
106
|
+
logger.error(error_message)
|
|
107
|
+
raise IOError(error_message)
|
|
106
108
|
|
|
107
109
|
def write(self, path: str, content: str) -> bool:
|
|
108
110
|
"""
|
|
@@ -130,17 +132,17 @@ class FilePrimitive:
|
|
|
130
132
|
# Create parent directories if needed
|
|
131
133
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
134
|
|
|
133
|
-
logger.debug(
|
|
134
|
-
with open(file_path, "w", encoding="utf-8") as
|
|
135
|
-
|
|
135
|
+
logger.debug("Writing to file: %s", file_path)
|
|
136
|
+
with open(file_path, "w", encoding="utf-8") as file_handle:
|
|
137
|
+
file_handle.write(content)
|
|
136
138
|
|
|
137
|
-
logger.info(
|
|
139
|
+
logger.info("Wrote %s bytes to %s", len(content), file_path)
|
|
138
140
|
return True
|
|
139
141
|
|
|
140
|
-
except Exception as
|
|
141
|
-
|
|
142
|
-
logger.error(
|
|
143
|
-
raise IOError(
|
|
142
|
+
except Exception as error:
|
|
143
|
+
error_message = f"Failed to write file {file_path}: {error}"
|
|
144
|
+
logger.error(error_message)
|
|
145
|
+
raise IOError(error_message)
|
|
144
146
|
|
|
145
147
|
def exists(self, path: str) -> bool:
|
|
146
148
|
"""
|
|
@@ -162,9 +164,9 @@ class FilePrimitive:
|
|
|
162
164
|
"""
|
|
163
165
|
self._check_determinism("exists")
|
|
164
166
|
file_path = self._resolve_path(path)
|
|
165
|
-
|
|
166
|
-
logger.debug(
|
|
167
|
-
return
|
|
167
|
+
file_exists = file_path.exists() and file_path.is_file()
|
|
168
|
+
logger.debug("File exists check for %s: %s", file_path, file_exists)
|
|
169
|
+
return file_exists
|
|
168
170
|
|
|
169
171
|
def size(self, path: str) -> int:
|
|
170
172
|
"""
|
|
@@ -187,13 +189,13 @@ class FilePrimitive:
|
|
|
187
189
|
file_path = self._resolve_path(path)
|
|
188
190
|
|
|
189
191
|
if not file_path.exists():
|
|
190
|
-
|
|
191
|
-
logger.error(
|
|
192
|
-
raise FileNotFoundError(
|
|
192
|
+
error_message = f"File not found: {file_path}"
|
|
193
|
+
logger.error(error_message)
|
|
194
|
+
raise FileNotFoundError(error_message)
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
logger.debug(
|
|
196
|
-
return
|
|
196
|
+
file_size_bytes = file_path.stat().st_size
|
|
197
|
+
logger.debug("File size for %s: %s bytes", file_path, file_size_bytes)
|
|
198
|
+
return file_size_bytes
|
|
197
199
|
|
|
198
200
|
def _resolve_path(self, path: str) -> Path:
|
|
199
201
|
"""
|
|
@@ -208,22 +210,22 @@ class FilePrimitive:
|
|
|
208
210
|
Raises:
|
|
209
211
|
ValueError: If absolute path or path traversal detected
|
|
210
212
|
"""
|
|
211
|
-
|
|
213
|
+
relative_path = Path(path)
|
|
212
214
|
|
|
213
215
|
# Security: Never allow absolute paths
|
|
214
|
-
if
|
|
216
|
+
if relative_path.is_absolute():
|
|
215
217
|
raise ValueError(f"Absolute paths not allowed: {path}")
|
|
216
218
|
|
|
217
219
|
# Resolve relative to base_path
|
|
218
|
-
|
|
220
|
+
resolved_path = (self.base_path / relative_path).resolve()
|
|
219
221
|
|
|
220
222
|
# Security: Verify resolved path is under base_path
|
|
221
223
|
try:
|
|
222
|
-
|
|
224
|
+
resolved_path.relative_to(self.base_path)
|
|
223
225
|
except ValueError:
|
|
224
226
|
raise ValueError(f"Path traversal detected: {path} resolves outside base directory")
|
|
225
227
|
|
|
226
|
-
return
|
|
228
|
+
return resolved_path
|
|
227
229
|
|
|
228
230
|
def __repr__(self) -> str:
|
|
229
231
|
return f"FilePrimitive(base_path={self.base_path})"
|
tactus/primitives/handles.py
CHANGED
|
@@ -17,7 +17,7 @@ Usage:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
-
from typing import Any, Optional,
|
|
20
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from tactus.dspy.agent import DSPyAgentHandle
|
|
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _convert_lua_table(lua_table):
|
|
29
|
+
def _convert_lua_table(lua_table: Any) -> Any:
|
|
30
30
|
"""
|
|
31
31
|
Convert a lupa Lua table to a Python dict or list.
|
|
32
32
|
|
|
@@ -87,7 +87,7 @@ class AgentHandle:
|
|
|
87
87
|
self._execution_context: Optional[Any] = None
|
|
88
88
|
logger.debug(f"AgentHandle created for '{name}'")
|
|
89
89
|
|
|
90
|
-
def __call__(self, inputs=None):
|
|
90
|
+
def __call__(self, inputs: Any = None) -> Any:
|
|
91
91
|
"""
|
|
92
92
|
Execute an agent turn using the callable interface.
|
|
93
93
|
|
|
@@ -108,27 +108,33 @@ class AgentHandle:
|
|
|
108
108
|
result = worker({message = "Process this task"})
|
|
109
109
|
print(result.response)
|
|
110
110
|
"""
|
|
111
|
-
logger.debug(
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
logger.debug(
|
|
112
|
+
"[CHECKPOINT] AgentHandle '%s'.__call__ invoked, _primitive=%s, "
|
|
113
|
+
"_execution_context=%s",
|
|
114
|
+
self.name,
|
|
115
|
+
self._primitive is not None,
|
|
116
|
+
self._execution_context is not None,
|
|
117
|
+
)
|
|
114
118
|
if self._primitive is None:
|
|
115
119
|
raise RuntimeError(
|
|
116
120
|
f"Agent '{self.name}' initialization failed.\n"
|
|
117
121
|
f"This should not happen with immediate agent creation.\n"
|
|
118
122
|
f"Please report this as a bug with a minimal reproduction example."
|
|
119
123
|
)
|
|
120
|
-
# Convert Lua table to Python dict if needed
|
|
121
|
-
converted_inputs = _convert_lua_table(inputs) if inputs is not None else None
|
|
122
|
-
|
|
123
|
-
# Convenience: allow shorthand string calls in Lua:
|
|
124
|
-
# World("Hello") == World({message = "Hello"})
|
|
125
|
-
if isinstance(converted_inputs, str):
|
|
126
|
-
converted_inputs = {"message": converted_inputs}
|
|
124
|
+
# Convert Lua table to Python dict if needed
|
|
125
|
+
converted_inputs = _convert_lua_table(inputs) if inputs is not None else None
|
|
126
|
+
|
|
127
|
+
# Convenience: allow shorthand string calls in Lua:
|
|
128
|
+
# World("Hello") == World({message = "Hello"})
|
|
129
|
+
if isinstance(converted_inputs, str):
|
|
130
|
+
converted_inputs = {"message": converted_inputs}
|
|
127
131
|
|
|
128
132
|
# If we have an execution context, checkpoint the agent call
|
|
129
|
-
logger.debug(
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
logger.debug(
|
|
134
|
+
"[CHECKPOINT] AgentHandle '%s' called, has_execution_context=%s",
|
|
135
|
+
self.name,
|
|
136
|
+
self._execution_context is not None,
|
|
137
|
+
)
|
|
132
138
|
if self._execution_context is not None:
|
|
133
139
|
|
|
134
140
|
def agent_call():
|
|
@@ -148,18 +154,20 @@ class AgentHandle:
|
|
|
148
154
|
"file": info.get("source", "unknown"),
|
|
149
155
|
"line": info.get("currentline", 0),
|
|
150
156
|
}
|
|
151
|
-
except Exception as
|
|
152
|
-
logger.debug(
|
|
153
|
-
|
|
154
|
-
logger.debug(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
except Exception as error:
|
|
158
|
+
logger.debug("Could not capture source location: %s", error)
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
"[CHECKPOINT] Creating checkpoint for agent '%s', type=agent_turn, source_info=%s",
|
|
162
|
+
self.name,
|
|
163
|
+
source_info,
|
|
164
|
+
)
|
|
165
|
+
result = self._execution_context.checkpoint(
|
|
166
|
+
agent_call, checkpoint_type="agent_turn", source_info=source_info
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
# No execution context - call directly without checkpointing
|
|
170
|
+
result = self._primitive(converted_inputs)
|
|
163
171
|
|
|
164
172
|
# Convenience: expose the last agent output on the handle as `.output`
|
|
165
173
|
# for Lua patterns like `agent(); return agent.output`.
|
|
@@ -201,9 +209,13 @@ class AgentHandle:
|
|
|
201
209
|
"""
|
|
202
210
|
self._primitive = primitive
|
|
203
211
|
self._execution_context = execution_context
|
|
204
|
-
logger.debug(
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
logger.debug(
|
|
213
|
+
"[CHECKPOINT] AgentHandle '%s' connected to primitive (checkpointing=%s, "
|
|
214
|
+
"execution_context=%s)",
|
|
215
|
+
self.name,
|
|
216
|
+
"enabled" if execution_context else "disabled",
|
|
217
|
+
execution_context,
|
|
218
|
+
)
|
|
207
219
|
|
|
208
220
|
def __repr__(self) -> str:
|
|
209
221
|
connected = "connected" if self._primitive else "disconnected"
|
|
@@ -227,7 +239,7 @@ class ModelHandle:
|
|
|
227
239
|
"""
|
|
228
240
|
self.name = name
|
|
229
241
|
self._primitive: Optional["ModelPrimitive"] = None
|
|
230
|
-
logger.debug(
|
|
242
|
+
logger.debug("ModelHandle created for '%s'", name)
|
|
231
243
|
|
|
232
244
|
def predict(self, data: Any) -> Any:
|
|
233
245
|
"""
|
|
@@ -289,7 +301,7 @@ class ModelHandle:
|
|
|
289
301
|
primitive: The ModelPrimitive to delegate to
|
|
290
302
|
"""
|
|
291
303
|
self._primitive = primitive
|
|
292
|
-
logger.debug(
|
|
304
|
+
logger.debug("ModelHandle '%s' connected to primitive", self.name)
|
|
293
305
|
|
|
294
306
|
def __repr__(self) -> str:
|
|
295
307
|
connected = "connected" if self._primitive else "disconnected"
|
|
@@ -303,7 +315,7 @@ class AgentLookup:
|
|
|
303
315
|
Injected into Lua as 'Agent'. Callable to look up agents by name.
|
|
304
316
|
"""
|
|
305
317
|
|
|
306
|
-
def __init__(self, registry:
|
|
318
|
+
def __init__(self, registry: dict[str, AgentHandle]):
|
|
307
319
|
"""
|
|
308
320
|
Initialize with reference to the agent registry.
|
|
309
321
|
|
|
@@ -344,7 +356,7 @@ class ModelLookup:
|
|
|
344
356
|
Injected into Lua as 'Model'. Callable to look up models by name.
|
|
345
357
|
"""
|
|
346
358
|
|
|
347
|
-
def __init__(self, registry:
|
|
359
|
+
def __init__(self, registry: dict[str, ModelHandle]):
|
|
348
360
|
"""
|
|
349
361
|
Initialize with reference to the model registry.
|
|
350
362
|
|
tactus/primitives/host.py
CHANGED
|
@@ -9,9 +9,10 @@ It delegates allowlisted operations to the trusted host-side broker via the
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Optional
|
|
13
13
|
|
|
14
14
|
from tactus.broker.client import BrokerClient
|
|
15
|
+
from tactus.utils.asyncio_helpers import clear_closed_event_loop
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class HostPrimitive:
|
|
@@ -27,7 +28,7 @@ class HostPrimitive:
|
|
|
27
28
|
|
|
28
29
|
self._registry = HostToolRegistry.default()
|
|
29
30
|
|
|
30
|
-
def _run_coro(self,
|
|
31
|
+
def _run_coro(self, coroutine: Any) -> Any:
|
|
31
32
|
"""
|
|
32
33
|
Run an async coroutine from Lua's synchronous context.
|
|
33
34
|
|
|
@@ -38,37 +39,38 @@ class HostPrimitive:
|
|
|
38
39
|
|
|
39
40
|
import threading
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
thread_result = {"value": None, "exception": None}
|
|
42
43
|
|
|
43
44
|
def run_in_thread():
|
|
44
45
|
try:
|
|
45
|
-
|
|
46
|
-
except Exception as
|
|
47
|
-
|
|
46
|
+
thread_result["value"] = asyncio.run(coroutine)
|
|
47
|
+
except Exception as error:
|
|
48
|
+
thread_result["exception"] = error
|
|
48
49
|
|
|
49
50
|
thread = threading.Thread(target=run_in_thread)
|
|
50
51
|
thread.start()
|
|
51
52
|
thread.join()
|
|
52
53
|
|
|
53
|
-
if
|
|
54
|
-
raise
|
|
55
|
-
return
|
|
54
|
+
if thread_result["exception"]:
|
|
55
|
+
raise thread_result["exception"]
|
|
56
|
+
return thread_result["value"]
|
|
56
57
|
|
|
57
58
|
except RuntimeError:
|
|
58
|
-
|
|
59
|
+
clear_closed_event_loop()
|
|
60
|
+
return asyncio.run(coroutine)
|
|
59
61
|
|
|
60
|
-
def _lua_to_python(self,
|
|
61
|
-
if
|
|
62
|
+
def _lua_to_python(self, value: Any) -> Any:
|
|
63
|
+
if value is None:
|
|
62
64
|
return None
|
|
63
|
-
if hasattr(
|
|
64
|
-
return {k: self._lua_to_python(v) for k, v in
|
|
65
|
-
if isinstance(
|
|
66
|
-
return {k: self._lua_to_python(v) for k, v in
|
|
67
|
-
if isinstance(
|
|
68
|
-
return [self._lua_to_python(v) for v in
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
def call(self, name: str, args: Optional[
|
|
65
|
+
if hasattr(value, "items") and not isinstance(value, dict):
|
|
66
|
+
return {k: self._lua_to_python(v) for k, v in value.items()}
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
return {k: self._lua_to_python(v) for k, v in value.items()}
|
|
69
|
+
if isinstance(value, (list, tuple)):
|
|
70
|
+
return [self._lua_to_python(v) for v in value]
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def call(self, name: str, args: Optional[dict[str, Any]] = None) -> Any:
|
|
72
74
|
"""
|
|
73
75
|
Call an allowlisted host tool via the broker.
|
|
74
76
|
|
|
@@ -78,17 +80,17 @@ class HostPrimitive:
|
|
|
78
80
|
if not isinstance(name, str) or not name:
|
|
79
81
|
raise ValueError("Host.call requires a non-empty tool name string")
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
if not isinstance(
|
|
83
|
+
args_payload = self._lua_to_python(args) or {}
|
|
84
|
+
if not isinstance(args_payload, dict):
|
|
83
85
|
raise ValueError("Host.call args must be an object/table")
|
|
84
86
|
|
|
85
87
|
if self._client is not None:
|
|
86
|
-
return self._run_coro(self._client.call_tool(name=name, args=
|
|
88
|
+
return self._run_coro(self._client.call_tool(name=name, args=args_payload))
|
|
87
89
|
|
|
88
90
|
if self._registry is not None:
|
|
89
91
|
try:
|
|
90
|
-
return self._registry.call(name,
|
|
91
|
-
except KeyError as
|
|
92
|
-
raise RuntimeError(f"Tool not allowlisted: {name}") from
|
|
92
|
+
return self._registry.call(name, args_payload)
|
|
93
|
+
except KeyError as error:
|
|
94
|
+
raise RuntimeError(f"Tool not allowlisted: {name}") from error
|
|
93
95
|
|
|
94
96
|
raise RuntimeError("Host.call requires TACTUS_BROKER_SOCKET to be set")
|