tactus 0.31.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 (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,499 @@
1
+ """
2
+ Registry system for Lua DSL declarations.
3
+
4
+ This module provides Pydantic models for collecting and validating
5
+ procedure declarations from .tac files.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Optional, Union
10
+
11
+ from pydantic import BaseModel, Field, ValidationError, ConfigDict
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OutputFieldDeclaration(BaseModel):
17
+ """Output field declaration from DSL."""
18
+
19
+ name: str
20
+ field_type: str = Field(alias="type") # string, number, boolean, array, object
21
+ required: bool = False
22
+ description: Optional[str] = None
23
+
24
+ model_config = ConfigDict(populate_by_name=True)
25
+
26
+
27
+ class MessageHistoryConfiguration(BaseModel):
28
+ """Message history configuration for agents.
29
+
30
+ Aligned with pydantic-ai's message_history concept.
31
+ """
32
+
33
+ source: str = "own" # "own", "shared", or another agent's name
34
+ filter: Optional[Any] = None # Lua function reference or filter name
35
+
36
+
37
+ class AgentOutputSchema(BaseModel):
38
+ """Maps to Pydantic AI's output."""
39
+
40
+ fields: dict[str, OutputFieldDeclaration] = Field(default_factory=dict)
41
+
42
+
43
+ class AgentDeclaration(BaseModel):
44
+ """Agent declaration from DSL."""
45
+
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
51
+ tools: list[Any] = Field(default_factory=list) # Tool/toolset references and expressions
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
55
+ max_turns: int = 50
56
+ disable_streaming: bool = (
57
+ False # Disable streaming for models that don't support tools in streaming mode
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
62
+
63
+ model_config = ConfigDict(extra="allow")
64
+
65
+
66
+ class HITLDeclaration(BaseModel):
67
+ """Human-in-the-loop interaction point declaration."""
68
+
69
+ name: str
70
+ hitl_type: str = Field(alias="type") # approval, input, review
71
+ message: str
72
+ timeout: Optional[int] = None
73
+ default: Any = None
74
+ options: Optional[list[dict[str, Any]]] = None
75
+
76
+ model_config = ConfigDict(populate_by_name=True)
77
+
78
+
79
+ class ScenarioDeclaration(BaseModel):
80
+ """BDD scenario declaration."""
81
+
82
+ name: str
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
87
+ mocks: dict[str, Any] = Field(default_factory=dict) # tool_name -> response
88
+
89
+
90
+ class SpecificationDeclaration(BaseModel):
91
+ """BDD specification declaration."""
92
+
93
+ name: str
94
+ scenarios: list[ScenarioDeclaration] = Field(default_factory=list)
95
+
96
+
97
+ class DependencyDeclaration(BaseModel):
98
+ """Dependency declaration from DSL."""
99
+
100
+ name: str
101
+ dependency_type: str = Field(alias="type") # http_client, postgres, redis
102
+ config: dict[str, Any] = Field(default_factory=dict) # Configuration dict
103
+
104
+ model_config = ConfigDict(populate_by_name=True)
105
+
106
+
107
+ class AgentMockConfig(BaseModel):
108
+ """Mock configuration for an agent's behavior.
109
+
110
+ Specifies what tool calls the agent should simulate when mocking is enabled.
111
+ This allows agent-based tests to pass in CI without making real LLM calls.
112
+ """
113
+
114
+ tool_calls: list[dict[str, Any]] = Field(default_factory=list)
115
+ # List of tool calls to simulate: [{"tool": "done", "args": {"reason": "..."}}, ...]
116
+ message: str = "" # The agent's final message response
117
+ data: dict[str, Any] = Field(
118
+ default_factory=dict,
119
+ description="Optional structured response payload (exposed as result.data in Lua)",
120
+ )
121
+ usage: dict[str, Any] = Field(
122
+ default_factory=dict,
123
+ description="Optional token usage payload (exposed as result.usage in Lua)",
124
+ )
125
+ temporal: list[dict[str, Any]] = Field(
126
+ default_factory=list,
127
+ description="Optional temporal mock turns (1-indexed by agent turn).",
128
+ )
129
+
130
+
131
+ class ProcedureRegistry(BaseModel):
132
+ """Collects all declarations from a .tac file."""
133
+
134
+ model_config = {"arbitrary_types_allowed": True}
135
+
136
+ # Metadata
137
+ description: Optional[str] = None
138
+
139
+ # Declarations
140
+ input_schema: dict[str, Any] = Field(default_factory=dict)
141
+ output_schema: dict[str, Any] = Field(default_factory=dict)
142
+ state_schema: dict[str, Any] = Field(default_factory=dict)
143
+ agents: dict[str, AgentDeclaration] = Field(default_factory=dict)
144
+ models: dict[str, dict[str, Any]] = Field(default_factory=dict) # ML models
145
+ toolsets: dict[str, dict[str, Any]] = Field(default_factory=dict)
146
+ lua_tools: dict[str, dict[str, Any]] = Field(default_factory=dict) # Lua function tools
147
+ hitl_points: dict[str, HITLDeclaration] = Field(default_factory=dict)
148
+ specifications: list[SpecificationDeclaration] = Field(default_factory=list)
149
+ dependencies: dict[str, DependencyDeclaration] = Field(default_factory=dict)
150
+ mocks: dict[str, dict[str, Any]] = Field(default_factory=dict) # Mock configurations
151
+ agent_mocks: dict[str, AgentMockConfig] = Field(default_factory=dict) # Agent mock configs
152
+
153
+ # Message history configuration (aligned with pydantic-ai)
154
+ message_history_config: dict[str, Any] = Field(default_factory=dict)
155
+
156
+ # Gherkin BDD Testing
157
+ gherkin_specifications: Optional[str] = None # Raw Gherkin text
158
+ custom_steps: dict[str, Any] = Field(default_factory=dict) # step_text -> lua_function
159
+ evaluation_config: dict[str, Any] = Field(default_factory=dict) # runs, parallel, etc.
160
+
161
+ # Pydantic Evals Integration
162
+ pydantic_evaluations: Optional[dict[str, Any]] = None # Pydantic Evals configuration
163
+
164
+ # Prompts
165
+ prompts: dict[str, str] = Field(default_factory=dict)
166
+ return_prompt: Optional[str] = None
167
+ error_prompt: Optional[str] = None
168
+ status_prompt: Optional[str] = None
169
+
170
+ # Execution settings
171
+ async_enabled: bool = False
172
+ max_depth: int = 5
173
+ max_turns: int = 50
174
+ default_provider: Optional[str] = None
175
+ default_model: Optional[str] = None
176
+
177
+ # Named procedures (for in-file sub-procedures)
178
+ named_procedures: dict[str, dict[str, Any]] = Field(default_factory=dict)
179
+ # Structure: {"proc_name": {"function": <lua_ref>, "input_schema": {...}, ...}}
180
+
181
+ # Script mode support (top-level input/output without explicit main procedure)
182
+ script_mode: bool = False
183
+ top_level_input_schema: dict[str, Any] = Field(default_factory=dict)
184
+ top_level_output_schema: dict[str, Any] = Field(default_factory=dict)
185
+
186
+ # Source locations for error messages (declaration_name -> (line, col))
187
+ source_locations: dict[str, tuple[int, int]] = Field(default_factory=dict)
188
+
189
+
190
+ class ValidationMessage(BaseModel):
191
+ """Validation error or warning message."""
192
+
193
+ level: str # "error" or "warning"
194
+ message: str
195
+ location: Optional[tuple[int, int]] = None
196
+ declaration: Optional[str] = None
197
+
198
+
199
+ class ValidationResult(BaseModel):
200
+ """Result of validation."""
201
+
202
+ valid: bool
203
+ errors: list[ValidationMessage] = Field(default_factory=list)
204
+ warnings: list[ValidationMessage] = Field(default_factory=list)
205
+ registry: Optional[ProcedureRegistry] = None
206
+
207
+
208
+ class RegistryBuilder:
209
+ """Builds ProcedureRegistry from DSL function calls."""
210
+
211
+ def __init__(self):
212
+ self.registry = ProcedureRegistry()
213
+ self.validation_messages: list[ValidationMessage] = []
214
+
215
+ def register_input_schema(self, schema: dict) -> None:
216
+ """Register input schema declaration."""
217
+ self.registry.input_schema = schema
218
+
219
+ def register_output_schema(self, schema: dict) -> None:
220
+ """Register output schema declaration."""
221
+ self.registry.output_schema = schema
222
+
223
+ def register_state_schema(self, schema: dict) -> None:
224
+ """Register state schema declaration."""
225
+ self.registry.state_schema = schema
226
+
227
+ def register_agent(self, name: str, config: dict, output_schema: Optional[dict] = None) -> None:
228
+ """Register an agent declaration."""
229
+ config["name"] = name
230
+
231
+ # Add output_schema to config if provided
232
+ if output_schema:
233
+ # Convert output_schema dict to AgentOutputSchema
234
+ fields = {}
235
+ for field_name, field_config in output_schema.items():
236
+ # Add the field name to the config (required by OutputFieldDeclaration)
237
+ field_config_with_name = dict(field_config)
238
+ field_config_with_name["name"] = field_name
239
+ fields[field_name] = OutputFieldDeclaration(**field_config_with_name)
240
+ config["output"] = AgentOutputSchema(fields=fields)
241
+
242
+ # Apply defaults
243
+ if "provider" not in config and self.registry.default_provider:
244
+ config["provider"] = self.registry.default_provider
245
+ if "model" not in config and self.registry.default_model:
246
+ config["model"] = self.registry.default_model
247
+ try:
248
+ self.registry.agents[name] = AgentDeclaration(**config)
249
+ except ValidationError as e:
250
+ self._add_error(f"Invalid agent '{name}': {e}")
251
+
252
+ def register_model(self, name: str, config: dict) -> None:
253
+ """Register a model declaration."""
254
+ config["name"] = name
255
+ self.registry.models[name] = config
256
+
257
+ def register_hitl(self, name: str, config: dict) -> None:
258
+ """Register a HITL interaction point."""
259
+ config["name"] = name
260
+ try:
261
+ self.registry.hitl_points[name] = HITLDeclaration(**config)
262
+ except ValidationError as e:
263
+ self._add_error(f"Invalid HITL point '{name}': {e}")
264
+
265
+ def register_dependency(self, name: str, config: dict) -> None:
266
+ """Register a dependency declaration."""
267
+ # The config dict contains the type and all other configuration
268
+ dependency_config = {
269
+ "name": name,
270
+ "type": config.get("type"),
271
+ "config": config, # Store the entire config dict
272
+ }
273
+ try:
274
+ self.registry.dependencies[name] = DependencyDeclaration(**dependency_config)
275
+ except ValidationError as e:
276
+ self._add_error(f"Invalid dependency '{name}': {e}")
277
+
278
+ def register_prompt(self, name: str, content: str) -> None:
279
+ """Register a prompt template."""
280
+ self.registry.prompts[name] = content
281
+
282
+ def register_toolset(self, name: str, config: dict) -> None:
283
+ """Register a toolset definition from DSL."""
284
+ self.registry.toolsets[name] = config
285
+
286
+ def register_tool(self, name: str, config: dict, lua_handler: Any) -> None:
287
+ """Register an individual Lua tool declaration.
288
+
289
+ Args:
290
+ name: Tool name
291
+ config: Dict with description, input, output schemas, and source info
292
+ lua_handler: Lupa function reference (or placeholder for external sources)
293
+ """
294
+ tool_def = {
295
+ "description": config.get("description", ""),
296
+ "input": config.get("input", {}), # Changed from parameters
297
+ "output": config.get("output", {}), # New: output schema
298
+ "handler": lua_handler,
299
+ }
300
+
301
+ # If this tool references an external source, store that info
302
+ if "source" in config:
303
+ tool_def["source"] = config["source"]
304
+
305
+ self.registry.lua_tools[name] = tool_def
306
+
307
+ def register_mock(self, tool_name: str, config: dict) -> None:
308
+ """Register a mock configuration for a tool.
309
+
310
+ Args:
311
+ tool_name: Name of the tool to mock
312
+ config: Mock configuration (output, temporal, conditional_mocks, error)
313
+ """
314
+ self.registry.mocks[tool_name] = config
315
+
316
+ def register_agent_mock(self, agent_name: str, config: dict) -> None:
317
+ """Register a mock configuration for an agent.
318
+
319
+ Args:
320
+ agent_name: Name of the agent to mock
321
+ config: Mock configuration with tool_calls and message
322
+ """
323
+ try:
324
+ self.registry.agent_mocks[agent_name] = AgentMockConfig(**config)
325
+ except Exception as e:
326
+ self._add_error(f"Invalid agent mock config for '{agent_name}': {e}")
327
+
328
+ def register_specification(self, name: str, scenarios: list) -> None:
329
+ """Register a BDD specification."""
330
+ try:
331
+ spec = SpecificationDeclaration(
332
+ name=name, scenarios=[ScenarioDeclaration(**s) for s in scenarios]
333
+ )
334
+ self.registry.specifications.append(spec)
335
+ except ValidationError as e:
336
+ self._add_error(f"Invalid specification '{name}': {e}")
337
+
338
+ def register_named_procedure(
339
+ self,
340
+ name: str,
341
+ lua_function: Any,
342
+ input_schema: dict[str, Any],
343
+ output_schema: dict[str, Any],
344
+ state_schema: dict[str, Any],
345
+ ) -> None:
346
+ """
347
+ Register a named procedure for in-file calling.
348
+
349
+ Args:
350
+ name: Procedure name
351
+ lua_function: Lua function reference
352
+ input_schema: Input validation schema
353
+ output_schema: Output validation schema
354
+ state_schema: State initialization schema
355
+ """
356
+ self.registry.named_procedures[name] = {
357
+ "function": lua_function,
358
+ "input_schema": input_schema,
359
+ "output_schema": output_schema,
360
+ "state_schema": state_schema,
361
+ }
362
+
363
+ # If this is the main entry point, also populate the top-level schemas so
364
+ # runtime output validation and tooling use a single canonical `output`.
365
+ if name == "main":
366
+ self.registry.input_schema = input_schema
367
+ self.registry.output_schema = output_schema
368
+ self.registry.state_schema = state_schema
369
+
370
+ def register_top_level_input(self, schema: dict) -> None:
371
+ """Register top-level input schema for script mode."""
372
+ self.registry.top_level_input_schema = schema
373
+ self.registry.script_mode = True
374
+
375
+ def register_top_level_output(self, schema: dict) -> None:
376
+ """Register top-level output schema for script mode."""
377
+ self.registry.top_level_output_schema = schema
378
+ self.registry.script_mode = True
379
+
380
+ def set_default_provider(self, provider: str) -> None:
381
+ """Set default provider for agents."""
382
+ self.registry.default_provider = provider
383
+
384
+ def set_default_model(self, model: str) -> None:
385
+ """Set default model for agents."""
386
+ self.registry.default_model = model
387
+
388
+ def set_return_prompt(self, prompt: str) -> None:
389
+ """Set return prompt."""
390
+ self.registry.return_prompt = prompt
391
+
392
+ def set_error_prompt(self, prompt: str) -> None:
393
+ """Set error prompt."""
394
+ self.registry.error_prompt = prompt
395
+
396
+ def set_status_prompt(self, prompt: str) -> None:
397
+ """Set status prompt."""
398
+ self.registry.status_prompt = prompt
399
+
400
+ def set_async(self, enabled: bool) -> None:
401
+ """Set async execution flag."""
402
+ self.registry.async_enabled = enabled
403
+
404
+ def set_max_depth(self, depth: int) -> None:
405
+ """Set maximum recursion depth."""
406
+ self.registry.max_depth = depth
407
+
408
+ def set_max_turns(self, turns: int) -> None:
409
+ """Set maximum turns."""
410
+ self.registry.max_turns = turns
411
+
412
+ def register_specifications(self, gherkin_text: str) -> None:
413
+ """Register Gherkin BDD specifications."""
414
+ self.registry.gherkin_specifications = gherkin_text
415
+
416
+ def register_custom_step(self, step_text: str, lua_function: Any) -> None:
417
+ """Register a custom step definition."""
418
+ self.registry.custom_steps[step_text] = lua_function
419
+
420
+ def set_evaluation_config(self, config: dict) -> None:
421
+ """Set evaluation configuration."""
422
+ self.registry.evaluation_config = config
423
+
424
+ def set_message_history_config(self, config: dict) -> None:
425
+ """Set procedure-level message history configuration."""
426
+ self.registry.message_history_config = config
427
+
428
+ def register_evaluations(self, config: dict) -> None:
429
+ """Register Pydantic Evals evaluation configuration."""
430
+ self.registry.pydantic_evaluations = config
431
+
432
+ def _add_error(self, message: str) -> None:
433
+ """Add an error message."""
434
+ self.validation_messages.append(ValidationMessage(level="error", message=message))
435
+
436
+ def _add_warning(self, message: str) -> None:
437
+ """Add a warning message."""
438
+ self.validation_messages.append(ValidationMessage(level="warning", message=message))
439
+
440
+ def validate(self) -> ValidationResult:
441
+ """Run all validations after declarations collected."""
442
+ errors = []
443
+ warnings = []
444
+
445
+ # Script mode: merge top-level schemas into main procedure
446
+ if self.registry.script_mode and "main" in self.registry.named_procedures:
447
+ main_proc = self.registry.named_procedures["main"]
448
+ # Merge top-level input schema if main doesn't have one
449
+ if not main_proc["input_schema"] and self.registry.top_level_input_schema:
450
+ main_proc["input_schema"] = self.registry.top_level_input_schema
451
+ # Merge top-level output schema if main doesn't have one
452
+ if not main_proc["output_schema"] and self.registry.top_level_output_schema:
453
+ main_proc["output_schema"] = self.registry.top_level_output_schema
454
+
455
+ # Check for multiple unnamed Procedures (all would register as "main")
456
+ # Count how many times a procedure was registered as "main"
457
+ main_count = sum(1 for name in self.registry.named_procedures.keys() if name == "main")
458
+ if main_count > 1:
459
+ errors.append(
460
+ ValidationMessage(
461
+ level="error",
462
+ 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.",
463
+ )
464
+ )
465
+
466
+ # Note: With immediate agent creation, Procedures are optional.
467
+ # Top-level code can execute directly without being wrapped in a Procedure.
468
+
469
+ # Agent validation
470
+ for agent in self.registry.agents.values():
471
+ # Check if agent has provider or if there's a default
472
+ if not agent.provider and not self.registry.default_provider:
473
+ errors.append(
474
+ ValidationMessage(
475
+ level="error",
476
+ message=f"Agent '{agent.name}' missing provider",
477
+ declaration=agent.name,
478
+ )
479
+ )
480
+
481
+ # Warnings for missing specifications
482
+ if not self.registry.specifications and not self.registry.gherkin_specifications:
483
+ warnings.append(
484
+ ValidationMessage(
485
+ level="warning",
486
+ message="No specifications defined - consider adding BDD tests using specifications([[...]])",
487
+ )
488
+ )
489
+
490
+ # Add any errors from registration
491
+ errors.extend([m for m in self.validation_messages if m.level == "error"])
492
+ warnings.extend([m for m in self.validation_messages if m.level == "warning"])
493
+
494
+ return ValidationResult(
495
+ valid=len(errors) == 0,
496
+ errors=errors,
497
+ warnings=warnings,
498
+ registry=self.registry if len(errors) == 0 else None,
499
+ )