swarmkit 0.1.34__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.
swarmkit/__init__.py ADDED
@@ -0,0 +1,152 @@
1
+ """SwarmKit Python SDK - Pythonic wrapper around the TypeScript SwarmKit SDK."""
2
+
3
+ from .agent import SwarmKit
4
+ from .config import (
5
+ AgentConfig,
6
+ E2BProvider,
7
+ SandboxProvider,
8
+ AgentType,
9
+ WorkspaceMode,
10
+ ReasoningEffort,
11
+ ValidationMode,
12
+ SchemaOptions,
13
+ )
14
+ from .results import AgentResponse, ExecuteResult, OutputResult
15
+ from .utils import read_local_dir, save_local_dir
16
+ from .bridge import (
17
+ SandboxNotFoundError,
18
+ BridgeConnectionError,
19
+ BridgeBuildError,
20
+ )
21
+ from .retry import RetryConfig, OnItemRetryCallback, execute_with_retry
22
+ from .swarm import (
23
+ Swarm,
24
+ SwarmConfig,
25
+ BestOfConfig,
26
+ VerifyConfig,
27
+ SwarmResult,
28
+ SwarmResultList,
29
+ ReduceResult,
30
+ BestOfResult,
31
+ BestOfInfo,
32
+ VerifyInfo,
33
+ IndexedMeta,
34
+ ReduceMeta,
35
+ JudgeMeta,
36
+ VerifyMeta,
37
+ VerifyDecision,
38
+ is_swarm_result,
39
+ # Callback types
40
+ OnCandidateCompleteCallback,
41
+ OnJudgeCompleteCallback,
42
+ OnWorkerCompleteCallback,
43
+ OnVerifierCompleteCallback,
44
+ )
45
+ from .pipeline import (
46
+ Pipeline,
47
+ TerminalPipeline,
48
+ MapConfig,
49
+ FilterConfig,
50
+ ReduceConfig,
51
+ StepResult,
52
+ PipelineResult,
53
+ PipelineEvents,
54
+ StepStartEvent,
55
+ StepCompleteEvent,
56
+ StepErrorEvent,
57
+ ItemRetryEvent,
58
+ WorkerCompleteEvent,
59
+ VerifierCompleteEvent,
60
+ CandidateCompleteEvent,
61
+ JudgeCompleteEvent,
62
+ EmitOption,
63
+ )
64
+
65
+ __version__ = '0.1.34'
66
+
67
+ __all__ = [
68
+ # Main classes
69
+ 'SwarmKit',
70
+ 'Swarm',
71
+ 'Pipeline',
72
+ 'TerminalPipeline',
73
+
74
+ # SwarmKit Configuration
75
+ 'AgentConfig',
76
+ 'E2BProvider',
77
+ 'SandboxProvider',
78
+ 'AgentType',
79
+ 'WorkspaceMode',
80
+ 'ReasoningEffort',
81
+ 'ValidationMode',
82
+ 'SchemaOptions',
83
+
84
+ # SwarmKit Results
85
+ 'AgentResponse',
86
+ 'ExecuteResult', # Backward compatibility alias for AgentResponse
87
+ 'OutputResult',
88
+
89
+ # Swarm Configuration
90
+ 'SwarmConfig',
91
+ 'BestOfConfig',
92
+ 'VerifyConfig',
93
+
94
+ # Swarm Results
95
+ 'SwarmResult',
96
+ 'SwarmResultList',
97
+ 'ReduceResult',
98
+ 'BestOfResult',
99
+ 'BestOfInfo',
100
+ 'VerifyInfo',
101
+ 'VerifyDecision',
102
+
103
+ # Swarm Metadata
104
+ 'IndexedMeta',
105
+ 'ReduceMeta',
106
+ 'JudgeMeta',
107
+ 'VerifyMeta',
108
+
109
+ # Swarm Helpers
110
+ 'is_swarm_result',
111
+
112
+ # Swarm Callback types
113
+ 'OnCandidateCompleteCallback',
114
+ 'OnJudgeCompleteCallback',
115
+ 'OnWorkerCompleteCallback',
116
+ 'OnVerifierCompleteCallback',
117
+
118
+ # Pipeline Configuration
119
+ 'MapConfig',
120
+ 'FilterConfig',
121
+ 'ReduceConfig',
122
+
123
+ # Pipeline Results
124
+ 'StepResult',
125
+ 'PipelineResult',
126
+
127
+ # Pipeline Events
128
+ 'PipelineEvents',
129
+ 'StepStartEvent',
130
+ 'StepCompleteEvent',
131
+ 'StepErrorEvent',
132
+ 'ItemRetryEvent',
133
+ 'WorkerCompleteEvent',
134
+ 'VerifierCompleteEvent',
135
+ 'CandidateCompleteEvent',
136
+ 'JudgeCompleteEvent',
137
+ 'EmitOption',
138
+
139
+ # Retry
140
+ 'RetryConfig',
141
+ 'OnItemRetryCallback',
142
+ 'execute_with_retry',
143
+
144
+ # Utilities
145
+ 'read_local_dir',
146
+ 'save_local_dir',
147
+
148
+ # Exceptions
149
+ 'SandboxNotFoundError',
150
+ 'BridgeConnectionError',
151
+ 'BridgeBuildError',
152
+ ]
swarmkit/agent.py ADDED
@@ -0,0 +1,480 @@
1
+ """Main SwarmKit class for Python SDK."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
7
+
8
+ from .bridge import BridgeManager, SandboxNotFoundError
9
+ from .config import AgentConfig, SandboxProvider, SchemaOptions, WorkspaceMode
10
+ from .results import AgentResponse, ExecuteResult, OutputResult
11
+ from .schema import is_pydantic_model, is_dataclass, to_json_schema, validate_and_parse
12
+ from .utils import _encode_files_for_transport, _filter_none
13
+
14
+
15
+ class SwarmKit:
16
+ """SwarmKit agent orchestrator.
17
+
18
+ Provides a Pythonic interface to the TypeScript SwarmKit SDK via JSON-RPC bridge.
19
+
20
+ Example:
21
+ >>> from swarmkit import SwarmKit
22
+ >>>
23
+ >>> # Minimal usage - uses SWARMKIT_API_KEY and E2B_API_KEY env vars
24
+ >>> async with SwarmKit() as swarmkit:
25
+ ... result = await swarmkit.run(prompt='Analyze data.csv')
26
+ ... output = await swarmkit.get_output_files()
27
+ ... for name, content in output.files.items():
28
+ ... print(f'{name}: {len(content)} bytes')
29
+ >>>
30
+ >>> # Or with explicit config
31
+ >>> from swarmkit import AgentConfig, E2BProvider
32
+ >>> swarmkit = SwarmKit(
33
+ ... config=AgentConfig(type='codex', api_key='sk-...'),
34
+ ... sandbox=E2BProvider(api_key='...')
35
+ ... )
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ config: Optional[AgentConfig] = None,
41
+ sandbox: Optional[SandboxProvider] = None,
42
+ working_directory: str = '/home/user/workspace',
43
+ workspace_mode: WorkspaceMode = 'knowledge',
44
+ system_prompt: Optional[str] = None,
45
+ context: Optional[Dict[str, Union[str, bytes]]] = None,
46
+ files: Optional[Dict[str, Union[str, bytes]]] = None,
47
+ mcp_servers: Optional[Dict[str, Any]] = None,
48
+ secrets: Optional[Dict[str, str]] = None,
49
+ sandbox_id: Optional[str] = None,
50
+ session_tag_prefix: Optional[str] = None,
51
+ schema: Optional[Union[Type, Dict[str, Any]]] = None,
52
+ schema_options: Optional[SchemaOptions] = None,
53
+ ):
54
+ """Initialize SwarmKit.
55
+
56
+ Args:
57
+ config: Agent configuration (optional - defaults to SWARMKIT_API_KEY env var with 'claude' type)
58
+ sandbox: Sandbox provider (optional - defaults to E2B with E2B_API_KEY env var)
59
+ working_directory: Working directory in sandbox (default: /home/user/workspace)
60
+ workspace_mode: Workspace setup mode - 'knowledge' (creates output/context/scripts/temp folders + default prompt)
61
+ or 'swe' (clean workspace for cloned repos) (default: 'knowledge')
62
+ system_prompt: Custom system prompt (appended to default in 'knowledge' mode, sole prompt in 'swe' mode)
63
+ context: Files to upload to context/ folder on first run - { "filename.txt": "content" }
64
+ files: Files to upload to working directory on first run - { "scripts/run.sh": "content" }
65
+ mcp_servers: MCP server configurations
66
+ secrets: Environment variables for sandbox
67
+ sandbox_id: Existing sandbox ID to reconnect to
68
+ session_tag_prefix: Optional semantic label for observability log files (e.g., 'experiment-7')
69
+ schema: Schema for structured output - Pydantic model, dataclass, or JSON Schema dict
70
+ schema_options: Validation options (mode: 'strict' or 'loose', default: 'loose')
71
+ """
72
+ self.config = config
73
+ self.sandbox = sandbox
74
+ self.working_directory = working_directory
75
+ self.workspace_mode = workspace_mode
76
+ self.system_prompt = system_prompt
77
+ self.context = context
78
+ self.files = files
79
+ self.mcp_servers = mcp_servers
80
+ self.secrets = secrets
81
+ self.sandbox_id = sandbox_id
82
+ self.session_tag_prefix = session_tag_prefix
83
+ self.schema_options = schema_options or SchemaOptions()
84
+
85
+ # Schema handling: store original + convert to JSON Schema
86
+ self._schema = schema
87
+ self._schema_json = to_json_schema(schema)
88
+
89
+ self.bridge = BridgeManager()
90
+ self._initialized = False
91
+ self._init_lock = asyncio.Lock()
92
+
93
+ async def _ensure_initialized(self):
94
+ """Ensure bridge is started and agent is initialized."""
95
+ async with self._init_lock:
96
+ if self._initialized:
97
+ return
98
+
99
+ await self.bridge.start()
100
+
101
+ # Build params with _filter_none to exclude None values
102
+ # TS SDK resolves defaults from env vars when not provided
103
+ params = _filter_none({
104
+ # Agent config (optional - TS SDK resolves from SWARMKIT_API_KEY)
105
+ 'agent_type': self.config.type if self.config else None,
106
+ 'api_key': self.config.api_key if self.config else None,
107
+ 'model': self.config.model if self.config else None,
108
+ 'reasoning_effort': self.config.reasoning_effort if self.config else None,
109
+ 'betas': self.config.betas if self.config else None,
110
+ # Sandbox (optional - TS SDK resolves from E2B_API_KEY)
111
+ 'sandbox_provider': {'type': self.sandbox.type, 'config': self.sandbox.config} if self.sandbox else None,
112
+ # Other settings
113
+ 'working_directory': self.working_directory,
114
+ 'workspace_mode': self.workspace_mode,
115
+ 'system_prompt': self.system_prompt,
116
+ 'context': _encode_files_for_transport(self.context) if self.context else None,
117
+ 'files': _encode_files_for_transport(self.files) if self.files else None,
118
+ 'mcp_servers': self.mcp_servers,
119
+ 'secrets': self.secrets,
120
+ 'sandbox_id': self.sandbox_id,
121
+ 'session_tag_prefix': self.session_tag_prefix,
122
+ 'schema': self._schema_json,
123
+ 'schema_options': {'mode': self.schema_options.mode} if self._schema_json else None,
124
+ # Always forward events
125
+ 'forward_stdout': True,
126
+ 'forward_stderr': True,
127
+ 'forward_content': True,
128
+ })
129
+
130
+ await self.bridge.call('initialize', params)
131
+ self._initialized = True
132
+
133
+ def on(self, event_type: str, callback: Callable[[Any], None]):
134
+ """Register event callback.
135
+
136
+ Args:
137
+ event_type: Event type ('stdout' | 'stderr' | 'content')
138
+ callback: Callback function invoked with the event payload
139
+ (str for stdout/stderr, dict for content)
140
+
141
+ Example:
142
+ >>> swarmkit.on('stdout', lambda data: print(data, end=''))
143
+ >>> swarmkit.on('stderr', lambda data: print(f'[ERR] {data}', end=''))
144
+ >>> swarmkit.on('content', lambda event: print(event['update']['sessionUpdate']))
145
+ """
146
+ self.bridge.on(event_type, callback)
147
+
148
+ def _get_rpc_timeout_s(self, timeout_ms: Optional[int]) -> float:
149
+ """Compute an RPC timeout aligned with sandbox execution timeout."""
150
+ if timeout_ms is None:
151
+ timeout_ms = getattr(self.sandbox, "timeout_ms", 3600000) if self.sandbox else 3600000
152
+ # Add small grace to allow bridge/agent cleanup after sandbox timeout.
153
+ return timeout_ms / 1000.0 + 30.0
154
+
155
+ async def run(
156
+ self,
157
+ prompt: str,
158
+ timeout_ms: Optional[int] = None,
159
+ background: bool = False,
160
+ ) -> AgentResponse:
161
+ """Run AI-assisted task (agent decides and acts).
162
+
163
+ Args:
164
+ prompt: Task description
165
+ timeout_ms: Optional timeout in milliseconds (default: 1 hour)
166
+ background: Run in background (default: False). If True, returns immediately
167
+ while agent continues running.
168
+
169
+ Returns:
170
+ AgentResponse with sandbox_id, exit_code, stdout, stderr
171
+
172
+ Example:
173
+ >>> result = await swarmkit.run(prompt='Analyze data and create report', timeout_ms=600000)
174
+ >>> # Background execution
175
+ >>> result = await swarmkit.run(prompt='Long task', background=True)
176
+ """
177
+ await self._ensure_initialized()
178
+
179
+ params: Dict[str, Any] = {
180
+ 'prompt': prompt,
181
+ }
182
+ if timeout_ms is not None:
183
+ params['timeout_ms'] = timeout_ms
184
+ if background:
185
+ params['background'] = background
186
+
187
+ response = await self.bridge.call(
188
+ 'run',
189
+ params,
190
+ timeout_s=self._get_rpc_timeout_s(timeout_ms),
191
+ )
192
+
193
+ return AgentResponse(
194
+ sandbox_id=response['sandbox_id'],
195
+ exit_code=response['exit_code'],
196
+ stdout=response['stdout'],
197
+ stderr=response['stderr'],
198
+ )
199
+
200
+ async def execute_command(
201
+ self,
202
+ command: str,
203
+ timeout_ms: Optional[int] = None,
204
+ background: bool = False,
205
+ ) -> AgentResponse:
206
+ """Execute direct shell command.
207
+
208
+ Args:
209
+ command: Shell command to execute
210
+ timeout_ms: Optional timeout in milliseconds (default: 1 hour)
211
+ background: Run in background (default: False)
212
+
213
+ Returns:
214
+ AgentResponse with sandbox_id, exit_code, stdout, stderr
215
+
216
+ Example:
217
+ >>> result = await swarmkit.execute_command(command='python script.py')
218
+ """
219
+ await self._ensure_initialized()
220
+
221
+ response = await self.bridge.call(
222
+ 'execute_command',
223
+ {
224
+ 'command': command,
225
+ 'timeout_ms': timeout_ms,
226
+ 'background': background,
227
+ },
228
+ timeout_s=self._get_rpc_timeout_s(timeout_ms),
229
+ )
230
+
231
+ return AgentResponse(
232
+ sandbox_id=response['sandbox_id'],
233
+ exit_code=response['exit_code'],
234
+ stdout=response['stdout'],
235
+ stderr=response['stderr'],
236
+ )
237
+
238
+ async def upload_context(
239
+ self,
240
+ files: Dict[str, Union[str, bytes]],
241
+ ):
242
+ """Upload files to context/ folder (runtime - immediate upload).
243
+
244
+ Args:
245
+ files: Dict mapping filename to content - { "filename.txt": "content", "data.json": jsonStr }
246
+
247
+ Example:
248
+ >>> await swarmkit.upload_context({
249
+ ... 'spec.json': json.dumps(spec),
250
+ ... 'readme.txt': 'Project documentation...',
251
+ ... })
252
+ """
253
+ await self._ensure_initialized()
254
+ await self.bridge.call('upload_context', {
255
+ 'files': _encode_files_for_transport(files),
256
+ })
257
+
258
+ async def upload_files(
259
+ self,
260
+ files: Dict[str, Union[str, bytes]],
261
+ ):
262
+ """Upload files to working directory (runtime - immediate upload).
263
+
264
+ Args:
265
+ files: Dict mapping path to content - { "scripts/run.sh": "#!/bin/bash...", "data/input.csv": csvData }
266
+
267
+ Example:
268
+ >>> await swarmkit.upload_files({
269
+ ... 'scripts/setup.sh': '#!/bin/bash\\necho hello',
270
+ ... 'temp/cache.json': json.dumps(cache),
271
+ ... })
272
+ """
273
+ await self._ensure_initialized()
274
+ await self.bridge.call('upload_files', {
275
+ 'files': _encode_files_for_transport(files),
276
+ })
277
+
278
+ async def get_output_files(self, recursive: bool = False) -> OutputResult:
279
+ """Get output files with optional schema validation result.
280
+
281
+ Returns files modified after the last run() call, along with schema
282
+ validation results if a schema was configured.
283
+
284
+ Matches TypeScript SDK's getOutputFiles() for exact parity.
285
+ Evidence: sdk-ts/src/types.ts OutputResult<T> interface
286
+
287
+ Args:
288
+ recursive: Include files in subdirectories (default: False)
289
+
290
+ Returns:
291
+ OutputResult containing:
292
+ - files: Dict mapping filename/path to content (str for text, bytes for binary)
293
+ - data: Parsed and validated result.json data (None if no schema or validation failed)
294
+ - error: Validation or parse error message, if any
295
+ - raw_data: Raw result.json string when parse or validation failed
296
+
297
+ Example:
298
+ >>> output = await swarmkit.get_output_files()
299
+ >>> for name, content in output.files.items():
300
+ ... with open(f'./downloads/{name}', 'wb') as f:
301
+ ... f.write(content if isinstance(content, bytes) else content.encode())
302
+ >>> if output.data:
303
+ ... print(f"Validated data: {output.data}")
304
+ >>> if output.error:
305
+ ... print(f"Validation error: {output.error}")
306
+ """
307
+ await self._ensure_initialized()
308
+
309
+ response = await self.bridge.call('get_output_files', {'recursive': recursive})
310
+
311
+ # Decode files from transport encoding
312
+ files: Dict[str, Union[str, bytes]] = {}
313
+ for name, file_data in response.get('files', {}).items():
314
+ content = file_data['content']
315
+ encoding = file_data.get('encoding')
316
+ if encoding == 'base64':
317
+ content = base64.b64decode(content)
318
+ files[name] = content
319
+
320
+ data = None
321
+ error = None
322
+ raw_data = None
323
+
324
+ # CASE 1: Pydantic model or dataclass → Native Python validation
325
+ if is_pydantic_model(self._schema) or is_dataclass(self._schema):
326
+ raw_json = files.get('result.json')
327
+ if raw_json is None:
328
+ error = "Schema provided but agent did not create output/result.json"
329
+ else:
330
+ if isinstance(raw_json, bytes):
331
+ raw_json = raw_json.decode('utf-8')
332
+
333
+ try:
334
+ strict = self.schema_options.mode == 'strict'
335
+ data = validate_and_parse(raw_json, self._schema, strict=strict)
336
+ except Exception as e:
337
+ error = f"Schema validation failed: {e}"
338
+ raw_data = raw_json
339
+
340
+ # CASE 2: JSON Schema dict → Use TS validation (backward compatible)
341
+ elif self._schema_json is not None:
342
+ data = response.get('data')
343
+ error = response.get('error')
344
+ raw_data = response.get('raw_data')
345
+
346
+ # CASE 3: No schema → Just return files (data stays None)
347
+
348
+ return OutputResult(
349
+ files=files,
350
+ data=data,
351
+ error=error,
352
+ raw_data=raw_data,
353
+ )
354
+
355
+ async def get_session(self) -> Optional[str]:
356
+ """Get sandbox ID for reuse.
357
+
358
+ Returns:
359
+ Sandbox ID or None if not initialized
360
+
361
+ Example:
362
+ >>> sandbox_id = await swarmkit.get_session()
363
+ >>> print(f'Sandbox ID: {sandbox_id}')
364
+ """
365
+ await self._ensure_initialized()
366
+ return await self.bridge.call('get_session')
367
+
368
+ async def set_session(self, session_id: str):
369
+ """Change sandbox session.
370
+
371
+ Args:
372
+ session_id: New sandbox ID to connect to
373
+
374
+ Example:
375
+ >>> await swarmkit.set_session('existing-sandbox-id')
376
+ """
377
+ await self._ensure_initialized()
378
+ await self.bridge.call('set_session', {
379
+ 'session_id': session_id,
380
+ })
381
+
382
+ async def pause(self):
383
+ """Pause sandbox to save costs while preserving state.
384
+
385
+ Example:
386
+ >>> await swarmkit.pause()
387
+ """
388
+ await self._ensure_initialized()
389
+ await self.bridge.call('pause')
390
+
391
+ async def resume(self):
392
+ """Resume paused sandbox.
393
+
394
+ Example:
395
+ >>> await swarmkit.resume()
396
+ """
397
+ await self._ensure_initialized()
398
+ await self.bridge.call('resume')
399
+
400
+ async def kill(self):
401
+ """Terminate sandbox and release all resources.
402
+
403
+ Example:
404
+ >>> await swarmkit.kill()
405
+ """
406
+ await self._ensure_initialized()
407
+ try:
408
+ await self.bridge.call('kill')
409
+ finally:
410
+ # Always stop bridge even if RPC fails (e.g., sandbox already gone)
411
+ await self.bridge.stop()
412
+ self._initialized = False
413
+
414
+ async def get_host(self, port: int) -> str:
415
+ """Get public URL for sandbox port.
416
+
417
+ Args:
418
+ port: Port number to expose
419
+
420
+ Returns:
421
+ Public URL for the port
422
+
423
+ Example:
424
+ >>> url = await swarmkit.get_host(8000)
425
+ >>> print(f'Server available at: {url}')
426
+ """
427
+ await self._ensure_initialized()
428
+ response = await self.bridge.call('get_host', {
429
+ 'port': port,
430
+ })
431
+ return response['url']
432
+
433
+ async def get_session_tag(self) -> Optional[str]:
434
+ """Get the observability session tag.
435
+
436
+ Returns the generated tag (e.g., 'my-prefix-a3f8b2c1') used for the
437
+ log file in ~/.swarmkit/observability/sessions/
438
+
439
+ Returns:
440
+ Session tag or None if not initialized
441
+
442
+ Example:
443
+ >>> tag = await swarmkit.get_session_tag()
444
+ >>> print(f'Log file tag: {tag}')
445
+ 'experiment-7-a3f8b2c1'
446
+ """
447
+ await self._ensure_initialized()
448
+ return await self.bridge.call('get_session_tag')
449
+
450
+ async def get_session_timestamp(self) -> Optional[str]:
451
+ """Get the session start timestamp (ISO format).
452
+
453
+ Returns:
454
+ ISO timestamp when session was created or None if not initialized
455
+
456
+ Example:
457
+ >>> timestamp = await swarmkit.get_session_timestamp()
458
+ >>> print(f'Session started: {timestamp}')
459
+ '2025-01-15T10:30:45.123Z'
460
+ """
461
+ await self._ensure_initialized()
462
+ return await self.bridge.call('get_session_timestamp')
463
+
464
+ async def __aenter__(self):
465
+ """Context manager entry."""
466
+ try:
467
+ await self._ensure_initialized()
468
+ return self
469
+ except Exception:
470
+ # Cleanup bridge process if initialization fails
471
+ await self.bridge.stop()
472
+ raise
473
+
474
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
475
+ """Context manager exit - cleanup resources."""
476
+ try:
477
+ await self.kill()
478
+ except Exception as e:
479
+ import warnings
480
+ warnings.warn(f"Error during cleanup: {e}", RuntimeWarning)