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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- 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})"
|