tactus 0.26.0__py3-none-any.whl → 0.27.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/broker_log.py +55 -0
- tactus/adapters/cli_log.py +0 -25
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +260 -0
- tactus/broker/server.py +505 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/app.py +38 -2
- tactus/core/dsl_stubs.py +2 -1
- tactus/core/output_validator.py +6 -3
- tactus/core/registry.py +8 -1
- tactus/core/runtime.py +15 -1
- tactus/core/yaml_parser.py +1 -11
- tactus/dspy/agent.py +190 -102
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +21 -8
- tactus/dspy/prediction.py +71 -5
- tactus/ide/server.py +37 -142
- tactus/primitives/__init__.py +2 -0
- tactus/primitives/handles.py +34 -7
- tactus/primitives/host.py +94 -0
- tactus/primitives/log.py +4 -0
- tactus/primitives/model.py +20 -2
- tactus/primitives/procedure.py +106 -51
- tactus/primitives/tool.py +0 -2
- tactus/protocols/__init__.py +0 -7
- tactus/protocols/log_handler.py +2 -2
- tactus/protocols/models.py +1 -1
- tactus/sandbox/config.py +33 -5
- tactus/sandbox/container_runner.py +498 -60
- tactus/sandbox/entrypoint.py +30 -17
- tactus/sandbox/protocol.py +0 -9
- tactus/testing/README.md +0 -4
- tactus/testing/mock_agent.py +80 -23
- tactus/testing/test_runner.py +0 -18
- {tactus-0.26.0.dist-info → tactus-0.27.0.dist-info}/METADATA +1 -1
- {tactus-0.26.0.dist-info → tactus-0.27.0.dist-info}/RECORD +40 -33
- {tactus-0.26.0.dist-info → tactus-0.27.0.dist-info}/WHEEL +0 -0
- {tactus-0.26.0.dist-info → tactus-0.27.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.26.0.dist-info → tactus-0.27.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/handles.py
CHANGED
|
@@ -149,12 +149,38 @@ class AgentHandle:
|
|
|
149
149
|
logger.debug(
|
|
150
150
|
f"[CHECKPOINT] Creating checkpoint for agent '{self.name}', type=agent_turn, source_info={source_info}"
|
|
151
151
|
)
|
|
152
|
-
|
|
153
|
-
agent_call, checkpoint_type="agent_turn", source_info=source_info
|
|
154
|
-
)
|
|
155
|
-
else:
|
|
156
|
-
# No execution context - call directly without checkpointing
|
|
157
|
-
|
|
152
|
+
result = self._execution_context.checkpoint(
|
|
153
|
+
agent_call, checkpoint_type="agent_turn", source_info=source_info
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
# No execution context - call directly without checkpointing
|
|
157
|
+
result = self._primitive(converted_inputs)
|
|
158
|
+
|
|
159
|
+
# Convenience: expose the last agent output on the handle as `.output`
|
|
160
|
+
# for Lua patterns like `agent(); return agent.output`.
|
|
161
|
+
output_text = None
|
|
162
|
+
if result is not None:
|
|
163
|
+
for attr in ("response", "message"):
|
|
164
|
+
try:
|
|
165
|
+
value = getattr(result, attr, None)
|
|
166
|
+
except Exception:
|
|
167
|
+
value = None
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
output_text = value
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
if output_text is None and isinstance(result, dict):
|
|
173
|
+
for key in ("response", "message"):
|
|
174
|
+
value = result.get(key)
|
|
175
|
+
if isinstance(value, str):
|
|
176
|
+
output_text = value
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if output_text is None:
|
|
180
|
+
output_text = str(result)
|
|
181
|
+
|
|
182
|
+
self.output = output_text
|
|
183
|
+
return result
|
|
158
184
|
|
|
159
185
|
def _set_primitive(
|
|
160
186
|
self, primitive: "DSPyAgentHandle", execution_context: Optional[Any] = None
|
|
@@ -216,7 +242,8 @@ class ModelHandle:
|
|
|
216
242
|
f"Model '{self.name}' initialization failed.\n"
|
|
217
243
|
f"This should not happen - please report this as a bug."
|
|
218
244
|
)
|
|
219
|
-
|
|
245
|
+
converted_data = _convert_lua_table(data) if data is not None else None
|
|
246
|
+
return self._primitive.predict(converted_data)
|
|
220
247
|
|
|
221
248
|
def __call__(self, data: Any = None) -> Any:
|
|
222
249
|
"""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Host Primitive - brokered host capabilities for the runtime container.
|
|
3
|
+
|
|
4
|
+
This primitive is intended to be used inside the sandboxed runtime container.
|
|
5
|
+
It delegates allowlisted operations to the trusted host-side broker via the
|
|
6
|
+
`TACTUS_BROKER_SOCKET` transport.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from tactus.broker.client import BrokerClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HostPrimitive:
|
|
18
|
+
"""Provides access to allowlisted host-side tools via the broker."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: Optional[BrokerClient] = None):
|
|
21
|
+
self._client = client or BrokerClient.from_environment()
|
|
22
|
+
self._registry = None
|
|
23
|
+
if self._client is None:
|
|
24
|
+
# Allow Host.call() to work in non-sandboxed runs (and in deterministic tests)
|
|
25
|
+
# without requiring a broker transport, while still staying deny-by-default.
|
|
26
|
+
from tactus.broker.server import HostToolRegistry
|
|
27
|
+
|
|
28
|
+
self._registry = HostToolRegistry.default()
|
|
29
|
+
|
|
30
|
+
def _run_coro(self, coro):
|
|
31
|
+
"""
|
|
32
|
+
Run an async coroutine from Lua's synchronous context.
|
|
33
|
+
|
|
34
|
+
Mirrors the approach used by `ToolHandle` for async tool handlers.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
asyncio.get_running_loop()
|
|
38
|
+
|
|
39
|
+
import threading
|
|
40
|
+
|
|
41
|
+
result_container = {"value": None, "exception": None}
|
|
42
|
+
|
|
43
|
+
def run_in_thread():
|
|
44
|
+
try:
|
|
45
|
+
result_container["value"] = asyncio.run(coro)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
result_container["exception"] = e
|
|
48
|
+
|
|
49
|
+
thread = threading.Thread(target=run_in_thread)
|
|
50
|
+
thread.start()
|
|
51
|
+
thread.join()
|
|
52
|
+
|
|
53
|
+
if result_container["exception"]:
|
|
54
|
+
raise result_container["exception"]
|
|
55
|
+
return result_container["value"]
|
|
56
|
+
|
|
57
|
+
except RuntimeError:
|
|
58
|
+
return asyncio.run(coro)
|
|
59
|
+
|
|
60
|
+
def _lua_to_python(self, obj: Any) -> Any:
|
|
61
|
+
if obj is None:
|
|
62
|
+
return None
|
|
63
|
+
if hasattr(obj, "items") and not isinstance(obj, dict):
|
|
64
|
+
return {k: self._lua_to_python(v) for k, v in obj.items()}
|
|
65
|
+
if isinstance(obj, dict):
|
|
66
|
+
return {k: self._lua_to_python(v) for k, v in obj.items()}
|
|
67
|
+
if isinstance(obj, (list, tuple)):
|
|
68
|
+
return [self._lua_to_python(v) for v in obj]
|
|
69
|
+
return obj
|
|
70
|
+
|
|
71
|
+
def call(self, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Call an allowlisted host tool via the broker.
|
|
74
|
+
|
|
75
|
+
Example (Lua):
|
|
76
|
+
local result = Host.call("host.ping", {value = 1})
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(name, str) or not name:
|
|
79
|
+
raise ValueError("Host.call requires a non-empty tool name string")
|
|
80
|
+
|
|
81
|
+
args_dict = self._lua_to_python(args) or {}
|
|
82
|
+
if not isinstance(args_dict, dict):
|
|
83
|
+
raise ValueError("Host.call args must be an object/table")
|
|
84
|
+
|
|
85
|
+
if self._client is not None:
|
|
86
|
+
return self._run_coro(self._client.call_tool(name=name, args=args_dict))
|
|
87
|
+
|
|
88
|
+
if self._registry is not None:
|
|
89
|
+
try:
|
|
90
|
+
return self._registry.call(name, args_dict)
|
|
91
|
+
except KeyError as e:
|
|
92
|
+
raise RuntimeError(f"Tool not allowlisted: {name}") from e
|
|
93
|
+
|
|
94
|
+
raise RuntimeError("Host.call requires TACTUS_BROKER_SOCKET to be set")
|
tactus/primitives/log.py
CHANGED
|
@@ -150,6 +150,10 @@ class LogPrimitive:
|
|
|
150
150
|
formatted = self._format_message(message, context)
|
|
151
151
|
self.logger.warning(formatted)
|
|
152
152
|
|
|
153
|
+
def warning(self, message: str, context: Optional[Dict[str, Any]] = None) -> None:
|
|
154
|
+
"""Alias for warn(), matching common logging APIs."""
|
|
155
|
+
self.warn(message, context)
|
|
156
|
+
|
|
153
157
|
def error(self, message: str, context: Optional[Dict[str, Any]] = None) -> None:
|
|
154
158
|
"""
|
|
155
159
|
Log error message.
|
tactus/primitives/model.py
CHANGED
|
@@ -3,7 +3,7 @@ Model primitive for ML inference with automatic checkpointing.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Optional
|
|
7
7
|
|
|
8
8
|
from tactus.core.execution_context import ExecutionContext
|
|
9
9
|
|
|
@@ -23,7 +23,13 @@ class ModelPrimitive:
|
|
|
23
23
|
Each .predict() call is automatically checkpointed for durability.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
model_name: str,
|
|
29
|
+
config: dict,
|
|
30
|
+
context: ExecutionContext | None = None,
|
|
31
|
+
mock_manager: Optional[Any] = None,
|
|
32
|
+
):
|
|
27
33
|
"""
|
|
28
34
|
Initialize model primitive.
|
|
29
35
|
|
|
@@ -39,6 +45,7 @@ class ModelPrimitive:
|
|
|
39
45
|
self.model_name = model_name
|
|
40
46
|
self.config = config
|
|
41
47
|
self.context = context
|
|
48
|
+
self.mock_manager = mock_manager
|
|
42
49
|
|
|
43
50
|
# Extract optional input/output schemas
|
|
44
51
|
self.input_schema = config.get("input", {})
|
|
@@ -124,6 +131,17 @@ class ModelPrimitive:
|
|
|
124
131
|
Returns:
|
|
125
132
|
Model prediction result
|
|
126
133
|
"""
|
|
134
|
+
if self.mock_manager is not None:
|
|
135
|
+
args = input_data if isinstance(input_data, dict) else {"input": input_data}
|
|
136
|
+
mock_result = self.mock_manager.get_mock_response(self.model_name, args)
|
|
137
|
+
if mock_result is not None:
|
|
138
|
+
# Ensure temporal mocks advance and calls are available for assertions.
|
|
139
|
+
try:
|
|
140
|
+
self.mock_manager.record_call(self.model_name, args, mock_result)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return mock_result
|
|
144
|
+
|
|
127
145
|
return self.backend.predict_sync(input_data)
|
|
128
146
|
|
|
129
147
|
def __call__(self, input_data: Any) -> Any:
|
tactus/primitives/procedure.py
CHANGED
|
@@ -75,6 +75,7 @@ class ProcedurePrimitive:
|
|
|
75
75
|
self,
|
|
76
76
|
execution_context: Any,
|
|
77
77
|
runtime_factory: Callable[[str, Dict[str, Any]], Any],
|
|
78
|
+
lua_sandbox: Any = None,
|
|
78
79
|
max_depth: int = 5,
|
|
79
80
|
current_depth: int = 0,
|
|
80
81
|
):
|
|
@@ -84,11 +85,13 @@ class ProcedurePrimitive:
|
|
|
84
85
|
Args:
|
|
85
86
|
execution_context: Execution context for state management
|
|
86
87
|
runtime_factory: Factory function to create TactusRuntime instances
|
|
88
|
+
lua_sandbox: LuaSandbox instance for in-file procedure lookup
|
|
87
89
|
max_depth: Maximum recursion depth
|
|
88
90
|
current_depth: Current recursion depth
|
|
89
91
|
"""
|
|
90
92
|
self.execution_context = execution_context
|
|
91
93
|
self.runtime_factory = runtime_factory
|
|
94
|
+
self.lua_sandbox = lua_sandbox
|
|
92
95
|
self.max_depth = max_depth
|
|
93
96
|
self.current_depth = current_depth
|
|
94
97
|
self.handles: Dict[str, ProcedureHandle] = {}
|
|
@@ -96,6 +99,28 @@ class ProcedurePrimitive:
|
|
|
96
99
|
|
|
97
100
|
logger.info(f"ProcedurePrimitive initialized (depth {current_depth}/{max_depth})")
|
|
98
101
|
|
|
102
|
+
def __call__(self, name: str) -> Any:
|
|
103
|
+
"""
|
|
104
|
+
Look up an in-file named procedure by name.
|
|
105
|
+
|
|
106
|
+
Enables Lua syntax:
|
|
107
|
+
local res = Procedure("my_proc")({ ... })
|
|
108
|
+
|
|
109
|
+
Named procedures are injected into Lua globals by the runtime during initialization.
|
|
110
|
+
"""
|
|
111
|
+
if not self.lua_sandbox or not hasattr(self.lua_sandbox, "lua"):
|
|
112
|
+
raise ProcedureExecutionError("Procedure lookup is not available (lua_sandbox missing)")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
proc = self.lua_sandbox.lua.globals()[name]
|
|
116
|
+
except Exception:
|
|
117
|
+
proc = None
|
|
118
|
+
|
|
119
|
+
if proc is None:
|
|
120
|
+
raise ProcedureExecutionError(f"Named procedure '{name}' not found")
|
|
121
|
+
|
|
122
|
+
return proc
|
|
123
|
+
|
|
99
124
|
def run(self, name: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
100
125
|
"""
|
|
101
126
|
Synchronous procedure invocation with auto-checkpointing.
|
|
@@ -122,6 +147,12 @@ class ProcedurePrimitive:
|
|
|
122
147
|
|
|
123
148
|
# Normalize params
|
|
124
149
|
params = params or {}
|
|
150
|
+
if hasattr(params, "items"):
|
|
151
|
+
from tactus.core.dsl_stubs import lua_table_to_dict
|
|
152
|
+
|
|
153
|
+
params = lua_table_to_dict(params)
|
|
154
|
+
if isinstance(params, list) and len(params) == 0:
|
|
155
|
+
params = {}
|
|
125
156
|
|
|
126
157
|
# Wrap execution in checkpoint for durability
|
|
127
158
|
def execute_procedure():
|
|
@@ -134,26 +165,37 @@ class ProcedurePrimitive:
|
|
|
134
165
|
|
|
135
166
|
# Execute synchronously (runtime.execute is async, so we need to run it)
|
|
136
167
|
import asyncio
|
|
168
|
+
import threading
|
|
169
|
+
|
|
170
|
+
async def run_subprocedure():
|
|
171
|
+
return await runtime.execute(source=source, context=params, format="lua")
|
|
137
172
|
|
|
138
173
|
try:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# Instead, we need to await it, but we're in a sync function
|
|
142
|
-
# Solution: Create a task and wait for it
|
|
143
|
-
result = asyncio.create_task(
|
|
144
|
-
runtime.execute(source=source, context=params, format="lua")
|
|
145
|
-
)
|
|
146
|
-
# This won't work in sync context - we need to handle this differently
|
|
147
|
-
# For now, use run_until_complete in a new loop
|
|
148
|
-
raise RuntimeError("Cannot run nested async in sync context")
|
|
174
|
+
asyncio.get_running_loop()
|
|
175
|
+
has_running_loop = True
|
|
149
176
|
except RuntimeError:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
177
|
+
has_running_loop = False
|
|
178
|
+
|
|
179
|
+
if has_running_loop:
|
|
180
|
+
result_holder = {}
|
|
181
|
+
error_holder = {}
|
|
182
|
+
|
|
183
|
+
def run_in_thread():
|
|
184
|
+
try:
|
|
185
|
+
result_holder["result"] = asyncio.run(run_subprocedure())
|
|
186
|
+
except Exception as e:
|
|
187
|
+
error_holder["error"] = e
|
|
188
|
+
|
|
189
|
+
t = threading.Thread(target=run_in_thread, daemon=True)
|
|
190
|
+
t.start()
|
|
191
|
+
t.join()
|
|
192
|
+
|
|
193
|
+
if "error" in error_holder:
|
|
194
|
+
raise error_holder["error"]
|
|
195
|
+
|
|
196
|
+
result = result_holder.get("result")
|
|
197
|
+
else:
|
|
198
|
+
result = asyncio.run(run_subprocedure())
|
|
157
199
|
|
|
158
200
|
# Extract result from execution response
|
|
159
201
|
if result.get("success"):
|
|
@@ -469,41 +511,54 @@ class ProcedurePrimitive:
|
|
|
469
511
|
Raises:
|
|
470
512
|
FileNotFoundError: If procedure file not found
|
|
471
513
|
"""
|
|
472
|
-
import os
|
|
473
514
|
from pathlib import Path
|
|
474
515
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
516
|
+
search_paths: list[Path] = []
|
|
517
|
+
seen: set[Path] = set()
|
|
518
|
+
|
|
519
|
+
def add_path(path: Path) -> None:
|
|
520
|
+
normalized = path.resolve() if path.is_absolute() else path
|
|
521
|
+
if normalized in seen:
|
|
522
|
+
return
|
|
523
|
+
seen.add(normalized)
|
|
524
|
+
search_paths.append(path)
|
|
525
|
+
|
|
526
|
+
name_path = Path(name)
|
|
527
|
+
|
|
528
|
+
def add_candidates(base: Path | None, rel: Path) -> None:
|
|
529
|
+
candidate = (base / rel) if base is not None else rel
|
|
530
|
+
add_path(candidate)
|
|
531
|
+
if candidate.suffix != ".tac":
|
|
532
|
+
add_path(Path(str(candidate) + ".tac"))
|
|
533
|
+
|
|
534
|
+
# Absolute path: try as-is.
|
|
535
|
+
if name_path.is_absolute():
|
|
536
|
+
add_candidates(None, name_path)
|
|
537
|
+
else:
|
|
538
|
+
# Relative to current working directory (CLI usage).
|
|
539
|
+
add_candidates(None, name_path)
|
|
540
|
+
|
|
541
|
+
# Relative to the current .tac file directory and its parents (BDD/temp cwd usage).
|
|
542
|
+
current_tac_file = getattr(self.execution_context, "current_tac_file", None)
|
|
543
|
+
if current_tac_file:
|
|
544
|
+
current_dir = Path(current_tac_file).parent
|
|
545
|
+
add_candidates(current_dir, name_path)
|
|
546
|
+
|
|
547
|
+
# Also try resolving from parent directories (helps when callers pass paths
|
|
548
|
+
# relative to project root, but cwd is not the project root).
|
|
549
|
+
for parent in list(current_dir.parents)[:5]:
|
|
550
|
+
add_candidates(parent, name_path)
|
|
551
|
+
|
|
552
|
+
# Fallback: examples directory relative to repo root in common layouts.
|
|
553
|
+
add_candidates(None, Path("examples") / name_path)
|
|
502
554
|
|
|
503
555
|
for path in search_paths:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return
|
|
508
|
-
|
|
509
|
-
|
|
556
|
+
try:
|
|
557
|
+
if path.exists() and path.is_file():
|
|
558
|
+
logger.debug(f"Loading procedure from: {path}")
|
|
559
|
+
return path.read_text()
|
|
560
|
+
except Exception:
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
searched = [str(p) for p in search_paths]
|
|
564
|
+
raise FileNotFoundError(f"Procedure '{name}' not found. Searched: {searched}")
|
tactus/primitives/tool.py
CHANGED
|
@@ -22,8 +22,6 @@ class ToolCall:
|
|
|
22
22
|
|
|
23
23
|
def __init__(self, name: str, args: Dict[str, Any], result: Any):
|
|
24
24
|
self.name = name
|
|
25
|
-
# Backward/compatibility alias used by some callers
|
|
26
|
-
self.tool_name = name
|
|
27
25
|
self.args = args
|
|
28
26
|
self.result = result
|
|
29
27
|
self.timestamp = None # Could add timestamp tracking
|
tactus/protocols/__init__.py
CHANGED
|
@@ -13,10 +13,6 @@ from tactus.protocols.models import (
|
|
|
13
13
|
ChatMessage,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
# Shared usage/cost + standard result
|
|
17
|
-
from tactus.protocols.cost import UsageStats, CostStats
|
|
18
|
-
from tactus.protocols.result import TactusResult
|
|
19
|
-
|
|
20
16
|
# Protocols
|
|
21
17
|
from tactus.protocols.storage import StorageBackend
|
|
22
18
|
from tactus.protocols.hitl import HITLHandler
|
|
@@ -32,9 +28,6 @@ __all__ = [
|
|
|
32
28
|
"HITLRequest",
|
|
33
29
|
"HITLResponse",
|
|
34
30
|
"ChatMessage",
|
|
35
|
-
"UsageStats",
|
|
36
|
-
"CostStats",
|
|
37
|
-
"TactusResult",
|
|
38
31
|
# Protocols
|
|
39
32
|
"StorageBackend",
|
|
40
33
|
"HITLHandler",
|
tactus/protocols/log_handler.py
CHANGED
|
@@ -6,7 +6,7 @@ Implementations can render logs differently (CLI with Rich, IDE with React, etc.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from typing import Protocol, Union
|
|
9
|
-
from tactus.protocols.models import
|
|
9
|
+
from tactus.protocols.models import LogEvent, ExecutionSummaryEvent
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class LogHandler(Protocol):
|
|
@@ -17,7 +17,7 @@ class LogHandler(Protocol):
|
|
|
17
17
|
appropriately for different environments (CLI, IDE, API, etc.).
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
def log(self, event: Union[LogEvent, ExecutionSummaryEvent
|
|
20
|
+
def log(self, event: Union[LogEvent, ExecutionSummaryEvent]) -> None:
|
|
21
21
|
"""
|
|
22
22
|
Handle a log or summary event.
|
|
23
23
|
|
tactus/protocols/models.py
CHANGED
|
@@ -203,7 +203,7 @@ class CostEvent(BaseModel):
|
|
|
203
203
|
|
|
204
204
|
# Response data (new field)
|
|
205
205
|
response_data: Optional[Dict[str, Any]] = Field(
|
|
206
|
-
None, description="Agent's response data (extracted from result.
|
|
206
|
+
None, description="Agent's response data (extracted from result.data)"
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
model_config = {"arbitrary_types_allowed": True}
|
tactus/sandbox/config.py
CHANGED
|
@@ -26,12 +26,12 @@ class SandboxConfig(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
# Core settings
|
|
28
28
|
# Security model:
|
|
29
|
-
# - enabled=None (default): Sandbox
|
|
29
|
+
# - enabled=None (default): Sandbox AUTO (use if available; otherwise run without isolation)
|
|
30
30
|
# - enabled=True: Sandbox REQUIRED, error if Docker unavailable
|
|
31
31
|
# - enabled=False: Sandbox explicitly disabled (security risk acknowledged)
|
|
32
32
|
enabled: Optional[bool] = Field(
|
|
33
33
|
default=None,
|
|
34
|
-
description="Enable sandbox mode. None
|
|
34
|
+
description="Enable sandbox mode. None=auto, True=required, False=disabled",
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
# Docker image settings
|
|
@@ -60,10 +60,38 @@ class SandboxConfig(BaseModel):
|
|
|
60
60
|
|
|
61
61
|
# Network mode
|
|
62
62
|
network: str = Field(
|
|
63
|
-
default="
|
|
63
|
+
default="none",
|
|
64
64
|
description="Docker network mode (bridge allows outbound, none blocks all)",
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
# Broker transport (how the secretless runtime reaches the host broker)
|
|
68
|
+
# - stdio: local Docker MVP (works on Docker Desktop with --network none)
|
|
69
|
+
# - tcp/tls: remote-mode spike (for K8s/cloud; requires container networking)
|
|
70
|
+
broker_transport: str = Field(
|
|
71
|
+
default="stdio",
|
|
72
|
+
description="Broker transport for the runtime container: stdio, tcp, or tls",
|
|
73
|
+
)
|
|
74
|
+
broker_host: str = Field(
|
|
75
|
+
default="host.docker.internal",
|
|
76
|
+
description="Broker hostname for tcp/tls (as seen from inside the container)",
|
|
77
|
+
)
|
|
78
|
+
broker_bind_host: str = Field(
|
|
79
|
+
default="0.0.0.0",
|
|
80
|
+
description="Bind address for the host-side broker server in tcp/tls modes",
|
|
81
|
+
)
|
|
82
|
+
broker_port: int = Field(
|
|
83
|
+
default=0,
|
|
84
|
+
description="Port for the host-side broker server in tcp/tls modes (0=auto)",
|
|
85
|
+
)
|
|
86
|
+
broker_tls_cert_file: Optional[str] = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description="TLS certificate file for broker (PEM). Required when broker_transport='tls'",
|
|
89
|
+
)
|
|
90
|
+
broker_tls_key_file: Optional[str] = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description="TLS private key file for broker (PEM). Required when broker_transport='tls'",
|
|
93
|
+
)
|
|
94
|
+
|
|
67
95
|
# Resource limits
|
|
68
96
|
limits: SandboxLimits = Field(
|
|
69
97
|
default_factory=SandboxLimits,
|
|
@@ -109,9 +137,9 @@ class SandboxConfig(BaseModel):
|
|
|
109
137
|
|
|
110
138
|
Returns:
|
|
111
139
|
True if Docker unavailability should be a fatal error.
|
|
112
|
-
This is True
|
|
140
|
+
This is True only when the user explicitly requires the sandbox (enabled=True).
|
|
113
141
|
"""
|
|
114
|
-
return
|
|
142
|
+
return self.enabled is True
|
|
115
143
|
|
|
116
144
|
model_config = {"arbitrary_types_allowed": True}
|
|
117
145
|
|