tactus 0.34.1__py3-none-any.whl → 0.35.1__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 +40 -25
  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.1.dist-info}/METADATA +15 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,7 @@ Provides:
7
7
 
8
8
  import logging
9
9
  import time
10
- from typing import Callable, Any, Optional, Dict
10
+ from typing import Any, Callable, Optional
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -27,12 +27,14 @@ class RetryPrimitive:
27
27
  """Initialize Retry primitive."""
28
28
  logger.debug("RetryPrimitive initialized")
29
29
 
30
- def with_backoff(self, fn: Callable, options: Optional[Dict[str, Any]] = None) -> Any:
30
+ def with_backoff(
31
+ self, function_to_retry: Callable, options: Optional[dict[str, Any]] = None
32
+ ) -> Any:
31
33
  """
32
34
  Retry a function with exponential backoff.
33
35
 
34
36
  Args:
35
- fn: Function to retry (Lua function)
37
+ function_to_retry: Function to retry (Lua function)
36
38
  options: Dict with:
37
39
  - max_attempts: Maximum retry attempts (default: 3)
38
40
  - initial_delay: Initial delay in seconds (default: 1)
@@ -61,58 +63,58 @@ class RetryPrimitive:
61
63
  })
62
64
  """
63
65
  # Convert Lua tables to Python dicts if needed
64
- opts = self._convert_lua_to_python(options) or {}
66
+ options_dict = self._convert_lua_to_python(options) or {}
65
67
 
66
- max_attempts = opts.get("max_attempts", 3)
67
- initial_delay = opts.get("initial_delay", 1.0)
68
- max_delay = opts.get("max_delay", 60.0)
69
- backoff_factor = opts.get("backoff_factor", 2.0)
70
- on_error = opts.get("on_error")
68
+ max_attempts = options_dict.get("max_attempts", 3)
69
+ initial_delay = options_dict.get("initial_delay", 1.0)
70
+ max_delay = options_dict.get("max_delay", 60.0)
71
+ backoff_factor = options_dict.get("backoff_factor", 2.0)
72
+ on_error = options_dict.get("on_error")
71
73
 
72
- attempt = 0
73
- delay = initial_delay
74
+ attempt_number = 0
75
+ current_delay = initial_delay
74
76
  last_error = None
75
77
 
76
- logger.info(f"Starting retry with_backoff (max_attempts={max_attempts})")
78
+ logger.info("Starting retry with_backoff (max_attempts=%s)", max_attempts)
77
79
 
78
- while attempt < max_attempts:
79
- attempt += 1
80
+ while attempt_number < max_attempts:
81
+ attempt_number += 1
80
82
 
81
83
  try:
82
- logger.debug(f"Retry attempt {attempt}/{max_attempts}")
83
- result = fn()
84
- logger.info(f"Success on attempt {attempt}/{max_attempts}")
84
+ logger.debug("Retry attempt %s/%s", attempt_number, max_attempts)
85
+ result = function_to_retry()
86
+ logger.info("Success on attempt %s/%s", attempt_number, max_attempts)
85
87
  return result
86
88
 
87
- except Exception as e:
88
- last_error = e
89
- logger.warning(f"Attempt {attempt}/{max_attempts} failed: {e}")
89
+ except Exception as error:
90
+ last_error = error
91
+ logger.warning("Attempt %s/%s failed: %s", attempt_number, max_attempts, error)
90
92
 
91
93
  # Call error callback if provided
92
94
  if on_error and callable(on_error):
93
95
  try:
94
96
  on_error(
95
97
  {
96
- "attempt": attempt,
98
+ "attempt": attempt_number,
97
99
  "max_attempts": max_attempts,
98
- "error": str(e),
99
- "delay": delay,
100
+ "error": str(error),
101
+ "delay": current_delay,
100
102
  }
101
103
  )
102
104
  except Exception as callback_error:
103
- logger.error(f"Error callback failed: {callback_error}")
105
+ logger.error("Error callback failed: %s", callback_error)
104
106
 
105
107
  # Check if we should retry
106
- if attempt >= max_attempts:
107
- logger.error(f"All {max_attempts} attempts failed")
108
+ if attempt_number >= max_attempts:
109
+ logger.error("All %s attempts failed", max_attempts)
108
110
  raise Exception(f"Retry failed after {max_attempts} attempts: {last_error}")
109
111
 
110
112
  # Wait with exponential backoff
111
- logger.info(f"Waiting {delay:.2f}s before retry...")
112
- time.sleep(delay)
113
+ logger.info("Waiting %.2fs before retry...", current_delay)
114
+ time.sleep(current_delay)
113
115
 
114
116
  # Increase delay for next attempt (exponential backoff)
115
- delay = min(delay * backoff_factor, max_delay)
117
+ current_delay = min(current_delay * backoff_factor, max_delay)
116
118
 
117
119
  # Should not reach here, but handle it
118
120
  raise Exception(f"Retry logic error: {last_error}")
@@ -9,7 +9,7 @@ from typing import Any, Optional
9
9
  try:
10
10
  from pydantic_ai.messages import ModelMessage, ModelRequest, ModelResponse, TextPart
11
11
  except ImportError:
12
- # Fallback types if pydantic_ai not available
12
+ # Fallback types when pydantic_ai is not available at runtime.
13
13
  ModelMessage = dict
14
14
  ModelRequest = dict
15
15
  ModelResponse = dict
@@ -39,28 +39,54 @@ class SessionPrimitive:
39
39
  self.session_manager = session_manager
40
40
  self.agent_name = agent_name
41
41
 
42
- def append(self, message_data: dict) -> None:
42
+ def _has_session_context(self) -> bool:
43
+ """
44
+ Return True when this primitive is bound to a session manager and agent.
45
+ """
46
+ return bool(self.session_manager and self.agent_name)
47
+
48
+ def _serialize_message(self, message: Any) -> dict[str, str]:
49
+ """
50
+ Convert a stored message into a Lua-friendly dict shape.
51
+ """
52
+ if isinstance(message, dict):
53
+ return {
54
+ "role": message.get("role", ""),
55
+ "content": str(message.get("content", "")),
56
+ }
57
+
58
+ # Handle pydantic_ai ModelMessage objects.
59
+ try:
60
+ return {
61
+ "role": getattr(message, "role", ""),
62
+ "content": str(getattr(message, "content", "")),
63
+ }
64
+ except Exception:
65
+ # Fallback: preserve content as a string with an unknown role.
66
+ return {"role": "unknown", "content": str(message)}
67
+
68
+ def append(self, message_payload: dict[str, Any]) -> None:
43
69
  """
44
70
  Append a message to the session history.
45
71
 
46
72
  Args:
47
- message_data: Dict with 'role' and 'content' keys
73
+ message_payload: dict with 'role' and 'content' keys
48
74
  role: 'user', 'assistant', 'system'
49
75
  content: message text
50
76
 
51
77
  Example:
52
78
  Session.append({role = "user", content = "Hello"})
53
79
  """
54
- if not self.session_manager or not self.agent_name:
80
+ if not self._has_session_context():
55
81
  return
56
82
 
57
- role = message_data.get("role", "user")
58
- content = message_data.get("content", "")
83
+ message_role = message_payload.get("role", "user")
84
+ message_content = message_payload.get("content", "")
59
85
 
60
86
  # Create a simple message dict
61
- message = {"role": role, "content": content}
87
+ message_entry = {"role": message_role, "content": message_content}
62
88
 
63
- self.session_manager.add_message(self.agent_name, message)
89
+ self.session_manager.add_message(self.agent_name, message_entry)
64
90
 
65
91
  def inject_system(self, text: str) -> None:
66
92
  """
@@ -84,12 +110,12 @@ class SessionPrimitive:
84
110
  Example:
85
111
  Session.clear()
86
112
  """
87
- if not self.session_manager or not self.agent_name:
113
+ if not self._has_session_context():
88
114
  return
89
115
 
90
116
  self.session_manager.clear_agent_history(self.agent_name)
91
117
 
92
- def history(self) -> list:
118
+ def history(self) -> list[dict[str, str]]:
93
119
  """
94
120
  Get the full conversation history for this agent.
95
121
 
@@ -102,30 +128,17 @@ class SessionPrimitive:
102
128
  Log.info(msg.role .. ": " .. msg.content)
103
129
  end
104
130
  """
105
- if not self.session_manager or not self.agent_name:
131
+ if not self._has_session_context():
106
132
  return []
107
133
 
108
134
  messages = self.session_manager.histories.get(self.agent_name, [])
109
135
 
110
136
  # Convert to Lua-friendly format
111
- result = []
112
- for msg in messages:
113
- if isinstance(msg, dict):
114
- result.append({"role": msg.get("role", ""), "content": str(msg.get("content", ""))})
115
- else:
116
- # Handle pydantic_ai ModelMessage objects
117
- try:
118
- result.append(
119
- {
120
- "role": getattr(msg, "role", ""),
121
- "content": str(getattr(msg, "content", "")),
122
- }
123
- )
124
- except Exception:
125
- # Fallback: convert to string
126
- result.append({"role": "unknown", "content": str(msg)})
127
-
128
- return result
137
+ serialized_messages: list[dict[str, str]] = [
138
+ self._serialize_message(message) for message in messages
139
+ ]
140
+
141
+ return serialized_messages
129
142
 
130
143
  def load_from_node(self, node: Any) -> None:
131
144
  """
@@ -10,7 +10,7 @@ Provides:
10
10
  """
11
11
 
12
12
  import logging
13
- from typing import Any, Dict
13
+ from typing import Any
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -23,22 +23,26 @@ class StatePrimitive:
23
23
  progress, accumulate results, and coordinate between agents.
24
24
  """
25
25
 
26
- def __init__(self, state_schema: Dict[str, Any] = None):
26
+ def __init__(self, state_schema: dict[str, Any] | None = None):
27
27
  """
28
28
  Initialize state storage.
29
29
 
30
30
  Args:
31
31
  state_schema: Optional state schema with field definitions and defaults
32
32
  """
33
- self._state: Dict[str, Any] = {}
34
- self._schema: Dict[str, Any] = state_schema or {}
33
+ self._state_values: dict[str, Any] = {}
34
+ self._state = self._state_values
35
+ self._schema_definitions: dict[str, Any] = state_schema or {}
35
36
 
36
37
  # Initialize state with defaults from schema
37
- for key, field_def in self._schema.items():
38
- if isinstance(field_def, dict) and "default" in field_def:
39
- self._state[key] = field_def["default"]
38
+ for state_key, schema_field_definition in self._schema_definitions.items():
39
+ if isinstance(schema_field_definition, dict) and "default" in schema_field_definition:
40
+ self._state_values[state_key] = schema_field_definition["default"]
40
41
 
41
- logger.debug(f"StatePrimitive initialized with {len(self._schema)} schema fields")
42
+ logger.debug(
43
+ "StatePrimitive initialized with %s schema fields",
44
+ len(self._schema_definitions),
45
+ )
42
46
 
43
47
  def get(self, key: str, default: Any = None) -> Any:
44
48
  """
@@ -54,9 +58,9 @@ class StatePrimitive:
54
58
  Example (Lua):
55
59
  local count = State.get("hypothesis_count", 0)
56
60
  """
57
- value = self._state.get(key, default)
58
- logger.debug(f"State.get('{key}') = {value}")
59
- return value
61
+ stored_value = self._state_values.get(key, default)
62
+ logger.debug("State.get('%s') = %s", key, stored_value)
63
+ return stored_value
60
64
 
61
65
  def set(self, key: str, value: Any) -> None:
62
66
  """
@@ -70,18 +74,20 @@ class StatePrimitive:
70
74
  State.set("current_phase", "exploration")
71
75
  """
72
76
  # Validate against schema if present
73
- if key in self._schema:
74
- field_def = self._schema[key]
75
- if isinstance(field_def, dict) and "type" in field_def:
76
- expected_type = field_def["type"]
77
- if not self._validate_type(value, expected_type):
77
+ if key in self._schema_definitions:
78
+ schema_field_definition = self._schema_definitions[key]
79
+ if isinstance(schema_field_definition, dict) and "type" in schema_field_definition:
80
+ expected_type = schema_field_definition["type"]
81
+ if not self._is_value_matching_schema_type(value, expected_type):
78
82
  logger.warning(
79
- f"State.set('{key}'): value type {type(value).__name__} "
80
- f"does not match schema type {expected_type}"
83
+ "State.set('%s'): value type %s does not match schema type %s",
84
+ key,
85
+ type(value).__name__,
86
+ expected_type,
81
87
  )
82
88
 
83
- self._state[key] = value
84
- logger.debug(f"State.set('{key}', {value})")
89
+ self._state_values[key] = value
90
+ logger.debug("State.set('%s', %s)", key, value)
85
91
 
86
92
  def increment(self, key: str, amount: float = 1) -> float:
87
93
  """
@@ -98,17 +104,17 @@ class StatePrimitive:
98
104
  State.increment("hypotheses_filed")
99
105
  State.increment("score", 10)
100
106
  """
101
- current = self._state.get(key, 0)
107
+ current_value = self._state_values.get(key, 0)
102
108
 
103
109
  # Ensure numeric
104
- if not isinstance(current, (int, float)):
105
- logger.warning(f"State.increment: '{key}' is not numeric, resetting to 0")
106
- current = 0
110
+ if not isinstance(current_value, (int, float)):
111
+ logger.warning("State.increment: '%s' is not numeric, resetting to 0", key)
112
+ current_value = 0
107
113
 
108
- new_value = current + amount
109
- self._state[key] = new_value
114
+ new_value = current_value + amount
115
+ self._state_values[key] = new_value
110
116
 
111
- logger.debug(f"State.increment('{key}', {amount}) = {new_value}")
117
+ logger.debug("State.increment('%s', %s) = %s", key, amount, new_value)
112
118
  return new_value
113
119
 
114
120
  def append(self, key: str, value: Any) -> None:
@@ -122,16 +128,21 @@ class StatePrimitive:
122
128
  Example (Lua):
123
129
  State.append("nodes_created", node_id)
124
130
  """
125
- if key not in self._state:
126
- self._state[key] = []
127
- elif not isinstance(self._state[key], list):
128
- logger.warning(f"State.append: '{key}' is not a list, converting")
129
- self._state[key] = [self._state[key]]
130
-
131
- self._state[key].append(value)
132
- logger.debug(f"State.append('{key}', {value}) -> list length: {len(self._state[key])}")
133
-
134
- def all(self) -> Dict[str, Any]:
131
+ if key not in self._state_values:
132
+ self._state_values[key] = []
133
+ elif not isinstance(self._state_values[key], list):
134
+ logger.warning("State.append: '%s' is not a list, converting", key)
135
+ self._state_values[key] = [self._state_values[key]]
136
+
137
+ self._state_values[key].append(value)
138
+ logger.debug(
139
+ "State.append('%s', %s) -> list length: %s",
140
+ key,
141
+ value,
142
+ len(self._state_values[key]),
143
+ )
144
+
145
+ def all(self) -> dict[str, Any]:
135
146
  """
136
147
  Get all state as a dictionary.
137
148
 
@@ -144,15 +155,15 @@ class StatePrimitive:
144
155
  print(k, v)
145
156
  end
146
157
  """
147
- logger.debug(f"State.all() returning {len(self._state)} keys")
148
- return self._state.copy()
158
+ logger.debug("State.all() returning %s keys", len(self._state_values))
159
+ return self._state_values.copy()
149
160
 
150
161
  def clear(self) -> None:
151
162
  """Clear all state (mainly for testing)."""
152
- self._state.clear()
163
+ self._state_values.clear()
153
164
  logger.debug("State.clear() - all state cleared")
154
165
 
155
- def _validate_type(self, value: Any, expected_type: str) -> bool:
166
+ def _is_value_matching_schema_type(self, value: Any, expected_type: str) -> bool:
156
167
  """
157
168
  Validate value against expected type from schema.
158
169
 
@@ -173,10 +184,10 @@ class StatePrimitive:
173
184
 
174
185
  expected_python_type = type_mapping.get(expected_type)
175
186
  if expected_python_type is None:
176
- logger.warning(f"Unknown type in schema: {expected_type}")
187
+ logger.warning("Unknown type in schema: %s", expected_type)
177
188
  return True # Allow unknown types
178
189
 
179
190
  return isinstance(value, expected_python_type)
180
191
 
181
192
  def __repr__(self) -> str:
182
- return f"StatePrimitive({len(self._state)} keys)"
193
+ return f"StatePrimitive({len(self._state_values)} keys)"
tactus/primitives/step.py CHANGED
@@ -46,7 +46,7 @@ class StepPrimitive:
46
46
  Returns:
47
47
  Result of fn() on first execution, cached result on replay
48
48
  """
49
- logger.debug(f"checkpoint() at position {self.execution_context.next_position()}")
49
+ logger.debug("checkpoint() at position %s", self.execution_context.next_position())
50
50
 
51
51
  # Prioritize Lua source info over Python stack inspection
52
52
  if lua_source_info:
@@ -54,11 +54,7 @@ class StepPrimitive:
54
54
  try:
55
55
  if hasattr(lua_source_info, "items"):
56
56
  # It's already dict-like
57
- lua_dict = (
58
- dict(lua_source_info.items())
59
- if hasattr(lua_source_info, "items")
60
- else lua_source_info
61
- )
57
+ lua_dict = dict(lua_source_info.items())
62
58
  else:
63
59
  # Try to convert if it's a LuaTable
64
60
  lua_dict = dict(lua_source_info)
@@ -72,14 +68,14 @@ class StepPrimitive:
72
68
  "line": lua_dict.get("line", 0),
73
69
  "function": lua_dict.get("function", "unknown"),
74
70
  }
75
- logger.debug(f"Using Lua source info: {source_info}")
71
+ logger.debug("Using Lua source info: %s", source_info)
76
72
  else:
77
73
  # Fallback to Python stack inspection (for backward compatibility)
78
74
  import inspect
79
75
 
80
- frame = inspect.currentframe()
81
- if frame and frame.f_back:
82
- caller_frame = frame.f_back
76
+ current_frame = inspect.currentframe()
77
+ if current_frame and current_frame.f_back:
78
+ caller_frame = current_frame.f_back
83
79
  source_info = {
84
80
  "file": caller_frame.f_code.co_filename,
85
81
  "line": caller_frame.f_lineno,
@@ -94,8 +90,8 @@ class StepPrimitive:
94
90
  )
95
91
  logger.debug("checkpoint() completed successfully")
96
92
  return result
97
- except Exception as e:
98
- logger.error(f"checkpoint() failed: {e}")
93
+ except Exception as error:
94
+ logger.error("checkpoint() failed: %s", error)
99
95
  raise
100
96
 
101
97
 
@@ -158,7 +154,7 @@ class CheckpointPrimitive:
158
154
  Example:
159
155
  Checkpoint.clear_after(3) -- Clear checkpoint 3 and beyond
160
156
  """
161
- logger.info(f"Clearing checkpoints after position {position}")
157
+ logger.info("Clearing checkpoints after position %s", position)
162
158
  self.execution_context.checkpoint_clear_after(position)
163
159
 
164
160
  def next_position(self) -> int:
@@ -8,7 +8,7 @@ Provides:
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Optional
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -22,19 +22,19 @@ class SystemPrimitive:
22
22
  self.procedure_id = procedure_id
23
23
  self.log_handler = log_handler
24
24
 
25
- def _lua_to_python(self, obj: Any) -> Any:
25
+ def _lua_to_python(self, value: Any) -> Any:
26
26
  """Convert Lua objects to Python equivalents recursively."""
27
- if obj is None:
27
+ if value is None:
28
28
  return None
29
- if hasattr(obj, "items") and not isinstance(obj, dict):
30
- return {k: self._lua_to_python(v) for k, v in obj.items()}
31
- if isinstance(obj, dict):
32
- return {k: self._lua_to_python(v) for k, v in obj.items()}
33
- if isinstance(obj, (list, tuple)):
34
- return [self._lua_to_python(v) for v in obj]
35
- return obj
36
-
37
- def alert(self, options: Optional[Dict[str, Any]] = None) -> None:
29
+ if hasattr(value, "items") and not isinstance(value, dict):
30
+ return {k: self._lua_to_python(v) for k, v in value.items()}
31
+ if isinstance(value, dict):
32
+ return {k: self._lua_to_python(v) for k, v in value.items()}
33
+ if isinstance(value, (list, tuple)):
34
+ return [self._lua_to_python(v) for v in value]
35
+ return value
36
+
37
+ def alert(self, options: Optional[dict[str, Any]] = None) -> None:
38
38
  """
39
39
  Emit a system alert (NON-BLOCKING).
40
40
 
@@ -45,12 +45,12 @@ class SystemPrimitive:
45
45
  - source: str - Where the alert originated (optional)
46
46
  - context: Dict - Additional structured context (optional)
47
47
  """
48
- opts = self._lua_to_python(options) or {}
48
+ options_dict = self._lua_to_python(options) or {}
49
49
 
50
- message = str(opts.get("message", "Alert"))
51
- level = str(opts.get("level", "info")).lower()
52
- source = opts.get("source")
53
- context = opts.get("context") or {}
50
+ message = str(options_dict.get("message", "Alert"))
51
+ level = str(options_dict.get("level", "info")).lower()
52
+ source = options_dict.get("source")
53
+ context = options_dict.get("context") or {}
54
54
 
55
55
  if level not in self._ALLOWED_LEVELS:
56
56
  raise ValueError(
@@ -71,8 +71,8 @@ class SystemPrimitive:
71
71
  )
72
72
  self.log_handler.log(event)
73
73
  return
74
- except Exception as e: # pragma: no cover
75
- logger.warning(f"Failed to emit SystemAlertEvent: {e}")
74
+ except Exception as error: # pragma: no cover
75
+ logger.warning("Failed to emit SystemAlertEvent: %s", error)
76
76
 
77
77
  # Fallback to standard logging
78
78
  python_level = {
@@ -84,9 +84,22 @@ class SystemPrimitive:
84
84
 
85
85
  origin = f" source={source}" if source is not None else ""
86
86
  if context:
87
- logger.log(python_level, f"System.alert [{level}]{origin}: {message} | {context}")
87
+ logger.log(
88
+ python_level,
89
+ "System.alert [%s]%s: %s | %s",
90
+ level,
91
+ origin,
92
+ message,
93
+ context,
94
+ )
88
95
  else:
89
- logger.log(python_level, f"System.alert [{level}]{origin}: {message}")
96
+ logger.log(
97
+ python_level,
98
+ "System.alert [%s]%s: %s",
99
+ level,
100
+ origin,
101
+ message,
102
+ )
90
103
 
91
104
  def __repr__(self) -> str:
92
105
  return f"SystemPrimitive(procedure_id={self.procedure_id})"