tactus 0.34.0__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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.0.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
|
|
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:
|
|
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:
|
|
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:
|
|
48
|
-
model:
|
|
49
|
-
system_prompt:
|
|
50
|
-
initial_message:
|
|
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:
|
|
54
|
-
message_history:
|
|
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:
|
|
60
|
-
max_tokens:
|
|
61
|
-
model_type:
|
|
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:
|
|
72
|
+
timeout: int | None = None
|
|
73
73
|
default: Any = None
|
|
74
|
-
options:
|
|
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:
|
|
85
|
-
then_output:
|
|
86
|
-
then_state:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
168
|
-
error_prompt:
|
|
169
|
-
status_prompt:
|
|
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:
|
|
176
|
-
default_model:
|
|
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:
|
|
197
|
-
declaration:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
for field_name,
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
if "model" not in
|
|
247
|
-
|
|
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(**
|
|
250
|
-
except ValidationError as
|
|
251
|
-
self._add_error(f"Invalid agent '{name}': {
|
|
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
|
|
264
|
-
self._add_error(f"Invalid HITL point '{name}': {
|
|
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
|
-
|
|
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(**
|
|
276
|
-
except ValidationError as
|
|
277
|
-
self._add_error(f"Invalid dependency '{name}': {
|
|
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
|
-
|
|
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
|
-
|
|
312
|
+
tool_definition["source"] = config["source"]
|
|
305
313
|
|
|
306
|
-
self.registry.lua_tools[name] =
|
|
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
|
|
327
|
-
self._add_error(f"Invalid agent mock config for '{agent_name}': {
|
|
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
|
|
337
|
-
self._add_error(f"Invalid specification '{name}': {
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
|
462
|
-
|
|
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
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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
|
|
485
|
-
|
|
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 '{
|
|
489
|
-
declaration=
|
|
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
|
-
|
|
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
|
|
500
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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(
|
|
513
|
-
errors=
|
|
514
|
-
warnings=
|
|
515
|
-
registry=self.registry if len(
|
|
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
|
)
|