tactus 0.36.0__py3-none-any.whl → 0.37.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/core/registry.py CHANGED
@@ -6,7 +6,7 @@ procedure declarations from .tac files.
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Any
9
+ from typing import Any, Dict, Optional, Union
10
10
 
11
11
  from pydantic import BaseModel, Field, ValidationError, ConfigDict
12
12
 
@@ -19,7 +19,7 @@ class OutputFieldDeclaration(BaseModel):
19
19
  name: str
20
20
  field_type: str = Field(alias="type") # string, number, boolean, array, object
21
21
  required: bool = False
22
- description: str | None = None
22
+ description: Optional[str] = None
23
23
 
24
24
  model_config = ConfigDict(populate_by_name=True)
25
25
 
@@ -31,7 +31,7 @@ class MessageHistoryConfiguration(BaseModel):
31
31
  """
32
32
 
33
33
  source: str = "own" # "own", "shared", or another agent's name
34
- filter: Any | None = None # Lua function reference or filter name
34
+ filter: Optional[Any] = None # Lua function reference or filter name
35
35
 
36
36
 
37
37
  class AgentOutputSchema(BaseModel):
@@ -44,21 +44,21 @@ class AgentDeclaration(BaseModel):
44
44
  """Agent declaration from DSL."""
45
45
 
46
46
  name: str
47
- provider: str | None = None
48
- model: str | dict[str, Any] = "gpt-4o"
49
- system_prompt: str | Any # String with {markers} or Lua function
50
- initial_message: str | None = None
47
+ provider: Optional[str] = None
48
+ model: Union[str, Dict[str, Any]] = "gpt-4o"
49
+ system_prompt: Union[str, Any] # String with {markers} or Lua function
50
+ initial_message: Optional[str] = None
51
51
  tools: list[Any] = Field(default_factory=list) # Tool/toolset references and expressions
52
52
  inline_tools: list[dict[str, Any]] = Field(default_factory=list) # Inline tool definitions
53
- output: AgentOutputSchema | None = None # Aligned with pydantic-ai
54
- message_history: MessageHistoryConfiguration | None = None
53
+ output: Optional[AgentOutputSchema] = None # Aligned with pydantic-ai
54
+ message_history: Optional[MessageHistoryConfiguration] = None
55
55
  max_turns: int = 50
56
56
  disable_streaming: bool = (
57
57
  False # Disable streaming for models that don't support tools in streaming mode
58
58
  )
59
- temperature: float | None = None
60
- max_tokens: int | None = None
61
- model_type: str | None = None # e.g., "chat", "responses" for reasoning models
59
+ temperature: Optional[float] = None
60
+ max_tokens: Optional[int] = None
61
+ model_type: Optional[str] = None # e.g., "chat", "responses" for reasoning models
62
62
 
63
63
  model_config = ConfigDict(extra="allow")
64
64
 
@@ -69,9 +69,9 @@ class HITLDeclaration(BaseModel):
69
69
  name: str
70
70
  hitl_type: str = Field(alias="type") # approval, input, review
71
71
  message: str
72
- timeout: int | None = None
72
+ timeout: Optional[int] = None
73
73
  default: Any = None
74
- options: list[dict[str, Any]] | None = None
74
+ options: Optional[list[dict[str, Any]]] = None
75
75
 
76
76
  model_config = ConfigDict(populate_by_name=True)
77
77
 
@@ -81,9 +81,9 @@ class ScenarioDeclaration(BaseModel):
81
81
 
82
82
  name: str
83
83
  given: dict[str, Any] = Field(default_factory=dict)
84
- when: str | None = None # defaults to "procedure_completes"
85
- then_output: dict[str, Any] | None = None
86
- then_state: dict[str, Any] | None = None
84
+ when: Optional[str] = None # defaults to "procedure_completes"
85
+ then_output: Optional[Dict[str, Any]] = None
86
+ then_state: Optional[Dict[str, Any]] = None
87
87
  mocks: dict[str, Any] = Field(default_factory=dict) # tool_name -> response
88
88
 
89
89
 
@@ -134,7 +134,7 @@ class ProcedureRegistry(BaseModel):
134
134
  model_config = {"arbitrary_types_allowed": True}
135
135
 
136
136
  # Metadata
137
- description: str | None = None
137
+ description: Optional[str] = None
138
138
 
139
139
  # Declarations
140
140
  input_schema: dict[str, Any] = Field(default_factory=dict)
@@ -154,26 +154,26 @@ class ProcedureRegistry(BaseModel):
154
154
  message_history_config: dict[str, Any] = Field(default_factory=dict)
155
155
 
156
156
  # Gherkin BDD Testing
157
- gherkin_specifications: str | None = None # Raw Gherkin text
157
+ gherkin_specifications: Optional[str] = None # Raw Gherkin text
158
158
  specs_from_references: list[str] = Field(default_factory=list) # External spec file paths
159
159
  custom_steps: dict[str, Any] = Field(default_factory=dict) # step_text -> lua_function
160
160
  evaluation_config: dict[str, Any] = Field(default_factory=dict) # runs, parallel, etc.
161
161
 
162
162
  # Pydantic Evals Integration
163
- pydantic_evaluations: dict[str, Any] | None = None # Pydantic Evals configuration
163
+ pydantic_evaluations: Optional[Dict[str, Any]] = None # Pydantic Evals configuration
164
164
 
165
165
  # Prompts
166
166
  prompts: dict[str, str] = Field(default_factory=dict)
167
- return_prompt: str | None = None
168
- error_prompt: str | None = None
169
- status_prompt: str | None = None
167
+ return_prompt: Optional[str] = None
168
+ error_prompt: Optional[str] = None
169
+ status_prompt: Optional[str] = None
170
170
 
171
171
  # Execution settings
172
172
  async_enabled: bool = False
173
173
  max_depth: int = 5
174
174
  max_turns: int = 50
175
- default_provider: str | None = None
176
- default_model: str | None = None
175
+ default_provider: Optional[str] = None
176
+ default_model: Optional[str] = None
177
177
 
178
178
  # Named procedures (for in-file sub-procedures)
179
179
  named_procedures: dict[str, dict[str, Any]] = Field(default_factory=dict)
@@ -193,8 +193,8 @@ class ValidationMessage(BaseModel):
193
193
 
194
194
  level: str # "error" or "warning"
195
195
  message: str
196
- location: tuple[int, int] | None = None
197
- declaration: str | None = None
196
+ location: Optional[tuple[int, int]] = None
197
+ declaration: Optional[str] = None
198
198
 
199
199
 
200
200
  class ValidationResult(BaseModel):
@@ -203,7 +203,7 @@ class ValidationResult(BaseModel):
203
203
  valid: bool
204
204
  errors: list[ValidationMessage] = Field(default_factory=list)
205
205
  warnings: list[ValidationMessage] = Field(default_factory=list)
206
- registry: ProcedureRegistry | None = None
206
+ registry: Optional["ProcedureRegistry"] = None
207
207
 
208
208
 
209
209
  class RegistryBuilder:
@@ -229,7 +229,7 @@ class RegistryBuilder:
229
229
  self,
230
230
  name: str,
231
231
  config: dict,
232
- output_schema: dict | None = None,
232
+ output_schema: Optional[dict] = None,
233
233
  ) -> None:
234
234
  """Register an agent declaration."""
235
235
  agent_config = dict(config)
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 Any
16
+ from typing import Any, Dict, List, Optional
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: StorageBackend | None = None,
72
- hitl_handler: HITLHandler | None = None,
73
- chat_recorder: ChatRecorder | None = None,
71
+ storage_backend: Optional[StorageBackend] = None,
72
+ hitl_handler: Optional[HITLHandler] = None,
73
+ chat_recorder: Optional[ChatRecorder] = None,
74
74
  mcp_server=None,
75
- mcp_servers: dict[str, Any] | None = None,
76
- openai_api_key: str | None = None,
75
+ mcp_servers: Optional[Dict[str, Any]] = None,
76
+ openai_api_key: Optional[str] = None,
77
77
  log_handler=None,
78
- tool_primitive: ToolPrimitive | None = None,
78
+ tool_primitive: Optional[ToolPrimitive] = None,
79
79
  recursion_depth: int = 0,
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,
80
+ tool_paths: Optional[List[str]] = None,
81
+ external_config: Optional[Dict[str, Any]] = None,
82
+ run_id: Optional[str] = None,
83
+ source_file_path: Optional[str] = None,
84
84
  ):
85
85
  """
86
86
  Initialize the Tactus runtime.
@@ -144,31 +144,31 @@ class TactusRuntime:
144
144
  self.source_file_path = source_file_path
145
145
 
146
146
  # Will be initialized during setup
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
147
+ self.config: Optional[Dict[str, Any]] = None # Legacy YAML support
148
+ self.registry: Optional[ProcedureRegistry] = None # New DSL registry
149
+ self.lua_sandbox: Optional[LuaSandbox] = None
150
+ self.output_validator: Optional[OutputValidator] = None
151
+ self.template_resolver: Optional[TemplateResolver] = None
152
+ self.message_history_manager: Optional[MessageHistoryManager] = None
153
153
 
154
154
  # Execution context
155
- self.execution_context: BaseExecutionContext | None = None
155
+ self.execution_context: Optional[BaseExecutionContext] = None
156
156
 
157
157
  # Primitives (shared across all agents)
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
158
+ self.state_primitive: Optional[StatePrimitive] = None
159
+ self.iterations_primitive: Optional[IterationsPrimitive] = None
160
+ self.stop_primitive: Optional[StopPrimitive] = None
161
+ self.tool_primitive: Optional[ToolPrimitive] = None
162
+ self.human_primitive: Optional[HumanPrimitive] = None
163
+ self.step_primitive: Optional[StepPrimitive] = None
164
+ self.checkpoint_primitive: Optional[CheckpointPrimitive] = None
165
+ self.log_primitive: Optional[LogPrimitive] = None
166
+ self.json_primitive: Optional[JsonPrimitive] = None
167
+ self.retry_primitive: Optional[RetryPrimitive] = None
168
+ self.file_primitive: Optional[FilePrimitive] = None
169
+ self.procedure_primitive: Optional[ProcedurePrimitive] = None
170
+ self.system_primitive: Optional[SystemPrimitive] = None
171
+ self.host_primitive: Optional[HostPrimitive] = None
172
172
 
173
173
  # Agent primitives (one per agent)
174
174
  self.agents: dict[str, Any] = {}
@@ -181,17 +181,17 @@ class TactusRuntime:
181
181
 
182
182
  # User dependencies (HTTP clients, DB connections, etc.)
183
183
  self.user_dependencies: dict[str, Any] = {}
184
- self.dependency_manager: Any | None = None # ResourceManager for cleanup
184
+ self.dependency_manager: Optional[Any] = None # ResourceManager for cleanup
185
185
 
186
186
  # Mock manager for testing
187
- self.mock_manager: Any | None = None # MockManager instance
188
- self.external_agent_mocks: dict[str, list[dict[str, Any]]] | None = None
187
+ self.mock_manager: Optional[Any] = None # MockManager instance
188
+ self.external_agent_mocks: Optional[Dict[str, List[Dict[str, Any]]]] = None
189
189
  self.mock_all_agents: bool = False
190
190
 
191
191
  logger.info("TactusRuntime initialized for procedure %s", procedure_id)
192
192
 
193
193
  async def execute(
194
- self, source: str, context: dict[str, Any] | None = None, format: str = "yaml"
194
+ self, source: str, context: Optional[Dict[str, Any]] = None, format: str = "yaml"
195
195
  ) -> dict[str, Any]:
196
196
  """
197
197
  Execute a workflow (Lua DSL or legacy YAML format).
@@ -772,7 +772,7 @@ class TactusRuntime:
772
772
  except Exception as e:
773
773
  logger.warning("Error cleaning up dependencies: %s", e)
774
774
 
775
- def _resolve_sandbox_base_path(self) -> str | None:
775
+ def _resolve_sandbox_base_path(self) -> Optional[str]:
776
776
  # Compute base_path for sandbox from source file path if available.
777
777
  # This ensures require() works correctly even when running from different directories.
778
778
  if not self.source_file_path:
@@ -798,7 +798,7 @@ class TactusRuntime:
798
798
 
799
799
  async def _initialize_primitives(
800
800
  self,
801
- placeholder_tool: ToolPrimitive | None = None,
801
+ placeholder_tool: Optional[ToolPrimitive] = None,
802
802
  ):
803
803
  """Initialize all primitive objects.
804
804
 
@@ -836,7 +836,7 @@ class TactusRuntime:
836
836
 
837
837
  logger.debug("All primitives initialized")
838
838
 
839
- def resolve_toolset(self, name: str) -> Any | None:
839
+ def resolve_toolset(self, name: str) -> Optional[Any]:
840
840
  """
841
841
  Resolve a toolset by name from runtime's registered toolsets.
842
842
 
@@ -1047,7 +1047,7 @@ class TactusRuntime:
1047
1047
  for name, toolset in self.toolset_registry.items():
1048
1048
  logger.debug(f" - {name}: {type(toolset)} -> {toolset}")
1049
1049
 
1050
- async def _resolve_tool_source(self, tool_name: str, source: str) -> Any | None:
1050
+ async def _resolve_tool_source(self, tool_name: str, source: str) -> Optional[Any]:
1051
1051
  """
1052
1052
  Resolve a tool from an external source.
1053
1053
 
@@ -1442,7 +1442,7 @@ class TactusRuntime:
1442
1442
 
1443
1443
  async def _create_toolset_from_config(
1444
1444
  self, name: str, definition: dict[str, Any]
1445
- ) -> Any | None:
1445
+ ) -> Optional[Any]:
1446
1446
  """
1447
1447
  Create toolset from YAML config definition.
1448
1448
 
@@ -2194,7 +2194,7 @@ class TactusRuntime:
2194
2194
  if is_required:
2195
2195
  fields[field_name] = (field_type, ...) # Required field
2196
2196
  else:
2197
- fields[field_name] = (field_type | None, None) # Optional field
2197
+ fields[field_name] = (Optional[field_type], None) # Optional field
2198
2198
 
2199
2199
  return create_model(model_name, **fields)
2200
2200
 
@@ -2652,7 +2652,7 @@ class TactusRuntime:
2652
2652
  in_body = False
2653
2653
  brace_depth = 0
2654
2654
  function_depth = 0 # Track function...end blocks
2655
- long_string_eq: str | None = None
2655
+ long_string_eq: Optional[str] = None
2656
2656
 
2657
2657
  decl_start = re.compile(
2658
2658
  r"^\s*(?:"
@@ -2871,7 +2871,7 @@ class TactusRuntime:
2871
2871
  return False
2872
2872
 
2873
2873
  def _parse_declarations(
2874
- self, source: str, tool_primitive: ToolPrimitive | None = None
2874
+ self, source: str, tool_primitive: Optional[ToolPrimitive] = None
2875
2875
  ) -> ProcedureRegistry:
2876
2876
  """
2877
2877
  Execute .tac to collect declarations.
tactus/dspy/broker_lm.py CHANGED
@@ -11,7 +11,7 @@ while still supporting streaming via DSPy's `streamify()` mechanism.
11
11
  from __future__ import annotations
12
12
 
13
13
  import logging
14
- from typing import Any
14
+ from typing import Any, Dict, List, Optional
15
15
 
16
16
  import dspy
17
17
  import litellm
@@ -42,10 +42,10 @@ class BrokeredLM(dspy.BaseLM):
42
42
  model: str,
43
43
  *,
44
44
  model_type: str = "chat",
45
- temperature: float | None = None,
46
- max_tokens: int | None = None,
47
- cache: bool | None = None,
48
- socket_path: str | None = None,
45
+ temperature: Optional[float] = None,
46
+ max_tokens: Optional[int] = None,
47
+ cache: Optional[bool] = None,
48
+ socket_path: Optional[str] = None,
49
49
  **kwargs: Any,
50
50
  ):
51
51
  if model_type != "chat":
@@ -70,12 +70,18 @@ class BrokeredLM(dspy.BaseLM):
70
70
  self._client = env_client
71
71
 
72
72
  def forward(
73
- self, prompt: str | None = None, messages: list[dict[str, Any]] | None = None, **kwargs: Any
73
+ self,
74
+ prompt: Optional[str] = None,
75
+ messages: Optional[List[Dict[str, Any]]] = None,
76
+ **kwargs: Any,
74
77
  ):
75
78
  return syncify(self.aforward)(prompt=prompt, messages=messages, **kwargs)
76
79
 
77
80
  async def aforward(
78
- self, prompt: str | None = None, messages: list[dict[str, Any]] | None = None, **kwargs: Any
81
+ self,
82
+ prompt: Optional[str] = None,
83
+ messages: Optional[List[Dict[str, Any]]] = None,
84
+ **kwargs: Any,
79
85
  ):
80
86
  provider, model_id = _split_provider_model(self.model)
81
87
 
tactus/dspy/config.py CHANGED
@@ -103,10 +103,13 @@ def configure_lm(
103
103
 
104
104
  logger = logging.getLogger(__name__)
105
105
 
106
- adapter = ChatAdapter(use_native_function_calling=True)
107
- logger.info(
108
- f"[ADAPTER] Created ChatAdapter with use_native_function_calling={adapter.use_native_function_calling}"
109
- )
106
+ try:
107
+ adapter = ChatAdapter(use_native_function_calling=True)
108
+ except TypeError:
109
+ adapter = ChatAdapter()
110
+
111
+ use_native = getattr(adapter, "use_native_function_calling", None)
112
+ logger.info(f"[ADAPTER] Created ChatAdapter with use_native_function_calling={use_native}")
110
113
 
111
114
  # Set as global default with adapter
112
115
  dspy.configure(lm=lm, adapter=adapter)
@@ -27,7 +27,7 @@ class ModelPrimitive:
27
27
  self,
28
28
  model_name: str,
29
29
  config: dict,
30
- context: ExecutionContext | None = None,
30
+ context: Optional[ExecutionContext] = None,
31
31
  mock_manager: Optional[Any] = None,
32
32
  ):
33
33
  """
@@ -550,7 +550,7 @@ class ProcedurePrimitive:
550
550
 
551
551
  name_path = Path(name)
552
552
 
553
- def add_candidates(base: Path | None, rel: Path) -> None:
553
+ def add_candidates(base: Optional[Path], rel: Path) -> None:
554
554
  candidate = (base / rel) if base is not None else rel
555
555
  add_path(candidate)
556
556
  if candidate.suffix != ".tac":
@@ -10,7 +10,7 @@ Provides:
10
10
  """
11
11
 
12
12
  import logging
13
- from typing import Any
13
+ from typing import Any, Dict, Optional
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -23,7 +23,7 @@ class StatePrimitive:
23
23
  progress, accumulate results, and coordinate between agents.
24
24
  """
25
25
 
26
- def __init__(self, state_schema: dict[str, Any] | None = None):
26
+ def __init__(self, state_schema: Optional[Dict[str, Any]] = None):
27
27
  """
28
28
  Initialize state storage.
29
29
 
@@ -17,7 +17,7 @@ import tempfile
17
17
  import time
18
18
  import uuid
19
19
  from pathlib import Path
20
- from typing import Any, Callable, Dict, List, Optional
20
+ from typing import Any, Callable, Dict, List, Optional, Tuple
21
21
 
22
22
  from .config import SandboxConfig
23
23
  from .docker_manager import (
@@ -523,6 +523,11 @@ class ContainerRunner:
523
523
  llm_backend_config=llm_backend_config,
524
524
  )
525
525
  finally:
526
+ try:
527
+ await broker_server.aclose()
528
+ broker_server = None
529
+ except Exception:
530
+ logger.debug("[BROKER] Failed to close broker server", exc_info=True)
526
531
  # Cancel broker task when container finishes
527
532
  broker_task.cancel()
528
533
  try:
@@ -589,7 +594,7 @@ class ContainerRunner:
589
594
  """
590
595
  broker_transport = (self.config.broker_transport or "stdio").lower()
591
596
 
592
- stdio_request_prefix: str | None = None
597
+ stdio_request_prefix: Optional[str] = None
593
598
  if broker_transport == "stdio":
594
599
  from tactus.broker.server import OpenAIChatBackend
595
600
  from tactus.broker.server import HostToolRegistry
@@ -950,9 +955,9 @@ class ContainerRunner:
950
955
  )
951
956
  logger.debug("[SANDBOX] Spawned container process pid=%s", process.pid)
952
957
 
953
- stdout_task: asyncio.Task[None] | None = None
954
- stderr_task: asyncio.Task[None] | None = None
955
- wait_task: asyncio.Task[int] | None = None
958
+ stdout_task: Optional[asyncio.Task[None]] = None
959
+ stderr_task: Optional[asyncio.Task[None]] = None
960
+ wait_task: Optional[asyncio.Task[int]] = None
956
961
 
957
962
  try:
958
963
  assert process.stdin is not None
@@ -1159,7 +1164,7 @@ class ContainerRunner:
1159
1164
  return
1160
1165
 
1161
1166
  # Rich/terminal: parse our container log format and re-emit.
1162
- current: tuple[str, int, list[str]] | None = None # (logger_name, levelno, lines)
1167
+ current: Optional[Tuple[str, int, List[str]]] = None # (logger_name, levelno, lines)
1163
1168
 
1164
1169
  def flush_current() -> None:
1165
1170
  nonlocal current
tactus/testing/context.py CHANGED
@@ -45,18 +45,18 @@ class TactusTestContext:
45
45
  self.total_tokens: int = 0 # Track total tokens
46
46
  self.cost_breakdown: List[Any] = [] # Track per-call costs
47
47
  self._agent_mock_turns: Dict[str, List[Dict[str, Any]]] = {}
48
- self._scenario_message: str | None = None
48
+ self._scenario_message: Optional[str] = None
49
49
 
50
50
  def set_scenario_message(self, message: str) -> None:
51
51
  """Set the scenario's primary injected message (for in-spec mocking coordination)."""
52
52
  self._scenario_message = message
53
53
 
54
- def get_scenario_message(self) -> str | None:
54
+ def get_scenario_message(self) -> Optional[str]:
55
55
  """Get the scenario's primary injected message, if set."""
56
56
  return self._scenario_message
57
57
 
58
58
  def mock_agent_response(
59
- self, agent: str, message: str, when_message: str | None = None
59
+ self, agent: str, message: str, when_message: Optional[str] = None
60
60
  ) -> None:
61
61
  """Add a mocked agent response for this scenario (temporal; 1 per agent turn).
62
62
 
@@ -79,8 +79,8 @@ class TactusTestContext:
79
79
  self,
80
80
  agent: str,
81
81
  tool: str,
82
- args: Dict[str, Any] | None = None,
83
- when_message: str | None = None,
82
+ args: Optional[Dict[str, Any]] = None,
83
+ when_message: Optional[str] = None,
84
84
  ) -> None:
85
85
  """Add a mocked tool call to an agent's next mocked turn for this scenario."""
86
86
  args = args or {}
@@ -114,7 +114,7 @@ class TactusTestContext:
114
114
  self.runtime.external_agent_mocks = self._agent_mock_turns
115
115
 
116
116
  def mock_agent_data(
117
- self, agent: str, data: Dict[str, Any], when_message: str | None = None
117
+ self, agent: str, data: Dict[str, Any], when_message: Optional[str] = None
118
118
  ) -> None:
119
119
  """Set structured output mock data for an agent's next mocked turn.
120
120
 
@@ -94,6 +94,9 @@ class TactusEvaluationRunner(TactusTestRunner):
94
94
  EvaluationResult with all metrics
95
95
  """
96
96
  logger.info(f"Evaluating scenario '{scenario_name}' with {runs} runs")
97
+ run_iteration = self._run_single_iteration
98
+ if isinstance(run_iteration, staticmethod):
99
+ run_iteration = run_iteration.__func__
97
100
 
98
101
  # Run scenario N times
99
102
  if parallel:
@@ -102,12 +105,9 @@ class TactusEvaluationRunner(TactusTestRunner):
102
105
  ctx = multiprocessing.get_context("spawn")
103
106
  with ctx.Pool(processes=workers) as pool:
104
107
  iteration_args = [(scenario_name, str(self.work_dir), i) for i in range(runs)]
105
- results = pool.starmap(self._run_single_iteration, iteration_args)
108
+ results = pool.starmap(run_iteration, iteration_args)
106
109
  else:
107
- results = [
108
- self._run_single_iteration(scenario_name, str(self.work_dir), i)
109
- for i in range(runs)
110
- ]
110
+ results = [run_iteration(scenario_name, str(self.work_dir), i) for i in range(runs)]
111
111
 
112
112
  # Calculate metrics
113
113
  return self._calculate_metrics(scenario_name, results)
@@ -14,7 +14,7 @@ Provides a comprehensive library of steps for testing:
14
14
  import logging
15
15
  import re
16
16
  import ast
17
- from typing import Any
17
+ from typing import Any, Optional
18
18
 
19
19
  from .registry import StepRegistry
20
20
 
@@ -645,7 +645,7 @@ def step_agent_takes_turn(context: Any, agent: str) -> None:
645
645
 
646
646
 
647
647
  def step_mock_agent_responds_with(
648
- context: Any, agent: str, message: str, when_message: str | None = None
648
+ context: Any, agent: str, message: str, when_message: Optional[str] = None
649
649
  ) -> None:
650
650
  """Configure a per-scenario mock agent response (temporal)."""
651
651
  message, _ = _parse_step_string_literal(message)
@@ -128,6 +128,10 @@ class TactusTestRunner:
128
128
  if not scenarios:
129
129
  raise ValueError(f"Scenario not found: {scenario_filter}")
130
130
 
131
+ run_scenario = self._run_single_scenario
132
+ if isinstance(run_scenario, staticmethod):
133
+ run_scenario = run_scenario.__func__
134
+
131
135
  # Run scenarios
132
136
  if parallel and len(scenarios) > 1:
133
137
  # Run in parallel using 'spawn' to avoid Behave global state conflicts
@@ -135,13 +139,11 @@ class TactusTestRunner:
135
139
  ctx = multiprocessing.get_context("spawn")
136
140
  with ctx.Pool(processes=min(len(scenarios), os.cpu_count() or 1)) as pool:
137
141
  scenario_results = pool.starmap(
138
- self._run_single_scenario, [(s.name, str(self.work_dir)) for s in scenarios]
142
+ run_scenario, [(s.name, str(self.work_dir)) for s in scenarios]
139
143
  )
140
144
  else:
141
145
  # Run sequentially
142
- scenario_results = [
143
- self._run_single_scenario(s.name, str(self.work_dir)) for s in scenarios
144
- ]
146
+ scenario_results = [run_scenario(s.name, str(self.work_dir)) for s in scenarios]
145
147
 
146
148
  # Build feature result
147
149
  feature_result = self._build_feature_result(scenario_results)
@@ -24,4 +24,5 @@ def clear_closed_event_loop() -> None:
24
24
  return
25
25
 
26
26
  if getattr(current_loop, "is_closed", lambda: False)():
27
- asyncio.set_event_loop(None)
27
+ replacement_loop = asyncio.new_event_loop()
28
+ asyncio.set_event_loop(replacement_loop)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tactus
3
- Version: 0.36.0
3
+ Version: 0.37.0
4
4
  Summary: Tactus: Lua-based DSL for agentic workflows
5
5
  Project-URL: Homepage, https://github.com/AnthusAI/Tactus
6
6
  Project-URL: Documentation, https://github.com/AnthusAI/Tactus/tree/main/docs
@@ -14,15 +14,17 @@ Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
17
19
  Classifier: Programming Language :: Python :: 3.11
18
20
  Classifier: Programming Language :: Python :: 3.12
19
21
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Requires-Python: >=3.11
23
+ Requires-Python: >=3.9
22
24
  Requires-Dist: antlr4-python3-runtime==4.13.1
23
25
  Requires-Dist: behave>=1.2.6
24
26
  Requires-Dist: boto3>=1.28.0
25
- Requires-Dist: dotyaml>=0.1.0
27
+ Requires-Dist: dotyaml>=0.1.4
26
28
  Requires-Dist: dspy>=2.5
27
29
  Requires-Dist: flask-cors>=4.0.0
28
30
  Requires-Dist: flask>=3.0.0
@@ -48,9 +50,9 @@ Requires-Dist: typer
48
50
  Provides-Extra: dev
49
51
  Requires-Dist: antlr4-tools>=0.2.1; extra == 'dev'
50
52
  Requires-Dist: behave>=1.2.6; extra == 'dev'
51
- Requires-Dist: black==25.12.0; extra == 'dev'
53
+ Requires-Dist: black==24.10.0; extra == 'dev'
52
54
  Requires-Dist: coverage>=7.4; extra == 'dev'
53
- Requires-Dist: fastmcp>=2.3.5; extra == 'dev'
55
+ Requires-Dist: fastmcp>=2.3.5; (python_version >= '3.10') and extra == 'dev'
54
56
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
55
57
  Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
56
58
  Requires-Dist: pytest>=8.0; extra == 'dev'