tactus 0.34.1__py3-none-any.whl → 0.35.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +40 -25
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
tactus/core/runtime.py
CHANGED
|
@@ -13,7 +13,7 @@ import io
|
|
|
13
13
|
import logging
|
|
14
14
|
import time
|
|
15
15
|
import uuid
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import Any
|
|
17
17
|
|
|
18
18
|
from tactus.core.registry import ProcedureRegistry, RegistryBuilder
|
|
19
19
|
from tactus.core.dsl_stubs import create_dsl_stubs, lua_table_to_dict
|
|
@@ -68,19 +68,19 @@ class TactusRuntime:
|
|
|
68
68
|
def __init__(
|
|
69
69
|
self,
|
|
70
70
|
procedure_id: str,
|
|
71
|
-
storage_backend:
|
|
72
|
-
hitl_handler:
|
|
73
|
-
chat_recorder:
|
|
71
|
+
storage_backend: StorageBackend | None = None,
|
|
72
|
+
hitl_handler: HITLHandler | None = None,
|
|
73
|
+
chat_recorder: ChatRecorder | None = None,
|
|
74
74
|
mcp_server=None,
|
|
75
|
-
mcp_servers:
|
|
76
|
-
openai_api_key:
|
|
75
|
+
mcp_servers: dict[str, Any] | None = None,
|
|
76
|
+
openai_api_key: str | None = None,
|
|
77
77
|
log_handler=None,
|
|
78
|
-
tool_primitive:
|
|
78
|
+
tool_primitive: ToolPrimitive | None = None,
|
|
79
79
|
recursion_depth: int = 0,
|
|
80
|
-
tool_paths:
|
|
81
|
-
external_config:
|
|
82
|
-
run_id:
|
|
83
|
-
source_file_path:
|
|
80
|
+
tool_paths: list[str] | None = None,
|
|
81
|
+
external_config: dict[str, Any] | None = None,
|
|
82
|
+
run_id: str | None = None,
|
|
83
|
+
source_file_path: str | None = None,
|
|
84
84
|
):
|
|
85
85
|
"""
|
|
86
86
|
Initialize the Tactus runtime.
|
|
@@ -117,7 +117,10 @@ class TactusRuntime:
|
|
|
117
117
|
)
|
|
118
118
|
# Wrap in adapter for HITLHandler compatibility
|
|
119
119
|
self.hitl_handler = ControlLoopHITLAdapter(control_handler)
|
|
120
|
-
logger.info(
|
|
120
|
+
logger.info(
|
|
121
|
+
"Auto-configured ControlLoopHandler with %s channel(s)",
|
|
122
|
+
len(channels),
|
|
123
|
+
)
|
|
121
124
|
else:
|
|
122
125
|
# No channels available, leave hitl_handler as None
|
|
123
126
|
self.hitl_handler = None
|
|
@@ -141,55 +144,55 @@ class TactusRuntime:
|
|
|
141
144
|
self.source_file_path = source_file_path
|
|
142
145
|
|
|
143
146
|
# Will be initialized during setup
|
|
144
|
-
self.config:
|
|
145
|
-
self.registry:
|
|
146
|
-
self.lua_sandbox:
|
|
147
|
-
self.output_validator:
|
|
148
|
-
self.template_resolver:
|
|
149
|
-
self.message_history_manager:
|
|
147
|
+
self.config: dict[str, Any] | None = None # Legacy YAML support
|
|
148
|
+
self.registry: ProcedureRegistry | None = None # New DSL registry
|
|
149
|
+
self.lua_sandbox: LuaSandbox | None = None
|
|
150
|
+
self.output_validator: OutputValidator | None = None
|
|
151
|
+
self.template_resolver: TemplateResolver | None = None
|
|
152
|
+
self.message_history_manager: MessageHistoryManager | None = None
|
|
150
153
|
|
|
151
154
|
# Execution context
|
|
152
|
-
self.execution_context:
|
|
155
|
+
self.execution_context: BaseExecutionContext | None = None
|
|
153
156
|
|
|
154
157
|
# Primitives (shared across all agents)
|
|
155
|
-
self.state_primitive:
|
|
156
|
-
self.iterations_primitive:
|
|
157
|
-
self.stop_primitive:
|
|
158
|
-
self.tool_primitive:
|
|
159
|
-
self.human_primitive:
|
|
160
|
-
self.step_primitive:
|
|
161
|
-
self.checkpoint_primitive:
|
|
162
|
-
self.log_primitive:
|
|
163
|
-
self.json_primitive:
|
|
164
|
-
self.retry_primitive:
|
|
165
|
-
self.file_primitive:
|
|
166
|
-
self.procedure_primitive:
|
|
167
|
-
self.system_primitive:
|
|
168
|
-
self.host_primitive:
|
|
158
|
+
self.state_primitive: StatePrimitive | None = None
|
|
159
|
+
self.iterations_primitive: IterationsPrimitive | None = None
|
|
160
|
+
self.stop_primitive: StopPrimitive | None = None
|
|
161
|
+
self.tool_primitive: ToolPrimitive | None = None
|
|
162
|
+
self.human_primitive: HumanPrimitive | None = None
|
|
163
|
+
self.step_primitive: StepPrimitive | None = None
|
|
164
|
+
self.checkpoint_primitive: CheckpointPrimitive | None = None
|
|
165
|
+
self.log_primitive: LogPrimitive | None = None
|
|
166
|
+
self.json_primitive: JsonPrimitive | None = None
|
|
167
|
+
self.retry_primitive: RetryPrimitive | None = None
|
|
168
|
+
self.file_primitive: FilePrimitive | None = None
|
|
169
|
+
self.procedure_primitive: ProcedurePrimitive | None = None
|
|
170
|
+
self.system_primitive: SystemPrimitive | None = None
|
|
171
|
+
self.host_primitive: HostPrimitive | None = None
|
|
169
172
|
|
|
170
173
|
# Agent primitives (one per agent)
|
|
171
|
-
self.agents:
|
|
174
|
+
self.agents: dict[str, Any] = {}
|
|
172
175
|
|
|
173
176
|
# Model primitives (one per model)
|
|
174
|
-
self.models:
|
|
177
|
+
self.models: dict[str, Any] = {}
|
|
175
178
|
|
|
176
179
|
# Toolset registry (name -> AbstractToolset instance)
|
|
177
|
-
self.toolset_registry:
|
|
180
|
+
self.toolset_registry: dict[str, Any] = {}
|
|
178
181
|
|
|
179
182
|
# User dependencies (HTTP clients, DB connections, etc.)
|
|
180
|
-
self.user_dependencies:
|
|
181
|
-
self.dependency_manager:
|
|
183
|
+
self.user_dependencies: dict[str, Any] = {}
|
|
184
|
+
self.dependency_manager: Any | None = None # ResourceManager for cleanup
|
|
182
185
|
|
|
183
186
|
# Mock manager for testing
|
|
184
|
-
self.mock_manager:
|
|
185
|
-
self.external_agent_mocks:
|
|
187
|
+
self.mock_manager: Any | None = None # MockManager instance
|
|
188
|
+
self.external_agent_mocks: dict[str, list[dict[str, Any]]] | None = None
|
|
186
189
|
self.mock_all_agents: bool = False
|
|
187
190
|
|
|
188
|
-
logger.info(
|
|
191
|
+
logger.info("TactusRuntime initialized for procedure %s", procedure_id)
|
|
189
192
|
|
|
190
193
|
async def execute(
|
|
191
|
-
self, source: str, context:
|
|
192
|
-
) ->
|
|
194
|
+
self, source: str, context: dict[str, Any] | None = None, format: str = "yaml"
|
|
195
|
+
) -> dict[str, Any]:
|
|
193
196
|
"""
|
|
194
197
|
Execute a workflow (Lua DSL or legacy YAML format).
|
|
195
198
|
|
|
@@ -210,7 +213,7 @@ class TactusRuntime:
|
|
|
210
213
|
Raises:
|
|
211
214
|
TactusRuntimeError: If execution fails
|
|
212
215
|
"""
|
|
213
|
-
|
|
216
|
+
chat_session_id = None
|
|
214
217
|
self.context = context or {} # Store context for param merging
|
|
215
218
|
|
|
216
219
|
try:
|
|
@@ -226,7 +229,8 @@ class TactusRuntime:
|
|
|
226
229
|
|
|
227
230
|
sandbox_base_path = str(Path(self.source_file_path).parent.resolve())
|
|
228
231
|
logger.debug(
|
|
229
|
-
|
|
232
|
+
"Using source file directory as sandbox base_path: %s",
|
|
233
|
+
sandbox_base_path,
|
|
230
234
|
)
|
|
231
235
|
|
|
232
236
|
self.lua_sandbox = LuaSandbox(
|
|
@@ -262,13 +266,13 @@ class TactusRuntime:
|
|
|
262
266
|
# Set .tac file path NOW (before parsing) so source location is available during agent calls
|
|
263
267
|
if self.source_file_path:
|
|
264
268
|
self.execution_context.set_tac_file(self.source_file_path, source)
|
|
265
|
-
logger.info(
|
|
269
|
+
logger.info("[CHECKPOINT] Set .tac file path EARLY: %s", self.source_file_path)
|
|
266
270
|
else:
|
|
267
271
|
logger.warning("[CHECKPOINT] .tac file path NOT set - source_file_path is None")
|
|
268
272
|
|
|
269
273
|
# 0b. For Lua DSL, inject placeholder primitives BEFORE parsing
|
|
270
274
|
# so they're available in the procedure function's closure
|
|
271
|
-
|
|
275
|
+
placeholder_tool_primitive = None # Will be set for Lua DSL
|
|
272
276
|
if format == "lua":
|
|
273
277
|
logger.debug("Pre-injecting placeholder primitives for Lua DSL parsing")
|
|
274
278
|
# Import here to avoid issues with YAML format
|
|
@@ -283,11 +287,11 @@ class TactusRuntime:
|
|
|
283
287
|
# Use injected tool primitive if provided (for mock mode)
|
|
284
288
|
# This ensures ToolHandles (like done) use the same primitive as MockAgentPrimitive
|
|
285
289
|
if self._injected_tool_primitive:
|
|
286
|
-
|
|
290
|
+
placeholder_tool_primitive = self._injected_tool_primitive
|
|
287
291
|
logger.debug("Using injected tool primitive for parsing (mock mode)")
|
|
288
292
|
else:
|
|
289
293
|
# Create tool primitive with log_handler so direct tool calls are tracked
|
|
290
|
-
|
|
294
|
+
placeholder_tool_primitive = LuaToolPrimitive(
|
|
291
295
|
log_handler=self.log_handler, procedure_id=self.procedure_id
|
|
292
296
|
)
|
|
293
297
|
placeholder_params = {} # Empty params dict
|
|
@@ -296,7 +300,8 @@ class TactusRuntime:
|
|
|
296
300
|
self.lua_sandbox.inject_primitive("_state_primitive", placeholder_state)
|
|
297
301
|
|
|
298
302
|
# Create State object with special methods and lowercase state proxy with metatable
|
|
299
|
-
self.lua_sandbox.lua.execute(
|
|
303
|
+
self.lua_sandbox.lua.execute(
|
|
304
|
+
"""
|
|
300
305
|
State = {
|
|
301
306
|
increment = function(key, amount)
|
|
302
307
|
return _state_primitive.increment(key, amount or 1)
|
|
@@ -318,8 +323,9 @@ class TactusRuntime:
|
|
|
318
323
|
_state_primitive.set(key, value)
|
|
319
324
|
end
|
|
320
325
|
})
|
|
321
|
-
"""
|
|
322
|
-
|
|
326
|
+
"""
|
|
327
|
+
)
|
|
328
|
+
self.lua_sandbox.inject_primitive("Tool", placeholder_tool_primitive)
|
|
323
329
|
self.lua_sandbox.inject_primitive("params", placeholder_params)
|
|
324
330
|
placeholder_system = LuaSystemPrimitive(
|
|
325
331
|
procedure_id=self.procedure_id, log_handler=self.log_handler
|
|
@@ -335,12 +341,14 @@ class TactusRuntime:
|
|
|
335
341
|
source = self._maybe_transform_script_mode_source(source)
|
|
336
342
|
|
|
337
343
|
# Pass placeholder_tool so tool() can return callable ToolHandles
|
|
338
|
-
self.registry = self._parse_declarations(source,
|
|
344
|
+
self.registry = self._parse_declarations(source, placeholder_tool_primitive)
|
|
339
345
|
logger.info("Loaded procedure from Lua DSL")
|
|
340
346
|
# Convert registry to config dict for compatibility
|
|
341
347
|
self.config = self._registry_to_config(self.registry)
|
|
342
348
|
logger.debug(
|
|
343
|
-
|
|
349
|
+
"Registry contents: agents=%s, lua_tools=%s",
|
|
350
|
+
list(self.registry.agents.keys()),
|
|
351
|
+
list(self.registry.lua_tools.keys()),
|
|
344
352
|
)
|
|
345
353
|
|
|
346
354
|
# Process mocks from registry if mock_manager exists
|
|
@@ -390,14 +398,18 @@ class TactusRuntime:
|
|
|
390
398
|
if key in self.external_config:
|
|
391
399
|
self.config[key] = self.external_config[key]
|
|
392
400
|
|
|
393
|
-
logger.debug(
|
|
401
|
+
logger.debug("Merged external config with %s keys", len(self.external_config))
|
|
394
402
|
else:
|
|
395
403
|
# Legacy YAML support
|
|
396
404
|
logger.info("Step 1: Parsing YAML configuration (legacy)")
|
|
397
405
|
if ProcedureYAMLParser is None:
|
|
398
406
|
raise TactusRuntimeError("YAML support not available - use Lua DSL format")
|
|
399
407
|
self.config = ProcedureYAMLParser.parse(source)
|
|
400
|
-
logger.info(
|
|
408
|
+
logger.info(
|
|
409
|
+
"Loaded procedure: %s v%s",
|
|
410
|
+
self.config["name"],
|
|
411
|
+
self.config["version"],
|
|
412
|
+
)
|
|
401
413
|
|
|
402
414
|
# 2. Setup output validator
|
|
403
415
|
logger.info("Step 2: Setting up output validator")
|
|
@@ -405,7 +417,9 @@ class TactusRuntime:
|
|
|
405
417
|
self.output_validator = OutputValidator(output_schema)
|
|
406
418
|
if output_schema:
|
|
407
419
|
logger.info(
|
|
408
|
-
|
|
420
|
+
"Output schema has %s fields: %s",
|
|
421
|
+
len(output_schema),
|
|
422
|
+
list(output_schema.keys()),
|
|
409
423
|
)
|
|
410
424
|
|
|
411
425
|
# 3. Lua sandbox is already set up in step 0
|
|
@@ -414,7 +428,7 @@ class TactusRuntime:
|
|
|
414
428
|
# 4. Initialize primitives
|
|
415
429
|
logger.info("Step 4: Initializing primitives")
|
|
416
430
|
# Pass placeholder_tool so direct tool calls are tracked in the same primitive
|
|
417
|
-
await self._initialize_primitives(placeholder_tool=
|
|
431
|
+
await self._initialize_primitives(placeholder_tool=placeholder_tool_primitive)
|
|
418
432
|
|
|
419
433
|
# 4b. Initialize template resolver and session manager
|
|
420
434
|
self.template_resolver = TemplateResolver(
|
|
@@ -427,9 +441,9 @@ class TactusRuntime:
|
|
|
427
441
|
# 5. Start chat session if recorder available
|
|
428
442
|
if self.chat_recorder:
|
|
429
443
|
logger.info("Step 5: Starting chat session")
|
|
430
|
-
|
|
431
|
-
if
|
|
432
|
-
logger.info(
|
|
444
|
+
chat_session_id = await self.chat_recorder.start_session(context)
|
|
445
|
+
if chat_session_id:
|
|
446
|
+
logger.info("Chat session started: %s", chat_session_id)
|
|
433
447
|
else:
|
|
434
448
|
logger.warning("Failed to create chat session - continuing without recording")
|
|
435
449
|
|
|
@@ -501,7 +515,7 @@ class TactusRuntime:
|
|
|
501
515
|
# 10.5. Apply return_prompt if specified (future: inject to agent for summary)
|
|
502
516
|
if self.config.get("return_prompt"):
|
|
503
517
|
return_prompt = self.config["return_prompt"]
|
|
504
|
-
logger.info(
|
|
518
|
+
logger.info("Return prompt specified: %s...", return_prompt[:50])
|
|
505
519
|
# TODO: In full implementation, inject this prompt to an agent to get a summary
|
|
506
520
|
# For now, just log it
|
|
507
521
|
|
|
@@ -524,8 +538,8 @@ class TactusRuntime:
|
|
|
524
538
|
await agent_primitive.flush_recordings()
|
|
525
539
|
|
|
526
540
|
# 13. End chat session
|
|
527
|
-
if self.chat_recorder and
|
|
528
|
-
await self.chat_recorder.end_session(
|
|
541
|
+
if self.chat_recorder and chat_session_id:
|
|
542
|
+
await self.chat_recorder.end_session(chat_session_id, status="COMPLETED")
|
|
529
543
|
|
|
530
544
|
# 14. Build final results
|
|
531
545
|
final_state = self.state_primitive.all() if self.state_primitive else {}
|
|
@@ -536,9 +550,9 @@ class TactusRuntime:
|
|
|
536
550
|
)
|
|
537
551
|
|
|
538
552
|
logger.info(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
553
|
+
"Workflow execution complete: %s iterations, %s tool calls",
|
|
554
|
+
self.iterations_primitive.current() if self.iterations_primitive else 0,
|
|
555
|
+
len(tools_used),
|
|
542
556
|
)
|
|
543
557
|
|
|
544
558
|
# Collect cost events and calculate totals
|
|
@@ -608,14 +622,14 @@ class TactusRuntime:
|
|
|
608
622
|
"tools_used": tools_used,
|
|
609
623
|
"stop_requested": self.stop_primitive.requested() if self.stop_primitive else False,
|
|
610
624
|
"stop_reason": self.stop_primitive.reason() if self.stop_primitive else None,
|
|
611
|
-
"session_id":
|
|
625
|
+
"session_id": chat_session_id,
|
|
612
626
|
"total_cost": total_cost,
|
|
613
627
|
"total_tokens": total_tokens,
|
|
614
628
|
"cost_breakdown": cost_breakdown,
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
except ProcedureWaitingForHuman as e:
|
|
618
|
-
logger.info(
|
|
632
|
+
logger.info("Procedure waiting for human: %s", e)
|
|
619
633
|
|
|
620
634
|
# Flush recordings before exiting
|
|
621
635
|
if self.chat_recorder:
|
|
@@ -632,17 +646,17 @@ class TactusRuntime:
|
|
|
632
646
|
"procedure_id": self.procedure_id,
|
|
633
647
|
"pending_message_id": getattr(e, "pending_message_id", None),
|
|
634
648
|
"message": str(e),
|
|
635
|
-
"session_id":
|
|
649
|
+
"session_id": chat_session_id,
|
|
636
650
|
}
|
|
637
651
|
|
|
638
652
|
except ProcedureConfigError as e:
|
|
639
|
-
logger.error(
|
|
653
|
+
logger.error("Configuration error: %s", e)
|
|
640
654
|
# Flush recordings even on error
|
|
641
|
-
if self.chat_recorder and
|
|
655
|
+
if self.chat_recorder and chat_session_id:
|
|
642
656
|
try:
|
|
643
|
-
await self.chat_recorder.end_session(
|
|
657
|
+
await self.chat_recorder.end_session(chat_session_id, status="FAILED")
|
|
644
658
|
except Exception as err:
|
|
645
|
-
logger.warning(
|
|
659
|
+
logger.warning("Failed to end chat session: %s", err)
|
|
646
660
|
|
|
647
661
|
# Send error summary event if log handler is available
|
|
648
662
|
if self.log_handler:
|
|
@@ -672,20 +686,20 @@ class TactusRuntime:
|
|
|
672
686
|
}
|
|
673
687
|
|
|
674
688
|
except LuaSandboxError as e:
|
|
675
|
-
logger.error(
|
|
689
|
+
logger.error("Lua execution error: %s", e)
|
|
676
690
|
|
|
677
691
|
# Apply error_prompt if specified (future: inject to agent for explanation)
|
|
678
692
|
if self.config and self.config.get("error_prompt"):
|
|
679
693
|
error_prompt = self.config["error_prompt"]
|
|
680
|
-
logger.info(
|
|
694
|
+
logger.info("Error prompt specified: %s...", error_prompt[:50])
|
|
681
695
|
# TODO: In full implementation, inject this prompt to an agent to get an explanation
|
|
682
696
|
|
|
683
697
|
# Flush recordings even on error
|
|
684
|
-
if self.chat_recorder and
|
|
698
|
+
if self.chat_recorder and chat_session_id:
|
|
685
699
|
try:
|
|
686
|
-
await self.chat_recorder.end_session(
|
|
700
|
+
await self.chat_recorder.end_session(chat_session_id, status="FAILED")
|
|
687
701
|
except Exception as err:
|
|
688
|
-
logger.warning(
|
|
702
|
+
logger.warning("Failed to end chat session: %s", err)
|
|
689
703
|
|
|
690
704
|
# Send error summary event if log handler is available
|
|
691
705
|
if self.log_handler:
|
|
@@ -714,26 +728,21 @@ class TactusRuntime:
|
|
|
714
728
|
"error": f"Lua execution error: {e}",
|
|
715
729
|
}
|
|
716
730
|
|
|
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
|
-
|
|
722
731
|
except Exception as e:
|
|
723
|
-
logger.error(
|
|
732
|
+
logger.error("Unexpected error: %s", e, exc_info=True)
|
|
724
733
|
|
|
725
734
|
# Apply error_prompt if specified (future: inject to agent for explanation)
|
|
726
735
|
if self.config and self.config.get("error_prompt"):
|
|
727
736
|
error_prompt = self.config["error_prompt"]
|
|
728
|
-
logger.info(
|
|
737
|
+
logger.info("Error prompt specified: %s...", error_prompt[:50])
|
|
729
738
|
# TODO: In full implementation, inject this prompt to an agent to get an explanation
|
|
730
739
|
|
|
731
740
|
# Flush recordings even on error
|
|
732
|
-
if self.chat_recorder and
|
|
741
|
+
if self.chat_recorder and chat_session_id:
|
|
733
742
|
try:
|
|
734
|
-
await self.chat_recorder.end_session(
|
|
743
|
+
await self.chat_recorder.end_session(chat_session_id, status="FAILED")
|
|
735
744
|
except Exception as err:
|
|
736
|
-
logger.warning(
|
|
745
|
+
logger.warning("Failed to end chat session: %s", err)
|
|
737
746
|
|
|
738
747
|
# Send error summary event if log handler is available
|
|
739
748
|
if self.log_handler:
|
|
@@ -769,7 +778,7 @@ class TactusRuntime:
|
|
|
769
778
|
await self.mcp_manager.__aexit__(None, None, None)
|
|
770
779
|
logger.info("Disconnected from MCP servers")
|
|
771
780
|
except Exception as e:
|
|
772
|
-
logger.warning(
|
|
781
|
+
logger.warning("Error disconnecting from MCP servers: %s", e)
|
|
773
782
|
|
|
774
783
|
# Cleanup: Close user dependencies
|
|
775
784
|
if self.dependency_manager:
|
|
@@ -777,9 +786,12 @@ class TactusRuntime:
|
|
|
777
786
|
await self.dependency_manager.cleanup()
|
|
778
787
|
logger.info("Cleaned up user dependencies")
|
|
779
788
|
except Exception as e:
|
|
780
|
-
logger.warning(
|
|
789
|
+
logger.warning("Error cleaning up dependencies: %s", e)
|
|
781
790
|
|
|
782
|
-
async def _initialize_primitives(
|
|
791
|
+
async def _initialize_primitives(
|
|
792
|
+
self,
|
|
793
|
+
placeholder_tool: ToolPrimitive | None = None,
|
|
794
|
+
):
|
|
783
795
|
"""Initialize all primitive objects.
|
|
784
796
|
|
|
785
797
|
Args:
|
|
@@ -798,7 +810,7 @@ class TactusRuntime:
|
|
|
798
810
|
self.tool_primitive = self._injected_tool_primitive
|
|
799
811
|
logger.info("Using injected tool primitive (mock mode)")
|
|
800
812
|
elif placeholder_tool:
|
|
801
|
-
# Reuse
|
|
813
|
+
# Reuse placeholder tool primitive so direct tool calls from ToolHandles are tracked
|
|
802
814
|
self.tool_primitive = placeholder_tool
|
|
803
815
|
logger.debug("Reusing placeholder tool primitive for direct tool call tracking")
|
|
804
816
|
else:
|
|
@@ -816,7 +828,7 @@ class TactusRuntime:
|
|
|
816
828
|
|
|
817
829
|
logger.debug("All primitives initialized")
|
|
818
830
|
|
|
819
|
-
def resolve_toolset(self, name: str) ->
|
|
831
|
+
def resolve_toolset(self, name: str) -> Any | None:
|
|
820
832
|
"""
|
|
821
833
|
Resolve a toolset by name from runtime's registered toolsets.
|
|
822
834
|
|
|
@@ -1027,7 +1039,7 @@ class TactusRuntime:
|
|
|
1027
1039
|
for name, toolset in self.toolset_registry.items():
|
|
1028
1040
|
logger.debug(f" - {name}: {type(toolset)} -> {toolset}")
|
|
1029
1041
|
|
|
1030
|
-
async def _resolve_tool_source(self, tool_name: str, source: str) ->
|
|
1042
|
+
async def _resolve_tool_source(self, tool_name: str, source: str) -> Any | None:
|
|
1031
1043
|
"""
|
|
1032
1044
|
Resolve a tool from an external source.
|
|
1033
1045
|
|
|
@@ -1102,34 +1114,38 @@ class TactusRuntime:
|
|
|
1102
1114
|
plugin_path = source[7:] # Remove "plugin." prefix
|
|
1103
1115
|
try:
|
|
1104
1116
|
# Split the plugin path into module and function
|
|
1105
|
-
|
|
1106
|
-
if len(
|
|
1117
|
+
path_segments = plugin_path.rsplit(".", 1)
|
|
1118
|
+
if len(path_segments) != 2:
|
|
1107
1119
|
logger.error(
|
|
1108
1120
|
f"Invalid plugin path format: {source} (expected plugin.module.function)"
|
|
1109
1121
|
)
|
|
1110
1122
|
return None
|
|
1111
1123
|
|
|
1112
|
-
module_name,
|
|
1124
|
+
module_name, function_name = path_segments
|
|
1113
1125
|
|
|
1114
1126
|
# Try to import the module
|
|
1115
1127
|
import importlib
|
|
1116
1128
|
|
|
1117
1129
|
try:
|
|
1118
|
-
|
|
1130
|
+
module_object = importlib.import_module(module_name)
|
|
1119
1131
|
except ModuleNotFoundError:
|
|
1120
1132
|
# Try with "tactus.plugins." prefix
|
|
1121
1133
|
try:
|
|
1122
|
-
|
|
1134
|
+
module_object = importlib.import_module(f"tactus.plugins.{module_name}")
|
|
1123
1135
|
except ModuleNotFoundError:
|
|
1124
1136
|
logger.error(f"Plugin module not found: {module_name}")
|
|
1125
1137
|
return None
|
|
1126
1138
|
|
|
1127
1139
|
# Get the function from the module
|
|
1128
|
-
if not hasattr(
|
|
1129
|
-
logger.error(
|
|
1140
|
+
if not hasattr(module_object, function_name):
|
|
1141
|
+
logger.error(
|
|
1142
|
+
"Function '%s' not found in module '%s'",
|
|
1143
|
+
function_name,
|
|
1144
|
+
module_name,
|
|
1145
|
+
)
|
|
1130
1146
|
return None
|
|
1131
1147
|
|
|
1132
|
-
|
|
1148
|
+
tool_function = getattr(module_object, function_name)
|
|
1133
1149
|
|
|
1134
1150
|
# Create a toolset with the plugin tool
|
|
1135
1151
|
from pydantic_ai.toolsets import FunctionToolset
|
|
@@ -1154,7 +1170,7 @@ class TactusRuntime:
|
|
|
1154
1170
|
return mock_result
|
|
1155
1171
|
|
|
1156
1172
|
# Call the plugin function
|
|
1157
|
-
result =
|
|
1173
|
+
result = tool_function(**kwargs)
|
|
1158
1174
|
logger.debug(f"Plugin tool '{tool_name}' returned: {result}")
|
|
1159
1175
|
|
|
1160
1176
|
# Track the call
|
|
@@ -1168,13 +1184,18 @@ class TactusRuntime:
|
|
|
1168
1184
|
# Copy metadata
|
|
1169
1185
|
tracked_plugin_tool.__name__ = tool_name
|
|
1170
1186
|
tracked_plugin_tool.__doc__ = getattr(
|
|
1171
|
-
|
|
1187
|
+
tool_function, "__doc__", f"Plugin tool: {tool_name}"
|
|
1172
1188
|
)
|
|
1173
1189
|
|
|
1174
1190
|
# Create and return toolset
|
|
1175
1191
|
wrapped_tool = Tool(tracked_plugin_tool, name=tool_name)
|
|
1176
1192
|
toolset = FunctionToolset(tools=[wrapped_tool])
|
|
1177
|
-
logger.info(
|
|
1193
|
+
logger.info(
|
|
1194
|
+
"Loaded plugin tool '%s' from %s.%s",
|
|
1195
|
+
tool_name,
|
|
1196
|
+
module_name,
|
|
1197
|
+
function_name,
|
|
1198
|
+
)
|
|
1178
1199
|
return toolset
|
|
1179
1200
|
|
|
1180
1201
|
except Exception as e:
|
|
@@ -1209,7 +1230,7 @@ class TactusRuntime:
|
|
|
1209
1230
|
return mock_result
|
|
1210
1231
|
|
|
1211
1232
|
# Build command line
|
|
1212
|
-
|
|
1233
|
+
command_arguments = [cli_command]
|
|
1213
1234
|
|
|
1214
1235
|
# Add arguments from kwargs
|
|
1215
1236
|
# Common patterns:
|
|
@@ -1220,23 +1241,23 @@ class TactusRuntime:
|
|
|
1220
1241
|
for key, value in kwargs.items():
|
|
1221
1242
|
if key == "args" and isinstance(value, list):
|
|
1222
1243
|
# Positional arguments
|
|
1223
|
-
|
|
1244
|
+
command_arguments.extend(value)
|
|
1224
1245
|
elif isinstance(value, bool):
|
|
1225
1246
|
if value:
|
|
1226
1247
|
# Boolean flag
|
|
1227
1248
|
flag = f"--{key.replace('_', '-')}"
|
|
1228
|
-
|
|
1249
|
+
command_arguments.append(flag)
|
|
1229
1250
|
elif value is not None:
|
|
1230
1251
|
# Key-value argument
|
|
1231
1252
|
flag = f"--{key.replace('_', '-')}"
|
|
1232
|
-
|
|
1253
|
+
command_arguments.extend([flag, str(value)])
|
|
1233
1254
|
|
|
1234
|
-
logger.debug(
|
|
1255
|
+
logger.debug("Executing CLI command: %s", " ".join(command_arguments))
|
|
1235
1256
|
|
|
1236
1257
|
try:
|
|
1237
1258
|
# Execute the command
|
|
1238
|
-
|
|
1239
|
-
|
|
1259
|
+
command_result = subprocess.run(
|
|
1260
|
+
command_arguments,
|
|
1240
1261
|
capture_output=True,
|
|
1241
1262
|
text=True,
|
|
1242
1263
|
check=False,
|
|
@@ -1244,31 +1265,31 @@ class TactusRuntime:
|
|
|
1244
1265
|
)
|
|
1245
1266
|
|
|
1246
1267
|
# Prepare response
|
|
1247
|
-
|
|
1248
|
-
"stdout":
|
|
1249
|
-
"stderr":
|
|
1250
|
-
"returncode":
|
|
1251
|
-
"success":
|
|
1268
|
+
command_response = {
|
|
1269
|
+
"stdout": command_result.stdout,
|
|
1270
|
+
"stderr": command_result.stderr,
|
|
1271
|
+
"returncode": command_result.returncode,
|
|
1272
|
+
"success": command_result.returncode == 0,
|
|
1252
1273
|
}
|
|
1253
1274
|
|
|
1254
1275
|
# Try to parse JSON output if possible
|
|
1255
|
-
if
|
|
1276
|
+
if command_result.stdout.strip().startswith(
|
|
1256
1277
|
"{"
|
|
1257
|
-
) or
|
|
1278
|
+
) or command_result.stdout.strip().startswith("["):
|
|
1258
1279
|
try:
|
|
1259
|
-
|
|
1280
|
+
command_response["json"] = json.loads(command_result.stdout)
|
|
1260
1281
|
except json.JSONDecodeError:
|
|
1261
1282
|
pass
|
|
1262
1283
|
|
|
1263
|
-
logger.debug(
|
|
1284
|
+
logger.debug("CLI tool '%s' returned: %s", tool_name, command_response)
|
|
1264
1285
|
|
|
1265
1286
|
# Track the call
|
|
1266
1287
|
if tool_primitive:
|
|
1267
|
-
tool_primitive.record_call(tool_name, kwargs,
|
|
1288
|
+
tool_primitive.record_call(tool_name, kwargs, command_response)
|
|
1268
1289
|
if self.mock_manager:
|
|
1269
|
-
self.mock_manager.record_call(tool_name, kwargs,
|
|
1290
|
+
self.mock_manager.record_call(tool_name, kwargs, command_response)
|
|
1270
1291
|
|
|
1271
|
-
return
|
|
1292
|
+
return command_response
|
|
1272
1293
|
|
|
1273
1294
|
except subprocess.TimeoutExpired:
|
|
1274
1295
|
error_response = {
|
|
@@ -1364,49 +1385,56 @@ class TactusRuntime:
|
|
|
1364
1385
|
|
|
1365
1386
|
from tactus.primitives.procedure_callable import ProcedureCallable
|
|
1366
1387
|
|
|
1367
|
-
for
|
|
1388
|
+
for procedure_name, procedure_definition in self.registry.named_procedures.items():
|
|
1368
1389
|
try:
|
|
1369
1390
|
logger.debug(
|
|
1370
|
-
|
|
1371
|
-
|
|
1391
|
+
"Processing named procedure '%s': function=%s, type=%s",
|
|
1392
|
+
procedure_name,
|
|
1393
|
+
procedure_definition["function"],
|
|
1394
|
+
type(procedure_definition["function"]),
|
|
1372
1395
|
)
|
|
1373
1396
|
|
|
1374
1397
|
# Create callable wrapper
|
|
1375
1398
|
callable_wrapper = ProcedureCallable(
|
|
1376
|
-
name=
|
|
1377
|
-
procedure_function=
|
|
1378
|
-
input_schema=
|
|
1379
|
-
output_schema=
|
|
1380
|
-
state_schema=
|
|
1399
|
+
name=procedure_name,
|
|
1400
|
+
procedure_function=procedure_definition["function"],
|
|
1401
|
+
input_schema=procedure_definition["input_schema"],
|
|
1402
|
+
output_schema=procedure_definition["output_schema"],
|
|
1403
|
+
state_schema=procedure_definition["state_schema"],
|
|
1381
1404
|
execution_context=self.execution_context,
|
|
1382
1405
|
lua_sandbox=self.lua_sandbox,
|
|
1383
1406
|
)
|
|
1384
1407
|
|
|
1385
1408
|
# Get the old stub (if it exists) to update its registry
|
|
1386
1409
|
try:
|
|
1387
|
-
old_value = self.lua_sandbox.lua.globals()[
|
|
1410
|
+
old_value = self.lua_sandbox.lua.globals()[procedure_name]
|
|
1388
1411
|
if old_value and hasattr(old_value, "registry"):
|
|
1389
1412
|
# Update the stub's registry so it delegates to the real callable
|
|
1390
|
-
old_value.registry[
|
|
1413
|
+
old_value.registry[procedure_name] = callable_wrapper
|
|
1391
1414
|
except (KeyError, AttributeError):
|
|
1392
1415
|
# Stub doesn't exist in globals yet, that's fine
|
|
1393
1416
|
pass
|
|
1394
1417
|
|
|
1395
1418
|
# Inject into Lua globals (replaces placeholder)
|
|
1396
|
-
self.lua_sandbox.lua.globals()[
|
|
1419
|
+
self.lua_sandbox.lua.globals()[procedure_name] = callable_wrapper
|
|
1397
1420
|
|
|
1398
|
-
logger.info(
|
|
1421
|
+
logger.info("Registered named procedure: %s", procedure_name)
|
|
1399
1422
|
except Exception as e:
|
|
1400
1423
|
logger.error(
|
|
1401
|
-
|
|
1424
|
+
"Failed to initialize named procedure '%s': %s",
|
|
1425
|
+
procedure_name,
|
|
1426
|
+
e,
|
|
1402
1427
|
exc_info=True,
|
|
1403
1428
|
)
|
|
1404
1429
|
|
|
1405
|
-
logger.info(
|
|
1430
|
+
logger.info(
|
|
1431
|
+
"Initialized %s named procedure(s)",
|
|
1432
|
+
len(self.registry.named_procedures),
|
|
1433
|
+
)
|
|
1406
1434
|
|
|
1407
1435
|
async def _create_toolset_from_config(
|
|
1408
|
-
self, name: str, definition:
|
|
1409
|
-
) ->
|
|
1436
|
+
self, name: str, definition: dict[str, Any]
|
|
1437
|
+
) -> Any | None:
|
|
1410
1438
|
"""
|
|
1411
1439
|
Create toolset from YAML config definition.
|
|
1412
1440
|
|
|
@@ -1481,7 +1509,9 @@ class TactusRuntime:
|
|
|
1481
1509
|
|
|
1482
1510
|
if pattern:
|
|
1483
1511
|
# Filter by regex pattern
|
|
1484
|
-
return source_toolset.filtered(
|
|
1512
|
+
return source_toolset.filtered(
|
|
1513
|
+
lambda execution_context, tool: re.match(pattern, tool.name)
|
|
1514
|
+
)
|
|
1485
1515
|
else:
|
|
1486
1516
|
logger.warning(f"Filtered toolset '{name}' has no filter pattern")
|
|
1487
1517
|
return source_toolset
|
|
@@ -1685,13 +1715,17 @@ class TactusRuntime:
|
|
|
1685
1715
|
if "include" in expr:
|
|
1686
1716
|
# Filter to specific tools
|
|
1687
1717
|
tool_names = set(expr["include"])
|
|
1688
|
-
toolset = toolset.filtered(
|
|
1718
|
+
toolset = toolset.filtered(
|
|
1719
|
+
lambda execution_context, tool: tool.name in tool_names
|
|
1720
|
+
)
|
|
1689
1721
|
logger.debug(f"Applied include filter to toolset '{name}': {tool_names}")
|
|
1690
1722
|
|
|
1691
1723
|
if "exclude" in expr:
|
|
1692
1724
|
# Exclude specific tools
|
|
1693
1725
|
tool_names = set(expr["exclude"])
|
|
1694
|
-
toolset = toolset.filtered(
|
|
1726
|
+
toolset = toolset.filtered(
|
|
1727
|
+
lambda execution_context, tool: tool.name not in tool_names
|
|
1728
|
+
)
|
|
1695
1729
|
logger.debug(f"Applied exclude filter to toolset '{name}': {tool_names}")
|
|
1696
1730
|
|
|
1697
1731
|
if "prefix" in expr:
|
|
@@ -1748,7 +1782,7 @@ class TactusRuntime:
|
|
|
1748
1782
|
logger.error(f"Failed to initialize dependencies: {e}")
|
|
1749
1783
|
raise RuntimeError(f"Dependency initialization failed: {e}")
|
|
1750
1784
|
|
|
1751
|
-
async def _setup_agents(self, context:
|
|
1785
|
+
async def _setup_agents(self, context: dict[str, Any]):
|
|
1752
1786
|
"""
|
|
1753
1787
|
Setup agent primitives with LLMs and tools using Pydantic AI.
|
|
1754
1788
|
|
|
@@ -2110,7 +2144,7 @@ class TactusRuntime:
|
|
|
2110
2144
|
logger.error(f"Failed to setup model '{model_name}': {e}")
|
|
2111
2145
|
raise
|
|
2112
2146
|
|
|
2113
|
-
def _create_pydantic_model_from_output(self, output_schema, model_name: str) -> type:
|
|
2147
|
+
def _create_pydantic_model_from_output(self, output_schema: Any, model_name: str) -> type:
|
|
2114
2148
|
"""
|
|
2115
2149
|
Convert output schema to Pydantic model.
|
|
2116
2150
|
|
|
@@ -2124,7 +2158,6 @@ class TactusRuntime:
|
|
|
2124
2158
|
Dynamically created Pydantic model class
|
|
2125
2159
|
"""
|
|
2126
2160
|
from pydantic import create_model
|
|
2127
|
-
from typing import Optional
|
|
2128
2161
|
|
|
2129
2162
|
fields = {}
|
|
2130
2163
|
|
|
@@ -2139,21 +2172,21 @@ class TactusRuntime:
|
|
|
2139
2172
|
# Extract field properties
|
|
2140
2173
|
if hasattr(field_def, "type"):
|
|
2141
2174
|
field_type_str = field_def.type
|
|
2142
|
-
|
|
2175
|
+
is_required = getattr(field_def, "required", True)
|
|
2143
2176
|
else:
|
|
2144
2177
|
# Fields from registry are plain dicts (FieldDefinition type is lost)
|
|
2145
2178
|
# Trust that they were created with field builders
|
|
2146
2179
|
field_type_str = field_def.get("type", "string")
|
|
2147
|
-
|
|
2180
|
+
is_required = field_def.get("required", True)
|
|
2148
2181
|
|
|
2149
2182
|
# Map type string to Python type
|
|
2150
2183
|
field_type = self._map_type_string(field_type_str)
|
|
2151
2184
|
|
|
2152
2185
|
# Create field tuple (type, default_or_required)
|
|
2153
|
-
if
|
|
2186
|
+
if is_required:
|
|
2154
2187
|
fields[field_name] = (field_type, ...) # Required field
|
|
2155
2188
|
else:
|
|
2156
|
-
fields[field_name] = (
|
|
2189
|
+
fields[field_name] = (field_type | None, None) # Optional field
|
|
2157
2190
|
|
|
2158
2191
|
return create_model(model_name, **fields)
|
|
2159
2192
|
|
|
@@ -2176,7 +2209,7 @@ class TactusRuntime:
|
|
|
2176
2209
|
return type_map.get(type_str.lower(), str)
|
|
2177
2210
|
|
|
2178
2211
|
def _create_output_model_from_schema(
|
|
2179
|
-
self, output_schema:
|
|
2212
|
+
self, output_schema: dict[str, Any], model_name: str = "OutputModel"
|
|
2180
2213
|
) -> type:
|
|
2181
2214
|
"""
|
|
2182
2215
|
Create a Pydantic model from output schema definition.
|
|
@@ -2334,14 +2367,14 @@ class TactusRuntime:
|
|
|
2334
2367
|
if isinstance(value, list):
|
|
2335
2368
|
# Convert Python list to Lua table (1-indexed)
|
|
2336
2369
|
lua_table = self.lua_sandbox.lua.table()
|
|
2337
|
-
for
|
|
2338
|
-
lua_table[
|
|
2370
|
+
for index, item in enumerate(value, 1):
|
|
2371
|
+
lua_table[index] = convert_to_lua(item)
|
|
2339
2372
|
return lua_table
|
|
2340
2373
|
elif isinstance(value, dict):
|
|
2341
2374
|
# Convert Python dict to Lua table
|
|
2342
2375
|
lua_table = self.lua_sandbox.lua.table()
|
|
2343
|
-
for
|
|
2344
|
-
lua_table[
|
|
2376
|
+
for key, item in value.items():
|
|
2377
|
+
lua_table[key] = convert_to_lua(item)
|
|
2345
2378
|
return lua_table
|
|
2346
2379
|
else:
|
|
2347
2380
|
return value
|
|
@@ -2381,7 +2414,8 @@ class TactusRuntime:
|
|
|
2381
2414
|
self.lua_sandbox.inject_primitive("_python_checkpoint", self.step_primitive.checkpoint)
|
|
2382
2415
|
|
|
2383
2416
|
# Create Lua wrapper that captures source location before calling Python
|
|
2384
|
-
self.lua_sandbox.lua.execute(
|
|
2417
|
+
self.lua_sandbox.lua.execute(
|
|
2418
|
+
"""
|
|
2385
2419
|
function checkpoint(fn)
|
|
2386
2420
|
-- Capture caller's source location (2 levels up: this wrapper -> caller)
|
|
2387
2421
|
local info = debug.getinfo(2, 'Sl')
|
|
@@ -2397,7 +2431,8 @@ class TactusRuntime:
|
|
|
2397
2431
|
return _python_checkpoint(fn, nil)
|
|
2398
2432
|
end
|
|
2399
2433
|
end
|
|
2400
|
-
"""
|
|
2434
|
+
"""
|
|
2435
|
+
)
|
|
2401
2436
|
logger.debug("Checkpoint wrapper injected with Lua source location tracking")
|
|
2402
2437
|
|
|
2403
2438
|
if self.checkpoint_primitive:
|
|
@@ -2715,7 +2750,7 @@ class TactusRuntime:
|
|
|
2715
2750
|
|
|
2716
2751
|
return transformed
|
|
2717
2752
|
|
|
2718
|
-
def _process_template(self, template: str, context:
|
|
2753
|
+
def _process_template(self, template: str, context: dict[str, Any]) -> str:
|
|
2719
2754
|
"""
|
|
2720
2755
|
Process a template string with variable substitution.
|
|
2721
2756
|
|
|
@@ -2733,14 +2768,14 @@ class TactusRuntime:
|
|
|
2733
2768
|
class DotFormatter(Formatter):
|
|
2734
2769
|
def get_field(self, field_name, args, kwargs):
|
|
2735
2770
|
# Support dot notation like {params.topic}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
for part in
|
|
2739
|
-
if isinstance(
|
|
2740
|
-
|
|
2771
|
+
path_parts = field_name.split(".")
|
|
2772
|
+
current_value = kwargs
|
|
2773
|
+
for part in path_parts:
|
|
2774
|
+
if isinstance(current_value, dict):
|
|
2775
|
+
current_value = current_value.get(part, "")
|
|
2741
2776
|
else:
|
|
2742
|
-
|
|
2743
|
-
return
|
|
2777
|
+
current_value = getattr(current_value, part, "")
|
|
2778
|
+
return current_value, field_name
|
|
2744
2779
|
|
|
2745
2780
|
template_vars = {}
|
|
2746
2781
|
|
|
@@ -2763,15 +2798,15 @@ class TactusRuntime:
|
|
|
2763
2798
|
|
|
2764
2799
|
# Use dot-notation formatter
|
|
2765
2800
|
formatter = DotFormatter()
|
|
2766
|
-
|
|
2767
|
-
return
|
|
2801
|
+
resolved_template = formatter.format(template, **template_vars)
|
|
2802
|
+
return resolved_template
|
|
2768
2803
|
|
|
2769
|
-
except KeyError as
|
|
2770
|
-
logger.warning(f"Template variable {
|
|
2804
|
+
except KeyError as exception:
|
|
2805
|
+
logger.warning(f"Template variable {exception} not found, using template as-is")
|
|
2771
2806
|
return template
|
|
2772
2807
|
|
|
2773
|
-
except Exception as
|
|
2774
|
-
logger.error(f"Error processing template: {
|
|
2808
|
+
except Exception as exception:
|
|
2809
|
+
logger.error(f"Error processing template: {exception}")
|
|
2775
2810
|
return template
|
|
2776
2811
|
|
|
2777
2812
|
def _format_output_schema_for_prompt(self) -> str:
|
|
@@ -2809,7 +2844,7 @@ class TactusRuntime:
|
|
|
2809
2844
|
|
|
2810
2845
|
return "\n".join(lines)
|
|
2811
2846
|
|
|
2812
|
-
def get_state(self) ->
|
|
2847
|
+
def get_state(self) -> dict[str, Any]:
|
|
2813
2848
|
"""Get current procedure state."""
|
|
2814
2849
|
if self.state_primitive:
|
|
2815
2850
|
return self.state_primitive.all()
|
|
@@ -2828,7 +2863,7 @@ class TactusRuntime:
|
|
|
2828
2863
|
return False
|
|
2829
2864
|
|
|
2830
2865
|
def _parse_declarations(
|
|
2831
|
-
self, source: str, tool_primitive:
|
|
2866
|
+
self, source: str, tool_primitive: ToolPrimitive | None = None
|
|
2832
2867
|
) -> ProcedureRegistry:
|
|
2833
2868
|
"""
|
|
2834
2869
|
Execute .tac to collect declarations.
|
|
@@ -2940,7 +2975,7 @@ class TactusRuntime:
|
|
|
2940
2975
|
logger.debug(f"Registry after parsing: lua_tools={list(result.registry.lua_tools.keys())}")
|
|
2941
2976
|
return result.registry
|
|
2942
2977
|
|
|
2943
|
-
def _registry_to_config(self, registry: ProcedureRegistry) ->
|
|
2978
|
+
def _registry_to_config(self, registry: ProcedureRegistry) -> dict[str, Any]:
|
|
2944
2979
|
"""
|
|
2945
2980
|
Convert registry to config dict format.
|
|
2946
2981
|
|
|
@@ -3049,7 +3084,7 @@ class TactusRuntime:
|
|
|
3049
3084
|
return config
|
|
3050
3085
|
|
|
3051
3086
|
def _create_runtime_for_procedure(
|
|
3052
|
-
self, procedure_name: str, params:
|
|
3087
|
+
self, procedure_name: str, params: dict[str, Any]
|
|
3053
3088
|
) -> "TactusRuntime":
|
|
3054
3089
|
"""
|
|
3055
3090
|
Create a new runtime instance for a sub-procedure.
|