evolvingmachines-evolve 0.0.55.dev1355__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.
- bridge/__init__.py +5 -0
- bridge/dist/bridge.bundle.cjs +1275 -0
- evolve/__init__.py +819 -0
- evolve/_http.py +71 -0
- evolve/agent.py +896 -0
- evolve/bridge.py +509 -0
- evolve/browser_credentials.py +265 -0
- evolve/browser_profiles.py +95 -0
- evolve/config.py +600 -0
- evolve/hosted.py +8958 -0
- evolve/integrations.py +173 -0
- evolve/managed_secrets.py +175 -0
- evolve/pipeline/__init__.py +59 -0
- evolve/pipeline/pipeline.py +512 -0
- evolve/pipeline/types.py +286 -0
- evolve/prompts/__init__.py +132 -0
- evolve/prompts/agent_md/judge.md +30 -0
- evolve/prompts/agent_md/reduce.md +7 -0
- evolve/prompts/agent_md/verify.md +33 -0
- evolve/prompts/user/judge.md +1 -0
- evolve/prompts/user/retry_feedback.md +9 -0
- evolve/prompts/user/verify.md +1 -0
- evolve/py.typed +0 -0
- evolve/results.py +315 -0
- evolve/retry.py +133 -0
- evolve/schema.py +107 -0
- evolve/sessions_client.py +167 -0
- evolve/storage_client.py +178 -0
- evolve/swarm/__init__.py +75 -0
- evolve/swarm/results.py +140 -0
- evolve/swarm/swarm.py +2116 -0
- evolve/swarm/types.py +241 -0
- evolve/utils.py +227 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/METADATA +52 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/RECORD +38 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/WHEEL +5 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/licenses/LICENSE +201 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/top_level.txt +2 -0
evolve/bridge.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Node.js bridge subprocess manager for JSON-RPC communication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import atexit
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Global registry of bridge process PIDs for atexit cleanup
|
|
16
|
+
# We store PIDs instead of process objects because asyncio.subprocess.Process
|
|
17
|
+
# doesn't have sync wait() - we use os.kill/os.waitpid for cleanup
|
|
18
|
+
_bridge_pids: List[int] = []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _atexit_cleanup():
|
|
22
|
+
"""Kill all bridge processes on Python exit."""
|
|
23
|
+
for pid in _bridge_pids:
|
|
24
|
+
try:
|
|
25
|
+
os.kill(pid, signal.SIGTERM)
|
|
26
|
+
except (OSError, ProcessLookupError):
|
|
27
|
+
pass # Process already dead
|
|
28
|
+
try:
|
|
29
|
+
# Non-blocking wait to reap zombie
|
|
30
|
+
os.waitpid(pid, os.WNOHANG)
|
|
31
|
+
except (OSError, ChildProcessError):
|
|
32
|
+
pass
|
|
33
|
+
_bridge_pids.clear()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
atexit.register(_atexit_cleanup)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SandboxNotFoundError(Exception):
|
|
40
|
+
"""Raised when sandbox is not found (expired or killed)."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BridgeConnectionError(Exception):
|
|
45
|
+
"""Raised when bridge process fails to start or dies unexpectedly."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BridgeBuildError(Exception):
|
|
50
|
+
"""Raised when bridge build (npm install/build) fails."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BridgeManager:
|
|
55
|
+
"""Manages Node.js subprocess running the JSON-RPC bridge.
|
|
56
|
+
|
|
57
|
+
Uses asyncio.create_subprocess_exec for native async I/O.
|
|
58
|
+
This allows proper cancellation and clean shutdown without blocking threads.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self.process: Optional[asyncio.subprocess.Process] = None
|
|
63
|
+
self.request_id = 0
|
|
64
|
+
self.pending_requests: Dict[int, asyncio.Future] = {}
|
|
65
|
+
# Serialize writes to stdin to avoid interleaved JSON-RPC requests
|
|
66
|
+
self._write_lock = asyncio.Lock()
|
|
67
|
+
# Default RPC timeout to prevent hangs (overridden per-call for long runs)
|
|
68
|
+
self.default_call_timeout_s: float = 120.0
|
|
69
|
+
self.stderr_task: Optional[asyncio.Task] = None
|
|
70
|
+
# Buffers for chunked stdout/stderr events from the bridge.
|
|
71
|
+
# The Node bridge may emit {seq, done} chunks for oversized streams.
|
|
72
|
+
self._stream_buffers: Dict[str, List[str]] = {"stdout": [], "stderr": []}
|
|
73
|
+
self.event_callbacks: Dict[str, List[Callable]] = {
|
|
74
|
+
'stdout': [], # str data
|
|
75
|
+
'stderr': [], # str data
|
|
76
|
+
'content': [], # dict params
|
|
77
|
+
'lifecycle': [], # dict params
|
|
78
|
+
}
|
|
79
|
+
self.reader_task: Optional[asyncio.Task] = None
|
|
80
|
+
self._pid: Optional[int] = None
|
|
81
|
+
|
|
82
|
+
async def start(self):
|
|
83
|
+
"""Start the Node.js bridge process."""
|
|
84
|
+
if self.process is not None:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Find bridge script (bundled version for distribution)
|
|
88
|
+
bridge_dir = Path(__file__).parent.parent / 'bridge'
|
|
89
|
+
bridge_script = bridge_dir / 'dist' / 'bridge.bundle.cjs'
|
|
90
|
+
|
|
91
|
+
# Fallback to unbundled version for development
|
|
92
|
+
if not bridge_script.exists():
|
|
93
|
+
bridge_script = bridge_dir / 'dist' / 'bridge.js'
|
|
94
|
+
|
|
95
|
+
# Auto-build bridge if missing (turnkey experience)
|
|
96
|
+
if not bridge_script.exists():
|
|
97
|
+
await self._build_bridge(bridge_dir)
|
|
98
|
+
|
|
99
|
+
# Start Node.js process with native asyncio subprocess
|
|
100
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
101
|
+
'node', str(bridge_script),
|
|
102
|
+
stdin=asyncio.subprocess.PIPE,
|
|
103
|
+
stdout=asyncio.subprocess.PIPE,
|
|
104
|
+
stderr=asyncio.subprocess.PIPE,
|
|
105
|
+
)
|
|
106
|
+
self._pid = self.process.pid
|
|
107
|
+
|
|
108
|
+
# Register PID for atexit cleanup
|
|
109
|
+
_bridge_pids.append(self._pid)
|
|
110
|
+
|
|
111
|
+
# Start reading responses (native async - no blocking threads)
|
|
112
|
+
self.reader_task = asyncio.create_task(self._read_responses())
|
|
113
|
+
# Drain stderr to avoid pipe backpressure
|
|
114
|
+
if self.process.stderr is not None:
|
|
115
|
+
self.stderr_task = asyncio.create_task(self._drain_stderr())
|
|
116
|
+
|
|
117
|
+
async def _build_bridge(self, bridge_dir: Path):
|
|
118
|
+
"""Build the bridge if missing (first run experience)."""
|
|
119
|
+
import shutil
|
|
120
|
+
import subprocess
|
|
121
|
+
|
|
122
|
+
# Check if Node.js and npm are installed
|
|
123
|
+
if not shutil.which('node'):
|
|
124
|
+
raise BridgeBuildError(
|
|
125
|
+
"Bridge build failed: Node.js not found in PATH.\n"
|
|
126
|
+
"Evolve requires Node.js 18+ to run the TypeScript bridge.\n"
|
|
127
|
+
"Install from https://nodejs.org/ or run 'make build' manually from packages/sdk-py/."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not shutil.which('npm'):
|
|
131
|
+
raise BridgeBuildError(
|
|
132
|
+
"Bridge build failed: npm not found in PATH.\n"
|
|
133
|
+
"npm is usually installed with Node.js - check your Node.js installation.\n"
|
|
134
|
+
"Alternatively, run 'make build' manually from packages/sdk-py/."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
logger.info("First run: building Node.js bridge...")
|
|
138
|
+
try:
|
|
139
|
+
# Run npm install/build in executor to avoid blocking event loop
|
|
140
|
+
loop = asyncio.get_running_loop()
|
|
141
|
+
|
|
142
|
+
# Install dependencies
|
|
143
|
+
await loop.run_in_executor(
|
|
144
|
+
None,
|
|
145
|
+
lambda: subprocess.run(
|
|
146
|
+
['npm', 'install'],
|
|
147
|
+
cwd=bridge_dir,
|
|
148
|
+
check=True,
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Build bridge
|
|
155
|
+
await loop.run_in_executor(
|
|
156
|
+
None,
|
|
157
|
+
lambda: subprocess.run(
|
|
158
|
+
['npm', 'run', 'build'],
|
|
159
|
+
cwd=bridge_dir,
|
|
160
|
+
check=True,
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
logger.info("Bridge built successfully")
|
|
166
|
+
except subprocess.CalledProcessError as e:
|
|
167
|
+
raise BridgeBuildError(
|
|
168
|
+
f"Bridge build failed during npm execution.\n"
|
|
169
|
+
f"Error: {e.stderr}\n"
|
|
170
|
+
f"Try running 'make build' manually from packages/sdk-py/ to see the full error."
|
|
171
|
+
) from e
|
|
172
|
+
|
|
173
|
+
async def stop(self):
|
|
174
|
+
"""Stop the Node.js bridge process."""
|
|
175
|
+
if self.process is None:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Best-effort flush of observability events before shutdown.
|
|
179
|
+
# Covers edge cases where kill() RPC failed but bridge is still alive.
|
|
180
|
+
try:
|
|
181
|
+
await self.call('flush_observability', timeout_s=5.0)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Note: We intentionally do NOT clear event_callbacks here
|
|
186
|
+
# User-registered callbacks should persist across bridge restarts
|
|
187
|
+
|
|
188
|
+
# Remove from atexit registry
|
|
189
|
+
if self._pid and self._pid in _bridge_pids:
|
|
190
|
+
_bridge_pids.remove(self._pid)
|
|
191
|
+
|
|
192
|
+
# Cancel reader tasks first (they will exit cleanly now that process is terminating)
|
|
193
|
+
if self.reader_task:
|
|
194
|
+
self.reader_task.cancel()
|
|
195
|
+
try:
|
|
196
|
+
await self.reader_task
|
|
197
|
+
except asyncio.CancelledError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if self.stderr_task:
|
|
201
|
+
self.stderr_task.cancel()
|
|
202
|
+
try:
|
|
203
|
+
await self.stderr_task
|
|
204
|
+
except asyncio.CancelledError:
|
|
205
|
+
pass
|
|
206
|
+
self.stderr_task = None
|
|
207
|
+
|
|
208
|
+
# Terminate process
|
|
209
|
+
try:
|
|
210
|
+
self.process.terminate()
|
|
211
|
+
await asyncio.wait_for(self.process.wait(), timeout=5)
|
|
212
|
+
except asyncio.TimeoutError:
|
|
213
|
+
self.process.kill()
|
|
214
|
+
await self.process.wait()
|
|
215
|
+
|
|
216
|
+
self.process = None
|
|
217
|
+
self.reader_task = None
|
|
218
|
+
self._pid = None
|
|
219
|
+
|
|
220
|
+
def on(self, event_type: str, callback: Callable):
|
|
221
|
+
"""Register event callback.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValueError: If event_type is not supported.
|
|
225
|
+
"""
|
|
226
|
+
if event_type not in self.event_callbacks:
|
|
227
|
+
supported = ", ".join(sorted(self.event_callbacks))
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Unsupported event type '{event_type}'. Supported event types: {supported}"
|
|
230
|
+
)
|
|
231
|
+
self.event_callbacks[event_type].append(callback)
|
|
232
|
+
|
|
233
|
+
async def call(
|
|
234
|
+
self,
|
|
235
|
+
method: str,
|
|
236
|
+
params: Optional[Dict[str, Any]] = None,
|
|
237
|
+
timeout_s: Optional[float] = None,
|
|
238
|
+
) -> Any:
|
|
239
|
+
"""Call a JSON-RPC method and wait for response.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
method: JSON-RPC method name
|
|
243
|
+
params: JSON-RPC params dict
|
|
244
|
+
timeout_s: Optional timeout in seconds. If None, uses default_call_timeout_s.
|
|
245
|
+
"""
|
|
246
|
+
if self.process is None or self.process.stdin is None:
|
|
247
|
+
raise BridgeConnectionError("Bridge not started. Call start() first.")
|
|
248
|
+
|
|
249
|
+
async with self._write_lock:
|
|
250
|
+
self.request_id += 1
|
|
251
|
+
request_id = self.request_id
|
|
252
|
+
|
|
253
|
+
request = {
|
|
254
|
+
'jsonrpc': '2.0',
|
|
255
|
+
'method': method,
|
|
256
|
+
'params': params or {},
|
|
257
|
+
'id': request_id,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Create future for response
|
|
261
|
+
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
|
|
262
|
+
self.pending_requests[request_id] = future
|
|
263
|
+
|
|
264
|
+
# Send request (native async write)
|
|
265
|
+
payload = json.dumps(request).encode('utf-8')
|
|
266
|
+
frame = len(payload).to_bytes(4, byteorder='big') + payload
|
|
267
|
+
self.process.stdin.write(frame)
|
|
268
|
+
await self.process.stdin.drain()
|
|
269
|
+
|
|
270
|
+
# Wait for response (error handling done in _handle_response)
|
|
271
|
+
timeout = timeout_s if timeout_s is not None else self.default_call_timeout_s
|
|
272
|
+
try:
|
|
273
|
+
return await asyncio.wait_for(asyncio.shield(future), timeout=timeout)
|
|
274
|
+
except asyncio.CancelledError:
|
|
275
|
+
removed = self.pending_requests.pop(request_id, None)
|
|
276
|
+
if removed is not None and not future.done():
|
|
277
|
+
future.cancel()
|
|
278
|
+
raise
|
|
279
|
+
except asyncio.TimeoutError as e:
|
|
280
|
+
# Drop pending request to avoid leaks; late response will be ignored.
|
|
281
|
+
removed = self.pending_requests.pop(request_id, None)
|
|
282
|
+
if removed is not None and not future.done():
|
|
283
|
+
future.cancel()
|
|
284
|
+
raise BridgeConnectionError(
|
|
285
|
+
f"Bridge call timed out after {timeout:.1f}s: {method}"
|
|
286
|
+
) from e
|
|
287
|
+
|
|
288
|
+
async def _read_responses(self):
|
|
289
|
+
"""Read framed JSON-RPC messages from bridge stdout.
|
|
290
|
+
|
|
291
|
+
Uses native asyncio reads - fully cancellable, no blocking threads.
|
|
292
|
+
"""
|
|
293
|
+
if self.process is None or self.process.stdout is None:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# 50MB cap on incoming frames. This applies to ALL bridge responses including
|
|
297
|
+
# RPC results (run().stdout, get_output_files(), etc.). If a response exceeds
|
|
298
|
+
# 50MB, the bridge connection fails. This is stricter than the TS SDK which
|
|
299
|
+
# has no response size limit. For very large outputs, consider streaming via
|
|
300
|
+
# stdout/stderr events or fetching files individually.
|
|
301
|
+
max_frame_bytes = 50 * 1024 * 1024
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
async def read_exact(n: int) -> Optional[bytes]:
|
|
305
|
+
"""Read exactly n bytes from stdout."""
|
|
306
|
+
chunks: List[bytes] = []
|
|
307
|
+
remaining = n
|
|
308
|
+
while remaining > 0:
|
|
309
|
+
chunk = await self.process.stdout.read(remaining)
|
|
310
|
+
if not chunk:
|
|
311
|
+
return None
|
|
312
|
+
chunks.append(chunk)
|
|
313
|
+
remaining -= len(chunk)
|
|
314
|
+
return b"".join(chunks)
|
|
315
|
+
|
|
316
|
+
while True:
|
|
317
|
+
header = await read_exact(4)
|
|
318
|
+
if not header:
|
|
319
|
+
break
|
|
320
|
+
length = int.from_bytes(header, byteorder='big')
|
|
321
|
+
if length <= 0 or length > max_frame_bytes:
|
|
322
|
+
logger.error(f"Invalid frame length from bridge: {length}")
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
payload = await read_exact(length)
|
|
326
|
+
if payload is None:
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
text = payload.decode('utf-8')
|
|
331
|
+
message = json.loads(text)
|
|
332
|
+
except Exception:
|
|
333
|
+
logger.exception("Failed to parse bridge frame")
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
if isinstance(message, dict) and message.get('method') == 'event':
|
|
337
|
+
self._handle_event(message.get('params') or {})
|
|
338
|
+
elif isinstance(message, dict) and 'id' in message:
|
|
339
|
+
self._handle_response(message)
|
|
340
|
+
|
|
341
|
+
except asyncio.CancelledError:
|
|
342
|
+
# Clean cancellation - expected during stop()
|
|
343
|
+
raise
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.error(f"Bridge reader died: {e}")
|
|
346
|
+
finally:
|
|
347
|
+
# Fail all pending requests so callers don't hang
|
|
348
|
+
error = BridgeConnectionError("Bridge process terminated unexpectedly")
|
|
349
|
+
for request_id, future in list(self.pending_requests.items()):
|
|
350
|
+
if not future.done():
|
|
351
|
+
future.set_exception(error)
|
|
352
|
+
self.pending_requests.clear()
|
|
353
|
+
|
|
354
|
+
async def _drain_stderr(self):
|
|
355
|
+
"""Drain bridge stderr to prevent blocking.
|
|
356
|
+
|
|
357
|
+
Uses native asyncio reads - fully cancellable.
|
|
358
|
+
"""
|
|
359
|
+
if self.process is None or self.process.stderr is None:
|
|
360
|
+
return
|
|
361
|
+
try:
|
|
362
|
+
while True:
|
|
363
|
+
line = await self.process.stderr.readline()
|
|
364
|
+
if not line:
|
|
365
|
+
break
|
|
366
|
+
try:
|
|
367
|
+
text = line.decode("utf-8", errors="ignore").rstrip()
|
|
368
|
+
except Exception:
|
|
369
|
+
text = str(line).rstrip()
|
|
370
|
+
if text:
|
|
371
|
+
logger.debug(f"[bridge stderr] {text}")
|
|
372
|
+
except asyncio.CancelledError:
|
|
373
|
+
pass
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.debug(f"Bridge stderr drain died: {e}")
|
|
376
|
+
|
|
377
|
+
def _handle_event(self, params: Dict[str, Any]):
|
|
378
|
+
"""Handle event notification from bridge."""
|
|
379
|
+
event_type = params.get('type')
|
|
380
|
+
callbacks = self.event_callbacks.get(event_type, [])
|
|
381
|
+
|
|
382
|
+
if event_type in ('stdout', 'stderr'):
|
|
383
|
+
data = params.get('data', '')
|
|
384
|
+
seq = params.get('seq')
|
|
385
|
+
done = params.get('done')
|
|
386
|
+
|
|
387
|
+
# If chunk metadata is present, reassemble to preserve "NDJSON line" semantics.
|
|
388
|
+
if seq is not None or done is not None:
|
|
389
|
+
buf = self._stream_buffers.setdefault(event_type, [])
|
|
390
|
+
if seq == 0 and buf:
|
|
391
|
+
# Best-effort flush of previous incomplete sequence.
|
|
392
|
+
prev = "".join(buf)
|
|
393
|
+
buf.clear()
|
|
394
|
+
for callback in callbacks:
|
|
395
|
+
try:
|
|
396
|
+
callback(prev)
|
|
397
|
+
except Exception:
|
|
398
|
+
logger.exception("Error in %s callback", event_type)
|
|
399
|
+
|
|
400
|
+
buf.append(data)
|
|
401
|
+
if done:
|
|
402
|
+
full = "".join(buf)
|
|
403
|
+
buf.clear()
|
|
404
|
+
for callback in callbacks:
|
|
405
|
+
try:
|
|
406
|
+
callback(full)
|
|
407
|
+
except Exception:
|
|
408
|
+
logger.exception("Error in %s callback", event_type)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# No chunk metadata → emit directly.
|
|
412
|
+
for callback in callbacks:
|
|
413
|
+
try:
|
|
414
|
+
callback(data)
|
|
415
|
+
except Exception:
|
|
416
|
+
logger.exception("Error in %s callback", event_type)
|
|
417
|
+
elif event_type in ('content', 'lifecycle'):
|
|
418
|
+
for callback in callbacks:
|
|
419
|
+
try:
|
|
420
|
+
callback(params)
|
|
421
|
+
except Exception:
|
|
422
|
+
logger.exception("Error in %s callback", event_type)
|
|
423
|
+
|
|
424
|
+
def _handle_response(self, message: Dict[str, Any]):
|
|
425
|
+
"""Handle JSON-RPC response."""
|
|
426
|
+
request_id = message.get('id')
|
|
427
|
+
if request_id is None or request_id not in self.pending_requests:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
future = self.pending_requests.pop(request_id)
|
|
431
|
+
if future.done():
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
if 'error' in message:
|
|
435
|
+
error = message['error']
|
|
436
|
+
error_code = error.get('code', -32603)
|
|
437
|
+
error_message = error.get('message', 'Unknown error')
|
|
438
|
+
|
|
439
|
+
# Check for NotFoundError (code -32001 or message pattern)
|
|
440
|
+
if error_code == -32001 or 'not found' in error_message.lower():
|
|
441
|
+
try:
|
|
442
|
+
future.set_exception(SandboxNotFoundError(error_message))
|
|
443
|
+
except asyncio.InvalidStateError:
|
|
444
|
+
pass
|
|
445
|
+
else:
|
|
446
|
+
try:
|
|
447
|
+
future.set_exception(Exception(error_message))
|
|
448
|
+
except asyncio.InvalidStateError:
|
|
449
|
+
pass
|
|
450
|
+
else:
|
|
451
|
+
try:
|
|
452
|
+
future.set_result(message.get('result'))
|
|
453
|
+
except asyncio.InvalidStateError:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
# =========================================================================
|
|
457
|
+
# MULTI-INSTANCE METHODS (for Swarm)
|
|
458
|
+
# =========================================================================
|
|
459
|
+
|
|
460
|
+
async def create_instance(
|
|
461
|
+
self,
|
|
462
|
+
instance_id: str,
|
|
463
|
+
params: Dict[str, Any],
|
|
464
|
+
timeout_s: Optional[float] = None,
|
|
465
|
+
) -> Any:
|
|
466
|
+
"""Create a new Evolve instance in the bridge."""
|
|
467
|
+
return await self.call(
|
|
468
|
+
'create_instance',
|
|
469
|
+
{'instance_id': instance_id, **params},
|
|
470
|
+
timeout_s=timeout_s,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
async def run_on_instance(
|
|
474
|
+
self,
|
|
475
|
+
instance_id: str,
|
|
476
|
+
prompt: str,
|
|
477
|
+
timeout_ms: Optional[int] = None,
|
|
478
|
+
call_timeout_s: Optional[float] = None,
|
|
479
|
+
) -> Any:
|
|
480
|
+
"""Run prompt on a specific Evolve instance."""
|
|
481
|
+
params = {'instance_id': instance_id, 'prompt': prompt}
|
|
482
|
+
if timeout_ms is not None:
|
|
483
|
+
params['timeout_ms'] = timeout_ms
|
|
484
|
+
return await self.call('run_on_instance', params, timeout_s=call_timeout_s)
|
|
485
|
+
|
|
486
|
+
async def get_output_on_instance(
|
|
487
|
+
self,
|
|
488
|
+
instance_id: str,
|
|
489
|
+
recursive: bool = False,
|
|
490
|
+
timeout_s: Optional[float] = None,
|
|
491
|
+
) -> Any:
|
|
492
|
+
"""Get output files from a specific Evolve instance."""
|
|
493
|
+
return await self.call(
|
|
494
|
+
'get_output_on_instance',
|
|
495
|
+
{'instance_id': instance_id, 'recursive': recursive},
|
|
496
|
+
timeout_s=timeout_s,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
async def kill_instance(
|
|
500
|
+
self,
|
|
501
|
+
instance_id: str,
|
|
502
|
+
timeout_s: Optional[float] = None,
|
|
503
|
+
) -> Any:
|
|
504
|
+
"""Kill and remove a specific Evolve instance."""
|
|
505
|
+
return await self.call(
|
|
506
|
+
'kill_instance',
|
|
507
|
+
{'instance_id': instance_id},
|
|
508
|
+
timeout_s=timeout_s,
|
|
509
|
+
)
|