tactus 0.31.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 (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,378 @@
1
+ """
2
+ DSL Handles - Lightweight placeholders returned by DSL declarations.
3
+
4
+ These handles are created during DSL parsing (before actual primitives exist)
5
+ and get connected to their real implementations at runtime via _enhance_handles().
6
+
7
+ Usage:
8
+ # During parsing:
9
+ Greeter = agent "greeter" { config } # Returns AgentHandle("greeter")
10
+
11
+ # During execution (callable syntax - preferred):
12
+ Greeter() # Direct call
13
+ Greeter({message = "Hello"}) # Call with options
14
+
15
+ # Lookup syntax also works:
16
+ Agent("greeter")() # Lookup + call
17
+ """
18
+
19
+ import logging
20
+ from typing import Any, Optional, Dict, TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from tactus.dspy.agent import DSPyAgentHandle
24
+ from tactus.primitives.model import ModelPrimitive
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _convert_lua_table(lua_table):
30
+ """
31
+ Convert a lupa Lua table to a Python dict or list.
32
+
33
+ Used to convert opts passed from Lua to Python methods.
34
+ """
35
+ if lua_table is None:
36
+ return None
37
+
38
+ # Check if it's a lupa table by checking for the items method
39
+ if not hasattr(lua_table, "items"):
40
+ # It's a primitive value (string, number, bool), return as-is
41
+ return lua_table
42
+
43
+ try:
44
+ # Get all keys
45
+ keys = list(lua_table.keys())
46
+
47
+ # Empty table - return empty dict for opts (different from dsl_stubs which returns [])
48
+ if not keys:
49
+ return {}
50
+
51
+ # Check if it's an array (all keys are consecutive integers starting from 1)
52
+ if all(isinstance(k, int) for k in keys):
53
+ sorted_keys = sorted(keys)
54
+ if sorted_keys == list(range(1, len(keys) + 1)):
55
+ # It's an array
56
+ return [_convert_lua_table(lua_table[k]) for k in sorted_keys]
57
+
58
+ # It's a dictionary
59
+ result = {}
60
+ for key, value in lua_table.items():
61
+ # Recursively convert nested tables
62
+ result[key] = _convert_lua_table(value)
63
+ return result
64
+
65
+ except (AttributeError, TypeError):
66
+ # Fallback: return as-is
67
+ return lua_table
68
+
69
+
70
+ class AgentHandle:
71
+ """
72
+ Lightweight handle returned by agent() DSL function.
73
+
74
+ Created during DSL parsing, enhanced at runtime with actual AgentPrimitive.
75
+ Supports callable syntax: agent() or agent({message = "..."})
76
+ """
77
+
78
+ def __init__(self, name: str):
79
+ """
80
+ Initialize agent handle.
81
+
82
+ Args:
83
+ name: Agent name (string identifier)
84
+ """
85
+ self.name = name
86
+ self._primitive: Optional["DSPyAgentHandle"] = None
87
+ self._execution_context: Optional[Any] = None
88
+ logger.debug(f"AgentHandle created for '{name}'")
89
+
90
+ def __call__(self, inputs=None):
91
+ """
92
+ Execute an agent turn using the callable interface.
93
+
94
+ This is the unified callable interface that allows:
95
+ result = worker({message = "Hello"})
96
+
97
+ Args:
98
+ inputs: Input dict with fields matching input_schema.
99
+ Default field 'message' is used as the user message.
100
+
101
+ Returns:
102
+ Result object with response and other fields
103
+
104
+ Raises:
105
+ RuntimeError: If handle not connected to primitive
106
+
107
+ Example (Lua):
108
+ result = worker({message = "Process this task"})
109
+ print(result.response)
110
+ """
111
+ logger.debug(
112
+ f"[CHECKPOINT] AgentHandle '{self.name}'.__call__ invoked, _primitive={self._primitive is not None}, _execution_context={self._execution_context is not None}"
113
+ )
114
+ if self._primitive is None:
115
+ raise RuntimeError(
116
+ f"Agent '{self.name}' initialization failed.\n"
117
+ f"This should not happen with immediate agent creation.\n"
118
+ f"Please report this as a bug with a minimal reproduction example."
119
+ )
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}
127
+
128
+ # If we have an execution context, checkpoint the agent call
129
+ logger.debug(
130
+ f"[CHECKPOINT] AgentHandle '{self.name}' called, has_execution_context={self._execution_context is not None}"
131
+ )
132
+ if self._execution_context is not None:
133
+
134
+ def agent_call():
135
+ return self._primitive(converted_inputs)
136
+
137
+ # Capture source location from Lua if available
138
+ source_info = None
139
+ if (
140
+ hasattr(self._execution_context, "lua_sandbox")
141
+ and self._execution_context.lua_sandbox
142
+ ):
143
+ try:
144
+ lua = self._execution_context.lua_sandbox.lua
145
+ info = lua.eval("debug.getinfo(2, 'Sl')")
146
+ if info:
147
+ source_info = {
148
+ "file": info.get("source", "unknown"),
149
+ "line": info.get("currentline", 0),
150
+ }
151
+ except Exception as e:
152
+ logger.debug(f"Could not capture source location: {e}")
153
+
154
+ logger.debug(
155
+ f"[CHECKPOINT] Creating checkpoint for agent '{self.name}', type=agent_turn, source_info={source_info}"
156
+ )
157
+ result = self._execution_context.checkpoint(
158
+ agent_call, checkpoint_type="agent_turn", source_info=source_info
159
+ )
160
+ else:
161
+ # No execution context - call directly without checkpointing
162
+ result = self._primitive(converted_inputs)
163
+
164
+ # Convenience: expose the last agent output on the handle as `.output`
165
+ # for Lua patterns like `agent(); return agent.output`.
166
+ output_text = None
167
+ if result is not None:
168
+ for attr in ("response", "message"):
169
+ try:
170
+ value = getattr(result, attr, None)
171
+ except Exception:
172
+ value = None
173
+ if isinstance(value, str):
174
+ output_text = value
175
+ break
176
+
177
+ if output_text is None and isinstance(result, dict):
178
+ for key in ("response", "message"):
179
+ value = result.get(key)
180
+ if isinstance(value, str):
181
+ output_text = value
182
+ break
183
+
184
+ if output_text is None:
185
+ output_text = str(result)
186
+
187
+ self.output = output_text
188
+ return result
189
+
190
+ def _set_primitive(
191
+ self, primitive: "DSPyAgentHandle", execution_context: Optional[Any] = None
192
+ ) -> None:
193
+ """
194
+ Connect this handle to its actual primitive and execution context.
195
+
196
+ Called by runtime._enhance_handles() after primitives are created.
197
+
198
+ Args:
199
+ primitive: The DSPyAgentHandle to delegate to
200
+ execution_context: Optional execution context for checkpointing
201
+ """
202
+ self._primitive = primitive
203
+ self._execution_context = execution_context
204
+ logger.debug(
205
+ f"[CHECKPOINT] AgentHandle '{self.name}' connected to primitive (checkpointing={'enabled' if execution_context else 'disabled'}, execution_context={execution_context})"
206
+ )
207
+
208
+ def __repr__(self) -> str:
209
+ connected = "connected" if self._primitive else "disconnected"
210
+ return f"AgentHandle('{self.name}', {connected})"
211
+
212
+
213
+ class ModelHandle:
214
+ """
215
+ Lightweight handle returned by model() DSL function.
216
+
217
+ Created during DSL parsing, enhanced at runtime with actual ModelPrimitive.
218
+ Delegates .predict() calls to the real primitive.
219
+ """
220
+
221
+ def __init__(self, name: str):
222
+ """
223
+ Initialize model handle.
224
+
225
+ Args:
226
+ name: Model name (string identifier)
227
+ """
228
+ self.name = name
229
+ self._primitive: Optional["ModelPrimitive"] = None
230
+ logger.debug(f"ModelHandle created for '{name}'")
231
+
232
+ def predict(self, data: Any) -> Any:
233
+ """
234
+ Run model prediction (delegates to ModelPrimitive.predict()).
235
+
236
+ Args:
237
+ data: Input data for prediction
238
+
239
+ Returns:
240
+ Prediction result
241
+
242
+ Raises:
243
+ RuntimeError: If handle not connected to primitive
244
+ """
245
+ if self._primitive is None:
246
+ raise RuntimeError(
247
+ f"Model '{self.name}' initialization failed.\n"
248
+ f"This should not happen - please report this as a bug."
249
+ )
250
+ converted_data = _convert_lua_table(data) if data is not None else None
251
+ return self._primitive.predict(converted_data)
252
+
253
+ def __call__(self, data: Any = None) -> Any:
254
+ """
255
+ Execute model prediction using the callable interface.
256
+
257
+ This is the unified callable interface that allows:
258
+ result = classifier({text = "Hello"})
259
+
260
+ Args:
261
+ data: Input data for prediction (format depends on model type)
262
+
263
+ Returns:
264
+ Model prediction result
265
+
266
+ Raises:
267
+ RuntimeError: If handle not connected to primitive
268
+
269
+ Example (Lua):
270
+ result = classifier({text = "This is great!"})
271
+ print(result.label) -- "positive"
272
+ """
273
+ if self._primitive is None:
274
+ raise RuntimeError(
275
+ f"Model '{self.name}' initialization failed.\n"
276
+ f"This should not happen - please report this as a bug."
277
+ )
278
+ # Convert Lua table to Python dict if needed
279
+ converted_data = _convert_lua_table(data) if data is not None else None
280
+ return self._primitive(converted_data)
281
+
282
+ def _set_primitive(self, primitive: "ModelPrimitive") -> None:
283
+ """
284
+ Connect this handle to its actual primitive.
285
+
286
+ Called by runtime._enhance_handles() after primitives are created.
287
+
288
+ Args:
289
+ primitive: The ModelPrimitive to delegate to
290
+ """
291
+ self._primitive = primitive
292
+ logger.debug(f"ModelHandle '{self.name}' connected to primitive")
293
+
294
+ def __repr__(self) -> str:
295
+ connected = "connected" if self._primitive else "disconnected"
296
+ return f"ModelHandle('{self.name}', {connected})"
297
+
298
+
299
+ class AgentLookup:
300
+ """
301
+ Agent lookup primitive - provides Agent("name") lookup functionality.
302
+
303
+ Injected into Lua as 'Agent'. Callable to look up agents by name.
304
+ """
305
+
306
+ def __init__(self, registry: Dict[str, AgentHandle]):
307
+ """
308
+ Initialize with reference to the agent registry.
309
+
310
+ Args:
311
+ registry: Dict mapping agent names to AgentHandle instances
312
+ """
313
+ self._registry = registry
314
+
315
+ def __call__(self, name: str) -> AgentHandle:
316
+ """
317
+ Look up an agent by name.
318
+
319
+ Args:
320
+ name: Agent name to look up
321
+
322
+ Returns:
323
+ AgentHandle for the named agent
324
+
325
+ Raises:
326
+ ValueError: If agent not found
327
+
328
+ Example (Lua):
329
+ Agent("greeter")() -- or Agent("greeter")({message = "Hello"})
330
+ """
331
+ if name not in self._registry:
332
+ available = list(self._registry.keys())
333
+ raise ValueError(f"Agent '{name}' not defined. Available agents: {available}")
334
+ return self._registry[name]
335
+
336
+ def __repr__(self) -> str:
337
+ return f"AgentLookup({len(self._registry)} agents)"
338
+
339
+
340
+ class ModelLookup:
341
+ """
342
+ Model lookup primitive - provides Model("name") lookup functionality.
343
+
344
+ Injected into Lua as 'Model'. Callable to look up models by name.
345
+ """
346
+
347
+ def __init__(self, registry: Dict[str, ModelHandle]):
348
+ """
349
+ Initialize with reference to the model registry.
350
+
351
+ Args:
352
+ registry: Dict mapping model names to ModelHandle instances
353
+ """
354
+ self._registry = registry
355
+
356
+ def __call__(self, name: str) -> ModelHandle:
357
+ """
358
+ Look up a model by name.
359
+
360
+ Args:
361
+ name: Model name to look up
362
+
363
+ Returns:
364
+ ModelHandle for the named model
365
+
366
+ Raises:
367
+ ValueError: If model not found
368
+
369
+ Example (Lua):
370
+ Model("classifier").predict(data)
371
+ """
372
+ if name not in self._registry:
373
+ available = list(self._registry.keys())
374
+ raise ValueError(f"Model '{name}' not defined. Available models: {available}")
375
+ return self._registry[name]
376
+
377
+ def __repr__(self) -> str:
378
+ return f"ModelLookup({len(self._registry)} models)"
@@ -0,0 +1,94 @@
1
+ """
2
+ Host Primitive - brokered host capabilities for the runtime container.
3
+
4
+ This primitive is intended to be used inside the sandboxed runtime container.
5
+ It delegates allowlisted operations to the trusted host-side broker via the
6
+ `TACTUS_BROKER_SOCKET` transport.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from typing import Any, Dict, Optional
13
+
14
+ from tactus.broker.client import BrokerClient
15
+
16
+
17
+ class HostPrimitive:
18
+ """Provides access to allowlisted host-side tools via the broker."""
19
+
20
+ def __init__(self, client: Optional[BrokerClient] = None):
21
+ self._client = client or BrokerClient.from_environment()
22
+ self._registry = None
23
+ if self._client is None:
24
+ # Allow Host.call() to work in non-sandboxed runs (and in deterministic tests)
25
+ # without requiring a broker transport, while still staying deny-by-default.
26
+ from tactus.broker.server import HostToolRegistry
27
+
28
+ self._registry = HostToolRegistry.default()
29
+
30
+ def _run_coro(self, coro):
31
+ """
32
+ Run an async coroutine from Lua's synchronous context.
33
+
34
+ Mirrors the approach used by `ToolHandle` for async tool handlers.
35
+ """
36
+ try:
37
+ asyncio.get_running_loop()
38
+
39
+ import threading
40
+
41
+ result_container = {"value": None, "exception": None}
42
+
43
+ def run_in_thread():
44
+ try:
45
+ result_container["value"] = asyncio.run(coro)
46
+ except Exception as e:
47
+ result_container["exception"] = e
48
+
49
+ thread = threading.Thread(target=run_in_thread)
50
+ thread.start()
51
+ thread.join()
52
+
53
+ if result_container["exception"]:
54
+ raise result_container["exception"]
55
+ return result_container["value"]
56
+
57
+ except RuntimeError:
58
+ return asyncio.run(coro)
59
+
60
+ def _lua_to_python(self, obj: Any) -> Any:
61
+ if obj is None:
62
+ return None
63
+ if hasattr(obj, "items") and not isinstance(obj, dict):
64
+ return {k: self._lua_to_python(v) for k, v in obj.items()}
65
+ if isinstance(obj, dict):
66
+ return {k: self._lua_to_python(v) for k, v in obj.items()}
67
+ if isinstance(obj, (list, tuple)):
68
+ return [self._lua_to_python(v) for v in obj]
69
+ return obj
70
+
71
+ def call(self, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
72
+ """
73
+ Call an allowlisted host tool via the broker.
74
+
75
+ Example (Lua):
76
+ local result = Host.call("host.ping", {value = 1})
77
+ """
78
+ if not isinstance(name, str) or not name:
79
+ raise ValueError("Host.call requires a non-empty tool name string")
80
+
81
+ args_dict = self._lua_to_python(args) or {}
82
+ if not isinstance(args_dict, dict):
83
+ raise ValueError("Host.call args must be an object/table")
84
+
85
+ if self._client is not None:
86
+ return self._run_coro(self._client.call_tool(name=name, args=args_dict))
87
+
88
+ if self._registry is not None:
89
+ try:
90
+ return self._registry.call(name, args_dict)
91
+ except KeyError as e:
92
+ raise RuntimeError(f"Tool not allowlisted: {name}") from e
93
+
94
+ raise RuntimeError("Host.call requires TACTUS_BROKER_SOCKET to be set")