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/procedure.py
CHANGED
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import uuid
|
|
10
10
|
import asyncio
|
|
11
11
|
import threading
|
|
12
|
-
from typing import Any, Optional,
|
|
12
|
+
from typing import Any, Optional, Callable
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ class ProcedureHandle:
|
|
|
29
29
|
completed_at: Optional[datetime] = None
|
|
30
30
|
thread: Optional[threading.Thread] = None
|
|
31
31
|
|
|
32
|
-
def to_dict(self) ->
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
33
|
"""Convert to dictionary for Lua access."""
|
|
34
34
|
return {
|
|
35
35
|
"procedure_id": self.procedure_id,
|
|
@@ -74,7 +74,7 @@ class ProcedurePrimitive:
|
|
|
74
74
|
def __init__(
|
|
75
75
|
self,
|
|
76
76
|
execution_context: Any,
|
|
77
|
-
runtime_factory: Callable[[str,
|
|
77
|
+
runtime_factory: Callable[[str, dict[str, Any]], Any],
|
|
78
78
|
lua_sandbox: Any = None,
|
|
79
79
|
max_depth: int = 5,
|
|
80
80
|
current_depth: int = 0,
|
|
@@ -94,10 +94,14 @@ class ProcedurePrimitive:
|
|
|
94
94
|
self.lua_sandbox = lua_sandbox
|
|
95
95
|
self.max_depth = max_depth
|
|
96
96
|
self.current_depth = current_depth
|
|
97
|
-
self.handles:
|
|
97
|
+
self.handles: dict[str, ProcedureHandle] = {}
|
|
98
98
|
self._lock = threading.Lock()
|
|
99
99
|
|
|
100
|
-
logger.info(
|
|
100
|
+
logger.info(
|
|
101
|
+
"ProcedurePrimitive initialized (depth %s/%s)",
|
|
102
|
+
current_depth,
|
|
103
|
+
max_depth,
|
|
104
|
+
)
|
|
101
105
|
|
|
102
106
|
def __call__(self, name: str) -> Any:
|
|
103
107
|
"""
|
|
@@ -112,16 +116,16 @@ class ProcedurePrimitive:
|
|
|
112
116
|
raise ProcedureExecutionError("Procedure lookup is not available (lua_sandbox missing)")
|
|
113
117
|
|
|
114
118
|
try:
|
|
115
|
-
|
|
119
|
+
procedure_callable = self.lua_sandbox.lua.globals()[name]
|
|
116
120
|
except Exception:
|
|
117
|
-
|
|
121
|
+
procedure_callable = None
|
|
118
122
|
|
|
119
|
-
if
|
|
123
|
+
if procedure_callable is None:
|
|
120
124
|
raise ProcedureExecutionError(f"Named procedure '{name}' not found")
|
|
121
125
|
|
|
122
|
-
return
|
|
126
|
+
return procedure_callable
|
|
123
127
|
|
|
124
|
-
def run(self, name: str, params: Optional[
|
|
128
|
+
def run(self, name: str, params: Optional[dict[str, Any]] = None) -> Any:
|
|
125
129
|
"""
|
|
126
130
|
Synchronous procedure invocation with auto-checkpointing.
|
|
127
131
|
|
|
@@ -143,16 +147,20 @@ class ProcedurePrimitive:
|
|
|
143
147
|
if self.current_depth >= self.max_depth:
|
|
144
148
|
raise ProcedureRecursionError(f"Maximum recursion depth ({self.max_depth}) exceeded")
|
|
145
149
|
|
|
146
|
-
logger.info(
|
|
150
|
+
logger.info(
|
|
151
|
+
"Running procedure '%s' synchronously (depth %s)",
|
|
152
|
+
name,
|
|
153
|
+
self.current_depth,
|
|
154
|
+
)
|
|
147
155
|
|
|
148
156
|
# Normalize params
|
|
149
|
-
|
|
150
|
-
if hasattr(
|
|
157
|
+
procedure_params = params or {}
|
|
158
|
+
if hasattr(procedure_params, "items"):
|
|
151
159
|
from tactus.core.dsl_stubs import lua_table_to_dict
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
if isinstance(
|
|
155
|
-
|
|
161
|
+
procedure_params = lua_table_to_dict(procedure_params)
|
|
162
|
+
if isinstance(procedure_params, list) and len(procedure_params) == 0:
|
|
163
|
+
procedure_params = {}
|
|
156
164
|
|
|
157
165
|
# Wrap execution in checkpoint for durability
|
|
158
166
|
def execute_procedure():
|
|
@@ -161,14 +169,13 @@ class ProcedurePrimitive:
|
|
|
161
169
|
source = self._load_procedure_source(name)
|
|
162
170
|
|
|
163
171
|
# Create runtime for sub-procedure
|
|
164
|
-
runtime = self.runtime_factory(name,
|
|
172
|
+
runtime = self.runtime_factory(name, procedure_params)
|
|
165
173
|
|
|
166
174
|
# Execute synchronously (runtime.execute is async, so we need to run it)
|
|
167
|
-
import asyncio
|
|
168
|
-
import threading
|
|
169
|
-
|
|
170
175
|
async def run_subprocedure():
|
|
171
|
-
return await runtime.execute(
|
|
176
|
+
return await runtime.execute(
|
|
177
|
+
source=source, context=procedure_params, format="lua"
|
|
178
|
+
)
|
|
172
179
|
|
|
173
180
|
try:
|
|
174
181
|
asyncio.get_running_loop()
|
|
@@ -177,18 +184,18 @@ class ProcedurePrimitive:
|
|
|
177
184
|
has_running_loop = False
|
|
178
185
|
|
|
179
186
|
if has_running_loop:
|
|
180
|
-
result_holder = {}
|
|
181
|
-
error_holder = {}
|
|
187
|
+
result_holder: dict[str, Any] = {}
|
|
188
|
+
error_holder: dict[str, Exception] = {}
|
|
182
189
|
|
|
183
190
|
def run_in_thread():
|
|
184
191
|
try:
|
|
185
192
|
result_holder["result"] = asyncio.run(run_subprocedure())
|
|
186
|
-
except Exception as
|
|
187
|
-
error_holder["error"] =
|
|
193
|
+
except Exception as error:
|
|
194
|
+
error_holder["error"] = error
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
worker_thread = threading.Thread(target=run_in_thread, daemon=True)
|
|
197
|
+
worker_thread.start()
|
|
198
|
+
worker_thread.join()
|
|
192
199
|
|
|
193
200
|
if "error" in error_holder:
|
|
194
201
|
raise error_holder["error"]
|
|
@@ -199,20 +206,20 @@ class ProcedurePrimitive:
|
|
|
199
206
|
|
|
200
207
|
# Extract result from execution response
|
|
201
208
|
if result.get("success"):
|
|
202
|
-
logger.info(
|
|
209
|
+
logger.info("Procedure '%s' completed successfully", name)
|
|
203
210
|
return result.get("result")
|
|
204
211
|
else:
|
|
205
212
|
error_msg = result.get("error", "Unknown error")
|
|
206
|
-
logger.error(
|
|
213
|
+
logger.error("Procedure '%s' failed: %s", name, error_msg)
|
|
207
214
|
raise ProcedureExecutionError(f"Procedure '{name}' failed: {error_msg}")
|
|
208
215
|
|
|
209
216
|
except ProcedureExecutionError:
|
|
210
217
|
raise
|
|
211
218
|
except ProcedureRecursionError:
|
|
212
219
|
raise
|
|
213
|
-
except Exception as
|
|
214
|
-
logger.error(
|
|
215
|
-
raise ProcedureExecutionError(f"Failed to execute procedure '{name}': {
|
|
220
|
+
except Exception as error:
|
|
221
|
+
logger.error("Error executing procedure '%s': %s", name, error)
|
|
222
|
+
raise ProcedureExecutionError(f"Failed to execute procedure '{name}': {error}")
|
|
216
223
|
|
|
217
224
|
# Auto-checkpoint sub-procedure call
|
|
218
225
|
# Try to capture Lua source location if available
|
|
@@ -228,9 +235,9 @@ class ProcedurePrimitive:
|
|
|
228
235
|
try:
|
|
229
236
|
info = lua_globals.debug.getinfo(level, "Sl")
|
|
230
237
|
if info:
|
|
231
|
-
|
|
232
|
-
source =
|
|
233
|
-
line =
|
|
238
|
+
lua_debug_info = dict(info.items()) if hasattr(info, "items") else {}
|
|
239
|
+
source = lua_debug_info.get("source", "")
|
|
240
|
+
line = lua_debug_info.get("currentline", -1)
|
|
234
241
|
# Look for a valid source location (not -1, not C function, not internal)
|
|
235
242
|
if (
|
|
236
243
|
line > 0
|
|
@@ -238,7 +245,7 @@ class ProcedurePrimitive:
|
|
|
238
245
|
and not source.startswith("=[C]")
|
|
239
246
|
and not source.startswith("[string")
|
|
240
247
|
):
|
|
241
|
-
debug_info =
|
|
248
|
+
debug_info = lua_debug_info
|
|
242
249
|
break
|
|
243
250
|
except Exception:
|
|
244
251
|
continue
|
|
@@ -257,9 +264,9 @@ class ProcedurePrimitive:
|
|
|
257
264
|
if not source_info:
|
|
258
265
|
import inspect
|
|
259
266
|
|
|
260
|
-
|
|
261
|
-
if
|
|
262
|
-
caller_frame =
|
|
267
|
+
current_frame = inspect.currentframe()
|
|
268
|
+
if current_frame and current_frame.f_back:
|
|
269
|
+
caller_frame = current_frame.f_back
|
|
263
270
|
# Use .tac file if available, otherwise use Python file
|
|
264
271
|
source_info = {
|
|
265
272
|
"file": self.execution_context.current_tac_file
|
|
@@ -272,7 +279,7 @@ class ProcedurePrimitive:
|
|
|
272
279
|
execute_procedure, "procedure_call", source_info=source_info
|
|
273
280
|
)
|
|
274
281
|
|
|
275
|
-
def spawn(self, name: str, params: Optional[
|
|
282
|
+
def spawn(self, name: str, params: Optional[dict[str, Any]] = None) -> ProcedureHandle:
|
|
276
283
|
"""
|
|
277
284
|
Async procedure invocation.
|
|
278
285
|
|
|
@@ -298,19 +305,23 @@ class ProcedurePrimitive:
|
|
|
298
305
|
with self._lock:
|
|
299
306
|
self.handles[procedure_id] = handle
|
|
300
307
|
|
|
301
|
-
logger.info(
|
|
308
|
+
logger.info(
|
|
309
|
+
"Spawning procedure '%s' asynchronously (id: %s)",
|
|
310
|
+
name,
|
|
311
|
+
procedure_id,
|
|
312
|
+
)
|
|
302
313
|
|
|
303
314
|
# Start async execution in thread
|
|
304
|
-
|
|
315
|
+
procedure_params = params or {}
|
|
305
316
|
thread = threading.Thread(
|
|
306
|
-
target=self._execute_async, args=(handle, name,
|
|
317
|
+
target=self._execute_async, args=(handle, name, procedure_params), daemon=True
|
|
307
318
|
)
|
|
308
319
|
handle.thread = thread
|
|
309
320
|
thread.start()
|
|
310
321
|
|
|
311
322
|
return handle
|
|
312
323
|
|
|
313
|
-
def _execute_async(self, handle: ProcedureHandle, name: str, params:
|
|
324
|
+
def _execute_async(self, handle: ProcedureHandle, name: str, params: dict[str, Any]):
|
|
314
325
|
"""Execute procedure asynchronously in background thread."""
|
|
315
326
|
try:
|
|
316
327
|
# Load procedure source
|
|
@@ -320,36 +331,44 @@ class ProcedurePrimitive:
|
|
|
320
331
|
runtime = self.runtime_factory(name, params)
|
|
321
332
|
|
|
322
333
|
# Execute in new event loop (thread-safe)
|
|
323
|
-
|
|
324
|
-
asyncio.set_event_loop(
|
|
334
|
+
event_loop = asyncio.new_event_loop()
|
|
335
|
+
asyncio.set_event_loop(event_loop)
|
|
325
336
|
|
|
326
|
-
result =
|
|
337
|
+
result = event_loop.run_until_complete(
|
|
327
338
|
runtime.execute(source=source, context=params, format="lua")
|
|
328
339
|
)
|
|
329
340
|
|
|
330
|
-
|
|
341
|
+
event_loop.close()
|
|
331
342
|
|
|
332
343
|
# Update handle
|
|
333
344
|
with self._lock:
|
|
334
345
|
if result.get("success"):
|
|
335
346
|
handle.status = "completed"
|
|
336
347
|
handle.result = result.get("result")
|
|
337
|
-
logger.info(
|
|
348
|
+
logger.info(
|
|
349
|
+
"Async procedure '%s' completed (id: %s)",
|
|
350
|
+
name,
|
|
351
|
+
handle.procedure_id,
|
|
352
|
+
)
|
|
338
353
|
else:
|
|
339
354
|
handle.status = "failed"
|
|
340
355
|
handle.error = result.get("error", "Unknown error")
|
|
341
|
-
logger.error(
|
|
356
|
+
logger.error(
|
|
357
|
+
"Async procedure '%s' failed: %s",
|
|
358
|
+
name,
|
|
359
|
+
handle.error,
|
|
360
|
+
)
|
|
342
361
|
|
|
343
362
|
handle.completed_at = datetime.now()
|
|
344
363
|
|
|
345
|
-
except Exception as
|
|
346
|
-
logger.error(
|
|
364
|
+
except Exception as error:
|
|
365
|
+
logger.error("Error in async procedure '%s': %s", name, error)
|
|
347
366
|
with self._lock:
|
|
348
367
|
handle.status = "failed"
|
|
349
|
-
handle.error = str(
|
|
368
|
+
handle.error = str(error)
|
|
350
369
|
handle.completed_at = datetime.now()
|
|
351
370
|
|
|
352
|
-
def status(self, handle: ProcedureHandle) ->
|
|
371
|
+
def status(self, handle: ProcedureHandle) -> dict[str, Any]:
|
|
353
372
|
"""
|
|
354
373
|
Get procedure status.
|
|
355
374
|
|
|
@@ -377,7 +396,7 @@ class ProcedurePrimitive:
|
|
|
377
396
|
ProcedureExecutionError: If procedure failed
|
|
378
397
|
TimeoutError: If timeout exceeded
|
|
379
398
|
"""
|
|
380
|
-
logger.debug(
|
|
399
|
+
logger.debug("Waiting for procedure %s", handle.procedure_id)
|
|
381
400
|
|
|
382
401
|
# Wait for thread to complete
|
|
383
402
|
if handle.thread:
|
|
@@ -409,7 +428,10 @@ class ProcedurePrimitive:
|
|
|
409
428
|
Note: This is a placeholder - full implementation requires
|
|
410
429
|
communication channel with running procedure.
|
|
411
430
|
"""
|
|
412
|
-
logger.warning(
|
|
431
|
+
logger.warning(
|
|
432
|
+
"Procedure.inject() not fully implemented - message ignored: %s",
|
|
433
|
+
message,
|
|
434
|
+
)
|
|
413
435
|
# TODO: Implement message injection mechanism
|
|
414
436
|
|
|
415
437
|
def cancel(self, handle: ProcedureHandle):
|
|
@@ -422,7 +444,7 @@ class ProcedurePrimitive:
|
|
|
422
444
|
Note: Python threads cannot be forcefully cancelled,
|
|
423
445
|
so this just marks the status.
|
|
424
446
|
"""
|
|
425
|
-
logger.info(
|
|
447
|
+
logger.info("Cancelling procedure %s", handle.procedure_id)
|
|
426
448
|
|
|
427
449
|
with self._lock:
|
|
428
450
|
handle.status = "cancelled"
|
|
@@ -430,7 +452,7 @@ class ProcedurePrimitive:
|
|
|
430
452
|
|
|
431
453
|
# Note: Thread will continue running but result will be ignored
|
|
432
454
|
|
|
433
|
-
def wait_any(self, handles:
|
|
455
|
+
def wait_any(self, handles: list[ProcedureHandle]) -> ProcedureHandle:
|
|
434
456
|
"""
|
|
435
457
|
Wait for first completion.
|
|
436
458
|
|
|
@@ -440,7 +462,7 @@ class ProcedurePrimitive:
|
|
|
440
462
|
Returns:
|
|
441
463
|
First completed handle
|
|
442
464
|
"""
|
|
443
|
-
logger.debug(
|
|
465
|
+
logger.debug("Waiting for any of %s procedures", len(handles))
|
|
444
466
|
|
|
445
467
|
while True:
|
|
446
468
|
# Check if any completed
|
|
@@ -454,7 +476,7 @@ class ProcedurePrimitive:
|
|
|
454
476
|
|
|
455
477
|
time.sleep(0.1)
|
|
456
478
|
|
|
457
|
-
def wait_all(self, handles:
|
|
479
|
+
def wait_all(self, handles: list[ProcedureHandle]) -> list[Any]:
|
|
458
480
|
"""
|
|
459
481
|
Wait for all completions.
|
|
460
482
|
|
|
@@ -464,7 +486,7 @@ class ProcedurePrimitive:
|
|
|
464
486
|
Returns:
|
|
465
487
|
List of results
|
|
466
488
|
"""
|
|
467
|
-
logger.debug(
|
|
489
|
+
logger.debug("Waiting for all %s procedures", len(handles))
|
|
468
490
|
|
|
469
491
|
results = []
|
|
470
492
|
for handle in handles:
|
|
@@ -486,7 +508,7 @@ class ProcedurePrimitive:
|
|
|
486
508
|
with self._lock:
|
|
487
509
|
return handle.status in ("completed", "failed", "cancelled")
|
|
488
510
|
|
|
489
|
-
def all_complete(self, handles:
|
|
511
|
+
def all_complete(self, handles: list[ProcedureHandle]) -> bool:
|
|
490
512
|
"""
|
|
491
513
|
Check if all procedures are complete.
|
|
492
514
|
|
|
@@ -555,7 +577,7 @@ class ProcedurePrimitive:
|
|
|
555
577
|
for path in search_paths:
|
|
556
578
|
try:
|
|
557
579
|
if path.exists() and path.is_file():
|
|
558
|
-
logger.debug(
|
|
580
|
+
logger.debug("Loading procedure from: %s", path)
|
|
559
581
|
return path.read_text()
|
|
560
582
|
except Exception:
|
|
561
583
|
continue
|
|
@@ -5,7 +5,7 @@ This module provides the ProcedureCallable class that enables direct function
|
|
|
5
5
|
call syntax for named procedures with automatic checkpointing and replay support.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ProcedureCallable:
|
|
@@ -31,9 +31,9 @@ class ProcedureCallable:
|
|
|
31
31
|
self,
|
|
32
32
|
name: str,
|
|
33
33
|
procedure_function: Any, # Lua function reference
|
|
34
|
-
input_schema:
|
|
35
|
-
output_schema:
|
|
36
|
-
state_schema:
|
|
34
|
+
input_schema: dict[str, Any],
|
|
35
|
+
output_schema: dict[str, Any],
|
|
36
|
+
state_schema: dict[str, Any],
|
|
37
37
|
execution_context, # ExecutionContext instance
|
|
38
38
|
lua_sandbox, # LuaSandbox instance
|
|
39
39
|
is_main: bool = False, # Whether this is the main entry procedure
|
|
@@ -60,7 +60,7 @@ class ProcedureCallable:
|
|
|
60
60
|
self.lua_sandbox = lua_sandbox
|
|
61
61
|
self.is_main = is_main
|
|
62
62
|
|
|
63
|
-
def __call__(self, params: Optional[
|
|
63
|
+
def __call__(self, params: Optional[dict[str, Any]] = None) -> Any:
|
|
64
64
|
"""
|
|
65
65
|
Execute the sub-procedure when called from Lua.
|
|
66
66
|
|
|
@@ -75,57 +75,57 @@ class ProcedureCallable:
|
|
|
75
75
|
Raises:
|
|
76
76
|
ValueError: If input validation fails or output is missing required fields
|
|
77
77
|
"""
|
|
78
|
-
|
|
78
|
+
input_params = params or {}
|
|
79
79
|
|
|
80
80
|
# Convert Lua table to dict if needed
|
|
81
|
-
if hasattr(
|
|
81
|
+
if hasattr(input_params, "items"):
|
|
82
82
|
from tactus.core.dsl_stubs import lua_table_to_dict
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
input_params = lua_table_to_dict(input_params)
|
|
85
85
|
|
|
86
86
|
# Handle empty list case (lua_table_to_dict converts empty {} to [])
|
|
87
|
-
if isinstance(
|
|
88
|
-
|
|
87
|
+
if isinstance(input_params, list) and len(input_params) == 0:
|
|
88
|
+
input_params = {}
|
|
89
89
|
|
|
90
90
|
# Validate input against schema
|
|
91
|
-
self._validate_input(
|
|
91
|
+
self._validate_input(input_params)
|
|
92
92
|
|
|
93
93
|
# Wrap execution in checkpoint for automatic replay
|
|
94
94
|
def execute_procedure():
|
|
95
95
|
# Convert Python lists/dicts to Lua tables before setting as input
|
|
96
|
-
def
|
|
96
|
+
def convert_python_value_to_lua(value):
|
|
97
97
|
"""Recursively convert Python lists and dicts to Lua tables."""
|
|
98
98
|
if isinstance(value, list):
|
|
99
99
|
# Convert Python list to Lua table (1-indexed)
|
|
100
100
|
lua_table = self.lua_sandbox.lua.table()
|
|
101
101
|
for i, item in enumerate(value, 1):
|
|
102
|
-
lua_table[i] =
|
|
102
|
+
lua_table[i] = convert_python_value_to_lua(item)
|
|
103
103
|
return lua_table
|
|
104
104
|
elif isinstance(value, dict):
|
|
105
105
|
# Convert Python dict to Lua table
|
|
106
106
|
lua_table = self.lua_sandbox.lua.table()
|
|
107
107
|
for k, v in value.items():
|
|
108
|
-
lua_table[k] =
|
|
108
|
+
lua_table[k] = convert_python_value_to_lua(v)
|
|
109
109
|
return lua_table
|
|
110
110
|
else:
|
|
111
111
|
return value
|
|
112
112
|
|
|
113
113
|
# Convert params to Lua-compatible format
|
|
114
|
-
|
|
115
|
-
for key, value in
|
|
116
|
-
|
|
114
|
+
lua_input_table = self.lua_sandbox.lua.table()
|
|
115
|
+
for key, value in input_params.items():
|
|
116
|
+
lua_input_table[key] = convert_python_value_to_lua(value)
|
|
117
117
|
|
|
118
118
|
# Initialize state defaults WITHOUT replacing the state table
|
|
119
119
|
# (preserving the metatable setup)
|
|
120
|
-
|
|
121
|
-
if
|
|
120
|
+
state_default_values = self._initialize_state()
|
|
121
|
+
if state_default_values:
|
|
122
122
|
# Access state via globals and assign - this will trigger the metatable
|
|
123
123
|
state_table = self.lua_sandbox.lua.globals()["state"]
|
|
124
|
-
for key, value in
|
|
125
|
-
state_table[key] =
|
|
124
|
+
for key, value in state_default_values.items():
|
|
125
|
+
state_table[key] = convert_python_value_to_lua(value)
|
|
126
126
|
|
|
127
127
|
# Execute the procedure function with input as explicit parameter
|
|
128
|
-
result = self.procedure_function(
|
|
128
|
+
result = self.procedure_function(lua_input_table)
|
|
129
129
|
|
|
130
130
|
# Convert Lua table result to Python dict
|
|
131
131
|
# Check for lupa table (not Python dict/list)
|
|
@@ -157,29 +157,33 @@ class ProcedureCallable:
|
|
|
157
157
|
if hasattr(lua_globals, "debug") and hasattr(lua_globals.debug, "getinfo"):
|
|
158
158
|
# Try different stack levels to find the Lua caller
|
|
159
159
|
debug_info = None
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for level in [1, 2, 3, 4, 5, 6, 7, 8]:
|
|
160
|
+
self._write_debug_line(f"DEBUG: Trying debug.getinfo for {self.name}")
|
|
161
|
+
for stack_level in [1, 2, 3, 4, 5, 6, 7, 8]:
|
|
163
162
|
try:
|
|
164
|
-
info = lua_globals.debug.getinfo(
|
|
163
|
+
info = lua_globals.debug.getinfo(stack_level, "Sl")
|
|
165
164
|
if info:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
lua_debug_info = (
|
|
166
|
+
dict(info.items()) if hasattr(info, "items") else {}
|
|
167
|
+
)
|
|
168
|
+
source = lua_debug_info.get("source", "")
|
|
169
|
+
line = lua_debug_info.get("currentline", -1)
|
|
170
|
+
self._write_debug_line(
|
|
171
|
+
f"DEBUG: Level {stack_level}: source={source}, line={line}"
|
|
172
|
+
)
|
|
171
173
|
# Look for a valid source location (not -1, not C function)
|
|
172
174
|
# Accept [string "<python>"] sources since that's our Lua code
|
|
173
175
|
if line > 0 and source:
|
|
174
176
|
if source.startswith("=[C]"):
|
|
175
177
|
continue # Skip C functions
|
|
176
|
-
debug_info =
|
|
177
|
-
|
|
178
|
-
f
|
|
178
|
+
debug_info = lua_debug_info
|
|
179
|
+
self._write_debug_line(
|
|
180
|
+
f"DEBUG: Found valid source at level {stack_level}"
|
|
181
|
+
)
|
|
179
182
|
break
|
|
180
|
-
except Exception as
|
|
181
|
-
|
|
182
|
-
f
|
|
183
|
+
except Exception as debug_error:
|
|
184
|
+
self._write_debug_line(
|
|
185
|
+
f"DEBUG: Level {stack_level} error: {debug_error}"
|
|
186
|
+
)
|
|
183
187
|
continue
|
|
184
188
|
|
|
185
189
|
if debug_info:
|
|
@@ -189,26 +193,24 @@ class ProcedureCallable:
|
|
|
189
193
|
"line": debug_info.get("currentline", 0),
|
|
190
194
|
"function": debug_info.get("name", self.name),
|
|
191
195
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
with open("/tmp/tactus-debug.log", "a") as f:
|
|
196
|
-
f.write(f"DEBUG: Exception getting Lua debug info: {e}\n")
|
|
196
|
+
self._write_debug_line(f"DEBUG: Final source_info: {source_info}")
|
|
197
|
+
except Exception as error:
|
|
198
|
+
self._write_debug_line(f"DEBUG: Exception getting Lua debug info: {error}")
|
|
197
199
|
|
|
198
200
|
# If we still don't have source_info, use fallback
|
|
199
201
|
if not source_info:
|
|
200
202
|
import inspect
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
if
|
|
204
|
-
caller_frame =
|
|
204
|
+
current_frame = inspect.currentframe()
|
|
205
|
+
if current_frame and current_frame.f_back:
|
|
206
|
+
caller_frame = current_frame.f_back
|
|
205
207
|
# Use .tac file if available, otherwise use Python file
|
|
206
208
|
tac_file = self.execution_context.current_tac_file
|
|
207
209
|
python_file = caller_frame.f_code.co_filename
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
self._write_debug_line(
|
|
211
|
+
"DEBUG: Fallback - current_tac_file=%s, python_file=%s"
|
|
212
|
+
% (tac_file, python_file)
|
|
213
|
+
)
|
|
212
214
|
source_info = {
|
|
213
215
|
"file": tac_file or python_file,
|
|
214
216
|
"line": 0, # Line number unknown without Lua debug
|
|
@@ -219,7 +221,7 @@ class ProcedureCallable:
|
|
|
219
221
|
execute_procedure, checkpoint_type="procedure_call", source_info=source_info
|
|
220
222
|
)
|
|
221
223
|
|
|
222
|
-
def _validate_input(self, params:
|
|
224
|
+
def _validate_input(self, params: dict[str, Any]) -> None:
|
|
223
225
|
"""
|
|
224
226
|
Validate input parameters against input schema.
|
|
225
227
|
|
|
@@ -304,7 +306,7 @@ class ProcedureCallable:
|
|
|
304
306
|
f"Procedure '{self.name}' missing required output: {field_name}"
|
|
305
307
|
)
|
|
306
308
|
|
|
307
|
-
def _initialize_state(self) ->
|
|
309
|
+
def _initialize_state(self) -> dict[str, Any]:
|
|
308
310
|
"""
|
|
309
311
|
Initialize state with default values from state schema.
|
|
310
312
|
|
|
@@ -316,3 +318,8 @@ class ProcedureCallable:
|
|
|
316
318
|
if isinstance(field_def, dict) and "default" in field_def:
|
|
317
319
|
state[field_name] = field_def["default"]
|
|
318
320
|
return state
|
|
321
|
+
|
|
322
|
+
def _write_debug_line(self, message: str) -> None:
|
|
323
|
+
"""Write a debug line to the temporary debug log."""
|
|
324
|
+
with open("/tmp/tactus-debug.log", "a") as debug_file:
|
|
325
|
+
debug_file.write(f"{message}\n")
|