tactus 0.34.1__py3-none-any.whl → 0.35.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 (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 +15 -6
  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.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.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, Optional, Union
9
+ from typing import Any
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: Optional[str] = None
22
+ description: str | None = 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: Optional[Any] = None # Lua function reference or filter name
34
+ filter: Any | None = 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: 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
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
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: Optional[AgentOutputSchema] = None # Aligned with pydantic-ai
54
- message_history: Optional[MessageHistoryConfiguration] = None
53
+ output: AgentOutputSchema | None = None # Aligned with pydantic-ai
54
+ message_history: MessageHistoryConfiguration | None = 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: Optional[float] = None
60
- max_tokens: Optional[int] = None
61
- model_type: Optional[str] = None # e.g., "chat", "responses" for reasoning models
59
+ temperature: float | None = None
60
+ max_tokens: int | None = None
61
+ model_type: str | None = 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: Optional[int] = None
72
+ timeout: int | None = None
73
73
  default: Any = None
74
- options: Optional[list[dict[str, Any]]] = None
74
+ options: list[dict[str, Any]] | None = 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: Optional[str] = None # defaults to "procedure_completes"
85
- then_output: Optional[dict[str, Any]] = None
86
- then_state: Optional[dict[str, Any]] = None
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
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: Optional[str] = None
137
+ description: str | None = 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: Optional[str] = None # Raw Gherkin text
157
+ gherkin_specifications: str | None = 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: Optional[dict[str, Any]] = None # Pydantic Evals configuration
163
+ pydantic_evaluations: dict[str, Any] | None = None # Pydantic Evals configuration
164
164
 
165
165
  # Prompts
166
166
  prompts: dict[str, str] = Field(default_factory=dict)
167
- return_prompt: Optional[str] = None
168
- error_prompt: Optional[str] = None
169
- status_prompt: Optional[str] = None
167
+ return_prompt: str | None = None
168
+ error_prompt: str | None = None
169
+ status_prompt: str | None = 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: Optional[str] = None
176
- default_model: Optional[str] = None
175
+ default_provider: str | None = None
176
+ default_model: str | None = 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: Optional[tuple[int, int]] = None
197
- declaration: Optional[str] = None
196
+ location: tuple[int, int] | None = None
197
+ declaration: str | None = 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: Optional[ProcedureRegistry] = None
206
+ registry: ProcedureRegistry | None = None
207
207
 
208
208
 
209
209
  class RegistryBuilder:
@@ -225,30 +225,38 @@ class RegistryBuilder:
225
225
  """Register state schema declaration."""
226
226
  self.registry.state_schema = schema
227
227
 
228
- def register_agent(self, name: str, config: dict, output_schema: Optional[dict] = None) -> None:
228
+ def register_agent(
229
+ self,
230
+ name: str,
231
+ config: dict,
232
+ output_schema: dict | None = None,
233
+ ) -> None:
229
234
  """Register an agent declaration."""
230
- config["name"] = name
235
+ agent_config = dict(config)
236
+ agent_config["name"] = name
231
237
 
232
238
  # Add output_schema to config if provided
233
239
  if output_schema:
234
240
  # Convert output_schema dict to AgentOutputSchema
235
- fields = {}
236
- for field_name, field_config in output_schema.items():
241
+ output_field_declarations: dict[str, OutputFieldDeclaration] = {}
242
+ for field_name, field_definition in output_schema.items():
237
243
  # Add the field name to the config (required by OutputFieldDeclaration)
238
- field_config_with_name = dict(field_config)
239
- field_config_with_name["name"] = field_name
240
- fields[field_name] = OutputFieldDeclaration(**field_config_with_name)
241
- config["output"] = AgentOutputSchema(fields=fields)
244
+ field_definition_with_name = dict(field_definition)
245
+ field_definition_with_name["name"] = field_name
246
+ output_field_declarations[field_name] = OutputFieldDeclaration(
247
+ **field_definition_with_name
248
+ )
249
+ agent_config["output"] = AgentOutputSchema(fields=output_field_declarations)
242
250
 
243
251
  # Apply defaults
244
- if "provider" not in config and self.registry.default_provider:
245
- config["provider"] = self.registry.default_provider
246
- if "model" not in config and self.registry.default_model:
247
- config["model"] = self.registry.default_model
252
+ if "provider" not in agent_config and self.registry.default_provider:
253
+ agent_config["provider"] = self.registry.default_provider
254
+ if "model" not in agent_config and self.registry.default_model:
255
+ agent_config["model"] = self.registry.default_model
248
256
  try:
249
- self.registry.agents[name] = AgentDeclaration(**config)
250
- except ValidationError as e:
251
- self._add_error(f"Invalid agent '{name}': {e}")
257
+ self.registry.agents[name] = AgentDeclaration(**agent_config)
258
+ except ValidationError as exception:
259
+ self._add_error(f"Invalid agent '{name}': {exception}")
252
260
 
253
261
  def register_model(self, name: str, config: dict) -> None:
254
262
  """Register a model declaration."""
@@ -260,21 +268,21 @@ class RegistryBuilder:
260
268
  config["name"] = name
261
269
  try:
262
270
  self.registry.hitl_points[name] = HITLDeclaration(**config)
263
- except ValidationError as e:
264
- self._add_error(f"Invalid HITL point '{name}': {e}")
271
+ except ValidationError as exception:
272
+ self._add_error(f"Invalid HITL point '{name}': {exception}")
265
273
 
266
274
  def register_dependency(self, name: str, config: dict) -> None:
267
275
  """Register a dependency declaration."""
268
276
  # The config dict contains the type and all other configuration
269
- dependency_config = {
277
+ dependency_declaration = {
270
278
  "name": name,
271
279
  "type": config.get("type"),
272
280
  "config": config, # Store the entire config dict
273
281
  }
274
282
  try:
275
- self.registry.dependencies[name] = DependencyDeclaration(**dependency_config)
276
- except ValidationError as e:
277
- self._add_error(f"Invalid dependency '{name}': {e}")
283
+ self.registry.dependencies[name] = DependencyDeclaration(**dependency_declaration)
284
+ except ValidationError as exception:
285
+ self._add_error(f"Invalid dependency '{name}': {exception}")
278
286
 
279
287
  def register_prompt(self, name: str, content: str) -> None:
280
288
  """Register a prompt template."""
@@ -292,7 +300,7 @@ class RegistryBuilder:
292
300
  config: Dict with description, input, output schemas, and source info
293
301
  lua_handler: Lupa function reference (or placeholder for external sources)
294
302
  """
295
- tool_def = {
303
+ tool_definition = {
296
304
  "description": config.get("description", ""),
297
305
  "input": config.get("input", {}), # Changed from parameters
298
306
  "output": config.get("output", {}), # New: output schema
@@ -301,9 +309,9 @@ class RegistryBuilder:
301
309
 
302
310
  # If this tool references an external source, store that info
303
311
  if "source" in config:
304
- tool_def["source"] = config["source"]
312
+ tool_definition["source"] = config["source"]
305
313
 
306
- self.registry.lua_tools[name] = tool_def
314
+ self.registry.lua_tools[name] = tool_definition
307
315
 
308
316
  def register_mock(self, tool_name: str, config: dict) -> None:
309
317
  """Register a mock configuration for a tool.
@@ -323,8 +331,8 @@ class RegistryBuilder:
323
331
  """
324
332
  try:
325
333
  self.registry.agent_mocks[agent_name] = AgentMockConfig(**config)
326
- except Exception as e:
327
- self._add_error(f"Invalid agent mock config for '{agent_name}': {e}")
334
+ except Exception as exception:
335
+ self._add_error(f"Invalid agent mock config for '{agent_name}': {exception}")
328
336
 
329
337
  def register_specification(self, name: str, scenarios: list) -> None:
330
338
  """Register a BDD specification."""
@@ -333,8 +341,8 @@ class RegistryBuilder:
333
341
  name=name, scenarios=[ScenarioDeclaration(**s) for s in scenarios]
334
342
  )
335
343
  self.registry.specifications.append(spec)
336
- except ValidationError as e:
337
- self._add_error(f"Invalid specification '{name}': {e}")
344
+ except ValidationError as exception:
345
+ self._add_error(f"Invalid specification '{name}': {exception}")
338
346
 
339
347
  def register_named_procedure(
340
348
  self,
@@ -451,24 +459,26 @@ class RegistryBuilder:
451
459
 
452
460
  def validate(self) -> ValidationResult:
453
461
  """Run all validations after declarations collected."""
454
- errors = []
455
- warnings = []
462
+ validation_errors: list[ValidationMessage] = []
463
+ validation_warnings: list[ValidationMessage] = []
456
464
 
457
465
  # Script mode: merge top-level schemas into main procedure
458
466
  if self.registry.script_mode and "main" in self.registry.named_procedures:
459
- main_proc = self.registry.named_procedures["main"]
467
+ main_procedure_entry = self.registry.named_procedures["main"]
460
468
  # Merge top-level input schema if main doesn't have one
461
- if not main_proc["input_schema"] and self.registry.top_level_input_schema:
462
- main_proc["input_schema"] = self.registry.top_level_input_schema
469
+ if not main_procedure_entry["input_schema"] and self.registry.top_level_input_schema:
470
+ main_procedure_entry["input_schema"] = self.registry.top_level_input_schema
463
471
  # Merge top-level output schema if main doesn't have one
464
- if not main_proc["output_schema"] and self.registry.top_level_output_schema:
465
- main_proc["output_schema"] = self.registry.top_level_output_schema
472
+ if not main_procedure_entry["output_schema"] and self.registry.top_level_output_schema:
473
+ main_procedure_entry["output_schema"] = self.registry.top_level_output_schema
466
474
 
467
475
  # Check for multiple unnamed Procedures (all would register as "main")
468
476
  # Count how many times a procedure was registered as "main"
469
- main_count = sum(1 for name in self.registry.named_procedures.keys() if name == "main")
470
- if main_count > 1:
471
- errors.append(
477
+ main_procedure_declaration_count = sum(
478
+ 1 for name in self.registry.named_procedures.keys() if name == "main"
479
+ )
480
+ if main_procedure_declaration_count > 1:
481
+ validation_errors.append(
472
482
  ValidationMessage(
473
483
  level="error",
474
484
  message="Multiple unnamed Procedures found. Only one unnamed Procedure is allowed as the main entry point. Use named Procedures (e.g., helper = Procedure {...}) for additional procedures.",
@@ -479,25 +489,25 @@ class RegistryBuilder:
479
489
  # Top-level code can execute directly without being wrapped in a Procedure.
480
490
 
481
491
  # Agent validation
482
- for agent in self.registry.agents.values():
492
+ for agent_declaration in self.registry.agents.values():
483
493
  # Check if agent has provider or if there's a default
484
- if not agent.provider and not self.registry.default_provider:
485
- errors.append(
494
+ if not agent_declaration.provider and not self.registry.default_provider:
495
+ validation_errors.append(
486
496
  ValidationMessage(
487
497
  level="error",
488
- message=f"Agent '{agent.name}' missing provider",
489
- declaration=agent.name,
498
+ message=f"Agent '{agent_declaration.name}' missing provider",
499
+ declaration=agent_declaration.name,
490
500
  )
491
501
  )
492
502
 
493
503
  # Warnings for missing specifications
494
- has_specs = (
504
+ has_specifications = (
495
505
  self.registry.specifications
496
506
  or self.registry.gherkin_specifications
497
507
  or self.registry.specs_from_references
498
508
  )
499
- if not has_specs:
500
- warnings.append(
509
+ if not has_specifications:
510
+ validation_warnings.append(
501
511
  ValidationMessage(
502
512
  level="warning",
503
513
  message='No specifications defined - consider adding BDD tests using Specification([[...]]) or Specification { from = "path" }',
@@ -505,12 +515,16 @@ class RegistryBuilder:
505
515
  )
506
516
 
507
517
  # Add any errors from registration
508
- errors.extend([m for m in self.validation_messages if m.level == "error"])
509
- warnings.extend([m for m in self.validation_messages if m.level == "warning"])
518
+ validation_errors.extend(
519
+ [message for message in self.validation_messages if message.level == "error"]
520
+ )
521
+ validation_warnings.extend(
522
+ [message for message in self.validation_messages if message.level == "warning"]
523
+ )
510
524
 
511
525
  return ValidationResult(
512
- valid=len(errors) == 0,
513
- errors=errors,
514
- warnings=warnings,
515
- registry=self.registry if len(errors) == 0 else None,
526
+ valid=len(validation_errors) == 0,
527
+ errors=validation_errors,
528
+ warnings=validation_warnings,
529
+ registry=self.registry if len(validation_errors) == 0 else None,
516
530
  )