tactus 0.25.0__py3-none-any.whl → 0.26.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.
tactus/__init__.py CHANGED
@@ -5,7 +5,7 @@ Tactus provides a declarative workflow engine for AI agents with pluggable
5
5
  backends for storage, HITL, and chat recording.
6
6
  """
7
7
 
8
- __version__ = "0.25.0"
8
+ __version__ = "0.26.0"
9
9
 
10
10
  # Core exports
11
11
  from tactus.core.runtime import TactusRuntime
@@ -32,7 +32,7 @@ class CLIHITLHandler:
32
32
  console: Rich Console instance (creates new one if not provided)
33
33
  """
34
34
  self.console = console or Console()
35
- logger.info("CLIHITLHandler initialized")
35
+ logger.debug("CLIHITLHandler initialized")
36
36
 
37
37
  def request_interaction(self, procedure_id: str, request: HITLRequest) -> HITLResponse:
38
38
  """
@@ -45,7 +45,7 @@ class CLIHITLHandler:
45
45
  Returns:
46
46
  HITLResponse with user's response
47
47
  """
48
- logger.info(f"HITL request: {request.request_type} - {request.message}")
48
+ logger.debug(f"HITL request: {request.request_type} - {request.message}")
49
49
 
50
50
  # Display the request in a panel
51
51
  self.console.print()
@@ -0,0 +1,56 @@
1
+ """
2
+ Cost-only log handler for headless/sandbox runs.
3
+
4
+ Collects CostEvent instances so the runtime can report total_cost/total_tokens,
5
+ without enabling streaming UI behavior.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import json
12
+ from typing import List
13
+
14
+ from tactus.protocols.models import CostEvent, LogEvent
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CostCollectorLogHandler:
20
+ """
21
+ Minimal LogHandler for sandbox runs.
22
+
23
+ This is useful in environments like Docker sandboxes where stdout is reserved
24
+ for protocol output, but we still want:
25
+ - accurate cost accounting (CostEvent)
26
+ - basic procedure logging (LogEvent) via stderr/Python logging
27
+ """
28
+
29
+ supports_streaming = False
30
+
31
+ def __init__(self):
32
+ self.cost_events: List[CostEvent] = []
33
+ logger.debug("CostCollectorLogHandler initialized")
34
+
35
+ def log(self, event: LogEvent) -> None:
36
+ if isinstance(event, CostEvent):
37
+ self.cost_events.append(event)
38
+ return
39
+
40
+ # Preserve useful procedure logs even when no IDE callback is present.
41
+ if isinstance(event, LogEvent):
42
+ event_logger = logging.getLogger(event.logger_name or "procedure")
43
+
44
+ msg = event.message
45
+ if event.context:
46
+ msg = f"{msg}\nContext: {json.dumps(event.context, indent=2, default=str)}"
47
+
48
+ level = (event.level or "INFO").upper()
49
+ if level == "DEBUG":
50
+ event_logger.debug(msg)
51
+ elif level in ("WARN", "WARNING"):
52
+ event_logger.warning(msg)
53
+ elif level == "ERROR":
54
+ event_logger.error(msg)
55
+ else:
56
+ event_logger.info(msg)
tactus/cli/app.py CHANGED
@@ -134,15 +134,92 @@ def load_tactus_config():
134
134
  return {}
135
135
 
136
136
 
137
- def setup_logging(verbose: bool = False):
138
- """Setup logging with rich handler."""
139
- level = logging.DEBUG if verbose else logging.INFO
140
-
141
- logging.basicConfig(
142
- level=level,
143
- format="%(message)s",
144
- handlers=[RichHandler(console=console, show_path=False, rich_tracebacks=True)],
145
- )
137
+ _LOG_LEVELS = {
138
+ "debug": logging.DEBUG,
139
+ "info": logging.INFO,
140
+ "warning": logging.WARNING,
141
+ "warn": logging.WARNING,
142
+ "error": logging.ERROR,
143
+ "critical": logging.CRITICAL,
144
+ }
145
+
146
+ _LOG_FORMATS = {"rich", "terminal", "raw"}
147
+
148
+
149
+ class _TerminalLogHandler(logging.Handler):
150
+ """Minimal, high-signal terminal logger (no timestamps/levels)."""
151
+
152
+ def __init__(self, console: Console):
153
+ super().__init__()
154
+ self._console = console
155
+ self.setFormatter(logging.Formatter("%(message)s"))
156
+
157
+ def emit(self, record: logging.LogRecord) -> None:
158
+ try:
159
+ message = self.format(record)
160
+
161
+ # Make procedure-level logs the most prominent.
162
+ if record.name.startswith("procedure"):
163
+ style = "bold"
164
+ elif record.levelno >= logging.ERROR:
165
+ style = "bold red"
166
+ elif record.levelno >= logging.WARNING:
167
+ style = "yellow"
168
+ elif record.levelno <= logging.DEBUG:
169
+ style = "dim"
170
+ else:
171
+ style = ""
172
+
173
+ self._console.print(message, style=style, markup=False, highlight=False)
174
+ except Exception:
175
+ self.handleError(record)
176
+
177
+
178
+ def setup_logging(
179
+ verbose: bool = False,
180
+ log_level: Optional[str] = None,
181
+ log_format: str = "rich",
182
+ ) -> None:
183
+ """Setup CLI logging (level + format)."""
184
+ if log_level is None:
185
+ level = logging.DEBUG if verbose else logging.INFO
186
+ else:
187
+ key = str(log_level).strip().lower()
188
+ if key not in _LOG_LEVELS:
189
+ raise typer.BadParameter(
190
+ f"Invalid --log-level '{log_level}'. "
191
+ f"Use one of: {', '.join(sorted(_LOG_LEVELS.keys()))}"
192
+ )
193
+ level = _LOG_LEVELS[key]
194
+
195
+ fmt = (log_format or "rich").strip().lower()
196
+ if fmt not in _LOG_FORMATS:
197
+ raise typer.BadParameter(
198
+ f"Invalid --log-format '{log_format}'. Use one of: {', '.join(sorted(_LOG_FORMATS))}"
199
+ )
200
+
201
+ # Default: rich logs (group repeated timestamps).
202
+ if fmt == "rich":
203
+ handler: logging.Handler = RichHandler(
204
+ console=console,
205
+ show_path=False,
206
+ rich_tracebacks=True,
207
+ omit_repeated_times=True,
208
+ )
209
+ handler.setFormatter(logging.Formatter("%(message)s"))
210
+ logging.basicConfig(level=level, format="%(message)s", handlers=[handler], force=True)
211
+ return
212
+
213
+ # Raw logs: one line per entry, CloudWatch-friendly.
214
+ if fmt == "raw":
215
+ handler = logging.StreamHandler(stream=sys.stderr)
216
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
217
+ logging.basicConfig(level=level, handlers=[handler], force=True)
218
+ return
219
+
220
+ # Terminal logs: no timestamps/levels, color by signal.
221
+ handler = _TerminalLogHandler(console)
222
+ logging.basicConfig(level=level, handlers=[handler], force=True)
146
223
 
147
224
 
148
225
  def _parse_value(value_str: str, field_type: str) -> Any:
@@ -352,6 +429,12 @@ def run(
352
429
  None, envvar="OPENAI_API_KEY", help="OpenAI API key"
353
430
  ),
354
431
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
432
+ log_level: Optional[str] = typer.Option(
433
+ None, "--log-level", help="Log level: debug, info, warning, error, critical"
434
+ ),
435
+ log_format: str = typer.Option(
436
+ "rich", "--log-format", help="Log format: rich (default), terminal, raw"
437
+ ),
355
438
  param: Optional[list[str]] = typer.Option(None, help="Parameters in format key=value"),
356
439
  interactive: bool = typer.Option(
357
440
  False, "--interactive", "-i", help="Interactively prompt for all inputs"
@@ -399,7 +482,7 @@ def run(
399
482
  # Use real implementation for specific tools while mocking others
400
483
  tactus run workflow.tac --mock-all --real done
401
484
  """
402
- setup_logging(verbose)
485
+ setup_logging(verbose=verbose, log_level=log_level, log_format=log_format)
403
486
 
404
487
  # Check if file exists
405
488
  if not workflow_file.exists():
@@ -530,6 +613,12 @@ def run(
530
613
  sandbox_config_dict["enabled"] = sandbox
531
614
  sandbox_config = SandboxConfig(**sandbox_config_dict)
532
615
 
616
+ # Pass logging preferences through to the sandbox container so container stderr matches CLI UX.
617
+ sandbox_config.env.setdefault(
618
+ "TACTUS_LOG_LEVEL", str(log_level or ("debug" if verbose else "info"))
619
+ )
620
+ sandbox_config.env.setdefault("TACTUS_LOG_FORMAT", str(log_format))
621
+
533
622
  # Check Docker availability
534
623
  docker_available, docker_reason = is_docker_available()
535
624
 
@@ -150,7 +150,7 @@ class ConfigManager:
150
150
  sidecar_config = self._load_yaml_file(sidecar_path)
151
151
  if sidecar_config:
152
152
  configs.append(("sidecar", sidecar_config))
153
- logger.info(f"Loaded sidecar config: {sidecar_path}")
153
+ logger.debug(f"Loaded sidecar config: {sidecar_path}")
154
154
 
155
155
  # Store for debugging
156
156
  self.loaded_configs = configs
@@ -158,7 +158,7 @@ class ConfigManager:
158
158
  # Merge all configs (later configs override earlier ones)
159
159
  merged = self._merge_configs([c[1] for c in configs])
160
160
 
161
- logger.info(f"Merged configuration from {len(configs)} source(s)")
161
+ logger.debug(f"Merged configuration from {len(configs)} source(s)")
162
162
  return merged
163
163
 
164
164
  def _find_sidecar_config(self, tac_path: Path) -> Optional[Path]:
tactus/core/dsl_stubs.py CHANGED
@@ -915,43 +915,49 @@ def create_dsl_stubs(
915
915
  if not isinstance(mock_config, dict):
916
916
  continue
917
917
 
918
- # Check if this is an agent mock (has tool_calls key)
919
- if "tool_calls" in mock_config:
920
- # Agent mock - specifies what tool calls the agent should simulate
918
+ # Tool mocks use explicit keys.
919
+ tool_mock_keys = {"returns", "temporal", "conditional", "error"}
920
+ if any(k in mock_config for k in tool_mock_keys):
921
+ # Convert DSL syntax to MockConfig format
922
+ processed_config = {}
923
+
924
+ # Static mocking with 'returns' key
925
+ if "returns" in mock_config:
926
+ processed_config["output"] = mock_config["returns"]
927
+
928
+ # Temporal mocking
929
+ elif "temporal" in mock_config:
930
+ processed_config["temporal"] = mock_config["temporal"]
931
+
932
+ # Conditional mocking
933
+ elif "conditional" in mock_config:
934
+ # Convert DSL conditional format to MockManager format
935
+ conditionals = []
936
+ for cond in mock_config["conditional"]:
937
+ if isinstance(cond, dict) and "when" in cond and "returns" in cond:
938
+ conditionals.append({"when": cond["when"], "return": cond["returns"]})
939
+ processed_config["conditional_mocks"] = conditionals
940
+
941
+ # Error simulation
942
+ elif "error" in mock_config:
943
+ processed_config["error"] = mock_config["error"]
944
+
945
+ # Register the tool mock configuration
946
+ builder.register_mock(name, processed_config)
947
+ continue
948
+
949
+ # Agent mocks can be message-only, tool_calls-only, or both.
950
+ if any(k in mock_config for k in ("tool_calls", "message", "data")):
921
951
  agent_config = {
922
952
  "tool_calls": mock_config.get("tool_calls", []),
923
953
  "message": mock_config.get("message", ""),
954
+ "data": mock_config.get("data"),
924
955
  }
925
956
  builder.register_agent_mock(name, agent_config)
926
957
  continue
927
958
 
928
- # Otherwise, it's a tool mock
929
- # Convert DSL syntax to MockConfig format
930
- processed_config = {}
931
-
932
- # Static mocking with 'returns' key
933
- if "returns" in mock_config:
934
- processed_config["output"] = mock_config["returns"]
935
-
936
- # Temporal mocking
937
- elif "temporal" in mock_config:
938
- processed_config["temporal"] = mock_config["temporal"]
939
-
940
- # Conditional mocking
941
- elif "conditional" in mock_config:
942
- # Convert DSL conditional format to MockManager format
943
- conditionals = []
944
- for cond in mock_config["conditional"]:
945
- if isinstance(cond, dict) and "when" in cond and "returns" in cond:
946
- conditionals.append({"when": cond["when"], "return": cond["returns"]})
947
- processed_config["conditional_mocks"] = conditionals
948
-
949
- # Error simulation
950
- elif "error" in mock_config:
951
- processed_config["error"] = mock_config["error"]
952
-
953
- # Register the tool mock configuration
954
- builder.register_mock(name, processed_config)
959
+ # Otherwise, ignore unknown mock config.
960
+ continue
955
961
 
956
962
  def _history(messages=None):
957
963
  """
@@ -1413,14 +1419,14 @@ def create_dsl_stubs(
1413
1419
 
1414
1420
  logger = logging.getLogger(__name__)
1415
1421
 
1416
- logger.info(
1422
+ logger.debug(
1417
1423
  f"[AGENT_CREATION] Agent '{agent_name}': runtime_context={bool(_runtime_context)}, skip_agents={_runtime_context.get('skip_agents', 'N/A') if _runtime_context else 'N/A'}, has_log_handler={('log_handler' in _runtime_context) if _runtime_context else False}"
1418
1424
  )
1419
1425
 
1420
1426
  if _runtime_context and not _runtime_context.get("skip_agents", False):
1421
1427
  from tactus.dspy.agent import create_dspy_agent
1422
1428
 
1423
- logger.info(f"[AGENT_CREATION] Attempting immediate creation for agent '{agent_name}'")
1429
+ logger.debug(f"[AGENT_CREATION] Attempting immediate creation for agent '{agent_name}'")
1424
1430
 
1425
1431
  try:
1426
1432
  # Create the actual agent primitive NOW
@@ -1450,7 +1456,7 @@ def create_dsl_stubs(
1450
1456
  handle._set_primitive(
1451
1457
  agent_primitive, execution_context=_runtime_context.get("execution_context")
1452
1458
  )
1453
- logger.info(
1459
+ logger.debug(
1454
1460
  f"[AGENT_CREATION] Agent '{agent_name}' created immediately during declaration, has_log_handler={hasattr(agent_primitive, 'log_handler') and agent_primitive.log_handler is not None}"
1455
1461
  )
1456
1462
 
@@ -1458,7 +1464,9 @@ def create_dsl_stubs(
1458
1464
  if "_created_agents" not in _runtime_context:
1459
1465
  _runtime_context["_created_agents"] = {}
1460
1466
  _runtime_context["_created_agents"][agent_name] = agent_primitive
1461
- logger.info(f"[AGENT_CREATION] Stored agent '{agent_name}' in _created_agents dict")
1467
+ logger.debug(
1468
+ f"[AGENT_CREATION] Stored agent '{agent_name}' in _created_agents dict"
1469
+ )
1462
1470
 
1463
1471
  except Exception as e:
1464
1472
  logger.error(
@@ -1567,14 +1575,14 @@ def create_dsl_stubs(
1567
1575
 
1568
1576
  logger = logging.getLogger(__name__)
1569
1577
 
1570
- logger.info(
1578
+ logger.debug(
1571
1579
  f"[AGENT_CREATION] Agent '{temp_name}': runtime_context={bool(_runtime_context)}, skip_agents={_runtime_context.get('skip_agents', 'N/A') if _runtime_context else 'N/A'}, has_log_handler={('log_handler' in _runtime_context) if _runtime_context else False}"
1572
1580
  )
1573
1581
 
1574
1582
  if _runtime_context and not _runtime_context.get("skip_agents", False):
1575
1583
  from tactus.dspy.agent import create_dspy_agent
1576
1584
 
1577
- logger.info(f"[AGENT_CREATION] Attempting immediate creation for agent '{temp_name}'")
1585
+ logger.debug(f"[AGENT_CREATION] Attempting immediate creation for agent '{temp_name}'")
1578
1586
 
1579
1587
  try:
1580
1588
  # Create the actual agent primitive NOW
@@ -1593,7 +1601,7 @@ def create_dsl_stubs(
1593
1601
  if "log_handler" in _runtime_context:
1594
1602
  agent_config["log_handler"] = _runtime_context["log_handler"]
1595
1603
 
1596
- logger.info(
1604
+ logger.debug(
1597
1605
  f"[AGENT_CREATION] Creating agent immediately: name={temp_name}, has_log_handler={'log_handler' in agent_config}"
1598
1606
  )
1599
1607
  agent_primitive = create_dspy_agent(
@@ -1607,7 +1615,7 @@ def create_dsl_stubs(
1607
1615
  handle._set_primitive(
1608
1616
  agent_primitive, execution_context=_runtime_context.get("execution_context")
1609
1617
  )
1610
- logger.info(
1618
+ logger.debug(
1611
1619
  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}"
1612
1620
  )
1613
1621
 
@@ -1615,7 +1623,7 @@ def create_dsl_stubs(
1615
1623
  if "_created_agents" not in _runtime_context:
1616
1624
  _runtime_context["_created_agents"] = {}
1617
1625
  _runtime_context["_created_agents"][temp_name] = agent_primitive
1618
- logger.info(f"[AGENT_CREATION] Stored agent '{temp_name}' in _created_agents dict")
1626
+ logger.debug(f"[AGENT_CREATION] Stored agent '{temp_name}' in _created_agents dict")
1619
1627
 
1620
1628
  except Exception as e:
1621
1629
  import traceback
@@ -1757,13 +1765,13 @@ def _make_binding_callback(
1757
1765
  old_name = value.name
1758
1766
  if old_name.startswith("_temp_agent_"):
1759
1767
  # Rename the agent handle
1760
- callback_logger.info(f"[AGENT_RENAME] Renaming agent '{old_name}' to '{name}'")
1768
+ callback_logger.debug(f"[AGENT_RENAME] Renaming agent '{old_name}' to '{name}'")
1761
1769
  value.name = name
1762
1770
 
1763
1771
  # Also rename the underlying primitive if it exists
1764
1772
  if value._primitive is not None:
1765
1773
  value._primitive.name = name
1766
- callback_logger.info(
1774
+ callback_logger.debug(
1767
1775
  f"[AGENT_RENAME] Updated primitive name: '{old_name}' -> '{name}'"
1768
1776
  )
1769
1777
 
@@ -1777,7 +1785,7 @@ def _make_binding_callback(
1777
1785
  if hasattr(builder, "registry") and old_name in builder.registry.agents:
1778
1786
  agent_data = builder.registry.agents.pop(old_name)
1779
1787
  builder.registry.agents[name] = agent_data
1780
- callback_logger.info(
1788
+ callback_logger.debug(
1781
1789
  f"[AGENT_RENAME] Re-registered agent '{name}' in builder.registry.agents"
1782
1790
  )
1783
1791
 
@@ -1786,7 +1794,7 @@ def _make_binding_callback(
1786
1794
  if old_name in runtime_context["_created_agents"]:
1787
1795
  agent_primitive = runtime_context["_created_agents"].pop(old_name)
1788
1796
  runtime_context["_created_agents"][name] = agent_primitive
1789
- callback_logger.info(
1797
+ callback_logger.debug(
1790
1798
  f"[AGENT_RENAME] Updated _created_agents dict: '{old_name}' -> '{name}'"
1791
1799
  )
1792
1800
 
@@ -187,7 +187,7 @@ class BaseExecutionContext(ExecutionContext):
187
187
  On replay, returns cached result from execution log.
188
188
  On first execution, runs fn(), records in log, and returns result.
189
189
  """
190
- logger.info(
190
+ logger.debug(
191
191
  f"[CHECKPOINT] checkpoint() called, type={checkpoint_type}, has_log_handler={self.log_handler is not None}"
192
192
  )
193
193
  current_position = self.metadata.replay_index
@@ -259,7 +259,7 @@ class BaseExecutionContext(ExecutionContext):
259
259
  source_location=source_location,
260
260
  procedure_id=self.procedure_id,
261
261
  )
262
- logger.info(
262
+ logger.debug(
263
263
  f"[CHECKPOINT] Emitting CheckpointCreatedEvent: position={current_position}, type={checkpoint_type}, duration_ms={duration_ms}"
264
264
  )
265
265
  self.log_handler.log(event)
@@ -73,7 +73,7 @@ class LuaSandbox:
73
73
  # Setup safe globals
74
74
  self._setup_safe_globals()
75
75
 
76
- logger.info("Lua sandbox initialized successfully")
76
+ logger.debug("Lua sandbox initialized successfully")
77
77
 
78
78
  def _attribute_filter(self, obj, attr_name, is_setting):
79
79
  """
@@ -274,7 +274,7 @@ class LuaSandbox:
274
274
  self.lua.globals()["math"] = safe_math_table
275
275
  self.lua.globals()["os"] = safe_os_table
276
276
 
277
- logger.info("Installed safe math and os libraries with determinism checking")
277
+ logger.debug("Installed safe math and os libraries with determinism checking")
278
278
  return # Skip default os.date setup below
279
279
 
280
280
  # Add safe subset of os module (only date function for timestamps)
tactus/docker/Dockerfile CHANGED
@@ -36,6 +36,10 @@ COPY pyproject.toml ./
36
36
  COPY README.md ./
37
37
  COPY tactus/ ./tactus/
38
38
 
39
+ # Ensure the non-root runtime user can read the codebase even if the host
40
+ # working tree has restrictive permissions (e.g., umask 077).
41
+ RUN chmod -R a+rX /app/tactus
42
+
39
43
  # Install Tactus and its dependencies
40
44
  RUN pip install --no-cache-dir -e .
41
45
 
tactus/dspy/agent.py CHANGED
@@ -191,11 +191,19 @@ class DSPyAgentHandle:
191
191
  if response and hasattr(response, "_hidden_params"):
192
192
  total_cost = response._hidden_params.get("response_cost")
193
193
 
194
- if total_cost is None and response:
194
+ if total_cost is None and total_tokens > 0:
195
195
  try:
196
- import litellm
197
-
198
- total_cost = litellm.completion_cost(completion_response=response)
196
+ # We already have token counts, so compute cost from tokens to avoid relying
197
+ # on provider-specific response object shapes.
198
+ from litellm.cost_calculator import cost_per_token
199
+
200
+ prompt_cost, completion_cost = cost_per_token(
201
+ model=str(model) if model is not None else "",
202
+ prompt_tokens=prompt_tokens,
203
+ completion_tokens=completion_tokens,
204
+ call_type="completion",
205
+ )
206
+ total_cost = float(prompt_cost) + float(completion_cost)
199
207
  except Exception as e:
200
208
  logger.warning(f"[COST] Agent '{self.name}': failed to calculate cost: {e}")
201
209
  total_cost = 0.0
@@ -326,6 +334,7 @@ class DSPyAgentHandle:
326
334
 
327
335
  Streaming is enabled when:
328
336
  - log_handler is available (for emitting events)
337
+ - log_handler supports streaming events
329
338
  - disable_streaming is False
330
339
  - No structured output schema (streaming only works with plain text)
331
340
 
@@ -337,6 +346,14 @@ class DSPyAgentHandle:
337
346
  logger.debug(f"[STREAMING] Agent '{self.name}': no log_handler, streaming disabled")
338
347
  return False
339
348
 
349
+ # Allow log handlers to opt out of streaming (e.g., cost-only collectors)
350
+ supports_streaming = getattr(self.log_handler, "supports_streaming", True)
351
+ if not supports_streaming:
352
+ logger.debug(
353
+ f"[STREAMING] Agent '{self.name}': log_handler supports_streaming=False, streaming disabled"
354
+ )
355
+ return False
356
+
340
357
  # Respect explicit disable flag
341
358
  if self.disable_streaming:
342
359
  logger.debug(
@@ -673,9 +690,7 @@ class DSPyAgentHandle:
673
690
  result = worker({message = "Process this task"})
674
691
  print(result.response)
675
692
  """
676
- logger.info(
677
- f"[CHECKPOINT] DSPyAgentHandle.__call__ invoked directly for agent '{self.name}' - THIS BYPASSES AgentHandle checkpoint logic!"
678
- )
693
+ logger.debug(f"Agent '{self.name}' invoked via __call__()")
679
694
  inputs = inputs or {}
680
695
 
681
696
  # Handle string input as a convenience: Agent("message") -> Agent({message="message"})
@@ -108,9 +108,9 @@ class AgentHandle:
108
108
  result = worker({message = "Process this task"})
109
109
  print(result.response)
110
110
  """
111
- logger.info(
112
- f"[CHECKPOINT] AgentHandle '{self.name}'.__call__ invoked, _primitive={self._primitive is not None}, _execution_context={self._execution_context is not None}"
113
- )
111
+ logger.debug(
112
+ f"[CHECKPOINT] AgentHandle '{self.name}'.__call__ invoked, _primitive={self._primitive is not None}, _execution_context={self._execution_context is not None}"
113
+ )
114
114
  if self._primitive is None:
115
115
  raise RuntimeError(
116
116
  f"Agent '{self.name}' initialization failed.\n"
@@ -121,9 +121,9 @@ class AgentHandle:
121
121
  converted_inputs = _convert_lua_table(inputs) if inputs is not None else None
122
122
 
123
123
  # If we have an execution context, checkpoint the agent call
124
- logger.info(
125
- f"[CHECKPOINT] AgentHandle '{self.name}' called, has_execution_context={self._execution_context is not None}"
126
- )
124
+ logger.debug(
125
+ f"[CHECKPOINT] AgentHandle '{self.name}' called, has_execution_context={self._execution_context is not None}"
126
+ )
127
127
  if self._execution_context is not None:
128
128
 
129
129
  def agent_call():
@@ -146,9 +146,9 @@ class AgentHandle:
146
146
  except Exception as e:
147
147
  logger.debug(f"Could not capture source location: {e}")
148
148
 
149
- logger.info(
150
- f"[CHECKPOINT] Creating checkpoint for agent '{self.name}', type=agent_turn, source_info={source_info}"
151
- )
149
+ logger.debug(
150
+ f"[CHECKPOINT] Creating checkpoint for agent '{self.name}', type=agent_turn, source_info={source_info}"
151
+ )
152
152
  return self._execution_context.checkpoint(
153
153
  agent_call, checkpoint_type="agent_turn", source_info=source_info
154
154
  )
@@ -170,9 +170,9 @@ class AgentHandle:
170
170
  """
171
171
  self._primitive = primitive
172
172
  self._execution_context = execution_context
173
- logger.info(
174
- f"[CHECKPOINT] AgentHandle '{self.name}' connected to primitive (checkpointing={'enabled' if execution_context else 'disabled'}, execution_context={execution_context})"
175
- )
173
+ logger.debug(
174
+ f"[CHECKPOINT] AgentHandle '{self.name}' connected to primitive (checkpointing={'enabled' if execution_context else 'disabled'}, execution_context={execution_context})"
175
+ )
176
176
 
177
177
  def __repr__(self) -> str:
178
178
  connected = "connected" if self._primitive else "disconnected"
@@ -8,7 +8,9 @@ and collecting results via stdio communication.
8
8
  import asyncio
9
9
  import logging
10
10
  import os
11
+ import re
11
12
  import shutil
13
+ import sys
12
14
  import tempfile
13
15
  import time
14
16
  import uuid
@@ -25,6 +27,22 @@ from .protocol import (
25
27
 
26
28
  logger = logging.getLogger(__name__)
27
29
 
30
+ _CONTAINER_LOG_RE = re.compile(
31
+ r"^(?P<asctime>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) "
32
+ r"\[(?P<level>[A-Z]+)\] "
33
+ r"(?P<logger>[^:]+): "
34
+ r"(?P<message>.*)$"
35
+ )
36
+
37
+ _LEVEL_MAP = {
38
+ "DEBUG": logging.DEBUG,
39
+ "INFO": logging.INFO,
40
+ "WARNING": logging.WARNING,
41
+ "WARN": logging.WARNING,
42
+ "ERROR": logging.ERROR,
43
+ "CRITICAL": logging.CRITICAL,
44
+ }
45
+
28
46
 
29
47
  class SandboxError(Exception):
30
48
  """Raised when sandbox execution fails."""
@@ -158,6 +176,7 @@ class ContainerRunner:
158
176
  extra_env: Optional[Dict[str, str]] = None,
159
177
  execution_id: Optional[str] = None,
160
178
  callback_url: Optional[str] = None,
179
+ volume_base_dir: Optional[Path] = None,
161
180
  ) -> List[str]:
162
181
  """
163
182
  Build the docker run command.
@@ -218,7 +237,7 @@ class ContainerRunner:
218
237
 
219
238
  # Additional user-configured volumes
220
239
  for volume in self.config.volumes:
221
- cmd.extend(["-v", volume])
240
+ cmd.extend(["-v", self._normalize_volume_spec(volume, base_dir=volume_base_dir)])
222
241
 
223
242
  # Pass through environment variables
224
243
  for var_name in self.PASSTHROUGH_ENV_VARS:
@@ -247,6 +266,42 @@ class ContainerRunner:
247
266
 
248
267
  return cmd
249
268
 
269
+ def _normalize_volume_spec(self, volume: str, base_dir: Optional[Path]) -> str:
270
+ """
271
+ Normalize a docker volume spec.
272
+
273
+ Docker only accepts absolute host paths for bind mounts. For convenience,
274
+ allow sidecar configs to use relative paths and normalize them here.
275
+
276
+ Expected formats:
277
+ - /abs/host:/container[:mode]
278
+ - ./rel/host:/container[:mode]
279
+ - ../rel/host:/container[:mode]
280
+ - volume_name:/container[:mode] (left unchanged)
281
+ """
282
+ # Basic split: host:container[:mode]
283
+ parts = volume.split(":")
284
+ if len(parts) < 2:
285
+ return volume
286
+
287
+ host = parts[0]
288
+ container = parts[1]
289
+ mode = parts[2] if len(parts) > 2 else None
290
+
291
+ host_is_path = host.startswith(("/", "./", "../", "~"))
292
+ if not host_is_path:
293
+ # Named volume (or other special form) - leave unchanged
294
+ return volume
295
+
296
+ host_path = Path(host).expanduser()
297
+ if not host_path.is_absolute():
298
+ host_path = (base_dir or Path.cwd()) / host_path
299
+ host_path = host_path.resolve()
300
+
301
+ if mode:
302
+ return f"{host_path}:{container}:{mode}"
303
+ return f"{host_path}:{container}"
304
+
250
305
  async def run(
251
306
  self,
252
307
  source: str,
@@ -302,12 +357,22 @@ class ContainerRunner:
302
357
  # Get MCP servers path
303
358
  mcp_path = self.config.get_mcp_servers_path()
304
359
 
360
+ # Resolve relative bind-mount paths in sandbox.volumes relative to the procedure file
361
+ # when available (makes sidecar configs portable).
362
+ volume_base_dir = None
363
+ if source_file_path:
364
+ try:
365
+ volume_base_dir = Path(source_file_path).resolve().parent
366
+ except Exception:
367
+ volume_base_dir = None
368
+
305
369
  # Build docker command
306
370
  docker_cmd = self._build_docker_command(
307
371
  working_dir=working_dir,
308
372
  mcp_servers_path=mcp_path if mcp_path.exists() else None,
309
373
  execution_id=execution_id,
310
374
  callback_url=callback_url,
375
+ volume_base_dir=volume_base_dir,
311
376
  )
312
377
 
313
378
  logger.debug(f"Docker command: {' '.join(docker_cmd)}")
@@ -389,10 +454,7 @@ class ContainerRunner:
389
454
  stdout = stdout_data.decode("utf-8", errors="replace")
390
455
  stderr = stderr_data.decode("utf-8", errors="replace")
391
456
 
392
- # Log stderr (container logs)
393
- if stderr:
394
- for line in stderr.strip().split("\n"):
395
- logger.info(f"[container] {line}")
457
+ self._handle_container_stderr(stderr)
396
458
 
397
459
  # Extract result from stdout
398
460
  result = extract_result_from_stdout(stdout)
@@ -437,6 +499,61 @@ class ContainerRunner:
437
499
  pass
438
500
  raise
439
501
 
502
+ def _handle_container_stderr(self, stderr: str) -> None:
503
+ """
504
+ Forward container stderr into the host log UX.
505
+
506
+ - raw: pass through container stderr as-is (CloudWatch-friendly)
507
+ - rich/terminal: parse container log lines and re-emit with host formatting
508
+ """
509
+ if not stderr:
510
+ return
511
+
512
+ fmt = str(self.config.env.get("TACTUS_LOG_FORMAT", "rich")).strip().lower()
513
+
514
+ # Raw mode: avoid double timestamps by forwarding container stderr directly.
515
+ if fmt == "raw":
516
+ sys.stderr.write(stderr)
517
+ sys.stderr.flush()
518
+ return
519
+
520
+ # Rich/terminal: parse our container log format and re-emit.
521
+ current: tuple[str, int, list[str]] | None = None # (logger_name, levelno, lines)
522
+
523
+ def flush_current() -> None:
524
+ nonlocal current
525
+ if current is None:
526
+ return
527
+ logger_name, levelno, lines = current
528
+ message = "\n".join(lines).rstrip("\n")
529
+ logging.getLogger(logger_name).log(levelno, message)
530
+ current = None
531
+
532
+ for line in stderr.splitlines():
533
+ m = _CONTAINER_LOG_RE.match(line)
534
+ if m:
535
+ flush_current()
536
+ levelno = _LEVEL_MAP.get(m.group("level"), logging.INFO)
537
+ current = (m.group("logger"), levelno, [m.group("message")])
538
+ continue
539
+
540
+ # Continuation heuristic: keep multi-line LogEvent context attached.
541
+ if current is not None and (
542
+ line == ""
543
+ or line.startswith((" ", "\t"))
544
+ or line.startswith("Context:")
545
+ or line.startswith("{")
546
+ or line.startswith("[")
547
+ ):
548
+ current[2].append(line)
549
+ continue
550
+
551
+ # Otherwise treat as standalone stderr (warnings/tracebacks/etc).
552
+ flush_current()
553
+ logging.getLogger("container.stderr").warning(line)
554
+
555
+ flush_current()
556
+
440
557
  def run_sync(
441
558
  self,
442
559
  source: str,
@@ -34,9 +34,12 @@ def calculate_source_hash(tactus_root: Path) -> str:
34
34
  # Key paths that affect sandbox behavior
35
35
  paths_to_hash = [
36
36
  tactus_root / "tactus" / "dspy",
37
+ tactus_root / "tactus" / "adapters",
37
38
  tactus_root / "tactus" / "core",
38
39
  tactus_root / "tactus" / "primitives",
39
40
  tactus_root / "tactus" / "sandbox",
41
+ tactus_root / "tactus" / "stdlib",
42
+ tactus_root / "tactus" / "docker",
40
43
  tactus_root / "pyproject.toml", # Dependencies affect sandbox
41
44
  ]
42
45
 
@@ -50,12 +53,21 @@ def calculate_source_hash(tactus_root: Path) -> str:
50
53
  # Hash file contents
51
54
  hasher.update(path.read_bytes())
52
55
  elif path.is_dir():
53
- # Hash all Python files in directory (recursively)
54
- for py_file in sorted(path.rglob("*.py")):
56
+ # Hash directory files (recursively), skipping caches.
57
+ for file in sorted(path.rglob("*")):
58
+ if not file.is_file():
59
+ continue
60
+ if "__pycache__" in file.parts:
61
+ continue
62
+ if file.suffix == ".pyc":
63
+ continue
64
+ if file.name == ".DS_Store":
65
+ continue
66
+
55
67
  # Hash relative path + contents for reproducibility
56
- rel_path = str(py_file.relative_to(tactus_root))
68
+ rel_path = str(file.relative_to(tactus_root))
57
69
  hasher.update(rel_path.encode())
58
- hasher.update(py_file.read_bytes())
70
+ hasher.update(file.read_bytes())
59
71
 
60
72
  # Return short hash (16 chars is plenty for collision avoidance)
61
73
  return hasher.hexdigest()[:16]
@@ -22,13 +22,35 @@ if TYPE_CHECKING:
22
22
  from tactus.sandbox.protocol import ExecutionResult
23
23
 
24
24
  # Configure logging to stderr (stdout is reserved for result)
25
+ _LOG_LEVELS = {
26
+ "debug": logging.DEBUG,
27
+ "info": logging.INFO,
28
+ "warning": logging.WARNING,
29
+ "warn": logging.WARNING,
30
+ "error": logging.ERROR,
31
+ "critical": logging.CRITICAL,
32
+ }
33
+
34
+ _log_level_str = os.environ.get("TACTUS_LOG_LEVEL", "info").strip().lower()
35
+ _log_level = _LOG_LEVELS.get(_log_level_str, logging.INFO)
36
+
37
+ # CloudWatch-friendly, one line per record.
38
+ _log_fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
39
+
25
40
  logging.basicConfig(
26
- level=logging.INFO,
27
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
41
+ level=_log_level,
42
+ format=_log_fmt,
28
43
  stream=sys.stderr,
29
44
  )
30
45
  logger = logging.getLogger(__name__)
31
46
 
47
+ # Keep container stderr focused on procedure logs by default.
48
+ # Use `TACTUS_LOG_LEVEL=debug` to include internal runtime logs.
49
+ if _log_level > logging.DEBUG:
50
+ logging.getLogger("tactus.core").setLevel(logging.WARNING)
51
+ logging.getLogger("tactus.primitives").setLevel(logging.WARNING)
52
+ logging.getLogger("tactus.stdlib").setLevel(logging.WARNING)
53
+
32
54
 
33
55
  def read_request_from_stdin() -> Optional[Dict[str, Any]]:
34
56
  """Read the execution request from stdin as JSON."""
@@ -81,6 +103,7 @@ async def execute_procedure(
81
103
  from tactus.core import TactusRuntime
82
104
  from tactus.adapters.memory import MemoryStorage
83
105
  from tactus.adapters.http_callback_log import HTTPCallbackLogHandler
106
+ from tactus.adapters.cost_collector_log import CostCollectorLogHandler
84
107
 
85
108
  # Create a unique procedure ID
86
109
  import uuid
@@ -93,6 +116,11 @@ async def execute_procedure(
93
116
  logger.info(
94
117
  f"[SANDBOX] Using HTTP callback log handler: {os.environ.get('TACTUS_CALLBACK_URL')}"
95
118
  )
119
+ else:
120
+ # Provide cost collection + checkpoint event handling even without IDE callbacks.
121
+ # This avoids misleading 0-cost summaries for sandbox runs, while keeping streaming off.
122
+ log_handler = CostCollectorLogHandler()
123
+ logger.info("[SANDBOX] No callback URL set; using CostCollectorLogHandler")
96
124
 
97
125
  # Create runtime with log handler for event streaming
98
126
  runtime = TactusRuntime(
tactus/stdlib/io/fs.py ADDED
@@ -0,0 +1,154 @@
1
+ """
2
+ tactus.io.fs - Filesystem helpers for Tactus.
3
+
4
+ Provides safe directory listing and globbing, sandboxed to the procedure's base directory.
5
+
6
+ Usage in .tac files:
7
+ local fs = require("tactus.io.fs")
8
+
9
+ -- List files in a directory
10
+ local entries = fs.list_dir("chapters")
11
+
12
+ -- Glob files (sorted by default)
13
+ local qmd_files = fs.glob("chapters/*.qmd")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import glob as _glob
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ # Get context (injected by loader)
25
+ _ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
26
+
27
+
28
+ def _base_path() -> str:
29
+ if _ctx and getattr(_ctx, "base_path", None):
30
+ return str(_ctx.base_path)
31
+ return os.getcwd()
32
+
33
+
34
+ def _validate_relative_path(path: str) -> None:
35
+ # Keep semantics consistent with other stdlib IO modules:
36
+ # - no absolute paths
37
+ # - no traversal segments
38
+ p = Path(path)
39
+ if p.is_absolute():
40
+ raise PermissionError(f"Absolute paths not allowed: {path}")
41
+ if any(part == ".." for part in p.parts):
42
+ raise PermissionError(f"Path traversal not allowed: {path}")
43
+
44
+
45
+ def list_dir(dirpath: str, options: Optional[Dict[str, Any]] = None) -> List[str]:
46
+ """
47
+ List entries in a directory.
48
+
49
+ Args:
50
+ dirpath: Directory path (relative to working directory)
51
+ options:
52
+ - files_only: bool (default True) - include only files
53
+ - dirs_only: bool (default False) - include only directories
54
+ - sort: bool (default True) - sort results
55
+
56
+ Returns:
57
+ List of entry paths (relative to working directory)
58
+ """
59
+ options = options or {}
60
+ files_only = bool(options.get("files_only", True))
61
+ dirs_only = bool(options.get("dirs_only", False))
62
+ sort = bool(options.get("sort", True))
63
+
64
+ _validate_relative_path(dirpath)
65
+
66
+ base = os.path.realpath(_base_path())
67
+ abs_dir = os.path.realpath(os.path.join(base, dirpath))
68
+
69
+ # Ensure the directory itself is within base path (symlink-safe via realpath)
70
+ if abs_dir != base and not abs_dir.startswith(base + os.sep):
71
+ raise PermissionError(f"Access denied: path outside working directory: {dirpath}")
72
+
73
+ if not os.path.isdir(abs_dir):
74
+ raise FileNotFoundError(f"Directory not found: {dirpath}")
75
+
76
+ entries: List[str] = []
77
+ for name in os.listdir(abs_dir):
78
+ abs_child = os.path.realpath(os.path.join(abs_dir, name))
79
+ if abs_child != base and not abs_child.startswith(base + os.sep):
80
+ # Skip symlink escapes
81
+ continue
82
+
83
+ is_dir = os.path.isdir(abs_child)
84
+ is_file = os.path.isfile(abs_child)
85
+
86
+ if dirs_only and not is_dir:
87
+ continue
88
+ if files_only and not is_file:
89
+ continue
90
+
91
+ rel = os.path.relpath(abs_child, base).replace("\\", "/")
92
+ entries.append(rel)
93
+
94
+ if sort:
95
+ entries.sort()
96
+
97
+ return entries
98
+
99
+
100
+ def glob(pattern: str, options: Optional[Dict[str, Any]] = None) -> List[str]:
101
+ """
102
+ Glob files within the working directory.
103
+
104
+ Args:
105
+ pattern: Glob pattern (relative to working directory), e.g. "chapters/*.qmd"
106
+ options:
107
+ - recursive: bool (default False) - enable ** patterns
108
+ - files_only: bool (default True) - include only files
109
+ - dirs_only: bool (default False) - include only directories
110
+ - sort: bool (default True) - sort results
111
+
112
+ Returns:
113
+ List of matched paths (relative to working directory)
114
+ """
115
+ options = options or {}
116
+ recursive = bool(options.get("recursive", False))
117
+ files_only = bool(options.get("files_only", True))
118
+ dirs_only = bool(options.get("dirs_only", False))
119
+ sort = bool(options.get("sort", True))
120
+
121
+ _validate_relative_path(pattern)
122
+
123
+ base = os.path.realpath(_base_path())
124
+ abs_pattern = os.path.realpath(os.path.join(base, pattern))
125
+
126
+ # Ensure the pattern root is within base path (symlink-safe via realpath)
127
+ if abs_pattern != base and not abs_pattern.startswith(base + os.sep):
128
+ raise PermissionError(f"Access denied: path outside working directory: {pattern}")
129
+
130
+ matches: List[str] = []
131
+ for match in _glob.glob(abs_pattern, recursive=recursive):
132
+ abs_match = os.path.realpath(match)
133
+ if abs_match != base and not abs_match.startswith(base + os.sep):
134
+ # Skip symlink escapes
135
+ continue
136
+
137
+ is_dir = os.path.isdir(abs_match)
138
+ is_file = os.path.isfile(abs_match)
139
+
140
+ if dirs_only and not is_dir:
141
+ continue
142
+ if files_only and not is_file:
143
+ continue
144
+
145
+ rel = os.path.relpath(abs_match, base).replace("\\", "/")
146
+ matches.append(rel)
147
+
148
+ if sort:
149
+ matches.sort()
150
+
151
+ return matches
152
+
153
+
154
+ __tactus_exports__ = ["list_dir", "glob"]
tactus/stdlib/loader.py CHANGED
@@ -134,7 +134,7 @@ class StdlibModuleLoader:
134
134
  # Cache
135
135
  self.loaded_modules[module_name] = lua_table
136
136
 
137
- logger.info(f"Loaded stdlib Python module: {module_name}")
137
+ logger.debug(f"Loaded stdlib Python module: {module_name}")
138
138
  return lua_table
139
139
 
140
140
  def _get_module_exports(self, module) -> Dict[str, Callable]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tactus
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: Tactus: Lua-based DSL for agentic workflows
5
5
  Project-URL: Homepage, https://github.com/AnthusAI/Tactus
6
6
  Project-URL: Documentation, https://github.com/AnthusAI/Tactus/tree/main/docs
@@ -1488,6 +1488,21 @@ tactus run workflow.tac # If procedure has required inputs, you'll be prompted
1488
1488
  tactus run workflow.tac --storage file --storage-path ./data
1489
1489
  ```
1490
1490
 
1491
+ #### Logging Options
1492
+
1493
+ The `run` command supports filtering and formatting logs:
1494
+
1495
+ ```bash
1496
+ # Show less/more output
1497
+ tactus run workflow.tac --log-level warning
1498
+ tactus run workflow.tac --log-level debug
1499
+
1500
+ # Choose a log format
1501
+ tactus run workflow.tac --log-format rich # default, grouped timestamps
1502
+ tactus run workflow.tac --log-format terminal # no timestamps, higher-signal terminal output
1503
+ tactus run workflow.tac --log-format raw # one-line-per-record, timestamped (CloudWatch-friendly)
1504
+ ```
1505
+
1491
1506
  The CLI automatically parses parameter types:
1492
1507
  - **Strings**: Direct values or quoted strings
1493
1508
  - **Numbers**: Integers or floats are auto-detected
@@ -1,7 +1,8 @@
1
- tactus/__init__.py,sha256=j3GKQg-FI6kEa8_EqgcxzyTLusQ6dIuqe0LHH9DOYgI,1245
1
+ tactus/__init__.py,sha256=DvHJYE4l1j3LCTY2nrgfiK0uQ5pzjUURsQydmI9QeOY,1245
2
2
  tactus/adapters/__init__.py,sha256=lU8uUxuryFRIpVrn_KeVK7aUhsvOT1tYsuE3FOOIFpI,289
3
- tactus/adapters/cli_hitl.py,sha256=l8jKU3y99g8z2vS11td0JXLVG77SF01nO-Ss4pRFXO0,6962
3
+ tactus/adapters/cli_hitl.py,sha256=3dH58du0lN4k-OvQrAHrAqHFqBjolqNKFb94JaNHtn8,6964
4
4
  tactus/adapters/cli_log.py,sha256=JKD693goi_wT_Kei4mTc2KJ-0QfgFZTpV3Prb8zfNZo,9779
5
+ tactus/adapters/cost_collector_log.py,sha256=Ha3pFMdC0HJ8eBnZpM1VRLm-94RKelu5PEPDgFgNKow,1701
5
6
  tactus/adapters/file_storage.py,sha256=c4prD6-NSEMixrpQsWsdr3xuKzxptoEaLjUkZl9SnKk,12759
6
7
  tactus/adapters/http_callback_log.py,sha256=likXNiLGiHXNHAeWGzUPhbd4Di94PJVjXz5ABNRfZig,3687
7
8
  tactus/adapters/ide_log.py,sha256=JfJE5rGM_bPfBImod-2_QDhyyk2ObVEmkvhsWSzaBKU,2026
@@ -14,14 +15,14 @@ tactus/backends/http_backend.py,sha256=D2N91I5bnjhHMLG84-U-BRS-mIuwoQq72Feffi7At
14
15
  tactus/backends/model_backend.py,sha256=P8dCUpDxJmA8_EO1snZuXyIyUZ_BlqReeC6zenO7Kv0,763
15
16
  tactus/backends/pytorch_backend.py,sha256=I7H7UTa_Scx9_FtmPWn-G4noadaNVEQj-9Kjtjpgl6E,3305
16
17
  tactus/cli/__init__.py,sha256=kVhdCkwWEPdt3vn9si-iKvh6M9817aOH6rLSsNzRuyg,80
17
- tactus/cli/app.py,sha256=aBpZ4IUka-y1HV4oHyBvydnX8XI5JRo6WwMgLji2Df4,78595
18
+ tactus/cli/app.py,sha256=KPTS7vAxMQE-sog7uAaZOyaF73XMthrM5iKuPWXQmOk,81825
18
19
  tactus/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  tactus/core/__init__.py,sha256=TK5rWr3HmOO_igFa5ESGp6teWwS58vnvQhIWqkcgqwk,880
20
- tactus/core/config_manager.py,sha256=DXWQs__rD4fXo4KPJb24hyXGdUBiiEv1mV_h4yu0vKU,31575
21
- tactus/core/dsl_stubs.py,sha256=naF27E1-zzDtuuYAKo1uThwQfLgaQOP35vBlGs6QsyY,66871
21
+ tactus/core/config_manager.py,sha256=u90XChTeb2reZ3I6c3o_zI2UlzlFCNtL9-NhE1Vm6L0,31577
22
+ tactus/core/dsl_stubs.py,sha256=JhXJiFQNNAMkW43fFeq7sNgnzbbqZaucLwcObfJpFDg,67261
22
23
  tactus/core/exceptions.py,sha256=HruvJQ4yIi78hOvvHviNZolcraMwXrlGQzxHqYFQapA,1468
23
- tactus/core/execution_context.py,sha256=DJuy3sU2fS6n3Rp_GK7nIOFOLob23uTenZaIXXDm7Sk,16656
24
- tactus/core/lua_sandbox.py,sha256=dylVIXktM8R4Mlu-ULyYpvAY7AuUSJxvjtPOJvy5lHg,18260
24
+ tactus/core/execution_context.py,sha256=WNd55_JhmkDifA9Td3xIs8to5bwGDlCgPcbvkvLYAv0,16658
25
+ tactus/core/lua_sandbox.py,sha256=WuXRuXSl9ylU0y3ZMvyDWjWiG6GdLVn2csMY-2Zmrbg,18262
25
26
  tactus/core/message_history_manager.py,sha256=e3_seV6bWSJVicrZYf2LmaQBvroDXDRc5Tho49fZ_ns,7596
26
27
  tactus/core/mocking.py,sha256=1MKtg8FHCYj-Goihi5b8xNNYo-MDuzSpsHEX_e7bnfg,9217
27
28
  tactus/core/output_validator.py,sha256=GDLGbIJAustb4__mThzLWQFpMyvRj84bgR4K28xMTjE,7777
@@ -31,10 +32,10 @@ tactus/core/template_resolver.py,sha256=6zRq4zT1lRHdLoa7M-yoLx4hFZ9RcjH9Y0EdaAtW
31
32
  tactus/core/yaml_parser.py,sha256=VbKej95wzen07JWP1K2x2eIOhWkZIDex_fHAm5Qitao,13890
32
33
  tactus/core/dependencies/__init__.py,sha256=28-TM7_i-JqTD3hvkq1kMzr__A8VjfIKXymdW9mn5NM,362
33
34
  tactus/core/dependencies/registry.py,sha256=bgRdqJJTUrnQlu0wvjv2In1EPq7prZq-b9eBDhE3rmE,6114
34
- tactus/docker/Dockerfile,sha256=Lo1erljpHSw9O-uOj0mn47gtg6abqzBvhbChAb-GQhI,1720
35
+ tactus/docker/Dockerfile,sha256=fnK5kZlgM-L7vboiwfTqcs70OsZsvh1ba4YzRxie138,1887
35
36
  tactus/docker/entrypoint.sh,sha256=-qYq5RQIGS6KirJ6NO0XiHZf_oAdfmk5HzHOBsiqxRM,1870
36
37
  tactus/dspy/__init__.py,sha256=beUkvMUFdPvZE9-bEOfRo2TH-FoCvPT_L9_dpJPW324,1226
37
- tactus/dspy/agent.py,sha256=sIvi4ySumUXL9bPnMNLhFODOhYbEMlElnqHv57IaFrs,37859
38
+ tactus/dspy/agent.py,sha256=H1QU_LPknF3vLL4uAg7MdrZXWOjxpXU8PQCZWOI3bWc,38699
38
39
  tactus/dspy/config.py,sha256=oXwYzfVCTBHRnbH_vvm591FhE0zKep7wgmujLSXKf_c,6013
39
40
  tactus/dspy/history.py,sha256=0yGi3P5ruRUPoRyaCWsUDeuEYYsfproc_7pMVZuhmUo,5980
40
41
  tactus/dspy/module.py,sha256=sJdFS-5A4SpuiMLjbwiZJCvg3pTtEx8x8MRVaqjCQ2I,15423
@@ -46,7 +47,7 @@ tactus/ide/server.py,sha256=KALEdXcoUHLjJPWFrbyMbPkQeoHKfhSu7J22HeXx7yA,98912
46
47
  tactus/primitives/__init__.py,sha256=2NHEGBCuasVJlD5bHE6OSYivZ3WEp90DJDTvoKehVzg,1712
47
48
  tactus/primitives/control.py,sha256=PjRt_Pegcj2L1Uy-IUBQKTYFRMXy7b9q1z2kzJNH8qw,4683
48
49
  tactus/primitives/file.py,sha256=-kz0RCst_i_3V860-LtGntYpE0Mm371U_KGHqELbMx0,7186
49
- tactus/primitives/handles.py,sha256=Kc65m3YWrOUGMkg-CX1_bHuBKoZUG7uvi8HtLOykgxM,11761
50
+ tactus/primitives/handles.py,sha256=5UUpoyASKZPaE3UcF6IOwTYicmGqYjuABncNNF2lAbY,11753
50
51
  tactus/primitives/human.py,sha256=yjh5LF4PNeKaFgMWMVJd-qkp9XSfNqQM4n4X-Qg60Ow,12397
51
52
  tactus/primitives/json.py,sha256=XQQQEwUvix_Djscq_thBTHeCf5VCex48rj4m6yp4f7Y,5722
52
53
  tactus/primitives/log.py,sha256=rLYgHeXgU1Pr3lYFXZxfuorZDdEFPGh4Mjeosjz-7Zc,6243
@@ -79,16 +80,17 @@ tactus/providers/google.py,sha256=wgZ3eiQif1rq1T8BK5V2kL_QVCmqBQZuWLz37y9cxOQ,31
79
80
  tactus/providers/openai.py,sha256=3qSXfdELTHdU7vuRSxQrtnfNctt0HhrePOLFj3YlViA,2692
80
81
  tactus/sandbox/__init__.py,sha256=UCBvPD63szvSdwSzpznLW-cnJOgGkVHiKcmJtsAmnuA,1424
81
82
  tactus/sandbox/config.py,sha256=zbv2ewzwN381HuJRaPnDgzdbZlxNIUYIWAbj9udYf0A,3713
82
- tactus/sandbox/container_runner.py,sha256=GUY460O8oTREzpc7NWUHcCqSrLapovDT0yWpA0hRV9Q,16161
83
- tactus/sandbox/docker_manager.py,sha256=fqSlLpanL4zdgj0NopCc0Du-Ac1TlbgswQJ5N3CNfrs,13535
84
- tactus/sandbox/entrypoint.py,sha256=wqfHP3EKVEJJBJuAR859xQu9UvP4oU8qENCMxwV0yi8,5122
83
+ tactus/sandbox/container_runner.py,sha256=lGpWK0e4_E0_eCyogXMNQ4uz1bKVGpfqn71yLX58-Y0,20249
84
+ tactus/sandbox/docker_manager.py,sha256=SGv0IEN9usgyQRvbonrgOLm6GICel8ifzBAopAwOyt0,13949
85
+ tactus/sandbox/entrypoint.py,sha256=GdJv_GHHKoAK9VjtkZ5LNt0oqYiz4x8F1exwkbAIjkA,6277
85
86
  tactus/sandbox/protocol.py,sha256=hk8yYj6hDmGA32bet4AJ3d3CKiFLMBGoJnc1OrD3nLc,6397
86
87
  tactus/stdlib/__init__.py,sha256=26AUJ2IGp6OuhWaUeGSRwTa1r7W4e6-fhoLa4u7RjLQ,288
87
- tactus/stdlib/loader.py,sha256=Xbn42fNMF31h6JkWGkvldN8U-GtnRB2CVXeLZJbalc0,8678
88
+ tactus/stdlib/loader.py,sha256=qjVnz5mn3Uu7g1O4vjSREHkR-YdRoON1vqJQq-oiFIE,8679
88
89
  tactus/stdlib/io/__init__.py,sha256=lOUgkUsV6x1ZdGKnFjh-v8wBJwn2UEM28h-mEal8aMw,379
89
90
  tactus/stdlib/io/csv.py,sha256=KsqlgO8gxJ01c9g50Zsc3U3I7aCeAohy9opy2qSWCVU,2295
90
91
  tactus/stdlib/io/excel.py,sha256=-6oxY-Rj__OV2YSt9K2YJ6-g--BFCV55Wt9KJ8cwSmI,3494
91
92
  tactus/stdlib/io/file.py,sha256=1_lvpn1oCTHZnZToNQEUNfC62Qa0f_Mp9yJVlnI-IGY,1977
93
+ tactus/stdlib/io/fs.py,sha256=Bx6Jxc3VBTWqabwq109ok-BMvL25k51ozGy8watiTj8,4802
92
94
  tactus/stdlib/io/hdf5.py,sha256=7ELLaQZI1GOsycU1a60J6RTPNLH7EMdaPAYlNx0dQLA,3025
93
95
  tactus/stdlib/io/json.py,sha256=P6C6rIwAxY97MivCanxKF8TmRotIUxBlHcuv8etmLu8,2641
94
96
  tactus/stdlib/io/parquet.py,sha256=hycr0pjqysjhggHx7_UZJL_jkeP1wz8BCozL39EWk-0,2045
@@ -142,8 +144,8 @@ tactus/validation/generated/LuaParserVisitor.py,sha256=ageKSmHPxnO3jBS2fBtkmYBOd
142
144
  tactus/validation/generated/__init__.py,sha256=5gWlwRI0UvmHw2fnBpj_IG6N8oZeabr5tbj1AODDvjc,196
143
145
  tactus/validation/grammar/LuaLexer.g4,sha256=t2MXiTCr127RWAyQGvamkcU_m4veqPzSuHUtAKwalw4,2771
144
146
  tactus/validation/grammar/LuaParser.g4,sha256=ceZenb90BdiZmVdOxMGj9qJk3QbbWVZe5HUqPgoePfY,3202
145
- tactus-0.25.0.dist-info/METADATA,sha256=WGpOx64_mmNzszKF7PRCYrzCZqwUSkwn9HlC98xUj4E,55250
146
- tactus-0.25.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
147
- tactus-0.25.0.dist-info/entry_points.txt,sha256=vWseqty8m3z-Worje0IYxlioMjPDCoSsm0AtY4GghBY,47
148
- tactus-0.25.0.dist-info/licenses/LICENSE,sha256=ivohBcAIYnaLPQ-lKEeCXSMvQUVISpQfKyxHBHoa4GA,1066
149
- tactus-0.25.0.dist-info/RECORD,,
147
+ tactus-0.26.0.dist-info/METADATA,sha256=ji52FYxdpMlkMoubu8rIc7tDmDHdmvTKdOAPmYt9OyA,55752
148
+ tactus-0.26.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
149
+ tactus-0.26.0.dist-info/entry_points.txt,sha256=vWseqty8m3z-Worje0IYxlioMjPDCoSsm0AtY4GghBY,47
150
+ tactus-0.26.0.dist-info/licenses/LICENSE,sha256=ivohBcAIYnaLPQ-lKEeCXSMvQUVISpQfKyxHBHoa4GA,1066
151
+ tactus-0.26.0.dist-info/RECORD,,