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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/retry.py
CHANGED
|
@@ -7,7 +7,7 @@ Provides:
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import time
|
|
10
|
-
from typing import
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
65
67
|
|
|
66
|
-
max_attempts =
|
|
67
|
-
initial_delay =
|
|
68
|
-
max_delay =
|
|
69
|
-
backoff_factor =
|
|
70
|
-
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
|
-
|
|
73
|
-
|
|
74
|
+
attempt_number = 0
|
|
75
|
+
current_delay = initial_delay
|
|
74
76
|
last_error = None
|
|
75
77
|
|
|
76
|
-
logger.info(
|
|
78
|
+
logger.info("Starting retry with_backoff (max_attempts=%s)", max_attempts)
|
|
77
79
|
|
|
78
|
-
while
|
|
79
|
-
|
|
80
|
+
while attempt_number < max_attempts:
|
|
81
|
+
attempt_number += 1
|
|
80
82
|
|
|
81
83
|
try:
|
|
82
|
-
logger.debug(
|
|
83
|
-
result =
|
|
84
|
-
logger.info(
|
|
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
|
|
88
|
-
last_error =
|
|
89
|
-
logger.warning(
|
|
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":
|
|
98
|
+
"attempt": attempt_number,
|
|
97
99
|
"max_attempts": max_attempts,
|
|
98
|
-
"error": str(
|
|
99
|
-
"delay":
|
|
100
|
+
"error": str(error),
|
|
101
|
+
"delay": current_delay,
|
|
100
102
|
}
|
|
101
103
|
)
|
|
102
104
|
except Exception as callback_error:
|
|
103
|
-
logger.error(
|
|
105
|
+
logger.error("Error callback failed: %s", callback_error)
|
|
104
106
|
|
|
105
107
|
# Check if we should retry
|
|
106
|
-
if
|
|
107
|
-
logger.error(
|
|
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(
|
|
112
|
-
time.sleep(
|
|
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
|
-
|
|
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}")
|
tactus/primitives/session.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
80
|
+
if not self._has_session_context():
|
|
55
81
|
return
|
|
56
82
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
87
|
+
message_entry = {"role": message_role, "content": message_content}
|
|
62
88
|
|
|
63
|
-
self.session_manager.add_message(self.agent_name,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
"""
|
tactus/primitives/state.py
CHANGED
|
@@ -10,7 +10,7 @@ Provides:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
-
from typing import Any
|
|
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:
|
|
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.
|
|
34
|
-
self.
|
|
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
|
|
38
|
-
if isinstance(
|
|
39
|
-
self.
|
|
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(
|
|
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
|
-
|
|
58
|
-
logger.debug(
|
|
59
|
-
return
|
|
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.
|
|
74
|
-
|
|
75
|
-
if isinstance(
|
|
76
|
-
expected_type =
|
|
77
|
-
if not self.
|
|
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
|
-
|
|
80
|
-
|
|
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.
|
|
84
|
-
logger.debug(
|
|
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
|
-
|
|
107
|
+
current_value = self._state_values.get(key, 0)
|
|
102
108
|
|
|
103
109
|
# Ensure numeric
|
|
104
|
-
if not isinstance(
|
|
105
|
-
logger.warning(
|
|
106
|
-
|
|
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 =
|
|
109
|
-
self.
|
|
114
|
+
new_value = current_value + amount
|
|
115
|
+
self._state_values[key] = new_value
|
|
110
116
|
|
|
111
|
-
logger.debug(
|
|
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.
|
|
126
|
-
self.
|
|
127
|
-
elif not isinstance(self.
|
|
128
|
-
logger.warning(
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
self.
|
|
132
|
-
logger.debug(
|
|
133
|
-
|
|
134
|
-
|
|
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(
|
|
148
|
-
return self.
|
|
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.
|
|
163
|
+
self._state_values.clear()
|
|
153
164
|
logger.debug("State.clear() - all state cleared")
|
|
154
165
|
|
|
155
|
-
def
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
81
|
-
if
|
|
82
|
-
caller_frame =
|
|
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
|
|
98
|
-
logger.error(
|
|
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(
|
|
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:
|
tactus/primitives/system.py
CHANGED
|
@@ -8,7 +8,7 @@ Provides:
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
-
from typing import Any,
|
|
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,
|
|
25
|
+
def _lua_to_python(self, value: Any) -> Any:
|
|
26
26
|
"""Convert Lua objects to Python equivalents recursively."""
|
|
27
|
-
if
|
|
27
|
+
if value is None:
|
|
28
28
|
return None
|
|
29
|
-
if hasattr(
|
|
30
|
-
return {k: self._lua_to_python(v) for k, v in
|
|
31
|
-
if isinstance(
|
|
32
|
-
return {k: self._lua_to_python(v) for k, v in
|
|
33
|
-
if isinstance(
|
|
34
|
-
return [self._lua_to_python(v) for v in
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
def alert(self, options: Optional[
|
|
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
|
-
|
|
48
|
+
options_dict = self._lua_to_python(options) or {}
|
|
49
49
|
|
|
50
|
-
message = str(
|
|
51
|
-
level = str(
|
|
52
|
-
source =
|
|
53
|
-
context =
|
|
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
|
|
75
|
-
logger.warning(
|
|
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(
|
|
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(
|
|
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})"
|