agentium-tracer 0.1.0__tar.gz
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.
- agentium_tracer-0.1.0/PKG-INFO +6 -0
- agentium_tracer-0.1.0/agent_runtime_layer/__init__.py +16 -0
- agentium_tracer-0.1.0/agent_runtime_layer/budget.py +195 -0
- agentium_tracer-0.1.0/agent_runtime_layer/capture.py +246 -0
- agentium_tracer-0.1.0/agent_runtime_layer/cli.py +274 -0
- agentium_tracer-0.1.0/agent_runtime_layer/client.py +42 -0
- agentium_tracer-0.1.0/agent_runtime_layer/integrations/__init__.py +1 -0
- agentium_tracer-0.1.0/agent_runtime_layer/integrations/aider.py +454 -0
- agentium_tracer-0.1.0/agent_runtime_layer/integrations/claude_code.py +307 -0
- agentium_tracer-0.1.0/agent_runtime_layer/integrations/codex.py +1015 -0
- agentium_tracer-0.1.0/agent_runtime_layer/integrations/cursor_agent.py +220 -0
- agentium_tracer-0.1.0/agent_runtime_layer/otel.py +148 -0
- agentium_tracer-0.1.0/agent_runtime_layer/proxy.py +231 -0
- agentium_tracer-0.1.0/agent_runtime_layer/redaction.py +35 -0
- agentium_tracer-0.1.0/agent_runtime_layer/trace.py +30 -0
- agentium_tracer-0.1.0/agent_runtime_layer/tracer.py +546 -0
- agentium_tracer-0.1.0/agentium_tracer.egg-info/PKG-INFO +6 -0
- agentium_tracer-0.1.0/agentium_tracer.egg-info/SOURCES.txt +28 -0
- agentium_tracer-0.1.0/agentium_tracer.egg-info/dependency_links.txt +1 -0
- agentium_tracer-0.1.0/agentium_tracer.egg-info/entry_points.txt +2 -0
- agentium_tracer-0.1.0/agentium_tracer.egg-info/top_level.txt +1 -0
- agentium_tracer-0.1.0/pyproject.toml +13 -0
- agentium_tracer-0.1.0/setup.cfg +4 -0
- agentium_tracer-0.1.0/tests/test_aider_integration.py +102 -0
- agentium_tracer-0.1.0/tests/test_capture.py +73 -0
- agentium_tracer-0.1.0/tests/test_claude_code_integration.py +122 -0
- agentium_tracer-0.1.0/tests/test_codex_integration.py +346 -0
- agentium_tracer-0.1.0/tests/test_cursor_integration.py +77 -0
- agentium_tracer-0.1.0/tests/test_otel.py +22 -0
- agentium_tracer-0.1.0/tests/test_tracer.py +89 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from agent_runtime_layer.capture import capture_command
|
|
2
|
+
from agent_runtime_layer.client import AgentRuntimeClient
|
|
3
|
+
from agent_runtime_layer.integrations.aider import capture_aider
|
|
4
|
+
from agent_runtime_layer.trace import TraceEvent
|
|
5
|
+
from agent_runtime_layer.tracer import AgentRuntimeTracer, context_hash, estimate_cost, prompt_hash
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentRuntimeClient",
|
|
9
|
+
"TraceEvent",
|
|
10
|
+
"capture_command",
|
|
11
|
+
"capture_aider",
|
|
12
|
+
"AgentRuntimeTracer",
|
|
13
|
+
"context_hash",
|
|
14
|
+
"estimate_cost",
|
|
15
|
+
"prompt_hash",
|
|
16
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Phase 1.8: Budget Governor — session cost and retry state tracker.
|
|
2
|
+
|
|
3
|
+
Reads .agentium/config.yaml and enforces per-run budget caps and retry limits.
|
|
4
|
+
Used by Claude Code and Codex hook handlers to block runaway runs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_CONFIG = {
|
|
19
|
+
"max_cost_per_run": 0.10,
|
|
20
|
+
"max_retries_per_task": 5,
|
|
21
|
+
"alert_threshold": 0.05,
|
|
22
|
+
"token_limit_per_call": 200000,
|
|
23
|
+
"enabled": True,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BudgetState:
|
|
29
|
+
session_id: str
|
|
30
|
+
task_id: str | None = None
|
|
31
|
+
cost: float = 0.0
|
|
32
|
+
retries: int = 0
|
|
33
|
+
blocked: bool = False
|
|
34
|
+
block_reason: str = ""
|
|
35
|
+
started_at: float = field(default_factory=time.time)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BudgetGovernor:
|
|
39
|
+
"""Session-scoped budget governor. One instance per agent session."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
repo_path: Path | str = ".",
|
|
44
|
+
base_url: str = "http://localhost:8000/api",
|
|
45
|
+
session_id: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.repo_path = Path(repo_path)
|
|
48
|
+
self.base_url = base_url
|
|
49
|
+
self.session_id = session_id or f"bsess_{uuid4().hex[:12]}"
|
|
50
|
+
self._config = self._load_config()
|
|
51
|
+
self._state = BudgetState(session_id=self.session_id)
|
|
52
|
+
|
|
53
|
+
def _load_config(self) -> dict[str, Any]:
|
|
54
|
+
"""Load .agentium/config.yaml or fall back to defaults."""
|
|
55
|
+
config_path = self.repo_path / ".agentium" / "config.yaml"
|
|
56
|
+
if config_path.exists():
|
|
57
|
+
try:
|
|
58
|
+
import yaml # type: ignore[import]
|
|
59
|
+
with open(config_path, encoding="utf-8") as f:
|
|
60
|
+
loaded = yaml.safe_load(f) or {}
|
|
61
|
+
return {**DEFAULT_CONFIG, **loaded}
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
# Try JSON fallback
|
|
65
|
+
json_path = self.repo_path / ".agentium" / "config.json"
|
|
66
|
+
if json_path.exists():
|
|
67
|
+
try:
|
|
68
|
+
with open(json_path, encoding="utf-8") as f:
|
|
69
|
+
loaded = json.load(f)
|
|
70
|
+
return {**DEFAULT_CONFIG, **loaded}
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return dict(DEFAULT_CONFIG)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def enabled(self) -> bool:
|
|
77
|
+
return bool(self._config.get("enabled", True))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def max_cost(self) -> float:
|
|
81
|
+
return float(self._config.get("max_cost_per_run", 0.10))
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def max_retries(self) -> int:
|
|
85
|
+
return int(self._config.get("max_retries_per_task", 5))
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def alert_threshold(self) -> float:
|
|
89
|
+
return float(self._config.get("alert_threshold", 0.05))
|
|
90
|
+
|
|
91
|
+
def set_task(self, task_id: str) -> None:
|
|
92
|
+
self._state.task_id = task_id
|
|
93
|
+
|
|
94
|
+
def add_cost(self, cost_dollars: float) -> None:
|
|
95
|
+
self._state.cost += cost_dollars
|
|
96
|
+
|
|
97
|
+
def increment_retry(self) -> None:
|
|
98
|
+
self._state.retries += 1
|
|
99
|
+
|
|
100
|
+
def check(self) -> tuple[bool, str]:
|
|
101
|
+
"""Check if current session is within budget and retry limits.
|
|
102
|
+
Returns (allowed: bool, reason: str).
|
|
103
|
+
"""
|
|
104
|
+
if not self.enabled:
|
|
105
|
+
return True, ""
|
|
106
|
+
|
|
107
|
+
if self._state.cost >= self.max_cost:
|
|
108
|
+
reason = (
|
|
109
|
+
f"Budget cap reached: ${self._state.cost:.4f} >= ${self.max_cost:.4f}. "
|
|
110
|
+
f"Run terminated by Agentium Budget Governor."
|
|
111
|
+
)
|
|
112
|
+
self._state.blocked = True
|
|
113
|
+
self._state.block_reason = reason
|
|
114
|
+
self._report_block("budget_cap", reason)
|
|
115
|
+
return False, reason
|
|
116
|
+
|
|
117
|
+
if self._state.retries >= self.max_retries:
|
|
118
|
+
reason = (
|
|
119
|
+
f"Retry limit reached: {self._state.retries} >= {self.max_retries} retries. "
|
|
120
|
+
f"Run terminated by Agentium Budget Governor."
|
|
121
|
+
)
|
|
122
|
+
self._state.blocked = True
|
|
123
|
+
self._state.block_reason = reason
|
|
124
|
+
self._report_block("retry_limit", reason)
|
|
125
|
+
return False, reason
|
|
126
|
+
|
|
127
|
+
if self._state.cost >= self.alert_threshold and self._state.cost < self.max_cost:
|
|
128
|
+
# Alert but don't block
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
return True, ""
|
|
132
|
+
|
|
133
|
+
def _report_block(self, event_type: str, reason: str) -> None:
|
|
134
|
+
"""Report a budget block event to the backend API."""
|
|
135
|
+
try:
|
|
136
|
+
import urllib.request
|
|
137
|
+
payload = json.dumps({
|
|
138
|
+
"event_id": f"bev_{uuid4().hex[:12]}",
|
|
139
|
+
"session_id": self.session_id,
|
|
140
|
+
"task_id": self._state.task_id,
|
|
141
|
+
"event_type": event_type,
|
|
142
|
+
"reason": reason,
|
|
143
|
+
"cost_at_block": self._state.cost,
|
|
144
|
+
"retries_at_block": self._state.retries,
|
|
145
|
+
"budget_limit": self.max_cost,
|
|
146
|
+
"retry_limit": self.max_retries,
|
|
147
|
+
}).encode("utf-8")
|
|
148
|
+
req = urllib.request.Request(
|
|
149
|
+
f"{self.base_url}/budget/events",
|
|
150
|
+
data=payload,
|
|
151
|
+
headers={"Content-Type": "application/json"},
|
|
152
|
+
method="POST",
|
|
153
|
+
)
|
|
154
|
+
urllib.request.urlopen(req, timeout=2)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass # Never block the agent because of reporting failures
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def current_cost(self) -> float:
|
|
160
|
+
return self._state.cost
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def current_retries(self) -> int:
|
|
164
|
+
return self._state.retries
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def summary(self) -> dict[str, Any]:
|
|
168
|
+
return {
|
|
169
|
+
"session_id": self.session_id,
|
|
170
|
+
"cost": self._state.cost,
|
|
171
|
+
"retries": self._state.retries,
|
|
172
|
+
"blocked": self._state.blocked,
|
|
173
|
+
"block_reason": self._state.block_reason,
|
|
174
|
+
"max_cost": self.max_cost,
|
|
175
|
+
"max_retries": self.max_retries,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_default_config(repo_path: Path | str = ".") -> Path:
|
|
180
|
+
"""Write a default .agentium/config.yaml to the given repo path."""
|
|
181
|
+
config_dir = Path(repo_path) / ".agentium"
|
|
182
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
config_path = config_dir / "config.yaml"
|
|
184
|
+
if not config_path.exists():
|
|
185
|
+
config_path.write_text(
|
|
186
|
+
"# Agentium Phase 1.8 Budget Governor configuration\n"
|
|
187
|
+
"# All settings apply per agent session.\n\n"
|
|
188
|
+
"max_cost_per_run: 0.10 # Stop run if cost exceeds this (dollars)\n"
|
|
189
|
+
"max_retries_per_task: 5 # Stop run after this many retries\n"
|
|
190
|
+
"alert_threshold: 0.05 # Log a warning at this cost (dollars)\n"
|
|
191
|
+
"token_limit_per_call: 200000 # Warn if a single call exceeds this token count\n"
|
|
192
|
+
"enabled: true # Set to false to disable all budget enforcement\n",
|
|
193
|
+
encoding="utf-8",
|
|
194
|
+
)
|
|
195
|
+
return config_path
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from agent_runtime_layer.client import AgentRuntimeClient
|
|
10
|
+
from agent_runtime_layer.redaction import redact_text, redact_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_TRACE_DIR = Path(".agent-runtime") / "traces"
|
|
14
|
+
SUMMARY_LIMIT = 4000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def utc_now() -> str:
|
|
18
|
+
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def summarize_stream(value: str, limit: int = SUMMARY_LIMIT) -> str:
|
|
22
|
+
redacted = redact_text(value)
|
|
23
|
+
if len(redacted) <= limit:
|
|
24
|
+
return redacted
|
|
25
|
+
omitted = len(redacted) - limit
|
|
26
|
+
return f"{redacted[:limit]}\n...[truncated {omitted} chars]"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def command_display(command: list[str]) -> str:
|
|
30
|
+
return " ".join(command)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def safe_git_diff_summary() -> str | None:
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["git", "diff", "--stat", "--no-ext-diff"],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=10,
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
except (OSError, subprocess.SubprocessError):
|
|
43
|
+
return None
|
|
44
|
+
output = (result.stdout or result.stderr or "").strip()
|
|
45
|
+
return summarize_stream(output, 2000) if output else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TraceCaptureResult:
|
|
50
|
+
task_id: str
|
|
51
|
+
trace_path: Path
|
|
52
|
+
exit_code: int
|
|
53
|
+
uploaded: bool = False
|
|
54
|
+
upload_response: dict | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LocalTraceCapture:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
command: list[str],
|
|
62
|
+
project_id: str = "default",
|
|
63
|
+
trace_dir: Path = DEFAULT_TRACE_DIR,
|
|
64
|
+
capture_full_logs: bool = False,
|
|
65
|
+
capture_diff: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
if not command:
|
|
68
|
+
raise ValueError("A command is required after --")
|
|
69
|
+
self.name = name
|
|
70
|
+
self.command = command
|
|
71
|
+
self.project_id = project_id
|
|
72
|
+
self.trace_dir = trace_dir
|
|
73
|
+
self.capture_full_logs = capture_full_logs
|
|
74
|
+
self.capture_diff = capture_diff
|
|
75
|
+
self.task_id = f"task_local_{uuid4().hex[:12]}"
|
|
76
|
+
self.task_span_id = f"span_task_{uuid4().hex[:8]}"
|
|
77
|
+
self.tool_span_id = f"span_tool_{uuid4().hex[:8]}"
|
|
78
|
+
|
|
79
|
+
def event(
|
|
80
|
+
self,
|
|
81
|
+
event_type: str,
|
|
82
|
+
span_id: str,
|
|
83
|
+
name: str,
|
|
84
|
+
attributes: dict | None = None,
|
|
85
|
+
payload: dict | None = None,
|
|
86
|
+
parent_span_id: str | None = None,
|
|
87
|
+
timestamp: str | None = None,
|
|
88
|
+
) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"event_id": f"evt_{uuid4().hex[:12]}",
|
|
91
|
+
"task_id": self.task_id,
|
|
92
|
+
"timestamp": timestamp or utc_now(),
|
|
93
|
+
"event_type": event_type,
|
|
94
|
+
"span_id": span_id,
|
|
95
|
+
"parent_span_id": parent_span_id,
|
|
96
|
+
"name": name,
|
|
97
|
+
"attributes": redact_value(attributes or {}),
|
|
98
|
+
"payload": redact_value(payload or {}),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def run(self) -> tuple[dict, int]:
|
|
102
|
+
started_at = utc_now()
|
|
103
|
+
start_time = time.perf_counter()
|
|
104
|
+
display = command_display(self.command)
|
|
105
|
+
events = [
|
|
106
|
+
self.event(
|
|
107
|
+
"task_start",
|
|
108
|
+
self.task_span_id,
|
|
109
|
+
"task_start",
|
|
110
|
+
payload={
|
|
111
|
+
"goal": self.name,
|
|
112
|
+
"agent_type": "local_command",
|
|
113
|
+
"latency_slo_seconds": None,
|
|
114
|
+
},
|
|
115
|
+
timestamp=started_at,
|
|
116
|
+
),
|
|
117
|
+
self.event(
|
|
118
|
+
"tool_call_start",
|
|
119
|
+
self.tool_span_id,
|
|
120
|
+
"local_command",
|
|
121
|
+
attributes={
|
|
122
|
+
"tool_name": "terminal",
|
|
123
|
+
"command": display,
|
|
124
|
+
"risk_level": "user_provided",
|
|
125
|
+
},
|
|
126
|
+
parent_span_id=self.task_span_id,
|
|
127
|
+
timestamp=started_at,
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
completed = subprocess.run(
|
|
132
|
+
self.command,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
check=False,
|
|
136
|
+
)
|
|
137
|
+
ended_at = utc_now()
|
|
138
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
139
|
+
status = "success" if completed.returncode == 0 else "failed"
|
|
140
|
+
stdout_preview = summarize_stream(completed.stdout or "")
|
|
141
|
+
stderr_preview = summarize_stream(completed.stderr or "")
|
|
142
|
+
terminal_payload = {
|
|
143
|
+
"stdout_preview": stdout_preview,
|
|
144
|
+
"stderr_preview": stderr_preview,
|
|
145
|
+
}
|
|
146
|
+
if self.capture_full_logs:
|
|
147
|
+
terminal_payload["stdout"] = redact_text(completed.stdout or "")
|
|
148
|
+
terminal_payload["stderr"] = redact_text(completed.stderr or "")
|
|
149
|
+
if self.capture_diff:
|
|
150
|
+
diff_summary = safe_git_diff_summary()
|
|
151
|
+
if diff_summary:
|
|
152
|
+
terminal_payload["git_diff_summary"] = diff_summary
|
|
153
|
+
|
|
154
|
+
events.extend(
|
|
155
|
+
[
|
|
156
|
+
self.event(
|
|
157
|
+
"terminal_event",
|
|
158
|
+
f"span_terminal_{uuid4().hex[:8]}",
|
|
159
|
+
"local_command_output",
|
|
160
|
+
attributes={
|
|
161
|
+
"command": display,
|
|
162
|
+
"duration_ms": duration_ms,
|
|
163
|
+
"exit_code": completed.returncode,
|
|
164
|
+
"stdout_preview": stdout_preview,
|
|
165
|
+
"stderr_preview": stderr_preview,
|
|
166
|
+
},
|
|
167
|
+
payload=terminal_payload,
|
|
168
|
+
parent_span_id=self.tool_span_id,
|
|
169
|
+
timestamp=ended_at,
|
|
170
|
+
),
|
|
171
|
+
self.event(
|
|
172
|
+
"tool_call_end",
|
|
173
|
+
self.tool_span_id,
|
|
174
|
+
"local_command_end",
|
|
175
|
+
attributes={
|
|
176
|
+
"latency_ms": duration_ms,
|
|
177
|
+
"status": status,
|
|
178
|
+
"exit_code": completed.returncode,
|
|
179
|
+
},
|
|
180
|
+
parent_span_id=self.task_span_id,
|
|
181
|
+
timestamp=ended_at,
|
|
182
|
+
),
|
|
183
|
+
self.event(
|
|
184
|
+
"task_end",
|
|
185
|
+
self.task_span_id,
|
|
186
|
+
"task_end",
|
|
187
|
+
payload={
|
|
188
|
+
"status": "completed" if completed.returncode == 0 else "failed",
|
|
189
|
+
"summary": f"Command exited with code {completed.returncode}.",
|
|
190
|
+
},
|
|
191
|
+
timestamp=ended_at,
|
|
192
|
+
),
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
trace = {
|
|
197
|
+
"project_id": self.project_id,
|
|
198
|
+
"task": {
|
|
199
|
+
"task_id": self.task_id,
|
|
200
|
+
"project_id": self.project_id,
|
|
201
|
+
"goal": self.name,
|
|
202
|
+
"agent_type": "local_command",
|
|
203
|
+
"budget_dollars": None,
|
|
204
|
+
"latency_slo_seconds": None,
|
|
205
|
+
},
|
|
206
|
+
"events": events,
|
|
207
|
+
}
|
|
208
|
+
return trace, completed.returncode
|
|
209
|
+
|
|
210
|
+
def write(self, trace: dict) -> Path:
|
|
211
|
+
self.trace_dir.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
trace_path = self.trace_dir / f"{self.task_id}.json"
|
|
213
|
+
trace_path.write_text(json.dumps(trace, indent=2), encoding="utf-8")
|
|
214
|
+
return trace_path
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def capture_command(
|
|
218
|
+
name: str,
|
|
219
|
+
command: list[str],
|
|
220
|
+
project_id: str = "default",
|
|
221
|
+
trace_dir: Path = DEFAULT_TRACE_DIR,
|
|
222
|
+
capture_full_logs: bool = False,
|
|
223
|
+
capture_diff: bool = False,
|
|
224
|
+
upload: bool = False,
|
|
225
|
+
base_url: str = "http://localhost:8000/api",
|
|
226
|
+
) -> TraceCaptureResult:
|
|
227
|
+
capture = LocalTraceCapture(
|
|
228
|
+
name=name,
|
|
229
|
+
command=command,
|
|
230
|
+
project_id=project_id,
|
|
231
|
+
trace_dir=trace_dir,
|
|
232
|
+
capture_full_logs=capture_full_logs,
|
|
233
|
+
capture_diff=capture_diff,
|
|
234
|
+
)
|
|
235
|
+
trace, exit_code = capture.run()
|
|
236
|
+
trace_path = capture.write(trace)
|
|
237
|
+
upload_response = None
|
|
238
|
+
if upload:
|
|
239
|
+
upload_response = AgentRuntimeClient(base_url).import_trace_file(trace_path)
|
|
240
|
+
return TraceCaptureResult(
|
|
241
|
+
task_id=capture.task_id,
|
|
242
|
+
trace_path=trace_path,
|
|
243
|
+
exit_code=exit_code,
|
|
244
|
+
uploaded=upload,
|
|
245
|
+
upload_response=upload_response,
|
|
246
|
+
)
|