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 +1 -1
- tactus/adapters/cli_hitl.py +2 -2
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/cli/app.py +100 -10
- tactus/core/config_manager.py +469 -16
- tactus/core/dsl_stubs.py +51 -43
- tactus/core/execution_context.py +2 -2
- tactus/core/lua_sandbox.py +2 -2
- tactus/docker/Dockerfile +4 -0
- tactus/dspy/agent.py +25 -8
- tactus/ide/server.py +35 -0
- tactus/primitives/handles.py +12 -12
- tactus/sandbox/container_runner.py +122 -5
- tactus/sandbox/docker_manager.py +16 -4
- tactus/sandbox/entrypoint.py +30 -2
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/loader.py +1 -1
- {tactus-0.24.0.dist-info → tactus-0.26.0.dist-info}/METADATA +16 -1
- {tactus-0.24.0.dist-info → tactus-0.26.0.dist-info}/RECORD +22 -20
- {tactus-0.24.0.dist-info → tactus-0.26.0.dist-info}/WHEEL +0 -0
- {tactus-0.24.0.dist-info → tactus-0.26.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.24.0.dist-info → tactus-0.26.0.dist-info}/licenses/LICENSE +0 -0
tactus/__init__.py
CHANGED
tactus/adapters/cli_hitl.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
138
|
-
""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
logging.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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():
|