tactus 0.30.0__py3-none-any.whl → 0.31.1__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/lua_tools.py +23 -1
- tactus/adapters/mcp_manager.py +62 -35
- tactus/broker/server.py +314 -0
- tactus/cli/app.py +11 -1
- tactus/core/dsl_stubs.py +138 -41
- tactus/core/output_validator.py +69 -15
- tactus/core/registry.py +13 -25
- tactus/core/runtime.py +208 -69
- tactus/dspy/agent.py +87 -30
- tactus/ide/server.py +0 -10
- tactus/primitives/__init__.py +0 -2
- tactus/primitives/handles.py +8 -3
- tactus/primitives/procedure_callable.py +36 -0
- tactus/protocols/config.py +0 -5
- tactus/protocols/result.py +3 -3
- tactus/stdlib/tac/tactus/tools/done.tac +1 -1
- tactus/stdlib/tac/tactus/tools/log.tac +1 -1
- tactus/testing/README.md +1 -12
- tactus/testing/behave_integration.py +12 -2
- tactus/testing/context.py +156 -46
- tactus/testing/mock_agent.py +43 -8
- tactus/testing/steps/builtin.py +264 -54
- tactus/testing/test_runner.py +6 -0
- tactus/validation/semantic_visitor.py +19 -11
- {tactus-0.30.0.dist-info → tactus-0.31.1.dist-info}/METADATA +9 -11
- {tactus-0.30.0.dist-info → tactus-0.31.1.dist-info}/RECORD +30 -31
- tactus/primitives/stage.py +0 -202
- {tactus-0.30.0.dist-info → tactus-0.31.1.dist-info}/WHEEL +0 -0
- {tactus-0.30.0.dist-info → tactus-0.31.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.30.0.dist-info → tactus-0.31.1.dist-info}/licenses/LICENSE +0 -0
tactus/core/dsl_stubs.py
CHANGED
|
@@ -469,20 +469,21 @@ def create_dsl_stubs(
|
|
|
469
469
|
|
|
470
470
|
return accept_config
|
|
471
471
|
|
|
472
|
-
def
|
|
473
|
-
"""Register
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
472
|
+
def _specification(*args) -> None:
|
|
473
|
+
"""Register BDD specs.
|
|
474
|
+
|
|
475
|
+
Supported forms:
|
|
476
|
+
- Specification([[ Gherkin text ]]) (alias for Specifications)
|
|
477
|
+
- Specification("name", { ... }) (structured form; legacy)
|
|
478
|
+
"""
|
|
479
|
+
if len(args) == 1:
|
|
480
|
+
builder.register_specifications(args[0])
|
|
481
|
+
return
|
|
482
|
+
if len(args) >= 2:
|
|
483
|
+
spec_name, scenarios = args[0], args[1]
|
|
484
|
+
builder.register_specification(spec_name, lua_table_to_dict(scenarios))
|
|
485
|
+
return
|
|
486
|
+
raise TypeError("Specification expects either (gherkin_text) or (name, scenarios)")
|
|
486
487
|
|
|
487
488
|
def _specifications(gherkin_text: str) -> None:
|
|
488
489
|
"""Register Gherkin BDD specifications."""
|
|
@@ -493,8 +494,17 @@ def create_dsl_stubs(
|
|
|
493
494
|
builder.register_custom_step(step_text, lua_function)
|
|
494
495
|
|
|
495
496
|
def _evaluation(config) -> None:
|
|
496
|
-
"""
|
|
497
|
-
|
|
497
|
+
"""Register evaluation configuration.
|
|
498
|
+
|
|
499
|
+
Supported forms:
|
|
500
|
+
- Evaluation({ runs=..., parallel=... }) (single-run config)
|
|
501
|
+
- Evaluation({ dataset=..., evaluators=..., ...}) (alias for Evaluations)
|
|
502
|
+
"""
|
|
503
|
+
config_dict = lua_table_to_dict(config or {})
|
|
504
|
+
if any(k in config_dict for k in ("dataset", "dataset_file", "evaluators", "thresholds")):
|
|
505
|
+
builder.register_evaluations(config_dict)
|
|
506
|
+
return
|
|
507
|
+
builder.set_evaluation_config(config_dict)
|
|
498
508
|
|
|
499
509
|
def _evaluations(config) -> None:
|
|
500
510
|
"""Register Pydantic Evals evaluation configuration."""
|
|
@@ -690,6 +700,28 @@ def create_dsl_stubs(
|
|
|
690
700
|
"integer": _field_builder("integer"),
|
|
691
701
|
}
|
|
692
702
|
|
|
703
|
+
def _evaluator_builder(evaluator_type: str):
|
|
704
|
+
"""Create a simple evaluator config builder for Evaluation(s)({ evaluators = {...} })."""
|
|
705
|
+
|
|
706
|
+
def build_evaluator(options=None):
|
|
707
|
+
if options is None:
|
|
708
|
+
options = {}
|
|
709
|
+
if hasattr(options, "items"):
|
|
710
|
+
options = lua_table_to_dict(options)
|
|
711
|
+
if not isinstance(options, dict):
|
|
712
|
+
options = {}
|
|
713
|
+
cfg = {"type": evaluator_type}
|
|
714
|
+
cfg.update(options)
|
|
715
|
+
return cfg
|
|
716
|
+
|
|
717
|
+
return build_evaluator
|
|
718
|
+
|
|
719
|
+
# Evaluation(s)() helper constructors (Pydantic Evals integration).
|
|
720
|
+
# These are configuration builders, not runtime behavior.
|
|
721
|
+
field["equals_expected"] = _evaluator_builder("equals_expected")
|
|
722
|
+
field["min_length"] = _evaluator_builder("min_length")
|
|
723
|
+
field["contains"] = _evaluator_builder("contains")
|
|
724
|
+
|
|
693
725
|
# Create lookup functions for uppercase names (Agent, Model)
|
|
694
726
|
# These allow: Agent("greeter")(), Model("classifier")() (callable syntax)
|
|
695
727
|
_Agent = AgentLookup(_agent_registry)
|
|
@@ -1256,6 +1288,12 @@ def create_dsl_stubs(
|
|
|
1256
1288
|
if len(config_dict) == 0:
|
|
1257
1289
|
config_dict = {}
|
|
1258
1290
|
|
|
1291
|
+
# Normalize empty schemas (lua {} -> python []) so tools treat empty schemas
|
|
1292
|
+
# as empty objects, not arrays.
|
|
1293
|
+
if isinstance(config_dict, dict):
|
|
1294
|
+
config_dict["input"] = _normalize_schema(config_dict.get("input", {}))
|
|
1295
|
+
config_dict["output"] = _normalize_schema(config_dict.get("output", {}))
|
|
1296
|
+
|
|
1259
1297
|
# Check for legacy handler field
|
|
1260
1298
|
if handler_fn is None and isinstance(config_dict, dict):
|
|
1261
1299
|
handler_fn = config_dict.pop("handler", None)
|
|
@@ -1429,6 +1467,12 @@ def create_dsl_stubs(
|
|
|
1429
1467
|
if len(config_dict) == 0:
|
|
1430
1468
|
config_dict = {}
|
|
1431
1469
|
|
|
1470
|
+
# Normalize empty schemas (lua {} -> python []) so tools treat empty schemas
|
|
1471
|
+
# as empty objects, not arrays.
|
|
1472
|
+
if isinstance(config_dict, dict):
|
|
1473
|
+
config_dict["input"] = _normalize_schema(config_dict.get("input", {}))
|
|
1474
|
+
config_dict["output"] = _normalize_schema(config_dict.get("output", {}))
|
|
1475
|
+
|
|
1432
1476
|
# Check for legacy handler field
|
|
1433
1477
|
if handler_fn is None and isinstance(config_dict, dict):
|
|
1434
1478
|
handler_fn = config_dict.pop("handler", None)
|
|
@@ -1573,19 +1617,44 @@ def create_dsl_stubs(
|
|
|
1573
1617
|
"""
|
|
1574
1618
|
config_dict = lua_table_to_dict(config)
|
|
1575
1619
|
|
|
1576
|
-
#
|
|
1620
|
+
# No alias support: toolsets -> tools is not supported.
|
|
1621
|
+
if "toolsets" in config_dict:
|
|
1622
|
+
raise ValueError(
|
|
1623
|
+
f"Agent '{agent_name}': 'toolsets' is not supported. Use 'tools' for tool/toolset references."
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
# inline_tools: inline tool definitions only (list of dicts with "handler")
|
|
1627
|
+
if "inline_tools" in config_dict:
|
|
1628
|
+
inline_tools = config_dict["inline_tools"]
|
|
1629
|
+
if isinstance(inline_tools, (list, tuple)):
|
|
1630
|
+
non_dict_items = [t for t in inline_tools if not isinstance(t, dict)]
|
|
1631
|
+
if non_dict_items:
|
|
1632
|
+
raise ValueError(
|
|
1633
|
+
f"Agent '{agent_name}': 'inline_tools' must be a list of inline tool definitions."
|
|
1634
|
+
)
|
|
1635
|
+
elif inline_tools is not None:
|
|
1636
|
+
raise ValueError(
|
|
1637
|
+
f"Agent '{agent_name}': 'inline_tools' must be a list of inline tool definitions."
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
# tools: tool/toolset references and toolset expressions (filter dicts)
|
|
1577
1641
|
if "tools" in config_dict:
|
|
1578
1642
|
tools = config_dict["tools"]
|
|
1579
1643
|
if isinstance(tools, (list, tuple)):
|
|
1580
|
-
|
|
1644
|
+
normalized = []
|
|
1581
1645
|
for t in tools:
|
|
1582
|
-
if
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1646
|
+
if isinstance(t, dict):
|
|
1647
|
+
if "handler" in t:
|
|
1648
|
+
raise ValueError(
|
|
1649
|
+
f"Agent '{agent_name}': inline tool definitions must be in 'inline_tools', not 'tools'."
|
|
1650
|
+
)
|
|
1651
|
+
normalized.append(t)
|
|
1652
|
+
continue
|
|
1653
|
+
if hasattr(t, "name"): # ToolHandle or ToolsetHandle
|
|
1654
|
+
normalized.append(t.name)
|
|
1655
|
+
else:
|
|
1656
|
+
normalized.append(t)
|
|
1657
|
+
config_dict["tools"] = normalized
|
|
1589
1658
|
|
|
1590
1659
|
# Extract input schema if present
|
|
1591
1660
|
input_schema = None
|
|
@@ -1605,9 +1674,11 @@ def create_dsl_stubs(
|
|
|
1605
1674
|
config_dict["output_schema"] = output_schema
|
|
1606
1675
|
del config_dict["output"]
|
|
1607
1676
|
|
|
1608
|
-
#
|
|
1609
|
-
if "session" in config_dict
|
|
1610
|
-
|
|
1677
|
+
# No compatibility aliases: session -> message_history is not supported.
|
|
1678
|
+
if "session" in config_dict:
|
|
1679
|
+
raise ValueError(
|
|
1680
|
+
f"Agent '{agent_name}': 'session' is not supported. Use 'message_history'."
|
|
1681
|
+
)
|
|
1611
1682
|
|
|
1612
1683
|
# Register agent with provided name
|
|
1613
1684
|
builder.register_agent(agent_name, config_dict, output_schema)
|
|
@@ -1635,6 +1706,12 @@ def create_dsl_stubs(
|
|
|
1635
1706
|
# expects name as a separate parameter. We need to pass config without 'name'.
|
|
1636
1707
|
agent_config = {k: v for k, v in config_dict.items() if k != "name"}
|
|
1637
1708
|
|
|
1709
|
+
# Agent DSL uses `tools` for tool/toolset references; the DSPy agent config uses
|
|
1710
|
+
# `toolsets` for the resolved toolsets list.
|
|
1711
|
+
if "tools" in agent_config and "toolsets" not in agent_config:
|
|
1712
|
+
agent_config["toolsets"] = agent_config["tools"]
|
|
1713
|
+
del agent_config["tools"]
|
|
1714
|
+
|
|
1638
1715
|
# Pre-process model format: combine provider and model into "provider:model"
|
|
1639
1716
|
# This matches what _setup_agents does
|
|
1640
1717
|
if "provider" in agent_config and "model" in agent_config:
|
|
@@ -1729,19 +1806,40 @@ def create_dsl_stubs(
|
|
|
1729
1806
|
|
|
1730
1807
|
config_dict = lua_table_to_dict(config)
|
|
1731
1808
|
|
|
1732
|
-
#
|
|
1809
|
+
# No alias support: toolsets -> tools is not supported.
|
|
1810
|
+
if "toolsets" in config_dict:
|
|
1811
|
+
raise ValueError("Agent: 'toolsets' is not supported. Use 'tools'.")
|
|
1812
|
+
|
|
1813
|
+
# inline_tools: inline tool definitions only (list of dicts with "handler")
|
|
1814
|
+
if "inline_tools" in config_dict:
|
|
1815
|
+
inline_tools = config_dict["inline_tools"]
|
|
1816
|
+
if isinstance(inline_tools, (list, tuple)):
|
|
1817
|
+
non_dict_items = [t for t in inline_tools if not isinstance(t, dict)]
|
|
1818
|
+
if non_dict_items:
|
|
1819
|
+
raise ValueError(
|
|
1820
|
+
"Agent: 'inline_tools' must be a list of inline tool definitions."
|
|
1821
|
+
)
|
|
1822
|
+
elif inline_tools is not None:
|
|
1823
|
+
raise ValueError("Agent: 'inline_tools' must be a list of inline tool definitions.")
|
|
1824
|
+
|
|
1825
|
+
# tools: tool/toolset references and toolset expressions (filter dicts)
|
|
1733
1826
|
if "tools" in config_dict:
|
|
1734
1827
|
tools = config_dict["tools"]
|
|
1735
1828
|
if isinstance(tools, (list, tuple)):
|
|
1736
|
-
|
|
1829
|
+
normalized = []
|
|
1737
1830
|
for t in tools:
|
|
1738
|
-
if
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1831
|
+
if isinstance(t, dict):
|
|
1832
|
+
if "handler" in t:
|
|
1833
|
+
raise ValueError(
|
|
1834
|
+
"Agent: inline tool definitions must be in 'inline_tools', not 'tools'."
|
|
1835
|
+
)
|
|
1836
|
+
normalized.append(t)
|
|
1837
|
+
continue
|
|
1838
|
+
if hasattr(t, "name"): # ToolHandle or ToolsetHandle
|
|
1839
|
+
normalized.append(t.name)
|
|
1840
|
+
else:
|
|
1841
|
+
normalized.append(t)
|
|
1842
|
+
config_dict["tools"] = normalized
|
|
1745
1843
|
|
|
1746
1844
|
# Extract input schema if present
|
|
1747
1845
|
input_schema = None
|
|
@@ -1761,9 +1859,9 @@ def create_dsl_stubs(
|
|
|
1761
1859
|
config_dict["output_schema"] = output_schema
|
|
1762
1860
|
del config_dict["output"]
|
|
1763
1861
|
|
|
1764
|
-
#
|
|
1765
|
-
if "session" in config_dict
|
|
1766
|
-
|
|
1862
|
+
# No compatibility aliases: session -> message_history is not supported.
|
|
1863
|
+
if "session" in config_dict:
|
|
1864
|
+
raise ValueError("Agent: 'session' is not supported. Use 'message_history'.")
|
|
1767
1865
|
|
|
1768
1866
|
# Generate a temporary name - will be replaced when assigned
|
|
1769
1867
|
import uuid
|
|
@@ -1863,7 +1961,6 @@ def create_dsl_stubs(
|
|
|
1863
1961
|
"Toolset": _toolset,
|
|
1864
1962
|
"Tool": _new_tool, # NEW syntax - assignment based
|
|
1865
1963
|
"Hitl": _hitl,
|
|
1866
|
-
"Stages": _stages,
|
|
1867
1964
|
"Specification": _specification,
|
|
1868
1965
|
# BDD Testing
|
|
1869
1966
|
"Specifications": _specifications,
|
tactus/core/output_validator.py
CHANGED
|
@@ -6,7 +6,7 @@ Enables type safety and composability for sub-agent workflows.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any, Optional, List
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -37,7 +37,16 @@ class OutputValidator:
|
|
|
37
37
|
"array": list,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
@classmethod
|
|
41
|
+
def _is_scalar_schema(cls, schema: Any) -> bool:
|
|
42
|
+
return (
|
|
43
|
+
isinstance(schema, dict)
|
|
44
|
+
and "type" in schema
|
|
45
|
+
and isinstance(schema.get("type"), str)
|
|
46
|
+
and schema.get("type") in cls.TYPE_MAP
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(self, output_schema: Optional[Any] = None):
|
|
41
50
|
"""
|
|
42
51
|
Initialize validator with output schema.
|
|
43
52
|
|
|
@@ -57,9 +66,17 @@ class OutputValidator:
|
|
|
57
66
|
}
|
|
58
67
|
"""
|
|
59
68
|
self.schema = output_schema or {}
|
|
60
|
-
logger.debug(f"OutputValidator initialized with {len(self.schema)} output fields")
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
if self._is_scalar_schema(self.schema):
|
|
71
|
+
logger.debug("OutputValidator initialized with scalar output schema")
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
field_count = len(self.schema)
|
|
75
|
+
except TypeError:
|
|
76
|
+
field_count = 0
|
|
77
|
+
logger.debug(f"OutputValidator initialized with {field_count} output fields")
|
|
78
|
+
|
|
79
|
+
def validate(self, output: Any) -> Any:
|
|
63
80
|
"""
|
|
64
81
|
Validate workflow output against schema.
|
|
65
82
|
|
|
@@ -72,16 +89,56 @@ class OutputValidator:
|
|
|
72
89
|
Raises:
|
|
73
90
|
OutputValidationError: If validation fails
|
|
74
91
|
"""
|
|
92
|
+
# If a procedure returns a Result wrapper, validate its `.output` payload
|
|
93
|
+
# while preserving the wrapper (so callers can still access usage/cost/etc.).
|
|
94
|
+
from tactus.protocols.result import TactusResult
|
|
95
|
+
|
|
96
|
+
wrapped_result: TactusResult | None = output if isinstance(output, TactusResult) else None
|
|
97
|
+
if wrapped_result is not None:
|
|
98
|
+
output = wrapped_result.output
|
|
99
|
+
|
|
75
100
|
# If no schema defined, accept any output
|
|
76
101
|
if not self.schema:
|
|
77
102
|
logger.debug("No output schema defined, skipping validation")
|
|
78
103
|
if isinstance(output, dict):
|
|
79
|
-
|
|
104
|
+
validated_payload = output
|
|
80
105
|
elif hasattr(output, "items"):
|
|
81
106
|
# Lua table - convert to dict
|
|
82
|
-
|
|
107
|
+
validated_payload = dict(output.items())
|
|
83
108
|
else:
|
|
84
|
-
|
|
109
|
+
validated_payload = output
|
|
110
|
+
|
|
111
|
+
if wrapped_result is not None:
|
|
112
|
+
return wrapped_result.model_copy(update={"output": validated_payload})
|
|
113
|
+
return validated_payload
|
|
114
|
+
|
|
115
|
+
# Scalar output schema: `output = field.string{...}` etc.
|
|
116
|
+
if self._is_scalar_schema(self.schema):
|
|
117
|
+
# Lua tables are not valid scalar outputs.
|
|
118
|
+
if hasattr(output, "items") and not isinstance(output, dict):
|
|
119
|
+
output = dict(output.items())
|
|
120
|
+
|
|
121
|
+
is_required = self.schema.get("required", False)
|
|
122
|
+
if output is None and not is_required:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
expected_type = self.schema.get("type")
|
|
126
|
+
if expected_type and not self._check_type(output, expected_type):
|
|
127
|
+
raise OutputValidationError(
|
|
128
|
+
f"Output should be {expected_type}, got {type(output).__name__}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if "enum" in self.schema and self.schema["enum"]:
|
|
132
|
+
allowed_values = self.schema["enum"]
|
|
133
|
+
if output not in allowed_values:
|
|
134
|
+
raise OutputValidationError(
|
|
135
|
+
f"Output has invalid value '{output}'. Allowed values: {allowed_values}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
validated_payload = output
|
|
139
|
+
if wrapped_result is not None:
|
|
140
|
+
return wrapped_result.model_copy(update={"output": validated_payload})
|
|
141
|
+
return validated_payload
|
|
85
142
|
|
|
86
143
|
# Convert Lua tables to dicts recursively
|
|
87
144
|
if hasattr(output, "items") or isinstance(output, dict):
|
|
@@ -99,16 +156,13 @@ class OutputValidator:
|
|
|
99
156
|
|
|
100
157
|
# Check required fields and validate types
|
|
101
158
|
for field_name, field_def in self.schema.items():
|
|
102
|
-
|
|
103
|
-
from tactus.core.dsl_stubs import FieldDefinition
|
|
104
|
-
|
|
105
|
-
if not isinstance(field_def, FieldDefinition):
|
|
159
|
+
if not isinstance(field_def, dict) or "type" not in field_def:
|
|
106
160
|
errors.append(
|
|
107
161
|
f"Field '{field_name}' uses old type syntax. "
|
|
108
162
|
f"Use field.{field_def.get('type', 'string')}{{}} instead."
|
|
109
163
|
)
|
|
110
164
|
continue
|
|
111
|
-
is_required = field_def.get("required", False)
|
|
165
|
+
is_required = bool(field_def.get("required", False))
|
|
112
166
|
|
|
113
167
|
if is_required and field_name not in output:
|
|
114
168
|
errors.append(f"Required field '{field_name}' is missing")
|
|
@@ -151,6 +205,8 @@ class OutputValidator:
|
|
|
151
205
|
raise OutputValidationError(error_msg)
|
|
152
206
|
|
|
153
207
|
logger.info(f"Output validation passed for {len(validated_output)} fields")
|
|
208
|
+
if wrapped_result is not None:
|
|
209
|
+
return wrapped_result.model_copy(update={"output": validated_output})
|
|
154
210
|
return validated_output
|
|
155
211
|
|
|
156
212
|
def _check_type(self, value: Any, expected_type: str) -> bool:
|
|
@@ -209,10 +265,8 @@ class OutputValidator:
|
|
|
209
265
|
def get_field_description(self, field_name: str) -> Optional[str]:
|
|
210
266
|
"""Get description for an output field."""
|
|
211
267
|
if field_name in self.schema:
|
|
212
|
-
from tactus.core.dsl_stubs import FieldDefinition
|
|
213
|
-
|
|
214
268
|
field_def = self.schema[field_name]
|
|
215
|
-
if isinstance(field_def,
|
|
269
|
+
if isinstance(field_def, dict):
|
|
216
270
|
return field_def.get("description")
|
|
217
271
|
return None
|
|
218
272
|
|
tactus/core/registry.py
CHANGED
|
@@ -48,13 +48,8 @@ class AgentDeclaration(BaseModel):
|
|
|
48
48
|
model: Union[str, dict[str, Any]] = "gpt-4o"
|
|
49
49
|
system_prompt: Union[str, Any] # String with {markers} or Lua function
|
|
50
50
|
initial_message: Optional[str] = None
|
|
51
|
-
tools: list[
|
|
52
|
-
|
|
53
|
-
) # Supports toolset expressions
|
|
54
|
-
inline_tool_defs: list[dict[str, Any]] = Field(
|
|
55
|
-
default_factory=list
|
|
56
|
-
) # Inline tool definitions with Lua handlers
|
|
57
|
-
output: Optional[AgentOutputSchema] = None # Legacy field
|
|
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
|
|
58
53
|
output: Optional[AgentOutputSchema] = None # Aligned with pydantic-ai
|
|
59
54
|
message_history: Optional[MessageHistoryConfiguration] = None
|
|
60
55
|
max_turns: int = 50
|
|
@@ -127,6 +122,10 @@ class AgentMockConfig(BaseModel):
|
|
|
127
122
|
default_factory=dict,
|
|
128
123
|
description="Optional token usage payload (exposed as result.usage in Lua)",
|
|
129
124
|
)
|
|
125
|
+
temporal: list[dict[str, Any]] = Field(
|
|
126
|
+
default_factory=list,
|
|
127
|
+
description="Optional temporal mock turns (1-indexed by agent turn).",
|
|
128
|
+
)
|
|
130
129
|
|
|
131
130
|
|
|
132
131
|
class ProcedureRegistry(BaseModel):
|
|
@@ -146,7 +145,6 @@ class ProcedureRegistry(BaseModel):
|
|
|
146
145
|
toolsets: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
147
146
|
lua_tools: dict[str, dict[str, Any]] = Field(default_factory=dict) # Lua function tools
|
|
148
147
|
hitl_points: dict[str, HITLDeclaration] = Field(default_factory=dict)
|
|
149
|
-
stages: list[str] = Field(default_factory=list)
|
|
150
148
|
specifications: list[SpecificationDeclaration] = Field(default_factory=list)
|
|
151
149
|
dependencies: dict[str, DependencyDeclaration] = Field(default_factory=dict)
|
|
152
150
|
mocks: dict[str, dict[str, Any]] = Field(default_factory=dict) # Mock configurations
|
|
@@ -241,19 +239,6 @@ class RegistryBuilder:
|
|
|
241
239
|
fields[field_name] = OutputFieldDeclaration(**field_config_with_name)
|
|
242
240
|
config["output"] = AgentOutputSchema(fields=fields)
|
|
243
241
|
|
|
244
|
-
# Handle toolsets -> tools rename (backward compatibility)
|
|
245
|
-
# The Lua DSL uses "toolsets" for toolset references and "tools" for inline tool definitions
|
|
246
|
-
# AgentDeclaration expects "tools" for toolset references
|
|
247
|
-
if "toolsets" in config:
|
|
248
|
-
if "tools" in config:
|
|
249
|
-
# Both exist: tools = inline defs, toolsets = references
|
|
250
|
-
# Keep inline defs in a temp field, use toolsets as tools
|
|
251
|
-
config["inline_tool_defs"] = config.pop("tools")
|
|
252
|
-
config["tools"] = config.pop("toolsets")
|
|
253
|
-
else:
|
|
254
|
-
# Only toolsets exists: rename to tools
|
|
255
|
-
config["tools"] = config.pop("toolsets")
|
|
256
|
-
|
|
257
242
|
# Apply defaults
|
|
258
243
|
if "provider" not in config and self.registry.default_provider:
|
|
259
244
|
config["provider"] = self.registry.default_provider
|
|
@@ -340,10 +325,6 @@ class RegistryBuilder:
|
|
|
340
325
|
except Exception as e:
|
|
341
326
|
self._add_error(f"Invalid agent mock config for '{agent_name}': {e}")
|
|
342
327
|
|
|
343
|
-
def set_stages(self, stage_names: list[str]) -> None:
|
|
344
|
-
"""Set stage names."""
|
|
345
|
-
self.registry.stages = stage_names
|
|
346
|
-
|
|
347
328
|
def register_specification(self, name: str, scenarios: list) -> None:
|
|
348
329
|
"""Register a BDD specification."""
|
|
349
330
|
try:
|
|
@@ -379,6 +360,13 @@ class RegistryBuilder:
|
|
|
379
360
|
"state_schema": state_schema,
|
|
380
361
|
}
|
|
381
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
|
+
|
|
382
370
|
def register_top_level_input(self, schema: dict) -> None:
|
|
383
371
|
"""Register top-level input schema for script mode."""
|
|
384
372
|
self.registry.top_level_input_schema = schema
|