tactus 0.36.0__py3-none-any.whl → 0.38.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tactus/__init__.py +1 -1
- tactus/adapters/channels/base.py +22 -2
- tactus/adapters/channels/broker.py +1 -0
- tactus/adapters/channels/host.py +3 -1
- tactus/adapters/channels/ipc.py +18 -3
- tactus/adapters/channels/sse.py +2 -0
- tactus/adapters/mcp_manager.py +24 -7
- tactus/backends/http_backend.py +2 -2
- tactus/backends/pytorch_backend.py +2 -2
- tactus/broker/client.py +3 -3
- tactus/broker/server.py +17 -5
- tactus/cli/app.py +212 -57
- tactus/core/compaction.py +17 -0
- tactus/core/context_assembler.py +73 -0
- tactus/core/context_models.py +41 -0
- tactus/core/dsl_stubs.py +560 -20
- tactus/core/exceptions.py +8 -0
- tactus/core/execution_context.py +24 -24
- tactus/core/message_history_manager.py +2 -2
- tactus/core/mocking.py +12 -0
- tactus/core/output_validator.py +6 -6
- tactus/core/registry.py +171 -29
- tactus/core/retrieval.py +317 -0
- tactus/core/retriever_tasks.py +30 -0
- tactus/core/runtime.py +431 -117
- tactus/dspy/agent.py +143 -82
- tactus/dspy/broker_lm.py +13 -7
- tactus/dspy/config.py +23 -4
- tactus/dspy/module.py +12 -1
- tactus/ide/coding_assistant.py +2 -2
- tactus/primitives/handles.py +79 -7
- tactus/primitives/model.py +1 -1
- tactus/primitives/procedure.py +1 -1
- tactus/primitives/state.py +2 -2
- tactus/sandbox/config.py +1 -1
- tactus/sandbox/container_runner.py +13 -6
- tactus/sandbox/entrypoint.py +51 -8
- tactus/sandbox/protocol.py +5 -0
- tactus/stdlib/README.md +10 -1
- tactus/stdlib/biblicus/__init__.py +3 -0
- tactus/stdlib/biblicus/text.py +189 -0
- tactus/stdlib/tac/biblicus/text.tac +32 -0
- tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
- tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
- tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
- tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
- tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
- tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
- tactus/testing/behave_integration.py +2 -0
- tactus/testing/context.py +10 -6
- tactus/testing/evaluation_runner.py +5 -5
- tactus/testing/steps/builtin.py +2 -2
- tactus/testing/test_runner.py +6 -4
- tactus/utils/asyncio_helpers.py +2 -1
- tactus/validation/semantic_visitor.py +357 -6
- tactus/validation/validator.py +142 -2
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/METADATA +9 -6
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/RECORD +65 -47
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/runtime.py
CHANGED
|
@@ -13,9 +13,10 @@ import io
|
|
|
13
13
|
import logging
|
|
14
14
|
import time
|
|
15
15
|
import uuid
|
|
16
|
-
from
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
17
18
|
|
|
18
|
-
from tactus.core.registry import ProcedureRegistry, RegistryBuilder
|
|
19
|
+
from tactus.core.registry import ProcedureRegistry, RegistryBuilder, TaskDeclaration
|
|
19
20
|
from tactus.core.dsl_stubs import create_dsl_stubs, lua_table_to_dict
|
|
20
21
|
from tactus.core.template_resolver import TemplateResolver
|
|
21
22
|
from tactus.core.message_history_manager import MessageHistoryManager
|
|
@@ -68,19 +69,19 @@ class TactusRuntime:
|
|
|
68
69
|
def __init__(
|
|
69
70
|
self,
|
|
70
71
|
procedure_id: str,
|
|
71
|
-
storage_backend: StorageBackend
|
|
72
|
-
hitl_handler: HITLHandler
|
|
73
|
-
chat_recorder: ChatRecorder
|
|
72
|
+
storage_backend: Optional[StorageBackend] = None,
|
|
73
|
+
hitl_handler: Optional[HITLHandler] = None,
|
|
74
|
+
chat_recorder: Optional[ChatRecorder] = None,
|
|
74
75
|
mcp_server=None,
|
|
75
|
-
mcp_servers:
|
|
76
|
-
openai_api_key: str
|
|
76
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
77
|
+
openai_api_key: Optional[str] = None,
|
|
77
78
|
log_handler=None,
|
|
78
|
-
tool_primitive: ToolPrimitive
|
|
79
|
+
tool_primitive: Optional[ToolPrimitive] = None,
|
|
79
80
|
recursion_depth: int = 0,
|
|
80
|
-
tool_paths:
|
|
81
|
-
external_config:
|
|
82
|
-
run_id: str
|
|
83
|
-
source_file_path: str
|
|
81
|
+
tool_paths: Optional[List[str]] = None,
|
|
82
|
+
external_config: Optional[Dict[str, Any]] = None,
|
|
83
|
+
run_id: Optional[str] = None,
|
|
84
|
+
source_file_path: Optional[str] = None,
|
|
84
85
|
):
|
|
85
86
|
"""
|
|
86
87
|
Initialize the Tactus runtime.
|
|
@@ -137,6 +138,7 @@ class TactusRuntime:
|
|
|
137
138
|
self.openai_api_key = openai_api_key
|
|
138
139
|
self.log_handler = log_handler
|
|
139
140
|
self._injected_tool_primitive = tool_primitive
|
|
141
|
+
self.task_name: Optional[str] = None
|
|
140
142
|
self.tool_paths = tool_paths or []
|
|
141
143
|
self.recursion_depth = recursion_depth
|
|
142
144
|
self.external_config = external_config or {}
|
|
@@ -144,31 +146,31 @@ class TactusRuntime:
|
|
|
144
146
|
self.source_file_path = source_file_path
|
|
145
147
|
|
|
146
148
|
# Will be initialized during setup
|
|
147
|
-
self.config:
|
|
148
|
-
self.registry: ProcedureRegistry
|
|
149
|
-
self.lua_sandbox: LuaSandbox
|
|
150
|
-
self.output_validator: OutputValidator
|
|
151
|
-
self.template_resolver: TemplateResolver
|
|
152
|
-
self.message_history_manager: MessageHistoryManager
|
|
149
|
+
self.config: Optional[Dict[str, Any]] = None # Legacy YAML support
|
|
150
|
+
self.registry: Optional[ProcedureRegistry] = None # New DSL registry
|
|
151
|
+
self.lua_sandbox: Optional[LuaSandbox] = None
|
|
152
|
+
self.output_validator: Optional[OutputValidator] = None
|
|
153
|
+
self.template_resolver: Optional[TemplateResolver] = None
|
|
154
|
+
self.message_history_manager: Optional[MessageHistoryManager] = None
|
|
153
155
|
|
|
154
156
|
# Execution context
|
|
155
|
-
self.execution_context: BaseExecutionContext
|
|
157
|
+
self.execution_context: Optional[BaseExecutionContext] = None
|
|
156
158
|
|
|
157
159
|
# Primitives (shared across all agents)
|
|
158
|
-
self.state_primitive: StatePrimitive
|
|
159
|
-
self.iterations_primitive: IterationsPrimitive
|
|
160
|
-
self.stop_primitive: StopPrimitive
|
|
161
|
-
self.tool_primitive: ToolPrimitive
|
|
162
|
-
self.human_primitive: HumanPrimitive
|
|
163
|
-
self.step_primitive: StepPrimitive
|
|
164
|
-
self.checkpoint_primitive: CheckpointPrimitive
|
|
165
|
-
self.log_primitive: LogPrimitive
|
|
166
|
-
self.json_primitive: JsonPrimitive
|
|
167
|
-
self.retry_primitive: RetryPrimitive
|
|
168
|
-
self.file_primitive: FilePrimitive
|
|
169
|
-
self.procedure_primitive: ProcedurePrimitive
|
|
170
|
-
self.system_primitive: SystemPrimitive
|
|
171
|
-
self.host_primitive: HostPrimitive
|
|
160
|
+
self.state_primitive: Optional[StatePrimitive] = None
|
|
161
|
+
self.iterations_primitive: Optional[IterationsPrimitive] = None
|
|
162
|
+
self.stop_primitive: Optional[StopPrimitive] = None
|
|
163
|
+
self.tool_primitive: Optional[ToolPrimitive] = None
|
|
164
|
+
self.human_primitive: Optional[HumanPrimitive] = None
|
|
165
|
+
self.step_primitive: Optional[StepPrimitive] = None
|
|
166
|
+
self.checkpoint_primitive: Optional[CheckpointPrimitive] = None
|
|
167
|
+
self.log_primitive: Optional[LogPrimitive] = None
|
|
168
|
+
self.json_primitive: Optional[JsonPrimitive] = None
|
|
169
|
+
self.retry_primitive: Optional[RetryPrimitive] = None
|
|
170
|
+
self.file_primitive: Optional[FilePrimitive] = None
|
|
171
|
+
self.procedure_primitive: Optional[ProcedurePrimitive] = None
|
|
172
|
+
self.system_primitive: Optional[SystemPrimitive] = None
|
|
173
|
+
self.host_primitive: Optional[HostPrimitive] = None
|
|
172
174
|
|
|
173
175
|
# Agent primitives (one per agent)
|
|
174
176
|
self.agents: dict[str, Any] = {}
|
|
@@ -181,17 +183,21 @@ class TactusRuntime:
|
|
|
181
183
|
|
|
182
184
|
# User dependencies (HTTP clients, DB connections, etc.)
|
|
183
185
|
self.user_dependencies: dict[str, Any] = {}
|
|
184
|
-
self.dependency_manager: Any
|
|
186
|
+
self.dependency_manager: Optional[Any] = None # ResourceManager for cleanup
|
|
185
187
|
|
|
186
188
|
# Mock manager for testing
|
|
187
|
-
self.mock_manager: Any
|
|
188
|
-
self.external_agent_mocks:
|
|
189
|
+
self.mock_manager: Optional[Any] = None # MockManager instance
|
|
190
|
+
self.external_agent_mocks: Optional[Dict[str, List[Dict[str, Any]]]] = None
|
|
189
191
|
self.mock_all_agents: bool = False
|
|
190
192
|
|
|
191
193
|
logger.info("TactusRuntime initialized for procedure %s", procedure_id)
|
|
192
194
|
|
|
193
195
|
async def execute(
|
|
194
|
-
self,
|
|
196
|
+
self,
|
|
197
|
+
source: str,
|
|
198
|
+
context: Optional[Dict[str, Any]] = None,
|
|
199
|
+
format: str = "yaml",
|
|
200
|
+
task_name: Optional[str] = None,
|
|
195
201
|
) -> dict[str, Any]:
|
|
196
202
|
"""
|
|
197
203
|
Execute a workflow (Lua DSL or legacy YAML format).
|
|
@@ -215,6 +221,7 @@ class TactusRuntime:
|
|
|
215
221
|
"""
|
|
216
222
|
chat_session_id = None
|
|
217
223
|
self.context = context or {} # Store context for param merging
|
|
224
|
+
self.task_name = task_name
|
|
218
225
|
|
|
219
226
|
try:
|
|
220
227
|
# 0. Setup Lua sandbox FIRST (needed for both YAML and Lua DSL)
|
|
@@ -353,10 +360,31 @@ class TactusRuntime:
|
|
|
353
360
|
raise TactusRuntimeError(
|
|
354
361
|
f"External agent mocks for '{agent_name}' must be a list of turns"
|
|
355
362
|
)
|
|
356
|
-
|
|
363
|
+
target_name = agent_name
|
|
364
|
+
if (
|
|
365
|
+
agent_name not in self.registry.agents
|
|
366
|
+
and len(self.registry.agents) == 1
|
|
367
|
+
):
|
|
368
|
+
target_name = next(iter(self.registry.agents))
|
|
369
|
+
self.registry.agent_mocks[target_name] = AgentMockConfig(
|
|
357
370
|
temporal=temporal_turns
|
|
358
371
|
)
|
|
359
372
|
|
|
373
|
+
# If agent mocks exist but use a non-matching name and there is only one agent,
|
|
374
|
+
# remap the mock to the sole agent name.
|
|
375
|
+
if self.registry and self.registry.agent_mocks and len(self.registry.agents) == 1:
|
|
376
|
+
sole_agent = next(iter(self.registry.agents))
|
|
377
|
+
if sole_agent not in self.registry.agent_mocks:
|
|
378
|
+
unmatched = [
|
|
379
|
+
name
|
|
380
|
+
for name in self.registry.agent_mocks
|
|
381
|
+
if name not in self.registry.agents
|
|
382
|
+
]
|
|
383
|
+
if len(unmatched) == 1:
|
|
384
|
+
self.registry.agent_mocks[sole_agent] = self.registry.agent_mocks.pop(
|
|
385
|
+
unmatched[0]
|
|
386
|
+
)
|
|
387
|
+
|
|
360
388
|
# If we're in mocked mode, ensure agents are mocked deterministically even if
|
|
361
389
|
# the .tac file doesn't declare `Mocks { ... }` for them.
|
|
362
390
|
if self.mock_all_agents and self.registry:
|
|
@@ -772,7 +800,7 @@ class TactusRuntime:
|
|
|
772
800
|
except Exception as e:
|
|
773
801
|
logger.warning("Error cleaning up dependencies: %s", e)
|
|
774
802
|
|
|
775
|
-
def _resolve_sandbox_base_path(self) -> str
|
|
803
|
+
def _resolve_sandbox_base_path(self) -> Optional[str]:
|
|
776
804
|
# Compute base_path for sandbox from source file path if available.
|
|
777
805
|
# This ensures require() works correctly even when running from different directories.
|
|
778
806
|
if not self.source_file_path:
|
|
@@ -798,7 +826,7 @@ class TactusRuntime:
|
|
|
798
826
|
|
|
799
827
|
async def _initialize_primitives(
|
|
800
828
|
self,
|
|
801
|
-
placeholder_tool: ToolPrimitive
|
|
829
|
+
placeholder_tool: Optional[ToolPrimitive] = None,
|
|
802
830
|
):
|
|
803
831
|
"""Initialize all primitive objects.
|
|
804
832
|
|
|
@@ -836,7 +864,7 @@ class TactusRuntime:
|
|
|
836
864
|
|
|
837
865
|
logger.debug("All primitives initialized")
|
|
838
866
|
|
|
839
|
-
def resolve_toolset(self, name: str) -> Any
|
|
867
|
+
def resolve_toolset(self, name: str) -> Optional[Any]:
|
|
840
868
|
"""
|
|
841
869
|
Resolve a toolset by name from runtime's registered toolsets.
|
|
842
870
|
|
|
@@ -978,66 +1006,29 @@ class TactusRuntime:
|
|
|
978
1006
|
)
|
|
979
1007
|
|
|
980
1008
|
# 6. Register DSL-defined toolsets from registry (after individual tools are registered)
|
|
981
|
-
# DEBUG: Write to stderr which should show up in logs
|
|
982
|
-
import sys
|
|
983
|
-
|
|
984
|
-
sys.stderr.write("\n\n=== DSL TOOLSET REGISTRATION START ===\n")
|
|
985
|
-
sys.stderr.write(f"Has registry: {hasattr(self, 'registry')}\n")
|
|
986
|
-
if hasattr(self, "registry") and self.registry:
|
|
987
|
-
sys.stderr.write("Registry is not None: True\n")
|
|
988
|
-
sys.stderr.write(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}\n")
|
|
989
|
-
if hasattr(self.registry, "toolsets"):
|
|
990
|
-
sys.stderr.write(f"Registry toolsets: {list(self.registry.toolsets.keys())}\n")
|
|
991
|
-
sys.stderr.write(f"Registry toolsets count: {len(self.registry.toolsets)}\n")
|
|
992
|
-
else:
|
|
993
|
-
sys.stderr.write("Registry is None or doesn't exist\n")
|
|
994
|
-
sys.stderr.flush()
|
|
995
|
-
|
|
996
|
-
logger.info("=== DSL TOOLSET REGISTRATION START ===")
|
|
997
|
-
logger.info(f"Has registry: {hasattr(self, 'registry')}")
|
|
998
|
-
logger.info(
|
|
999
|
-
f"Registry is not None: {self.registry is not None if hasattr(self, 'registry') else False}"
|
|
1000
|
-
)
|
|
1001
|
-
if hasattr(self, "registry") and self.registry:
|
|
1002
|
-
logger.info(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}")
|
|
1003
|
-
if hasattr(self.registry, "toolsets"):
|
|
1004
|
-
logger.info(f"Registry toolsets: {list(self.registry.toolsets.keys())}")
|
|
1005
|
-
logger.info(f"Registry toolsets count: {len(self.registry.toolsets)}")
|
|
1006
|
-
|
|
1007
1009
|
if hasattr(self, "registry") and self.registry and hasattr(self.registry, "toolsets"):
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1010
|
+
logger.debug(
|
|
1011
|
+
"Registering %s DSL toolset(s): %s",
|
|
1012
|
+
len(self.registry.toolsets),
|
|
1013
|
+
list(self.registry.toolsets.keys()),
|
|
1014
|
+
)
|
|
1011
1015
|
for name, definition in self.registry.toolsets.items():
|
|
1012
|
-
sys.stderr.write(
|
|
1013
|
-
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}\n"
|
|
1014
|
-
)
|
|
1015
|
-
sys.stderr.flush()
|
|
1016
|
-
logger.info(
|
|
1017
|
-
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}"
|
|
1018
|
-
)
|
|
1019
1016
|
try:
|
|
1017
|
+
logger.debug(
|
|
1018
|
+
"Creating DSL toolset %r with config keys: %s",
|
|
1019
|
+
name,
|
|
1020
|
+
list(definition.keys()),
|
|
1021
|
+
)
|
|
1020
1022
|
toolset = await self._create_toolset_from_config(name, definition)
|
|
1021
1023
|
if toolset:
|
|
1022
1024
|
self.toolset_registry[name] = toolset
|
|
1023
|
-
sys.stderr.write(f"✓ Registered DSL-defined toolset '{name}'\n")
|
|
1024
|
-
sys.stderr.flush()
|
|
1025
|
-
logger.info(f"✓ Registered DSL-defined toolset '{name}'")
|
|
1026
1025
|
else:
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
sys.stderr.write(f"✗ Failed to create DSL toolset '{name}': {e}\n")
|
|
1032
|
-
sys.stderr.flush()
|
|
1033
|
-
logger.error(f"✗ Failed to create DSL toolset '{name}': {e}", exc_info=True)
|
|
1026
|
+
logger.error("DSL toolset %r creation returned None", name)
|
|
1027
|
+
except Exception as exc:
|
|
1028
|
+
# Toolset creation failures should not crash the whole runtime initialization.
|
|
1029
|
+
logger.error("Failed to create DSL toolset %r: %s", name, exc, exc_info=True)
|
|
1034
1030
|
else:
|
|
1035
|
-
|
|
1036
|
-
sys.stderr.flush()
|
|
1037
|
-
logger.warning("No DSL toolsets to register (registry.toolsets not available)")
|
|
1038
|
-
sys.stderr.write("=== DSL TOOLSET REGISTRATION END ===\n")
|
|
1039
|
-
sys.stderr.flush()
|
|
1040
|
-
logger.info("=== DSL TOOLSET REGISTRATION END ===")
|
|
1031
|
+
logger.debug("No DSL toolsets to register")
|
|
1041
1032
|
|
|
1042
1033
|
logger.info(
|
|
1043
1034
|
f"Toolset registry initialized with {len(self.toolset_registry)} toolset(s): {list(self.toolset_registry.keys())}"
|
|
@@ -1047,7 +1038,7 @@ class TactusRuntime:
|
|
|
1047
1038
|
for name, toolset in self.toolset_registry.items():
|
|
1048
1039
|
logger.debug(f" - {name}: {type(toolset)} -> {toolset}")
|
|
1049
1040
|
|
|
1050
|
-
async def _resolve_tool_source(self, tool_name: str, source: str) -> Any
|
|
1041
|
+
async def _resolve_tool_source(self, tool_name: str, source: str) -> Optional[Any]:
|
|
1051
1042
|
"""
|
|
1052
1043
|
Resolve a tool from an external source.
|
|
1053
1044
|
|
|
@@ -1442,7 +1433,7 @@ class TactusRuntime:
|
|
|
1442
1433
|
|
|
1443
1434
|
async def _create_toolset_from_config(
|
|
1444
1435
|
self, name: str, definition: dict[str, Any]
|
|
1445
|
-
) -> Any
|
|
1436
|
+
) -> Optional[Any]:
|
|
1446
1437
|
"""
|
|
1447
1438
|
Create toolset from YAML config definition.
|
|
1448
1439
|
|
|
@@ -1797,8 +1788,6 @@ class TactusRuntime:
|
|
|
1797
1788
|
Args:
|
|
1798
1789
|
context: Procedure context with pre-loaded data
|
|
1799
1790
|
"""
|
|
1800
|
-
import sys # For debug output
|
|
1801
|
-
|
|
1802
1791
|
logger.info(
|
|
1803
1792
|
f"_setup_agents called. Toolset registry has {len(self.toolset_registry)} toolsets: {list(self.toolset_registry.keys())}"
|
|
1804
1793
|
)
|
|
@@ -1980,20 +1969,14 @@ class TactusRuntime:
|
|
|
1980
1969
|
logger.info(f"Agent '{agent_name}' has NO tools (explicitly empty - passing None)")
|
|
1981
1970
|
else:
|
|
1982
1971
|
# Parse toolset expressions
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1972
|
+
logger.debug(
|
|
1973
|
+
"Agent %r raw tools config: %s (available toolsets: %s)",
|
|
1974
|
+
agent_name,
|
|
1975
|
+
agent_tools_config,
|
|
1976
|
+
list(self.toolset_registry.keys()),
|
|
1988
1977
|
)
|
|
1989
|
-
sys.stderr.flush()
|
|
1990
|
-
logger.info(f"Agent '{agent_name}' raw tools config: {agent_tools_config}")
|
|
1991
1978
|
filtered_toolsets = self._parse_toolset_expressions(agent_tools_config)
|
|
1992
|
-
|
|
1993
|
-
f"[AGENT_SETUP] Agent '{agent_name}' parsed toolsets: {filtered_toolsets}\n"
|
|
1994
|
-
)
|
|
1995
|
-
sys.stderr.flush()
|
|
1996
|
-
logger.info(f"Agent '{agent_name}' parsed toolsets: {filtered_toolsets}")
|
|
1979
|
+
logger.debug("Agent %r parsed toolsets: %s", agent_name, filtered_toolsets)
|
|
1997
1980
|
|
|
1998
1981
|
# Append inline tools toolset if present
|
|
1999
1982
|
if inline_tools_toolset:
|
|
@@ -2065,6 +2048,7 @@ class TactusRuntime:
|
|
|
2065
2048
|
"system_prompt": system_prompt_template,
|
|
2066
2049
|
"model": model_name,
|
|
2067
2050
|
"provider": agent_config.get("provider"),
|
|
2051
|
+
"context": agent_config.get("context"),
|
|
2068
2052
|
"tools": filtered_tools,
|
|
2069
2053
|
"toolsets": filtered_toolsets,
|
|
2070
2054
|
"output_schema": output_schema,
|
|
@@ -2112,10 +2096,7 @@ class TactusRuntime:
|
|
|
2112
2096
|
# The agent was created during parsing WITHOUT toolsets, now we update it
|
|
2113
2097
|
# to the new agent that HAS toolsets
|
|
2114
2098
|
self.lua_sandbox.lua.globals()[agent_name] = agent_primitive
|
|
2115
|
-
|
|
2116
|
-
f"[AGENT_FIX] Updated Lua global '{agent_name}' to new agent with toolsets\n"
|
|
2117
|
-
)
|
|
2118
|
-
sys.stderr.flush()
|
|
2099
|
+
logger.debug("Updated Lua global %r to new agent with toolsets", agent_name)
|
|
2119
2100
|
|
|
2120
2101
|
logger.info(f"Agent '{agent_name}' configured successfully with model '{model_name}'")
|
|
2121
2102
|
|
|
@@ -2194,7 +2175,7 @@ class TactusRuntime:
|
|
|
2194
2175
|
if is_required:
|
|
2195
2176
|
fields[field_name] = (field_type, ...) # Required field
|
|
2196
2177
|
else:
|
|
2197
|
-
fields[field_name] = (field_type
|
|
2178
|
+
fields[field_name] = (Optional[field_type], None) # Optional field
|
|
2198
2179
|
|
|
2199
2180
|
return create_model(model_name, **fields)
|
|
2200
2181
|
|
|
@@ -2516,6 +2497,57 @@ class TactusRuntime:
|
|
|
2516
2497
|
Returns:
|
|
2517
2498
|
Result from Lua procedure execution
|
|
2518
2499
|
"""
|
|
2500
|
+
if not self.task_name and self.registry:
|
|
2501
|
+
explicit_tasks = getattr(self.registry, "tasks", {}) or {}
|
|
2502
|
+
|
|
2503
|
+
def _flatten_tasks(task_map: dict, prefix: str = "") -> list[str]:
|
|
2504
|
+
names: list[str] = []
|
|
2505
|
+
for task_name, task in task_map.items():
|
|
2506
|
+
full_name = f"{prefix}{task_name}" if not prefix else f"{prefix}:{task_name}"
|
|
2507
|
+
names.append(full_name)
|
|
2508
|
+
if task.children:
|
|
2509
|
+
names.extend(_flatten_tasks(task.children, full_name))
|
|
2510
|
+
return names
|
|
2511
|
+
|
|
2512
|
+
implicit_tasks: list[str] = []
|
|
2513
|
+
retrievers = getattr(self.registry, "retrievers", {}) or {}
|
|
2514
|
+
if retrievers:
|
|
2515
|
+
from tactus.core.retriever_tasks import (
|
|
2516
|
+
resolve_retriever_id,
|
|
2517
|
+
supported_retriever_tasks,
|
|
2518
|
+
)
|
|
2519
|
+
|
|
2520
|
+
for retriever_name, retriever in retrievers.items():
|
|
2521
|
+
config = getattr(retriever, "config", {})
|
|
2522
|
+
retriever_id = resolve_retriever_id(config if isinstance(config, dict) else {})
|
|
2523
|
+
for task in sorted(supported_retriever_tasks(retriever_id)):
|
|
2524
|
+
if task in explicit_tasks:
|
|
2525
|
+
continue
|
|
2526
|
+
if task not in implicit_tasks:
|
|
2527
|
+
implicit_tasks.append(task)
|
|
2528
|
+
implicit_tasks.append(f"{task}:{retriever_name}")
|
|
2529
|
+
|
|
2530
|
+
if explicit_tasks:
|
|
2531
|
+
if len(explicit_tasks) == 1:
|
|
2532
|
+
self.task_name = next(iter(explicit_tasks.keys()))
|
|
2533
|
+
elif "run" in explicit_tasks:
|
|
2534
|
+
self.task_name = "run"
|
|
2535
|
+
else:
|
|
2536
|
+
from tactus.core.exceptions import TaskSelectionRequired
|
|
2537
|
+
|
|
2538
|
+
raise TaskSelectionRequired(_flatten_tasks(explicit_tasks) + implicit_tasks)
|
|
2539
|
+
elif (
|
|
2540
|
+
implicit_tasks
|
|
2541
|
+
and not getattr(self, "_top_level_result", None)
|
|
2542
|
+
and not getattr(self.registry, "named_procedures", None)
|
|
2543
|
+
):
|
|
2544
|
+
from tactus.core.exceptions import TaskSelectionRequired
|
|
2545
|
+
|
|
2546
|
+
raise TaskSelectionRequired(implicit_tasks)
|
|
2547
|
+
|
|
2548
|
+
if self.task_name:
|
|
2549
|
+
return self._execute_task(self.task_name)
|
|
2550
|
+
|
|
2519
2551
|
if self.registry:
|
|
2520
2552
|
# Check for named 'main' procedure first
|
|
2521
2553
|
if "main" in self.registry.named_procedures:
|
|
@@ -2604,6 +2636,198 @@ class TactusRuntime:
|
|
|
2604
2636
|
logger.error(f"Legacy procedure execution failed: {e}")
|
|
2605
2637
|
raise
|
|
2606
2638
|
|
|
2639
|
+
def _execute_task(self, task_name: str) -> Any:
|
|
2640
|
+
if not self.registry:
|
|
2641
|
+
raise RuntimeError("No registry available for task execution")
|
|
2642
|
+
|
|
2643
|
+
task = self._resolve_task(task_name)
|
|
2644
|
+
if task is None:
|
|
2645
|
+
# Allow run fallback to main procedure
|
|
2646
|
+
if task_name == "run":
|
|
2647
|
+
self.task_name = None
|
|
2648
|
+
return self._execute_workflow()
|
|
2649
|
+
|
|
2650
|
+
retriever_tasks = self._resolve_retriever_task_targets(task_name)
|
|
2651
|
+
if retriever_tasks:
|
|
2652
|
+
return self._execute_retriever_tasks(task_name, retriever_tasks)
|
|
2653
|
+
|
|
2654
|
+
raise RuntimeError(f"Task '{task_name}' not found")
|
|
2655
|
+
|
|
2656
|
+
task_payload = task.model_dump()
|
|
2657
|
+
entry = task_payload.get("entry")
|
|
2658
|
+
if entry is None:
|
|
2659
|
+
if task.children:
|
|
2660
|
+
raise RuntimeError(
|
|
2661
|
+
f"Task '{task_name}' has no entry. Available sub-tasks: "
|
|
2662
|
+
f"{', '.join(task.children.keys())}"
|
|
2663
|
+
)
|
|
2664
|
+
raise RuntimeError(f"Task '{task_name}' has no entry")
|
|
2665
|
+
|
|
2666
|
+
if not callable(entry):
|
|
2667
|
+
raise RuntimeError(f"Task '{task_name}' entry must be a function")
|
|
2668
|
+
return entry()
|
|
2669
|
+
|
|
2670
|
+
def _execute_retriever_tasks(self, task_name: str, retriever_names: list[str]) -> Any:
|
|
2671
|
+
if not retriever_names:
|
|
2672
|
+
raise RuntimeError(f"No retrievers available for task '{task_name}'")
|
|
2673
|
+
|
|
2674
|
+
base_task = task_name.split(":")[0]
|
|
2675
|
+
if base_task == "index":
|
|
2676
|
+
results = []
|
|
2677
|
+
for retriever_name in retriever_names:
|
|
2678
|
+
results.append(self._execute_retriever_index(retriever_name))
|
|
2679
|
+
return results[0] if len(results) == 1 else results
|
|
2680
|
+
|
|
2681
|
+
raise RuntimeError(f"Retriever task '{base_task}' is not supported")
|
|
2682
|
+
|
|
2683
|
+
def _execute_retriever_index(self, retriever_name: str) -> Any:
|
|
2684
|
+
if not self.registry:
|
|
2685
|
+
raise RuntimeError("No registry available for retriever execution")
|
|
2686
|
+
|
|
2687
|
+
retriever = self.registry.retrievers.get(retriever_name)
|
|
2688
|
+
if retriever is None:
|
|
2689
|
+
raise RuntimeError(f"Retriever '{retriever_name}' not found")
|
|
2690
|
+
|
|
2691
|
+
if not retriever.corpus:
|
|
2692
|
+
raise RuntimeError(f"Retriever '{retriever_name}' has no corpus configured")
|
|
2693
|
+
|
|
2694
|
+
corpus_decl = self.registry.corpora.get(retriever.corpus)
|
|
2695
|
+
if corpus_decl is None:
|
|
2696
|
+
raise RuntimeError(f"Corpus '{retriever.corpus}' not found for '{retriever_name}'")
|
|
2697
|
+
|
|
2698
|
+
corpus_root = corpus_decl.config.get("corpus_root") or corpus_decl.config.get("root")
|
|
2699
|
+
if not corpus_root:
|
|
2700
|
+
raise RuntimeError(f"Corpus '{retriever.corpus}' is missing a root path")
|
|
2701
|
+
|
|
2702
|
+
extraction_pipeline = {}
|
|
2703
|
+
corpus_configuration = (
|
|
2704
|
+
corpus_decl.config.get("configuration", {})
|
|
2705
|
+
if isinstance(corpus_decl.config, dict)
|
|
2706
|
+
else {}
|
|
2707
|
+
)
|
|
2708
|
+
if isinstance(corpus_configuration, dict):
|
|
2709
|
+
pipeline = corpus_configuration.get("pipeline", {}) or {}
|
|
2710
|
+
if isinstance(pipeline, dict):
|
|
2711
|
+
extraction_pipeline = pipeline.get("extract", {}) or {}
|
|
2712
|
+
if isinstance(extraction_pipeline, list):
|
|
2713
|
+
extraction_pipeline = {}
|
|
2714
|
+
|
|
2715
|
+
retriever_id = retriever.config.get("retriever_id") or retriever.config.get(
|
|
2716
|
+
"retriever_type"
|
|
2717
|
+
)
|
|
2718
|
+
if not retriever_id:
|
|
2719
|
+
raise RuntimeError(
|
|
2720
|
+
f"Retriever '{retriever_name}' is missing retriever_id; cannot build snapshot"
|
|
2721
|
+
)
|
|
2722
|
+
|
|
2723
|
+
configuration = retriever.config.get("configuration", {})
|
|
2724
|
+
pipeline = configuration.get("pipeline", {}) if isinstance(configuration, dict) else {}
|
|
2725
|
+
if not pipeline and isinstance(retriever.config.get("pipeline"), dict):
|
|
2726
|
+
pipeline = retriever.config.get("pipeline") or {}
|
|
2727
|
+
index_config = pipeline.get("index", {}) if isinstance(pipeline, dict) else {}
|
|
2728
|
+
if isinstance(index_config, list):
|
|
2729
|
+
index_config = {}
|
|
2730
|
+
|
|
2731
|
+
try:
|
|
2732
|
+
from biblicus.corpus import Corpus
|
|
2733
|
+
from biblicus.extraction import build_extraction_snapshot
|
|
2734
|
+
from biblicus.retrievers import get_retriever
|
|
2735
|
+
except Exception as exc:
|
|
2736
|
+
raise RuntimeError(f"Biblicus retrieval retriever unavailable: {exc}") from exc
|
|
2737
|
+
|
|
2738
|
+
corpus = Corpus(Path(corpus_root))
|
|
2739
|
+
retriever_impl = get_retriever(retriever_id)
|
|
2740
|
+
configuration_name = retriever_name
|
|
2741
|
+
|
|
2742
|
+
if extraction_pipeline and isinstance(extraction_pipeline, dict):
|
|
2743
|
+
extraction_manifest = build_extraction_snapshot(
|
|
2744
|
+
corpus,
|
|
2745
|
+
extractor_id="pipeline",
|
|
2746
|
+
configuration_name=f"{retriever.corpus or retriever_name}-extract",
|
|
2747
|
+
configuration=extraction_pipeline,
|
|
2748
|
+
)
|
|
2749
|
+
if isinstance(index_config, dict) and "extraction_snapshot" not in index_config:
|
|
2750
|
+
index_config["extraction_snapshot"] = (
|
|
2751
|
+
f"{extraction_manifest.configuration.extractor_id}:"
|
|
2752
|
+
f"{extraction_manifest.snapshot_id}"
|
|
2753
|
+
)
|
|
2754
|
+
snapshot = retriever_impl.build_snapshot(
|
|
2755
|
+
corpus, configuration_name=configuration_name, configuration=index_config
|
|
2756
|
+
)
|
|
2757
|
+
|
|
2758
|
+
return snapshot.model_dump()
|
|
2759
|
+
|
|
2760
|
+
def _resolve_task(self, task_name: str) -> Optional[TaskDeclaration]:
|
|
2761
|
+
if not self.registry:
|
|
2762
|
+
return None
|
|
2763
|
+
segments = [segment for segment in task_name.split(":") if segment]
|
|
2764
|
+
if not segments:
|
|
2765
|
+
return None
|
|
2766
|
+
current = self.registry.tasks.get(segments[0])
|
|
2767
|
+
for segment in segments[1:]:
|
|
2768
|
+
if current is None:
|
|
2769
|
+
return None
|
|
2770
|
+
child = current.children.get(segment)
|
|
2771
|
+
if child is None:
|
|
2772
|
+
payload = current.model_dump(exclude={"name", "children"})
|
|
2773
|
+
inline_child = payload.get(segment)
|
|
2774
|
+
if isinstance(inline_child, dict):
|
|
2775
|
+
task_payload = dict(inline_child)
|
|
2776
|
+
task_payload["name"] = segment
|
|
2777
|
+
try:
|
|
2778
|
+
return TaskDeclaration(**task_payload)
|
|
2779
|
+
except Exception:
|
|
2780
|
+
return None
|
|
2781
|
+
return None
|
|
2782
|
+
current = child
|
|
2783
|
+
return current
|
|
2784
|
+
|
|
2785
|
+
def _resolve_retriever_task_targets(self, task_name: str) -> list[str]:
|
|
2786
|
+
if not self.registry or not self.registry.retrievers:
|
|
2787
|
+
return []
|
|
2788
|
+
segments = [segment for segment in task_name.split(":") if segment]
|
|
2789
|
+
if not segments:
|
|
2790
|
+
return []
|
|
2791
|
+
task = segments[0]
|
|
2792
|
+
target = segments[1] if len(segments) > 1 else None
|
|
2793
|
+
from tactus.core.retriever_tasks import resolve_retriever_id, supported_retriever_tasks
|
|
2794
|
+
|
|
2795
|
+
targets: list[str] = []
|
|
2796
|
+
for retriever_name, retriever in self.registry.retrievers.items():
|
|
2797
|
+
config = getattr(retriever, "config", {})
|
|
2798
|
+
retriever_id = resolve_retriever_id(config if isinstance(config, dict) else {})
|
|
2799
|
+
if task in supported_retriever_tasks(retriever_id):
|
|
2800
|
+
targets.append(retriever_name)
|
|
2801
|
+
|
|
2802
|
+
if target:
|
|
2803
|
+
return [name for name in targets if name == target]
|
|
2804
|
+
return targets
|
|
2805
|
+
|
|
2806
|
+
def _expand_inline_task_children(self, registry: ProcedureRegistry) -> None:
|
|
2807
|
+
if not registry or not registry.tasks:
|
|
2808
|
+
return
|
|
2809
|
+
|
|
2810
|
+
def add_children(parent: TaskDeclaration) -> None:
|
|
2811
|
+
payload = parent.model_dump(exclude={"name", "children"})
|
|
2812
|
+
for key, value in payload.items():
|
|
2813
|
+
if not isinstance(key, str) or not isinstance(value, dict):
|
|
2814
|
+
continue
|
|
2815
|
+
if not value.get("__tactus_task_config"):
|
|
2816
|
+
continue
|
|
2817
|
+
if key in parent.children:
|
|
2818
|
+
continue
|
|
2819
|
+
task_payload = dict(value)
|
|
2820
|
+
task_payload["name"] = key
|
|
2821
|
+
try:
|
|
2822
|
+
child = TaskDeclaration(**task_payload)
|
|
2823
|
+
except Exception:
|
|
2824
|
+
continue
|
|
2825
|
+
parent.children[key] = child
|
|
2826
|
+
add_children(child)
|
|
2827
|
+
|
|
2828
|
+
for task in registry.tasks.values():
|
|
2829
|
+
add_children(task)
|
|
2830
|
+
|
|
2607
2831
|
def _maybe_transform_script_mode_source(self, source: str) -> str:
|
|
2608
2832
|
"""
|
|
2609
2833
|
Transform "script mode" source into an implicit Procedure wrapper.
|
|
@@ -2652,11 +2876,12 @@ class TactusRuntime:
|
|
|
2652
2876
|
in_body = False
|
|
2653
2877
|
brace_depth = 0
|
|
2654
2878
|
function_depth = 0 # Track function...end blocks
|
|
2655
|
-
long_string_eq: str
|
|
2879
|
+
long_string_eq: Optional[str] = None
|
|
2656
2880
|
|
|
2657
2881
|
decl_start = re.compile(
|
|
2658
2882
|
r"^\s*(?:"
|
|
2659
2883
|
r"input|output|Mocks|Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt|"
|
|
2884
|
+
r"Task|IncludeTasks|Context|Corpus|Retriever|Compactor|"
|
|
2660
2885
|
r"Specifications|Evaluation|Evaluations|"
|
|
2661
2886
|
r"default_provider|default_model|return_prompt|error_prompt|status_prompt|async|"
|
|
2662
2887
|
r"max_depth|max_turns"
|
|
@@ -2665,7 +2890,9 @@ class TactusRuntime:
|
|
|
2665
2890
|
require_stmt = re.compile(r"^\s*(?:local\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*require\(")
|
|
2666
2891
|
assignment_decl = re.compile(
|
|
2667
2892
|
r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*(?:"
|
|
2668
|
-
r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt"
|
|
2893
|
+
r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt|"
|
|
2894
|
+
r"Task|TaskFunction|Context|Corpus|Retriever|Compactor|function|"
|
|
2895
|
+
r"[A-Za-z_][A-Za-z0-9_]*Retriever|[A-Za-z_][A-Za-z0-9_]*\.Retriever"
|
|
2669
2896
|
r")\b"
|
|
2670
2897
|
)
|
|
2671
2898
|
# Match function definitions: function name() or local function name()
|
|
@@ -2871,7 +3098,7 @@ class TactusRuntime:
|
|
|
2871
3098
|
return False
|
|
2872
3099
|
|
|
2873
3100
|
def _parse_declarations(
|
|
2874
|
-
self, source: str, tool_primitive: ToolPrimitive
|
|
3101
|
+
self, source: str, tool_primitive: Optional[ToolPrimitive] = None
|
|
2875
3102
|
) -> ProcedureRegistry:
|
|
2876
3103
|
"""
|
|
2877
3104
|
Execute .tac to collect declarations.
|
|
@@ -2940,6 +3167,93 @@ class TactusRuntime:
|
|
|
2940
3167
|
except LuaSandboxError as e:
|
|
2941
3168
|
raise TactusRuntimeError(f"Failed to parse DSL: {e}")
|
|
2942
3169
|
|
|
3170
|
+
self._expand_inline_task_children(builder.registry)
|
|
3171
|
+
|
|
3172
|
+
# Execute IncludeTasks files to register additional tasks
|
|
3173
|
+
if builder.registry.include_tasks:
|
|
3174
|
+
base_path = Path(self.source_file_path).parent if self.source_file_path else Path.cwd()
|
|
3175
|
+
include_queue = [
|
|
3176
|
+
{
|
|
3177
|
+
"path": include.get("path"),
|
|
3178
|
+
"namespace": include.get("namespace"),
|
|
3179
|
+
"base": base_path,
|
|
3180
|
+
}
|
|
3181
|
+
for include in builder.registry.include_tasks
|
|
3182
|
+
]
|
|
3183
|
+
seen_includes: set[Path] = set()
|
|
3184
|
+
|
|
3185
|
+
while include_queue:
|
|
3186
|
+
include = include_queue.pop(0)
|
|
3187
|
+
include_path = include.get("path")
|
|
3188
|
+
if not include_path:
|
|
3189
|
+
continue
|
|
3190
|
+
include_base = include.get("base") or base_path
|
|
3191
|
+
include_file = (include_base / include_path).resolve()
|
|
3192
|
+
if include_file in seen_includes:
|
|
3193
|
+
raise TactusRuntimeError(f"IncludeTasks cycle detected: {include_file}")
|
|
3194
|
+
seen_includes.add(include_file)
|
|
3195
|
+
if not include_file.exists():
|
|
3196
|
+
raise TactusRuntimeError(f"Included tasks file not found: {include_file}")
|
|
3197
|
+
|
|
3198
|
+
pre_task_names = set(builder.registry.tasks.keys())
|
|
3199
|
+
pre_include_count = len(builder.registry.include_tasks)
|
|
3200
|
+
pre_counts = {
|
|
3201
|
+
"agents": len(builder.registry.agents),
|
|
3202
|
+
"toolsets": len(builder.registry.toolsets),
|
|
3203
|
+
"lua_tools": len(builder.registry.lua_tools),
|
|
3204
|
+
"contexts": len(builder.registry.contexts),
|
|
3205
|
+
"corpora": len(builder.registry.corpora),
|
|
3206
|
+
"retrievers": len(builder.registry.retrievers),
|
|
3207
|
+
"compactors": len(builder.registry.compactors),
|
|
3208
|
+
"named_procedures": len(builder.registry.named_procedures),
|
|
3209
|
+
}
|
|
3210
|
+
include_source = include_file.read_text()
|
|
3211
|
+
try:
|
|
3212
|
+
sandbox.execute(include_source)
|
|
3213
|
+
except LuaSandboxError as e:
|
|
3214
|
+
raise TactusRuntimeError(f"Failed to execute IncludeTasks file: {e}")
|
|
3215
|
+
|
|
3216
|
+
post_counts = {
|
|
3217
|
+
"agents": len(builder.registry.agents),
|
|
3218
|
+
"toolsets": len(builder.registry.toolsets),
|
|
3219
|
+
"lua_tools": len(builder.registry.lua_tools),
|
|
3220
|
+
"contexts": len(builder.registry.contexts),
|
|
3221
|
+
"corpora": len(builder.registry.corpora),
|
|
3222
|
+
"retrievers": len(builder.registry.retrievers),
|
|
3223
|
+
"compactors": len(builder.registry.compactors),
|
|
3224
|
+
"named_procedures": len(builder.registry.named_procedures),
|
|
3225
|
+
}
|
|
3226
|
+
if any(post_counts[key] != pre_counts[key] for key in post_counts):
|
|
3227
|
+
raise TactusRuntimeError(
|
|
3228
|
+
f"IncludeTasks files must only contain Task declarations: {include_file}"
|
|
3229
|
+
)
|
|
3230
|
+
|
|
3231
|
+
new_task_names = set(builder.registry.tasks.keys()) - pre_task_names
|
|
3232
|
+
namespace = include.get("namespace")
|
|
3233
|
+
if namespace and new_task_names:
|
|
3234
|
+
if namespace in builder.registry.tasks:
|
|
3235
|
+
raise TactusRuntimeError(f"Duplicate task namespace '{namespace}'")
|
|
3236
|
+
namespaced_children = {
|
|
3237
|
+
name: builder.registry.tasks.pop(name) for name in new_task_names
|
|
3238
|
+
}
|
|
3239
|
+
builder.registry.tasks[namespace] = TaskDeclaration(
|
|
3240
|
+
name=namespace,
|
|
3241
|
+
children=namespaced_children,
|
|
3242
|
+
)
|
|
3243
|
+
|
|
3244
|
+
new_includes = builder.registry.include_tasks[pre_include_count:]
|
|
3245
|
+
if new_includes:
|
|
3246
|
+
include_queue.extend(
|
|
3247
|
+
[
|
|
3248
|
+
{
|
|
3249
|
+
"path": nested.get("path"),
|
|
3250
|
+
"namespace": nested.get("namespace"),
|
|
3251
|
+
"base": include_file.parent,
|
|
3252
|
+
}
|
|
3253
|
+
for nested in new_includes
|
|
3254
|
+
]
|
|
3255
|
+
)
|
|
3256
|
+
|
|
2943
3257
|
# Auto-register plain function main() if it exists
|
|
2944
3258
|
#
|
|
2945
3259
|
# Some .tac files use plain Lua syntax: `function main() ... end`
|