tactus 0.36.0__py3-none-any.whl → 0.38.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 (65) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/channels/base.py +22 -2
  3. tactus/adapters/channels/broker.py +1 -0
  4. tactus/adapters/channels/host.py +3 -1
  5. tactus/adapters/channels/ipc.py +18 -3
  6. tactus/adapters/channels/sse.py +2 -0
  7. tactus/adapters/mcp_manager.py +24 -7
  8. tactus/backends/http_backend.py +2 -2
  9. tactus/backends/pytorch_backend.py +2 -2
  10. tactus/broker/client.py +3 -3
  11. tactus/broker/server.py +17 -5
  12. tactus/cli/app.py +212 -57
  13. tactus/core/compaction.py +17 -0
  14. tactus/core/context_assembler.py +73 -0
  15. tactus/core/context_models.py +41 -0
  16. tactus/core/dsl_stubs.py +560 -20
  17. tactus/core/exceptions.py +8 -0
  18. tactus/core/execution_context.py +24 -24
  19. tactus/core/message_history_manager.py +2 -2
  20. tactus/core/mocking.py +12 -0
  21. tactus/core/output_validator.py +6 -6
  22. tactus/core/registry.py +171 -29
  23. tactus/core/retrieval.py +317 -0
  24. tactus/core/retriever_tasks.py +30 -0
  25. tactus/core/runtime.py +431 -117
  26. tactus/dspy/agent.py +143 -82
  27. tactus/dspy/broker_lm.py +13 -7
  28. tactus/dspy/config.py +23 -4
  29. tactus/dspy/module.py +12 -1
  30. tactus/ide/coding_assistant.py +2 -2
  31. tactus/primitives/handles.py +79 -7
  32. tactus/primitives/model.py +1 -1
  33. tactus/primitives/procedure.py +1 -1
  34. tactus/primitives/state.py +2 -2
  35. tactus/sandbox/config.py +1 -1
  36. tactus/sandbox/container_runner.py +13 -6
  37. tactus/sandbox/entrypoint.py +51 -8
  38. tactus/sandbox/protocol.py +5 -0
  39. tactus/stdlib/README.md +10 -1
  40. tactus/stdlib/biblicus/__init__.py +3 -0
  41. tactus/stdlib/biblicus/text.py +189 -0
  42. tactus/stdlib/tac/biblicus/text.tac +32 -0
  43. tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
  44. tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
  45. tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
  46. tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
  47. tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
  48. tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
  49. tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
  50. tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
  51. tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
  52. tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
  53. tactus/testing/behave_integration.py +2 -0
  54. tactus/testing/context.py +10 -6
  55. tactus/testing/evaluation_runner.py +5 -5
  56. tactus/testing/steps/builtin.py +2 -2
  57. tactus/testing/test_runner.py +6 -4
  58. tactus/utils/asyncio_helpers.py +2 -1
  59. tactus/validation/semantic_visitor.py +357 -6
  60. tactus/validation/validator.py +142 -2
  61. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/METADATA +9 -6
  62. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/RECORD +65 -47
  63. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
  64. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
  65. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/dsl_stubs.py CHANGED
@@ -31,10 +31,19 @@ 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, Dict, Optional
35
35
 
36
36
  from .registry import RegistryBuilder
37
- from tactus.primitives.handles import AgentHandle, ModelHandle, AgentLookup, ModelLookup
37
+ from tactus.primitives.handles import (
38
+ AgentHandle,
39
+ AgentLookup,
40
+ CompactorHandle,
41
+ ContextHandle,
42
+ CorpusHandle,
43
+ ModelHandle,
44
+ ModelLookup,
45
+ RetrieverHandle,
46
+ )
38
47
  from tactus.stdlib.classify import ClassifyPrimitive
39
48
 
40
49
 
@@ -108,11 +117,35 @@ def _normalize_schema(schema):
108
117
  return schema
109
118
 
110
119
 
120
+ def _normalize_handle_name(value: Any) -> Any:
121
+ """Normalize handle references to their names when possible."""
122
+ if hasattr(value, "name"):
123
+ return value.name
124
+ return value
125
+
126
+
127
+ def _normalize_context_pack_entry(entry: Any) -> dict[str, Any]:
128
+ """Normalize Context pack entries into a name-based dict."""
129
+ if isinstance(entry, dict):
130
+ normalized = dict(entry)
131
+ if "name" in normalized:
132
+ normalized["name"] = _normalize_handle_name(normalized["name"])
133
+ return normalized
134
+ return {"name": _normalize_handle_name(entry)}
135
+
136
+
137
+ def _normalize_template_vars(vars_dict: Any) -> Any:
138
+ """Normalize template vars coming from Lua tables."""
139
+ if isinstance(vars_dict, list) and len(vars_dict) == 0:
140
+ return {}
141
+ return vars_dict
142
+
143
+
111
144
  def create_dsl_stubs(
112
145
  builder: RegistryBuilder,
113
146
  tool_primitive: Any = None,
114
147
  mock_manager: Any = None,
115
- runtime_context: dict[str, Any] | None = None,
148
+ runtime_context: Optional[Dict[str, Any]] = None,
116
149
  ) -> dict[str, Callable]:
117
150
  """
118
151
  Create DSL stub functions that populate the registry.
@@ -136,6 +169,10 @@ def create_dsl_stubs(
136
169
  _agent_registry: dict[str, AgentHandle] = {}
137
170
  _tool_registry: dict[str, Any] = {} # ToolHandle instances
138
171
  _model_registry: dict[str, ModelHandle] = {}
172
+ _context_registry: dict[str, ContextHandle] = {}
173
+ _corpus_registry: dict[str, CorpusHandle] = {}
174
+ _retriever_registry: dict[str, RetrieverHandle] = {}
175
+ _compactor_registry: dict[str, CompactorHandle] = {}
139
176
 
140
177
  # Store runtime context for immediate agent creation
141
178
  _runtime_context = runtime_context or {}
@@ -144,7 +181,7 @@ def create_dsl_stubs(
144
181
  _procedure_registry = {}
145
182
 
146
183
  def _process_procedure_config(
147
- name: str | None, config: Any, procedure_registry: dict[str, Any]
184
+ name: Optional[str], config: Any, procedure_registry: Dict[str, Any]
148
185
  ):
149
186
  """
150
187
  Process procedure config and register the procedure.
@@ -353,12 +390,89 @@ def create_dsl_stubs(
353
390
  _procedure_registry[name] = stub # Store stub temporarily
354
391
  return stub
355
392
 
356
- # New curried syntax - return a function that accepts config
357
- def accept_config(config):
358
- """Accept config (with function as last unnamed element) and register procedure."""
359
- return _process_procedure_config(name, config, _procedure_registry)
393
+ if name is not None:
360
394
 
361
- return accept_config
395
+ def _curried(config_table=None):
396
+ if config_table is None or not hasattr(config_table, "items"):
397
+ raise TypeError(
398
+ f"procedure '{name}' requires a configuration table. "
399
+ 'Use: Procedure "name" { ... } or name = Procedure { ... }.'
400
+ )
401
+ return _process_procedure_config(name, config_table, _procedure_registry)
402
+
403
+ return _curried
404
+
405
+ def _task(name_or_config=None, config=None):
406
+ """
407
+ Task declaration stub.
408
+
409
+ Supports:
410
+ Task "name" { ... }
411
+ name = Task { ... } (name not available at runtime)
412
+ """
413
+ # Task "name" { ... }
414
+ if isinstance(name_or_config, str):
415
+ task_name = name_or_config
416
+ task_config = config or {}
417
+ if hasattr(task_config, "__setitem__"):
418
+ try:
419
+ task_config["__task_name"] = task_name
420
+ task_config["__tactus_task_config"] = True
421
+ except Exception:
422
+ pass
423
+ task_config_dict = lua_table_to_dict(task_config)
424
+ if "entry" in task_config_dict and not callable(task_config_dict["entry"]):
425
+ raise TypeError(f"Task '{task_name}' entry must be a function")
426
+ builder.register_task(task_name, task_config_dict)
427
+
428
+ if hasattr(task_config, "items"):
429
+ for key, value in task_config.items():
430
+ child_name = None
431
+ if isinstance(key, str):
432
+ child_name = key
433
+ if not hasattr(value, "items"):
434
+ continue
435
+ child_config = lua_table_to_dict(value)
436
+ if not child_name and isinstance(child_config, dict):
437
+ child_name = child_config.get("__task_name")
438
+ if child_name and isinstance(child_config, dict) and "entry" in child_config:
439
+ builder.register_task(child_name, child_config, parent=task_name)
440
+ return task_config
441
+
442
+ # Assignment-based: name = Task { ... }
443
+ if name_or_config is not None and hasattr(name_or_config, "items"):
444
+ task_config = name_or_config
445
+ if hasattr(task_config, "__setitem__"):
446
+ try:
447
+ task_config["__tactus_task_config"] = True
448
+ except Exception:
449
+ pass
450
+ if hasattr(task_config, "items"):
451
+ child_tasks = {}
452
+ for key, value in task_config.items():
453
+ if isinstance(key, str) and hasattr(value, "items"):
454
+ child_tasks[key] = value
455
+ if child_tasks:
456
+ try:
457
+ task_config["__tactus_child_tasks"] = child_tasks
458
+ except Exception:
459
+ pass
460
+ return task_config
461
+
462
+ return {}
463
+
464
+ def _include_tasks(path=None, namespace=None):
465
+ """IncludeTasks stub (records include for runtime)."""
466
+ if hasattr(path, "items"):
467
+ include_config = lua_table_to_dict(path)
468
+ include_path = include_config.get("path")
469
+ include_namespace = include_config.get("namespace")
470
+ if isinstance(include_path, str):
471
+ builder.register_include_tasks(include_path, include_namespace)
472
+ return None
473
+ if isinstance(path, str):
474
+ builder.register_include_tasks(path, namespace if isinstance(namespace, str) else None)
475
+ return None
362
476
 
363
477
  def _prompt(prompt_name: str, content: str) -> None:
364
478
  """Register a prompt template."""
@@ -477,6 +591,67 @@ def create_dsl_stubs(
477
591
 
478
592
  return accept_config
479
593
 
594
+ def _template(template_text: str, vars_table: Any = None) -> dict[str, Any]:
595
+ """
596
+ Create a template directive for context messages.
597
+
598
+ Args:
599
+ template_text: Template string with {input.*} or {context.*} markers
600
+ vars_table: Optional vars table for template values
601
+
602
+ Returns:
603
+ Dict describing the template directive
604
+ """
605
+ if not isinstance(template_text, str):
606
+ raise TypeError("template() expects a string")
607
+ vars_dict = lua_table_to_dict(vars_table) if vars_table is not None else {}
608
+ vars_dict = _normalize_template_vars(vars_dict)
609
+ return {"template": template_text, "vars": vars_dict}
610
+
611
+ def _message_from_arg(message_type: str, arg: Any) -> dict[str, Any]:
612
+ """Create a message directive from a literal or template directive."""
613
+ if isinstance(arg, dict) and "template" in arg:
614
+ return {
615
+ "type": message_type,
616
+ "template": arg.get("template"),
617
+ "vars": arg.get("vars", {}),
618
+ }
619
+ if isinstance(arg, str):
620
+ return {"type": message_type, "content": arg}
621
+ raise TypeError(f"{message_type}() expects a string or template(...) directive")
622
+
623
+ def _system(arg: Any) -> dict[str, Any]:
624
+ """System message directive for Context messages."""
625
+ return _message_from_arg("system", arg)
626
+
627
+ def _user(arg: Any) -> dict[str, Any]:
628
+ """User message directive for Context messages."""
629
+ return _message_from_arg("user", arg)
630
+
631
+ def _assistant(arg: Any) -> dict[str, Any]:
632
+ """Assistant message directive for Context messages."""
633
+ return _message_from_arg("assistant", arg)
634
+
635
+ def _context_insert(pack: Any, budget: Any = None) -> dict[str, Any]:
636
+ """Context pack insertion directive for Context messages."""
637
+ if hasattr(pack, "name"):
638
+ pack_name = pack.name
639
+ else:
640
+ pack_name = pack
641
+ if not isinstance(pack_name, str):
642
+ raise TypeError("context() expects a context or retriever reference")
643
+ budget_dict = lua_table_to_dict(budget) if budget is not None else None
644
+ if isinstance(budget_dict, list) and len(budget_dict) == 0:
645
+ budget_dict = None
646
+ directive = {"type": "context", "name": pack_name}
647
+ if budget_dict is not None:
648
+ directive["budget"] = budget_dict
649
+ return directive
650
+
651
+ def _context_history() -> dict[str, Any]:
652
+ """History insertion directive for Context messages."""
653
+ return {"type": "history"}
654
+
480
655
  def _specification(*args) -> None:
481
656
  """Register BDD specs.
482
657
 
@@ -1044,7 +1219,7 @@ def create_dsl_stubs(
1044
1219
  # Otherwise, ignore unknown mock config.
1045
1220
  continue
1046
1221
 
1047
- def _history(messages=None):
1222
+ def _dspy_history(messages=None):
1048
1223
  """
1049
1224
  Create a History for managing conversation messages.
1050
1225
 
@@ -1666,6 +1841,214 @@ def create_dsl_stubs(
1666
1841
 
1667
1842
  return handle
1668
1843
 
1844
+ def _new_context(name_or_config=None) -> ContextHandle:
1845
+ """
1846
+ New Context factory for assignment-based syntax.
1847
+
1848
+ Syntax:
1849
+ support_context = Context {
1850
+ policy = { ... },
1851
+ messages = { ... }
1852
+ }
1853
+ """
1854
+ if isinstance(name_or_config, str):
1855
+ raise TypeError(
1856
+ "Curried Context syntax is not supported. Use assignment syntax: "
1857
+ "my_context = Context { ... }."
1858
+ )
1859
+
1860
+ config = name_or_config
1861
+ if config is None:
1862
+ raise TypeError("Context requires a configuration table")
1863
+
1864
+ config_dict = lua_table_to_dict(config)
1865
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1866
+ config_dict = {}
1867
+
1868
+ explicit_name = None
1869
+ if isinstance(config_dict, dict):
1870
+ explicit_name = config_dict.pop("name", None)
1871
+ if explicit_name is not None and not isinstance(explicit_name, str):
1872
+ raise TypeError("Context 'name' must be a string")
1873
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1874
+ raise TypeError("Context 'name' cannot be empty")
1875
+
1876
+ packs = config_dict.get("packs")
1877
+
1878
+ if isinstance(packs, list):
1879
+ config_dict["packs"] = [_normalize_context_pack_entry(pack) for pack in packs]
1880
+ elif packs is not None:
1881
+ config_dict["packs"] = [_normalize_context_pack_entry(packs)]
1882
+
1883
+ policy = config_dict.get("policy")
1884
+ if isinstance(policy, dict):
1885
+ compactor_value = policy.get("compactor")
1886
+ if hasattr(compactor_value, "name"):
1887
+ policy["compactor"] = compactor_value.name
1888
+
1889
+ import uuid
1890
+
1891
+ temporary_name = (
1892
+ explicit_name.strip()
1893
+ if isinstance(explicit_name, str)
1894
+ else f"_temp_context_{uuid.uuid4().hex[:8]}"
1895
+ )
1896
+
1897
+ builder.register_context(temporary_name, config_dict)
1898
+ handle = ContextHandle(temporary_name)
1899
+ _context_registry[temporary_name] = handle
1900
+ return handle
1901
+
1902
+ def _new_corpus(name_or_config=None) -> CorpusHandle:
1903
+ """
1904
+ New Corpus factory for assignment-based syntax.
1905
+ """
1906
+ if isinstance(name_or_config, str):
1907
+ raise TypeError(
1908
+ "Curried Corpus syntax is not supported. Use assignment syntax: "
1909
+ "my_corpus = Corpus { ... }."
1910
+ )
1911
+
1912
+ config = name_or_config
1913
+ if config is None:
1914
+ raise TypeError("Corpus requires a configuration table")
1915
+
1916
+ config_dict = lua_table_to_dict(config)
1917
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1918
+ config_dict = {}
1919
+
1920
+ if isinstance(config_dict, dict):
1921
+ if "root" in config_dict and "corpus_root" not in config_dict:
1922
+ config_dict["corpus_root"] = config_dict.pop("root")
1923
+
1924
+ if isinstance(config_dict, dict):
1925
+ configuration = config_dict.get("configuration")
1926
+ if isinstance(configuration, dict):
1927
+ pipeline = configuration.get("pipeline")
1928
+ if isinstance(pipeline, list) and len(pipeline) == 0:
1929
+ configuration["pipeline"] = {}
1930
+
1931
+ explicit_name = None
1932
+ if isinstance(config_dict, dict):
1933
+ explicit_name = config_dict.pop("name", None)
1934
+ if explicit_name is not None and not isinstance(explicit_name, str):
1935
+ raise TypeError("Corpus 'name' must be a string")
1936
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1937
+ raise TypeError("Corpus 'name' cannot be empty")
1938
+
1939
+ import uuid
1940
+
1941
+ temporary_name = (
1942
+ explicit_name.strip()
1943
+ if isinstance(explicit_name, str)
1944
+ else f"_temp_corpus_{uuid.uuid4().hex[:8]}"
1945
+ )
1946
+
1947
+ builder.register_corpus(temporary_name, config_dict)
1948
+ handle = CorpusHandle(temporary_name)
1949
+ _corpus_registry[temporary_name] = handle
1950
+ return handle
1951
+
1952
+ def _new_retriever(name_or_config=None) -> RetrieverHandle:
1953
+ """
1954
+ New Retriever factory for assignment-based syntax.
1955
+ """
1956
+ if isinstance(name_or_config, str):
1957
+ raise TypeError(
1958
+ "Curried Retriever syntax is not supported. Use assignment syntax: "
1959
+ "my_retriever = Retriever { ... }."
1960
+ )
1961
+
1962
+ config = name_or_config
1963
+ if config is None:
1964
+ raise TypeError("Retriever requires a configuration table")
1965
+
1966
+ config_dict = lua_table_to_dict(config)
1967
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1968
+ config_dict = {}
1969
+
1970
+ if isinstance(config_dict, dict):
1971
+ config_dict["corpus"] = _normalize_handle_name(config_dict.get("corpus"))
1972
+
1973
+ explicit_name = None
1974
+ if isinstance(config_dict, dict):
1975
+ explicit_name = config_dict.pop("name", None)
1976
+ if explicit_name is not None and not isinstance(explicit_name, str):
1977
+ raise TypeError("Retriever 'name' must be a string")
1978
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1979
+ raise TypeError("Retriever 'name' cannot be empty")
1980
+
1981
+ import uuid
1982
+
1983
+ temporary_name = (
1984
+ explicit_name.strip()
1985
+ if isinstance(explicit_name, str)
1986
+ else f"_temp_retriever_{uuid.uuid4().hex[:8]}"
1987
+ )
1988
+
1989
+ builder.register_retriever(temporary_name, config_dict)
1990
+ handle = RetrieverHandle(temporary_name)
1991
+ _retriever_registry[temporary_name] = handle
1992
+ return handle
1993
+
1994
+ def _task_function(function):
1995
+ """
1996
+ Wrap a callable so Task entries can use call-style syntax without executing immediately.
1997
+ """
1998
+ if not callable(function):
1999
+ raise TypeError("TaskFunction expects a callable")
2000
+
2001
+ def _deferred(args):
2002
+ def _runner():
2003
+ if hasattr(args, "items"):
2004
+ args_dict = lua_table_to_dict(args)
2005
+ else:
2006
+ args_dict = args
2007
+ return function(args_dict)
2008
+
2009
+ return _runner
2010
+
2011
+ return _deferred
2012
+
2013
+ def _new_compactor(name_or_config=None) -> CompactorHandle:
2014
+ """
2015
+ New Compactor factory for assignment-based syntax.
2016
+ """
2017
+ if isinstance(name_or_config, str):
2018
+ raise TypeError(
2019
+ "Curried Compactor syntax is not supported. Use assignment syntax: "
2020
+ "my_compactor = Compactor { ... }."
2021
+ )
2022
+
2023
+ config = name_or_config
2024
+ if config is None:
2025
+ raise TypeError("Compactor requires a configuration table")
2026
+
2027
+ config_dict = lua_table_to_dict(config)
2028
+ if isinstance(config_dict, list) and len(config_dict) == 0:
2029
+ config_dict = {}
2030
+
2031
+ explicit_name = None
2032
+ if isinstance(config_dict, dict):
2033
+ explicit_name = config_dict.pop("name", None)
2034
+ if explicit_name is not None and not isinstance(explicit_name, str):
2035
+ raise TypeError("Compactor 'name' must be a string")
2036
+ if isinstance(explicit_name, str) and not explicit_name.strip():
2037
+ raise TypeError("Compactor 'name' cannot be empty")
2038
+
2039
+ import uuid
2040
+
2041
+ temporary_name = (
2042
+ explicit_name.strip()
2043
+ if isinstance(explicit_name, str)
2044
+ else f"_temp_compactor_{uuid.uuid4().hex[:8]}"
2045
+ )
2046
+
2047
+ builder.register_compactor(temporary_name, config_dict)
2048
+ handle = CompactorHandle(temporary_name)
2049
+ _compactor_registry[temporary_name] = handle
2050
+ return handle
2051
+
1669
2052
  def _process_agent_config(agent_name, config):
1670
2053
  """
1671
2054
  Process agent configuration for both curried and direct syntax.
@@ -1718,6 +2101,11 @@ def create_dsl_stubs(
1718
2101
  normalized.append(tool_entry)
1719
2102
  config_dict["tools"] = normalized
1720
2103
 
2104
+ if "context" in config_dict:
2105
+ context_value = config_dict["context"]
2106
+ if hasattr(context_value, "name"):
2107
+ config_dict["context"] = context_value.name
2108
+
1721
2109
  # Extract input schema if present
1722
2110
  input_schema = None
1723
2111
  if "input" in config_dict:
@@ -1903,6 +2291,11 @@ def create_dsl_stubs(
1903
2291
  normalized.append(tool_entry)
1904
2292
  config_dict["tools"] = normalized
1905
2293
 
2294
+ if "context" in config_dict:
2295
+ context_value = config_dict["context"]
2296
+ if hasattr(context_value, "name"):
2297
+ config_dict["context"] = context_value.name
2298
+
1906
2299
  # Extract input schema if present
1907
2300
  input_schema = None
1908
2301
  if "input" in config_dict:
@@ -2001,13 +2394,21 @@ def create_dsl_stubs(
2001
2394
  )
2002
2395
 
2003
2396
  except Exception as error:
2004
- import traceback
2005
-
2006
- logger.error(
2007
- f"[AGENT_CREATION] Failed to create agent '{temporary_agent_name}' immediately: {error}",
2008
- exc_info=True,
2009
- )
2010
- logger.debug(f"Full traceback: {traceback.format_exc()}")
2397
+ if isinstance(error, ValueError) and str(error).startswith("LM not configured"):
2398
+ # Two-phase initialization supports late LM setup (common in tests and
2399
+ # simple scripts). Treat this as an expected fallback rather than a hard error.
2400
+ logger.debug(
2401
+ "[AGENT_CREATION] Delayed creation for agent '%s': %s",
2402
+ temporary_agent_name,
2403
+ error,
2404
+ )
2405
+ else:
2406
+ logger.error(
2407
+ "[AGENT_CREATION] Failed to create agent '%s' immediately: %s",
2408
+ temporary_agent_name,
2409
+ error,
2410
+ exc_info=True,
2411
+ )
2011
2412
  # Fall back to two-phase initialization if immediate creation fails
2012
2413
 
2013
2414
  # Register handle for lookup
@@ -2086,7 +2487,14 @@ def create_dsl_stubs(
2086
2487
  return classify_primitive(lua_table_to_dict(config))
2087
2488
 
2088
2489
  binding_callback = _make_binding_callback(
2089
- builder, _tool_registry, _agent_registry, _runtime_context
2490
+ builder,
2491
+ _tool_registry,
2492
+ _agent_registry,
2493
+ _context_registry,
2494
+ _corpus_registry,
2495
+ _retriever_registry,
2496
+ _compactor_registry,
2497
+ _runtime_context,
2090
2498
  )
2091
2499
 
2092
2500
  return {
@@ -2097,9 +2505,16 @@ def create_dsl_stubs(
2097
2505
  "Agent": _new_agent, # NEW syntax - assignment based
2098
2506
  "Model": HybridModel(_model, _Model),
2099
2507
  "Procedure": _procedure,
2508
+ "Task": _task,
2509
+ "IncludeTasks": _include_tasks,
2100
2510
  "Prompt": _prompt,
2101
2511
  "Toolset": _toolset,
2102
2512
  "Tool": _new_tool, # NEW syntax - assignment based
2513
+ "Context": _new_context,
2514
+ "Corpus": _new_corpus,
2515
+ "Retriever": _new_retriever,
2516
+ "Compactor": _new_compactor,
2517
+ "TaskFunction": _task_function,
2103
2518
  "Classify": _new_classify, # NEW stdlib: smart classification with retry
2104
2519
  "Hitl": _hitl,
2105
2520
  "Specification": _specification,
@@ -2116,9 +2531,11 @@ def create_dsl_stubs(
2116
2531
  "get_current_lm": _get_current_lm,
2117
2532
  "Signature": _signature,
2118
2533
  "Module": _module,
2119
- "History": _history,
2534
+ "History": _dspy_history,
2120
2535
  "Message": _message,
2121
2536
  "DSPyAgent": _dspy_agent,
2537
+ "_tactus_internal_corpus": _new_corpus,
2538
+ "_tactus_internal_retriever": _new_retriever,
2122
2539
  # Script mode (top-level declarations)
2123
2540
  "input": _input,
2124
2541
  "output": _output,
@@ -2131,6 +2548,13 @@ def create_dsl_stubs(
2131
2548
  "async": _async,
2132
2549
  "max_depth": _max_depth,
2133
2550
  "max_turns": _max_turns,
2551
+ # Context message directives
2552
+ "system": _system,
2553
+ "user": _user,
2554
+ "assistant": _assistant,
2555
+ "context": _context_insert,
2556
+ "history": _context_history,
2557
+ "template": _template,
2134
2558
  # Built-in filters (exposed as a table)
2135
2559
  "filters": {
2136
2560
  "last_n": _last_n,
@@ -2155,6 +2579,10 @@ def create_dsl_stubs(
2155
2579
  "agent": _agent_registry,
2156
2580
  "tool": _tool_registry,
2157
2581
  "model": _model_registry,
2582
+ "context": _context_registry,
2583
+ "corpus": _corpus_registry,
2584
+ "retriever": _retriever_registry,
2585
+ "compactor": _compactor_registry,
2158
2586
  },
2159
2587
  # Assignment interception callback
2160
2588
  "_tactus_register_binding": binding_callback,
@@ -2162,7 +2590,14 @@ def create_dsl_stubs(
2162
2590
 
2163
2591
 
2164
2592
  def _make_binding_callback(
2165
- builder: RegistryBuilder, tool_registry: dict, agent_registry: dict, runtime_context: dict
2593
+ builder: RegistryBuilder,
2594
+ tool_registry: dict,
2595
+ agent_registry: dict,
2596
+ context_registry: dict,
2597
+ corpus_registry: dict,
2598
+ retriever_registry: dict,
2599
+ compactor_registry: dict,
2600
+ runtime_context: dict,
2166
2601
  ):
2167
2602
  """
2168
2603
  Factory to create the binding callback with closure over builder/registries/runtime_context.
@@ -2170,6 +2605,12 @@ def _make_binding_callback(
2170
2605
  This callback is called by Lua's __newindex metatable when assignments happen.
2171
2606
  """
2172
2607
  import logging
2608
+ from tactus.primitives.handles import (
2609
+ CompactorHandle,
2610
+ ContextHandle,
2611
+ CorpusHandle,
2612
+ RetrieverHandle,
2613
+ )
2173
2614
  from tactus.primitives.tool_handle import ToolHandle
2174
2615
 
2175
2616
  callback_logger = logging.getLogger(__name__)
@@ -2254,6 +2695,105 @@ def _make_binding_callback(
2254
2695
  f"[AGENT_RENAME] Updated _created_agents dict: '{old_name}' -> '{name}'"
2255
2696
  )
2256
2697
 
2698
+ if isinstance(value, ContextHandle):
2699
+ old_name = value.name
2700
+ if old_name.startswith("_temp_context_"):
2701
+ callback_logger.debug(f"Renaming context '{old_name}' to '{name}'")
2702
+ value.name = name
2703
+ if old_name in context_registry:
2704
+ del context_registry[old_name]
2705
+ context_registry[name] = value
2706
+ if hasattr(builder, "registry") and old_name in builder.registry.contexts:
2707
+ context_data = builder.registry.contexts.pop(old_name)
2708
+ builder.registry.contexts[name] = context_data
2709
+ elif old_name != name:
2710
+ raise RuntimeError(
2711
+ f"Context name mismatch: assigned to '{name}' but context is named '{old_name}'. "
2712
+ "Remove the Context config 'name' field or make it match the assigned variable."
2713
+ )
2714
+
2715
+ if isinstance(value, CorpusHandle):
2716
+ old_name = value.name
2717
+ if old_name.startswith("_temp_corpus_"):
2718
+ callback_logger.debug(f"Renaming corpus '{old_name}' to '{name}'")
2719
+ value.name = name
2720
+ if old_name in corpus_registry:
2721
+ del corpus_registry[old_name]
2722
+ corpus_registry[name] = value
2723
+ if hasattr(builder, "registry") and old_name in builder.registry.corpora:
2724
+ corpus_data = builder.registry.corpora.pop(old_name)
2725
+ builder.registry.corpora[name] = corpus_data
2726
+ elif old_name != name:
2727
+ raise RuntimeError(
2728
+ f"Corpus name mismatch: assigned to '{name}' but corpus is named '{old_name}'. "
2729
+ "Remove the Corpus config 'name' field or make it match the assigned variable."
2730
+ )
2731
+
2732
+ if isinstance(value, RetrieverHandle):
2733
+ old_name = value.name
2734
+ if old_name.startswith("_temp_retriever_"):
2735
+ callback_logger.debug(f"Renaming retriever '{old_name}' to '{name}'")
2736
+ value.name = name
2737
+ if old_name in retriever_registry:
2738
+ del retriever_registry[old_name]
2739
+ retriever_registry[name] = value
2740
+ if hasattr(builder, "registry") and old_name in builder.registry.retrievers:
2741
+ retriever_data = builder.registry.retrievers.pop(old_name)
2742
+ builder.registry.retrievers[name] = retriever_data
2743
+ elif old_name != name:
2744
+ raise RuntimeError(
2745
+ f"Retriever name mismatch: assigned to '{name}' but retriever is named '{old_name}'. "
2746
+ "Remove the Retriever config 'name' field or make it match the assigned variable."
2747
+ )
2748
+
2749
+ if isinstance(value, CompactorHandle):
2750
+ old_name = value.name
2751
+ if old_name.startswith("_temp_compactor_"):
2752
+ callback_logger.debug(f"Renaming compactor '{old_name}' to '{name}'")
2753
+ value.name = name
2754
+ if old_name in compactor_registry:
2755
+ del compactor_registry[old_name]
2756
+ compactor_registry[name] = value
2757
+ if hasattr(builder, "registry") and old_name in builder.registry.compactors:
2758
+ compactor_data = builder.registry.compactors.pop(old_name)
2759
+ builder.registry.compactors[name] = compactor_data
2760
+ elif old_name != name:
2761
+ raise RuntimeError(
2762
+ f"Compactor name mismatch: assigned to '{name}' but compactor is named '{old_name}'. "
2763
+ "Remove the Compactor config 'name' field or make it match the assigned variable."
2764
+ )
2765
+
2766
+ if hasattr(value, "items"):
2767
+ try:
2768
+ task_config = lua_table_to_dict(value)
2769
+ except Exception:
2770
+ task_config = None
2771
+ if isinstance(task_config, dict) and task_config.get("__tactus_task_config"):
2772
+ if hasattr(builder, "registry") and name not in builder.registry.tasks:
2773
+ if "entry" in task_config and not callable(task_config["entry"]):
2774
+ raise TypeError(f"Task '{name}' entry must be a function")
2775
+ builder.register_task(name, task_config)
2776
+
2777
+ child_sources = task_config.get("__tactus_child_tasks")
2778
+ if isinstance(child_sources, dict):
2779
+ child_iter = child_sources.items()
2780
+ else:
2781
+ child_iter = value.items()
2782
+
2783
+ for key, child_value in child_iter:
2784
+ child_name = key if isinstance(key, str) else None
2785
+ if not hasattr(child_value, "items"):
2786
+ continue
2787
+ child_config = lua_table_to_dict(child_value)
2788
+ if not child_name and isinstance(child_config, dict):
2789
+ child_name = child_config.get("__task_name")
2790
+ if child_name and isinstance(child_config, dict):
2791
+ if "entry" in child_config and not callable(child_config["entry"]):
2792
+ raise TypeError(
2793
+ f"Task '{name}:{child_name}' entry must be a function"
2794
+ )
2795
+ builder.register_task(child_name, child_config, parent=name)
2796
+
2257
2797
  # Log all assignments for debugging (only at trace level to avoid noise)
2258
2798
  callback_logger.debug(f"Assignment captured: {name} = {type(value).__name__}")
2259
2799