fleet-python 0.2.66b2__py3-none-any.whl → 0.2.105__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.
- examples/export_tasks.py +16 -5
- examples/export_tasks_filtered.py +245 -0
- examples/fetch_tasks.py +230 -0
- examples/import_tasks.py +140 -8
- examples/iterate_verifiers.py +725 -0
- fleet/__init__.py +128 -5
- fleet/_async/__init__.py +27 -3
- fleet/_async/base.py +24 -9
- fleet/_async/client.py +938 -41
- fleet/_async/env/client.py +60 -3
- fleet/_async/instance/client.py +52 -7
- fleet/_async/models.py +15 -0
- fleet/_async/resources/api.py +200 -0
- fleet/_async/resources/sqlite.py +1801 -46
- fleet/_async/tasks.py +122 -25
- fleet/_async/verifiers/bundler.py +22 -21
- fleet/_async/verifiers/verifier.py +25 -19
- fleet/agent/__init__.py +32 -0
- fleet/agent/gemini_cua/Dockerfile +45 -0
- fleet/agent/gemini_cua/__init__.py +10 -0
- fleet/agent/gemini_cua/agent.py +759 -0
- fleet/agent/gemini_cua/mcp/main.py +108 -0
- fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
- fleet/agent/gemini_cua/mcp_server/main.py +105 -0
- fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
- fleet/agent/gemini_cua/requirements.txt +5 -0
- fleet/agent/gemini_cua/start.sh +30 -0
- fleet/agent/orchestrator.py +854 -0
- fleet/agent/types.py +49 -0
- fleet/agent/utils.py +34 -0
- fleet/base.py +34 -9
- fleet/cli.py +1061 -0
- fleet/client.py +1060 -48
- fleet/config.py +1 -1
- fleet/env/__init__.py +16 -0
- fleet/env/client.py +60 -3
- fleet/eval/__init__.py +15 -0
- fleet/eval/uploader.py +231 -0
- fleet/exceptions.py +8 -0
- fleet/instance/client.py +53 -8
- fleet/instance/models.py +1 -0
- fleet/models.py +303 -0
- fleet/proxy/__init__.py +25 -0
- fleet/proxy/proxy.py +453 -0
- fleet/proxy/whitelist.py +244 -0
- fleet/resources/api.py +200 -0
- fleet/resources/sqlite.py +1845 -46
- fleet/tasks.py +113 -20
- fleet/utils/__init__.py +7 -0
- fleet/utils/http_logging.py +178 -0
- fleet/utils/logging.py +13 -0
- fleet/utils/playwright.py +440 -0
- fleet/verifiers/bundler.py +22 -21
- fleet/verifiers/db.py +985 -1
- fleet/verifiers/decorator.py +1 -1
- fleet/verifiers/verifier.py +25 -19
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
- fleet_python-0.2.105.dist-info/RECORD +115 -0
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
- fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
- tests/test_app_method.py +85 -0
- tests/test_expect_exactly.py +4148 -0
- tests/test_expect_only.py +2593 -0
- tests/test_instance_dispatch.py +607 -0
- tests/test_sqlite_resource_dual_mode.py +263 -0
- tests/test_sqlite_shared_memory_behavior.py +117 -0
- fleet_python-0.2.66b2.dist-info/RECORD +0 -81
- tests/test_verifier_security.py +0 -427
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/top_level.txt +0 -0
fleet/client.py
CHANGED
|
@@ -17,11 +17,18 @@
|
|
|
17
17
|
import base64
|
|
18
18
|
import cloudpickle
|
|
19
19
|
import concurrent.futures
|
|
20
|
+
import dataclasses
|
|
20
21
|
import httpx
|
|
21
22
|
import json
|
|
22
23
|
import logging
|
|
23
24
|
import os
|
|
24
|
-
from
|
|
25
|
+
from datetime import date, datetime
|
|
26
|
+
from decimal import Decimal
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import List, Optional, Dict, Any, TYPE_CHECKING, Union
|
|
30
|
+
from urllib.parse import urlparse
|
|
31
|
+
from uuid import UUID
|
|
25
32
|
|
|
26
33
|
from .base import EnvironmentBase, SyncWrapper
|
|
27
34
|
from .models import (
|
|
@@ -35,18 +42,130 @@ from .models import (
|
|
|
35
42
|
TaskRequest,
|
|
36
43
|
TaskResponse,
|
|
37
44
|
TaskUpdateRequest,
|
|
45
|
+
Run,
|
|
46
|
+
HeartbeatResponse,
|
|
47
|
+
JobCreateRequest,
|
|
48
|
+
JobResponse,
|
|
49
|
+
JobListResponse,
|
|
50
|
+
JobCreateResponse,
|
|
51
|
+
JobSessionsResponse,
|
|
52
|
+
SessionTranscriptResponse,
|
|
53
|
+
SessionIngestRequest,
|
|
54
|
+
SessionIngestMessage,
|
|
55
|
+
SessionIngestResponse,
|
|
56
|
+
SessionStatus,
|
|
38
57
|
)
|
|
39
58
|
from .tasks import Task
|
|
40
59
|
|
|
41
60
|
if TYPE_CHECKING:
|
|
42
61
|
from .verifiers import SyncVerifierFunction
|
|
43
62
|
|
|
63
|
+
|
|
64
|
+
def _json_default(x: Any) -> Any:
|
|
65
|
+
"""Default JSON serializer for non-native types."""
|
|
66
|
+
if isinstance(x, (datetime, date)):
|
|
67
|
+
return x.isoformat()
|
|
68
|
+
if isinstance(x, (UUID, Path)):
|
|
69
|
+
return str(x)
|
|
70
|
+
if isinstance(x, Decimal):
|
|
71
|
+
return float(x)
|
|
72
|
+
if isinstance(x, Enum):
|
|
73
|
+
return x.value
|
|
74
|
+
if isinstance(x, bytes):
|
|
75
|
+
return base64.b64encode(x).decode("utf-8")
|
|
76
|
+
if isinstance(x, set):
|
|
77
|
+
return list(x)
|
|
78
|
+
if dataclasses.is_dataclass(x) and not isinstance(x, type):
|
|
79
|
+
return dataclasses.asdict(x)
|
|
80
|
+
# Handle objects with __dict__ (generic objects)
|
|
81
|
+
if hasattr(x, "__dict__"):
|
|
82
|
+
return x.__dict__
|
|
83
|
+
raise TypeError(f"Not JSON serializable: {type(x)}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _to_dict(obj: Any) -> Any:
|
|
87
|
+
"""Convert any object to a JSON-serializable dict/value.
|
|
88
|
+
|
|
89
|
+
Handles:
|
|
90
|
+
- Pydantic v2 models (model_dump)
|
|
91
|
+
- Pydantic v1 models (.dict())
|
|
92
|
+
- dataclasses (asdict)
|
|
93
|
+
- TypedDict (just dict at runtime)
|
|
94
|
+
- Objects with __dict__
|
|
95
|
+
- Primitives pass through
|
|
96
|
+
"""
|
|
97
|
+
if obj is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Pydantic v2
|
|
101
|
+
if hasattr(obj, "model_dump"):
|
|
102
|
+
return obj.model_dump()
|
|
103
|
+
|
|
104
|
+
# Pydantic v1
|
|
105
|
+
if hasattr(obj, "dict") and callable(obj.dict):
|
|
106
|
+
return obj.dict()
|
|
107
|
+
|
|
108
|
+
# dataclass
|
|
109
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
110
|
+
return dataclasses.asdict(obj)
|
|
111
|
+
|
|
112
|
+
# Already a dict or list - recursively convert
|
|
113
|
+
if isinstance(obj, dict):
|
|
114
|
+
return {k: _to_dict(v) for k, v in obj.items()}
|
|
115
|
+
if isinstance(obj, list):
|
|
116
|
+
return [_to_dict(v) for v in obj]
|
|
117
|
+
|
|
118
|
+
# Primitives
|
|
119
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
120
|
+
return obj
|
|
121
|
+
|
|
122
|
+
# bytes -> base64
|
|
123
|
+
if isinstance(obj, bytes):
|
|
124
|
+
return base64.b64encode(obj).decode("utf-8")
|
|
125
|
+
|
|
126
|
+
# datetime/date
|
|
127
|
+
if isinstance(obj, (datetime, date)):
|
|
128
|
+
return obj.isoformat()
|
|
129
|
+
|
|
130
|
+
# UUID, Path
|
|
131
|
+
if isinstance(obj, (UUID, Path)):
|
|
132
|
+
return str(obj)
|
|
133
|
+
|
|
134
|
+
# Enum
|
|
135
|
+
if isinstance(obj, Enum):
|
|
136
|
+
return obj.value
|
|
137
|
+
|
|
138
|
+
# Decimal
|
|
139
|
+
if isinstance(obj, Decimal):
|
|
140
|
+
return float(obj)
|
|
141
|
+
|
|
142
|
+
# set
|
|
143
|
+
if isinstance(obj, set):
|
|
144
|
+
return list(obj)
|
|
145
|
+
|
|
146
|
+
# Generic object with __dict__
|
|
147
|
+
if hasattr(obj, "__dict__"):
|
|
148
|
+
return {k: _to_dict(v) for k, v in obj.__dict__.items() if not k.startswith("_")}
|
|
149
|
+
|
|
150
|
+
# Fallback - try to convert, or return string representation
|
|
151
|
+
try:
|
|
152
|
+
json.dumps(obj)
|
|
153
|
+
return obj
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
return str(obj)
|
|
156
|
+
|
|
157
|
+
|
|
44
158
|
from .instance import (
|
|
45
159
|
InstanceClient,
|
|
46
160
|
ResetRequest,
|
|
47
161
|
ResetResponse,
|
|
48
162
|
ExecuteFunctionResponse,
|
|
49
163
|
)
|
|
164
|
+
from .instance.models import (
|
|
165
|
+
Resource as ResourceModel,
|
|
166
|
+
ResourceType,
|
|
167
|
+
ResourceMode,
|
|
168
|
+
)
|
|
50
169
|
from .config import (
|
|
51
170
|
DEFAULT_MAX_RETRIES,
|
|
52
171
|
DEFAULT_TIMEOUT,
|
|
@@ -59,16 +178,183 @@ from .resources.base import Resource
|
|
|
59
178
|
from .resources.sqlite import SQLiteResource
|
|
60
179
|
from .resources.browser import BrowserResource
|
|
61
180
|
from .resources.mcp import SyncMCPResource
|
|
181
|
+
from .resources.api import APIResource
|
|
62
182
|
|
|
63
183
|
logger = logging.getLogger(__name__)
|
|
64
184
|
|
|
65
185
|
|
|
186
|
+
class Session:
|
|
187
|
+
"""A session for logging agent interactions to Fleet.
|
|
188
|
+
|
|
189
|
+
This provides a simple interface for streaming messages during an agent run.
|
|
190
|
+
Messages are sent one-by-one as they happen.
|
|
191
|
+
|
|
192
|
+
Usage:
|
|
193
|
+
session = fleet.session(job_id=job_id)
|
|
194
|
+
|
|
195
|
+
# Log LLM calls
|
|
196
|
+
session.log(history, response)
|
|
197
|
+
|
|
198
|
+
# Complete when done
|
|
199
|
+
session.complete() # or session.fail()
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
client: "Fleet",
|
|
205
|
+
session_id: Optional[str] = None,
|
|
206
|
+
job_id: Optional[str] = None,
|
|
207
|
+
config: Optional[Any] = None,
|
|
208
|
+
model: Optional[str] = None,
|
|
209
|
+
task_key: Optional[str] = None,
|
|
210
|
+
instance_id: Optional[str] = None,
|
|
211
|
+
):
|
|
212
|
+
self.session_id = session_id
|
|
213
|
+
self.job_id = job_id
|
|
214
|
+
self.config = config
|
|
215
|
+
self.model = model
|
|
216
|
+
self.task_key = task_key
|
|
217
|
+
self.instance_id = instance_id
|
|
218
|
+
self._client = client
|
|
219
|
+
self._message_count = 0
|
|
220
|
+
self._logged_count = 0 # Track how many messages from history have been logged
|
|
221
|
+
self._config_sent = False # Only send config/model/task_key/instance_id on first log
|
|
222
|
+
|
|
223
|
+
def log(self, history: List[Any], response: Any) -> "SessionIngestResponse":
|
|
224
|
+
"""Log an LLM call to the session.
|
|
225
|
+
|
|
226
|
+
Pass the input history and the model response. The session tracks what's
|
|
227
|
+
already been logged and only sends new messages. Objects are automatically
|
|
228
|
+
serialized to JSON (supports Pydantic, dataclasses, TypedDict, etc.).
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
response = model.generate(history)
|
|
232
|
+
session.log(history, response.content)
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
history: The input messages sent to the model
|
|
236
|
+
response: The model's response (any serializable object)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
SessionIngestResponse with updated message count
|
|
240
|
+
"""
|
|
241
|
+
from .models import SessionIngestResponse
|
|
242
|
+
|
|
243
|
+
# Collect new history messages since last call
|
|
244
|
+
new_history = history[self._logged_count:]
|
|
245
|
+
|
|
246
|
+
# Update tracked count to include the response we're about to send
|
|
247
|
+
# This prevents the response from being sent again as "new history" in the next call
|
|
248
|
+
self._logged_count = len(history) + (1 if response is not None else 0)
|
|
249
|
+
|
|
250
|
+
# Build the payload - serialize history + response to JSON
|
|
251
|
+
payload: Dict[str, Any] = {
|
|
252
|
+
"history": [_to_dict(msg) for msg in new_history],
|
|
253
|
+
"response": _to_dict(response),
|
|
254
|
+
}
|
|
255
|
+
if self.session_id:
|
|
256
|
+
payload["session_id"] = self.session_id
|
|
257
|
+
if self.job_id:
|
|
258
|
+
payload["job_id"] = self.job_id
|
|
259
|
+
# Include config, model, task_key, instance_id on first log only
|
|
260
|
+
if not self._config_sent:
|
|
261
|
+
if self.config is not None:
|
|
262
|
+
payload["config"] = _to_dict(self.config)
|
|
263
|
+
if self.model is not None:
|
|
264
|
+
payload["model"] = self.model
|
|
265
|
+
if self.task_key is not None:
|
|
266
|
+
payload["task_key"] = self.task_key
|
|
267
|
+
if self.instance_id is not None:
|
|
268
|
+
payload["instance_id"] = self.instance_id
|
|
269
|
+
self._config_sent = True
|
|
270
|
+
|
|
271
|
+
if not new_history and response is None:
|
|
272
|
+
return SessionIngestResponse(
|
|
273
|
+
success=True,
|
|
274
|
+
session_id=self.session_id or "",
|
|
275
|
+
message_count=self._message_count,
|
|
276
|
+
created_new_session=False,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
result = self._client._ingest_raw(payload=payload)
|
|
280
|
+
self._message_count = result.message_count
|
|
281
|
+
# Update session_id if this was the first log (new session created)
|
|
282
|
+
if not self.session_id and result.session_id:
|
|
283
|
+
self.session_id = result.session_id
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
def complete(
|
|
287
|
+
self,
|
|
288
|
+
verifier_execution_id: Optional[str] = None,
|
|
289
|
+
) -> "SessionIngestResponse":
|
|
290
|
+
"""Mark the session as completed successfully.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
verifier_execution_id: Optional ID of the verifier execution record
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
SessionIngestResponse with final state
|
|
297
|
+
"""
|
|
298
|
+
from datetime import datetime
|
|
299
|
+
|
|
300
|
+
payload: Dict[str, Any] = {
|
|
301
|
+
"session_id": self.session_id,
|
|
302
|
+
"status": "completed",
|
|
303
|
+
"ended_at": datetime.now().isoformat(),
|
|
304
|
+
}
|
|
305
|
+
if verifier_execution_id:
|
|
306
|
+
payload["verifier_execution_id"] = verifier_execution_id
|
|
307
|
+
|
|
308
|
+
response = self._client._ingest_raw(payload)
|
|
309
|
+
self._message_count = response.message_count
|
|
310
|
+
return response
|
|
311
|
+
|
|
312
|
+
def fail(
|
|
313
|
+
self,
|
|
314
|
+
verifier_execution_id: Optional[str] = None,
|
|
315
|
+
) -> "SessionIngestResponse":
|
|
316
|
+
"""Mark the session as failed.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
verifier_execution_id: Optional ID of the verifier execution record
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
SessionIngestResponse with final state
|
|
323
|
+
"""
|
|
324
|
+
from datetime import datetime
|
|
325
|
+
|
|
326
|
+
payload: Dict[str, Any] = {
|
|
327
|
+
"session_id": self.session_id,
|
|
328
|
+
"status": "failed",
|
|
329
|
+
"ended_at": datetime.now().isoformat(),
|
|
330
|
+
}
|
|
331
|
+
if verifier_execution_id:
|
|
332
|
+
payload["verifier_execution_id"] = verifier_execution_id
|
|
333
|
+
|
|
334
|
+
response = self._client._ingest_raw(payload)
|
|
335
|
+
self._message_count = response.message_count
|
|
336
|
+
return response
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def message_count(self) -> int:
|
|
340
|
+
"""Get the current message count."""
|
|
341
|
+
return self._message_count
|
|
342
|
+
|
|
343
|
+
|
|
66
344
|
class SyncEnv(EnvironmentBase):
|
|
67
345
|
def __init__(self, client: Optional[SyncWrapper], **kwargs):
|
|
68
346
|
super().__init__(**kwargs)
|
|
69
347
|
self._client = client
|
|
70
348
|
self._apps: Dict[str, InstanceClient] = {}
|
|
71
349
|
self._instance: Optional[InstanceClient] = None
|
|
350
|
+
self._manager_url_override: Optional[str] = None # For URL mode
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def manager_url(self) -> str:
|
|
354
|
+
"""Override to support URL mode where urls is None."""
|
|
355
|
+
if self._manager_url_override is not None:
|
|
356
|
+
return self._manager_url_override
|
|
357
|
+
return super().manager_url
|
|
72
358
|
|
|
73
359
|
@property
|
|
74
360
|
def instance(self) -> InstanceClient:
|
|
@@ -80,17 +366,17 @@ class SyncEnv(EnvironmentBase):
|
|
|
80
366
|
|
|
81
367
|
def app(self, name: str) -> InstanceClient:
|
|
82
368
|
if name not in self._apps:
|
|
83
|
-
# Extract
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
369
|
+
# Extract scheme://netloc from manager_url, then construct /{name}/api/v1/env
|
|
370
|
+
# Supports all URL formats:
|
|
371
|
+
# https://host/api/v1/env -> https://host/{name}/api/v1/env
|
|
372
|
+
# https://host/sentry/api/v1/env -> https://host/{name}/api/v1/env
|
|
373
|
+
# http://localhost:8080/api/v1/env -> http://localhost:8080/{name}/api/v1/env
|
|
374
|
+
parsed = urlparse(self.manager_url)
|
|
375
|
+
root = f"{parsed.scheme}://{parsed.netloc}"
|
|
376
|
+
new_url = f"{root}/{name}/api/v1/env"
|
|
91
377
|
|
|
92
378
|
self._apps[name] = InstanceClient(
|
|
93
|
-
|
|
379
|
+
new_url,
|
|
94
380
|
self._client.httpx_client if self._client else None,
|
|
95
381
|
)
|
|
96
382
|
return self._apps[name]
|
|
@@ -112,6 +398,29 @@ class SyncEnv(EnvironmentBase):
|
|
|
112
398
|
def browser(self, name: str = "cdp") -> BrowserResource:
|
|
113
399
|
return self.instance.browser(name)
|
|
114
400
|
|
|
401
|
+
def api(self, name: str = "api") -> APIResource:
|
|
402
|
+
"""Get an API resource for making HTTP requests to the app's API.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
name: Name for the API resource (default: "api")
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
APIResource for making HTTP requests
|
|
409
|
+
"""
|
|
410
|
+
# Use urls.api if available, otherwise fall back to urls.root + "/raw"
|
|
411
|
+
if self.urls and self.urls.api:
|
|
412
|
+
base_url = self.urls.api
|
|
413
|
+
elif self.urls and self.urls.root:
|
|
414
|
+
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
415
|
+
elif self._manager_url_override and self._manager_url_override != "local://":
|
|
416
|
+
# URL mode: strip /api/v1/env suffix to get root URL
|
|
417
|
+
base_url = self._manager_url_override.rstrip('/')
|
|
418
|
+
if base_url.endswith('/api/v1/env'):
|
|
419
|
+
base_url = base_url[:-len('/api/v1/env')]
|
|
420
|
+
else:
|
|
421
|
+
raise ValueError("No API URL configured for this environment")
|
|
422
|
+
return self.instance.api(name, base_url)
|
|
423
|
+
|
|
115
424
|
@property
|
|
116
425
|
def mcp(self) -> SyncMCPResource:
|
|
117
426
|
mcp_url = f"{self.urls.root}mcp"
|
|
@@ -126,6 +435,23 @@ class SyncEnv(EnvironmentBase):
|
|
|
126
435
|
def close(self) -> InstanceResponse:
|
|
127
436
|
return _delete_instance(self._load_client, self.instance_id)
|
|
128
437
|
|
|
438
|
+
def heartbeat(self) -> HeartbeatResponse:
|
|
439
|
+
"""Send heartbeat to keep instance alive (if heartbeat monitoring is enabled).
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
HeartbeatResponse containing heartbeat status and deadline information
|
|
443
|
+
"""
|
|
444
|
+
body = {}
|
|
445
|
+
if self.heartbeat_region:
|
|
446
|
+
body["region"] = self.heartbeat_region
|
|
447
|
+
|
|
448
|
+
response = self._load_client.request(
|
|
449
|
+
"POST",
|
|
450
|
+
f"/v1/env/instances/{self.instance_id}/heartbeat",
|
|
451
|
+
json=body
|
|
452
|
+
)
|
|
453
|
+
return HeartbeatResponse(**response.json())
|
|
454
|
+
|
|
129
455
|
def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
130
456
|
return self.instance.verify(validator)
|
|
131
457
|
|
|
@@ -148,6 +474,7 @@ class SyncEnv(EnvironmentBase):
|
|
|
148
474
|
kwargs: dict,
|
|
149
475
|
timeout: Optional[int] = 30,
|
|
150
476
|
needs_upload: bool = True,
|
|
477
|
+
verifier_runtime_version: Optional[str] = None,
|
|
151
478
|
) -> VerifiersExecuteResponse:
|
|
152
479
|
return _execute_verifier_remote(
|
|
153
480
|
self._load_client,
|
|
@@ -160,6 +487,7 @@ class SyncEnv(EnvironmentBase):
|
|
|
160
487
|
kwargs,
|
|
161
488
|
timeout,
|
|
162
489
|
needs_upload,
|
|
490
|
+
verifier_runtime_version,
|
|
163
491
|
)
|
|
164
492
|
|
|
165
493
|
def __getstate__(self):
|
|
@@ -212,6 +540,8 @@ class Fleet:
|
|
|
212
540
|
env_variables: Optional[Dict[str, Any]] = None,
|
|
213
541
|
image_type: Optional[str] = None,
|
|
214
542
|
ttl_seconds: Optional[int] = None,
|
|
543
|
+
run_id: Optional[str] = None,
|
|
544
|
+
heartbeat_interval: Optional[int] = None,
|
|
215
545
|
) -> SyncEnv:
|
|
216
546
|
if ":" in env_key:
|
|
217
547
|
env_key_part, env_version = env_key.split(":", 1)
|
|
@@ -247,6 +577,8 @@ class Fleet:
|
|
|
247
577
|
image_type=image_type,
|
|
248
578
|
created_from="sdk",
|
|
249
579
|
ttl_seconds=ttl_seconds,
|
|
580
|
+
run_id=run_id,
|
|
581
|
+
heartbeat_interval=heartbeat_interval,
|
|
250
582
|
)
|
|
251
583
|
|
|
252
584
|
# Only use region-specific base URL if no custom base URL is set
|
|
@@ -269,13 +601,17 @@ class Fleet:
|
|
|
269
601
|
return self.make(env_key=f"{task.env_id}:{task.version}")
|
|
270
602
|
|
|
271
603
|
def instances(
|
|
272
|
-
self, status: Optional[str] = None, region: Optional[str] = None
|
|
604
|
+
self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None, profile_id: Optional[str] = None
|
|
273
605
|
) -> List[SyncEnv]:
|
|
274
606
|
params = {}
|
|
275
607
|
if status:
|
|
276
608
|
params["status"] = status
|
|
277
609
|
if region:
|
|
278
610
|
params["region"] = region
|
|
611
|
+
if run_id:
|
|
612
|
+
params["run_id"] = run_id
|
|
613
|
+
if profile_id:
|
|
614
|
+
params["profile_id"] = profile_id
|
|
279
615
|
|
|
280
616
|
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
281
617
|
return [
|
|
@@ -283,11 +619,165 @@ class Fleet:
|
|
|
283
619
|
for instance_data in response.json()
|
|
284
620
|
]
|
|
285
621
|
|
|
286
|
-
def instance(self, instance_id: str) -> SyncEnv:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
622
|
+
def instance(self, instance_id: Union[str, Dict[str, str]]) -> SyncEnv:
|
|
623
|
+
"""Create or connect to an environment instance.
|
|
624
|
+
|
|
625
|
+
Supports three modes based on input type:
|
|
626
|
+
1. dict: Local filesystem mode - {"current": "./data.db", "seed": "./seed.db"}
|
|
627
|
+
2. str starting with http:// or https://: Localhost/URL mode
|
|
628
|
+
3. str (other): Remote cloud instance mode
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
instance_id: Instance identifier (str), URL (str starting with http://),
|
|
632
|
+
or local db mapping (dict)
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
SyncEnv: Environment instance
|
|
636
|
+
"""
|
|
637
|
+
# Local filesystem mode - dict of resource names to file paths
|
|
638
|
+
if isinstance(instance_id, dict):
|
|
639
|
+
return self._create_local_instance(instance_id)
|
|
640
|
+
|
|
641
|
+
# Localhost/direct URL mode - string starting with http:// or https://
|
|
642
|
+
elif isinstance(instance_id, str) and instance_id.startswith(("http://", "https://")):
|
|
643
|
+
return self._create_url_instance(instance_id)
|
|
644
|
+
|
|
645
|
+
# Remote mode - existing behavior
|
|
646
|
+
else:
|
|
647
|
+
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
648
|
+
instance = SyncEnv(client=self.client, **response.json())
|
|
649
|
+
instance.instance.load()
|
|
650
|
+
return instance
|
|
651
|
+
|
|
652
|
+
def _create_url_instance(self, base_url: str) -> SyncEnv:
|
|
653
|
+
"""Create instance connected to a direct URL (localhost or custom).
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
base_url: URL of the instance manager API
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
SyncEnv: Environment instance configured for URL mode
|
|
660
|
+
"""
|
|
661
|
+
instance_client = InstanceClient(url=base_url, httpx_client=self._httpx_client)
|
|
662
|
+
|
|
663
|
+
# Create a minimal environment for URL mode
|
|
664
|
+
env = SyncEnv(
|
|
665
|
+
client=self.client,
|
|
666
|
+
instance_id=base_url,
|
|
667
|
+
env_key="localhost",
|
|
668
|
+
version="",
|
|
669
|
+
status="running",
|
|
670
|
+
subdomain="localhost",
|
|
671
|
+
created_at="",
|
|
672
|
+
updated_at="",
|
|
673
|
+
terminated_at=None,
|
|
674
|
+
team_id="",
|
|
675
|
+
region="localhost",
|
|
676
|
+
env_variables=None,
|
|
677
|
+
data_key=None,
|
|
678
|
+
data_version=None,
|
|
679
|
+
urls=None,
|
|
680
|
+
health=None,
|
|
681
|
+
)
|
|
682
|
+
env._instance = instance_client
|
|
683
|
+
env._manager_url_override = base_url # Set manager_url for URL mode
|
|
684
|
+
return env
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _normalize_db_path(path: str) -> tuple[str, bool]:
|
|
688
|
+
"""Normalize database path and detect if it's in-memory.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
path: Database path - can be:
|
|
692
|
+
- File path: "./data.db"
|
|
693
|
+
- Plain memory: ":memory:"
|
|
694
|
+
- Named memory: ":memory:namespace"
|
|
695
|
+
- URI: "file:name?mode=memory&cache=shared"
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Tuple of (normalized_path, is_memory)
|
|
699
|
+
"""
|
|
700
|
+
import uuid
|
|
701
|
+
import sqlite3
|
|
702
|
+
|
|
703
|
+
if path == ":memory:":
|
|
704
|
+
# Plain :memory: - create unique namespace
|
|
705
|
+
name = f"mem_{uuid.uuid4().hex[:8]}"
|
|
706
|
+
return f"file:{name}?mode=memory&cache=shared", True
|
|
707
|
+
elif path.startswith(":memory:"):
|
|
708
|
+
# Named memory: :memory:current -> file:current?mode=memory&cache=shared
|
|
709
|
+
namespace = path[8:] # Remove ":memory:" prefix
|
|
710
|
+
return f"file:{namespace}?mode=memory&cache=shared", True
|
|
711
|
+
elif "mode=memory" in path:
|
|
712
|
+
# Already a proper memory URI
|
|
713
|
+
return path, True
|
|
714
|
+
else:
|
|
715
|
+
# Regular file path
|
|
716
|
+
return path, False
|
|
717
|
+
|
|
718
|
+
def _create_local_instance(self, dbs: Dict[str, str]) -> SyncEnv:
|
|
719
|
+
"""Create instance with local file-based or in-memory SQLite resources.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
dbs: Map of resource names to paths (e.g., {"current": "./data.db"} or
|
|
723
|
+
{"current": ":memory:current"})
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
SyncEnv: Environment instance configured for local mode
|
|
727
|
+
"""
|
|
728
|
+
import sqlite3
|
|
729
|
+
|
|
730
|
+
instance_client = InstanceClient(url="local://", httpx_client=None)
|
|
731
|
+
instance_client._resources = [] # Mark as loaded
|
|
732
|
+
instance_client._memory_anchors = {} # Store anchor connections for in-memory DBs
|
|
733
|
+
|
|
734
|
+
# Store creation parameters for local SQLiteResources
|
|
735
|
+
# This allows db() to create new instances each time (matching HTTP mode behavior)
|
|
736
|
+
for name, path in dbs.items():
|
|
737
|
+
# Normalize path and detect if it's in-memory
|
|
738
|
+
normalized_path, is_memory = self._normalize_db_path(path)
|
|
739
|
+
|
|
740
|
+
# Create anchor connection for in-memory databases
|
|
741
|
+
# This keeps the database alive as long as the env exists
|
|
742
|
+
if is_memory:
|
|
743
|
+
anchor_conn = sqlite3.connect(normalized_path, uri=True)
|
|
744
|
+
instance_client._memory_anchors[name] = anchor_conn
|
|
745
|
+
|
|
746
|
+
resource_model = ResourceModel(
|
|
747
|
+
name=name,
|
|
748
|
+
type=ResourceType.db,
|
|
749
|
+
mode=ResourceMode.rw,
|
|
750
|
+
label=f"Local: {path}",
|
|
751
|
+
)
|
|
752
|
+
instance_client._resources_state[ResourceType.db.value][name] = {
|
|
753
|
+
'type': 'local',
|
|
754
|
+
'resource_model': resource_model,
|
|
755
|
+
'db_path': normalized_path,
|
|
756
|
+
'is_memory': is_memory
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
# Create a minimal environment for local mode
|
|
760
|
+
env = SyncEnv(
|
|
761
|
+
client=self.client,
|
|
762
|
+
instance_id="local",
|
|
763
|
+
env_key="local",
|
|
764
|
+
version="",
|
|
765
|
+
status="running",
|
|
766
|
+
subdomain="local",
|
|
767
|
+
created_at="",
|
|
768
|
+
updated_at="",
|
|
769
|
+
terminated_at=None,
|
|
770
|
+
team_id="",
|
|
771
|
+
region="local",
|
|
772
|
+
env_variables=None,
|
|
773
|
+
data_key=None,
|
|
774
|
+
data_version=None,
|
|
775
|
+
urls=None,
|
|
776
|
+
health=None,
|
|
777
|
+
)
|
|
778
|
+
env._instance = instance_client
|
|
779
|
+
env._manager_url_override = "local://" # Set manager_url for local mode
|
|
780
|
+
return env
|
|
291
781
|
|
|
292
782
|
def check_bundle_exists(self, bundle_hash: str) -> VerifiersCheckResponse:
|
|
293
783
|
return _check_bundle_exists(self.client, bundle_hash)
|
|
@@ -300,6 +790,65 @@ class Fleet:
|
|
|
300
790
|
def delete(self, instance_id: str) -> InstanceResponse:
|
|
301
791
|
return _delete_instance(self.client, instance_id)
|
|
302
792
|
|
|
793
|
+
def close(self, instance_id: str) -> InstanceResponse:
|
|
794
|
+
"""Close (delete) a specific instance by ID.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
instance_id: The instance ID to close
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
InstanceResponse containing the deleted instance details
|
|
801
|
+
"""
|
|
802
|
+
return _delete_instance(self.client, instance_id)
|
|
803
|
+
|
|
804
|
+
def heartbeat(self, instance_id: str, region: Optional[str] = None) -> HeartbeatResponse:
|
|
805
|
+
"""Send heartbeat to keep instance alive (if heartbeat monitoring is enabled).
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
instance_id: The instance ID to send heartbeat for
|
|
809
|
+
region: Optional region override for cross-region heartbeats
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
HeartbeatResponse containing heartbeat status and deadline information
|
|
813
|
+
"""
|
|
814
|
+
return _send_heartbeat(self.client, instance_id, region)
|
|
815
|
+
|
|
816
|
+
def close_all(self, run_id: Optional[str] = None, profile_id: Optional[str] = None) -> List[InstanceResponse]:
|
|
817
|
+
"""Close (delete) instances using the batch delete endpoint.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
run_id: Optional run ID to filter instances by
|
|
821
|
+
profile_id: Optional profile ID to filter instances by (use "self" for your own profile)
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
List[InstanceResponse] containing the deleted instances
|
|
825
|
+
|
|
826
|
+
Note:
|
|
827
|
+
At least one of run_id or profile_id must be provided.
|
|
828
|
+
"""
|
|
829
|
+
return _delete_instances_batch(self.client, run_id=run_id, profile_id=profile_id)
|
|
830
|
+
|
|
831
|
+
def list_runs(
|
|
832
|
+
self, profile_id: Optional[str] = None, status: Optional[str] = "active"
|
|
833
|
+
) -> List[Run]:
|
|
834
|
+
"""List all runs (groups of instances by run_id) with aggregated statistics.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
profile_id: Optional profile ID to filter runs by (use "self" for your own profile)
|
|
838
|
+
status: Filter by run status - "active" (default), "inactive", or "all"
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
List[Run] containing run information with instance counts and timestamps
|
|
842
|
+
"""
|
|
843
|
+
params = {}
|
|
844
|
+
if profile_id:
|
|
845
|
+
params["profile_id"] = profile_id
|
|
846
|
+
if status:
|
|
847
|
+
params["active"] = status
|
|
848
|
+
|
|
849
|
+
response = self.client.request("GET", "/v1/env/runs", params=params)
|
|
850
|
+
return [Run(**run_data) for run_data in response.json()]
|
|
851
|
+
|
|
303
852
|
def load_tasks_from_file(self, filename: str) -> List[Task]:
|
|
304
853
|
with open(filename, "r", encoding="utf-8") as f:
|
|
305
854
|
tasks_data = f.read()
|
|
@@ -366,6 +915,11 @@ class Fleet:
|
|
|
366
915
|
if not verifier_id:
|
|
367
916
|
verifier_id = task_json.get("key", task_json.get("id"))
|
|
368
917
|
|
|
918
|
+
# Extract verifier_runtime_version from metadata if present
|
|
919
|
+
verifier_runtime_version = None
|
|
920
|
+
if "metadata" in task_json and isinstance(task_json["metadata"], dict):
|
|
921
|
+
verifier_runtime_version = task_json["metadata"].get("verifier_runtime_version")
|
|
922
|
+
|
|
369
923
|
try:
|
|
370
924
|
if verifier_id and verifier_code:
|
|
371
925
|
verifier = self._create_verifier_from_data(
|
|
@@ -373,13 +927,14 @@ class Fleet:
|
|
|
373
927
|
verifier_key=task_json.get("key", task_json.get("id")),
|
|
374
928
|
verifier_code=verifier_code,
|
|
375
929
|
verifier_sha=verifier_sha,
|
|
930
|
+
verifier_runtime_version=verifier_runtime_version,
|
|
376
931
|
)
|
|
377
932
|
except Exception as e:
|
|
378
933
|
error_msg = f"Failed to create verifier {task_json.get('key', task_json.get('id'))}: {e}"
|
|
379
934
|
if raise_on_verifier_error:
|
|
380
935
|
raise ValueError(error_msg) from e
|
|
381
|
-
else:
|
|
382
|
-
|
|
936
|
+
# else:
|
|
937
|
+
# logger.warning(error_msg)
|
|
383
938
|
|
|
384
939
|
task = Task(
|
|
385
940
|
key=task_json.get("key", task_json.get("id")),
|
|
@@ -396,7 +951,10 @@ class Fleet:
|
|
|
396
951
|
verifier=verifier, # Use created verifier or None
|
|
397
952
|
verifier_id=verifier_id, # Set verifier_id so _rebuild_verifier works
|
|
398
953
|
verifier_sha=verifier_sha, # Set verifier_sha
|
|
954
|
+
verifier_runtime_version=verifier_runtime_version, # Set verifier_runtime_version
|
|
399
955
|
metadata=task_json.get("metadata", {}), # Default empty metadata
|
|
956
|
+
writer_metadata=task_json.get("writer_metadata"), # Writer metadata
|
|
957
|
+
qa_metadata=task_json.get("qa_metadata"), # QA metadata
|
|
400
958
|
output_json_schema=task_json.get("output_json_schema"), # JSON schema for output
|
|
401
959
|
)
|
|
402
960
|
return task
|
|
@@ -469,23 +1027,23 @@ class Fleet:
|
|
|
469
1027
|
verifier_sha=tr.verifier.sha256,
|
|
470
1028
|
)
|
|
471
1029
|
except Exception as e:
|
|
472
|
-
logger.warning(
|
|
473
|
-
|
|
474
|
-
)
|
|
1030
|
+
# logger.warning(
|
|
1031
|
+
# f"Failed to create verifier {tr.verifier.key}: {e}"
|
|
1032
|
+
# )
|
|
475
1033
|
return None
|
|
476
1034
|
else:
|
|
477
1035
|
# Fallback: try fetching by ID
|
|
478
1036
|
try:
|
|
479
|
-
logger.warning(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
)
|
|
1037
|
+
# logger.warning(
|
|
1038
|
+
# f"Embedded verifier code missing for {tr.verifier.key} (NoSuchKey). "
|
|
1039
|
+
# f"Attempting to refetch by id {tr.verifier.verifier_id}"
|
|
1040
|
+
# )
|
|
483
1041
|
return self._load_verifier(tr.verifier.verifier_id)
|
|
484
1042
|
except Exception as e:
|
|
485
|
-
logger.warning(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
1043
|
+
# logger.warning(
|
|
1044
|
+
# f"Refetch by verifier id failed for {tr.verifier.key}: {e}. "
|
|
1045
|
+
# "Leaving verifier unset."
|
|
1046
|
+
# )
|
|
489
1047
|
return None
|
|
490
1048
|
|
|
491
1049
|
# Add the task for parallel execution
|
|
@@ -525,7 +1083,7 @@ class Fleet:
|
|
|
525
1083
|
result = future.result()
|
|
526
1084
|
verifier_results.append(result)
|
|
527
1085
|
except Exception as e:
|
|
528
|
-
logger.warning(f"Verifier loading failed: {e}")
|
|
1086
|
+
# logger.warning(f"Verifier loading failed: {e}")
|
|
529
1087
|
verifier_results.append(None)
|
|
530
1088
|
|
|
531
1089
|
# Build tasks with results
|
|
@@ -548,6 +1106,21 @@ class Fleet:
|
|
|
548
1106
|
if not is_embedded_error:
|
|
549
1107
|
verifier_func = embedded_code
|
|
550
1108
|
|
|
1109
|
+
# Extract verifier metadata
|
|
1110
|
+
verifier_id = task_response.verifier_id
|
|
1111
|
+
if not verifier_id and task_response.verifier:
|
|
1112
|
+
verifier_id = task_response.verifier.verifier_id
|
|
1113
|
+
|
|
1114
|
+
verifier_sha = None
|
|
1115
|
+
if task_response.verifier:
|
|
1116
|
+
verifier_sha = task_response.verifier.sha256
|
|
1117
|
+
|
|
1118
|
+
# Extract verifier_runtime_version from metadata if present
|
|
1119
|
+
verifier_runtime_version = None
|
|
1120
|
+
metadata = task_response.metadata or {}
|
|
1121
|
+
if isinstance(metadata, dict):
|
|
1122
|
+
verifier_runtime_version = metadata.get("verifier_runtime_version")
|
|
1123
|
+
|
|
551
1124
|
task = Task(
|
|
552
1125
|
key=task_response.key,
|
|
553
1126
|
prompt=task_response.prompt,
|
|
@@ -559,7 +1132,12 @@ class Fleet:
|
|
|
559
1132
|
env_variables=task_response.env_variables or {},
|
|
560
1133
|
verifier_func=verifier_func, # Set verifier code
|
|
561
1134
|
verifier=verifier, # Use created verifier or None
|
|
562
|
-
|
|
1135
|
+
verifier_id=verifier_id, # Set verifier_id
|
|
1136
|
+
verifier_sha=verifier_sha, # Set verifier_sha
|
|
1137
|
+
verifier_runtime_version=verifier_runtime_version, # Set verifier_runtime_version
|
|
1138
|
+
metadata=metadata,
|
|
1139
|
+
writer_metadata=getattr(task_response, "writer_metadata", None), # Writer metadata
|
|
1140
|
+
qa_metadata=getattr(task_response, "qa_metadata", None), # QA metadata
|
|
563
1141
|
output_json_schema=getattr(task_response, "output_json_schema", None), # Get output_json_schema if available
|
|
564
1142
|
)
|
|
565
1143
|
tasks.append(task)
|
|
@@ -612,10 +1190,10 @@ class Fleet:
|
|
|
612
1190
|
with open(filename, "w", encoding="utf-8") as f:
|
|
613
1191
|
json.dump(tasks_data, f, indent=2, default=str)
|
|
614
1192
|
|
|
615
|
-
logger.info(f"Exported {len(tasks)} tasks to {filename}")
|
|
1193
|
+
# logger.info(f"Exported {len(tasks)} tasks to {filename}")
|
|
616
1194
|
return filename
|
|
617
1195
|
else:
|
|
618
|
-
logger.info("No tasks found to export")
|
|
1196
|
+
# logger.info("No tasks found to export")
|
|
619
1197
|
return None
|
|
620
1198
|
|
|
621
1199
|
def import_single_task(self, task: Task, project_key: Optional[str] = None):
|
|
@@ -644,7 +1222,7 @@ class Fleet:
|
|
|
644
1222
|
)
|
|
645
1223
|
return response
|
|
646
1224
|
except Exception as e:
|
|
647
|
-
logger.error(f"Failed to import task {task.key}: {e}")
|
|
1225
|
+
# logger.error(f"Failed to import task {task.key}: {e}")
|
|
648
1226
|
return None
|
|
649
1227
|
|
|
650
1228
|
def import_tasks(self, filename: str, project_key: Optional[str] = None):
|
|
@@ -706,6 +1284,9 @@ class Fleet:
|
|
|
706
1284
|
task_key: str,
|
|
707
1285
|
prompt: Optional[str] = None,
|
|
708
1286
|
verifier_code: Optional[str] = None,
|
|
1287
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1288
|
+
writer_metadata: Optional[Dict[str, Any]] = None,
|
|
1289
|
+
qa_metadata: Optional[Dict[str, Any]] = None,
|
|
709
1290
|
) -> TaskResponse:
|
|
710
1291
|
"""Update an existing task.
|
|
711
1292
|
|
|
@@ -713,11 +1294,20 @@ class Fleet:
|
|
|
713
1294
|
task_key: The key of the task to update
|
|
714
1295
|
prompt: New prompt text for the task (optional)
|
|
715
1296
|
verifier_code: Python code for task verification (optional)
|
|
1297
|
+
metadata: Additional metadata for the task (optional)
|
|
1298
|
+
writer_metadata: Metadata filled by task writer (optional)
|
|
1299
|
+
qa_metadata: Metadata filled by QA reviewer (optional)
|
|
716
1300
|
|
|
717
1301
|
Returns:
|
|
718
1302
|
TaskResponse containing the updated task details
|
|
719
1303
|
"""
|
|
720
|
-
payload = TaskUpdateRequest(
|
|
1304
|
+
payload = TaskUpdateRequest(
|
|
1305
|
+
prompt=prompt,
|
|
1306
|
+
verifier_code=verifier_code,
|
|
1307
|
+
metadata=metadata,
|
|
1308
|
+
writer_metadata=writer_metadata,
|
|
1309
|
+
qa_metadata=qa_metadata,
|
|
1310
|
+
)
|
|
721
1311
|
response = self.client.request(
|
|
722
1312
|
"PUT", f"/v1/tasks/{task_key}", json=payload.model_dump(exclude_none=True)
|
|
723
1313
|
)
|
|
@@ -750,8 +1340,393 @@ class Fleet:
|
|
|
750
1340
|
)
|
|
751
1341
|
return TaskResponse(**response.json())
|
|
752
1342
|
|
|
1343
|
+
# Jobs API methods
|
|
1344
|
+
|
|
1345
|
+
def list_jobs(self, team_id: Optional[str] = None) -> List[JobResponse]:
|
|
1346
|
+
"""List all jobs for the authenticated team.
|
|
1347
|
+
|
|
1348
|
+
Args:
|
|
1349
|
+
team_id: Optional team_id to filter by (admin only)
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
List[JobResponse] containing job information
|
|
1353
|
+
"""
|
|
1354
|
+
params = {}
|
|
1355
|
+
if team_id is not None:
|
|
1356
|
+
params["team_id"] = team_id
|
|
1357
|
+
|
|
1358
|
+
response = self.client.request("GET", "/v1/jobs", params=params)
|
|
1359
|
+
job_list = JobListResponse(**response.json())
|
|
1360
|
+
return job_list.jobs
|
|
1361
|
+
|
|
1362
|
+
def create_job(
|
|
1363
|
+
self,
|
|
1364
|
+
models: List[str],
|
|
1365
|
+
name: Optional[str] = None,
|
|
1366
|
+
pass_k: int = 1,
|
|
1367
|
+
env_key: Optional[str] = None,
|
|
1368
|
+
project_key: Optional[str] = None,
|
|
1369
|
+
task_keys: Optional[List[str]] = None,
|
|
1370
|
+
excluded_task_keys: Optional[List[str]] = None,
|
|
1371
|
+
max_steps: Optional[int] = None,
|
|
1372
|
+
max_duration_minutes: int = 60,
|
|
1373
|
+
max_concurrent_per_model: int = 30,
|
|
1374
|
+
mode: Optional[str] = None,
|
|
1375
|
+
system_prompt: Optional[str] = None,
|
|
1376
|
+
model_prompts: Optional[Dict[str, str]] = None,
|
|
1377
|
+
byok_keys: Optional[Dict[str, str]] = None,
|
|
1378
|
+
byok_ttl_minutes: Optional[int] = None,
|
|
1379
|
+
harness: Optional[str] = None,
|
|
1380
|
+
) -> JobCreateResponse:
|
|
1381
|
+
"""Create a new job.
|
|
1382
|
+
|
|
1383
|
+
Args:
|
|
1384
|
+
models: List of model identifiers in "provider/model" format
|
|
1385
|
+
name: Optional job name. Supports placeholders: {id} (UUID), {sid} (short UUID), {i} (auto-increment, must be suffix)
|
|
1386
|
+
pass_k: Number of passes (default: 1)
|
|
1387
|
+
env_key: Environment key (mutually exclusive with project_key/task_keys)
|
|
1388
|
+
project_key: Project key (mutually exclusive with env_key/task_keys)
|
|
1389
|
+
task_keys: Specific task keys (mutually exclusive with env_key/project_key)
|
|
1390
|
+
excluded_task_keys: Task keys to exclude
|
|
1391
|
+
max_steps: Maximum agent steps
|
|
1392
|
+
max_duration_minutes: Timeout in minutes (default: 60)
|
|
1393
|
+
max_concurrent_per_model: Max concurrent per model (default: 30)
|
|
1394
|
+
mode: "tool-use" or "computer-use"
|
|
1395
|
+
system_prompt: Custom system prompt
|
|
1396
|
+
model_prompts: Per-model prompts (model -> prompt)
|
|
1397
|
+
byok_keys: Bring Your Own Keys (provider -> API key)
|
|
1398
|
+
byok_ttl_minutes: TTL for BYOK keys in minutes
|
|
1399
|
+
harness: Harness identifier
|
|
1400
|
+
|
|
1401
|
+
Returns:
|
|
1402
|
+
JobCreateResponse containing job_id, workflow_job_id, status, and name
|
|
1403
|
+
"""
|
|
1404
|
+
request = JobCreateRequest(
|
|
1405
|
+
name=name,
|
|
1406
|
+
models=models,
|
|
1407
|
+
pass_k=pass_k,
|
|
1408
|
+
env_key=env_key,
|
|
1409
|
+
project_key=project_key,
|
|
1410
|
+
task_keys=task_keys,
|
|
1411
|
+
excluded_task_keys=excluded_task_keys,
|
|
1412
|
+
max_steps=max_steps,
|
|
1413
|
+
max_duration_minutes=max_duration_minutes,
|
|
1414
|
+
max_concurrent_per_model=max_concurrent_per_model,
|
|
1415
|
+
mode=mode,
|
|
1416
|
+
system_prompt=system_prompt,
|
|
1417
|
+
model_prompts=model_prompts,
|
|
1418
|
+
byok_keys=byok_keys,
|
|
1419
|
+
byok_ttl_minutes=byok_ttl_minutes,
|
|
1420
|
+
harness=harness,
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
response = self.client.request(
|
|
1424
|
+
"POST", "/v1/jobs", json=request.model_dump(exclude_none=True)
|
|
1425
|
+
)
|
|
1426
|
+
return JobCreateResponse(**response.json())
|
|
1427
|
+
|
|
1428
|
+
def get_job(self, job_id: str, team_id: Optional[str] = None) -> JobResponse:
|
|
1429
|
+
"""Get a specific job by ID.
|
|
1430
|
+
|
|
1431
|
+
Args:
|
|
1432
|
+
job_id: The job ID
|
|
1433
|
+
team_id: Optional team_id to filter by (admin only)
|
|
1434
|
+
|
|
1435
|
+
Returns:
|
|
1436
|
+
JobResponse containing job information
|
|
1437
|
+
"""
|
|
1438
|
+
params = {}
|
|
1439
|
+
if team_id is not None:
|
|
1440
|
+
params["team_id"] = team_id
|
|
1441
|
+
|
|
1442
|
+
response = self.client.request("GET", f"/v1/jobs/{job_id}", params=params)
|
|
1443
|
+
return JobResponse(**response.json())
|
|
1444
|
+
|
|
1445
|
+
# Sessions API methods
|
|
1446
|
+
|
|
1447
|
+
def list_job_sessions(self, job_id: str) -> JobSessionsResponse:
|
|
1448
|
+
"""List all sessions for a job, grouped by task.
|
|
1449
|
+
|
|
1450
|
+
Args:
|
|
1451
|
+
job_id: The job ID
|
|
1452
|
+
|
|
1453
|
+
Returns:
|
|
1454
|
+
JobSessionsResponse containing sessions grouped by task with statistics
|
|
1455
|
+
"""
|
|
1456
|
+
response = self.client.request("GET", f"/v1/sessions/job/{job_id}")
|
|
1457
|
+
return JobSessionsResponse(**response.json())
|
|
1458
|
+
|
|
1459
|
+
def get_session_transcript(self, session_id: str) -> SessionTranscriptResponse:
|
|
1460
|
+
"""Get the transcript for a specific session.
|
|
1461
|
+
|
|
1462
|
+
Args:
|
|
1463
|
+
session_id: The session ID
|
|
1464
|
+
|
|
1465
|
+
Returns:
|
|
1466
|
+
SessionTranscriptResponse containing task, instance, verifier result, and messages
|
|
1467
|
+
"""
|
|
1468
|
+
response = self.client.request(
|
|
1469
|
+
"GET", f"/v1/sessions/{session_id}/transcript"
|
|
1470
|
+
)
|
|
1471
|
+
return SessionTranscriptResponse(**response.json())
|
|
1472
|
+
|
|
1473
|
+
def _ingest(
|
|
1474
|
+
self,
|
|
1475
|
+
messages: List[Dict[str, Any]],
|
|
1476
|
+
session_id: Optional[str] = None,
|
|
1477
|
+
model: Optional[str] = None,
|
|
1478
|
+
task_key: Optional[str] = None,
|
|
1479
|
+
job_id: Optional[str] = None,
|
|
1480
|
+
instance_id: Optional[str] = None,
|
|
1481
|
+
status: Optional[str] = None,
|
|
1482
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1483
|
+
started_at: Optional[str] = None,
|
|
1484
|
+
ended_at: Optional[str] = None,
|
|
1485
|
+
verifier_execution_id: Optional[str] = None,
|
|
1486
|
+
) -> SessionIngestResponse:
|
|
1487
|
+
"""Internal method to ingest session data."""
|
|
1488
|
+
message_objects = [SessionIngestMessage(**msg) for msg in messages]
|
|
1489
|
+
request = SessionIngestRequest(
|
|
1490
|
+
messages=message_objects,
|
|
1491
|
+
session_id=session_id,
|
|
1492
|
+
model=model,
|
|
1493
|
+
task_key=task_key,
|
|
1494
|
+
job_id=job_id,
|
|
1495
|
+
instance_id=instance_id,
|
|
1496
|
+
status=SessionStatus(status) if status else None,
|
|
1497
|
+
metadata=metadata,
|
|
1498
|
+
started_at=started_at,
|
|
1499
|
+
ended_at=ended_at,
|
|
1500
|
+
verifier_execution_id=verifier_execution_id,
|
|
1501
|
+
)
|
|
1502
|
+
response = self.client.request(
|
|
1503
|
+
"POST",
|
|
1504
|
+
"/v1/sessions/ingest",
|
|
1505
|
+
json=request.model_dump(exclude_none=True),
|
|
1506
|
+
)
|
|
1507
|
+
return SessionIngestResponse(**response.json())
|
|
1508
|
+
|
|
1509
|
+
def _ingest_raw(
|
|
1510
|
+
self,
|
|
1511
|
+
payload: Dict[str, Any],
|
|
1512
|
+
) -> SessionIngestResponse:
|
|
1513
|
+
"""Internal method to ingest raw session data as JSON.
|
|
1514
|
+
|
|
1515
|
+
This sends the history and response as-is to the backend,
|
|
1516
|
+
letting the backend handle format normalization.
|
|
1517
|
+
"""
|
|
1518
|
+
# Pre-serialize with our custom handler to ensure all types are JSON-safe
|
|
1519
|
+
json_str = json.dumps(payload, default=_json_default)
|
|
1520
|
+
clean_payload = json.loads(json_str)
|
|
1521
|
+
|
|
1522
|
+
response = self.client.request(
|
|
1523
|
+
"POST",
|
|
1524
|
+
"/v1/traces/logs",
|
|
1525
|
+
json=clean_payload,
|
|
1526
|
+
)
|
|
1527
|
+
return SessionIngestResponse(**response.json())
|
|
1528
|
+
|
|
1529
|
+
def start_session(
|
|
1530
|
+
self,
|
|
1531
|
+
session_id: Optional[str] = None,
|
|
1532
|
+
job_id: Optional[str] = None,
|
|
1533
|
+
config: Optional[Any] = None,
|
|
1534
|
+
model: Optional[str] = None,
|
|
1535
|
+
task_key: Optional[str] = None,
|
|
1536
|
+
instance_id: Optional[str] = None,
|
|
1537
|
+
) -> Session:
|
|
1538
|
+
"""Start a new session for logging agent interactions.
|
|
1539
|
+
|
|
1540
|
+
This returns a Session object. The session is created on the backend
|
|
1541
|
+
when you call log() for the first time.
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
session_id: Optional existing session ID to resume
|
|
1545
|
+
job_id: Optional job ID to associate with the session
|
|
1546
|
+
config: Optional config object (e.g., GenerateContentConfig) to log
|
|
1547
|
+
model: Optional model name to log
|
|
1548
|
+
task_key: Optional Fleet task key
|
|
1549
|
+
instance_id: Optional Fleet instance ID
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
Session object with log(), complete(), and fail() methods
|
|
1553
|
+
|
|
1554
|
+
Example:
|
|
1555
|
+
session = fleet_client.start_session(config=config, model="gpt-4", task_key="task_123")
|
|
1556
|
+
|
|
1557
|
+
# Log LLM calls during agent run
|
|
1558
|
+
session.log(history, response)
|
|
1559
|
+
|
|
1560
|
+
# Complete when done
|
|
1561
|
+
session.complete()
|
|
1562
|
+
"""
|
|
1563
|
+
return Session(
|
|
1564
|
+
client=self,
|
|
1565
|
+
session_id=session_id,
|
|
1566
|
+
job_id=job_id,
|
|
1567
|
+
config=config,
|
|
1568
|
+
model=model,
|
|
1569
|
+
task_key=task_key,
|
|
1570
|
+
instance_id=instance_id,
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
def trace_job(self, name: Optional[str] = None) -> str:
|
|
1574
|
+
"""Create a new trace job.
|
|
1575
|
+
|
|
1576
|
+
Args:
|
|
1577
|
+
name: Name of the job (generated server-side if not provided)
|
|
1578
|
+
|
|
1579
|
+
Returns:
|
|
1580
|
+
The job_id string
|
|
1581
|
+
"""
|
|
1582
|
+
from .models import TraceJobRequest, TraceJobResponse
|
|
1583
|
+
|
|
1584
|
+
request = TraceJobRequest(name=name)
|
|
1585
|
+
response = self.client.request(
|
|
1586
|
+
"POST",
|
|
1587
|
+
"/v1/traces/jobs",
|
|
1588
|
+
json=request.model_dump(),
|
|
1589
|
+
)
|
|
1590
|
+
result = TraceJobResponse(**response.json())
|
|
1591
|
+
return result.job_id
|
|
1592
|
+
|
|
1593
|
+
def create_session(
|
|
1594
|
+
self,
|
|
1595
|
+
model: Optional[str] = None,
|
|
1596
|
+
task_key: Optional[str] = None,
|
|
1597
|
+
job_id: Optional[str] = None,
|
|
1598
|
+
instance_id: Optional[str] = None,
|
|
1599
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1600
|
+
started_at: Optional[str] = None,
|
|
1601
|
+
initial_message: Optional[Dict[str, Any]] = None,
|
|
1602
|
+
) -> SessionIngestResponse:
|
|
1603
|
+
"""Create a new session, optionally with an initial message.
|
|
1604
|
+
|
|
1605
|
+
This is useful for streaming scenarios where you want to create
|
|
1606
|
+
a session first and then append messages one by one.
|
|
1607
|
+
|
|
1608
|
+
Args:
|
|
1609
|
+
model: Model identifier (e.g., "anthropic/claude-sonnet-4")
|
|
1610
|
+
task_key: Task key to associate with the session
|
|
1611
|
+
job_id: Job ID to associate with the session
|
|
1612
|
+
instance_id: Instance ID to associate with the session
|
|
1613
|
+
metadata: Additional metadata for the session
|
|
1614
|
+
started_at: ISO timestamp when session started
|
|
1615
|
+
initial_message: Optional first message dict with 'role' and 'content'
|
|
1616
|
+
|
|
1617
|
+
Returns:
|
|
1618
|
+
SessionIngestResponse containing session_id
|
|
1619
|
+
|
|
1620
|
+
Example:
|
|
1621
|
+
# Create session and get ID
|
|
1622
|
+
session = fleet.create_session(
|
|
1623
|
+
model="anthropic/claude-sonnet-4",
|
|
1624
|
+
task_key="my_task",
|
|
1625
|
+
started_at=datetime.now().isoformat()
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Append messages as they happen
|
|
1629
|
+
fleet.append_message(session.session_id, {"role": "user", "content": "Hello"})
|
|
1630
|
+
fleet.append_message(session.session_id, {"role": "assistant", "content": "Hi!"})
|
|
1631
|
+
"""
|
|
1632
|
+
# Use a placeholder message if none provided
|
|
1633
|
+
if initial_message:
|
|
1634
|
+
messages = [initial_message]
|
|
1635
|
+
else:
|
|
1636
|
+
messages = [{"role": "system", "content": "[session created]"}]
|
|
1637
|
+
|
|
1638
|
+
return self._ingest(
|
|
1639
|
+
messages=messages,
|
|
1640
|
+
model=model,
|
|
1641
|
+
task_key=task_key,
|
|
1642
|
+
job_id=job_id,
|
|
1643
|
+
instance_id=instance_id,
|
|
1644
|
+
status="running",
|
|
1645
|
+
metadata=metadata,
|
|
1646
|
+
started_at=started_at,
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
def append_message(
|
|
1650
|
+
self,
|
|
1651
|
+
session_id: str,
|
|
1652
|
+
message: Dict[str, Any],
|
|
1653
|
+
status: Optional[str] = None,
|
|
1654
|
+
ended_at: Optional[str] = None,
|
|
1655
|
+
) -> SessionIngestResponse:
|
|
1656
|
+
"""Append a single message to an existing session.
|
|
1657
|
+
|
|
1658
|
+
This is useful for streaming scenarios where you want to send
|
|
1659
|
+
messages one by one as they happen.
|
|
1660
|
+
|
|
1661
|
+
Args:
|
|
1662
|
+
session_id: The session ID to append to
|
|
1663
|
+
message: Message dict with 'role' and 'content' keys.
|
|
1664
|
+
Optional keys: 'tool_calls', 'tool_call_id', 'timestamp', 'tokens', 'metadata'
|
|
1665
|
+
status: Optional status update ("running", "completed", "failed")
|
|
1666
|
+
ended_at: ISO timestamp when session ended (set when completing)
|
|
1667
|
+
|
|
1668
|
+
Returns:
|
|
1669
|
+
SessionIngestResponse with updated message count
|
|
1670
|
+
|
|
1671
|
+
Example:
|
|
1672
|
+
# Append user message
|
|
1673
|
+
fleet.append_message(session_id, {"role": "user", "content": "What's 2+2?"})
|
|
1674
|
+
|
|
1675
|
+
# Append assistant response
|
|
1676
|
+
fleet.append_message(session_id, {"role": "assistant", "content": "4"})
|
|
1677
|
+
|
|
1678
|
+
# Complete the session
|
|
1679
|
+
fleet.append_message(
|
|
1680
|
+
session_id,
|
|
1681
|
+
{"role": "assistant", "content": "Done!"},
|
|
1682
|
+
status="completed",
|
|
1683
|
+
ended_at=datetime.now().isoformat()
|
|
1684
|
+
)
|
|
1685
|
+
"""
|
|
1686
|
+
return self._ingest(
|
|
1687
|
+
messages=[message],
|
|
1688
|
+
session_id=session_id,
|
|
1689
|
+
status=status,
|
|
1690
|
+
ended_at=ended_at,
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
def complete_session(
|
|
1694
|
+
self,
|
|
1695
|
+
session_id: str,
|
|
1696
|
+
status: str = "completed",
|
|
1697
|
+
ended_at: Optional[str] = None,
|
|
1698
|
+
final_message: Optional[Dict[str, Any]] = None,
|
|
1699
|
+
) -> SessionIngestResponse:
|
|
1700
|
+
"""Mark a session as complete.
|
|
1701
|
+
|
|
1702
|
+
Args:
|
|
1703
|
+
session_id: The session ID to complete
|
|
1704
|
+
status: Final status ("completed", "failed", "cancelled")
|
|
1705
|
+
ended_at: ISO timestamp when session ended (defaults to now)
|
|
1706
|
+
final_message: Optional final message to append
|
|
1707
|
+
|
|
1708
|
+
Returns:
|
|
1709
|
+
SessionIngestResponse with final state
|
|
1710
|
+
"""
|
|
1711
|
+
from datetime import datetime as dt
|
|
1712
|
+
|
|
1713
|
+
if ended_at is None:
|
|
1714
|
+
ended_at = dt.now().isoformat()
|
|
1715
|
+
|
|
1716
|
+
if final_message:
|
|
1717
|
+
messages = [final_message]
|
|
1718
|
+
else:
|
|
1719
|
+
messages = [{"role": "system", "content": f"[session {status}]"}]
|
|
1720
|
+
|
|
1721
|
+
return self._ingest(
|
|
1722
|
+
messages=messages,
|
|
1723
|
+
session_id=session_id,
|
|
1724
|
+
status=status,
|
|
1725
|
+
ended_at=ended_at,
|
|
1726
|
+
)
|
|
1727
|
+
|
|
753
1728
|
def _create_verifier_from_data(
|
|
754
|
-
self, verifier_id: str, verifier_key: str, verifier_code: str, verifier_sha: str
|
|
1729
|
+
self, verifier_id: str, verifier_key: str, verifier_code: str, verifier_sha: str, verifier_runtime_version: Optional[str] = None
|
|
755
1730
|
) -> "SyncVerifierFunction":
|
|
756
1731
|
"""Create an AsyncVerifierFunction from verifier data.
|
|
757
1732
|
|
|
@@ -773,6 +1748,7 @@ class Fleet:
|
|
|
773
1748
|
verifier_id=verifier_id,
|
|
774
1749
|
verifier_key=verifier_key,
|
|
775
1750
|
sha256=verifier_sha,
|
|
1751
|
+
verifier_runtime_version=verifier_runtime_version or "",
|
|
776
1752
|
)
|
|
777
1753
|
|
|
778
1754
|
# Store the original verifier code for reference
|
|
@@ -808,6 +1784,37 @@ def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
|
|
|
808
1784
|
return InstanceResponse(**response.json())
|
|
809
1785
|
|
|
810
1786
|
|
|
1787
|
+
def _send_heartbeat(client: SyncWrapper, instance_id: str, region: Optional[str] = None) -> HeartbeatResponse:
|
|
1788
|
+
"""Send heartbeat to keep instance alive."""
|
|
1789
|
+
body = {}
|
|
1790
|
+
if region:
|
|
1791
|
+
body["region"] = region
|
|
1792
|
+
|
|
1793
|
+
response = client.request(
|
|
1794
|
+
"POST",
|
|
1795
|
+
f"/v1/env/instances/{instance_id}/heartbeat",
|
|
1796
|
+
json=body
|
|
1797
|
+
)
|
|
1798
|
+
return HeartbeatResponse(**response.json())
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _delete_instances_batch(
|
|
1802
|
+
client: SyncWrapper, run_id: Optional[str] = None, profile_id: Optional[str] = None
|
|
1803
|
+
) -> List[InstanceResponse]:
|
|
1804
|
+
"""Delete instances using the batch endpoint with flexible filtering."""
|
|
1805
|
+
params = {}
|
|
1806
|
+
if run_id:
|
|
1807
|
+
params["run_id"] = run_id
|
|
1808
|
+
if profile_id:
|
|
1809
|
+
params["profile_id"] = profile_id
|
|
1810
|
+
|
|
1811
|
+
if not params:
|
|
1812
|
+
raise ValueError("At least one of run_id or profile_id must be provided")
|
|
1813
|
+
|
|
1814
|
+
response = client.request("DELETE", "/v1/env/instances/batch", params=params)
|
|
1815
|
+
return [InstanceResponse(**instance_data) for instance_data in response.json()]
|
|
1816
|
+
|
|
1817
|
+
|
|
811
1818
|
def _check_bundle_exists(
|
|
812
1819
|
client: SyncWrapper, bundle_hash: str
|
|
813
1820
|
) -> VerifiersCheckResponse:
|
|
@@ -826,6 +1833,7 @@ def _execute_verifier_remote(
|
|
|
826
1833
|
kwargs: dict,
|
|
827
1834
|
timeout: Optional[int] = 30,
|
|
828
1835
|
needs_upload: bool = True,
|
|
1836
|
+
verifier_runtime_version: Optional[str] = None,
|
|
829
1837
|
) -> VerifiersExecuteResponse:
|
|
830
1838
|
# Pickle args and kwargs together
|
|
831
1839
|
# The first arg should be None as a placeholder for env
|
|
@@ -849,18 +1857,22 @@ def _execute_verifier_remote(
|
|
|
849
1857
|
bundle_b64 = base64.b64encode(bundle_data).decode("utf-8")
|
|
850
1858
|
request_data["bundle"] = bundle_b64
|
|
851
1859
|
|
|
1860
|
+
# Add verifier_runtime_version if present
|
|
1861
|
+
if verifier_runtime_version:
|
|
1862
|
+
request_data["verifier_runtime_version"] = verifier_runtime_version
|
|
1863
|
+
|
|
852
1864
|
# Debug logging
|
|
853
|
-
logger.debug(
|
|
854
|
-
|
|
855
|
-
)
|
|
856
|
-
logger.debug(f"Request has bundle: {needs_upload}")
|
|
857
|
-
logger.debug(f"Using client with base_url: {client.base_url}")
|
|
858
|
-
logger.debug(f"Request data keys: {list(request_data.keys())}")
|
|
859
|
-
logger.debug(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
)
|
|
1865
|
+
# logger.debug(
|
|
1866
|
+
# f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
|
|
1867
|
+
# )
|
|
1868
|
+
# logger.debug(f"Request has bundle: {needs_upload}")
|
|
1869
|
+
# logger.debug(f"Using client with base_url: {client.base_url}")
|
|
1870
|
+
# logger.debug(f"Request data keys: {list(request_data.keys())}")
|
|
1871
|
+
# logger.debug(
|
|
1872
|
+
# f"Bundle size: {len(request_data.get('bundle', ''))} chars"
|
|
1873
|
+
# if "bundle" in request_data
|
|
1874
|
+
# else "No bundle"
|
|
1875
|
+
# )
|
|
864
1876
|
|
|
865
1877
|
# Note: This should be called on the instance URL, not the orchestrator
|
|
866
1878
|
# The instance has manager URLs for verifier execution
|
|
@@ -868,6 +1880,6 @@ def _execute_verifier_remote(
|
|
|
868
1880
|
|
|
869
1881
|
# Debug the response
|
|
870
1882
|
response_json = response.json()
|
|
871
|
-
logger.debug(f"Verifier execute response: {response_json}")
|
|
1883
|
+
# logger.debug(f"Verifier execute response: {response_json}")
|
|
872
1884
|
|
|
873
1885
|
return VerifiersExecuteResponse(**response_json)
|