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.
- 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 +40 -25
- 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.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
tactus/utils/safe_libraries.py
CHANGED
|
@@ -6,12 +6,12 @@ warn when used outside checkpoint boundaries to prevent replay bugs in
|
|
|
6
6
|
Tactus's checkpoint-and-replay execution system.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import warnings
|
|
10
|
-
import time
|
|
11
|
-
import random
|
|
12
9
|
import math
|
|
10
|
+
import random
|
|
11
|
+
import time
|
|
12
|
+
import warnings
|
|
13
13
|
from datetime import datetime
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import Any, Callable, Optional
|
|
15
15
|
from functools import wraps
|
|
16
16
|
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ class NonDeterministicError(Exception):
|
|
|
27
27
|
pass
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def warn_if_unsafe(
|
|
30
|
+
def warn_if_unsafe(operation_label: str, get_context: Callable[[], Optional[Any]]):
|
|
31
31
|
"""
|
|
32
32
|
Decorator that warns when non-deterministic operation used outside checkpoint.
|
|
33
33
|
|
|
@@ -43,31 +43,31 @@ def warn_if_unsafe(operation_name: str, get_context: Callable[[], Optional[Any]]
|
|
|
43
43
|
@wraps(func)
|
|
44
44
|
def wrapper(*args, **kwargs):
|
|
45
45
|
# Get context via callback
|
|
46
|
-
|
|
46
|
+
execution_context = get_context()
|
|
47
47
|
|
|
48
48
|
# If no context, can't enforce (allow silently for REPL/testing)
|
|
49
|
-
if
|
|
49
|
+
if execution_context is None:
|
|
50
50
|
return func(*args, **kwargs)
|
|
51
51
|
|
|
52
52
|
# Check if inside checkpoint
|
|
53
|
-
inside_checkpoint = getattr(
|
|
53
|
+
inside_checkpoint = getattr(execution_context, "_inside_checkpoint", False)
|
|
54
54
|
|
|
55
55
|
if not inside_checkpoint:
|
|
56
56
|
message = (
|
|
57
57
|
f"\n{'=' * 70}\n"
|
|
58
|
-
f"DETERMINISM WARNING: {
|
|
58
|
+
f"DETERMINISM WARNING: {operation_label} called outside checkpoint\n"
|
|
59
59
|
f"{'=' * 70}\n\n"
|
|
60
60
|
f"Non-deterministic operations must be wrapped in checkpoints "
|
|
61
61
|
f"for durability.\n\n"
|
|
62
62
|
f"To fix, wrap your code in a checkpoint:\n\n"
|
|
63
63
|
f" -- Lua example:\n"
|
|
64
64
|
f" local random_value = Step.checkpoint(function()\n"
|
|
65
|
-
f" return {
|
|
65
|
+
f" return {operation_label}\n"
|
|
66
66
|
f" end)\n\n"
|
|
67
67
|
f"Or use checkpoint() directly:\n\n"
|
|
68
68
|
f" local result = checkpoint(function()\n"
|
|
69
69
|
f" -- Your non-deterministic code here\n"
|
|
70
|
-
f" return {
|
|
70
|
+
f" return {operation_label}\n"
|
|
71
71
|
f" end)\n\n"
|
|
72
72
|
f"Why: Tactus uses checkpointing for durable execution. "
|
|
73
73
|
f"Operations outside\n"
|
|
@@ -77,7 +77,7 @@ def warn_if_unsafe(operation_name: str, get_context: Callable[[], Optional[Any]]
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
# Check strict mode
|
|
80
|
-
strict_mode = getattr(
|
|
80
|
+
strict_mode = getattr(execution_context, "strict_determinism", False)
|
|
81
81
|
|
|
82
82
|
if strict_mode:
|
|
83
83
|
raise NonDeterministicError(message)
|
|
@@ -105,7 +105,7 @@ def create_safe_math_library(get_context: Callable, strict_mode: bool = False):
|
|
|
105
105
|
"""
|
|
106
106
|
|
|
107
107
|
@warn_if_unsafe("math.random()", get_context)
|
|
108
|
-
def safe_random(
|
|
108
|
+
def safe_random(minimum_inclusive=None, maximum_inclusive=None):
|
|
109
109
|
"""
|
|
110
110
|
Safe math.random() with checkpoint warning.
|
|
111
111
|
|
|
@@ -114,20 +114,20 @@ def create_safe_math_library(get_context: Callable, strict_mode: bool = False):
|
|
|
114
114
|
- math.random(n): returns integer in [1, n]
|
|
115
115
|
- math.random(m, n): returns integer in [m, n]
|
|
116
116
|
"""
|
|
117
|
-
if
|
|
117
|
+
if minimum_inclusive is None and maximum_inclusive is None:
|
|
118
118
|
# No arguments: return float [0, 1)
|
|
119
119
|
return random.random()
|
|
120
|
-
elif
|
|
120
|
+
elif maximum_inclusive is None:
|
|
121
121
|
# One argument: return integer [1, m]
|
|
122
|
-
return random.randint(1, int(
|
|
122
|
+
return random.randint(1, int(minimum_inclusive))
|
|
123
123
|
else:
|
|
124
124
|
# Two arguments: return integer [m, n]
|
|
125
|
-
return random.randint(int(
|
|
125
|
+
return random.randint(int(minimum_inclusive), int(maximum_inclusive))
|
|
126
126
|
|
|
127
127
|
@warn_if_unsafe("math.randomseed()", get_context)
|
|
128
|
-
def safe_randomseed(
|
|
128
|
+
def safe_randomseed(seed_value):
|
|
129
129
|
"""Safe math.randomseed() with checkpoint warning."""
|
|
130
|
-
random.seed(int(
|
|
130
|
+
random.seed(int(seed_value))
|
|
131
131
|
return None
|
|
132
132
|
|
|
133
133
|
# Standard math functions (deterministic - pass through)
|
|
@@ -178,9 +178,9 @@ def create_safe_os_library(get_context: Callable, strict_mode: bool = False):
|
|
|
178
178
|
"""
|
|
179
179
|
|
|
180
180
|
@warn_if_unsafe("os.time()", get_context)
|
|
181
|
-
def safe_time(
|
|
181
|
+
def safe_time(lua_date_table=None):
|
|
182
182
|
"""Safe os.time() with checkpoint warning."""
|
|
183
|
-
if
|
|
183
|
+
if lua_date_table is None:
|
|
184
184
|
return int(time.time())
|
|
185
185
|
else:
|
|
186
186
|
# Lua date table format: {year, month, day, hour, min, sec}
|
|
@@ -189,20 +189,20 @@ def create_safe_os_library(get_context: Callable, strict_mode: bool = False):
|
|
|
189
189
|
return int(time.time())
|
|
190
190
|
|
|
191
191
|
@warn_if_unsafe("os.date()", get_context)
|
|
192
|
-
def safe_date(
|
|
192
|
+
def safe_date(format_string=None):
|
|
193
193
|
"""Safe os.date() with checkpoint warning."""
|
|
194
194
|
now = datetime.utcnow()
|
|
195
195
|
|
|
196
|
-
if
|
|
196
|
+
if format_string is None:
|
|
197
197
|
# Default format like Lua's os.date()
|
|
198
198
|
return now.strftime("%a %b %d %H:%M:%S %Y")
|
|
199
|
-
elif
|
|
199
|
+
elif format_string == "%Y-%m-%dT%H:%M:%SZ":
|
|
200
200
|
# ISO 8601 format
|
|
201
201
|
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
202
202
|
else:
|
|
203
203
|
# Support Python strftime formats
|
|
204
204
|
try:
|
|
205
|
-
return now.strftime(
|
|
205
|
+
return now.strftime(format_string)
|
|
206
206
|
except Exception:
|
|
207
207
|
return now.strftime("%a %b %d %H:%M:%S %Y")
|
|
208
208
|
|
|
@@ -212,11 +212,11 @@ def create_safe_os_library(get_context: Callable, strict_mode: bool = False):
|
|
|
212
212
|
return time.process_time()
|
|
213
213
|
|
|
214
214
|
@warn_if_unsafe("os.getenv()", get_context)
|
|
215
|
-
def safe_getenv(
|
|
215
|
+
def safe_getenv(variable_name):
|
|
216
216
|
"""Safe os.getenv() with checkpoint warning - environment variables can change."""
|
|
217
217
|
import os
|
|
218
218
|
|
|
219
|
-
return os.getenv(
|
|
219
|
+
return os.getenv(variable_name)
|
|
220
220
|
|
|
221
221
|
@warn_if_unsafe("os.tmpname()", get_context)
|
|
222
222
|
def safe_tmpname():
|
|
@@ -9,13 +9,26 @@ from tactus.core.registry import ValidationMessage
|
|
|
9
9
|
class TactusErrorListener(ErrorListener):
|
|
10
10
|
"""Collects syntax errors from ANTLR parser."""
|
|
11
11
|
|
|
12
|
-
def __init__(self):
|
|
13
|
-
self.
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.syntax_errors: list[ValidationMessage] = []
|
|
14
|
+
# Backward-compatible alias used by formatter/tests.
|
|
15
|
+
self.errors = self.syntax_errors
|
|
14
16
|
|
|
15
|
-
def syntaxError(
|
|
17
|
+
def syntaxError(
|
|
18
|
+
self,
|
|
19
|
+
parser,
|
|
20
|
+
offending_token,
|
|
21
|
+
line_number: int,
|
|
22
|
+
column_number: int,
|
|
23
|
+
antlr_error_message: str,
|
|
24
|
+
antlr_exception,
|
|
25
|
+
) -> None:
|
|
16
26
|
"""Called when parser encounters a syntax error."""
|
|
17
|
-
|
|
27
|
+
del parser, offending_token, antlr_exception
|
|
28
|
+
self.syntax_errors.append(
|
|
18
29
|
ValidationMessage(
|
|
19
|
-
level="error",
|
|
30
|
+
level="error",
|
|
31
|
+
message=f"Syntax error: {antlr_error_message}",
|
|
32
|
+
location=(line_number, column_number),
|
|
20
33
|
)
|
|
21
34
|
)
|