tactus 0.33.0__py3-none-any.whl → 0.34.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/__init__.py +18 -1
- tactus/adapters/broker_log.py +127 -34
- tactus/adapters/channels/__init__.py +153 -0
- tactus/adapters/channels/base.py +174 -0
- tactus/adapters/channels/broker.py +179 -0
- tactus/adapters/channels/cli.py +448 -0
- tactus/adapters/channels/host.py +225 -0
- tactus/adapters/channels/ipc.py +297 -0
- tactus/adapters/channels/sse.py +305 -0
- tactus/adapters/cli_hitl.py +223 -1
- tactus/adapters/control_loop.py +879 -0
- tactus/adapters/file_storage.py +35 -2
- tactus/adapters/ide_log.py +7 -1
- tactus/backends/http_backend.py +0 -1
- tactus/broker/client.py +31 -1
- tactus/broker/server.py +416 -92
- tactus/cli/app.py +270 -7
- tactus/cli/control.py +393 -0
- tactus/core/config_manager.py +33 -6
- tactus/core/dsl_stubs.py +102 -18
- tactus/core/execution_context.py +265 -8
- tactus/core/lua_sandbox.py +8 -9
- tactus/core/registry.py +19 -2
- tactus/core/runtime.py +235 -27
- tactus/docker/Dockerfile.pypi +49 -0
- tactus/docs/__init__.py +33 -0
- tactus/docs/extractor.py +326 -0
- tactus/docs/html_renderer.py +72 -0
- tactus/docs/models.py +121 -0
- tactus/docs/templates/base.html +204 -0
- tactus/docs/templates/index.html +58 -0
- tactus/docs/templates/module.html +96 -0
- tactus/dspy/agent.py +382 -22
- tactus/dspy/broker_lm.py +57 -6
- tactus/dspy/config.py +14 -3
- tactus/dspy/history.py +2 -1
- tactus/dspy/module.py +136 -11
- tactus/dspy/signature.py +0 -1
- tactus/ide/server.py +300 -9
- tactus/primitives/human.py +619 -47
- tactus/primitives/system.py +0 -1
- tactus/protocols/__init__.py +25 -0
- tactus/protocols/control.py +427 -0
- tactus/protocols/notification.py +207 -0
- tactus/sandbox/container_runner.py +79 -11
- tactus/sandbox/docker_manager.py +23 -0
- tactus/sandbox/entrypoint.py +26 -0
- tactus/sandbox/protocol.py +3 -0
- tactus/stdlib/README.md +77 -0
- tactus/stdlib/__init__.py +27 -1
- tactus/stdlib/classify/__init__.py +165 -0
- tactus/stdlib/classify/classify.spec.tac +195 -0
- tactus/stdlib/classify/classify.tac +257 -0
- tactus/stdlib/classify/fuzzy.py +282 -0
- tactus/stdlib/classify/llm.py +319 -0
- tactus/stdlib/classify/primitive.py +287 -0
- tactus/stdlib/core/__init__.py +57 -0
- tactus/stdlib/core/base.py +320 -0
- tactus/stdlib/core/confidence.py +211 -0
- tactus/stdlib/core/models.py +161 -0
- tactus/stdlib/core/retry.py +171 -0
- tactus/stdlib/core/validation.py +274 -0
- tactus/stdlib/extract/__init__.py +125 -0
- tactus/stdlib/extract/llm.py +330 -0
- tactus/stdlib/extract/primitive.py +256 -0
- tactus/stdlib/tac/tactus/classify/base.tac +51 -0
- tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
- tactus/stdlib/tac/tactus/classify/index.md +77 -0
- tactus/stdlib/tac/tactus/classify/init.tac +29 -0
- tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
- tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
- tactus/stdlib/tac/tactus/extract/base.tac +138 -0
- tactus/stdlib/tac/tactus/extract/index.md +96 -0
- tactus/stdlib/tac/tactus/extract/init.tac +27 -0
- tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
- tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
- tactus/stdlib/tac/tactus/generate/base.tac +142 -0
- tactus/stdlib/tac/tactus/generate/index.md +195 -0
- tactus/stdlib/tac/tactus/generate/init.tac +28 -0
- tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
- tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
- tactus/testing/behave_integration.py +171 -7
- tactus/testing/context.py +0 -1
- tactus/testing/evaluation_runner.py +0 -1
- tactus/testing/gherkin_parser.py +0 -1
- tactus/testing/mock_hitl.py +0 -1
- tactus/testing/mock_tools.py +0 -1
- tactus/testing/models.py +0 -1
- tactus/testing/steps/builtin.py +0 -1
- tactus/testing/steps/custom.py +81 -22
- tactus/testing/steps/registry.py +0 -1
- tactus/testing/test_runner.py +7 -1
- tactus/validation/semantic_visitor.py +11 -5
- tactus/validation/validator.py +0 -1
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/METADATA +14 -2
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/RECORD +100 -49
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/WHEEL +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/licenses/LICENSE +0 -0
tactus/core/runtime.py
CHANGED
|
@@ -102,7 +102,31 @@ class TactusRuntime:
|
|
|
102
102
|
"""
|
|
103
103
|
self.procedure_id = procedure_id
|
|
104
104
|
self.storage_backend = storage_backend
|
|
105
|
-
|
|
105
|
+
|
|
106
|
+
# Initialize HITL handler - use new ControlLoopHandler by default
|
|
107
|
+
if hitl_handler is None:
|
|
108
|
+
# Auto-configure control loop with channels
|
|
109
|
+
from tactus.adapters.channels import load_default_channels
|
|
110
|
+
from tactus.adapters.control_loop import ControlLoopHandler, ControlLoopHITLAdapter
|
|
111
|
+
|
|
112
|
+
channels = load_default_channels(procedure_id=procedure_id)
|
|
113
|
+
if channels:
|
|
114
|
+
control_handler = ControlLoopHandler(
|
|
115
|
+
channels=channels,
|
|
116
|
+
storage=storage_backend,
|
|
117
|
+
)
|
|
118
|
+
# Wrap in adapter for HITLHandler compatibility
|
|
119
|
+
self.hitl_handler = ControlLoopHITLAdapter(control_handler)
|
|
120
|
+
logger.info(f"Auto-configured ControlLoopHandler with {len(channels)} channel(s)")
|
|
121
|
+
else:
|
|
122
|
+
# No channels available, leave hitl_handler as None
|
|
123
|
+
self.hitl_handler = None
|
|
124
|
+
logger.warning(
|
|
125
|
+
"No control channels available - HITL interactions will use defaults"
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
self.hitl_handler = hitl_handler
|
|
129
|
+
|
|
106
130
|
self.chat_recorder = chat_recorder
|
|
107
131
|
self.mcp_server = mcp_server # Keep for backward compatibility
|
|
108
132
|
self.mcp_servers = mcp_servers or {}
|
|
@@ -272,8 +296,7 @@ class TactusRuntime:
|
|
|
272
296
|
self.lua_sandbox.inject_primitive("_state_primitive", placeholder_state)
|
|
273
297
|
|
|
274
298
|
# Create State object with special methods and lowercase state proxy with metatable
|
|
275
|
-
self.lua_sandbox.lua.execute(
|
|
276
|
-
"""
|
|
299
|
+
self.lua_sandbox.lua.execute("""
|
|
277
300
|
State = {
|
|
278
301
|
increment = function(key, amount)
|
|
279
302
|
return _state_primitive.increment(key, amount or 1)
|
|
@@ -295,8 +318,7 @@ class TactusRuntime:
|
|
|
295
318
|
_state_primitive.set(key, value)
|
|
296
319
|
end
|
|
297
320
|
})
|
|
298
|
-
"""
|
|
299
|
-
)
|
|
321
|
+
""")
|
|
300
322
|
self.lua_sandbox.inject_primitive("Tool", placeholder_tool)
|
|
301
323
|
self.lua_sandbox.inject_primitive("params", placeholder_params)
|
|
302
324
|
placeholder_system = LuaSystemPrimitive(
|
|
@@ -692,6 +714,11 @@ class TactusRuntime:
|
|
|
692
714
|
"error": f"Lua execution error: {e}",
|
|
693
715
|
}
|
|
694
716
|
|
|
717
|
+
except ProcedureWaitingForHuman:
|
|
718
|
+
# Re-raise this exception to trigger exit-and-resume pattern
|
|
719
|
+
# Don't treat this as an error - it's expected behavior
|
|
720
|
+
raise
|
|
721
|
+
|
|
695
722
|
except Exception as e:
|
|
696
723
|
logger.error(f"Unexpected error: {e}", exc_info=True)
|
|
697
724
|
|
|
@@ -931,15 +958,66 @@ class TactusRuntime:
|
|
|
931
958
|
)
|
|
932
959
|
|
|
933
960
|
# 6. Register DSL-defined toolsets from registry (after individual tools are registered)
|
|
961
|
+
# DEBUG: Write to stderr which should show up in logs
|
|
962
|
+
import sys
|
|
963
|
+
|
|
964
|
+
sys.stderr.write("\n\n=== DSL TOOLSET REGISTRATION START ===\n")
|
|
965
|
+
sys.stderr.write(f"Has registry: {hasattr(self, 'registry')}\n")
|
|
966
|
+
if hasattr(self, "registry") and self.registry:
|
|
967
|
+
sys.stderr.write("Registry is not None: True\n")
|
|
968
|
+
sys.stderr.write(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}\n")
|
|
969
|
+
if hasattr(self.registry, "toolsets"):
|
|
970
|
+
sys.stderr.write(f"Registry toolsets: {list(self.registry.toolsets.keys())}\n")
|
|
971
|
+
sys.stderr.write(f"Registry toolsets count: {len(self.registry.toolsets)}\n")
|
|
972
|
+
else:
|
|
973
|
+
sys.stderr.write("Registry is None or doesn't exist\n")
|
|
974
|
+
sys.stderr.flush()
|
|
975
|
+
|
|
976
|
+
logger.info("=== DSL TOOLSET REGISTRATION START ===")
|
|
977
|
+
logger.info(f"Has registry: {hasattr(self, 'registry')}")
|
|
978
|
+
logger.info(
|
|
979
|
+
f"Registry is not None: {self.registry is not None if hasattr(self, 'registry') else False}"
|
|
980
|
+
)
|
|
981
|
+
if hasattr(self, "registry") and self.registry:
|
|
982
|
+
logger.info(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}")
|
|
983
|
+
if hasattr(self.registry, "toolsets"):
|
|
984
|
+
logger.info(f"Registry toolsets: {list(self.registry.toolsets.keys())}")
|
|
985
|
+
logger.info(f"Registry toolsets count: {len(self.registry.toolsets)}")
|
|
986
|
+
|
|
934
987
|
if hasattr(self, "registry") and self.registry and hasattr(self.registry, "toolsets"):
|
|
988
|
+
sys.stderr.write(f"Processing {len(self.registry.toolsets)} DSL toolsets\n")
|
|
989
|
+
sys.stderr.flush()
|
|
990
|
+
logger.info(f"Processing {len(self.registry.toolsets)} DSL toolsets")
|
|
935
991
|
for name, definition in self.registry.toolsets.items():
|
|
992
|
+
sys.stderr.write(
|
|
993
|
+
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}\n"
|
|
994
|
+
)
|
|
995
|
+
sys.stderr.flush()
|
|
996
|
+
logger.info(
|
|
997
|
+
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}"
|
|
998
|
+
)
|
|
936
999
|
try:
|
|
937
1000
|
toolset = await self._create_toolset_from_config(name, definition)
|
|
938
1001
|
if toolset:
|
|
939
1002
|
self.toolset_registry[name] = toolset
|
|
940
|
-
|
|
1003
|
+
sys.stderr.write(f"✓ Registered DSL-defined toolset '{name}'\n")
|
|
1004
|
+
sys.stderr.flush()
|
|
1005
|
+
logger.info(f"✓ Registered DSL-defined toolset '{name}'")
|
|
1006
|
+
else:
|
|
1007
|
+
sys.stderr.write(f"✗ Toolset '{name}' creation returned None\n")
|
|
1008
|
+
sys.stderr.flush()
|
|
1009
|
+
logger.error(f"✗ Toolset '{name}' creation returned None")
|
|
941
1010
|
except Exception as e:
|
|
942
|
-
|
|
1011
|
+
sys.stderr.write(f"✗ Failed to create DSL toolset '{name}': {e}\n")
|
|
1012
|
+
sys.stderr.flush()
|
|
1013
|
+
logger.error(f"✗ Failed to create DSL toolset '{name}': {e}", exc_info=True)
|
|
1014
|
+
else:
|
|
1015
|
+
sys.stderr.write("No DSL toolsets to register (registry.toolsets not available)\n")
|
|
1016
|
+
sys.stderr.flush()
|
|
1017
|
+
logger.warning("No DSL toolsets to register (registry.toolsets not available)")
|
|
1018
|
+
sys.stderr.write("=== DSL TOOLSET REGISTRATION END ===\n")
|
|
1019
|
+
sys.stderr.flush()
|
|
1020
|
+
logger.info("=== DSL TOOLSET REGISTRATION END ===")
|
|
943
1021
|
|
|
944
1022
|
logger.info(
|
|
945
1023
|
f"Toolset registry initialized with {len(self.toolset_registry)} toolset(s): {list(self.toolset_registry.keys())}"
|
|
@@ -1439,23 +1517,38 @@ class TactusRuntime:
|
|
|
1439
1517
|
# DSL toolsets can have:
|
|
1440
1518
|
# - "tools" field with list of tool names or inline tool definitions
|
|
1441
1519
|
# - "use" field to import from a file or other source
|
|
1520
|
+
logger.info(f"[TOOLSET_CREATE] '{name}' has no explicit type, checking for tools/use")
|
|
1442
1521
|
|
|
1443
1522
|
if "tools" in definition:
|
|
1444
1523
|
# Handle tools list (can be tool names or inline definitions)
|
|
1445
1524
|
tools_list = definition["tools"]
|
|
1525
|
+
logger.info(
|
|
1526
|
+
f"[TOOLSET_CREATE] '{name}' has tools field with {len(tools_list) if isinstance(tools_list, list) else '?'} items"
|
|
1527
|
+
)
|
|
1446
1528
|
|
|
1447
1529
|
# Check if we have inline tool definitions (dicts with a Lua handler)
|
|
1448
1530
|
has_inline_tools = False
|
|
1449
1531
|
if isinstance(tools_list, list):
|
|
1450
|
-
for item in tools_list:
|
|
1451
|
-
|
|
1452
|
-
"
|
|
1453
|
-
)
|
|
1454
|
-
|
|
1455
|
-
|
|
1532
|
+
for idx, item in enumerate(tools_list):
|
|
1533
|
+
logger.info(
|
|
1534
|
+
f"[TOOLSET_CREATE] Tool {idx}: type={type(item).__name__}, is_dict={isinstance(item, dict)}"
|
|
1535
|
+
)
|
|
1536
|
+
if isinstance(item, dict):
|
|
1537
|
+
logger.info(f"[TOOLSET_CREATE] Tool {idx} keys: {list(item.keys())}")
|
|
1538
|
+
has_handler = "handler" in item
|
|
1539
|
+
has_callable_1 = 1 in item and callable(item.get(1))
|
|
1540
|
+
logger.info(
|
|
1541
|
+
f"[TOOLSET_CREATE] Tool {idx}: has_handler={has_handler}, has_callable_1={has_callable_1}"
|
|
1542
|
+
)
|
|
1543
|
+
if has_handler or has_callable_1:
|
|
1544
|
+
has_inline_tools = True
|
|
1545
|
+
break
|
|
1546
|
+
|
|
1547
|
+
logger.info(f"[TOOLSET_CREATE] '{name}' has_inline_tools={has_inline_tools}")
|
|
1456
1548
|
|
|
1457
1549
|
if has_inline_tools:
|
|
1458
1550
|
# Create toolset from inline Lua tools
|
|
1551
|
+
logger.info(f"[TOOLSET_CREATE] Creating inline toolset for '{name}'")
|
|
1459
1552
|
try:
|
|
1460
1553
|
from tactus.adapters.lua_tools import LuaToolsAdapter
|
|
1461
1554
|
|
|
@@ -1464,10 +1557,15 @@ class TactusRuntime:
|
|
|
1464
1557
|
)
|
|
1465
1558
|
|
|
1466
1559
|
# Create a toolset from inline tool definitions
|
|
1467
|
-
|
|
1560
|
+
toolset = lua_adapter.create_inline_toolset(name, tools_list)
|
|
1561
|
+
logger.info(
|
|
1562
|
+
f"[TOOLSET_CREATE] ✓ Created inline toolset '{name}': {toolset}"
|
|
1563
|
+
)
|
|
1564
|
+
return toolset
|
|
1468
1565
|
except Exception as e:
|
|
1469
1566
|
logger.error(
|
|
1470
|
-
f"Failed to create inline toolset '{name}': {e}",
|
|
1567
|
+
f"[TOOLSET_CREATE] ✗ Failed to create inline toolset '{name}': {e}",
|
|
1568
|
+
exc_info=True,
|
|
1471
1569
|
)
|
|
1472
1570
|
return None
|
|
1473
1571
|
else:
|
|
@@ -1657,6 +1755,8 @@ class TactusRuntime:
|
|
|
1657
1755
|
Args:
|
|
1658
1756
|
context: Procedure context with pre-loaded data
|
|
1659
1757
|
"""
|
|
1758
|
+
import sys # For debug output
|
|
1759
|
+
|
|
1660
1760
|
logger.info(
|
|
1661
1761
|
f"_setup_agents called. Toolset registry has {len(self.toolset_registry)} toolsets: {list(self.toolset_registry.keys())}"
|
|
1662
1762
|
)
|
|
@@ -1664,8 +1764,15 @@ class TactusRuntime:
|
|
|
1664
1764
|
# Initialize user dependencies first (needed by agents)
|
|
1665
1765
|
await self._initialize_dependencies()
|
|
1666
1766
|
|
|
1667
|
-
# Get agent configurations
|
|
1668
|
-
|
|
1767
|
+
# Get agent configurations from registry (Lua-parsed) if available, otherwise from YAML config
|
|
1768
|
+
if hasattr(self, "registry") and self.registry and hasattr(self.registry, "agents"):
|
|
1769
|
+
agents_config = self.registry.agents
|
|
1770
|
+
logger.info(
|
|
1771
|
+
f"Using {len(agents_config)} agent(s) from registry: {list(agents_config.keys())}"
|
|
1772
|
+
)
|
|
1773
|
+
else:
|
|
1774
|
+
agents_config = self.config.get("agents", {})
|
|
1775
|
+
logger.info(f"Using {len(agents_config)} agent(s) from YAML config")
|
|
1669
1776
|
|
|
1670
1777
|
if not agents_config:
|
|
1671
1778
|
logger.info("No agents defined in configuration - skipping agent setup")
|
|
@@ -1682,7 +1789,18 @@ class TactusRuntime:
|
|
|
1682
1789
|
logger.info(f"Default toolsets configured: {default_toolset_names}")
|
|
1683
1790
|
|
|
1684
1791
|
# Setup each agent
|
|
1685
|
-
for agent_name,
|
|
1792
|
+
for agent_name, agent_config_raw in agents_config.items():
|
|
1793
|
+
# Convert AgentDeclaration to dict if needed
|
|
1794
|
+
if hasattr(agent_config_raw, "model_dump"):
|
|
1795
|
+
# Pydantic v2
|
|
1796
|
+
agent_config = agent_config_raw.model_dump()
|
|
1797
|
+
elif hasattr(agent_config_raw, "dict"):
|
|
1798
|
+
# Pydantic v1
|
|
1799
|
+
agent_config = agent_config_raw.dict()
|
|
1800
|
+
else:
|
|
1801
|
+
# Already a dict
|
|
1802
|
+
agent_config = agent_config_raw
|
|
1803
|
+
|
|
1686
1804
|
# Skip if agent was already created during immediate initialization
|
|
1687
1805
|
if agent_name in self.agents:
|
|
1688
1806
|
logger.debug(
|
|
@@ -1820,8 +1938,19 @@ class TactusRuntime:
|
|
|
1820
1938
|
logger.info(f"Agent '{agent_name}' has NO tools (explicitly empty - passing None)")
|
|
1821
1939
|
else:
|
|
1822
1940
|
# Parse toolset expressions
|
|
1941
|
+
sys.stderr.write(
|
|
1942
|
+
f"\n[AGENT_SETUP] Agent '{agent_name}' raw tools config: {agent_tools_config}\n"
|
|
1943
|
+
)
|
|
1944
|
+
sys.stderr.write(
|
|
1945
|
+
f"[AGENT_SETUP] toolset_registry has: {list(self.toolset_registry.keys())}\n"
|
|
1946
|
+
)
|
|
1947
|
+
sys.stderr.flush()
|
|
1823
1948
|
logger.info(f"Agent '{agent_name}' raw tools config: {agent_tools_config}")
|
|
1824
1949
|
filtered_toolsets = self._parse_toolset_expressions(agent_tools_config)
|
|
1950
|
+
sys.stderr.write(
|
|
1951
|
+
f"[AGENT_SETUP] Agent '{agent_name}' parsed toolsets: {filtered_toolsets}\n"
|
|
1952
|
+
)
|
|
1953
|
+
sys.stderr.flush()
|
|
1825
1954
|
logger.info(f"Agent '{agent_name}' parsed toolsets: {filtered_toolsets}")
|
|
1826
1955
|
|
|
1827
1956
|
# Append inline tools toolset if present
|
|
@@ -1887,6 +2016,9 @@ class TactusRuntime:
|
|
|
1887
2016
|
)
|
|
1888
2017
|
|
|
1889
2018
|
# Create DSPy-based agent
|
|
2019
|
+
tool_choice = agent_config.get("tool_choice")
|
|
2020
|
+
logger.info(f"Agent '{agent_name}' config has tool_choice={tool_choice}")
|
|
2021
|
+
|
|
1890
2022
|
dspy_config = {
|
|
1891
2023
|
"system_prompt": system_prompt_template,
|
|
1892
2024
|
"model": model_name,
|
|
@@ -1912,14 +2044,19 @@ class TactusRuntime:
|
|
|
1912
2044
|
"disable_streaming": agent_config.get("disable_streaming", False),
|
|
1913
2045
|
"initial_message": initial_message,
|
|
1914
2046
|
"log_handler": self.log_handler,
|
|
2047
|
+
"tool_choice": tool_choice, # Pass through tool_choice
|
|
1915
2048
|
}
|
|
2049
|
+
logger.info(
|
|
2050
|
+
f"Agent '{agent_name}' dspy_config has tool_choice={dspy_config.get('tool_choice')}"
|
|
2051
|
+
)
|
|
1916
2052
|
|
|
1917
|
-
# Create DSPy agent with registry
|
|
2053
|
+
# Create DSPy agent with registry, mock_manager, and execution_context
|
|
1918
2054
|
agent_primitive = create_dspy_agent(
|
|
1919
2055
|
agent_name,
|
|
1920
2056
|
dspy_config,
|
|
1921
2057
|
registry=self.registry,
|
|
1922
2058
|
mock_manager=self.mock_manager,
|
|
2059
|
+
execution_context=self.execution_context,
|
|
1923
2060
|
)
|
|
1924
2061
|
|
|
1925
2062
|
# Store additional context for compatibility
|
|
@@ -1929,6 +2066,15 @@ class TactusRuntime:
|
|
|
1929
2066
|
|
|
1930
2067
|
self.agents[agent_name] = agent_primitive
|
|
1931
2068
|
|
|
2069
|
+
# CRITICAL FIX: Update the Lua global to point to the new agent with toolsets
|
|
2070
|
+
# The agent was created during parsing WITHOUT toolsets, now we update it
|
|
2071
|
+
# to the new agent that HAS toolsets
|
|
2072
|
+
self.lua_sandbox.lua.globals()[agent_name] = agent_primitive
|
|
2073
|
+
sys.stderr.write(
|
|
2074
|
+
f"[AGENT_FIX] Updated Lua global '{agent_name}' to new agent with toolsets\n"
|
|
2075
|
+
)
|
|
2076
|
+
sys.stderr.flush()
|
|
2077
|
+
|
|
1932
2078
|
logger.info(f"Agent '{agent_name}' configured successfully with model '{model_name}'")
|
|
1933
2079
|
|
|
1934
2080
|
async def _setup_models(self):
|
|
@@ -2235,8 +2381,7 @@ class TactusRuntime:
|
|
|
2235
2381
|
self.lua_sandbox.inject_primitive("_python_checkpoint", self.step_primitive.checkpoint)
|
|
2236
2382
|
|
|
2237
2383
|
# Create Lua wrapper that captures source location before calling Python
|
|
2238
|
-
self.lua_sandbox.lua.execute(
|
|
2239
|
-
"""
|
|
2384
|
+
self.lua_sandbox.lua.execute("""
|
|
2240
2385
|
function checkpoint(fn)
|
|
2241
2386
|
-- Capture caller's source location (2 levels up: this wrapper -> caller)
|
|
2242
2387
|
local info = debug.getinfo(2, 'Sl')
|
|
@@ -2252,8 +2397,7 @@ class TactusRuntime:
|
|
|
2252
2397
|
return _python_checkpoint(fn, nil)
|
|
2253
2398
|
end
|
|
2254
2399
|
end
|
|
2255
|
-
"""
|
|
2256
|
-
)
|
|
2400
|
+
""")
|
|
2257
2401
|
logger.debug("Checkpoint wrapper injected with Lua source location tracking")
|
|
2258
2402
|
|
|
2259
2403
|
if self.checkpoint_primitive:
|
|
@@ -2367,9 +2511,14 @@ class TactusRuntime:
|
|
|
2367
2511
|
# We can't check FieldDefinition type here as it's lost during storage
|
|
2368
2512
|
input_params[key] = field_def["default"]
|
|
2369
2513
|
# If required and not in context, it will fail validation in ProcedureCallable
|
|
2370
|
-
|
|
2371
2514
|
logger.debug(f"Calling main with input_params: {input_params}")
|
|
2372
2515
|
|
|
2516
|
+
# Set procedure metadata for HITL context display
|
|
2517
|
+
if hasattr(self, "execution_context") and self.execution_context:
|
|
2518
|
+
self.execution_context.set_procedure_metadata(
|
|
2519
|
+
procedure_name=main_proc.get("name", "main"), input_data=input_params
|
|
2520
|
+
)
|
|
2521
|
+
|
|
2373
2522
|
# Execute main procedure
|
|
2374
2523
|
result = main_callable(input_params)
|
|
2375
2524
|
|
|
@@ -2384,6 +2533,9 @@ class TactusRuntime:
|
|
|
2384
2533
|
|
|
2385
2534
|
logger.info("Named 'main' procedure execution completed successfully")
|
|
2386
2535
|
return result
|
|
2536
|
+
except ProcedureWaitingForHuman:
|
|
2537
|
+
# Re-raise without wrapping - this is expected behavior
|
|
2538
|
+
raise
|
|
2387
2539
|
except Exception as e:
|
|
2388
2540
|
logger.error(f"Named 'main' procedure execution failed: {e}")
|
|
2389
2541
|
raise LuaSandboxError(f"Named 'main' procedure execution failed: {e}")
|
|
@@ -2436,6 +2588,11 @@ class TactusRuntime:
|
|
|
2436
2588
|
if re.search(r"(?m)^\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?Procedure\b", source):
|
|
2437
2589
|
return source
|
|
2438
2590
|
|
|
2591
|
+
# If there are named function definitions (function name()), don't transform.
|
|
2592
|
+
# These are procedure definitions that will be explicitly called.
|
|
2593
|
+
if re.search(r"(?m)^\s*(?:local\s+)?function\s+[A-Za-z_][A-Za-z0-9_]*\s*\(", source):
|
|
2594
|
+
return source
|
|
2595
|
+
|
|
2439
2596
|
# Detect script mode by top-level input/output declarations OR a top-level `return`.
|
|
2440
2597
|
# We intentionally treat simple "hello world" scripts as script-mode so agent/tool
|
|
2441
2598
|
# calls don't execute during the parse/declaration phase.
|
|
@@ -2451,6 +2608,7 @@ class TactusRuntime:
|
|
|
2451
2608
|
# Once we enter executable code, everything stays in the body.
|
|
2452
2609
|
in_body = False
|
|
2453
2610
|
brace_depth = 0
|
|
2611
|
+
function_depth = 0 # Track function...end blocks
|
|
2454
2612
|
long_string_eq: str | None = None
|
|
2455
2613
|
|
|
2456
2614
|
decl_start = re.compile(
|
|
@@ -2467,6 +2625,8 @@ class TactusRuntime:
|
|
|
2467
2625
|
r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt"
|
|
2468
2626
|
r")\b"
|
|
2469
2627
|
)
|
|
2628
|
+
# Match function definitions: function name() or local function name()
|
|
2629
|
+
function_def = re.compile(r"^\s*(?:local\s+)?function\s+[A-Za-z_][A-Za-z0-9_]*\s*\(")
|
|
2470
2630
|
|
|
2471
2631
|
long_string_open = re.compile(r"\[(=*)\[")
|
|
2472
2632
|
|
|
@@ -2485,15 +2645,20 @@ class TactusRuntime:
|
|
|
2485
2645
|
long_string_eq = None
|
|
2486
2646
|
continue
|
|
2487
2647
|
|
|
2488
|
-
# If we're inside a declaration block, keep consuming until braces balance.
|
|
2648
|
+
# If we're inside a declaration block, keep consuming until braces/functions balance.
|
|
2489
2649
|
added_to_decl = False
|
|
2490
|
-
if brace_depth > 0:
|
|
2650
|
+
if brace_depth > 0 or function_depth > 0:
|
|
2491
2651
|
decl_lines.append(line)
|
|
2492
2652
|
added_to_decl = True
|
|
2493
2653
|
elif stripped == "" or stripped.startswith("--"):
|
|
2494
2654
|
decl_lines.append(line)
|
|
2495
2655
|
added_to_decl = True
|
|
2496
|
-
elif
|
|
2656
|
+
elif (
|
|
2657
|
+
decl_start.match(line)
|
|
2658
|
+
or assignment_decl.match(line)
|
|
2659
|
+
or require_stmt.match(line)
|
|
2660
|
+
or function_def.match(line)
|
|
2661
|
+
):
|
|
2497
2662
|
decl_lines.append(line)
|
|
2498
2663
|
added_to_decl = True
|
|
2499
2664
|
else:
|
|
@@ -2516,6 +2681,18 @@ class TactusRuntime:
|
|
|
2516
2681
|
if brace_depth < 0:
|
|
2517
2682
|
brace_depth = 0
|
|
2518
2683
|
|
|
2684
|
+
# Track function...end blocks for named function definitions
|
|
2685
|
+
# Count 'function' keywords (both named and anonymous)
|
|
2686
|
+
# Note: This is a simple heuristic that counts keywords in comments/strings too,
|
|
2687
|
+
# but that's acceptable for well-formed DSL code
|
|
2688
|
+
import re as re_module
|
|
2689
|
+
|
|
2690
|
+
function_count = len(re_module.findall(r"\bfunction\b", line))
|
|
2691
|
+
end_count = len(re_module.findall(r"\bend\b", line))
|
|
2692
|
+
function_depth += function_count - end_count
|
|
2693
|
+
if function_depth < 0:
|
|
2694
|
+
function_depth = 0
|
|
2695
|
+
|
|
2519
2696
|
# If there is no executable code, nothing to wrap.
|
|
2520
2697
|
if not any(line.strip() for line in body_lines):
|
|
2521
2698
|
return source
|
|
@@ -2720,6 +2897,37 @@ class TactusRuntime:
|
|
|
2720
2897
|
except LuaSandboxError as e:
|
|
2721
2898
|
raise TactusRuntimeError(f"Failed to parse DSL: {e}")
|
|
2722
2899
|
|
|
2900
|
+
# Auto-register plain function main() if it exists
|
|
2901
|
+
#
|
|
2902
|
+
# Some .tac files use plain Lua syntax: `function main() ... end`
|
|
2903
|
+
# instead of the Procedure DSL syntax: `Procedure { function(input) ... end }`
|
|
2904
|
+
#
|
|
2905
|
+
# For these files, we need to explicitly check lua.globals() after execution
|
|
2906
|
+
# and register any function named "main" as the main procedure.
|
|
2907
|
+
#
|
|
2908
|
+
# This allows both syntax styles to work:
|
|
2909
|
+
# 1. DSL style (self-registering): Procedure { function(input) ... end }
|
|
2910
|
+
# 2. Plain Lua style (auto-registered): function main() ... end
|
|
2911
|
+
#
|
|
2912
|
+
# The script mode transformation (in _maybe_transform_script_mode_source)
|
|
2913
|
+
# is designed to skip files with named function definitions to avoid wrapping
|
|
2914
|
+
# them incorrectly.
|
|
2915
|
+
lua_globals = sandbox.lua.globals()
|
|
2916
|
+
if "main" in lua_globals:
|
|
2917
|
+
main_func = lua_globals["main"]
|
|
2918
|
+
# Check if it's a function and not already registered
|
|
2919
|
+
if callable(main_func) and "main" not in builder.registry.named_procedures:
|
|
2920
|
+
logger.info(
|
|
2921
|
+
"[AUTO_REGISTER] Found plain function main(), auto-registering as main procedure"
|
|
2922
|
+
)
|
|
2923
|
+
builder.register_named_procedure(
|
|
2924
|
+
name="main",
|
|
2925
|
+
lua_function=main_func,
|
|
2926
|
+
input_schema={},
|
|
2927
|
+
output_schema={},
|
|
2928
|
+
state_schema={},
|
|
2929
|
+
)
|
|
2930
|
+
|
|
2723
2931
|
# Validate and return registry
|
|
2724
2932
|
result = builder.validate()
|
|
2725
2933
|
if not result.valid:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Tactus Sandbox Container (PyPI-based)
|
|
2
|
+
#
|
|
3
|
+
# Builds a sandbox image by installing Tactus from PyPI. This is intended for
|
|
4
|
+
# environments where the Tactus source tree is not available (e.g., pip install tactus).
|
|
5
|
+
|
|
6
|
+
FROM python:3.11-slim
|
|
7
|
+
|
|
8
|
+
# Labels for image management
|
|
9
|
+
ARG TACTUS_VERSION=dev
|
|
10
|
+
LABEL tactus.version="${TACTUS_VERSION}"
|
|
11
|
+
LABEL maintainer="Anthus <info@anthus.ai>"
|
|
12
|
+
LABEL description="Tactus sandbox container for secure procedure execution (PyPI)"
|
|
13
|
+
|
|
14
|
+
# Install system dependencies
|
|
15
|
+
# - Node.js for JavaScript/TypeScript MCP servers
|
|
16
|
+
# - git for any git operations
|
|
17
|
+
# - build-essential for native Python packages
|
|
18
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
19
|
+
curl \
|
|
20
|
+
git \
|
|
21
|
+
build-essential \
|
|
22
|
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
23
|
+
&& apt-get install -y --no-install-recommends nodejs \
|
|
24
|
+
&& apt-get clean \
|
|
25
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
26
|
+
|
|
27
|
+
# Create non-root user for security
|
|
28
|
+
RUN useradd -m -s /bin/bash tactus
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
|
|
31
|
+
# Install Tactus from PyPI
|
|
32
|
+
RUN pip install --no-cache-dir "tactus==${TACTUS_VERSION}"
|
|
33
|
+
|
|
34
|
+
# Create workspace and mcp-servers directories
|
|
35
|
+
RUN mkdir -p /workspace /mcp-servers && \
|
|
36
|
+
chown -R tactus:tactus /workspace /mcp-servers
|
|
37
|
+
|
|
38
|
+
# Copy entrypoint script
|
|
39
|
+
COPY tactus/docker/entrypoint.sh /entrypoint.sh
|
|
40
|
+
RUN chmod +x /entrypoint.sh
|
|
41
|
+
|
|
42
|
+
# Switch to non-root user
|
|
43
|
+
USER tactus
|
|
44
|
+
|
|
45
|
+
# Set working directory for procedure execution
|
|
46
|
+
WORKDIR /workspace
|
|
47
|
+
|
|
48
|
+
# Default entrypoint runs the sandbox entrypoint module
|
|
49
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
tactus/docs/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tactus Documentation Generator.
|
|
3
|
+
|
|
4
|
+
Extract documentation from .tac files and generate HTML output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tactus.docs.extractor import DirectoryExtractor
|
|
9
|
+
from tactus.docs.html_renderer import HTMLRenderer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_docs(input_path: Path, output_path: Path):
|
|
13
|
+
"""
|
|
14
|
+
Generate HTML documentation from Tactus .tac files.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
input_path: Path to directory containing .tac files
|
|
18
|
+
output_path: Path where HTML files will be written
|
|
19
|
+
"""
|
|
20
|
+
# Extract documentation
|
|
21
|
+
extractor = DirectoryExtractor(input_path)
|
|
22
|
+
tree = extractor.extract_all()
|
|
23
|
+
|
|
24
|
+
print(f"Found {len(tree.modules)} module(s) to document")
|
|
25
|
+
|
|
26
|
+
# Render HTML
|
|
27
|
+
renderer = HTMLRenderer()
|
|
28
|
+
renderer.render_all(tree, output_path)
|
|
29
|
+
|
|
30
|
+
print(f"✓ Documentation generated at {output_path}/index.html")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["generate_docs", "DirectoryExtractor", "HTMLRenderer"]
|