tactus 0.31.2__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.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,121 @@
1
+ """
2
+ tactus.io.hdf5 - HDF5 file operations for Tactus.
3
+
4
+ Provides HDF5 dataset read/write operations with sandboxing
5
+ to the procedure's base directory.
6
+
7
+ Requires: h5py
8
+
9
+ Usage in .tac files:
10
+ local hdf5 = require("tactus.io.hdf5")
11
+
12
+ -- Read dataset from HDF5 file
13
+ local data = hdf5.read("data.h5", "measurements/temperature")
14
+
15
+ -- Write dataset to HDF5 file
16
+ hdf5.write("output.h5", "results/scores", {95, 87, 92, 88})
17
+
18
+ -- List all datasets in file
19
+ local datasets = hdf5.list("data.h5")
20
+ """
21
+
22
+ import os
23
+ import sys
24
+ from typing import Any, List
25
+
26
+ try:
27
+ import h5py
28
+ import numpy as np
29
+ except ImportError:
30
+ raise ImportError("h5py is required for HDF5 support. Install with: pip install h5py")
31
+
32
+ # Get context (injected by loader)
33
+ _ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
34
+
35
+
36
+ def read(filepath: str, dataset: str) -> List[Any]:
37
+ """
38
+ Read dataset from HDF5 file.
39
+
40
+ Args:
41
+ filepath: Path to HDF5 file (relative to working directory)
42
+ dataset: Dataset path within the HDF5 file (e.g., "group/dataset")
43
+
44
+ Returns:
45
+ Dataset contents as a list
46
+
47
+ Raises:
48
+ FileNotFoundError: If file does not exist
49
+ KeyError: If dataset does not exist in file
50
+ PermissionError: If path is outside working directory
51
+ """
52
+ if _ctx:
53
+ filepath = _ctx.validate_path(filepath)
54
+
55
+ with h5py.File(filepath, "r") as f:
56
+ return f[dataset][:].tolist()
57
+
58
+
59
+ def write(filepath: str, dataset: str, data: List[Any]) -> None:
60
+ """
61
+ Write data to HDF5 dataset.
62
+
63
+ If the dataset already exists, it will be replaced.
64
+ Parent groups will be created automatically.
65
+
66
+ Args:
67
+ filepath: Path to HDF5 file
68
+ dataset: Dataset path within the HDF5 file (e.g., "group/dataset")
69
+ data: Data to write (list or nested lists for multi-dimensional arrays)
70
+
71
+ Raises:
72
+ PermissionError: If path is outside working directory
73
+ ValueError: If data is invalid
74
+ """
75
+ if _ctx:
76
+ filepath = _ctx.validate_path(filepath)
77
+
78
+ # Create parent directories
79
+ os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
80
+
81
+ with h5py.File(filepath, "a") as f:
82
+ # Delete existing dataset if present
83
+ if dataset in f:
84
+ del f[dataset]
85
+
86
+ # Create dataset
87
+ f.create_dataset(dataset, data=np.array(data))
88
+
89
+
90
+ def list(filepath: str) -> List[str]:
91
+ """
92
+ List all datasets in HDF5 file.
93
+
94
+ Args:
95
+ filepath: Path to HDF5 file
96
+
97
+ Returns:
98
+ List of dataset paths
99
+
100
+ Raises:
101
+ FileNotFoundError: If file does not exist
102
+ PermissionError: If path is outside working directory
103
+ """
104
+ if _ctx:
105
+ filepath = _ctx.validate_path(filepath)
106
+
107
+ datasets = []
108
+
109
+ with h5py.File(filepath, "r") as f:
110
+
111
+ def visitor(name, obj):
112
+ if isinstance(obj, h5py.Dataset):
113
+ datasets.append(name)
114
+
115
+ f.visititems(visitor)
116
+
117
+ return datasets
118
+
119
+
120
+ # Explicit exports
121
+ __tactus_exports__ = ["read", "write", "list"]
@@ -0,0 +1,109 @@
1
+ """
2
+ tactus.io.json - JSON file operations for Tactus.
3
+
4
+ Provides JSON read/write operations with automatic path validation
5
+ and sandboxing to the procedure's base directory.
6
+
7
+ Usage in .tac files:
8
+ local json = require("tactus.io.json")
9
+
10
+ -- Read JSON file
11
+ local data = json.read("config.json")
12
+
13
+ -- Write JSON file
14
+ json.write("output.json", {name = "Alice", score = 95})
15
+
16
+ -- Encode/decode strings
17
+ local str = json.encode({key = "value"})
18
+ local obj = json.decode('{"key": "value"}')
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ from typing import Any, Optional
25
+
26
+ # Get context (injected by loader)
27
+ _ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
28
+
29
+
30
+ def read(filepath: str) -> Any:
31
+ """
32
+ Read and parse a JSON file.
33
+
34
+ Args:
35
+ filepath: Path to JSON file (relative to working directory)
36
+
37
+ Returns:
38
+ Parsed JSON data (dict, list, or primitive)
39
+
40
+ Raises:
41
+ FileNotFoundError: If file does not exist
42
+ json.JSONDecodeError: If file is not valid JSON
43
+ PermissionError: If path is outside working directory
44
+ """
45
+ if _ctx:
46
+ filepath = _ctx.validate_path(filepath)
47
+
48
+ with open(filepath, "r", encoding="utf-8") as f:
49
+ return json.load(f)
50
+
51
+
52
+ def write(filepath: str, data: Any, indent: int = 2) -> None:
53
+ """
54
+ Write data to a JSON file.
55
+
56
+ Args:
57
+ filepath: Path to JSON file
58
+ data: Data to write (dict, list, or primitive)
59
+ indent: Indentation level (default 2)
60
+
61
+ Raises:
62
+ PermissionError: If path is outside working directory
63
+ TypeError: If data is not JSON serializable
64
+ """
65
+ if _ctx:
66
+ filepath = _ctx.validate_path(filepath)
67
+
68
+ # Create parent directories
69
+ os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
70
+
71
+ with open(filepath, "w", encoding="utf-8") as f:
72
+ json.dump(data, f, indent=indent, ensure_ascii=False)
73
+
74
+
75
+ def encode(data: Any, indent: Optional[int] = None) -> str:
76
+ """
77
+ Encode data to JSON string.
78
+
79
+ Args:
80
+ data: Data to encode
81
+ indent: Optional indentation
82
+
83
+ Returns:
84
+ JSON string
85
+
86
+ Raises:
87
+ TypeError: If data is not JSON serializable
88
+ """
89
+ return json.dumps(data, indent=indent, ensure_ascii=False)
90
+
91
+
92
+ def decode(json_string: str) -> Any:
93
+ """
94
+ Decode JSON string to data.
95
+
96
+ Args:
97
+ json_string: JSON string to parse
98
+
99
+ Returns:
100
+ Parsed data
101
+
102
+ Raises:
103
+ json.JSONDecodeError: If string is not valid JSON
104
+ """
105
+ return json.loads(json_string)
106
+
107
+
108
+ # Explicit exports (only these functions exposed to Lua)
109
+ __tactus_exports__ = ["read", "write", "encode", "decode"]
@@ -0,0 +1,83 @@
1
+ """
2
+ tactus.io.parquet - Apache Parquet file operations for Tactus.
3
+
4
+ Provides Parquet read/write operations with sandboxing
5
+ to the procedure's base directory.
6
+
7
+ Requires: pyarrow
8
+
9
+ Usage in .tac files:
10
+ local parquet = require("tactus.io.parquet")
11
+
12
+ -- Read Parquet file
13
+ local data = parquet.read("data.parquet")
14
+
15
+ -- Write Parquet file
16
+ parquet.write("output.parquet", {
17
+ {name = "Alice", score = 95},
18
+ {name = "Bob", score = 87}
19
+ })
20
+ """
21
+
22
+ import os
23
+ import sys
24
+ from typing import Any, Dict, List
25
+
26
+ try:
27
+ import pyarrow as pa
28
+ import pyarrow.parquet as pq
29
+ except ImportError:
30
+ raise ImportError("pyarrow is required for Parquet support. Install with: pip install pyarrow")
31
+
32
+ # Get context (injected by loader)
33
+ _ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
34
+
35
+
36
+ def read(filepath: str) -> List[Dict[str, Any]]:
37
+ """
38
+ Read Parquet file, returning list of dictionaries.
39
+
40
+ Args:
41
+ filepath: Path to Parquet file (relative to working directory)
42
+
43
+ Returns:
44
+ List of dictionaries, one per row
45
+
46
+ Raises:
47
+ FileNotFoundError: If file does not exist
48
+ PermissionError: If path is outside working directory
49
+ """
50
+ if _ctx:
51
+ filepath = _ctx.validate_path(filepath)
52
+
53
+ table = pq.read_table(filepath)
54
+ return table.to_pylist()
55
+
56
+
57
+ def write(filepath: str, data: List[Dict[str, Any]]) -> None:
58
+ """
59
+ Write list of dictionaries to Parquet file.
60
+
61
+ Args:
62
+ filepath: Path to Parquet file
63
+ data: List of dictionaries to write
64
+
65
+ Raises:
66
+ PermissionError: If path is outside working directory
67
+ ValueError: If data is empty or invalid
68
+ """
69
+ if _ctx:
70
+ filepath = _ctx.validate_path(filepath)
71
+
72
+ if not data:
73
+ raise ValueError("Cannot write empty data to Parquet")
74
+
75
+ # Create parent directories
76
+ os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
77
+
78
+ table = pa.Table.from_pylist(data)
79
+ pq.write_table(table, filepath)
80
+
81
+
82
+ # Explicit exports
83
+ __tactus_exports__ = ["read", "write"]
@@ -0,0 +1,88 @@
1
+ """
2
+ tactus.io.tsv - TSV (tab-separated values) file operations for Tactus.
3
+
4
+ Provides TSV read/write operations with automatic header handling
5
+ and sandboxing to the procedure's base directory.
6
+
7
+ Usage in .tac files:
8
+ local tsv = require("tactus.io.tsv")
9
+
10
+ -- Read TSV file
11
+ local data = tsv.read("data.tsv")
12
+
13
+ -- Write TSV file
14
+ tsv.write("output.tsv", {
15
+ {name = "Alice", age = 30},
16
+ {name = "Bob", age = 25}
17
+ })
18
+ """
19
+
20
+ import csv
21
+ import os
22
+ import sys
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ # Get context (injected by loader)
26
+ _ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
27
+
28
+
29
+ def read(filepath: str) -> List[Dict[str, Any]]:
30
+ """
31
+ Read TSV file, returning list of dictionaries with headers as keys.
32
+
33
+ Args:
34
+ filepath: Path to TSV file (relative to working directory)
35
+
36
+ Returns:
37
+ List of dictionaries, one per row
38
+
39
+ Raises:
40
+ FileNotFoundError: If file does not exist
41
+ PermissionError: If path is outside working directory
42
+ """
43
+ if _ctx:
44
+ filepath = _ctx.validate_path(filepath)
45
+
46
+ with open(filepath, "r", encoding="utf-8", newline="") as f:
47
+ reader = csv.DictReader(f, delimiter="\t")
48
+ return list(reader)
49
+
50
+
51
+ def write(
52
+ filepath: str, data: List[Dict[str, Any]], options: Optional[Dict[str, Any]] = None
53
+ ) -> None:
54
+ """
55
+ Write list of dictionaries to TSV file.
56
+
57
+ Args:
58
+ filepath: Path to TSV file
59
+ data: List of dictionaries to write
60
+ options: Optional dict with 'headers' key for custom header order
61
+
62
+ Raises:
63
+ PermissionError: If path is outside working directory
64
+ ValueError: If data is empty or cannot determine headers
65
+ """
66
+ if _ctx:
67
+ filepath = _ctx.validate_path(filepath)
68
+
69
+ if not data:
70
+ raise ValueError("Cannot write empty data to TSV")
71
+
72
+ # Determine headers
73
+ options = options or {}
74
+ headers = options.get("headers")
75
+ if not headers:
76
+ headers = list(data[0].keys())
77
+
78
+ # Create parent directories
79
+ os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
80
+
81
+ with open(filepath, "w", encoding="utf-8", newline="") as f:
82
+ writer = csv.DictWriter(f, fieldnames=headers, delimiter="\t")
83
+ writer.writeheader()
84
+ writer.writerows(data)
85
+
86
+
87
+ # Explicit exports
88
+ __tactus_exports__ = ["read", "write"]
@@ -0,0 +1,274 @@
1
+ """
2
+ Python stdlib module loader for Tactus.
3
+
4
+ Provides a mechanism to load Python modules from tactus/stdlib/
5
+ via Lua's require() function. Only modules with the "tactus." prefix
6
+ can be loaded, ensuring user code cannot import arbitrary Python modules.
7
+ """
8
+
9
+ import importlib.util
10
+ import inspect
11
+ import logging
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class StdlibModuleLoader:
20
+ """
21
+ Loads Python modules from tactus/stdlib/ for Lua require().
22
+
23
+ Security: Only loads from the stdlib path, never user directories.
24
+ """
25
+
26
+ def __init__(self, lua_sandbox, base_path: str):
27
+ """
28
+ Initialize the stdlib loader.
29
+
30
+ Args:
31
+ lua_sandbox: LuaSandbox instance for table creation
32
+ base_path: Base path for file operations (passed to modules)
33
+ """
34
+ self.lua_sandbox = lua_sandbox
35
+ self.base_path = base_path
36
+ self.stdlib_path = self._get_stdlib_path()
37
+ self.loaded_modules: Dict[str, Any] = {}
38
+
39
+ def _get_stdlib_path(self) -> Path:
40
+ """Get the stdlib directory path."""
41
+ import tactus
42
+
43
+ package_root = Path(tactus.__file__).parent
44
+ return package_root / "stdlib"
45
+
46
+ def create_loader_function(self) -> Callable:
47
+ """
48
+ Create the loader function to inject into Lua's package.loaders.
49
+
50
+ Returns:
51
+ Python function that Lua can call as a module loader
52
+ """
53
+
54
+ def python_stdlib_loader(module_name: str):
55
+ """
56
+ Lua module loader for Python stdlib modules.
57
+
58
+ Args:
59
+ module_name: Module path like "tactus.io.json"
60
+
61
+ Returns:
62
+ Lua table with module functions, or None if not found
63
+ """
64
+ # Only handle tactus.* modules (stdlib)
65
+ if not module_name.startswith("tactus."):
66
+ return None
67
+
68
+ # Convert module path to file path
69
+ # "tactus.io.json" -> "io/json.py"
70
+ relative_path = module_name[7:].replace(".", "/") # Remove "tactus." prefix
71
+ python_path = self.stdlib_path / f"{relative_path}.py"
72
+
73
+ # Security: Verify path is within stdlib
74
+ try:
75
+ python_path = python_path.resolve()
76
+ python_path.relative_to(self.stdlib_path.resolve())
77
+ except ValueError:
78
+ logger.warning(f"Path traversal attempt blocked: {module_name}")
79
+ return None
80
+
81
+ # Check if Python module exists
82
+ if not python_path.exists():
83
+ return None
84
+
85
+ # Load and wrap the Python module
86
+ try:
87
+ return self._load_python_module(module_name, python_path)
88
+ except Exception as e:
89
+ logger.error(f"Failed to load stdlib module {module_name}: {e}")
90
+ # Re-raise as a string for Lua error handling
91
+ raise RuntimeError(f"Failed to load module '{module_name}': {e}")
92
+
93
+ return python_stdlib_loader
94
+
95
+ def _load_python_module(self, module_name: str, path: Path) -> Any:
96
+ """
97
+ Load a Python module and convert to Lua table.
98
+
99
+ Args:
100
+ module_name: Full module name (e.g., "tactus.io.json")
101
+ path: Path to Python file
102
+
103
+ Returns:
104
+ Lua table with module exports
105
+ """
106
+ # Check cache
107
+ if module_name in self.loaded_modules:
108
+ return self.loaded_modules[module_name]
109
+
110
+ # Create unique module name for Python's import system
111
+ internal_name = f"tactus_stdlib_{module_name.replace('.', '_')}"
112
+
113
+ # Load module dynamically
114
+ spec = importlib.util.spec_from_file_location(internal_name, path)
115
+ if spec is None or spec.loader is None:
116
+ raise ImportError(f"Could not load spec for {path}")
117
+
118
+ module = importlib.util.module_from_spec(spec)
119
+ sys.modules[internal_name] = module
120
+
121
+ # Inject context before executing module
122
+ module.__tactus_context__ = TactusStdlibContext(
123
+ base_path=self.base_path, lua_sandbox=self.lua_sandbox
124
+ )
125
+
126
+ spec.loader.exec_module(module)
127
+
128
+ # Get exports
129
+ exports = self._get_module_exports(module)
130
+
131
+ # Convert to Lua table
132
+ lua_table = self._create_lua_module(exports)
133
+
134
+ # Cache
135
+ self.loaded_modules[module_name] = lua_table
136
+
137
+ logger.debug(f"Loaded stdlib Python module: {module_name}")
138
+ return lua_table
139
+
140
+ def _get_module_exports(self, module) -> Dict[str, Callable]:
141
+ """
142
+ Get exportable functions from a module.
143
+
144
+ Args:
145
+ module: Loaded Python module
146
+
147
+ Returns:
148
+ Dict of function name -> function
149
+ """
150
+ exports = {}
151
+
152
+ # Check for explicit exports list
153
+ explicit_exports = getattr(module, "__tactus_exports__", None)
154
+
155
+ for name, obj in inspect.getmembers(module):
156
+ # Skip private
157
+ if name.startswith("_"):
158
+ continue
159
+
160
+ # Skip non-functions
161
+ if not callable(obj):
162
+ continue
163
+
164
+ # Skip if not in explicit exports (when defined)
165
+ if explicit_exports is not None and name not in explicit_exports:
166
+ continue
167
+
168
+ # Skip imports from other modules
169
+ if hasattr(obj, "__module__") and obj.__module__ != module.__name__:
170
+ continue
171
+
172
+ exports[name] = obj
173
+
174
+ return exports
175
+
176
+ def _create_lua_module(self, exports: Dict[str, Callable]) -> Any:
177
+ """
178
+ Create a Lua table from Python exports.
179
+
180
+ Args:
181
+ exports: Dict of function name -> function
182
+
183
+ Returns:
184
+ Lua table
185
+ """
186
+ lua_table = self.lua_sandbox.lua.table()
187
+
188
+ for name, func in exports.items():
189
+ # Wrap function to handle type conversion
190
+ wrapped = self._wrap_function(func, name)
191
+ lua_table[name] = wrapped
192
+
193
+ return lua_table
194
+
195
+ def _wrap_function(self, func: Callable, name: str) -> Callable:
196
+ """
197
+ Wrap a Python function for Lua interop.
198
+
199
+ Handles:
200
+ - Lua table -> Python dict conversion
201
+ - Python dict -> Lua table conversion
202
+ - Exception propagation
203
+ """
204
+
205
+ def wrapper(*args):
206
+ try:
207
+ # Convert Lua tables to Python dicts
208
+ python_args = [self._lua_to_python(arg) for arg in args]
209
+
210
+ # Call function
211
+ result = func(*python_args)
212
+
213
+ # Convert result to Lua
214
+ return self._python_to_lua(result)
215
+
216
+ except Exception as e:
217
+ logger.error(f"Error in stdlib function {name}: {e}")
218
+ raise
219
+
220
+ return wrapper
221
+
222
+ def _lua_to_python(self, value: Any) -> Any:
223
+ """Convert Lua value to Python."""
224
+ # Use existing conversion from safe_file_library
225
+ from tactus.utils.safe_file_library import _lua_to_python
226
+
227
+ return _lua_to_python(value)
228
+
229
+ def _python_to_lua(self, value: Any) -> Any:
230
+ """Convert Python value to Lua table recursively."""
231
+ if value is None:
232
+ return None
233
+ if isinstance(value, (str, int, float, bool)):
234
+ return value
235
+ if isinstance(value, dict):
236
+ # Convert dict to Lua table recursively
237
+ lua_table = self.lua_sandbox.lua.table()
238
+ for k, v in value.items():
239
+ lua_table[k] = self._python_to_lua(v)
240
+ return lua_table
241
+ if isinstance(value, (list, tuple)):
242
+ # Convert to Lua array (1-indexed)
243
+ lua_table = self.lua_sandbox.lua.table()
244
+ for i, item in enumerate(value, start=1):
245
+ lua_table[i] = self._python_to_lua(item)
246
+ return lua_table
247
+ # Fallback
248
+ return value
249
+
250
+
251
+ class TactusStdlibContext:
252
+ """
253
+ Context object passed to stdlib Python modules.
254
+
255
+ Provides access to sandbox resources in a controlled way.
256
+ """
257
+
258
+ def __init__(self, base_path: str, lua_sandbox):
259
+ self.base_path = base_path
260
+ self._lua_sandbox = lua_sandbox
261
+ self._path_validator = None
262
+
263
+ @property
264
+ def path_validator(self):
265
+ """Lazy-create path validator."""
266
+ if self._path_validator is None:
267
+ from tactus.utils.safe_file_library import PathValidator
268
+
269
+ self._path_validator = PathValidator(self.base_path)
270
+ return self._path_validator
271
+
272
+ def validate_path(self, filepath: str) -> str:
273
+ """Validate and resolve a file path."""
274
+ return self.path_validator.validate(filepath)
@@ -0,0 +1,33 @@
1
+ --[[
2
+ tactus.tools.done: Signal task completion
3
+
4
+ Usage:
5
+ local done = require("tactus.tools.done")
6
+
7
+ -- In an agent's toolset
8
+ agent = Agent {
9
+ tools = {"done"},
10
+ ...
11
+ }
12
+
13
+ -- Check if done was called
14
+ if done.called() then
15
+ local result = done.last_call()
16
+ end
17
+ ]]--
18
+
19
+ -- Provide explicit name so the tool is recorded/mocked as "done"
20
+ return Tool {
21
+ name = "done",
22
+ description = "Signal task completion",
23
+ input = {
24
+ reason = field.string{required = false, description = "Reason for completion"}
25
+ },
26
+ function(args)
27
+ return {
28
+ status = "completed",
29
+ reason = args.reason or "Task completed",
30
+ tool = "done"
31
+ }
32
+ end
33
+ }