tactus 0.24.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.24.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
 
@@ -1179,6 +1268,7 @@ def test(
1179
1268
  ("aws", "access_key_id"): "AWS_ACCESS_KEY_ID",
1180
1269
  ("aws", "secret_access_key"): "AWS_SECRET_ACCESS_KEY",
1181
1270
  ("aws", "default_region"): "AWS_DEFAULT_REGION",
1271
+ ("aws", "profile"): "AWS_PROFILE",
1182
1272
  }
1183
1273
 
1184
1274
  for config_key, env_key in env_mappings.items():