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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +40 -25
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
  81. {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 Dict, Any, Optional
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: Optional[StorageBackend] = None,
72
- hitl_handler: Optional[HITLHandler] = None,
73
- chat_recorder: Optional[ChatRecorder] = None,
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: Optional[Dict[str, Any]] = None,
76
- openai_api_key: Optional[str] = None,
75
+ mcp_servers: dict[str, Any] | None = None,
76
+ openai_api_key: str | None = None,
77
77
  log_handler=None,
78
- tool_primitive: Optional[ToolPrimitive] = None,
78
+ tool_primitive: ToolPrimitive | None = None,
79
79
  recursion_depth: int = 0,
80
- tool_paths: Optional[list] = None,
81
- external_config: Optional[Dict[str, Any]] = None,
82
- run_id: Optional[str] = None,
83
- source_file_path: Optional[str] = None,
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(f"Auto-configured ControlLoopHandler with {len(channels)} channel(s)")
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: Optional[Dict[str, Any]] = None # Legacy YAML support
145
- self.registry: Optional[ProcedureRegistry] = None # New DSL registry
146
- self.lua_sandbox: Optional[LuaSandbox] = None
147
- self.output_validator: Optional[OutputValidator] = None
148
- self.template_resolver: Optional[TemplateResolver] = None
149
- self.message_history_manager: Optional[MessageHistoryManager] = None
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: Optional[BaseExecutionContext] = None
155
+ self.execution_context: BaseExecutionContext | None = None
153
156
 
154
157
  # Primitives (shared across all agents)
155
- self.state_primitive: Optional[StatePrimitive] = None
156
- self.iterations_primitive: Optional[IterationsPrimitive] = None
157
- self.stop_primitive: Optional[StopPrimitive] = None
158
- self.tool_primitive: Optional[ToolPrimitive] = None
159
- self.human_primitive: Optional[HumanPrimitive] = None
160
- self.step_primitive: Optional[StepPrimitive] = None
161
- self.checkpoint_primitive: Optional[CheckpointPrimitive] = None
162
- self.log_primitive: Optional[LogPrimitive] = None
163
- self.json_primitive: Optional[JsonPrimitive] = None
164
- self.retry_primitive: Optional[RetryPrimitive] = None
165
- self.file_primitive: Optional[FilePrimitive] = None
166
- self.procedure_primitive: Optional[ProcedurePrimitive] = None
167
- self.system_primitive: Optional[SystemPrimitive] = None
168
- self.host_primitive: Optional[HostPrimitive] = None
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: Dict[str, Any] = {}
174
+ self.agents: dict[str, Any] = {}
172
175
 
173
176
  # Model primitives (one per model)
174
- self.models: Dict[str, Any] = {}
177
+ self.models: dict[str, Any] = {}
175
178
 
176
179
  # Toolset registry (name -> AbstractToolset instance)
177
- self.toolset_registry: Dict[str, Any] = {}
180
+ self.toolset_registry: dict[str, Any] = {}
178
181
 
179
182
  # User dependencies (HTTP clients, DB connections, etc.)
180
- self.user_dependencies: Dict[str, Any] = {}
181
- self.dependency_manager: Optional[Any] = None # ResourceManager for cleanup
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: Optional[Any] = None # MockManager instance
185
- self.external_agent_mocks: Optional[dict[str, list[dict[str, Any]]]] = None
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(f"TactusRuntime initialized for procedure {procedure_id}")
191
+ logger.info("TactusRuntime initialized for procedure %s", procedure_id)
189
192
 
190
193
  async def execute(
191
- self, source: str, context: Optional[Dict[str, Any]] = None, format: str = "yaml"
192
- ) -> Dict[str, Any]:
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
- session_id = None
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
- f"Using source file directory as sandbox base_path: {sandbox_base_path}"
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(f"[CHECKPOINT] Set .tac file path EARLY: {self.source_file_path}")
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
- placeholder_tool = None # Will be set for Lua DSL
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
- placeholder_tool = self._injected_tool_primitive
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
- placeholder_tool = LuaToolPrimitive(
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
- self.lua_sandbox.inject_primitive("Tool", placeholder_tool)
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, placeholder_tool)
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
- f"Registry contents: agents={list(self.registry.agents.keys())}, lua_tools={list(self.registry.lua_tools.keys())}"
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(f"Merged external config with {len(self.external_config)} keys")
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(f"Loaded procedure: {self.config['name']} v{self.config['version']}")
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
- f"Output schema has {len(output_schema)} fields: {list(output_schema.keys())}"
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=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
- session_id = await self.chat_recorder.start_session(context)
431
- if session_id:
432
- logger.info(f"Chat session started: {session_id}")
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(f"Return prompt specified: {return_prompt[:50]}...")
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 session_id:
528
- await self.chat_recorder.end_session(session_id, status="COMPLETED")
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
- f"Workflow execution complete: "
540
- f"{self.iterations_primitive.current() if self.iterations_primitive else 0} iterations, "
541
- f"{len(tools_used)} tool calls"
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": 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(f"Procedure waiting for human: {e}")
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": session_id,
649
+ "session_id": chat_session_id,
636
650
  }
637
651
 
638
652
  except ProcedureConfigError as e:
639
- logger.error(f"Configuration error: {e}")
653
+ logger.error("Configuration error: %s", e)
640
654
  # Flush recordings even on error
641
- if self.chat_recorder and session_id:
655
+ if self.chat_recorder and chat_session_id:
642
656
  try:
643
- await self.chat_recorder.end_session(session_id, status="FAILED")
657
+ await self.chat_recorder.end_session(chat_session_id, status="FAILED")
644
658
  except Exception as err:
645
- logger.warning(f"Failed to end chat session: {err}")
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(f"Lua execution error: {e}")
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(f"Error prompt specified: {error_prompt[:50]}...")
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 session_id:
698
+ if self.chat_recorder and chat_session_id:
685
699
  try:
686
- await self.chat_recorder.end_session(session_id, status="FAILED")
700
+ await self.chat_recorder.end_session(chat_session_id, status="FAILED")
687
701
  except Exception as err:
688
- logger.warning(f"Failed to end chat session: {err}")
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(f"Unexpected error: {e}", exc_info=True)
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(f"Error prompt specified: {error_prompt[:50]}...")
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 session_id:
741
+ if self.chat_recorder and chat_session_id:
733
742
  try:
734
- await self.chat_recorder.end_session(session_id, status="FAILED")
743
+ await self.chat_recorder.end_session(chat_session_id, status="FAILED")
735
744
  except Exception as err:
736
- logger.warning(f"Failed to end chat session: {err}")
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(f"Error disconnecting from MCP servers: {e}")
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(f"Error cleaning up dependencies: {e}")
789
+ logger.warning("Error cleaning up dependencies: %s", e)
781
790
 
782
- async def _initialize_primitives(self, placeholder_tool: Optional[ToolPrimitive] = None):
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 placeholder_tool so direct tool calls from ToolHandles are tracked
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) -> Optional[Any]:
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) -> Optional[Any]:
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
- parts = plugin_path.rsplit(".", 1)
1106
- if len(parts) != 2:
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, func_name = parts
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
- module = importlib.import_module(module_name)
1130
+ module_object = importlib.import_module(module_name)
1119
1131
  except ModuleNotFoundError:
1120
1132
  # Try with "tactus.plugins." prefix
1121
1133
  try:
1122
- module = importlib.import_module(f"tactus.plugins.{module_name}")
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(module, func_name):
1129
- logger.error(f"Function '{func_name}' not found in module '{module_name}'")
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
- tool_func = getattr(module, func_name)
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 = tool_func(**kwargs)
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
- tool_func, "__doc__", f"Plugin tool: {tool_name}"
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(f"Loaded plugin tool '{tool_name}' from {module_name}.{func_name}")
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
- cmd = [cli_command]
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
- cmd.extend(value)
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
- cmd.append(flag)
1249
+ command_arguments.append(flag)
1229
1250
  elif value is not None:
1230
1251
  # Key-value argument
1231
1252
  flag = f"--{key.replace('_', '-')}"
1232
- cmd.extend([flag, str(value)])
1253
+ command_arguments.extend([flag, str(value)])
1233
1254
 
1234
- logger.debug(f"Executing CLI command: {' '.join(cmd)}")
1255
+ logger.debug("Executing CLI command: %s", " ".join(command_arguments))
1235
1256
 
1236
1257
  try:
1237
1258
  # Execute the command
1238
- result = subprocess.run(
1239
- cmd,
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
- response = {
1248
- "stdout": result.stdout,
1249
- "stderr": result.stderr,
1250
- "returncode": result.returncode,
1251
- "success": result.returncode == 0,
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 result.stdout.strip().startswith(
1276
+ if command_result.stdout.strip().startswith(
1256
1277
  "{"
1257
- ) or result.stdout.strip().startswith("["):
1278
+ ) or command_result.stdout.strip().startswith("["):
1258
1279
  try:
1259
- response["json"] = json.loads(result.stdout)
1280
+ command_response["json"] = json.loads(command_result.stdout)
1260
1281
  except json.JSONDecodeError:
1261
1282
  pass
1262
1283
 
1263
- logger.debug(f"CLI tool '{tool_name}' returned: {response}")
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, response)
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, response)
1290
+ self.mock_manager.record_call(tool_name, kwargs, command_response)
1270
1291
 
1271
- return response
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 proc_name, proc_def in self.registry.named_procedures.items():
1388
+ for procedure_name, procedure_definition in self.registry.named_procedures.items():
1368
1389
  try:
1369
1390
  logger.debug(
1370
- f"Processing named procedure '{proc_name}': "
1371
- f"function={proc_def['function']}, type={type(proc_def['function'])}"
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=proc_name,
1377
- procedure_function=proc_def["function"],
1378
- input_schema=proc_def["input_schema"],
1379
- output_schema=proc_def["output_schema"],
1380
- state_schema=proc_def["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()[proc_name]
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[proc_name] = callable_wrapper
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()[proc_name] = callable_wrapper
1419
+ self.lua_sandbox.lua.globals()[procedure_name] = callable_wrapper
1397
1420
 
1398
- logger.info(f"Registered named procedure: {proc_name}")
1421
+ logger.info("Registered named procedure: %s", procedure_name)
1399
1422
  except Exception as e:
1400
1423
  logger.error(
1401
- f"Failed to initialize named procedure '{proc_name}': {e}",
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(f"Initialized {len(self.registry.named_procedures)} named procedure(s)")
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: Dict[str, Any]
1409
- ) -> Optional[Any]:
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(lambda ctx, tool: re.match(pattern, tool.name))
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(lambda ctx, tool: tool.name in tool_names)
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(lambda ctx, tool: tool.name not in tool_names)
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: Dict[str, Any]):
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
- required = getattr(field_def, "required", True)
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
- required = field_def.get("required", True)
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 required:
2186
+ if is_required:
2154
2187
  fields[field_name] = (field_type, ...) # Required field
2155
2188
  else:
2156
- fields[field_name] = (Optional[field_type], None) # Optional field
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: Dict[str, Any], model_name: str = "OutputModel"
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 i, item in enumerate(value, 1):
2338
- lua_table[i] = convert_to_lua(item)
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 k, v in value.items():
2344
- lua_table[k] = convert_to_lua(v)
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: Dict[str, Any]) -> str:
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
- parts = field_name.split(".")
2737
- obj = kwargs
2738
- for part in parts:
2739
- if isinstance(obj, dict):
2740
- obj = obj.get(part, "")
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
- obj = getattr(obj, part, "")
2743
- return obj, field_name
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
- result = formatter.format(template, **template_vars)
2767
- return result
2801
+ resolved_template = formatter.format(template, **template_vars)
2802
+ return resolved_template
2768
2803
 
2769
- except KeyError as e:
2770
- logger.warning(f"Template variable {e} not found, using template as-is")
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 e:
2774
- logger.error(f"Error processing template: {e}")
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) -> Dict[str, Any]:
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: Optional[ToolPrimitive] = None
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) -> Dict[str, Any]:
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: Dict[str, Any]
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.