tactus 0.35.1__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.
Files changed (43) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/channels/base.py +20 -2
  3. tactus/adapters/channels/broker.py +1 -0
  4. tactus/adapters/channels/host.py +3 -1
  5. tactus/adapters/channels/ipc.py +18 -3
  6. tactus/adapters/channels/sse.py +13 -5
  7. tactus/adapters/control_loop.py +44 -30
  8. tactus/adapters/mcp_manager.py +24 -7
  9. tactus/backends/http_backend.py +2 -2
  10. tactus/backends/pytorch_backend.py +2 -2
  11. tactus/broker/client.py +3 -3
  12. tactus/broker/server.py +17 -5
  13. tactus/core/dsl_stubs.py +3 -3
  14. tactus/core/execution_context.py +32 -27
  15. tactus/core/lua_sandbox.py +42 -34
  16. tactus/core/message_history_manager.py +51 -28
  17. tactus/core/output_validator.py +65 -51
  18. tactus/core/registry.py +29 -29
  19. tactus/core/runtime.py +69 -61
  20. tactus/dspy/broker_lm.py +13 -7
  21. tactus/dspy/config.py +7 -4
  22. tactus/ide/server.py +63 -33
  23. tactus/primitives/host.py +19 -16
  24. tactus/primitives/message_history.py +11 -14
  25. tactus/primitives/model.py +1 -1
  26. tactus/primitives/procedure.py +11 -8
  27. tactus/primitives/session.py +9 -9
  28. tactus/primitives/state.py +2 -2
  29. tactus/primitives/tool_handle.py +27 -24
  30. tactus/sandbox/container_runner.py +11 -6
  31. tactus/testing/context.py +6 -6
  32. tactus/testing/evaluation_runner.py +5 -5
  33. tactus/testing/mock_hitl.py +2 -2
  34. tactus/testing/models.py +2 -0
  35. tactus/testing/steps/builtin.py +2 -2
  36. tactus/testing/test_runner.py +6 -4
  37. tactus/utils/asyncio_helpers.py +2 -1
  38. tactus/utils/safe_libraries.py +2 -2
  39. {tactus-0.35.1.dist-info → tactus-0.37.0.dist-info}/METADATA +11 -5
  40. {tactus-0.35.1.dist-info → tactus-0.37.0.dist-info}/RECORD +43 -43
  41. {tactus-0.35.1.dist-info → tactus-0.37.0.dist-info}/WHEEL +0 -0
  42. {tactus-0.35.1.dist-info → tactus-0.37.0.dist-info}/entry_points.txt +0 -0
  43. {tactus-0.35.1.dist-info → tactus-0.37.0.dist-info}/licenses/LICENSE +0 -0
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).
@@ -221,17 +221,7 @@ class TactusRuntime:
221
221
  logger.info("Step 0: Setting up Lua sandbox")
222
222
  strict_determinism = self.external_config.get("strict_determinism", False)
223
223
 
224
- # Compute base_path for sandbox from source file path if available
225
- # This ensures require() works correctly even when running from different directories
226
- sandbox_base_path = None
227
- if self.source_file_path:
228
- from pathlib import Path
229
-
230
- sandbox_base_path = str(Path(self.source_file_path).parent.resolve())
231
- logger.debug(
232
- "Using source file directory as sandbox base_path: %s",
233
- sandbox_base_path,
234
- )
224
+ sandbox_base_path = self._resolve_sandbox_base_path()
235
225
 
236
226
  self.lua_sandbox = LuaSandbox(
237
227
  execution_context=None,
@@ -242,13 +232,7 @@ class TactusRuntime:
242
232
  # 0.5. Create execution context EARLY so it's available during DSL parsing
243
233
  # This is critical for immediate agent creation during parsing
244
234
  logger.info("Step 0.5: Creating execution context (early)")
245
- self.execution_context = BaseExecutionContext(
246
- procedure_id=self.procedure_id,
247
- storage_backend=self.storage_backend,
248
- hitl_handler=self.hitl_handler,
249
- strict_determinism=strict_determinism,
250
- log_handler=self.log_handler,
251
- )
235
+ self.execution_context = self._create_execution_context(strict_determinism)
252
236
 
253
237
  # Set run_id if provided
254
238
  if self.run_id:
@@ -788,9 +772,33 @@ class TactusRuntime:
788
772
  except Exception as e:
789
773
  logger.warning("Error cleaning up dependencies: %s", e)
790
774
 
775
+ def _resolve_sandbox_base_path(self) -> Optional[str]:
776
+ # Compute base_path for sandbox from source file path if available.
777
+ # This ensures require() works correctly even when running from different directories.
778
+ if not self.source_file_path:
779
+ return None
780
+
781
+ from pathlib import Path
782
+
783
+ sandbox_base_path = str(Path(self.source_file_path).parent.resolve())
784
+ logger.debug(
785
+ "Using source file directory as sandbox base_path: %s",
786
+ sandbox_base_path,
787
+ )
788
+ return sandbox_base_path
789
+
790
+ def _create_execution_context(self, strict_determinism: bool) -> BaseExecutionContext:
791
+ return BaseExecutionContext(
792
+ procedure_id=self.procedure_id,
793
+ storage_backend=self.storage_backend,
794
+ hitl_handler=self.hitl_handler,
795
+ strict_determinism=strict_determinism,
796
+ log_handler=self.log_handler,
797
+ )
798
+
791
799
  async def _initialize_primitives(
792
800
  self,
793
- placeholder_tool: ToolPrimitive | None = None,
801
+ placeholder_tool: Optional[ToolPrimitive] = None,
794
802
  ):
795
803
  """Initialize all primitive objects.
796
804
 
@@ -828,7 +836,7 @@ class TactusRuntime:
828
836
 
829
837
  logger.debug("All primitives initialized")
830
838
 
831
- def resolve_toolset(self, name: str) -> Any | None:
839
+ def resolve_toolset(self, name: str) -> Optional[Any]:
832
840
  """
833
841
  Resolve a toolset by name from runtime's registered toolsets.
834
842
 
@@ -1039,7 +1047,7 @@ class TactusRuntime:
1039
1047
  for name, toolset in self.toolset_registry.items():
1040
1048
  logger.debug(f" - {name}: {type(toolset)} -> {toolset}")
1041
1049
 
1042
- 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]:
1043
1051
  """
1044
1052
  Resolve a tool from an external source.
1045
1053
 
@@ -1434,7 +1442,7 @@ class TactusRuntime:
1434
1442
 
1435
1443
  async def _create_toolset_from_config(
1436
1444
  self, name: str, definition: dict[str, Any]
1437
- ) -> Any | None:
1445
+ ) -> Optional[Any]:
1438
1446
  """
1439
1447
  Create toolset from YAML config definition.
1440
1448
 
@@ -2186,7 +2194,7 @@ class TactusRuntime:
2186
2194
  if is_required:
2187
2195
  fields[field_name] = (field_type, ...) # Required field
2188
2196
  else:
2189
- fields[field_name] = (field_type | None, None) # Optional field
2197
+ fields[field_name] = (Optional[field_type], None) # Optional field
2190
2198
 
2191
2199
  return create_model(model_name, **fields)
2192
2200
 
@@ -2644,7 +2652,7 @@ class TactusRuntime:
2644
2652
  in_body = False
2645
2653
  brace_depth = 0
2646
2654
  function_depth = 0 # Track function...end blocks
2647
- long_string_eq: str | None = None
2655
+ long_string_eq: Optional[str] = None
2648
2656
 
2649
2657
  decl_start = re.compile(
2650
2658
  r"^\s*(?:"
@@ -2863,7 +2871,7 @@ class TactusRuntime:
2863
2871
  return False
2864
2872
 
2865
2873
  def _parse_declarations(
2866
- self, source: str, tool_primitive: ToolPrimitive | None = None
2874
+ self, source: str, tool_primitive: Optional[ToolPrimitive] = None
2867
2875
  ) -> ProcedureRegistry:
2868
2876
  """
2869
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)