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.
- bridge/__init__.py +5 -0
- bridge/dist/bridge.bundle.cjs +8 -0
- swarmkit/__init__.py +152 -0
- swarmkit/agent.py +480 -0
- swarmkit/bridge.py +475 -0
- swarmkit/config.py +92 -0
- swarmkit/pipeline/__init__.py +59 -0
- swarmkit/pipeline/pipeline.py +487 -0
- swarmkit/pipeline/types.py +272 -0
- swarmkit/prompts/__init__.py +126 -0
- swarmkit/prompts/agent_md/judge.md +30 -0
- swarmkit/prompts/agent_md/reduce.md +7 -0
- swarmkit/prompts/agent_md/verify.md +33 -0
- swarmkit/prompts/user/judge.md +1 -0
- swarmkit/prompts/user/retry_feedback.md +9 -0
- swarmkit/prompts/user/verify.md +1 -0
- swarmkit/results.py +45 -0
- swarmkit/retry.py +133 -0
- swarmkit/schema.py +107 -0
- swarmkit/swarm/__init__.py +75 -0
- swarmkit/swarm/results.py +140 -0
- swarmkit/swarm/swarm.py +1751 -0
- swarmkit/swarm/types.py +193 -0
- swarmkit/utils.py +82 -0
- swarmkit-0.1.34.dist-info/METADATA +80 -0
- swarmkit-0.1.34.dist-info/RECORD +29 -0
- swarmkit-0.1.34.dist-info/WHEEL +5 -0
- swarmkit-0.1.34.dist-info/licenses/LICENSE +24 -0
- swarmkit-0.1.34.dist-info/top_level.txt +2 -0
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)
|