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/agent.py
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"""Main Evolve class for Python SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Type, Union
|
|
7
|
+
|
|
8
|
+
from .bridge import BridgeManager, SandboxNotFoundError
|
|
9
|
+
from .config import AgentConfig, AgentPluginConfig, BrowserConfig, BrowserCredentialsConfig, IntegrationsSetup, ManagedSecretRef, SandboxCreateOptions, SandboxProvider, SchemaOptions, StorageConfig, WorkspaceMode
|
|
10
|
+
from .results import AgentResponse, CheckpointInfo, ExecuteResult, OutputResult, RunCost, SessionCost, SessionStatus
|
|
11
|
+
from .storage_client import StorageClient
|
|
12
|
+
from . import integrations as integrations_helpers
|
|
13
|
+
from .schema import is_pydantic_model, is_dataclass, to_json_schema, validate_and_parse
|
|
14
|
+
from .utils import _encode_files_for_transport, _decode_files_from_transport, _filter_none, _parse_checkpoint, _require_checkpoint
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Evolve:
|
|
18
|
+
"""Evolve agent orchestrator.
|
|
19
|
+
|
|
20
|
+
Provides a Pythonic interface to the TypeScript Evolve SDK via JSON-RPC bridge.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> from evolve import Evolve
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Minimal usage - uses EVOLVE_API_KEY and E2B_API_KEY env vars
|
|
26
|
+
>>> async with Evolve() as evolve:
|
|
27
|
+
... result = await evolve.run(prompt='Analyze data.csv')
|
|
28
|
+
... output = await evolve.get_output_files()
|
|
29
|
+
... for name, content in output.files.items():
|
|
30
|
+
... print(f'{name}: {len(content)} bytes')
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Or with explicit config (E2B)
|
|
33
|
+
>>> from evolve import AgentConfig, E2BProvider
|
|
34
|
+
>>> evolve = Evolve(
|
|
35
|
+
... config=AgentConfig(type='codex', api_key='sk-...'),
|
|
36
|
+
... sandbox=E2BProvider(api_key='...')
|
|
37
|
+
... )
|
|
38
|
+
>>>
|
|
39
|
+
>>> # Or with Daytona
|
|
40
|
+
>>> from evolve import DaytonaProvider
|
|
41
|
+
>>> evolve = Evolve(sandbox=DaytonaProvider(api_key='...'))
|
|
42
|
+
>>> # Or with Modal
|
|
43
|
+
>>> from evolve import ModalProvider
|
|
44
|
+
>>> evolve = Evolve(sandbox=ModalProvider())
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Static helpers for Integrations pre-auth flows (no instance required)
|
|
48
|
+
integrations = integrations_helpers
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
config: Optional[AgentConfig] = None,
|
|
53
|
+
sandbox: Optional[SandboxProvider] = None,
|
|
54
|
+
working_directory: str = '/home/user/workspace',
|
|
55
|
+
workspace_mode: WorkspaceMode = 'knowledge',
|
|
56
|
+
system_prompt: Optional[str] = None,
|
|
57
|
+
context: Optional[Dict[str, Union[str, bytes]]] = None,
|
|
58
|
+
files: Optional[Dict[str, Union[str, bytes]]] = None,
|
|
59
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
60
|
+
skills: Optional[List[str]] = None,
|
|
61
|
+
secrets: Optional[Dict[str, str]] = None,
|
|
62
|
+
managed_secrets: Optional[List[Union[ManagedSecretRef, Dict[str, Any]]]] = None,
|
|
63
|
+
sandbox_id: Optional[str] = None,
|
|
64
|
+
session_tag_prefix: Optional[str] = None,
|
|
65
|
+
schema: Optional[Union[Type, Dict[str, Any]]] = None,
|
|
66
|
+
schema_options: Optional[SchemaOptions] = None,
|
|
67
|
+
integrations: Optional[IntegrationsSetup] = None,
|
|
68
|
+
storage: Optional[StorageConfig] = None,
|
|
69
|
+
browser: Optional[BrowserConfig] = None,
|
|
70
|
+
browser_credentials: Optional[BrowserCredentialsConfig] = None,
|
|
71
|
+
plugins: Optional[Union[AgentPluginConfig, List[AgentPluginConfig]]] = None,
|
|
72
|
+
sandbox_create_options: Optional[SandboxCreateOptions] = None,
|
|
73
|
+
):
|
|
74
|
+
"""Initialize Evolve.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: Agent configuration (optional - defaults to EVOLVE_API_KEY env var with 'claude' type)
|
|
78
|
+
sandbox: Sandbox provider (optional - auto-resolves from env vars:
|
|
79
|
+
E2B_API_KEY → E2B direct, DAYTONA_API_KEY → Daytona direct,
|
|
80
|
+
MODAL_TOKEN_ID+MODAL_TOKEN_SECRET → Modal direct,
|
|
81
|
+
EVOLVE_API_KEY → E2B via gateway. User sandbox keys take priority.)
|
|
82
|
+
working_directory: Working directory in sandbox (default: /home/user/workspace)
|
|
83
|
+
workspace_mode: 'knowledge', 'swe', or 'task'. Task mode leaves the
|
|
84
|
+
task-owned working directory untouched.
|
|
85
|
+
system_prompt: Custom system prompt (appended to default in 'knowledge' mode, sole prompt in 'swe' mode)
|
|
86
|
+
context: Files to upload to context/ folder on first run - { "filename.txt": "content" }
|
|
87
|
+
files: Files to upload to working directory on first run - { "scripts/run.sh": "content" }
|
|
88
|
+
mcp_servers: MCP server configurations
|
|
89
|
+
skills: Skill references to mount into the sandbox — real references,
|
|
90
|
+
no built-in catalog: 'skills.sh/<owner>/<repo>[/<skill>]',
|
|
91
|
+
'org/repo[@ref]', an https git URL, or a local folder path
|
|
92
|
+
containing SKILL.md. Resolved pinned + content-cached by the
|
|
93
|
+
SDK's one resolver and mounted where the harness discovers them.
|
|
94
|
+
secrets: Environment variables for sandbox
|
|
95
|
+
managed_secrets: Dashboard-stored managed secrets to attach
|
|
96
|
+
({'name', 'label'?, 'as'?} or ManagedSecretRef). Brokered
|
|
97
|
+
secrets ride as opaque placeholder env vars behind the
|
|
98
|
+
egress proxy; direct secrets land as raw env values
|
|
99
|
+
sandbox_id: Existing sandbox ID to reconnect to
|
|
100
|
+
session_tag_prefix: Optional semantic label for observability log files (e.g., 'experiment-7')
|
|
101
|
+
schema: Schema for structured output - Pydantic model, dataclass, or JSON Schema dict
|
|
102
|
+
schema_options: Validation options (mode: 'strict' or 'loose', default: 'loose')
|
|
103
|
+
integrations: managed app integrations setup
|
|
104
|
+
storage: Storage configuration for checkpoint persistence (BYOK S3 or gateway mode)
|
|
105
|
+
browser: Browser automation provider. Use {'provider': 'agent-browser', 'remote': True}
|
|
106
|
+
for managed remote browser automation.
|
|
107
|
+
browser_credentials: Saved browser login MCP setup. Requires managed remote agent-browser.
|
|
108
|
+
plugins: Agent plugins/extensions to install in the sandbox user profile before first run.
|
|
109
|
+
sandbox_create_options: Provider-neutral image, env, metadata, timeout,
|
|
110
|
+
working-directory and outbound-network options for fresh sandbox creation.
|
|
111
|
+
"""
|
|
112
|
+
self.config = config
|
|
113
|
+
self.sandbox = sandbox
|
|
114
|
+
self.sandbox_create_options = sandbox_create_options
|
|
115
|
+
self.working_directory = working_directory
|
|
116
|
+
self.workspace_mode = workspace_mode
|
|
117
|
+
self.system_prompt = system_prompt
|
|
118
|
+
self.context = context
|
|
119
|
+
self.files = files
|
|
120
|
+
self.mcp_servers = mcp_servers
|
|
121
|
+
self.browser = self._normalize_browser(browser)
|
|
122
|
+
self.browser_credentials = browser_credentials
|
|
123
|
+
self.skills = skills
|
|
124
|
+
self.secrets = secrets
|
|
125
|
+
self.managed_secrets = self._normalize_managed_secrets(managed_secrets)
|
|
126
|
+
self.sandbox_id = sandbox_id
|
|
127
|
+
self.session_tag_prefix = session_tag_prefix
|
|
128
|
+
self.schema_options = schema_options or SchemaOptions()
|
|
129
|
+
self._integrations = integrations
|
|
130
|
+
self._storage_config = storage
|
|
131
|
+
self.plugins = self._normalize_plugins(plugins)
|
|
132
|
+
|
|
133
|
+
# Schema handling: store original + convert to JSON Schema
|
|
134
|
+
self._schema = schema
|
|
135
|
+
self._schema_json = to_json_schema(schema)
|
|
136
|
+
|
|
137
|
+
self.bridge = BridgeManager()
|
|
138
|
+
self._initialized = False
|
|
139
|
+
self._init_lock = asyncio.Lock()
|
|
140
|
+
|
|
141
|
+
async def _ensure_initialized(self):
|
|
142
|
+
"""Ensure bridge is started and agent is initialized."""
|
|
143
|
+
async with self._init_lock:
|
|
144
|
+
if self._initialized:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
await self.bridge.start()
|
|
148
|
+
|
|
149
|
+
# Build params with _filter_none to exclude None values
|
|
150
|
+
# TS SDK resolves defaults from env vars when not provided
|
|
151
|
+
params = _filter_none({
|
|
152
|
+
# Agent config (optional - TS SDK resolves from env vars)
|
|
153
|
+
'agent_type': self.config.type if self.config else None,
|
|
154
|
+
'api_key': self.config.api_key if self.config else None,
|
|
155
|
+
'provider_api_key': self.config.provider_api_key if self.config else None,
|
|
156
|
+
'oauth_token': self.config.oauth_token if self.config else None,
|
|
157
|
+
'provider_base_url': self.config.provider_base_url if self.config else None,
|
|
158
|
+
'model': self.config.model if self.config else None,
|
|
159
|
+
'reasoning_effort': self.config.reasoning_effort if self.config else None,
|
|
160
|
+
'max_context_size': self.config.max_context_size if self.config else None,
|
|
161
|
+
'agent_config': self.config.config if self.config else None,
|
|
162
|
+
'agent_preset': self.config.preset if self.config else None,
|
|
163
|
+
# Sandbox (optional - TS SDK auto-resolves from EVOLVE_API_KEY/E2B_API_KEY/DAYTONA_API_KEY)
|
|
164
|
+
'sandbox_provider': {'type': self.sandbox.type, 'config': self.sandbox.config} if self.sandbox else None,
|
|
165
|
+
'sandbox_create_options': self.sandbox_create_options,
|
|
166
|
+
# Other settings
|
|
167
|
+
'working_directory': self.working_directory,
|
|
168
|
+
'workspace_mode': self.workspace_mode,
|
|
169
|
+
'system_prompt': self.system_prompt,
|
|
170
|
+
'context': _encode_files_for_transport(self.context) if self.context else None,
|
|
171
|
+
'files': _encode_files_for_transport(self.files) if self.files else None,
|
|
172
|
+
'mcp_servers': self.mcp_servers,
|
|
173
|
+
'browser': self.browser,
|
|
174
|
+
'browser_credentials': self.browser_credentials.to_dict() if self.browser_credentials else None,
|
|
175
|
+
'plugins': self.plugins,
|
|
176
|
+
'skills': self.skills,
|
|
177
|
+
'secrets': self.secrets,
|
|
178
|
+
'managed_secrets': self.managed_secrets,
|
|
179
|
+
'sandbox_id': self.sandbox_id,
|
|
180
|
+
'session_tag_prefix': self.session_tag_prefix,
|
|
181
|
+
'schema': self._schema_json,
|
|
182
|
+
'schema_options': {'mode': self.schema_options.mode} if self._schema_json else None,
|
|
183
|
+
# Managed integrations
|
|
184
|
+
'integrations': self._integrations.to_dict() if self._integrations else None,
|
|
185
|
+
# Storage / Checkpointing
|
|
186
|
+
'storage': self._storage_config.to_dict() if self._storage_config else None,
|
|
187
|
+
# Always forward events
|
|
188
|
+
'forward_stdout': True,
|
|
189
|
+
'forward_stderr': True,
|
|
190
|
+
'forward_content': True,
|
|
191
|
+
'forward_lifecycle': True,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await self.bridge.call('initialize', params, timeout_s=self._get_rpc_timeout_s(None))
|
|
195
|
+
self._initialized = True
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _normalize_browser(browser: Optional[BrowserConfig]) -> Optional[BrowserConfig]:
|
|
199
|
+
"""Normalize browser automation shorthand for bridge transport."""
|
|
200
|
+
if browser is None:
|
|
201
|
+
return None
|
|
202
|
+
if browser in ('browser-use', 'actionbook', 'agent-browser'):
|
|
203
|
+
return browser
|
|
204
|
+
if isinstance(browser, dict):
|
|
205
|
+
provider = browser.get('provider', 'agent-browser')
|
|
206
|
+
if provider not in ('actionbook', 'agent-browser'):
|
|
207
|
+
raise ValueError("browser provider must be 'actionbook' or 'agent-browser'")
|
|
208
|
+
normalized = dict(browser)
|
|
209
|
+
normalized['provider'] = provider
|
|
210
|
+
if 'profile' in normalized and 'remote' not in normalized and browser.get('provider') is None:
|
|
211
|
+
normalized['remote'] = True
|
|
212
|
+
if 'profile' in normalized and normalized.get('remote') is not True:
|
|
213
|
+
raise ValueError("browser profile requires managed remote browser mode")
|
|
214
|
+
return normalized
|
|
215
|
+
raise ValueError("browser must be 'browser-use', 'actionbook', 'agent-browser', a managed browser config dict, or None")
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _normalize_managed_secrets(
|
|
219
|
+
managed_secrets: Optional[List[Union[ManagedSecretRef, Dict[str, Any]]]]
|
|
220
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
221
|
+
if managed_secrets is None:
|
|
222
|
+
return None
|
|
223
|
+
if not managed_secrets:
|
|
224
|
+
raise ValueError('managed_secrets requires at least one secret')
|
|
225
|
+
normalized: List[Dict[str, Any]] = []
|
|
226
|
+
for secret in managed_secrets:
|
|
227
|
+
if isinstance(secret, ManagedSecretRef):
|
|
228
|
+
normalized.append(secret.to_dict())
|
|
229
|
+
elif isinstance(secret, dict):
|
|
230
|
+
item = dict(secret)
|
|
231
|
+
if 'as_name' in item:
|
|
232
|
+
item['as'] = item.pop('as_name')
|
|
233
|
+
normalized.append(item)
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError('managed_secrets entries must be ManagedSecretRef or dict')
|
|
236
|
+
return normalized
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _normalize_plugins(
|
|
240
|
+
plugins: Optional[Union[AgentPluginConfig, List[AgentPluginConfig]]]
|
|
241
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
242
|
+
"""Normalize plugin config and convert Python snake_case flags for TS transport."""
|
|
243
|
+
if plugins is None:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
if isinstance(plugins, dict) or is_dataclass(plugins):
|
|
247
|
+
raw_plugins: List[Any] = [plugins]
|
|
248
|
+
elif isinstance(plugins, list):
|
|
249
|
+
raw_plugins = plugins
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError('plugins must be a plugin dict, list of plugin dicts, or None')
|
|
252
|
+
|
|
253
|
+
normalized: List[Dict[str, Any]] = []
|
|
254
|
+
for plugin in raw_plugins:
|
|
255
|
+
if is_dataclass(plugin):
|
|
256
|
+
item = asdict(plugin)
|
|
257
|
+
elif isinstance(plugin, dict):
|
|
258
|
+
item = dict(plugin)
|
|
259
|
+
else:
|
|
260
|
+
raise ValueError('each plugin must be a dict-like object')
|
|
261
|
+
|
|
262
|
+
for python_key, ts_key in (
|
|
263
|
+
('auto_update', 'autoUpdate'),
|
|
264
|
+
('pre_release', 'preRelease'),
|
|
265
|
+
('skip_settings', 'skipSettings'),
|
|
266
|
+
):
|
|
267
|
+
if python_key in item:
|
|
268
|
+
item[ts_key] = item.pop(python_key)
|
|
269
|
+
|
|
270
|
+
normalized.append(item)
|
|
271
|
+
|
|
272
|
+
return normalized
|
|
273
|
+
|
|
274
|
+
def on(
|
|
275
|
+
self,
|
|
276
|
+
event_type: Literal['stdout', 'stderr', 'content', 'lifecycle'],
|
|
277
|
+
callback: Callable[[Any], None]
|
|
278
|
+
):
|
|
279
|
+
"""Register event callback.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
event_type: Event type ('stdout' | 'stderr' | 'content' | 'lifecycle')
|
|
283
|
+
callback: Callback function invoked with the event payload
|
|
284
|
+
(str for stdout/stderr, dict for content/lifecycle)
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
>>> evolve.on('stdout', lambda data: print(data, end=''))
|
|
288
|
+
>>> evolve.on('stderr', lambda data: print(f'[ERR] {data}', end=''))
|
|
289
|
+
>>> evolve.on('content', lambda event: print(event['update']['sessionUpdate']))
|
|
290
|
+
>>> evolve.on('lifecycle', lambda event: print(event['reason']))
|
|
291
|
+
"""
|
|
292
|
+
self.bridge.on(event_type, callback)
|
|
293
|
+
|
|
294
|
+
def _get_rpc_timeout_s(self, timeout_ms: Optional[int]) -> float:
|
|
295
|
+
"""Compute an RPC timeout aligned with sandbox execution timeout."""
|
|
296
|
+
if timeout_ms is None:
|
|
297
|
+
timeout_ms = getattr(self.sandbox, "timeout_ms", 3600000) if self.sandbox else 3600000
|
|
298
|
+
# Add small grace to allow bridge/agent cleanup after sandbox timeout.
|
|
299
|
+
return timeout_ms / 1000.0 + 30.0
|
|
300
|
+
|
|
301
|
+
async def run(
|
|
302
|
+
self,
|
|
303
|
+
prompt: str,
|
|
304
|
+
timeout_ms: Optional[int] = None,
|
|
305
|
+
background: bool = False,
|
|
306
|
+
from_checkpoint: Optional[str] = None,
|
|
307
|
+
checkpoint_comment: Optional[str] = None,
|
|
308
|
+
resume: Optional[bool] = None,
|
|
309
|
+
) -> AgentResponse:
|
|
310
|
+
"""Run AI-assisted task (agent decides and acts).
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
prompt: Task description
|
|
314
|
+
timeout_ms: Optional timeout in milliseconds (default: 1 hour)
|
|
315
|
+
background: Run in background (default: False). If True, returns
|
|
316
|
+
immediately with a handshake response (`exit_code=0`).
|
|
317
|
+
Final completion is delivered asynchronously via
|
|
318
|
+
lifecycle events or status polling.
|
|
319
|
+
from_checkpoint: Restore from checkpoint ID before running (requires storage).
|
|
320
|
+
Use 'latest' to restore the most recent checkpoint.
|
|
321
|
+
checkpoint_comment: Optional label for the auto-checkpoint after this run
|
|
322
|
+
resume: Whether this run continues the agent's previous conversation
|
|
323
|
+
in this sandbox. Omitted, the first run in a sandbox is fresh
|
|
324
|
+
and every run after it resumes. Pass False to force a FRESH
|
|
325
|
+
conversation in a sandbox the agent has already run in — the
|
|
326
|
+
shape a sequence of independent tasks against one shared
|
|
327
|
+
sandbox needs, where the environment persists but the context
|
|
328
|
+
should not.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
AgentResponse with sandbox_id, exit_code, stdout, stderr, checkpoint
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> result = await evolve.run(prompt='Analyze data and create report', timeout_ms=600000)
|
|
335
|
+
>>> # Background execution
|
|
336
|
+
>>> result = await evolve.run(prompt='Long task', background=True)
|
|
337
|
+
>>> # Restore from checkpoint
|
|
338
|
+
>>> result = await evolve.run(prompt='Continue work', from_checkpoint='latest')
|
|
339
|
+
>>> # Independent tasks in one sandbox: keep the box, drop the context
|
|
340
|
+
>>> result = await evolve.run(prompt='Second, unrelated task', resume=False)
|
|
341
|
+
"""
|
|
342
|
+
await self._ensure_initialized()
|
|
343
|
+
|
|
344
|
+
params: Dict[str, Any] = {
|
|
345
|
+
'prompt': prompt,
|
|
346
|
+
}
|
|
347
|
+
if timeout_ms is not None:
|
|
348
|
+
params['timeout_ms'] = timeout_ms
|
|
349
|
+
if background:
|
|
350
|
+
params['background'] = background
|
|
351
|
+
if from_checkpoint is not None:
|
|
352
|
+
params['from'] = from_checkpoint
|
|
353
|
+
if checkpoint_comment is not None:
|
|
354
|
+
params['checkpoint_comment'] = checkpoint_comment
|
|
355
|
+
if resume is not None:
|
|
356
|
+
params['resume'] = resume
|
|
357
|
+
|
|
358
|
+
response = await self.bridge.call(
|
|
359
|
+
'run',
|
|
360
|
+
params,
|
|
361
|
+
timeout_s=self._get_rpc_timeout_s(timeout_ms),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return AgentResponse(
|
|
365
|
+
sandbox_id=response['sandbox_id'],
|
|
366
|
+
exit_code=response['exit_code'],
|
|
367
|
+
stdout=response['stdout'],
|
|
368
|
+
stderr=response['stderr'],
|
|
369
|
+
session_id=response.get('session_id'),
|
|
370
|
+
browser=response.get('browser'),
|
|
371
|
+
run_id=response.get('run_id'),
|
|
372
|
+
checkpoint=_parse_checkpoint(response.get('checkpoint')),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
async def execute_command(
|
|
376
|
+
self,
|
|
377
|
+
command: str,
|
|
378
|
+
timeout_ms: Optional[int] = None,
|
|
379
|
+
background: bool = False,
|
|
380
|
+
) -> AgentResponse:
|
|
381
|
+
"""Execute direct shell command.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
command: Shell command to execute
|
|
385
|
+
timeout_ms: Optional timeout in milliseconds (default: 1 hour)
|
|
386
|
+
background: Run in background (default: False). If True, returns
|
|
387
|
+
immediately with a handshake response (`exit_code=0`).
|
|
388
|
+
Final completion is delivered via lifecycle events.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
AgentResponse with sandbox_id, exit_code, stdout, stderr
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> result = await evolve.execute_command(command='python script.py')
|
|
395
|
+
"""
|
|
396
|
+
await self._ensure_initialized()
|
|
397
|
+
|
|
398
|
+
response = await self.bridge.call(
|
|
399
|
+
'execute_command',
|
|
400
|
+
{
|
|
401
|
+
'command': command,
|
|
402
|
+
'timeout_ms': timeout_ms,
|
|
403
|
+
'background': background,
|
|
404
|
+
},
|
|
405
|
+
timeout_s=self._get_rpc_timeout_s(timeout_ms),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return AgentResponse(
|
|
409
|
+
sandbox_id=response['sandbox_id'],
|
|
410
|
+
exit_code=response['exit_code'],
|
|
411
|
+
stdout=response['stdout'],
|
|
412
|
+
stderr=response['stderr'],
|
|
413
|
+
session_id=response.get('session_id'),
|
|
414
|
+
browser=response.get('browser'),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
async def seal_credentials(self) -> None:
|
|
418
|
+
"""Irreversibly revoke Evolve-managed model credentials for this sandbox."""
|
|
419
|
+
await self._ensure_initialized()
|
|
420
|
+
await self.bridge.call('seal_credentials', {}, timeout_s=30)
|
|
421
|
+
|
|
422
|
+
async def collect_artifacts(
|
|
423
|
+
self,
|
|
424
|
+
paths: List[str],
|
|
425
|
+
) -> Dict[str, Union[str, bytes]]:
|
|
426
|
+
"""Collect declared task files after :meth:`seal_credentials`."""
|
|
427
|
+
await self._ensure_initialized()
|
|
428
|
+
response = await self.bridge.call(
|
|
429
|
+
'collect_artifacts',
|
|
430
|
+
{'paths': paths},
|
|
431
|
+
timeout_s=self._get_rpc_timeout_s(None),
|
|
432
|
+
)
|
|
433
|
+
return _decode_files_from_transport(response.get('files', {}))
|
|
434
|
+
|
|
435
|
+
async def upload_context(
|
|
436
|
+
self,
|
|
437
|
+
files: Dict[str, Union[str, bytes]],
|
|
438
|
+
):
|
|
439
|
+
"""Upload files to context/ folder (runtime - immediate upload).
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
files: Dict mapping filename to content - { "filename.txt": "content", "data.json": jsonStr }
|
|
443
|
+
|
|
444
|
+
Example:
|
|
445
|
+
>>> await evolve.upload_context({
|
|
446
|
+
... 'spec.json': json.dumps(spec),
|
|
447
|
+
... 'readme.txt': 'Project documentation...',
|
|
448
|
+
... })
|
|
449
|
+
"""
|
|
450
|
+
await self._ensure_initialized()
|
|
451
|
+
await self.bridge.call('upload_context', {
|
|
452
|
+
'files': _encode_files_for_transport(files),
|
|
453
|
+
}, timeout_s=self._get_rpc_timeout_s(None))
|
|
454
|
+
|
|
455
|
+
async def upload_files(
|
|
456
|
+
self,
|
|
457
|
+
files: Dict[str, Union[str, bytes]],
|
|
458
|
+
):
|
|
459
|
+
"""Upload files to working directory (runtime - immediate upload).
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
files: Dict mapping path to content - { "scripts/run.sh": "#!/bin/bash...", "data/input.csv": csvData }
|
|
463
|
+
|
|
464
|
+
Example:
|
|
465
|
+
>>> await evolve.upload_files({
|
|
466
|
+
... 'scripts/setup.sh': '#!/bin/bash\\necho hello',
|
|
467
|
+
... 'temp/cache.json': json.dumps(cache),
|
|
468
|
+
... })
|
|
469
|
+
"""
|
|
470
|
+
await self._ensure_initialized()
|
|
471
|
+
await self.bridge.call('upload_files', {
|
|
472
|
+
'files': _encode_files_for_transport(files),
|
|
473
|
+
}, timeout_s=self._get_rpc_timeout_s(None))
|
|
474
|
+
|
|
475
|
+
async def upload_file_from_path(
|
|
476
|
+
self,
|
|
477
|
+
sandbox_path: str,
|
|
478
|
+
local_path: str,
|
|
479
|
+
):
|
|
480
|
+
"""Upload one LOCAL file into the sandbox by path, without buffering it.
|
|
481
|
+
|
|
482
|
+
The memory-bounded counterpart to ``upload_files()``: that one takes the
|
|
483
|
+
bytes as a value, so a large artifact costs one full-size copy per
|
|
484
|
+
concurrent upload. This takes the path, and the provider streams it off
|
|
485
|
+
disk — peak memory is a chunk rather than the file.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
sandbox_path: Destination in the sandbox. Absolute paths are used
|
|
489
|
+
as-is; relative paths resolve under the working directory.
|
|
490
|
+
local_path: Path on THIS machine to upload.
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> await evolve.upload_file_from_path('/tmp/dataset.tgz', './dataset.tgz')
|
|
494
|
+
"""
|
|
495
|
+
await self._ensure_initialized()
|
|
496
|
+
await self.bridge.call('upload_file_from_path', {
|
|
497
|
+
'sandbox_path': sandbox_path,
|
|
498
|
+
'local_path': local_path,
|
|
499
|
+
}, timeout_s=self._get_rpc_timeout_s(None))
|
|
500
|
+
|
|
501
|
+
async def get_output_files(self, recursive: bool = False) -> OutputResult:
|
|
502
|
+
"""Get output files with optional schema validation result.
|
|
503
|
+
|
|
504
|
+
Returns files modified after the last run() call, along with schema
|
|
505
|
+
validation results if a schema was configured.
|
|
506
|
+
|
|
507
|
+
Matches TypeScript SDK's getOutputFiles() for exact parity.
|
|
508
|
+
Evidence: sdk-ts/src/types.ts OutputResult<T> interface
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
recursive: Include files in subdirectories (default: False)
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
OutputResult containing:
|
|
515
|
+
- files: Dict mapping filename/path to content (str for text, bytes for binary)
|
|
516
|
+
- data: Parsed and validated result.json data (None if no schema or validation failed)
|
|
517
|
+
- error: Validation or parse error message, if any
|
|
518
|
+
- raw_data: Raw result.json string when parse or validation failed
|
|
519
|
+
|
|
520
|
+
Example:
|
|
521
|
+
>>> output = await evolve.get_output_files()
|
|
522
|
+
>>> for name, content in output.files.items():
|
|
523
|
+
... with open(f'./downloads/{name}', 'wb') as f:
|
|
524
|
+
... f.write(content if isinstance(content, bytes) else content.encode())
|
|
525
|
+
>>> if output.data:
|
|
526
|
+
... print(f"Validated data: {output.data}")
|
|
527
|
+
>>> if output.error:
|
|
528
|
+
... print(f"Validation error: {output.error}")
|
|
529
|
+
"""
|
|
530
|
+
await self._ensure_initialized()
|
|
531
|
+
|
|
532
|
+
response = await self.bridge.call('get_output_files', {'recursive': recursive}, timeout_s=self._get_rpc_timeout_s(None))
|
|
533
|
+
|
|
534
|
+
files = _decode_files_from_transport(response.get('files', {}))
|
|
535
|
+
|
|
536
|
+
data = None
|
|
537
|
+
error = None
|
|
538
|
+
raw_data = None
|
|
539
|
+
|
|
540
|
+
# CASE 1: Pydantic model or dataclass → Native Python validation
|
|
541
|
+
if is_pydantic_model(self._schema) or is_dataclass(self._schema):
|
|
542
|
+
raw_json = files.get('result.json')
|
|
543
|
+
if raw_json is None:
|
|
544
|
+
error = "Schema provided but agent did not create output/result.json"
|
|
545
|
+
else:
|
|
546
|
+
if isinstance(raw_json, bytes):
|
|
547
|
+
raw_json = raw_json.decode('utf-8')
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
strict = self.schema_options.mode == 'strict'
|
|
551
|
+
data = validate_and_parse(raw_json, self._schema, strict=strict)
|
|
552
|
+
except Exception as e:
|
|
553
|
+
error = f"Schema validation failed: {e}"
|
|
554
|
+
raw_data = raw_json
|
|
555
|
+
|
|
556
|
+
# CASE 2: JSON Schema dict → Use TS validation (backward compatible)
|
|
557
|
+
elif self._schema_json is not None:
|
|
558
|
+
data = response.get('data')
|
|
559
|
+
error = response.get('error')
|
|
560
|
+
raw_data = response.get('raw_data')
|
|
561
|
+
|
|
562
|
+
# CASE 3: No schema → Just return files (data stays None)
|
|
563
|
+
|
|
564
|
+
return OutputResult(
|
|
565
|
+
files=files,
|
|
566
|
+
data=data,
|
|
567
|
+
error=error,
|
|
568
|
+
raw_data=raw_data,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# =========================================================================
|
|
572
|
+
# STORAGE / CHECKPOINTING
|
|
573
|
+
# =========================================================================
|
|
574
|
+
|
|
575
|
+
async def checkpoint(self, comment: Optional[str] = None) -> CheckpointInfo:
|
|
576
|
+
"""Create an explicit checkpoint of the current sandbox state.
|
|
577
|
+
|
|
578
|
+
Requires a prior run() call (needs an active sandbox to snapshot).
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
comment: Optional label for this checkpoint
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
CheckpointInfo with id, hash, tag, timestamp, etc.
|
|
585
|
+
|
|
586
|
+
Example:
|
|
587
|
+
>>> info = await evolve.checkpoint(comment='before refactor')
|
|
588
|
+
>>> print(f'Checkpoint: {info.id}')
|
|
589
|
+
"""
|
|
590
|
+
await self._ensure_initialized()
|
|
591
|
+
params: Dict[str, Any] = {}
|
|
592
|
+
if comment is not None:
|
|
593
|
+
params['comment'] = comment
|
|
594
|
+
response = await self.bridge.call('checkpoint', params, timeout_s=self._get_rpc_timeout_s(None))
|
|
595
|
+
return _require_checkpoint(response)
|
|
596
|
+
|
|
597
|
+
async def list_checkpoints(
|
|
598
|
+
self,
|
|
599
|
+
limit: Optional[int] = None,
|
|
600
|
+
tag: Optional[str] = None,
|
|
601
|
+
) -> List[CheckpointInfo]:
|
|
602
|
+
"""List checkpoints (requires storage configuration).
|
|
603
|
+
|
|
604
|
+
Does not require a running sandbox — only storage configuration.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
limit: Maximum number of checkpoints to return
|
|
608
|
+
tag: Filter by session tag
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of CheckpointInfo sorted by newest first
|
|
612
|
+
|
|
613
|
+
Example:
|
|
614
|
+
>>> checkpoints = await evolve.list_checkpoints(limit=5)
|
|
615
|
+
>>> for cp in checkpoints:
|
|
616
|
+
... print(f'{cp.id} ({cp.comment})')
|
|
617
|
+
"""
|
|
618
|
+
await self._ensure_initialized()
|
|
619
|
+
params: Dict[str, Any] = {}
|
|
620
|
+
if limit is not None:
|
|
621
|
+
params['limit'] = limit
|
|
622
|
+
if tag is not None:
|
|
623
|
+
params['tag'] = tag
|
|
624
|
+
response = await self.bridge.call('list_checkpoints', params, timeout_s=self._get_rpc_timeout_s(None))
|
|
625
|
+
return [_require_checkpoint(cp) for cp in response]
|
|
626
|
+
|
|
627
|
+
def storage(self) -> StorageClient:
|
|
628
|
+
"""Get a StorageClient bound to this instance's storage configuration.
|
|
629
|
+
|
|
630
|
+
Same API surface as the standalone ``storage()`` factory, but uses
|
|
631
|
+
the Evolve instance's bridge and gateway credentials.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
StorageClient with list_checkpoints, get_checkpoint,
|
|
635
|
+
download_checkpoint, download_files methods
|
|
636
|
+
|
|
637
|
+
Raises:
|
|
638
|
+
RuntimeError: If storage is not configured
|
|
639
|
+
|
|
640
|
+
Example:
|
|
641
|
+
>>> store = evolve.storage()
|
|
642
|
+
>>> checkpoints = await store.list_checkpoints(limit=5)
|
|
643
|
+
>>> files = await store.download_files(checkpoints[0].id)
|
|
644
|
+
"""
|
|
645
|
+
if self._storage_config is None:
|
|
646
|
+
raise RuntimeError("Storage not configured. Pass storage=StorageConfig() to Evolve().")
|
|
647
|
+
return StorageClient(self.bridge, storage_config=None, _init_fn=self._ensure_initialized)
|
|
648
|
+
|
|
649
|
+
async def get_session(self) -> Optional[str]:
|
|
650
|
+
"""Get sandbox ID for reuse.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Sandbox ID or None if not initialized
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
>>> sandbox_id = await evolve.get_session()
|
|
657
|
+
>>> print(f'Sandbox ID: {sandbox_id}')
|
|
658
|
+
"""
|
|
659
|
+
await self._ensure_initialized()
|
|
660
|
+
return await self.bridge.call('get_session')
|
|
661
|
+
|
|
662
|
+
async def prepare_sandbox(self) -> str:
|
|
663
|
+
"""Create and initialize the sandbox without starting the agent."""
|
|
664
|
+
await self._ensure_initialized()
|
|
665
|
+
sandbox_id = await self.bridge.call('prepare_sandbox')
|
|
666
|
+
if not isinstance(sandbox_id, str) or not sandbox_id:
|
|
667
|
+
raise RuntimeError('Sandbox preparation did not return a sandbox ID')
|
|
668
|
+
return sandbox_id
|
|
669
|
+
|
|
670
|
+
async def set_session(self, session_id: str):
|
|
671
|
+
"""Change sandbox session.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
session_id: New sandbox ID to connect to
|
|
675
|
+
|
|
676
|
+
Example:
|
|
677
|
+
>>> await evolve.set_session('existing-sandbox-id')
|
|
678
|
+
"""
|
|
679
|
+
await self._ensure_initialized()
|
|
680
|
+
await self.bridge.call('set_session', {
|
|
681
|
+
'session_id': session_id,
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
async def pause(self):
|
|
685
|
+
"""Pause sandbox to save costs while preserving state.
|
|
686
|
+
|
|
687
|
+
Example:
|
|
688
|
+
>>> await evolve.pause()
|
|
689
|
+
"""
|
|
690
|
+
await self._ensure_initialized()
|
|
691
|
+
await self.bridge.call('pause')
|
|
692
|
+
|
|
693
|
+
async def interrupt(self) -> bool:
|
|
694
|
+
"""Interrupt active process without killing the sandbox.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
True if an active process was interrupted; False otherwise.
|
|
698
|
+
"""
|
|
699
|
+
await self._ensure_initialized()
|
|
700
|
+
interrupted = await self.bridge.call('interrupt')
|
|
701
|
+
return bool(interrupted)
|
|
702
|
+
|
|
703
|
+
async def status(self) -> SessionStatus:
|
|
704
|
+
"""Get runtime status snapshot for sandbox and agent."""
|
|
705
|
+
await self._ensure_initialized()
|
|
706
|
+
response = await self.bridge.call('status')
|
|
707
|
+
return SessionStatus(
|
|
708
|
+
sandbox_id=response.get('sandbox_id'),
|
|
709
|
+
sandbox=response.get('sandbox', 'stopped'),
|
|
710
|
+
agent=response.get('agent', 'idle'),
|
|
711
|
+
active_process_id=response.get('active_process_id'),
|
|
712
|
+
has_run=bool(response.get('has_run', False)),
|
|
713
|
+
timestamp=response.get('timestamp', ''),
|
|
714
|
+
browser=response.get('browser'),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
async def resume(self):
|
|
718
|
+
"""Resume paused sandbox.
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
>>> await evolve.resume()
|
|
722
|
+
"""
|
|
723
|
+
await self._ensure_initialized()
|
|
724
|
+
await self.bridge.call('resume')
|
|
725
|
+
|
|
726
|
+
async def kill(self):
|
|
727
|
+
"""Terminate sandbox and release all resources.
|
|
728
|
+
|
|
729
|
+
Example:
|
|
730
|
+
>>> await evolve.kill()
|
|
731
|
+
"""
|
|
732
|
+
await self._ensure_initialized()
|
|
733
|
+
try:
|
|
734
|
+
await self.bridge.call('kill')
|
|
735
|
+
finally:
|
|
736
|
+
# Always stop bridge even if RPC fails (e.g., sandbox already gone)
|
|
737
|
+
await self.bridge.stop()
|
|
738
|
+
self._initialized = False
|
|
739
|
+
|
|
740
|
+
async def get_host(self, port: int) -> str:
|
|
741
|
+
"""Get public URL for sandbox port.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
port: Port number to expose
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Public URL for the port
|
|
748
|
+
|
|
749
|
+
Example:
|
|
750
|
+
>>> url = await evolve.get_host(8000)
|
|
751
|
+
>>> print(f'Server available at: {url}')
|
|
752
|
+
"""
|
|
753
|
+
await self._ensure_initialized()
|
|
754
|
+
response = await self.bridge.call('get_host', {
|
|
755
|
+
'port': port,
|
|
756
|
+
})
|
|
757
|
+
return response['url']
|
|
758
|
+
|
|
759
|
+
async def get_session_tag(self) -> Optional[str]:
|
|
760
|
+
"""Get the observability session tag.
|
|
761
|
+
|
|
762
|
+
Returns the generated tag (e.g., 'my-prefix-a3f8b2c1') used for the
|
|
763
|
+
log file in ~/.evolve-sdk/observability/sessions/
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
Session tag or None if not initialized
|
|
767
|
+
|
|
768
|
+
Example:
|
|
769
|
+
>>> tag = await evolve.get_session_tag()
|
|
770
|
+
>>> print(f'Log file tag: {tag}')
|
|
771
|
+
'experiment-7-a3f8b2c1'
|
|
772
|
+
"""
|
|
773
|
+
await self._ensure_initialized()
|
|
774
|
+
return await self.bridge.call('get_session_tag')
|
|
775
|
+
|
|
776
|
+
async def get_session_timestamp(self) -> Optional[str]:
|
|
777
|
+
"""Get the session start timestamp (ISO format).
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
ISO timestamp when session was created or None if not initialized
|
|
781
|
+
|
|
782
|
+
Example:
|
|
783
|
+
>>> timestamp = await evolve.get_session_timestamp()
|
|
784
|
+
>>> print(f'Session started: {timestamp}')
|
|
785
|
+
'2025-01-15T10:30:45.123Z'
|
|
786
|
+
"""
|
|
787
|
+
await self._ensure_initialized()
|
|
788
|
+
return await self.bridge.call('get_session_timestamp')
|
|
789
|
+
|
|
790
|
+
# =========================================================================
|
|
791
|
+
# COST
|
|
792
|
+
# =========================================================================
|
|
793
|
+
|
|
794
|
+
async def get_session_cost(self) -> SessionCost:
|
|
795
|
+
"""Get cost breakdown for the current session (all runs).
|
|
796
|
+
|
|
797
|
+
Cost data can lag live usage by about a minute.
|
|
798
|
+
Note: after kill(), the bridge process is stopped. To query costs for a
|
|
799
|
+
completed session, create a new Evolve instance — the TS bridge handles
|
|
800
|
+
previousSessionTag fallback internally.
|
|
801
|
+
|
|
802
|
+
Requires gateway mode (EVOLVE_API_KEY).
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
SessionCost with total cost, token counts, and per-run breakdown
|
|
806
|
+
|
|
807
|
+
Example:
|
|
808
|
+
>>> cost = await evolve.get_session_cost()
|
|
809
|
+
>>> print(f'Total: ${cost.total_cost:.4f}')
|
|
810
|
+
>>> for run in cost.runs:
|
|
811
|
+
... print(f' Run {run.index}: ${run.cost:.4f} ({run.model})')
|
|
812
|
+
"""
|
|
813
|
+
await self._ensure_initialized()
|
|
814
|
+
response = await self.bridge.call('get_session_cost')
|
|
815
|
+
return self._parse_session_cost(response)
|
|
816
|
+
|
|
817
|
+
async def get_run_cost(
|
|
818
|
+
self,
|
|
819
|
+
*,
|
|
820
|
+
run_id: Optional[str] = None,
|
|
821
|
+
index: Optional[int] = None,
|
|
822
|
+
) -> RunCost:
|
|
823
|
+
"""Get cost for a specific run by ID or index.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
run_id: Run ID from AgentResponse.run_id
|
|
827
|
+
index: 1-based chronological position (negative = from end, e.g. -1 = last run)
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
RunCost with cost, tokens, model, and completion status
|
|
831
|
+
|
|
832
|
+
Example:
|
|
833
|
+
>>> # By run ID
|
|
834
|
+
>>> result = await evolve.run('Analyze data')
|
|
835
|
+
>>> cost = await evolve.get_run_cost(run_id=result.run_id)
|
|
836
|
+
>>> # By index (last run)
|
|
837
|
+
>>> cost = await evolve.get_run_cost(index=-1)
|
|
838
|
+
"""
|
|
839
|
+
if run_id is not None and index is not None:
|
|
840
|
+
raise ValueError('Specify run_id or index, not both')
|
|
841
|
+
if run_id is None and index is None:
|
|
842
|
+
raise ValueError('Specify either run_id or index')
|
|
843
|
+
await self._ensure_initialized()
|
|
844
|
+
params: Dict[str, Any] = {}
|
|
845
|
+
if run_id is not None:
|
|
846
|
+
params['run_id'] = run_id
|
|
847
|
+
if index is not None:
|
|
848
|
+
params['index'] = index
|
|
849
|
+
response = await self.bridge.call('get_run_cost', params)
|
|
850
|
+
return self._parse_run_cost(response)
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def _parse_run_cost(data: Dict[str, Any]) -> RunCost:
|
|
854
|
+
"""Parse run cost dict from bridge response into RunCost."""
|
|
855
|
+
return RunCost(
|
|
856
|
+
run_id=data['run_id'],
|
|
857
|
+
index=data['index'],
|
|
858
|
+
cost=data['cost'],
|
|
859
|
+
tokens=data['tokens'],
|
|
860
|
+
model=data['model'],
|
|
861
|
+
requests=data['requests'],
|
|
862
|
+
as_of=data['as_of'],
|
|
863
|
+
is_complete=data['is_complete'],
|
|
864
|
+
truncated=data['truncated'],
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
@classmethod
|
|
868
|
+
def _parse_session_cost(cls, data: Dict[str, Any]) -> SessionCost:
|
|
869
|
+
"""Parse session cost dict from bridge response into SessionCost."""
|
|
870
|
+
return SessionCost(
|
|
871
|
+
session_tag=data['session_tag'],
|
|
872
|
+
total_cost=data['total_cost'],
|
|
873
|
+
total_tokens=data['total_tokens'],
|
|
874
|
+
runs=[cls._parse_run_cost(r) for r in data.get('runs', [])],
|
|
875
|
+
as_of=data['as_of'],
|
|
876
|
+
is_complete=data['is_complete'],
|
|
877
|
+
truncated=data['truncated'],
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
async def __aenter__(self):
|
|
881
|
+
"""Context manager entry."""
|
|
882
|
+
try:
|
|
883
|
+
await self._ensure_initialized()
|
|
884
|
+
return self
|
|
885
|
+
except Exception:
|
|
886
|
+
# Cleanup bridge process if initialization fails
|
|
887
|
+
await self.bridge.stop()
|
|
888
|
+
raise
|
|
889
|
+
|
|
890
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
891
|
+
"""Context manager exit - cleanup resources."""
|
|
892
|
+
try:
|
|
893
|
+
await self.kill()
|
|
894
|
+
except Exception as e:
|
|
895
|
+
import warnings
|
|
896
|
+
warnings.warn(f"Error during cleanup: {e}", RuntimeWarning)
|