tactus 0.34.1__py3-none-any.whl → 0.35.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/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 +40 -25
- 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.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
tactus/core/dsl_stubs.py
CHANGED
|
@@ -31,7 +31,7 @@ Agent/Tool calls use direct variable access:
|
|
|
31
31
|
done.last_result() -- Get last tool result
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
from typing import Any, Callable
|
|
34
|
+
from typing import Any, Callable
|
|
35
35
|
|
|
36
36
|
from .registry import RegistryBuilder
|
|
37
37
|
from tactus.primitives.handles import AgentHandle, ModelHandle, AgentLookup, ModelLookup
|
|
@@ -45,7 +45,7 @@ class FieldDefinition(dict):
|
|
|
45
45
|
pass
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def lua_table_to_dict(lua_table):
|
|
48
|
+
def lua_table_to_dict(lua_table: Any) -> Any:
|
|
49
49
|
"""
|
|
50
50
|
Convert lupa table to Python dict or list recursively.
|
|
51
51
|
|
|
@@ -66,35 +66,35 @@ def lua_table_to_dict(lua_table):
|
|
|
66
66
|
|
|
67
67
|
try:
|
|
68
68
|
# Get all keys
|
|
69
|
-
|
|
69
|
+
table_keys = list(lua_table.keys())
|
|
70
70
|
|
|
71
71
|
# Empty table - return empty list (common for tools = {})
|
|
72
|
-
if not
|
|
72
|
+
if not table_keys:
|
|
73
73
|
return []
|
|
74
74
|
|
|
75
75
|
# Check if it's an array (all keys are consecutive integers starting from 1)
|
|
76
|
-
if all(isinstance(
|
|
77
|
-
sorted_keys = sorted(
|
|
78
|
-
if sorted_keys == list(range(1, len(
|
|
76
|
+
if all(isinstance(key, int) for key in table_keys):
|
|
77
|
+
sorted_keys = sorted(table_keys)
|
|
78
|
+
if sorted_keys == list(range(1, len(table_keys) + 1)):
|
|
79
79
|
# It's an array
|
|
80
80
|
return [
|
|
81
81
|
(
|
|
82
|
-
lua_table_to_dict(lua_table[
|
|
83
|
-
if hasattr(lua_table[
|
|
84
|
-
else lua_table[
|
|
82
|
+
lua_table_to_dict(lua_table[key])
|
|
83
|
+
if hasattr(lua_table[key], "items")
|
|
84
|
+
else lua_table[key]
|
|
85
85
|
)
|
|
86
|
-
for
|
|
86
|
+
for key in sorted_keys
|
|
87
87
|
]
|
|
88
88
|
|
|
89
89
|
# It's a dictionary
|
|
90
|
-
|
|
90
|
+
converted_mapping: dict[Any, Any] = {}
|
|
91
91
|
for key, value in lua_table.items():
|
|
92
92
|
# Recursively convert nested tables
|
|
93
93
|
if hasattr(value, "items"):
|
|
94
|
-
|
|
94
|
+
converted_mapping[key] = lua_table_to_dict(value)
|
|
95
95
|
else:
|
|
96
|
-
|
|
97
|
-
return
|
|
96
|
+
converted_mapping[key] = value
|
|
97
|
+
return converted_mapping
|
|
98
98
|
|
|
99
99
|
except (AttributeError, TypeError):
|
|
100
100
|
# Fallback: return as-is
|
|
@@ -112,7 +112,7 @@ def create_dsl_stubs(
|
|
|
112
112
|
builder: RegistryBuilder,
|
|
113
113
|
tool_primitive: Any = None,
|
|
114
114
|
mock_manager: Any = None,
|
|
115
|
-
runtime_context:
|
|
115
|
+
runtime_context: dict[str, Any] | None = None,
|
|
116
116
|
) -> dict[str, Callable]:
|
|
117
117
|
"""
|
|
118
118
|
Create DSL stub functions that populate the registry.
|
|
@@ -133,9 +133,9 @@ def create_dsl_stubs(
|
|
|
133
133
|
- Uppercase lookup functions: Agent, Tool, Model
|
|
134
134
|
"""
|
|
135
135
|
# Registries for handle lookup
|
|
136
|
-
_agent_registry:
|
|
137
|
-
_tool_registry:
|
|
138
|
-
_model_registry:
|
|
136
|
+
_agent_registry: dict[str, AgentHandle] = {}
|
|
137
|
+
_tool_registry: dict[str, Any] = {} # ToolHandle instances
|
|
138
|
+
_model_registry: dict[str, ModelHandle] = {}
|
|
139
139
|
|
|
140
140
|
# Store runtime context for immediate agent creation
|
|
141
141
|
_runtime_context = runtime_context or {}
|
|
@@ -143,7 +143,9 @@ def create_dsl_stubs(
|
|
|
143
143
|
# Global registry for named procedure stubs to find their implementations
|
|
144
144
|
_procedure_registry = {}
|
|
145
145
|
|
|
146
|
-
def _process_procedure_config(
|
|
146
|
+
def _process_procedure_config(
|
|
147
|
+
name: str | None, config: Any, procedure_registry: dict[str, Any]
|
|
148
|
+
):
|
|
147
149
|
"""
|
|
148
150
|
Process procedure config and register the procedure.
|
|
149
151
|
|
|
@@ -163,18 +165,18 @@ def create_dsl_stubs(
|
|
|
163
165
|
name = "main"
|
|
164
166
|
# Extract the function from the raw Lua table before conversion
|
|
165
167
|
# In Lua tables, unnamed elements are stored with numeric indices (1-based)
|
|
166
|
-
|
|
168
|
+
run_function = None
|
|
167
169
|
|
|
168
170
|
# Check for function in array part of table (numeric indices)
|
|
169
171
|
if hasattr(config, "__getitem__"):
|
|
170
172
|
# Try to get function from numeric indices (Lua uses 1-based indexing)
|
|
171
|
-
for
|
|
173
|
+
for index in range(1, 10): # Check first few positions
|
|
172
174
|
try:
|
|
173
|
-
|
|
174
|
-
if callable(
|
|
175
|
-
|
|
175
|
+
candidate_item = config[index]
|
|
176
|
+
if callable(candidate_item):
|
|
177
|
+
run_function = candidate_item
|
|
176
178
|
# Remove from table so it doesn't appear in config_dict
|
|
177
|
-
config[
|
|
179
|
+
config[index] = None
|
|
178
180
|
break
|
|
179
181
|
except (KeyError, TypeError):
|
|
180
182
|
break
|
|
@@ -190,12 +192,15 @@ def create_dsl_stubs(
|
|
|
190
192
|
config_dict = [x for x in config_dict if x is not None]
|
|
191
193
|
if len(config_dict) == 0:
|
|
192
194
|
config_dict = {}
|
|
195
|
+
else:
|
|
196
|
+
# Ignore extra positional entries that cannot be mapped to config fields.
|
|
197
|
+
config_dict = {}
|
|
193
198
|
|
|
194
199
|
# If no function found in array part, check for legacy 'run' field
|
|
195
|
-
if
|
|
196
|
-
|
|
200
|
+
if run_function is None:
|
|
201
|
+
run_function = config_dict.pop("run", None)
|
|
197
202
|
|
|
198
|
-
if
|
|
203
|
+
if run_function is None:
|
|
199
204
|
raise TypeError(
|
|
200
205
|
f"Procedure '{name}' requires a function. "
|
|
201
206
|
f"Use: Procedure {{ input = {{...}}, function() ... end }}"
|
|
@@ -214,7 +219,9 @@ def create_dsl_stubs(
|
|
|
214
219
|
if dependencies_schema:
|
|
215
220
|
state_schema["_dependencies"] = dependencies_schema
|
|
216
221
|
|
|
217
|
-
builder.register_named_procedure(
|
|
222
|
+
builder.register_named_procedure(
|
|
223
|
+
name, run_function, input_schema, output_schema, state_schema
|
|
224
|
+
)
|
|
218
225
|
|
|
219
226
|
# Return a stub that will delegate to the registry at call time
|
|
220
227
|
class NamedProcedureStub:
|
|
@@ -479,15 +486,21 @@ def create_dsl_stubs(
|
|
|
479
486
|
- Specification { from = "path" } (external file reference)
|
|
480
487
|
"""
|
|
481
488
|
if len(args) == 1:
|
|
482
|
-
|
|
489
|
+
single_argument = args[0]
|
|
483
490
|
# Check if it's a table with 'from' parameter
|
|
484
|
-
if isinstance(
|
|
485
|
-
|
|
491
|
+
if isinstance(single_argument, dict) or (
|
|
492
|
+
hasattr(single_argument, "keys") and callable(single_argument.keys)
|
|
493
|
+
):
|
|
494
|
+
config = (
|
|
495
|
+
lua_table_to_dict(single_argument)
|
|
496
|
+
if not isinstance(single_argument, dict)
|
|
497
|
+
else single_argument
|
|
498
|
+
)
|
|
486
499
|
if "from" in config:
|
|
487
500
|
builder.register_specs_from(config["from"])
|
|
488
501
|
return
|
|
489
502
|
# Otherwise treat as inline Gherkin text
|
|
490
|
-
builder.register_specifications(
|
|
503
|
+
builder.register_specifications(single_argument)
|
|
491
504
|
return
|
|
492
505
|
if len(args) >= 2:
|
|
493
506
|
spec_name, scenarios = args[0], args[1]
|
|
@@ -511,7 +524,8 @@ def create_dsl_stubs(
|
|
|
511
524
|
- Evaluation({ dataset=..., evaluators=..., ...}) (alias for Evaluations)
|
|
512
525
|
"""
|
|
513
526
|
config_dict = lua_table_to_dict(config or {})
|
|
514
|
-
|
|
527
|
+
evaluation_keys = ("dataset", "dataset_file", "evaluators", "thresholds")
|
|
528
|
+
if any(key in config_dict for key in evaluation_keys):
|
|
515
529
|
builder.register_evaluations(config_dict)
|
|
516
530
|
return
|
|
517
531
|
builder.set_evaluation_config(config_dict)
|
|
@@ -557,14 +571,30 @@ def create_dsl_stubs(
|
|
|
557
571
|
"""Filter to keep last N messages."""
|
|
558
572
|
return ("last_n", n)
|
|
559
573
|
|
|
574
|
+
def _first_n(n: int) -> tuple:
|
|
575
|
+
"""Filter to keep first N messages."""
|
|
576
|
+
return ("first_n", n)
|
|
577
|
+
|
|
560
578
|
def _token_budget(max_tokens: int) -> tuple:
|
|
561
579
|
"""Filter by token budget."""
|
|
562
580
|
return ("token_budget", max_tokens)
|
|
563
581
|
|
|
582
|
+
def _head_tokens(max_tokens: int) -> tuple:
|
|
583
|
+
"""Filter to keep earliest messages within token budget."""
|
|
584
|
+
return ("head_tokens", max_tokens)
|
|
585
|
+
|
|
586
|
+
def _tail_tokens(max_tokens: int) -> tuple:
|
|
587
|
+
"""Filter to keep latest messages within token budget."""
|
|
588
|
+
return ("tail_tokens", max_tokens)
|
|
589
|
+
|
|
564
590
|
def _by_role(role: str) -> tuple:
|
|
565
591
|
"""Filter by message role."""
|
|
566
592
|
return ("by_role", role)
|
|
567
593
|
|
|
594
|
+
def _system_prefix() -> tuple:
|
|
595
|
+
"""Filter to keep leading system messages."""
|
|
596
|
+
return ("system_prefix", None)
|
|
597
|
+
|
|
568
598
|
def _compose(*filters) -> tuple:
|
|
569
599
|
"""Compose multiple filters."""
|
|
570
600
|
return ("compose", filters)
|
|
@@ -616,14 +646,14 @@ def create_dsl_stubs(
|
|
|
616
646
|
|
|
617
647
|
# Type shorthand helper functions
|
|
618
648
|
# OLD type functions - keeping temporarily until examples are updated
|
|
619
|
-
def _required(type_name: str, description: str = None) -> dict:
|
|
649
|
+
def _required(type_name: str, description: str = None) -> dict: # pragma: no cover
|
|
620
650
|
"""Create a required field of given type."""
|
|
621
651
|
result = {"type": type_name, "required": True}
|
|
622
652
|
if description:
|
|
623
653
|
result["description"] = description
|
|
624
654
|
return result
|
|
625
655
|
|
|
626
|
-
def _string(default: str = None, description: str = None) -> dict:
|
|
656
|
+
def _string(default: str = None, description: str = None) -> dict: # pragma: no cover
|
|
627
657
|
"""Create an optional string field."""
|
|
628
658
|
result = {"type": "string", "required": False}
|
|
629
659
|
if default is not None:
|
|
@@ -632,7 +662,7 @@ def create_dsl_stubs(
|
|
|
632
662
|
result["description"] = description
|
|
633
663
|
return result
|
|
634
664
|
|
|
635
|
-
def _number(default: float = None, description: str = None) -> dict:
|
|
665
|
+
def _number(default: float = None, description: str = None) -> dict: # pragma: no cover
|
|
636
666
|
"""Create an optional number field."""
|
|
637
667
|
result = {"type": "number", "required": False}
|
|
638
668
|
if default is not None:
|
|
@@ -641,7 +671,7 @@ def create_dsl_stubs(
|
|
|
641
671
|
result["description"] = description
|
|
642
672
|
return result
|
|
643
673
|
|
|
644
|
-
def _boolean(default: bool = None, description: str = None) -> dict:
|
|
674
|
+
def _boolean(default: bool = None, description: str = None) -> dict: # pragma: no cover
|
|
645
675
|
"""Create an optional boolean field."""
|
|
646
676
|
result = {"type": "boolean", "required": False}
|
|
647
677
|
if default is not None:
|
|
@@ -650,7 +680,7 @@ def create_dsl_stubs(
|
|
|
650
680
|
result["description"] = description
|
|
651
681
|
return result
|
|
652
682
|
|
|
653
|
-
def _array(default: list = None, description: str = None) -> dict:
|
|
683
|
+
def _array(default: list = None, description: str = None) -> dict: # pragma: no cover
|
|
654
684
|
"""Create an optional array field."""
|
|
655
685
|
result = {"type": "array", "required": False}
|
|
656
686
|
if default is not None:
|
|
@@ -659,7 +689,7 @@ def create_dsl_stubs(
|
|
|
659
689
|
result["description"] = description
|
|
660
690
|
return result
|
|
661
691
|
|
|
662
|
-
def _object(default: dict = None, description: str = None) -> dict:
|
|
692
|
+
def _object(default: dict = None, description: str = None) -> dict: # pragma: no cover
|
|
663
693
|
"""Create an optional object field."""
|
|
664
694
|
result = {"type": "object", "required": False}
|
|
665
695
|
if default is not None:
|
|
@@ -680,6 +710,8 @@ def create_dsl_stubs(
|
|
|
680
710
|
# Convert Lua table to dict if needed
|
|
681
711
|
if hasattr(options, "items"):
|
|
682
712
|
options = lua_table_to_dict(options)
|
|
713
|
+
if isinstance(options, list) and len(options) == 0:
|
|
714
|
+
options = {}
|
|
683
715
|
|
|
684
716
|
# Create a FieldDefinition (subclass of dict) to mark new syntax
|
|
685
717
|
result = FieldDefinition()
|
|
@@ -720,9 +752,9 @@ def create_dsl_stubs(
|
|
|
720
752
|
options = lua_table_to_dict(options)
|
|
721
753
|
if not isinstance(options, dict):
|
|
722
754
|
options = {}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return
|
|
755
|
+
evaluator_config = {"type": evaluator_type}
|
|
756
|
+
evaluator_config.update(options)
|
|
757
|
+
return evaluator_config
|
|
726
758
|
|
|
727
759
|
return build_evaluator
|
|
728
760
|
|
|
@@ -759,12 +791,12 @@ def create_dsl_stubs(
|
|
|
759
791
|
# Assignment syntax: generate temp name and register
|
|
760
792
|
import uuid
|
|
761
793
|
|
|
762
|
-
|
|
794
|
+
temporary_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
|
|
763
795
|
config_dict = lua_table_to_dict(name)
|
|
764
|
-
builder.register_model(
|
|
796
|
+
builder.register_model(temporary_name, config_dict)
|
|
765
797
|
|
|
766
|
-
handle = ModelHandle(
|
|
767
|
-
_model_registry[
|
|
798
|
+
handle = ModelHandle(temporary_name)
|
|
799
|
+
_model_registry[temporary_name] = handle
|
|
768
800
|
return handle
|
|
769
801
|
|
|
770
802
|
# If config is provided, it's old-style definition: Model("name", {config})
|
|
@@ -785,19 +817,19 @@ def create_dsl_stubs(
|
|
|
785
817
|
return self.definer(name)
|
|
786
818
|
|
|
787
819
|
# Otherwise pass through to definer
|
|
788
|
-
return self.definer(name, config)
|
|
789
|
-
except TypeError as
|
|
820
|
+
return self.definer(name, config) # pragma: no cover
|
|
821
|
+
except TypeError as error:
|
|
790
822
|
# Handle unhashable type errors from Lua tables
|
|
791
|
-
if "unhashable type" in str(
|
|
823
|
+
if "unhashable type" in str(error):
|
|
792
824
|
# This is assignment syntax with a Lua table
|
|
793
825
|
import uuid
|
|
794
826
|
|
|
795
|
-
|
|
827
|
+
temporary_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
|
|
796
828
|
config_dict = lua_table_to_dict(name)
|
|
797
|
-
builder.register_model(
|
|
829
|
+
builder.register_model(temporary_name, config_dict)
|
|
798
830
|
|
|
799
|
-
handle = ModelHandle(
|
|
800
|
-
_model_registry[
|
|
831
|
+
handle = ModelHandle(temporary_name)
|
|
832
|
+
_model_registry[temporary_name] = handle
|
|
801
833
|
return handle
|
|
802
834
|
raise
|
|
803
835
|
|
|
@@ -846,9 +878,9 @@ def create_dsl_stubs(
|
|
|
846
878
|
return create_signature(sig_input)
|
|
847
879
|
else:
|
|
848
880
|
# This is a name for curried form: Signature "name" {...}
|
|
849
|
-
def accept_config(
|
|
881
|
+
def accept_config(config):
|
|
850
882
|
"""Accept config and create structured signature."""
|
|
851
|
-
config_dict = lua_table_to_dict(
|
|
883
|
+
config_dict = lua_table_to_dict(config)
|
|
852
884
|
|
|
853
885
|
# Normalize empty config
|
|
854
886
|
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
@@ -898,9 +930,9 @@ def create_dsl_stubs(
|
|
|
898
930
|
# Check if this is curried syntax (config is None, return acceptor)
|
|
899
931
|
if config is None:
|
|
900
932
|
# Return a function that accepts config
|
|
901
|
-
def accept_config(
|
|
902
|
-
|
|
903
|
-
return configure_lm(model, **
|
|
933
|
+
def accept_config(config_override=None):
|
|
934
|
+
config_dict = lua_table_to_dict(config_override) if config_override else {}
|
|
935
|
+
return configure_lm(model, **config_dict)
|
|
904
936
|
|
|
905
937
|
# Also allow immediate call without config
|
|
906
938
|
# This handles: LM("openai/gpt-4o") with no second arg
|
|
@@ -971,7 +1003,7 @@ def create_dsl_stubs(
|
|
|
971
1003
|
|
|
972
1004
|
# Tool mocks use explicit keys.
|
|
973
1005
|
tool_mock_keys = {"returns", "temporal", "conditional", "error"}
|
|
974
|
-
if any(
|
|
1006
|
+
if any(key in mock_config for key in tool_mock_keys):
|
|
975
1007
|
# Convert DSL syntax to MockConfig format
|
|
976
1008
|
processed_config = {}
|
|
977
1009
|
|
|
@@ -987,13 +1019,22 @@ def create_dsl_stubs(
|
|
|
987
1019
|
elif "conditional" in mock_config:
|
|
988
1020
|
# Convert DSL conditional format to MockManager format
|
|
989
1021
|
conditionals = []
|
|
990
|
-
for
|
|
991
|
-
if
|
|
992
|
-
|
|
1022
|
+
for conditional in mock_config["conditional"]:
|
|
1023
|
+
if (
|
|
1024
|
+
isinstance(conditional, dict)
|
|
1025
|
+
and "when" in conditional
|
|
1026
|
+
and "returns" in conditional
|
|
1027
|
+
):
|
|
1028
|
+
conditionals.append(
|
|
1029
|
+
{
|
|
1030
|
+
"when": conditional["when"],
|
|
1031
|
+
"return": conditional["returns"],
|
|
1032
|
+
}
|
|
1033
|
+
)
|
|
993
1034
|
processed_config["conditional_mocks"] = conditionals
|
|
994
1035
|
|
|
995
|
-
# Error simulation
|
|
996
|
-
|
|
1036
|
+
# Error simulation (fallback when no other tool mock key matched)
|
|
1037
|
+
else:
|
|
997
1038
|
processed_config["error"] = mock_config["error"]
|
|
998
1039
|
|
|
999
1040
|
# Register the tool mock configuration
|
|
@@ -1031,8 +1072,8 @@ def create_dsl_stubs(
|
|
|
1031
1072
|
from tactus.dspy import create_history
|
|
1032
1073
|
|
|
1033
1074
|
if messages is not None:
|
|
1034
|
-
|
|
1035
|
-
return create_history(
|
|
1075
|
+
message_entries = lua_table_to_dict(messages)
|
|
1076
|
+
return create_history(message_entries)
|
|
1036
1077
|
return create_history()
|
|
1037
1078
|
|
|
1038
1079
|
class TactusMessage:
|
|
@@ -1100,7 +1141,9 @@ def create_dsl_stubs(
|
|
|
1100
1141
|
raise ValueError("Message requires 'content' field")
|
|
1101
1142
|
|
|
1102
1143
|
# Extract any additional metadata
|
|
1103
|
-
metadata = {
|
|
1144
|
+
metadata = {
|
|
1145
|
+
key: value for key, value in config_dict.items() if key not in ("role", "content")
|
|
1146
|
+
}
|
|
1104
1147
|
|
|
1105
1148
|
return TactusMessage(role, content, **metadata)
|
|
1106
1149
|
|
|
@@ -1142,9 +1185,9 @@ def create_dsl_stubs(
|
|
|
1142
1185
|
)
|
|
1143
1186
|
|
|
1144
1187
|
# New curried syntax - return a function that accepts config
|
|
1145
|
-
def accept_config(
|
|
1188
|
+
def accept_config(config):
|
|
1146
1189
|
"""Accept config and create module."""
|
|
1147
|
-
config_dict = lua_table_to_dict(
|
|
1190
|
+
config_dict = lua_table_to_dict(config)
|
|
1148
1191
|
return create_module(
|
|
1149
1192
|
module_name, config_dict, registry=builder.registry, mock_manager=mock_manager
|
|
1150
1193
|
)
|
|
@@ -1194,9 +1237,9 @@ def create_dsl_stubs(
|
|
|
1194
1237
|
)
|
|
1195
1238
|
|
|
1196
1239
|
# Curried form - return function that accepts config
|
|
1197
|
-
def accept_config(
|
|
1240
|
+
def accept_config(config):
|
|
1198
1241
|
"""Accept config and create DSPy agent."""
|
|
1199
|
-
config_dict = lua_table_to_dict(
|
|
1242
|
+
config_dict = lua_table_to_dict(config)
|
|
1200
1243
|
agent_name = config_dict.pop("name", "dspy_agent")
|
|
1201
1244
|
return create_dspy_agent(
|
|
1202
1245
|
agent_name, config_dict, registry=builder.registry, mock_manager=mock_manager
|
|
@@ -1264,7 +1307,7 @@ def create_dsl_stubs(
|
|
|
1264
1307
|
|
|
1265
1308
|
_mcp_namespace = McpNamespace()
|
|
1266
1309
|
|
|
1267
|
-
def _process_tool_config(tool_name, config):
|
|
1310
|
+
def _process_tool_config(tool_name, config): # pragma: no cover
|
|
1268
1311
|
"""
|
|
1269
1312
|
Process tool configuration for both curried and direct syntax.
|
|
1270
1313
|
|
|
@@ -1278,14 +1321,14 @@ def create_dsl_stubs(
|
|
|
1278
1321
|
from tactus.primitives.tool_handle import ToolHandle
|
|
1279
1322
|
|
|
1280
1323
|
# Extract function from config
|
|
1281
|
-
|
|
1324
|
+
handler_function = None
|
|
1282
1325
|
if hasattr(config, "__getitem__"):
|
|
1283
|
-
for
|
|
1326
|
+
for index in range(1, 10):
|
|
1284
1327
|
try:
|
|
1285
|
-
|
|
1286
|
-
if callable(
|
|
1287
|
-
|
|
1288
|
-
config[
|
|
1328
|
+
candidate_item = config[index]
|
|
1329
|
+
if callable(candidate_item):
|
|
1330
|
+
handler_function = candidate_item
|
|
1331
|
+
config[index] = None
|
|
1289
1332
|
break
|
|
1290
1333
|
except (KeyError, TypeError, IndexError):
|
|
1291
1334
|
break
|
|
@@ -1296,8 +1339,8 @@ def create_dsl_stubs(
|
|
|
1296
1339
|
# Clean up None values from function extraction
|
|
1297
1340
|
if isinstance(config_dict, list):
|
|
1298
1341
|
config_dict = [x for x in config_dict if x is not None]
|
|
1299
|
-
|
|
1300
|
-
|
|
1342
|
+
# Ignore extra positional entries that can't be mapped to config fields.
|
|
1343
|
+
config_dict = {}
|
|
1301
1344
|
|
|
1302
1345
|
# Normalize empty schemas (lua {} -> python []) so tools treat empty schemas
|
|
1303
1346
|
# as empty objects, not arrays.
|
|
@@ -1306,8 +1349,8 @@ def create_dsl_stubs(
|
|
|
1306
1349
|
config_dict["output"] = _normalize_schema(config_dict.get("output", {}))
|
|
1307
1350
|
|
|
1308
1351
|
# Check for legacy handler field
|
|
1309
|
-
if
|
|
1310
|
-
|
|
1352
|
+
if handler_function is None and isinstance(config_dict, dict):
|
|
1353
|
+
handler_function = config_dict.pop("handler", None)
|
|
1311
1354
|
|
|
1312
1355
|
# Tool sources: allow `use = "..."` (or legacy/internal `source = "..."`) in lieu of a handler.
|
|
1313
1356
|
source = None
|
|
@@ -1320,14 +1363,16 @@ def create_dsl_stubs(
|
|
|
1320
1363
|
else:
|
|
1321
1364
|
source = config_dict.get("source")
|
|
1322
1365
|
|
|
1323
|
-
if
|
|
1366
|
+
if handler_function is not None and isinstance(source, str) and source.strip():
|
|
1324
1367
|
raise TypeError(
|
|
1325
1368
|
f"Tool '{tool_name}' cannot specify both a function and 'use = \"...\"'"
|
|
1326
1369
|
)
|
|
1327
1370
|
|
|
1328
|
-
is_source_tool =
|
|
1371
|
+
is_source_tool = (
|
|
1372
|
+
handler_function is None and isinstance(source, str) and bool(source.strip())
|
|
1373
|
+
)
|
|
1329
1374
|
|
|
1330
|
-
if
|
|
1375
|
+
if handler_function is None and not is_source_tool:
|
|
1331
1376
|
raise TypeError(
|
|
1332
1377
|
f"Tool '{tool_name}' requires either a function or 'use = \"...\"'. "
|
|
1333
1378
|
'Example: my_tool = Tool { use = "broker.host.ping" }'
|
|
@@ -1358,7 +1403,7 @@ def create_dsl_stubs(
|
|
|
1358
1403
|
f"Tool '{tool_name}' not resolved from source '{source_str}'"
|
|
1359
1404
|
)
|
|
1360
1405
|
|
|
1361
|
-
|
|
1406
|
+
tool_function = tool_primitive._extract_tool_function(toolset, tool_name)
|
|
1362
1407
|
|
|
1363
1408
|
# Support both tool_fn(**kwargs) and tool_fn(args_dict) styles.
|
|
1364
1409
|
# Prefer kwargs (pydantic-ai Tool functions) then fall back to dict.
|
|
@@ -1369,9 +1414,9 @@ def create_dsl_stubs(
|
|
|
1369
1414
|
if not isinstance(args_dict, dict):
|
|
1370
1415
|
raise TypeError(f"Tool '{tool_name}' args must be an object/table")
|
|
1371
1416
|
|
|
1372
|
-
if asyncio.iscoroutinefunction(
|
|
1417
|
+
if asyncio.iscoroutinefunction(tool_function):
|
|
1373
1418
|
|
|
1374
|
-
def
|
|
1419
|
+
def run_coroutine_in_thread(coro):
|
|
1375
1420
|
try:
|
|
1376
1421
|
asyncio.get_running_loop()
|
|
1377
1422
|
except RuntimeError:
|
|
@@ -1382,8 +1427,8 @@ def create_dsl_stubs(
|
|
|
1382
1427
|
def run_in_thread():
|
|
1383
1428
|
try:
|
|
1384
1429
|
result_container["value"] = asyncio.run(coro)
|
|
1385
|
-
except Exception as
|
|
1386
|
-
result_container["exception"] =
|
|
1430
|
+
except Exception as error:
|
|
1431
|
+
result_container["exception"] = error
|
|
1387
1432
|
|
|
1388
1433
|
thread = threading.Thread(target=run_in_thread)
|
|
1389
1434
|
thread.start()
|
|
@@ -1394,22 +1439,22 @@ def create_dsl_stubs(
|
|
|
1394
1439
|
return result_container["value"]
|
|
1395
1440
|
|
|
1396
1441
|
try:
|
|
1397
|
-
return
|
|
1442
|
+
return run_coroutine_in_thread(tool_function(**args_dict))
|
|
1398
1443
|
except TypeError:
|
|
1399
|
-
return
|
|
1444
|
+
return run_coroutine_in_thread(tool_function(args_dict))
|
|
1400
1445
|
|
|
1401
1446
|
try:
|
|
1402
|
-
return
|
|
1447
|
+
return tool_function(**args_dict)
|
|
1403
1448
|
except TypeError:
|
|
1404
|
-
return
|
|
1449
|
+
return tool_function(args_dict)
|
|
1405
1450
|
|
|
1406
|
-
|
|
1451
|
+
handler_function = source_tool_handler
|
|
1407
1452
|
|
|
1408
1453
|
# Register tool with provided name
|
|
1409
|
-
builder.register_tool(tool_name, config_dict,
|
|
1454
|
+
builder.register_tool(tool_name, config_dict, handler_function)
|
|
1410
1455
|
handle = ToolHandle(
|
|
1411
1456
|
tool_name,
|
|
1412
|
-
|
|
1457
|
+
handler_function,
|
|
1413
1458
|
tool_primitive,
|
|
1414
1459
|
record_calls=not is_source_tool,
|
|
1415
1460
|
)
|
|
@@ -1457,14 +1502,14 @@ def create_dsl_stubs(
|
|
|
1457
1502
|
raise TypeError("Tool requires a configuration table")
|
|
1458
1503
|
|
|
1459
1504
|
# Extract function from config
|
|
1460
|
-
|
|
1505
|
+
handler_function = None
|
|
1461
1506
|
if hasattr(config, "__getitem__"):
|
|
1462
|
-
for
|
|
1507
|
+
for index in range(1, 10):
|
|
1463
1508
|
try:
|
|
1464
|
-
|
|
1465
|
-
if callable(
|
|
1466
|
-
|
|
1467
|
-
config[
|
|
1509
|
+
candidate_item = config[index]
|
|
1510
|
+
if callable(candidate_item):
|
|
1511
|
+
handler_function = candidate_item
|
|
1512
|
+
config[index] = None
|
|
1468
1513
|
break
|
|
1469
1514
|
except (KeyError, TypeError, IndexError):
|
|
1470
1515
|
break
|
|
@@ -1475,8 +1520,8 @@ def create_dsl_stubs(
|
|
|
1475
1520
|
# Clean up None values from function extraction
|
|
1476
1521
|
if isinstance(config_dict, list):
|
|
1477
1522
|
config_dict = [x for x in config_dict if x is not None]
|
|
1478
|
-
|
|
1479
|
-
|
|
1523
|
+
# Ignore extra positional entries that can't be mapped to config fields.
|
|
1524
|
+
config_dict = {}
|
|
1480
1525
|
|
|
1481
1526
|
# Normalize empty schemas (lua {} -> python []) so tools treat empty schemas
|
|
1482
1527
|
# as empty objects, not arrays.
|
|
@@ -1485,8 +1530,8 @@ def create_dsl_stubs(
|
|
|
1485
1530
|
config_dict["output"] = _normalize_schema(config_dict.get("output", {}))
|
|
1486
1531
|
|
|
1487
1532
|
# Check for legacy handler field
|
|
1488
|
-
if
|
|
1489
|
-
|
|
1533
|
+
if handler_function is None and isinstance(config_dict, dict):
|
|
1534
|
+
handler_function = config_dict.pop("handler", None)
|
|
1490
1535
|
|
|
1491
1536
|
# Tool sources: allow `use = "..."` (or legacy/internal `source = "..."`) in lieu of a handler.
|
|
1492
1537
|
source = None
|
|
@@ -1499,12 +1544,14 @@ def create_dsl_stubs(
|
|
|
1499
1544
|
else:
|
|
1500
1545
|
source = config_dict.get("source")
|
|
1501
1546
|
|
|
1502
|
-
if
|
|
1547
|
+
if handler_function is not None and isinstance(source, str) and source.strip():
|
|
1503
1548
|
raise TypeError("Tool cannot specify both a function and 'use = \"...\"'")
|
|
1504
1549
|
|
|
1505
|
-
is_source_tool =
|
|
1550
|
+
is_source_tool = (
|
|
1551
|
+
handler_function is None and isinstance(source, str) and bool(source.strip())
|
|
1552
|
+
)
|
|
1506
1553
|
|
|
1507
|
-
if
|
|
1554
|
+
if handler_function is None and not is_source_tool:
|
|
1508
1555
|
raise TypeError(
|
|
1509
1556
|
"Tool requires either a function or 'use = \"...\"'. "
|
|
1510
1557
|
'Example: my_tool = Tool { use = "broker.host.ping" }'
|
|
@@ -1522,7 +1569,7 @@ def create_dsl_stubs(
|
|
|
1522
1569
|
# Generate a temporary name - will be replaced when assigned
|
|
1523
1570
|
import uuid
|
|
1524
1571
|
|
|
1525
|
-
|
|
1572
|
+
temporary_name = (
|
|
1526
1573
|
explicit_name.strip()
|
|
1527
1574
|
if isinstance(explicit_name, str)
|
|
1528
1575
|
else f"_temp_tool_{uuid.uuid4().hex[:8]}"
|
|
@@ -1533,7 +1580,7 @@ def create_dsl_stubs(
|
|
|
1533
1580
|
import threading
|
|
1534
1581
|
|
|
1535
1582
|
source_str = source.strip()
|
|
1536
|
-
|
|
1583
|
+
handle_reference = {"handle": None}
|
|
1537
1584
|
|
|
1538
1585
|
def source_tool_handler(args):
|
|
1539
1586
|
# Resolve at call time so runtime toolsets are available.
|
|
@@ -1544,14 +1591,18 @@ def create_dsl_stubs(
|
|
|
1544
1591
|
if runtime is None:
|
|
1545
1592
|
raise RuntimeError("Tool not available (runtime not connected)")
|
|
1546
1593
|
|
|
1547
|
-
resolved_name =
|
|
1594
|
+
resolved_name = (
|
|
1595
|
+
handle_reference["handle"].name
|
|
1596
|
+
if handle_reference["handle"]
|
|
1597
|
+
else temporary_name
|
|
1598
|
+
)
|
|
1548
1599
|
toolset = runtime.toolset_registry.get(resolved_name)
|
|
1549
1600
|
if toolset is None:
|
|
1550
1601
|
raise RuntimeError(
|
|
1551
1602
|
f"Tool '{resolved_name}' not resolved from source '{source_str}'"
|
|
1552
1603
|
)
|
|
1553
1604
|
|
|
1554
|
-
|
|
1605
|
+
tool_function = tool_primitive._extract_tool_function(toolset, resolved_name)
|
|
1555
1606
|
|
|
1556
1607
|
# Support both tool_fn(**kwargs) and tool_fn(args_dict) styles.
|
|
1557
1608
|
# Prefer kwargs (pydantic-ai Tool functions) then fall back to dict.
|
|
@@ -1562,9 +1613,9 @@ def create_dsl_stubs(
|
|
|
1562
1613
|
if not isinstance(args_dict, dict):
|
|
1563
1614
|
raise TypeError("Tool args must be an object/table")
|
|
1564
1615
|
|
|
1565
|
-
if asyncio.iscoroutinefunction(
|
|
1616
|
+
if asyncio.iscoroutinefunction(tool_function):
|
|
1566
1617
|
|
|
1567
|
-
def
|
|
1618
|
+
def run_coroutine_in_thread(coro):
|
|
1568
1619
|
try:
|
|
1569
1620
|
asyncio.get_running_loop()
|
|
1570
1621
|
except RuntimeError:
|
|
@@ -1575,8 +1626,8 @@ def create_dsl_stubs(
|
|
|
1575
1626
|
def run_in_thread():
|
|
1576
1627
|
try:
|
|
1577
1628
|
result_container["value"] = asyncio.run(coro)
|
|
1578
|
-
except Exception as
|
|
1579
|
-
result_container["exception"] =
|
|
1629
|
+
except Exception as error:
|
|
1630
|
+
result_container["exception"] = error
|
|
1580
1631
|
|
|
1581
1632
|
thread = threading.Thread(target=run_in_thread)
|
|
1582
1633
|
thread.start()
|
|
@@ -1587,31 +1638,31 @@ def create_dsl_stubs(
|
|
|
1587
1638
|
return result_container["value"]
|
|
1588
1639
|
|
|
1589
1640
|
try:
|
|
1590
|
-
return
|
|
1641
|
+
return run_coroutine_in_thread(tool_function(**args_dict))
|
|
1591
1642
|
except TypeError:
|
|
1592
|
-
return
|
|
1643
|
+
return run_coroutine_in_thread(tool_function(args_dict))
|
|
1593
1644
|
|
|
1594
1645
|
try:
|
|
1595
|
-
return
|
|
1646
|
+
return tool_function(**args_dict)
|
|
1596
1647
|
except TypeError:
|
|
1597
|
-
return
|
|
1648
|
+
return tool_function(args_dict)
|
|
1598
1649
|
|
|
1599
|
-
|
|
1650
|
+
handler_function = source_tool_handler
|
|
1600
1651
|
|
|
1601
1652
|
# Register tool
|
|
1602
|
-
builder.register_tool(
|
|
1653
|
+
builder.register_tool(temporary_name, config_dict, handler_function)
|
|
1603
1654
|
handle = ToolHandle(
|
|
1604
|
-
|
|
1605
|
-
|
|
1655
|
+
temporary_name,
|
|
1656
|
+
handler_function,
|
|
1606
1657
|
tool_primitive,
|
|
1607
1658
|
record_calls=not is_source_tool,
|
|
1608
1659
|
)
|
|
1609
1660
|
|
|
1610
1661
|
# Store in registry with temp name
|
|
1611
|
-
_tool_registry[
|
|
1662
|
+
_tool_registry[temporary_name] = handle
|
|
1612
1663
|
|
|
1613
1664
|
if is_source_tool:
|
|
1614
|
-
|
|
1665
|
+
handle_reference["handle"] = handle
|
|
1615
1666
|
|
|
1616
1667
|
return handle
|
|
1617
1668
|
|
|
@@ -1650,21 +1701,21 @@ def create_dsl_stubs(
|
|
|
1650
1701
|
|
|
1651
1702
|
# tools: tool/toolset references and toolset expressions (filter dicts)
|
|
1652
1703
|
if "tools" in config_dict:
|
|
1653
|
-
|
|
1654
|
-
if isinstance(
|
|
1704
|
+
tool_references = config_dict["tools"]
|
|
1705
|
+
if isinstance(tool_references, (list, tuple)):
|
|
1655
1706
|
normalized = []
|
|
1656
|
-
for
|
|
1657
|
-
if isinstance(
|
|
1658
|
-
if "handler" in
|
|
1707
|
+
for tool_entry in tool_references:
|
|
1708
|
+
if isinstance(tool_entry, dict):
|
|
1709
|
+
if "handler" in tool_entry:
|
|
1659
1710
|
raise ValueError(
|
|
1660
1711
|
f"Agent '{agent_name}': inline tool definitions must be in 'inline_tools', not 'tools'."
|
|
1661
1712
|
)
|
|
1662
|
-
normalized.append(
|
|
1713
|
+
normalized.append(tool_entry)
|
|
1663
1714
|
continue
|
|
1664
|
-
if hasattr(
|
|
1665
|
-
normalized.append(
|
|
1715
|
+
if hasattr(tool_entry, "name"): # ToolHandle or ToolsetHandle
|
|
1716
|
+
normalized.append(tool_entry.name)
|
|
1666
1717
|
else:
|
|
1667
|
-
normalized.append(
|
|
1718
|
+
normalized.append(tool_entry)
|
|
1668
1719
|
config_dict["tools"] = normalized
|
|
1669
1720
|
|
|
1670
1721
|
# Extract input schema if present
|
|
@@ -1762,9 +1813,9 @@ def create_dsl_stubs(
|
|
|
1762
1813
|
f"[AGENT_CREATION] Stored agent '{agent_name}' in _created_agents dict"
|
|
1763
1814
|
)
|
|
1764
1815
|
|
|
1765
|
-
except Exception as
|
|
1816
|
+
except Exception as error:
|
|
1766
1817
|
logger.error(
|
|
1767
|
-
f"[AGENT_CREATION] Failed to create agent '{agent_name}' immediately: {
|
|
1818
|
+
f"[AGENT_CREATION] Failed to create agent '{agent_name}' immediately: {error}",
|
|
1768
1819
|
exc_info=True,
|
|
1769
1820
|
)
|
|
1770
1821
|
# Fall back to two-phase initialization if immediate creation fails
|
|
@@ -1835,21 +1886,21 @@ def create_dsl_stubs(
|
|
|
1835
1886
|
|
|
1836
1887
|
# tools: tool/toolset references and toolset expressions (filter dicts)
|
|
1837
1888
|
if "tools" in config_dict:
|
|
1838
|
-
|
|
1839
|
-
if isinstance(
|
|
1889
|
+
tool_references = config_dict["tools"]
|
|
1890
|
+
if isinstance(tool_references, (list, tuple)):
|
|
1840
1891
|
normalized = []
|
|
1841
|
-
for
|
|
1842
|
-
if isinstance(
|
|
1843
|
-
if "handler" in
|
|
1892
|
+
for tool_entry in tool_references:
|
|
1893
|
+
if isinstance(tool_entry, dict):
|
|
1894
|
+
if "handler" in tool_entry:
|
|
1844
1895
|
raise ValueError(
|
|
1845
1896
|
"Agent: inline tool definitions must be in 'inline_tools', not 'tools'."
|
|
1846
1897
|
)
|
|
1847
|
-
normalized.append(
|
|
1898
|
+
normalized.append(tool_entry)
|
|
1848
1899
|
continue
|
|
1849
|
-
if hasattr(
|
|
1850
|
-
normalized.append(
|
|
1900
|
+
if hasattr(tool_entry, "name"): # ToolHandle or ToolsetHandle
|
|
1901
|
+
normalized.append(tool_entry.name)
|
|
1851
1902
|
else:
|
|
1852
|
-
normalized.append(
|
|
1903
|
+
normalized.append(tool_entry)
|
|
1853
1904
|
config_dict["tools"] = normalized
|
|
1854
1905
|
|
|
1855
1906
|
# Extract input schema if present
|
|
@@ -1877,13 +1928,13 @@ def create_dsl_stubs(
|
|
|
1877
1928
|
# Generate a temporary name - will be replaced when assigned
|
|
1878
1929
|
import uuid
|
|
1879
1930
|
|
|
1880
|
-
|
|
1931
|
+
temporary_agent_name = f"_temp_agent_{uuid.uuid4().hex[:8]}"
|
|
1881
1932
|
|
|
1882
1933
|
# Register agent
|
|
1883
|
-
builder.register_agent(
|
|
1934
|
+
builder.register_agent(temporary_agent_name, config_dict, output_schema)
|
|
1884
1935
|
|
|
1885
1936
|
# Create handle
|
|
1886
|
-
handle = AgentHandle(
|
|
1937
|
+
handle = AgentHandle(temporary_agent_name)
|
|
1887
1938
|
|
|
1888
1939
|
# If we have runtime context, create the agent primitive immediately
|
|
1889
1940
|
import logging
|
|
@@ -1891,13 +1942,15 @@ def create_dsl_stubs(
|
|
|
1891
1942
|
logger = logging.getLogger(__name__)
|
|
1892
1943
|
|
|
1893
1944
|
logger.debug(
|
|
1894
|
-
f"[AGENT_CREATION] Agent '{
|
|
1945
|
+
f"[AGENT_CREATION] Agent '{temporary_agent_name}': runtime_context={bool(_runtime_context)}, has_log_handler={('log_handler' in _runtime_context) if _runtime_context else False}"
|
|
1895
1946
|
)
|
|
1896
1947
|
|
|
1897
1948
|
if _runtime_context:
|
|
1898
1949
|
from tactus.dspy.agent import create_dspy_agent
|
|
1899
1950
|
|
|
1900
|
-
logger.debug(
|
|
1951
|
+
logger.debug(
|
|
1952
|
+
f"[AGENT_CREATION] Attempting immediate creation for agent '{temporary_agent_name}'"
|
|
1953
|
+
)
|
|
1901
1954
|
|
|
1902
1955
|
try:
|
|
1903
1956
|
# Create the actual agent primitive NOW
|
|
@@ -1917,10 +1970,10 @@ def create_dsl_stubs(
|
|
|
1917
1970
|
agent_config["log_handler"] = _runtime_context["log_handler"]
|
|
1918
1971
|
|
|
1919
1972
|
logger.debug(
|
|
1920
|
-
f"[AGENT_CREATION] Creating agent immediately: name={
|
|
1973
|
+
f"[AGENT_CREATION] Creating agent immediately: name={temporary_agent_name}, has_log_handler={'log_handler' in agent_config}"
|
|
1921
1974
|
)
|
|
1922
1975
|
agent_primitive = create_dspy_agent(
|
|
1923
|
-
|
|
1976
|
+
temporary_agent_name,
|
|
1924
1977
|
agent_config,
|
|
1925
1978
|
registry=builder.registry,
|
|
1926
1979
|
mock_manager=_runtime_context.get("mock_manager"),
|
|
@@ -1936,27 +1989,29 @@ def create_dsl_stubs(
|
|
|
1936
1989
|
agent_primitive, execution_context=_runtime_context.get("execution_context")
|
|
1937
1990
|
)
|
|
1938
1991
|
logger.debug(
|
|
1939
|
-
f"[AGENT_CREATION] Agent '{
|
|
1992
|
+
f"[AGENT_CREATION] Agent '{temporary_agent_name}' created immediately during declaration, has_log_handler={hasattr(agent_primitive, 'log_handler') and agent_primitive.log_handler is not None}"
|
|
1940
1993
|
)
|
|
1941
1994
|
|
|
1942
1995
|
# Store primitive in a dict so runtime can access it later
|
|
1943
1996
|
if "_created_agents" not in _runtime_context:
|
|
1944
1997
|
_runtime_context["_created_agents"] = {}
|
|
1945
|
-
_runtime_context["_created_agents"][
|
|
1946
|
-
logger.debug(
|
|
1998
|
+
_runtime_context["_created_agents"][temporary_agent_name] = agent_primitive
|
|
1999
|
+
logger.debug(
|
|
2000
|
+
f"[AGENT_CREATION] Stored agent '{temporary_agent_name}' in _created_agents dict"
|
|
2001
|
+
)
|
|
1947
2002
|
|
|
1948
|
-
except Exception as
|
|
2003
|
+
except Exception as error:
|
|
1949
2004
|
import traceback
|
|
1950
2005
|
|
|
1951
2006
|
logger.error(
|
|
1952
|
-
f"[AGENT_CREATION] Failed to create agent '{
|
|
2007
|
+
f"[AGENT_CREATION] Failed to create agent '{temporary_agent_name}' immediately: {error}",
|
|
1953
2008
|
exc_info=True,
|
|
1954
2009
|
)
|
|
1955
2010
|
logger.debug(f"Full traceback: {traceback.format_exc()}")
|
|
1956
2011
|
# Fall back to two-phase initialization if immediate creation fails
|
|
1957
2012
|
|
|
1958
2013
|
# Register handle for lookup
|
|
1959
|
-
_agent_registry[
|
|
2014
|
+
_agent_registry[temporary_agent_name] = handle
|
|
1960
2015
|
|
|
1961
2016
|
return handle
|
|
1962
2017
|
|
|
@@ -2079,8 +2134,12 @@ def create_dsl_stubs(
|
|
|
2079
2134
|
# Built-in filters (exposed as a table)
|
|
2080
2135
|
"filters": {
|
|
2081
2136
|
"last_n": _last_n,
|
|
2137
|
+
"first_n": _first_n,
|
|
2082
2138
|
"token_budget": _token_budget,
|
|
2139
|
+
"head_tokens": _head_tokens,
|
|
2140
|
+
"tail_tokens": _tail_tokens,
|
|
2083
2141
|
"by_role": _by_role,
|
|
2142
|
+
"system_prefix": _system_prefix,
|
|
2084
2143
|
"compose": _compose,
|
|
2085
2144
|
},
|
|
2086
2145
|
# Built-in matchers
|