tactus 0.34.1__py3-none-any.whl → 0.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/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, Dict
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
- keys = list(lua_table.keys())
69
+ table_keys = list(lua_table.keys())
70
70
 
71
71
  # Empty table - return empty list (common for tools = {})
72
- if not keys:
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(k, int) for k in keys):
77
- sorted_keys = sorted(keys)
78
- if sorted_keys == list(range(1, len(keys) + 1)):
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[k])
83
- if hasattr(lua_table[k], "items")
84
- else lua_table[k]
82
+ lua_table_to_dict(lua_table[key])
83
+ if hasattr(lua_table[key], "items")
84
+ else lua_table[key]
85
85
  )
86
- for k in sorted_keys
86
+ for key in sorted_keys
87
87
  ]
88
88
 
89
89
  # It's a dictionary
90
- result = {}
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
- result[key] = lua_table_to_dict(value)
94
+ converted_mapping[key] = lua_table_to_dict(value)
95
95
  else:
96
- result[key] = value
97
- return result
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: Dict[str, Any] | None = None,
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: Dict[str, AgentHandle] = {}
137
- _tool_registry: Dict[str, Any] = {} # ToolHandle instances
138
- _model_registry: Dict[str, ModelHandle] = {}
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(name: str | None, config, procedure_registry: dict):
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
- run_fn = None
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 i in range(1, 10): # Check first few positions
173
+ for index in range(1, 10): # Check first few positions
172
174
  try:
173
- item = config[i]
174
- if callable(item):
175
- run_fn = item
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[i] = None
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 run_fn is None:
196
- run_fn = config_dict.pop("run", None)
200
+ if run_function is None:
201
+ run_function = config_dict.pop("run", None)
197
202
 
198
- if run_fn is None:
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(name, run_fn, input_schema, output_schema, state_schema)
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
- arg = args[0]
489
+ single_argument = args[0]
483
490
  # Check if it's a table with 'from' parameter
484
- if isinstance(arg, dict) or (hasattr(arg, "keys") and callable(arg.keys)):
485
- config = lua_table_to_dict(arg) if not isinstance(arg, dict) else arg
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(arg)
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
- if any(k in config_dict for k in ("dataset", "dataset_file", "evaluators", "thresholds")):
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
- cfg = {"type": evaluator_type}
724
- cfg.update(options)
725
- return cfg
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
- temp_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
794
+ temporary_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
763
795
  config_dict = lua_table_to_dict(name)
764
- builder.register_model(temp_name, config_dict)
796
+ builder.register_model(temporary_name, config_dict)
765
797
 
766
- handle = ModelHandle(temp_name)
767
- _model_registry[temp_name] = handle
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 e:
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(e):
823
+ if "unhashable type" in str(error):
792
824
  # This is assignment syntax with a Lua table
793
825
  import uuid
794
826
 
795
- temp_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
827
+ temporary_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
796
828
  config_dict = lua_table_to_dict(name)
797
- builder.register_model(temp_name, config_dict)
829
+ builder.register_model(temporary_name, config_dict)
798
830
 
799
- handle = ModelHandle(temp_name)
800
- _model_registry[temp_name] = handle
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(cfg):
881
+ def accept_config(config):
850
882
  """Accept config and create structured signature."""
851
- config_dict = lua_table_to_dict(cfg)
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(cfg=None):
902
- cfg_dict = lua_table_to_dict(cfg) if cfg else {}
903
- return configure_lm(model, **cfg_dict)
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(k in mock_config for k in tool_mock_keys):
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 cond in mock_config["conditional"]:
991
- if isinstance(cond, dict) and "when" in cond and "returns" in cond:
992
- conditionals.append({"when": cond["when"], "return": cond["returns"]})
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
- elif "error" in mock_config:
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
- messages_list = lua_table_to_dict(messages)
1035
- return create_history(messages_list)
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 = {k: v for k, v in config_dict.items() if k not in ("role", "content")}
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(cfg):
1188
+ def accept_config(config):
1146
1189
  """Accept config and create module."""
1147
- config_dict = lua_table_to_dict(cfg)
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(cfg):
1240
+ def accept_config(config):
1198
1241
  """Accept config and create DSPy agent."""
1199
- config_dict = lua_table_to_dict(cfg)
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
- handler_fn = None
1324
+ handler_function = None
1282
1325
  if hasattr(config, "__getitem__"):
1283
- for i in range(1, 10):
1326
+ for index in range(1, 10):
1284
1327
  try:
1285
- item = config[i]
1286
- if callable(item):
1287
- handler_fn = item
1288
- config[i] = None
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
- if len(config_dict) == 0:
1300
- config_dict = {}
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 handler_fn is None and isinstance(config_dict, dict):
1310
- handler_fn = config_dict.pop("handler", None)
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 handler_fn is not None and isinstance(source, str) and source.strip():
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 = handler_fn is None and isinstance(source, str) and bool(source.strip())
1371
+ is_source_tool = (
1372
+ handler_function is None and isinstance(source, str) and bool(source.strip())
1373
+ )
1329
1374
 
1330
- if handler_fn is None and not is_source_tool:
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
- tool_fn = tool_primitive._extract_tool_function(toolset, tool_name)
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(tool_fn):
1417
+ if asyncio.iscoroutinefunction(tool_function):
1373
1418
 
1374
- def _run_coro(coro):
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 e:
1386
- result_container["exception"] = e
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 _run_coro(tool_fn(**args_dict))
1442
+ return run_coroutine_in_thread(tool_function(**args_dict))
1398
1443
  except TypeError:
1399
- return _run_coro(tool_fn(args_dict))
1444
+ return run_coroutine_in_thread(tool_function(args_dict))
1400
1445
 
1401
1446
  try:
1402
- return tool_fn(**args_dict)
1447
+ return tool_function(**args_dict)
1403
1448
  except TypeError:
1404
- return tool_fn(args_dict)
1449
+ return tool_function(args_dict)
1405
1450
 
1406
- handler_fn = source_tool_handler
1451
+ handler_function = source_tool_handler
1407
1452
 
1408
1453
  # Register tool with provided name
1409
- builder.register_tool(tool_name, config_dict, handler_fn)
1454
+ builder.register_tool(tool_name, config_dict, handler_function)
1410
1455
  handle = ToolHandle(
1411
1456
  tool_name,
1412
- handler_fn,
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
- handler_fn = None
1505
+ handler_function = None
1461
1506
  if hasattr(config, "__getitem__"):
1462
- for i in range(1, 10):
1507
+ for index in range(1, 10):
1463
1508
  try:
1464
- item = config[i]
1465
- if callable(item):
1466
- handler_fn = item
1467
- config[i] = None
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
- if len(config_dict) == 0:
1479
- config_dict = {}
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 handler_fn is None and isinstance(config_dict, dict):
1489
- handler_fn = config_dict.pop("handler", None)
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 handler_fn is not None and isinstance(source, str) and source.strip():
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 = handler_fn is None and isinstance(source, str) and bool(source.strip())
1550
+ is_source_tool = (
1551
+ handler_function is None and isinstance(source, str) and bool(source.strip())
1552
+ )
1506
1553
 
1507
- if handler_fn is None and not is_source_tool:
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
- temp_name = (
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
- handle_ref = {"handle": None}
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 = handle_ref["handle"].name if handle_ref["handle"] else temp_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
- tool_fn = tool_primitive._extract_tool_function(toolset, resolved_name)
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(tool_fn):
1616
+ if asyncio.iscoroutinefunction(tool_function):
1566
1617
 
1567
- def _run_coro(coro):
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 e:
1579
- result_container["exception"] = e
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 _run_coro(tool_fn(**args_dict))
1641
+ return run_coroutine_in_thread(tool_function(**args_dict))
1591
1642
  except TypeError:
1592
- return _run_coro(tool_fn(args_dict))
1643
+ return run_coroutine_in_thread(tool_function(args_dict))
1593
1644
 
1594
1645
  try:
1595
- return tool_fn(**args_dict)
1646
+ return tool_function(**args_dict)
1596
1647
  except TypeError:
1597
- return tool_fn(args_dict)
1648
+ return tool_function(args_dict)
1598
1649
 
1599
- handler_fn = source_tool_handler
1650
+ handler_function = source_tool_handler
1600
1651
 
1601
1652
  # Register tool
1602
- builder.register_tool(temp_name, config_dict, handler_fn)
1653
+ builder.register_tool(temporary_name, config_dict, handler_function)
1603
1654
  handle = ToolHandle(
1604
- temp_name,
1605
- handler_fn,
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[temp_name] = handle
1662
+ _tool_registry[temporary_name] = handle
1612
1663
 
1613
1664
  if is_source_tool:
1614
- handle_ref["handle"] = handle
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
- tools = config_dict["tools"]
1654
- if isinstance(tools, (list, tuple)):
1704
+ tool_references = config_dict["tools"]
1705
+ if isinstance(tool_references, (list, tuple)):
1655
1706
  normalized = []
1656
- for t in tools:
1657
- if isinstance(t, dict):
1658
- if "handler" in t:
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(t)
1713
+ normalized.append(tool_entry)
1663
1714
  continue
1664
- if hasattr(t, "name"): # ToolHandle or ToolsetHandle
1665
- normalized.append(t.name)
1715
+ if hasattr(tool_entry, "name"): # ToolHandle or ToolsetHandle
1716
+ normalized.append(tool_entry.name)
1666
1717
  else:
1667
- normalized.append(t)
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 e:
1816
+ except Exception as error:
1766
1817
  logger.error(
1767
- f"[AGENT_CREATION] Failed to create agent '{agent_name}' immediately: {e}",
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
- tools = config_dict["tools"]
1839
- if isinstance(tools, (list, tuple)):
1889
+ tool_references = config_dict["tools"]
1890
+ if isinstance(tool_references, (list, tuple)):
1840
1891
  normalized = []
1841
- for t in tools:
1842
- if isinstance(t, dict):
1843
- if "handler" in t:
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(t)
1898
+ normalized.append(tool_entry)
1848
1899
  continue
1849
- if hasattr(t, "name"): # ToolHandle or ToolsetHandle
1850
- normalized.append(t.name)
1900
+ if hasattr(tool_entry, "name"): # ToolHandle or ToolsetHandle
1901
+ normalized.append(tool_entry.name)
1851
1902
  else:
1852
- normalized.append(t)
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
- temp_name = f"_temp_agent_{uuid.uuid4().hex[:8]}"
1931
+ temporary_agent_name = f"_temp_agent_{uuid.uuid4().hex[:8]}"
1881
1932
 
1882
1933
  # Register agent
1883
- builder.register_agent(temp_name, config_dict, output_schema)
1934
+ builder.register_agent(temporary_agent_name, config_dict, output_schema)
1884
1935
 
1885
1936
  # Create handle
1886
- handle = AgentHandle(temp_name)
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 '{temp_name}': runtime_context={bool(_runtime_context)}, has_log_handler={('log_handler' in _runtime_context) if _runtime_context else False}"
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(f"[AGENT_CREATION] Attempting immediate creation for agent '{temp_name}'")
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={temp_name}, has_log_handler={'log_handler' in agent_config}"
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
- temp_name,
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 '{temp_name}' created immediately during declaration, has_log_handler={hasattr(agent_primitive, 'log_handler') and agent_primitive.log_handler is not None}"
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"][temp_name] = agent_primitive
1946
- logger.debug(f"[AGENT_CREATION] Stored agent '{temp_name}' in _created_agents dict")
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 e:
2003
+ except Exception as error:
1949
2004
  import traceback
1950
2005
 
1951
2006
  logger.error(
1952
- f"[AGENT_CREATION] Failed to create agent '{temp_name}' immediately: {e}",
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[temp_name] = handle
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