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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ import logging
9
9
  import uuid
10
10
  import asyncio
11
11
  import threading
12
- from typing import Any, Optional, Dict, List, Callable
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) -> Dict[str, Any]:
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, Dict[str, Any]], Any],
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: Dict[str, ProcedureHandle] = {}
97
+ self.handles: dict[str, ProcedureHandle] = {}
98
98
  self._lock = threading.Lock()
99
99
 
100
- logger.info(f"ProcedurePrimitive initialized (depth {current_depth}/{max_depth})")
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
- proc = self.lua_sandbox.lua.globals()[name]
119
+ procedure_callable = self.lua_sandbox.lua.globals()[name]
116
120
  except Exception:
117
- proc = None
121
+ procedure_callable = None
118
122
 
119
- if proc is None:
123
+ if procedure_callable is None:
120
124
  raise ProcedureExecutionError(f"Named procedure '{name}' not found")
121
125
 
122
- return proc
126
+ return procedure_callable
123
127
 
124
- def run(self, name: str, params: Optional[Dict[str, Any]] = None) -> Any:
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(f"Running procedure '{name}' synchronously (depth {self.current_depth})")
150
+ logger.info(
151
+ "Running procedure '%s' synchronously (depth %s)",
152
+ name,
153
+ self.current_depth,
154
+ )
147
155
 
148
156
  # Normalize params
149
- params = params or {}
150
- if hasattr(params, "items"):
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
- params = lua_table_to_dict(params)
154
- if isinstance(params, list) and len(params) == 0:
155
- params = {}
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, params)
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(source=source, context=params, format="lua")
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 e:
187
- error_holder["error"] = e
193
+ except Exception as error:
194
+ error_holder["error"] = error
188
195
 
189
- t = threading.Thread(target=run_in_thread, daemon=True)
190
- t.start()
191
- t.join()
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(f"Procedure '{name}' completed successfully")
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(f"Procedure '{name}' failed: {error_msg}")
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 e:
214
- logger.error(f"Error executing procedure '{name}': {e}")
215
- raise ProcedureExecutionError(f"Failed to execute procedure '{name}': {e}")
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
- lua_dict = dict(info.items()) if hasattr(info, "items") else {}
232
- source = lua_dict.get("source", "")
233
- line = lua_dict.get("currentline", -1)
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 = lua_dict
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
- frame = inspect.currentframe()
261
- if frame and frame.f_back:
262
- caller_frame = frame.f_back
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[Dict[str, Any]] = None) -> ProcedureHandle:
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(f"Spawning procedure '{name}' asynchronously (id: {procedure_id})")
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
- params = params or {}
315
+ procedure_params = params or {}
305
316
  thread = threading.Thread(
306
- target=self._execute_async, args=(handle, name, params), daemon=True
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: Dict[str, Any]):
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
- loop = asyncio.new_event_loop()
324
- asyncio.set_event_loop(loop)
334
+ event_loop = asyncio.new_event_loop()
335
+ asyncio.set_event_loop(event_loop)
325
336
 
326
- result = loop.run_until_complete(
337
+ result = event_loop.run_until_complete(
327
338
  runtime.execute(source=source, context=params, format="lua")
328
339
  )
329
340
 
330
- loop.close()
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(f"Async procedure '{name}' completed (id: {handle.procedure_id})")
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(f"Async procedure '{name}' failed: {handle.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 e:
346
- logger.error(f"Error in async procedure '{name}': {e}")
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(e)
368
+ handle.error = str(error)
350
369
  handle.completed_at = datetime.now()
351
370
 
352
- def status(self, handle: ProcedureHandle) -> Dict[str, Any]:
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(f"Waiting for procedure {handle.procedure_id}")
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(f"Procedure.inject() not fully implemented - message ignored: {message}")
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(f"Cancelling procedure {handle.procedure_id}")
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: List[ProcedureHandle]) -> ProcedureHandle:
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(f"Waiting for any of {len(handles)} procedures")
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: List[ProcedureHandle]) -> List[Any]:
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(f"Waiting for all {len(handles)} procedures")
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: List[ProcedureHandle]) -> bool:
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(f"Loading procedure from: {path}")
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, Dict, Optional
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: Dict[str, Any],
35
- output_schema: Dict[str, Any],
36
- state_schema: Dict[str, Any],
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[Dict[str, Any]] = None) -> Any:
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
- params = params or {}
78
+ input_params = params or {}
79
79
 
80
80
  # Convert Lua table to dict if needed
81
- if hasattr(params, "items"):
81
+ if hasattr(input_params, "items"):
82
82
  from tactus.core.dsl_stubs import lua_table_to_dict
83
83
 
84
- params = lua_table_to_dict(params)
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(params, list) and len(params) == 0:
88
- params = {}
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(params)
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 convert_to_lua(value):
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] = convert_to_lua(item)
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] = convert_to_lua(v)
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
- lua_params = self.lua_sandbox.lua.table()
115
- for key, value in params.items():
116
- lua_params[key] = convert_to_lua(value)
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
- state_defaults = self._initialize_state()
121
- if state_defaults:
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 state_defaults.items():
125
- state_table[key] = convert_to_lua(value)
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(lua_params)
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
- with open("/tmp/tactus-debug.log", "a") as f:
161
- f.write(f"DEBUG: Trying debug.getinfo for {self.name}\n")
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(level, "Sl")
163
+ info = lua_globals.debug.getinfo(stack_level, "Sl")
165
164
  if info:
166
- lua_dict = dict(info.items()) if hasattr(info, "items") else {}
167
- source = lua_dict.get("source", "")
168
- line = lua_dict.get("currentline", -1)
169
- with open("/tmp/tactus-debug.log", "a") as f:
170
- f.write(f"DEBUG: Level {level}: source={source}, line={line}\n")
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 = lua_dict
177
- with open("/tmp/tactus-debug.log", "a") as f:
178
- f.write(f"DEBUG: Found valid source at level {level}\n")
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 inner_e:
181
- with open("/tmp/tactus-debug.log", "a") as f:
182
- f.write(f"DEBUG: Level {level} error: {inner_e}\n")
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
- with open("/tmp/tactus-debug.log", "a") as f:
193
- f.write(f"DEBUG: Final source_info: {source_info}\n")
194
- except Exception as e:
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
- frame = inspect.currentframe()
203
- if frame and frame.f_back:
204
- caller_frame = frame.f_back
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
- with open("/tmp/tactus-debug.log", "a") as f:
209
- f.write(
210
- f"DEBUG: Fallback - current_tac_file={tac_file}, python_file={python_file}\n"
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: Dict[str, Any]) -> None:
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) -> Dict[str, Any]:
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")