tactus 0.37.0__py3-none-any.whl → 0.39.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 (48) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/channels/base.py +2 -0
  3. tactus/cli/app.py +212 -57
  4. tactus/core/compaction.py +17 -0
  5. tactus/core/context_assembler.py +73 -0
  6. tactus/core/context_models.py +41 -0
  7. tactus/core/dsl_stubs.py +568 -17
  8. tactus/core/exceptions.py +8 -0
  9. tactus/core/execution_context.py +1 -1
  10. tactus/core/mocking.py +12 -0
  11. tactus/core/registry.py +142 -0
  12. tactus/core/retrieval.py +317 -0
  13. tactus/core/retriever_tasks.py +30 -0
  14. tactus/core/runtime.py +441 -75
  15. tactus/dspy/agent.py +143 -82
  16. tactus/dspy/config.py +16 -0
  17. tactus/dspy/module.py +12 -1
  18. tactus/ide/coding_assistant.py +2 -2
  19. tactus/plugins/__init__.py +3 -0
  20. tactus/plugins/noaa.py +76 -0
  21. tactus/primitives/handles.py +79 -7
  22. tactus/sandbox/config.py +1 -1
  23. tactus/sandbox/container_runner.py +2 -0
  24. tactus/sandbox/entrypoint.py +51 -8
  25. tactus/sandbox/protocol.py +5 -0
  26. tactus/stdlib/README.md +10 -1
  27. tactus/stdlib/biblicus/__init__.py +3 -0
  28. tactus/stdlib/biblicus/text.py +208 -0
  29. tactus/stdlib/tac/biblicus/text.tac +32 -0
  30. tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
  31. tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
  32. tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
  33. tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
  34. tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
  35. tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
  36. tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
  37. tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
  38. tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
  39. tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
  40. tactus/testing/behave_integration.py +2 -0
  41. tactus/testing/context.py +4 -0
  42. tactus/validation/semantic_visitor.py +430 -88
  43. tactus/validation/validator.py +142 -2
  44. {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/METADATA +3 -2
  45. {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/RECORD +48 -28
  46. {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/WHEEL +0 -0
  47. {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/entry_points.txt +0 -0
  48. {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/dsl_stubs.py CHANGED
@@ -34,7 +34,16 @@ Agent/Tool calls use direct variable access:
34
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,6 +117,30 @@ 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,
@@ -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 {}
@@ -353,12 +390,100 @@ 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
+ if config is None:
417
+
418
+ def _curried(task_config=None):
419
+ if task_config is None or not hasattr(task_config, "items"):
420
+ raise TypeError(
421
+ f"Task '{task_name}' requires a configuration table. "
422
+ 'Use: Task "name" { ... } or name = Task { ... }.'
423
+ )
424
+ return _task(task_name, task_config)
425
+
426
+ return _curried
427
+ task_config = config or {}
428
+ if hasattr(task_config, "__setitem__"):
429
+ try:
430
+ task_config["__task_name"] = task_name
431
+ task_config["__tactus_task_config"] = True
432
+ except Exception:
433
+ pass
434
+ task_config_dict = lua_table_to_dict(task_config)
435
+ if "entry" in task_config_dict and not callable(task_config_dict["entry"]):
436
+ raise TypeError(f"Task '{task_name}' entry must be a function")
437
+ builder.register_task(task_name, task_config_dict)
438
+
439
+ if hasattr(task_config, "items"):
440
+ for key, value in task_config.items():
441
+ child_name = None
442
+ if isinstance(key, str):
443
+ child_name = key
444
+ if not hasattr(value, "items"):
445
+ continue
446
+ child_config = lua_table_to_dict(value)
447
+ if not child_name and isinstance(child_config, dict):
448
+ child_name = child_config.get("__task_name")
449
+ if child_name and isinstance(child_config, dict) and "entry" in child_config:
450
+ builder.register_task(child_name, child_config, parent=task_name)
451
+ return task_config
452
+
453
+ # Assignment-based: name = Task { ... }
454
+ if name_or_config is not None and hasattr(name_or_config, "items"):
455
+ task_config = name_or_config
456
+ if hasattr(task_config, "__setitem__"):
457
+ try:
458
+ task_config["__tactus_task_config"] = True
459
+ except Exception:
460
+ pass
461
+ if hasattr(task_config, "items"):
462
+ child_tasks = {}
463
+ for key, value in task_config.items():
464
+ if isinstance(key, str) and hasattr(value, "items"):
465
+ child_tasks[key] = value
466
+ if child_tasks:
467
+ try:
468
+ task_config["__tactus_child_tasks"] = child_tasks
469
+ except Exception:
470
+ pass
471
+ return task_config
472
+
473
+ return {}
474
+
475
+ def _include_tasks(path=None, namespace=None):
476
+ """IncludeTasks stub (records include for runtime)."""
477
+ if hasattr(path, "items"):
478
+ include_config = lua_table_to_dict(path)
479
+ include_path = include_config.get("path")
480
+ include_namespace = include_config.get("namespace")
481
+ if isinstance(include_path, str):
482
+ builder.register_include_tasks(include_path, include_namespace)
483
+ return None
484
+ if isinstance(path, str):
485
+ builder.register_include_tasks(path, namespace if isinstance(namespace, str) else None)
486
+ return None
362
487
 
363
488
  def _prompt(prompt_name: str, content: str) -> None:
364
489
  """Register a prompt template."""
@@ -477,6 +602,67 @@ def create_dsl_stubs(
477
602
 
478
603
  return accept_config
479
604
 
605
+ def _template(template_text: str, vars_table: Any = None) -> dict[str, Any]:
606
+ """
607
+ Create a template directive for context messages.
608
+
609
+ Args:
610
+ template_text: Template string with {input.*} or {context.*} markers
611
+ vars_table: Optional vars table for template values
612
+
613
+ Returns:
614
+ Dict describing the template directive
615
+ """
616
+ if not isinstance(template_text, str):
617
+ raise TypeError("template() expects a string")
618
+ vars_dict = lua_table_to_dict(vars_table) if vars_table is not None else {}
619
+ vars_dict = _normalize_template_vars(vars_dict)
620
+ return {"template": template_text, "vars": vars_dict}
621
+
622
+ def _message_from_arg(message_type: str, arg: Any) -> dict[str, Any]:
623
+ """Create a message directive from a literal or template directive."""
624
+ if isinstance(arg, dict) and "template" in arg:
625
+ return {
626
+ "type": message_type,
627
+ "template": arg.get("template"),
628
+ "vars": arg.get("vars", {}),
629
+ }
630
+ if isinstance(arg, str):
631
+ return {"type": message_type, "content": arg}
632
+ raise TypeError(f"{message_type}() expects a string or template(...) directive")
633
+
634
+ def _system(arg: Any) -> dict[str, Any]:
635
+ """System message directive for Context messages."""
636
+ return _message_from_arg("system", arg)
637
+
638
+ def _user(arg: Any) -> dict[str, Any]:
639
+ """User message directive for Context messages."""
640
+ return _message_from_arg("user", arg)
641
+
642
+ def _assistant(arg: Any) -> dict[str, Any]:
643
+ """Assistant message directive for Context messages."""
644
+ return _message_from_arg("assistant", arg)
645
+
646
+ def _context_insert(pack: Any, budget: Any = None) -> dict[str, Any]:
647
+ """Context pack insertion directive for Context messages."""
648
+ if hasattr(pack, "name"):
649
+ pack_name = pack.name
650
+ else:
651
+ pack_name = pack
652
+ if not isinstance(pack_name, str):
653
+ raise TypeError("context() expects a context or retriever reference")
654
+ budget_dict = lua_table_to_dict(budget) if budget is not None else None
655
+ if isinstance(budget_dict, list) and len(budget_dict) == 0:
656
+ budget_dict = None
657
+ directive = {"type": "context", "name": pack_name}
658
+ if budget_dict is not None:
659
+ directive["budget"] = budget_dict
660
+ return directive
661
+
662
+ def _context_history() -> dict[str, Any]:
663
+ """History insertion directive for Context messages."""
664
+ return {"type": "history"}
665
+
480
666
  def _specification(*args) -> None:
481
667
  """Register BDD specs.
482
668
 
@@ -1044,7 +1230,7 @@ def create_dsl_stubs(
1044
1230
  # Otherwise, ignore unknown mock config.
1045
1231
  continue
1046
1232
 
1047
- def _history(messages=None):
1233
+ def _dspy_history(messages=None):
1048
1234
  """
1049
1235
  Create a History for managing conversation messages.
1050
1236
 
@@ -1666,6 +1852,214 @@ def create_dsl_stubs(
1666
1852
 
1667
1853
  return handle
1668
1854
 
1855
+ def _new_context(name_or_config=None) -> ContextHandle:
1856
+ """
1857
+ New Context factory for assignment-based syntax.
1858
+
1859
+ Syntax:
1860
+ support_context = Context {
1861
+ policy = { ... },
1862
+ messages = { ... }
1863
+ }
1864
+ """
1865
+ if isinstance(name_or_config, str):
1866
+ raise TypeError(
1867
+ "Curried Context syntax is not supported. Use assignment syntax: "
1868
+ "my_context = Context { ... }."
1869
+ )
1870
+
1871
+ config = name_or_config
1872
+ if config is None:
1873
+ raise TypeError("Context requires a configuration table")
1874
+
1875
+ config_dict = lua_table_to_dict(config)
1876
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1877
+ config_dict = {}
1878
+
1879
+ explicit_name = None
1880
+ if isinstance(config_dict, dict):
1881
+ explicit_name = config_dict.pop("name", None)
1882
+ if explicit_name is not None and not isinstance(explicit_name, str):
1883
+ raise TypeError("Context 'name' must be a string")
1884
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1885
+ raise TypeError("Context 'name' cannot be empty")
1886
+
1887
+ packs = config_dict.get("packs")
1888
+
1889
+ if isinstance(packs, list):
1890
+ config_dict["packs"] = [_normalize_context_pack_entry(pack) for pack in packs]
1891
+ elif packs is not None:
1892
+ config_dict["packs"] = [_normalize_context_pack_entry(packs)]
1893
+
1894
+ policy = config_dict.get("policy")
1895
+ if isinstance(policy, dict):
1896
+ compactor_value = policy.get("compactor")
1897
+ if hasattr(compactor_value, "name"):
1898
+ policy["compactor"] = compactor_value.name
1899
+
1900
+ import uuid
1901
+
1902
+ temporary_name = (
1903
+ explicit_name.strip()
1904
+ if isinstance(explicit_name, str)
1905
+ else f"_temp_context_{uuid.uuid4().hex[:8]}"
1906
+ )
1907
+
1908
+ builder.register_context(temporary_name, config_dict)
1909
+ handle = ContextHandle(temporary_name)
1910
+ _context_registry[temporary_name] = handle
1911
+ return handle
1912
+
1913
+ def _new_corpus(name_or_config=None) -> CorpusHandle:
1914
+ """
1915
+ New Corpus factory for assignment-based syntax.
1916
+ """
1917
+ if isinstance(name_or_config, str):
1918
+ raise TypeError(
1919
+ "Curried Corpus syntax is not supported. Use assignment syntax: "
1920
+ "my_corpus = Corpus { ... }."
1921
+ )
1922
+
1923
+ config = name_or_config
1924
+ if config is None:
1925
+ raise TypeError("Corpus requires a configuration table")
1926
+
1927
+ config_dict = lua_table_to_dict(config)
1928
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1929
+ config_dict = {}
1930
+
1931
+ if isinstance(config_dict, dict):
1932
+ if "root" in config_dict and "corpus_root" not in config_dict:
1933
+ config_dict["corpus_root"] = config_dict.pop("root")
1934
+
1935
+ if isinstance(config_dict, dict):
1936
+ configuration = config_dict.get("configuration")
1937
+ if isinstance(configuration, dict):
1938
+ pipeline = configuration.get("pipeline")
1939
+ if isinstance(pipeline, list) and len(pipeline) == 0:
1940
+ configuration["pipeline"] = {}
1941
+
1942
+ explicit_name = None
1943
+ if isinstance(config_dict, dict):
1944
+ explicit_name = config_dict.pop("name", None)
1945
+ if explicit_name is not None and not isinstance(explicit_name, str):
1946
+ raise TypeError("Corpus 'name' must be a string")
1947
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1948
+ raise TypeError("Corpus 'name' cannot be empty")
1949
+
1950
+ import uuid
1951
+
1952
+ temporary_name = (
1953
+ explicit_name.strip()
1954
+ if isinstance(explicit_name, str)
1955
+ else f"_temp_corpus_{uuid.uuid4().hex[:8]}"
1956
+ )
1957
+
1958
+ builder.register_corpus(temporary_name, config_dict)
1959
+ handle = CorpusHandle(temporary_name)
1960
+ _corpus_registry[temporary_name] = handle
1961
+ return handle
1962
+
1963
+ def _new_retriever(name_or_config=None) -> RetrieverHandle:
1964
+ """
1965
+ New Retriever factory for assignment-based syntax.
1966
+ """
1967
+ if isinstance(name_or_config, str):
1968
+ raise TypeError(
1969
+ "Curried Retriever syntax is not supported. Use assignment syntax: "
1970
+ "my_retriever = Retriever { ... }."
1971
+ )
1972
+
1973
+ config = name_or_config
1974
+ if config is None:
1975
+ raise TypeError("Retriever requires a configuration table")
1976
+
1977
+ config_dict = lua_table_to_dict(config)
1978
+ if isinstance(config_dict, list) and len(config_dict) == 0:
1979
+ config_dict = {}
1980
+
1981
+ if isinstance(config_dict, dict):
1982
+ config_dict["corpus"] = _normalize_handle_name(config_dict.get("corpus"))
1983
+
1984
+ explicit_name = None
1985
+ if isinstance(config_dict, dict):
1986
+ explicit_name = config_dict.pop("name", None)
1987
+ if explicit_name is not None and not isinstance(explicit_name, str):
1988
+ raise TypeError("Retriever 'name' must be a string")
1989
+ if isinstance(explicit_name, str) and not explicit_name.strip():
1990
+ raise TypeError("Retriever 'name' cannot be empty")
1991
+
1992
+ import uuid
1993
+
1994
+ temporary_name = (
1995
+ explicit_name.strip()
1996
+ if isinstance(explicit_name, str)
1997
+ else f"_temp_retriever_{uuid.uuid4().hex[:8]}"
1998
+ )
1999
+
2000
+ builder.register_retriever(temporary_name, config_dict)
2001
+ handle = RetrieverHandle(temporary_name)
2002
+ _retriever_registry[temporary_name] = handle
2003
+ return handle
2004
+
2005
+ def _task_function(function):
2006
+ """
2007
+ Wrap a callable so Task entries can use call-style syntax without executing immediately.
2008
+ """
2009
+ if not callable(function):
2010
+ raise TypeError("TaskFunction expects a callable")
2011
+
2012
+ def _deferred(args):
2013
+ def _runner():
2014
+ if hasattr(args, "items"):
2015
+ args_dict = lua_table_to_dict(args)
2016
+ else:
2017
+ args_dict = args
2018
+ return function(args_dict)
2019
+
2020
+ return _runner
2021
+
2022
+ return _deferred
2023
+
2024
+ def _new_compactor(name_or_config=None) -> CompactorHandle:
2025
+ """
2026
+ New Compactor factory for assignment-based syntax.
2027
+ """
2028
+ if isinstance(name_or_config, str):
2029
+ raise TypeError(
2030
+ "Curried Compactor syntax is not supported. Use assignment syntax: "
2031
+ "my_compactor = Compactor { ... }."
2032
+ )
2033
+
2034
+ config = name_or_config
2035
+ if config is None:
2036
+ raise TypeError("Compactor requires a configuration table")
2037
+
2038
+ config_dict = lua_table_to_dict(config)
2039
+ if isinstance(config_dict, list) and len(config_dict) == 0:
2040
+ config_dict = {}
2041
+
2042
+ explicit_name = None
2043
+ if isinstance(config_dict, dict):
2044
+ explicit_name = config_dict.pop("name", None)
2045
+ if explicit_name is not None and not isinstance(explicit_name, str):
2046
+ raise TypeError("Compactor 'name' must be a string")
2047
+ if isinstance(explicit_name, str) and not explicit_name.strip():
2048
+ raise TypeError("Compactor 'name' cannot be empty")
2049
+
2050
+ import uuid
2051
+
2052
+ temporary_name = (
2053
+ explicit_name.strip()
2054
+ if isinstance(explicit_name, str)
2055
+ else f"_temp_compactor_{uuid.uuid4().hex[:8]}"
2056
+ )
2057
+
2058
+ builder.register_compactor(temporary_name, config_dict)
2059
+ handle = CompactorHandle(temporary_name)
2060
+ _compactor_registry[temporary_name] = handle
2061
+ return handle
2062
+
1669
2063
  def _process_agent_config(agent_name, config):
1670
2064
  """
1671
2065
  Process agent configuration for both curried and direct syntax.
@@ -1718,6 +2112,11 @@ def create_dsl_stubs(
1718
2112
  normalized.append(tool_entry)
1719
2113
  config_dict["tools"] = normalized
1720
2114
 
2115
+ if "context" in config_dict:
2116
+ context_value = config_dict["context"]
2117
+ if hasattr(context_value, "name"):
2118
+ config_dict["context"] = context_value.name
2119
+
1721
2120
  # Extract input schema if present
1722
2121
  input_schema = None
1723
2122
  if "input" in config_dict:
@@ -1903,6 +2302,11 @@ def create_dsl_stubs(
1903
2302
  normalized.append(tool_entry)
1904
2303
  config_dict["tools"] = normalized
1905
2304
 
2305
+ if "context" in config_dict:
2306
+ context_value = config_dict["context"]
2307
+ if hasattr(context_value, "name"):
2308
+ config_dict["context"] = context_value.name
2309
+
1906
2310
  # Extract input schema if present
1907
2311
  input_schema = None
1908
2312
  if "input" in config_dict:
@@ -2001,13 +2405,21 @@ def create_dsl_stubs(
2001
2405
  )
2002
2406
 
2003
2407
  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()}")
2408
+ if isinstance(error, ValueError) and str(error).startswith("LM not configured"):
2409
+ # Two-phase initialization supports late LM setup (common in tests and
2410
+ # simple scripts). Treat this as an expected fallback rather than a hard error.
2411
+ logger.debug(
2412
+ "[AGENT_CREATION] Delayed creation for agent '%s': %s",
2413
+ temporary_agent_name,
2414
+ error,
2415
+ )
2416
+ else:
2417
+ logger.error(
2418
+ "[AGENT_CREATION] Failed to create agent '%s' immediately: %s",
2419
+ temporary_agent_name,
2420
+ error,
2421
+ exc_info=True,
2422
+ )
2011
2423
  # Fall back to two-phase initialization if immediate creation fails
2012
2424
 
2013
2425
  # Register handle for lookup
@@ -2086,7 +2498,14 @@ def create_dsl_stubs(
2086
2498
  return classify_primitive(lua_table_to_dict(config))
2087
2499
 
2088
2500
  binding_callback = _make_binding_callback(
2089
- builder, _tool_registry, _agent_registry, _runtime_context
2501
+ builder,
2502
+ _tool_registry,
2503
+ _agent_registry,
2504
+ _context_registry,
2505
+ _corpus_registry,
2506
+ _retriever_registry,
2507
+ _compactor_registry,
2508
+ _runtime_context,
2090
2509
  )
2091
2510
 
2092
2511
  return {
@@ -2097,9 +2516,16 @@ def create_dsl_stubs(
2097
2516
  "Agent": _new_agent, # NEW syntax - assignment based
2098
2517
  "Model": HybridModel(_model, _Model),
2099
2518
  "Procedure": _procedure,
2519
+ "Task": _task,
2520
+ "IncludeTasks": _include_tasks,
2100
2521
  "Prompt": _prompt,
2101
2522
  "Toolset": _toolset,
2102
2523
  "Tool": _new_tool, # NEW syntax - assignment based
2524
+ "Context": _new_context,
2525
+ "Corpus": _new_corpus,
2526
+ "Retriever": _new_retriever,
2527
+ "Compactor": _new_compactor,
2528
+ "TaskFunction": _task_function,
2103
2529
  "Classify": _new_classify, # NEW stdlib: smart classification with retry
2104
2530
  "Hitl": _hitl,
2105
2531
  "Specification": _specification,
@@ -2116,9 +2542,11 @@ def create_dsl_stubs(
2116
2542
  "get_current_lm": _get_current_lm,
2117
2543
  "Signature": _signature,
2118
2544
  "Module": _module,
2119
- "History": _history,
2545
+ "History": _dspy_history,
2120
2546
  "Message": _message,
2121
2547
  "DSPyAgent": _dspy_agent,
2548
+ "_tactus_internal_corpus": _new_corpus,
2549
+ "_tactus_internal_retriever": _new_retriever,
2122
2550
  # Script mode (top-level declarations)
2123
2551
  "input": _input,
2124
2552
  "output": _output,
@@ -2131,6 +2559,13 @@ def create_dsl_stubs(
2131
2559
  "async": _async,
2132
2560
  "max_depth": _max_depth,
2133
2561
  "max_turns": _max_turns,
2562
+ # Context message directives
2563
+ "system": _system,
2564
+ "user": _user,
2565
+ "assistant": _assistant,
2566
+ "context": _context_insert,
2567
+ "history": _context_history,
2568
+ "template": _template,
2134
2569
  # Built-in filters (exposed as a table)
2135
2570
  "filters": {
2136
2571
  "last_n": _last_n,
@@ -2155,6 +2590,10 @@ def create_dsl_stubs(
2155
2590
  "agent": _agent_registry,
2156
2591
  "tool": _tool_registry,
2157
2592
  "model": _model_registry,
2593
+ "context": _context_registry,
2594
+ "corpus": _corpus_registry,
2595
+ "retriever": _retriever_registry,
2596
+ "compactor": _compactor_registry,
2158
2597
  },
2159
2598
  # Assignment interception callback
2160
2599
  "_tactus_register_binding": binding_callback,
@@ -2162,7 +2601,14 @@ def create_dsl_stubs(
2162
2601
 
2163
2602
 
2164
2603
  def _make_binding_callback(
2165
- builder: RegistryBuilder, tool_registry: dict, agent_registry: dict, runtime_context: dict
2604
+ builder: RegistryBuilder,
2605
+ tool_registry: dict,
2606
+ agent_registry: dict,
2607
+ context_registry: dict,
2608
+ corpus_registry: dict,
2609
+ retriever_registry: dict,
2610
+ compactor_registry: dict,
2611
+ runtime_context: dict,
2166
2612
  ):
2167
2613
  """
2168
2614
  Factory to create the binding callback with closure over builder/registries/runtime_context.
@@ -2170,6 +2616,12 @@ def _make_binding_callback(
2170
2616
  This callback is called by Lua's __newindex metatable when assignments happen.
2171
2617
  """
2172
2618
  import logging
2619
+ from tactus.primitives.handles import (
2620
+ CompactorHandle,
2621
+ ContextHandle,
2622
+ CorpusHandle,
2623
+ RetrieverHandle,
2624
+ )
2173
2625
  from tactus.primitives.tool_handle import ToolHandle
2174
2626
 
2175
2627
  callback_logger = logging.getLogger(__name__)
@@ -2254,6 +2706,105 @@ def _make_binding_callback(
2254
2706
  f"[AGENT_RENAME] Updated _created_agents dict: '{old_name}' -> '{name}'"
2255
2707
  )
2256
2708
 
2709
+ if isinstance(value, ContextHandle):
2710
+ old_name = value.name
2711
+ if old_name.startswith("_temp_context_"):
2712
+ callback_logger.debug(f"Renaming context '{old_name}' to '{name}'")
2713
+ value.name = name
2714
+ if old_name in context_registry:
2715
+ del context_registry[old_name]
2716
+ context_registry[name] = value
2717
+ if hasattr(builder, "registry") and old_name in builder.registry.contexts:
2718
+ context_data = builder.registry.contexts.pop(old_name)
2719
+ builder.registry.contexts[name] = context_data
2720
+ elif old_name != name:
2721
+ raise RuntimeError(
2722
+ f"Context name mismatch: assigned to '{name}' but context is named '{old_name}'. "
2723
+ "Remove the Context config 'name' field or make it match the assigned variable."
2724
+ )
2725
+
2726
+ if isinstance(value, CorpusHandle):
2727
+ old_name = value.name
2728
+ if old_name.startswith("_temp_corpus_"):
2729
+ callback_logger.debug(f"Renaming corpus '{old_name}' to '{name}'")
2730
+ value.name = name
2731
+ if old_name in corpus_registry:
2732
+ del corpus_registry[old_name]
2733
+ corpus_registry[name] = value
2734
+ if hasattr(builder, "registry") and old_name in builder.registry.corpora:
2735
+ corpus_data = builder.registry.corpora.pop(old_name)
2736
+ builder.registry.corpora[name] = corpus_data
2737
+ elif old_name != name:
2738
+ raise RuntimeError(
2739
+ f"Corpus name mismatch: assigned to '{name}' but corpus is named '{old_name}'. "
2740
+ "Remove the Corpus config 'name' field or make it match the assigned variable."
2741
+ )
2742
+
2743
+ if isinstance(value, RetrieverHandle):
2744
+ old_name = value.name
2745
+ if old_name.startswith("_temp_retriever_"):
2746
+ callback_logger.debug(f"Renaming retriever '{old_name}' to '{name}'")
2747
+ value.name = name
2748
+ if old_name in retriever_registry:
2749
+ del retriever_registry[old_name]
2750
+ retriever_registry[name] = value
2751
+ if hasattr(builder, "registry") and old_name in builder.registry.retrievers:
2752
+ retriever_data = builder.registry.retrievers.pop(old_name)
2753
+ builder.registry.retrievers[name] = retriever_data
2754
+ elif old_name != name:
2755
+ raise RuntimeError(
2756
+ f"Retriever name mismatch: assigned to '{name}' but retriever is named '{old_name}'. "
2757
+ "Remove the Retriever config 'name' field or make it match the assigned variable."
2758
+ )
2759
+
2760
+ if isinstance(value, CompactorHandle):
2761
+ old_name = value.name
2762
+ if old_name.startswith("_temp_compactor_"):
2763
+ callback_logger.debug(f"Renaming compactor '{old_name}' to '{name}'")
2764
+ value.name = name
2765
+ if old_name in compactor_registry:
2766
+ del compactor_registry[old_name]
2767
+ compactor_registry[name] = value
2768
+ if hasattr(builder, "registry") and old_name in builder.registry.compactors:
2769
+ compactor_data = builder.registry.compactors.pop(old_name)
2770
+ builder.registry.compactors[name] = compactor_data
2771
+ elif old_name != name:
2772
+ raise RuntimeError(
2773
+ f"Compactor name mismatch: assigned to '{name}' but compactor is named '{old_name}'. "
2774
+ "Remove the Compactor config 'name' field or make it match the assigned variable."
2775
+ )
2776
+
2777
+ if hasattr(value, "items"):
2778
+ try:
2779
+ task_config = lua_table_to_dict(value)
2780
+ except Exception:
2781
+ task_config = None
2782
+ if isinstance(task_config, dict) and task_config.get("__tactus_task_config"):
2783
+ if hasattr(builder, "registry") and name not in builder.registry.tasks:
2784
+ if "entry" in task_config and not callable(task_config["entry"]):
2785
+ raise TypeError(f"Task '{name}' entry must be a function")
2786
+ builder.register_task(name, task_config)
2787
+
2788
+ child_sources = task_config.get("__tactus_child_tasks")
2789
+ if isinstance(child_sources, dict):
2790
+ child_iter = child_sources.items()
2791
+ else:
2792
+ child_iter = value.items()
2793
+
2794
+ for key, child_value in child_iter:
2795
+ child_name = key if isinstance(key, str) else None
2796
+ if not hasattr(child_value, "items"):
2797
+ continue
2798
+ child_config = lua_table_to_dict(child_value)
2799
+ if not child_name and isinstance(child_config, dict):
2800
+ child_name = child_config.get("__task_name")
2801
+ if child_name and isinstance(child_config, dict):
2802
+ if "entry" in child_config and not callable(child_config["entry"]):
2803
+ raise TypeError(
2804
+ f"Task '{name}:{child_name}' entry must be a function"
2805
+ )
2806
+ builder.register_task(child_name, child_config, parent=name)
2807
+
2257
2808
  # Log all assignments for debugging (only at trace level to avoid noise)
2258
2809
  callback_logger.debug(f"Assignment captured: {name} = {type(value).__name__}")
2259
2810