tactus 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,182 @@
1
+ """
2
+ State Primitive - Mutable state management for procedures.
3
+
4
+ Provides:
5
+ - State.get(key, default) - Get state value
6
+ - State.set(key, value) - Set state value
7
+ - State.increment(key, amount) - Increment numeric value
8
+ - State.append(key, value) - Append to list
9
+ - State.all() - Get all state as table
10
+ """
11
+
12
+ import logging
13
+ from typing import Any, Dict
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StatePrimitive:
19
+ """
20
+ Manages mutable state for procedure execution.
21
+
22
+ State is preserved across agent turns and can be used to track
23
+ progress, accumulate results, and coordinate between agents.
24
+ """
25
+
26
+ def __init__(self, state_schema: Dict[str, Any] = None):
27
+ """
28
+ Initialize state storage.
29
+
30
+ Args:
31
+ state_schema: Optional state schema with field definitions and defaults
32
+ """
33
+ self._state: Dict[str, Any] = {}
34
+ self._schema: Dict[str, Any] = state_schema or {}
35
+
36
+ # 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"]
40
+
41
+ logger.debug(f"StatePrimitive initialized with {len(self._schema)} schema fields")
42
+
43
+ def get(self, key: str, default: Any = None) -> Any:
44
+ """
45
+ Get a value from state.
46
+
47
+ Args:
48
+ key: State key to retrieve
49
+ default: Default value if key not found
50
+
51
+ Returns:
52
+ Stored value or default
53
+
54
+ Example (Lua):
55
+ local count = State.get("hypothesis_count", 0)
56
+ """
57
+ value = self._state.get(key, default)
58
+ logger.debug(f"State.get('{key}') = {value}")
59
+ return value
60
+
61
+ def set(self, key: str, value: Any) -> None:
62
+ """
63
+ Set a value in state.
64
+
65
+ Args:
66
+ key: State key to set
67
+ value: Value to store
68
+
69
+ Example (Lua):
70
+ State.set("current_phase", "exploration")
71
+ """
72
+ # 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):
78
+ logger.warning(
79
+ f"State.set('{key}'): value type {type(value).__name__} "
80
+ f"does not match schema type {expected_type}"
81
+ )
82
+
83
+ self._state[key] = value
84
+ logger.debug(f"State.set('{key}', {value})")
85
+
86
+ def increment(self, key: str, amount: float = 1) -> float:
87
+ """
88
+ Increment a numeric value in state.
89
+
90
+ Args:
91
+ key: State key to increment
92
+ amount: Amount to increment by (default 1)
93
+
94
+ Returns:
95
+ New value after increment
96
+
97
+ Example (Lua):
98
+ State.increment("hypotheses_filed")
99
+ State.increment("score", 10)
100
+ """
101
+ current = self._state.get(key, 0)
102
+
103
+ # 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
107
+
108
+ new_value = current + amount
109
+ self._state[key] = new_value
110
+
111
+ logger.debug(f"State.increment('{key}', {amount}) = {new_value}")
112
+ return new_value
113
+
114
+ def append(self, key: str, value: Any) -> None:
115
+ """
116
+ Append a value to a list in state.
117
+
118
+ Args:
119
+ key: State key (will be created as list if doesn't exist)
120
+ value: Value to append
121
+
122
+ Example (Lua):
123
+ State.append("nodes_created", node_id)
124
+ """
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]:
135
+ """
136
+ Get all state as a dictionary.
137
+
138
+ Returns:
139
+ Complete state dictionary
140
+
141
+ Example (Lua):
142
+ local state = State.all()
143
+ for k, v in pairs(state) do
144
+ print(k, v)
145
+ end
146
+ """
147
+ logger.debug(f"State.all() returning {len(self._state)} keys")
148
+ return self._state.copy()
149
+
150
+ def clear(self) -> None:
151
+ """Clear all state (mainly for testing)."""
152
+ self._state.clear()
153
+ logger.debug("State.clear() - all state cleared")
154
+
155
+ def _validate_type(self, value: Any, expected_type: str) -> bool:
156
+ """
157
+ Validate value against expected type from schema.
158
+
159
+ Args:
160
+ value: Value to validate
161
+ expected_type: Expected type string (string, number, boolean, array, object)
162
+
163
+ Returns:
164
+ True if value matches expected type, False otherwise
165
+ """
166
+ type_mapping = {
167
+ "string": str,
168
+ "number": (int, float),
169
+ "boolean": bool,
170
+ "array": list,
171
+ "object": dict,
172
+ }
173
+
174
+ expected_python_type = type_mapping.get(expected_type)
175
+ if expected_python_type is None:
176
+ logger.warning(f"Unknown type in schema: {expected_type}")
177
+ return True # Allow unknown types
178
+
179
+ return isinstance(value, expected_python_type)
180
+
181
+ def __repr__(self) -> str:
182
+ return f"StatePrimitive({len(self._state)} keys)"
@@ -0,0 +1,209 @@
1
+ """
2
+ Step primitive for checkpointed operations.
3
+
4
+ Provides checkpoint() for creating explicit checkpoints in procedures.
5
+ """
6
+
7
+ from typing import Any, Callable
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class StepPrimitive:
14
+ """
15
+ Step primitive for checkpointing operations.
16
+
17
+ Example usage:
18
+ local metrics = checkpoint(function()
19
+ return some_evaluation_function({
20
+ model_id = input.model_id,
21
+ version = "champion"
22
+ })
23
+ end)
24
+
25
+ On first execution: runs the function and caches result at current position
26
+ On replay: returns cached result from execution log
27
+ """
28
+
29
+ def __init__(self, execution_context):
30
+ """
31
+ Initialize Step primitive.
32
+
33
+ Args:
34
+ execution_context: ExecutionContext instance for checkpoint operations
35
+ """
36
+ self.execution_context = execution_context
37
+
38
+ def checkpoint(self, fn: Callable[[], Any], lua_source_info=None) -> Any:
39
+ """
40
+ Execute function with position-based checkpointing.
41
+
42
+ Args:
43
+ fn: Function to execute (must be deterministic)
44
+ lua_source_info: Optional dict with Lua source location {file, line, function}
45
+
46
+ Returns:
47
+ Result of fn() on first execution, cached result on replay
48
+ """
49
+ logger.debug(f"checkpoint() at position {self.execution_context.next_position()}")
50
+
51
+ # Prioritize Lua source info over Python stack inspection
52
+ if lua_source_info:
53
+ # Convert Lua table to dict if needed (lupa might pass a LuaTable object)
54
+ try:
55
+ if hasattr(lua_source_info, "items"):
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
+ )
62
+ else:
63
+ # Try to convert if it's a LuaTable
64
+ lua_dict = dict(lua_source_info)
65
+ except Exception:
66
+ # Fallback - treat as dict
67
+ lua_dict = lua_source_info if isinstance(lua_source_info, dict) else {}
68
+
69
+ # Use source info from Lua debug.getinfo
70
+ source_info = {
71
+ "file": self.execution_context.current_tac_file or lua_dict.get("file", "unknown"),
72
+ "line": lua_dict.get("line", 0),
73
+ "function": lua_dict.get("function", "unknown"),
74
+ }
75
+ logger.debug(f"Using Lua source info: {source_info}")
76
+ else:
77
+ # Fallback to Python stack inspection (for backward compatibility)
78
+ import inspect
79
+
80
+ frame = inspect.currentframe()
81
+ if frame and frame.f_back:
82
+ caller_frame = frame.f_back
83
+ source_info = {
84
+ "file": caller_frame.f_code.co_filename,
85
+ "line": caller_frame.f_lineno,
86
+ "function": caller_frame.f_code.co_name,
87
+ }
88
+ else:
89
+ source_info = None
90
+
91
+ try:
92
+ result = self.execution_context.checkpoint(
93
+ fn, "explicit_checkpoint", source_info=source_info
94
+ )
95
+ logger.debug("checkpoint() completed successfully")
96
+ return result
97
+ except Exception as e:
98
+ logger.error(f"checkpoint() failed: {e}")
99
+ raise
100
+
101
+
102
+ class CheckpointPrimitive:
103
+ """
104
+ Checkpoint management primitive.
105
+
106
+ Provides checkpoint clearing operations for testing.
107
+ """
108
+
109
+ def __init__(self, execution_context):
110
+ """
111
+ Initialize Checkpoint primitive.
112
+
113
+ Args:
114
+ execution_context: ExecutionContext instance
115
+ """
116
+ self.execution_context = execution_context
117
+
118
+ def _coerce_position(self, position: Any) -> int:
119
+ """
120
+ Coerce a Lua/Python value into a checkpoint position (int).
121
+
122
+ Lua commonly passes numbers as int/float and may pass strings; accept both.
123
+ """
124
+ if isinstance(position, bool):
125
+ raise TypeError("Checkpoint position must be a number (bool is not allowed)")
126
+
127
+ if isinstance(position, int):
128
+ return position
129
+
130
+ if isinstance(position, float) and position.is_integer():
131
+ return int(position)
132
+
133
+ if isinstance(position, str):
134
+ stripped = position.strip()
135
+ if stripped.isdigit() or (stripped.startswith("-") and stripped[1:].isdigit()):
136
+ return int(stripped)
137
+ raise TypeError(f"Checkpoint position must be an integer (got string {position!r})")
138
+
139
+ raise TypeError(f"Checkpoint position must be an integer (got {type(position).__name__})")
140
+
141
+ def clear_all(self) -> None:
142
+ """
143
+ Clear all checkpoints. Restarts procedure from beginning.
144
+
145
+ Example:
146
+ Checkpoint.clear_all()
147
+ """
148
+ logger.info("Clearing all checkpoints")
149
+ self.execution_context.checkpoint_clear_all()
150
+
151
+ def clear_after(self, position: int) -> None:
152
+ """
153
+ Clear checkpoint at position and all subsequent ones.
154
+
155
+ Args:
156
+ position: Checkpoint position to clear from
157
+
158
+ Example:
159
+ Checkpoint.clear_after(3) -- Clear checkpoint 3 and beyond
160
+ """
161
+ logger.info(f"Clearing checkpoints after position {position}")
162
+ self.execution_context.checkpoint_clear_after(position)
163
+
164
+ def next_position(self) -> int:
165
+ """
166
+ Get the next checkpoint position.
167
+
168
+ Returns:
169
+ Next position in execution log
170
+
171
+ Example:
172
+ local pos = Checkpoint.next_position()
173
+ print("Next checkpoint will be at position: " .. pos)
174
+ """
175
+ return self.execution_context.next_position()
176
+
177
+ def exists(self, position: Any) -> bool:
178
+ """
179
+ Check if a checkpoint exists at the given position.
180
+
181
+ Args:
182
+ position: Checkpoint position (0-indexed)
183
+
184
+ Returns:
185
+ True if an entry exists at that position, else False
186
+ """
187
+ coerced = self._coerce_position(position)
188
+ metadata = getattr(self.execution_context, "metadata", None)
189
+ if metadata is None or not hasattr(metadata, "execution_log"):
190
+ raise RuntimeError("ExecutionContext does not expose checkpoint metadata")
191
+ return 0 <= coerced < len(metadata.execution_log)
192
+
193
+ def get(self, position: Any) -> Any:
194
+ """
195
+ Get the cached value from a checkpoint without advancing replay.
196
+
197
+ Args:
198
+ position: Checkpoint position (0-indexed)
199
+
200
+ Returns:
201
+ Cached result at that position, or None (Lua nil) if not present
202
+ """
203
+ coerced = self._coerce_position(position)
204
+ metadata = getattr(self.execution_context, "metadata", None)
205
+ if metadata is None or not hasattr(metadata, "execution_log"):
206
+ raise RuntimeError("ExecutionContext does not expose checkpoint metadata")
207
+ if coerced < 0 or coerced >= len(metadata.execution_log):
208
+ return None
209
+ return metadata.execution_log[coerced].result
@@ -0,0 +1,93 @@
1
+ """
2
+ System Primitive - non-blocking operational alerts.
3
+
4
+ Provides:
5
+ - System.alert(opts) - Emit structured alert event (non-blocking)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Dict, Optional
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SystemPrimitive:
18
+ """System-level primitives that are safe to call from anywhere."""
19
+
20
+ _ALLOWED_LEVELS = {"info", "warning", "error", "critical"}
21
+
22
+ def __init__(self, procedure_id: Optional[str] = None, log_handler: Any = None):
23
+ self.procedure_id = procedure_id
24
+ self.log_handler = log_handler
25
+
26
+ def _lua_to_python(self, obj: Any) -> Any:
27
+ """Convert Lua objects to Python equivalents recursively."""
28
+ if obj is None:
29
+ return None
30
+ if hasattr(obj, "items") and not isinstance(obj, dict):
31
+ return {k: self._lua_to_python(v) for k, v in obj.items()}
32
+ if isinstance(obj, dict):
33
+ return {k: self._lua_to_python(v) for k, v in obj.items()}
34
+ if isinstance(obj, (list, tuple)):
35
+ return [self._lua_to_python(v) for v in obj]
36
+ return obj
37
+
38
+ def alert(self, options: Optional[Dict[str, Any]] = None) -> None:
39
+ """
40
+ Emit a system alert (NON-BLOCKING).
41
+
42
+ Args:
43
+ options: Dict with:
44
+ - message: str - Alert message (required)
45
+ - level: str - info, warning, error, critical (default: info)
46
+ - source: str - Where the alert originated (optional)
47
+ - context: Dict - Additional structured context (optional)
48
+ """
49
+ opts = self._lua_to_python(options) or {}
50
+
51
+ message = str(opts.get("message", "Alert"))
52
+ level = str(opts.get("level", "info")).lower()
53
+ source = opts.get("source")
54
+ context = opts.get("context") or {}
55
+
56
+ if level not in self._ALLOWED_LEVELS:
57
+ raise ValueError(
58
+ f"Invalid alert level '{level}'. Allowed levels: {sorted(self._ALLOWED_LEVELS)}"
59
+ )
60
+
61
+ # Emit structured event if possible (preferred for CLI/IDE)
62
+ if self.log_handler:
63
+ try:
64
+ from tactus.protocols.models import SystemAlertEvent
65
+
66
+ event = SystemAlertEvent(
67
+ level=level,
68
+ message=message,
69
+ source=str(source) if source is not None else None,
70
+ context=context if isinstance(context, dict) else {"context": context},
71
+ procedure_id=self.procedure_id,
72
+ )
73
+ self.log_handler.log(event)
74
+ return
75
+ except Exception as e: # pragma: no cover
76
+ logger.warning(f"Failed to emit SystemAlertEvent: {e}")
77
+
78
+ # Fallback to standard logging
79
+ python_level = {
80
+ "info": logging.INFO,
81
+ "warning": logging.WARNING,
82
+ "error": logging.ERROR,
83
+ "critical": logging.CRITICAL,
84
+ }[level]
85
+
86
+ origin = f" source={source}" if source is not None else ""
87
+ if context:
88
+ logger.log(python_level, f"System.alert [{level}]{origin}: {message} | {context}")
89
+ else:
90
+ logger.log(python_level, f"System.alert [{level}]{origin}: {message}")
91
+
92
+ def __repr__(self) -> str:
93
+ return f"SystemPrimitive(procedure_id={self.procedure_id})"