tactus 0.34.0__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.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/exceptions.py CHANGED
@@ -22,11 +22,20 @@ class ProcedureWaitingForHuman(Exception):
22
22
  4. Wait for resume trigger
23
23
  """
24
24
 
25
+ message_template = (
26
+ "Procedure {procedure_id} waiting for human response to message {pending_message_id}"
27
+ )
28
+
29
+ procedure_id: str
30
+ pending_message_id: str
31
+
25
32
  def __init__(self, procedure_id: str, pending_message_id: str):
26
33
  self.procedure_id = procedure_id
27
34
  self.pending_message_id = pending_message_id
28
35
  super().__init__(
29
- f"Procedure {procedure_id} waiting for human response to message {pending_message_id}"
36
+ self.message_template.format(
37
+ procedure_id=procedure_id, pending_message_id=pending_message_id
38
+ )
30
39
  )
31
40
 
32
41
 
@@ -6,7 +6,7 @@ Uses pluggable storage and HITL handlers via protocols.
6
6
  """
7
7
 
8
8
  from abc import ABC, abstractmethod
9
- from typing import Any, Optional, Callable, List, Dict
9
+ from typing import Any, Callable
10
10
  from datetime import datetime, timezone
11
11
  import logging
12
12
  import time
@@ -39,7 +39,7 @@ class ExecutionContext(ABC):
39
39
  self,
40
40
  fn: Callable[[], Any],
41
41
  checkpoint_type: str,
42
- source_info: Optional[Dict[str, Any]] = None,
42
+ source_info: dict[str, Any] | None = None,
43
43
  ) -> Any:
44
44
  """
45
45
  Execute fn with position-based checkpointing. On replay, return stored result.
@@ -59,9 +59,9 @@ class ExecutionContext(ABC):
59
59
  self,
60
60
  request_type: str,
61
61
  message: str,
62
- timeout_seconds: Optional[int],
62
+ timeout_seconds: int | None,
63
63
  default_value: Any,
64
- options: Optional[List[dict]],
64
+ options: list[dict] | None,
65
65
  metadata: dict,
66
66
  ) -> HITLResponse:
67
67
  """
@@ -121,7 +121,7 @@ class BaseExecutionContext(ExecutionContext):
121
121
  self,
122
122
  procedure_id: str,
123
123
  storage_backend: StorageBackend,
124
- hitl_handler: Optional[HITLHandler] = None,
124
+ hitl_handler: HITLHandler | None = None,
125
125
  strict_determinism: bool = False,
126
126
  log_handler=None,
127
127
  ):
@@ -145,14 +145,14 @@ class BaseExecutionContext(ExecutionContext):
145
145
  self._inside_checkpoint = False
146
146
 
147
147
  # Run ID tracking for distinguishing between different executions
148
- self.current_run_id: Optional[str] = None
148
+ self.current_run_id: str | None = None
149
149
 
150
150
  # .tac file tracking for accurate source locations
151
- self.current_tac_file: Optional[str] = None
152
- self.current_tac_content: Optional[str] = None
151
+ self.current_tac_file: str | None = None
152
+ self.current_tac_content: str | None = None
153
153
 
154
154
  # Lua sandbox reference for debug.getinfo access
155
- self.lua_sandbox: Optional[Any] = None
155
+ self.lua_sandbox: Any | None = None
156
156
 
157
157
  # Rich metadata for HITL notifications
158
158
  self.procedure_name: str = procedure_id # Use procedure_id as default name
@@ -172,7 +172,7 @@ class BaseExecutionContext(ExecutionContext):
172
172
  """Set the run_id for subsequent checkpoints in this execution."""
173
173
  self.current_run_id = run_id
174
174
 
175
- def set_tac_file(self, file_path: str, content: Optional[str] = None) -> None:
175
+ def set_tac_file(self, file_path: str, content: str | None = None) -> None:
176
176
  """
177
177
  Store the currently executing .tac file for accurate source location capture.
178
178
 
@@ -188,7 +188,7 @@ class BaseExecutionContext(ExecutionContext):
188
188
  self.lua_sandbox = lua_sandbox
189
189
 
190
190
  def set_procedure_metadata(
191
- self, procedure_name: Optional[str] = None, input_data: Any = None
191
+ self, procedure_name: str | None = None, input_data: Any = None
192
192
  ) -> None:
193
193
  """
194
194
  Set rich metadata for HITL notifications.
@@ -206,7 +206,7 @@ class BaseExecutionContext(ExecutionContext):
206
206
  self,
207
207
  fn: Callable[[], Any],
208
208
  checkpoint_type: str,
209
- source_info: Optional[Dict[str, Any]] = None,
209
+ source_info: dict[str, Any] | None = None,
210
210
  ) -> Any:
211
211
  """
212
212
  Execute fn with position-based checkpointing and source tracking.
@@ -215,52 +215,67 @@ class BaseExecutionContext(ExecutionContext):
215
215
  On first execution, runs fn(), records in log, and returns result.
216
216
  """
217
217
  logger.info(
218
- f"[CHECKPOINT] checkpoint() called, type={checkpoint_type}, position={self.metadata.replay_index}, "
219
- f"current_run_id={self.current_run_id}, has_log_handler={self.log_handler is not None}"
218
+ "[CHECKPOINT] checkpoint() called, type=%s, position=%s, current_run_id=%s, "
219
+ "has_log_handler=%s",
220
+ checkpoint_type,
221
+ self.metadata.replay_index,
222
+ self.current_run_id,
223
+ self.log_handler is not None,
220
224
  )
221
- current_position = self.metadata.replay_index
225
+ checkpoint_position = self.metadata.replay_index
222
226
 
223
227
  # Check if we're in replay mode (checkpoint exists at this position)
224
- if current_position < len(self.metadata.execution_log):
225
- entry = self.metadata.execution_log[current_position]
228
+ if checkpoint_position < len(self.metadata.execution_log):
229
+ checkpoint_entry = self.metadata.execution_log[checkpoint_position]
226
230
  logger.info(
227
- f"[CHECKPOINT] Found existing checkpoint at position {current_position}: "
228
- f"type={entry.type}, run_id={entry.run_id}, result_type={type(entry.result).__name__}"
231
+ "[CHECKPOINT] Found existing checkpoint at position %s: type=%s, run_id=%s, "
232
+ "result_type=%s",
233
+ checkpoint_position,
234
+ checkpoint_entry.type,
235
+ checkpoint_entry.run_id,
236
+ type(checkpoint_entry.result).__name__,
229
237
  )
230
238
 
231
239
  # CRITICAL: Only replay checkpoints from the CURRENT run
232
240
  # Each new run should execute fresh, not use cached results from previous runs
233
- if entry.run_id != self.current_run_id:
241
+ if checkpoint_entry.run_id != self.current_run_id:
234
242
  logger.info(
235
- f"[CHECKPOINT] Checkpoint is from DIFFERENT run "
236
- f"(checkpoint run_id={entry.run_id}, current run_id={self.current_run_id}), "
237
- f"executing fresh (NOT replaying)"
243
+ "[CHECKPOINT] Checkpoint is from DIFFERENT run (checkpoint run_id=%s, "
244
+ "current run_id=%s), executing fresh (NOT replaying)",
245
+ checkpoint_entry.run_id,
246
+ self.current_run_id,
238
247
  )
239
248
  # Fall through to execute mode - this is a new run
240
249
  # Special case: HITL checkpoints may have result=None if saved before response arrived
241
250
  # In this case, re-execute to check for cached response from control loop
242
- elif entry.result is None and checkpoint_type.startswith("hitl_"):
251
+ elif checkpoint_entry.result is None and checkpoint_type.startswith("hitl_"):
243
252
  logger.info(
244
- f"[CHECKPOINT] HITL checkpoint at position {current_position} has no result, "
245
- f"re-executing to check for cached response"
253
+ "[CHECKPOINT] HITL checkpoint at position %s has no result, re-executing "
254
+ "to check for cached response",
255
+ checkpoint_position,
246
256
  )
247
257
  # Fall through to execute mode - will check for cached response
248
258
  else:
249
259
  # Normal replay: return cached result from CURRENT run
250
260
  self.metadata.replay_index += 1
251
261
  logger.info(
252
- f"[CHECKPOINT] REPLAYING checkpoint at position {current_position}, "
253
- f"type={entry.type}, run_id={entry.run_id}, returning cached result"
262
+ "[CHECKPOINT] REPLAYING checkpoint at position %s, type=%s, run_id=%s, "
263
+ "returning cached result",
264
+ checkpoint_position,
265
+ checkpoint_entry.type,
266
+ checkpoint_entry.run_id,
254
267
  )
255
- return entry.result
268
+ return checkpoint_entry.result
256
269
  else:
257
270
  logger.info(
258
- f"[CHECKPOINT] No checkpoint at position {current_position} "
259
- f"(only {len(self.metadata.execution_log)} checkpoints exist), executing fresh"
271
+ "[CHECKPOINT] No checkpoint at position %s (only %s checkpoints exist), "
272
+ "executing fresh",
273
+ checkpoint_position,
274
+ len(self.metadata.execution_log),
260
275
  )
261
276
 
262
277
  # Execute mode: run function with checkpoint scope tracking
263
- old_checkpoint_flag = self._inside_checkpoint
278
+ previous_checkpoint_state = self._inside_checkpoint
264
279
  self._inside_checkpoint = True
265
280
 
266
281
  # Capture source location if provided
@@ -282,17 +297,17 @@ class BaseExecutionContext(ExecutionContext):
282
297
  )
283
298
 
284
299
  try:
285
- start_time = time.time()
300
+ execution_start_time = time.time()
286
301
  result = fn()
287
- duration_ms = (time.time() - start_time) * 1000
302
+ execution_duration_ms = (time.time() - execution_start_time) * 1000
288
303
 
289
304
  # Create checkpoint entry with source location and run_id (if available)
290
- entry = CheckpointEntry(
291
- position=current_position,
305
+ checkpoint_entry = CheckpointEntry(
306
+ position=checkpoint_position,
292
307
  type=checkpoint_type,
293
308
  result=result,
294
309
  timestamp=datetime.now(timezone.utc),
295
- duration_ms=duration_ms,
310
+ duration_ms=execution_duration_ms,
296
311
  run_id=self.current_run_id, # Can be None for backward compatibility
297
312
  source_location=source_location,
298
313
  captured_vars=(
@@ -303,13 +318,13 @@ class BaseExecutionContext(ExecutionContext):
303
318
  # CRITICAL: For HITL checkpoints, we need to save the checkpoint BEFORE exiting
304
319
  # This enables transparent resume - on restart, we'll have a checkpoint at this position
305
320
  # with result=None, and the control loop will check for cached responses
306
- duration_ms = (time.time() - start_time) * 1000
307
- entry = CheckpointEntry(
308
- position=current_position,
321
+ execution_duration_ms = (time.time() - execution_start_time) * 1000
322
+ checkpoint_entry = CheckpointEntry(
323
+ position=checkpoint_position,
309
324
  type=checkpoint_type,
310
325
  result=None, # Will be filled in when response arrives
311
326
  timestamp=datetime.now(timezone.utc),
312
- duration_ms=duration_ms,
327
+ duration_ms=execution_duration_ms,
313
328
  run_id=self.current_run_id,
314
329
  source_location=source_location,
315
330
  captured_vars=(
@@ -317,38 +332,41 @@ class BaseExecutionContext(ExecutionContext):
317
332
  ),
318
333
  )
319
334
  # Only append if checkpoint doesn't already exist (from previous failed attempt)
320
- if current_position < len(self.metadata.execution_log):
335
+ if checkpoint_position < len(self.metadata.execution_log):
321
336
  # Checkpoint already exists - update it
322
337
  logger.debug(
323
- f"[CHECKPOINT] Updating existing HITL checkpoint at position {current_position} before exit"
338
+ "[CHECKPOINT] Updating existing HITL checkpoint at position %s " "before exit",
339
+ checkpoint_position,
324
340
  )
325
- self.metadata.execution_log[current_position] = entry
341
+ self.metadata.execution_log[checkpoint_position] = checkpoint_entry
326
342
  else:
327
343
  # New checkpoint - append and increment
328
344
  logger.debug(
329
- f"[CHECKPOINT] Creating new HITL checkpoint at position {current_position} before exit"
345
+ "[CHECKPOINT] Creating new HITL checkpoint at position %s before exit",
346
+ checkpoint_position,
330
347
  )
331
- self.metadata.execution_log.append(entry)
348
+ self.metadata.execution_log.append(checkpoint_entry)
332
349
  self.metadata.replay_index += 1
333
350
 
334
351
  self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
335
352
  # Restore checkpoint flag and re-raise
336
- self._inside_checkpoint = old_checkpoint_flag
353
+ self._inside_checkpoint = previous_checkpoint_state
337
354
  raise
338
355
  finally:
339
356
  # Always restore checkpoint flag, even if fn() raises
340
- self._inside_checkpoint = old_checkpoint_flag
357
+ self._inside_checkpoint = previous_checkpoint_state
341
358
 
342
359
  # Add to execution log (or update if checkpoint already exists from HITL exit)
343
- if current_position < len(self.metadata.execution_log):
360
+ if checkpoint_position < len(self.metadata.execution_log):
344
361
  # Checkpoint already exists (saved during HITL exit) - update it with the result
345
362
  logger.debug(
346
- f"[CHECKPOINT] Updating existing HITL checkpoint at position {current_position} with result"
363
+ "[CHECKPOINT] Updating existing HITL checkpoint at position %s with result",
364
+ checkpoint_position,
347
365
  )
348
- self.metadata.execution_log[current_position] = entry
366
+ self.metadata.execution_log[checkpoint_position] = checkpoint_entry
349
367
  else:
350
368
  # New checkpoint - append to log
351
- self.metadata.execution_log.append(entry)
369
+ self.metadata.execution_log.append(checkpoint_entry)
352
370
  self.metadata.replay_index += 1
353
371
 
354
372
  # Emit checkpoint created event if we have a log handler
@@ -357,18 +375,22 @@ class BaseExecutionContext(ExecutionContext):
357
375
  from tactus.protocols.models import CheckpointCreatedEvent
358
376
 
359
377
  event = CheckpointCreatedEvent(
360
- checkpoint_position=current_position,
378
+ checkpoint_position=checkpoint_position,
361
379
  checkpoint_type=checkpoint_type,
362
- duration_ms=duration_ms,
380
+ duration_ms=execution_duration_ms,
363
381
  source_location=source_location,
364
382
  procedure_id=self.procedure_id,
365
383
  )
366
384
  logger.debug(
367
- f"[CHECKPOINT] Emitting CheckpointCreatedEvent: position={current_position}, type={checkpoint_type}, duration_ms={duration_ms}"
385
+ "[CHECKPOINT] Emitting CheckpointCreatedEvent: position=%s, type=%s, "
386
+ "duration_ms=%s",
387
+ checkpoint_position,
388
+ checkpoint_type,
389
+ execution_duration_ms,
368
390
  )
369
391
  self.log_handler.log(event)
370
- except Exception as e:
371
- logger.warning(f"Failed to emit checkpoint event: {e}")
392
+ except Exception as exception:
393
+ logger.warning("Failed to emit checkpoint event: %s", exception)
372
394
  else:
373
395
  logger.warning("[CHECKPOINT] No log_handler available to emit checkpoint event")
374
396
 
@@ -377,14 +399,16 @@ class BaseExecutionContext(ExecutionContext):
377
399
 
378
400
  return result
379
401
 
380
- def _get_code_context(self, file_path: str, line: int, context_lines: int = 3) -> Optional[str]:
402
+ def _get_code_context(
403
+ self, file_path: str, line_number: int, context_lines: int = 3
404
+ ) -> str | None:
381
405
  """Read source file and extract surrounding lines for debugging."""
382
406
  try:
383
- with open(file_path, "r") as f:
384
- lines = f.readlines()
385
- start = max(0, line - context_lines - 1)
386
- end = min(len(lines), line + context_lines)
387
- return "".join(lines[start:end])
407
+ with open(file_path, "r") as source_file:
408
+ lines = source_file.readlines()
409
+ start_index = max(0, line_number - context_lines - 1)
410
+ end_index = min(len(lines), line_number + context_lines)
411
+ return "".join(lines[start_index:end_index])
388
412
  except Exception:
389
413
  return None
390
414
 
@@ -392,9 +416,9 @@ class BaseExecutionContext(ExecutionContext):
392
416
  self,
393
417
  request_type: str,
394
418
  message: str,
395
- timeout_seconds: Optional[int],
419
+ timeout_seconds: int | None,
396
420
  default_value: Any,
397
- options: Optional[List[dict]],
421
+ options: list[dict] | None,
398
422
  metadata: dict,
399
423
  ) -> HITLResponse:
400
424
  """
@@ -402,20 +426,25 @@ class BaseExecutionContext(ExecutionContext):
402
426
 
403
427
  Delegates to the HITLHandler protocol implementation.
404
428
  """
429
+ message_preview = message[:50] if message else "None"
405
430
  logger.debug(
406
- f"[HITL] wait_for_human called: type={request_type}, message={message[:50] if message else 'None'}, hitl_handler={self.hitl}"
431
+ "[HITL] wait_for_human called: type=%s, message=%s, hitl_handler=%s",
432
+ request_type,
433
+ message_preview,
434
+ self.hitl,
407
435
  )
408
436
  if not self.hitl:
409
437
  # No HITL handler - return default immediately
410
438
  logger.warning(
411
- f"[HITL] No HITL handler configured - returning default value: {default_value}"
439
+ "[HITL] No HITL handler configured - returning default value: %s",
440
+ default_value,
412
441
  )
413
442
  return HITLResponse(
414
443
  value=default_value, responded_at=datetime.now(timezone.utc), timed_out=True
415
444
  )
416
445
 
417
446
  # Create HITL request
418
- request = HITLRequest(
447
+ hitl_request = HITLRequest(
419
448
  request_type=request_type,
420
449
  message=message,
421
450
  timeout_seconds=timeout_seconds,
@@ -426,7 +455,9 @@ class BaseExecutionContext(ExecutionContext):
426
455
 
427
456
  # Delegate to HITL handler (may raise ProcedureWaitingForHuman)
428
457
  # Pass self (execution_context) for deterministic request ID generation
429
- return self.hitl.request_interaction(self.procedure_id, request, execution_context=self)
458
+ return self.hitl.request_interaction(
459
+ self.procedure_id, hitl_request, execution_context=self
460
+ )
430
461
 
431
462
  def sleep(self, seconds: int) -> None:
432
463
  """
@@ -465,14 +496,11 @@ class BaseExecutionContext(ExecutionContext):
465
496
  Args:
466
497
  handle: ProcedureHandle instance
467
498
  """
468
- # Store in metadata under "async_procedures" key
469
- if "async_procedures" not in self.metadata:
470
- self.metadata["async_procedures"] = {}
471
-
472
- self.metadata["async_procedures"][handle.procedure_id] = handle.to_dict()
499
+ async_procedure_handles = self._get_async_procedures()
500
+ async_procedure_handles[handle.procedure_id] = handle.to_dict()
473
501
  self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
474
502
 
475
- def get_procedure_handle(self, procedure_id: str) -> Optional[Dict[str, Any]]:
503
+ def get_procedure_handle(self, procedure_id: str) -> dict[str, Any] | None:
476
504
  """
477
505
  Retrieve procedure handle.
478
506
 
@@ -482,17 +510,16 @@ class BaseExecutionContext(ExecutionContext):
482
510
  Returns:
483
511
  Handle dict or None
484
512
  """
485
- async_procedures = self.metadata.get("async_procedures", {})
486
- return async_procedures.get(procedure_id)
513
+ return self._get_async_procedures().get(procedure_id)
487
514
 
488
- def list_pending_procedures(self) -> List[Dict[str, Any]]:
515
+ def list_pending_procedures(self) -> list[dict[str, Any]]:
489
516
  """
490
517
  List all pending async procedures.
491
518
 
492
519
  Returns:
493
520
  List of handle dicts for procedures with status "running" or "waiting"
494
521
  """
495
- async_procedures = self.metadata.get("async_procedures", {})
522
+ async_procedures = self._get_async_procedures()
496
523
  return [
497
524
  handle
498
525
  for handle in async_procedures.values()
@@ -511,11 +538,9 @@ class BaseExecutionContext(ExecutionContext):
511
538
  result: Optional result value
512
539
  error: Optional error message
513
540
  """
514
- if "async_procedures" not in self.metadata:
515
- return
516
-
517
- if procedure_id in self.metadata["async_procedures"]:
518
- handle = self.metadata["async_procedures"][procedure_id]
541
+ async_procedures = self._get_async_procedures()
542
+ if procedure_id in async_procedures:
543
+ handle = async_procedures[procedure_id]
519
544
  handle["status"] = status
520
545
  if result is not None:
521
546
  handle["result"] = result
@@ -526,6 +551,17 @@ class BaseExecutionContext(ExecutionContext):
526
551
 
527
552
  self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
528
553
 
554
+ def _get_async_procedures(self) -> dict[str, Any]:
555
+ """Return the async procedures map stored on metadata."""
556
+ if isinstance(self.metadata, dict):
557
+ return self.metadata.setdefault("async_procedures", {})
558
+ store = getattr(self.metadata, "__dict__", None)
559
+ if store is None:
560
+ return {}
561
+ if "async_procedures" not in store:
562
+ store["async_procedures"] = {}
563
+ return store["async_procedures"]
564
+
529
565
  def save_execution_run(
530
566
  self, procedure_name: str, file_path: str, status: str = "COMPLETED"
531
567
  ) -> str:
@@ -568,19 +604,19 @@ class BaseExecutionContext(ExecutionContext):
568
604
 
569
605
  return run_id
570
606
 
571
- def get_subject(self) -> Optional[str]:
607
+ def get_subject(self) -> str | None:
572
608
  """
573
609
  Return a human-readable subject line for this execution.
574
610
 
575
611
  Returns:
576
612
  Subject line combining procedure name and current checkpoint position
577
613
  """
578
- checkpoint_pos = self.next_position()
614
+ checkpoint_position = self.next_position()
579
615
  if self.procedure_name:
580
- return f"{self.procedure_name} (checkpoint {checkpoint_pos})"
581
- return f"Procedure {self.procedure_id} (checkpoint {checkpoint_pos})"
616
+ return f"{self.procedure_name} (checkpoint {checkpoint_position})"
617
+ return f"Procedure {self.procedure_id} (checkpoint {checkpoint_position})"
582
618
 
583
- def get_started_at(self) -> Optional[datetime]:
619
+ def get_started_at(self) -> datetime | None:
584
620
  """
585
621
  Return when this execution started.
586
622
 
@@ -589,7 +625,7 @@ class BaseExecutionContext(ExecutionContext):
589
625
  """
590
626
  return self._started_at
591
627
 
592
- def get_input_summary(self) -> Optional[Dict[str, Any]]:
628
+ def get_input_summary(self) -> dict[str, Any] | None:
593
629
  """
594
630
  Return a summary of the initial input to this procedure.
595
631
 
@@ -606,7 +642,7 @@ class BaseExecutionContext(ExecutionContext):
606
642
  # Otherwise wrap it in a dict
607
643
  return {"value": self._input_data}
608
644
 
609
- def get_conversation_history(self) -> Optional[List[Dict]]:
645
+ def get_conversation_history(self) -> list[dict] | None:
610
646
  """
611
647
  Return conversation history if available.
612
648
 
@@ -617,7 +653,7 @@ class BaseExecutionContext(ExecutionContext):
617
653
  # in future implementations
618
654
  return None
619
655
 
620
- def get_prior_control_interactions(self) -> Optional[List[Dict]]:
656
+ def get_prior_control_interactions(self) -> list[dict] | None:
621
657
  """
622
658
  Return list of prior HITL interactions in this execution.
623
659
 
@@ -641,7 +677,7 @@ class BaseExecutionContext(ExecutionContext):
641
677
 
642
678
  return hitl_checkpoints if hitl_checkpoints else None
643
679
 
644
- def get_lua_source_line(self) -> Optional[int]:
680
+ def get_lua_source_line(self) -> int | None:
645
681
  """
646
682
  Get the current source line from Lua debug.getinfo.
647
683
 
@@ -653,27 +689,31 @@ class BaseExecutionContext(ExecutionContext):
653
689
 
654
690
  try:
655
691
  # Access Lua debug module to get current line
656
- debug_mod = self.lua_sandbox.globals().debug
657
- if debug_mod and hasattr(debug_mod, "getinfo"):
692
+ debug_module = self.lua_sandbox.globals().debug
693
+ if debug_module and hasattr(debug_module, "getinfo"):
658
694
  # getinfo(2) gets info about the calling function
659
695
  # We need to go up the stack to find the user's code
660
696
  for level in range(2, 10):
661
697
  try:
662
- info = debug_mod.getinfo(level, "Sl")
663
- if info:
664
- line = info.get("currentline")
665
- source = info.get("source", "")
698
+ debug_info = debug_module.getinfo(level, "Sl")
699
+ if debug_info:
700
+ current_line = debug_info.get("currentline")
701
+ source_name = debug_info.get("source", "")
666
702
  # Skip internal sources (start with @)
667
- if line and line > 0 and not source.startswith("@"):
668
- return int(line)
703
+ if (
704
+ current_line
705
+ and current_line > 0
706
+ and not source_name.startswith("@")
707
+ ):
708
+ return int(current_line)
669
709
  except Exception:
670
710
  break
671
- except Exception as e:
672
- logger.debug(f"Could not get Lua source line: {e}")
711
+ except Exception as exception:
712
+ logger.debug("Could not get Lua source line: %s", exception)
673
713
 
674
714
  return None
675
715
 
676
- def get_runtime_context(self) -> Dict[str, Any]:
716
+ def get_runtime_context(self) -> dict[str, Any]:
677
717
  """
678
718
  Build RuntimeContext dict for HITL requests.
679
719
 
@@ -683,9 +723,9 @@ class BaseExecutionContext(ExecutionContext):
683
723
  Dict with runtime context fields
684
724
  """
685
725
  # Calculate elapsed time
686
- elapsed = 0.0
726
+ elapsed_seconds = 0.0
687
727
  if self._started_at:
688
- elapsed = (datetime.now(timezone.utc) - self._started_at).total_seconds()
728
+ elapsed_seconds = (datetime.now(timezone.utc) - self._started_at).total_seconds()
689
729
 
690
730
  # Get current source location
691
731
  source_line = self.get_lua_source_line()
@@ -694,14 +734,14 @@ class BaseExecutionContext(ExecutionContext):
694
734
  backtrace = []
695
735
  if self.metadata and self.metadata.execution_log:
696
736
  for entry in self.metadata.execution_log:
697
- bt_entry = {
737
+ backtrace_entry = {
698
738
  "checkpoint_type": entry.type,
699
739
  "duration_ms": entry.duration_ms,
700
740
  }
701
741
  if entry.source_location:
702
- bt_entry["line"] = entry.source_location.line
703
- bt_entry["function_name"] = entry.source_location.function
704
- backtrace.append(bt_entry)
742
+ backtrace_entry["line"] = entry.source_location.line
743
+ backtrace_entry["function_name"] = entry.source_location.function
744
+ backtrace.append(backtrace_entry)
705
745
 
706
746
  return {
707
747
  "source_line": source_line,
@@ -710,7 +750,7 @@ class BaseExecutionContext(ExecutionContext):
710
750
  "procedure_name": self.procedure_name,
711
751
  "invocation_id": self.invocation_id,
712
752
  "started_at": self._started_at.isoformat() if self._started_at else None,
713
- "elapsed_seconds": elapsed,
753
+ "elapsed_seconds": elapsed_seconds,
714
754
  "backtrace": backtrace,
715
755
  }
716
756
 
@@ -723,7 +763,7 @@ class InMemoryExecutionContext(BaseExecutionContext):
723
763
  and simple CLI workflows that don't need to survive restarts.
724
764
  """
725
765
 
726
- def __init__(self, procedure_id: str, hitl_handler: Optional[HITLHandler] = None):
766
+ def __init__(self, procedure_id: str, hitl_handler: HITLHandler | None = None):
727
767
  """
728
768
  Initialize with in-memory storage.
729
769