sprucelab 0.1.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.
- sprucelab/__init__.py +19 -0
- sprucelab/cli/__init__.py +3 -0
- sprucelab/cli/__main__.py +5 -0
- sprucelab/cli/_auth.py +44 -0
- sprucelab/cli/_errors.py +174 -0
- sprucelab/cli/api_client.py +226 -0
- sprucelab/cli/app.py +547 -0
- sprucelab/cli/capabilities.py +159 -0
- sprucelab/cli/cde/__init__.py +1 -0
- sprucelab/cli/claims.py +330 -0
- sprucelab/cli/config.py +99 -0
- sprucelab/cli/dev.py +441 -0
- sprucelab/cli/embed.py +211 -0
- sprucelab/cli/executor.py +343 -0
- sprucelab/cli/files.py +915 -0
- sprucelab/cli/issues.py +271 -0
- sprucelab/cli/log.py +160 -0
- sprucelab/cli/models.py +102 -0
- sprucelab/cli/projects.py +839 -0
- sprucelab/cli/scopes.py +148 -0
- sprucelab/cli/scripts.py +146 -0
- sprucelab/cli/steps/__init__.py +1 -0
- sprucelab/cli/types.py +319 -0
- sprucelab/cli/utils/__init__.py +1 -0
- sprucelab/cli/verify.py +100 -0
- sprucelab/cli/webhooks.py +524 -0
- sprucelab/mcp/__init__.py +8 -0
- sprucelab/mcp/__main__.py +6 -0
- sprucelab/mcp/client.py +102 -0
- sprucelab/mcp/server.py +653 -0
- sprucelab-0.1.0.dist-info/METADATA +188 -0
- sprucelab-0.1.0.dist-info/RECORD +34 -0
- sprucelab-0.1.0.dist-info/WHEEL +4 -0
- sprucelab-0.1.0.dist-info/entry_points.txt +3 -0
sprucelab/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""sprucelab — agent-first surface for the Sprucelab platform.
|
|
2
|
+
|
|
3
|
+
Ships two transports — both designed for agents to drive fast, both
|
|
4
|
+
usable by humans at the terminal when they want to:
|
|
5
|
+
|
|
6
|
+
- ``spruce`` — typed CLI. Terminal-resident agents (Claude Code,
|
|
7
|
+
Aider, anything that drives a shell) bash-call ``spruce …`` and
|
|
8
|
+
read structured JSON / SSE off stdout. Humans get the same
|
|
9
|
+
commands with ``--help`` and pretty rendering.
|
|
10
|
+
- ``sprucelab-mcp`` — MCP server over stdio. Host-resident agents
|
|
11
|
+
(Claude Desktop, Cursor, Continue) launch this as a subprocess
|
|
12
|
+
and call the same operations as typed tool calls.
|
|
13
|
+
|
|
14
|
+
Both wrap the same public HTTP surface advertised at
|
|
15
|
+
``/api/capabilities/``. The web app at sprucelab.io is the
|
|
16
|
+
human-first surface; these are the agent-first ones.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
sprucelab/cli/_auth.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single source of truth for resolving the CLI's API token.
|
|
3
|
+
|
|
4
|
+
Resolution order (first hit wins):
|
|
5
|
+
1. Explicit ``--token`` flag passed to a command.
|
|
6
|
+
2. ``SPRUCELAB_ADMIN_TOKEN`` environment variable (CI/scripts path).
|
|
7
|
+
3. ``keyring`` (set by ``spruce auth register --token ...``).
|
|
8
|
+
|
|
9
|
+
This unifies the previously fragmented behavior where command modules
|
|
10
|
+
(``models``, ``verify``, ``types``, …) read only the env var while the
|
|
11
|
+
``auth``/``capabilities`` modules read only the keyring. After this
|
|
12
|
+
helper, every command honors all three paths.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_token(override: Optional[str] = None) -> Optional[str]:
|
|
21
|
+
"""Return the API token to use, or None if nothing is configured."""
|
|
22
|
+
if override:
|
|
23
|
+
return override
|
|
24
|
+
env_token = os.environ.get('SPRUCELAB_ADMIN_TOKEN')
|
|
25
|
+
if env_token:
|
|
26
|
+
return env_token
|
|
27
|
+
try:
|
|
28
|
+
from .config import get_api_key
|
|
29
|
+
kr_token = get_api_key()
|
|
30
|
+
if kr_token:
|
|
31
|
+
return kr_token
|
|
32
|
+
except Exception:
|
|
33
|
+
# Keyring backend missing or locked — fall through to None.
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def auth_headers(override: Optional[str] = None) -> dict:
|
|
39
|
+
"""Standard JSON request headers including Authorization if we have a token."""
|
|
40
|
+
headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
|
|
41
|
+
token = resolve_token(override)
|
|
42
|
+
if token:
|
|
43
|
+
headers['Authorization'] = f'Bearer {token}'
|
|
44
|
+
return headers
|
sprucelab/cli/_errors.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verbatim "Try: ..." next-command hints for CLI errors.
|
|
3
|
+
|
|
4
|
+
Design rule (`feedback-agent-first-or-die`): error messages should suggest
|
|
5
|
+
the next command verbatim. Agents learn faster from
|
|
6
|
+
``Try: spruce auth register --token <KEY>`` than from a bare
|
|
7
|
+
``401 Unauthorized``.
|
|
8
|
+
|
|
9
|
+
Each command module passes a short ``command_context`` string (e.g.
|
|
10
|
+
``"models list"``, ``"types classify"``, ``"webhooks deliveries"``) so we can
|
|
11
|
+
tailor the suggestion to the noun the user was working with.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Map (command_context_prefix -> noun used in `spruce <noun> list` hints).
|
|
24
|
+
# The first key whose prefix matches command_context wins. Fall back to the
|
|
25
|
+
# raw context string for the 5xx/network case where the hint isn't about a
|
|
26
|
+
# specific noun.
|
|
27
|
+
_NOUN_FOR_404 = {
|
|
28
|
+
'models': 'models list',
|
|
29
|
+
'types': 'types list --model <MODEL_UUID>',
|
|
30
|
+
'verify': 'models list',
|
|
31
|
+
'scripts run': 'scripts list',
|
|
32
|
+
'scripts': 'scripts list',
|
|
33
|
+
'webhooks deliveries': 'webhooks deliveries',
|
|
34
|
+
'webhooks redeliver': 'webhooks deliveries',
|
|
35
|
+
'webhooks test': 'webhooks list',
|
|
36
|
+
'webhooks': 'webhooks list',
|
|
37
|
+
'embed pass revoke': 'embed pass list',
|
|
38
|
+
'embed pass refresh': 'embed pass list',
|
|
39
|
+
'embed': 'embed pass list',
|
|
40
|
+
'files show': 'files list',
|
|
41
|
+
'files download': 'files list',
|
|
42
|
+
'files reprocess': 'files list',
|
|
43
|
+
'files versions': 'files list',
|
|
44
|
+
'files upload': 'files list # confirm the project id',
|
|
45
|
+
'files': 'files list',
|
|
46
|
+
'log': 'log list',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def format_http_error_hint(status: int, command_context: str) -> Optional[str]:
|
|
51
|
+
"""
|
|
52
|
+
Return a verbatim ``Try: ...`` suggestion for ``status`` in ``command_context``.
|
|
53
|
+
|
|
54
|
+
Returns ``None`` when no useful hint applies (e.g. unrecognized status).
|
|
55
|
+
The returned string does NOT include the ``Try: `` prefix or trailing
|
|
56
|
+
newline — caller decides framing (Rich vs JSON).
|
|
57
|
+
"""
|
|
58
|
+
if status in (401, 403):
|
|
59
|
+
return (
|
|
60
|
+
'spruce auth register --token <KEY> '
|
|
61
|
+
'# or: spruce auth status'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if status == 404:
|
|
65
|
+
for prefix, suggestion in _NOUN_FOR_404.items():
|
|
66
|
+
if command_context.startswith(prefix):
|
|
67
|
+
return f'spruce {suggestion} # confirm the ID exists'
|
|
68
|
+
return 'spruce capabilities # confirm the endpoint exists on this backend'
|
|
69
|
+
|
|
70
|
+
if status == 400 or status == 422:
|
|
71
|
+
# Validation errors. Echo the field error then nudge to re-read help.
|
|
72
|
+
return (
|
|
73
|
+
f'spruce {command_context} --help '
|
|
74
|
+
'# field error in payload above; check required flags'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if status >= 500:
|
|
78
|
+
return (
|
|
79
|
+
f'spruce {command_context} --json | jq .body '
|
|
80
|
+
'# inspect the server response in full'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if status == 405:
|
|
84
|
+
return 'spruce capabilities # this action may not be implemented on this backend yet'
|
|
85
|
+
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_request_error_hint() -> str:
|
|
90
|
+
"""
|
|
91
|
+
Hint for connection / DNS / timeout errors (httpx.RequestError).
|
|
92
|
+
|
|
93
|
+
Always returns a non-empty string. Agents/users can't recover without
|
|
94
|
+
pointing at the URL config, so we always offer the same two breadcrumbs.
|
|
95
|
+
"""
|
|
96
|
+
return (
|
|
97
|
+
'spruce config show '
|
|
98
|
+
'# verify api_url; override with $SPRUCE_API_URL or `spruce auth register --url <URL>`'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def print_http_error(
|
|
103
|
+
console,
|
|
104
|
+
err: httpx.HTTPStatusError,
|
|
105
|
+
*,
|
|
106
|
+
json_out: bool,
|
|
107
|
+
command_context: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Pretty-print an httpx.HTTPStatusError with a verbatim next-command hint.
|
|
111
|
+
|
|
112
|
+
Always raises ``typer.Exit(1)``. Caller supplies a Rich Console for human
|
|
113
|
+
output and a short ``command_context`` (e.g. ``"models list"``) so the
|
|
114
|
+
hint can be scoped.
|
|
115
|
+
|
|
116
|
+
JSON shape (preserved from the previous unstructured handlers, plus
|
|
117
|
+
``hint`` and ``body`` keys):
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
"error": "HTTP <status>",
|
|
121
|
+
"status": <status>,
|
|
122
|
+
"body": <parsed JSON body or raw text>,
|
|
123
|
+
"hint": "<verbatim next command>" | null
|
|
124
|
+
}
|
|
125
|
+
"""
|
|
126
|
+
body_text = err.response.text
|
|
127
|
+
parsed = None
|
|
128
|
+
try:
|
|
129
|
+
parsed = err.response.json()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
hint = format_http_error_hint(err.response.status_code, command_context)
|
|
134
|
+
body_payload = parsed if parsed is not None else body_text
|
|
135
|
+
|
|
136
|
+
if json_out:
|
|
137
|
+
payload = {
|
|
138
|
+
'error': f'HTTP {err.response.status_code}',
|
|
139
|
+
'status': err.response.status_code,
|
|
140
|
+
'body': body_payload,
|
|
141
|
+
'hint': hint,
|
|
142
|
+
}
|
|
143
|
+
sys.stdout.write(json.dumps(payload) + '\n')
|
|
144
|
+
else:
|
|
145
|
+
body_pretty = json.dumps(parsed, indent=2) if parsed is not None else body_text
|
|
146
|
+
console.print(f'[red]HTTP {err.response.status_code}[/red]\n{body_pretty}')
|
|
147
|
+
if hint:
|
|
148
|
+
console.print(f'[yellow]Try:[/yellow] [cyan]{hint}[/cyan]')
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def print_request_error(
|
|
153
|
+
console,
|
|
154
|
+
err: httpx.RequestError,
|
|
155
|
+
*,
|
|
156
|
+
json_out: bool,
|
|
157
|
+
command_context: str,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Pretty-print a connection / DNS / timeout error with a config hint.
|
|
161
|
+
|
|
162
|
+
Always raises ``typer.Exit(1)``.
|
|
163
|
+
"""
|
|
164
|
+
hint = format_request_error_hint()
|
|
165
|
+
if json_out:
|
|
166
|
+
sys.stdout.write(json.dumps({
|
|
167
|
+
'error': 'request_failed',
|
|
168
|
+
'detail': str(err),
|
|
169
|
+
'hint': hint,
|
|
170
|
+
}) + '\n')
|
|
171
|
+
else:
|
|
172
|
+
console.print(f'[red]Request failed:[/red] {err}')
|
|
173
|
+
console.print(f'[yellow]Try:[/yellow] [cyan]{hint}[/cyan]')
|
|
174
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""HTTP client for Sprucelab API communication."""
|
|
2
|
+
from typing import Optional, Dict, Any, List
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .config import get_api_url, get_api_key, get_agent_id, get_hostname
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SprucelabClient:
|
|
9
|
+
"""Client for Sprucelab automation API."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, api_url: Optional[str] = None, api_key: Optional[str] = None):
|
|
12
|
+
self.api_url = api_url or get_api_url()
|
|
13
|
+
self.api_key = api_key or get_api_key()
|
|
14
|
+
self.agent_id = get_agent_id()
|
|
15
|
+
self.hostname = get_hostname()
|
|
16
|
+
|
|
17
|
+
def _headers(self) -> Dict[str, str]:
|
|
18
|
+
"""Get request headers with authentication."""
|
|
19
|
+
headers = {"Content-Type": "application/json"}
|
|
20
|
+
if self.api_key:
|
|
21
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
22
|
+
return headers
|
|
23
|
+
|
|
24
|
+
def _url(self, path: str) -> str:
|
|
25
|
+
"""Build full URL for API path."""
|
|
26
|
+
return f"{self.api_url}/api/automation{path}"
|
|
27
|
+
|
|
28
|
+
# Pipeline endpoints
|
|
29
|
+
|
|
30
|
+
def list_pipelines(self) -> List[Dict[str, Any]]:
|
|
31
|
+
"""List all available pipelines."""
|
|
32
|
+
with httpx.Client() as client:
|
|
33
|
+
response = client.get(
|
|
34
|
+
self._url("/pipelines/"),
|
|
35
|
+
headers=self._headers()
|
|
36
|
+
)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response.json()
|
|
39
|
+
|
|
40
|
+
def get_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
41
|
+
"""Get pipeline details."""
|
|
42
|
+
with httpx.Client() as client:
|
|
43
|
+
response = client.get(
|
|
44
|
+
self._url(f"/pipelines/{pipeline_id}/"),
|
|
45
|
+
headers=self._headers()
|
|
46
|
+
)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
return response.json()
|
|
49
|
+
|
|
50
|
+
def trigger_run(
|
|
51
|
+
self,
|
|
52
|
+
pipeline_id: str,
|
|
53
|
+
project_id: Optional[str] = None,
|
|
54
|
+
parameters: Optional[Dict] = None
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""Trigger a pipeline run."""
|
|
57
|
+
data = {
|
|
58
|
+
"triggered_by": f"cli:{self.hostname}"
|
|
59
|
+
}
|
|
60
|
+
if project_id:
|
|
61
|
+
data["project_id"] = project_id
|
|
62
|
+
if parameters:
|
|
63
|
+
data["parameters"] = parameters
|
|
64
|
+
|
|
65
|
+
with httpx.Client() as client:
|
|
66
|
+
response = client.post(
|
|
67
|
+
self._url(f"/pipelines/{pipeline_id}/run/"),
|
|
68
|
+
headers=self._headers(),
|
|
69
|
+
json=data
|
|
70
|
+
)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
return response.json()
|
|
73
|
+
|
|
74
|
+
# Run endpoints
|
|
75
|
+
|
|
76
|
+
def list_runs(
|
|
77
|
+
self,
|
|
78
|
+
pipeline_id: Optional[str] = None,
|
|
79
|
+
project_id: Optional[str] = None,
|
|
80
|
+
status: Optional[str] = None
|
|
81
|
+
) -> List[Dict[str, Any]]:
|
|
82
|
+
"""List pipeline runs."""
|
|
83
|
+
params = {}
|
|
84
|
+
if pipeline_id:
|
|
85
|
+
params["pipeline"] = pipeline_id
|
|
86
|
+
if project_id:
|
|
87
|
+
params["project"] = project_id
|
|
88
|
+
if status:
|
|
89
|
+
params["status"] = status
|
|
90
|
+
|
|
91
|
+
with httpx.Client() as client:
|
|
92
|
+
response = client.get(
|
|
93
|
+
self._url("/runs/"),
|
|
94
|
+
headers=self._headers(),
|
|
95
|
+
params=params
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
return response.json()
|
|
99
|
+
|
|
100
|
+
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
101
|
+
"""Get run details."""
|
|
102
|
+
with httpx.Client() as client:
|
|
103
|
+
response = client.get(
|
|
104
|
+
self._url(f"/runs/{run_id}/"),
|
|
105
|
+
headers=self._headers()
|
|
106
|
+
)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
return response.json()
|
|
109
|
+
|
|
110
|
+
def get_run_logs(self, run_id: str) -> Dict[str, Any]:
|
|
111
|
+
"""Get run logs."""
|
|
112
|
+
with httpx.Client() as client:
|
|
113
|
+
response = client.get(
|
|
114
|
+
self._url(f"/runs/{run_id}/logs/"),
|
|
115
|
+
headers=self._headers()
|
|
116
|
+
)
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
# Agent endpoints
|
|
121
|
+
|
|
122
|
+
def register_agent(self, name: str) -> Dict[str, Any]:
|
|
123
|
+
"""Register this machine as an agent."""
|
|
124
|
+
with httpx.Client() as client:
|
|
125
|
+
response = client.post(
|
|
126
|
+
self._url("/agent/register/"),
|
|
127
|
+
headers=self._headers(),
|
|
128
|
+
json={
|
|
129
|
+
"name": name,
|
|
130
|
+
"hostname": self.hostname,
|
|
131
|
+
"capabilities": ["ifcopenshell", "python"]
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
return response.json()
|
|
136
|
+
|
|
137
|
+
def heartbeat(self) -> bool:
|
|
138
|
+
"""Send agent heartbeat."""
|
|
139
|
+
if not self.agent_id:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
with httpx.Client() as client:
|
|
143
|
+
try:
|
|
144
|
+
response = client.post(
|
|
145
|
+
self._url("/agent/heartbeat/"),
|
|
146
|
+
headers=self._headers(),
|
|
147
|
+
json={
|
|
148
|
+
"agent_id": self.agent_id,
|
|
149
|
+
"hostname": self.hostname
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
return response.status_code == 200
|
|
153
|
+
except Exception:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def poll_jobs(self) -> List[Dict[str, Any]]:
|
|
157
|
+
"""Poll for pending jobs."""
|
|
158
|
+
if not self.agent_id:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
with httpx.Client() as client:
|
|
162
|
+
response = client.get(
|
|
163
|
+
self._url("/agent/jobs/"),
|
|
164
|
+
headers=self._headers(),
|
|
165
|
+
params={"agent_id": self.agent_id}
|
|
166
|
+
)
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
return response.json().get("jobs", [])
|
|
169
|
+
|
|
170
|
+
def claim_job(self, run_id: str) -> bool:
|
|
171
|
+
"""Claim a job for execution."""
|
|
172
|
+
with httpx.Client() as client:
|
|
173
|
+
response = client.post(
|
|
174
|
+
self._url(f"/agent/jobs/{run_id}/claim/"),
|
|
175
|
+
headers=self._headers(),
|
|
176
|
+
json={
|
|
177
|
+
"agent_id": self.agent_id,
|
|
178
|
+
"agent_hostname": self.hostname
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return response.status_code == 200
|
|
182
|
+
|
|
183
|
+
def step_start(self, run_id: str, step_id: str) -> bool:
|
|
184
|
+
"""Mark a step as started."""
|
|
185
|
+
with httpx.Client() as client:
|
|
186
|
+
response = client.post(
|
|
187
|
+
self._url(f"/agent/jobs/{run_id}/step/{step_id}/start/"),
|
|
188
|
+
headers=self._headers()
|
|
189
|
+
)
|
|
190
|
+
return response.status_code == 200
|
|
191
|
+
|
|
192
|
+
def step_complete(
|
|
193
|
+
self,
|
|
194
|
+
run_id: str,
|
|
195
|
+
step_id: str,
|
|
196
|
+
status: str,
|
|
197
|
+
output_log: str = "",
|
|
198
|
+
result_data: Optional[Dict] = None,
|
|
199
|
+
error_message: str = "",
|
|
200
|
+
output_files: Optional[List[str]] = None
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""Mark a step as completed."""
|
|
203
|
+
with httpx.Client() as client:
|
|
204
|
+
response = client.post(
|
|
205
|
+
self._url(f"/agent/jobs/{run_id}/step/{step_id}/complete/"),
|
|
206
|
+
headers=self._headers(),
|
|
207
|
+
json={
|
|
208
|
+
"status": status,
|
|
209
|
+
"output_log": output_log,
|
|
210
|
+
"result_data": result_data or {},
|
|
211
|
+
"error_message": error_message,
|
|
212
|
+
"output_files": output_files or []
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return response.status_code == 200
|
|
216
|
+
|
|
217
|
+
def run_complete(self, run_id: str, error_message: str = "") -> Dict[str, Any]:
|
|
218
|
+
"""Mark a run as completed."""
|
|
219
|
+
with httpx.Client() as client:
|
|
220
|
+
response = client.post(
|
|
221
|
+
self._url(f"/agent/jobs/{run_id}/complete/"),
|
|
222
|
+
headers=self._headers(),
|
|
223
|
+
json={"error_message": error_message}
|
|
224
|
+
)
|
|
225
|
+
response.raise_for_status()
|
|
226
|
+
return response.json()
|