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/swarm/swarm.py
ADDED
|
@@ -0,0 +1,2116 @@
|
|
|
1
|
+
"""Swarm - Functional programming abstractions for AI agents.
|
|
2
|
+
|
|
3
|
+
Provides map, filter, reduce, and bestOf operations for parallel AI agent execution.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
```python
|
|
7
|
+
from evolve import Swarm
|
|
8
|
+
|
|
9
|
+
# Minimal usage - uses EVOLVE_API_KEY (or E2B_API_KEY/DAYTONA_API_KEY/MODAL_TOKEN_*) env vars
|
|
10
|
+
swarm = Swarm()
|
|
11
|
+
|
|
12
|
+
# Or with explicit config (E2B)
|
|
13
|
+
from evolve import SwarmConfig, AgentConfig, E2BProvider
|
|
14
|
+
swarm = Swarm(SwarmConfig(
|
|
15
|
+
agent=AgentConfig(type="claude", api_key="..."),
|
|
16
|
+
sandbox=E2BProvider(api_key="..."),
|
|
17
|
+
))
|
|
18
|
+
|
|
19
|
+
# Or with Daytona
|
|
20
|
+
from evolve import DaytonaProvider
|
|
21
|
+
swarm = Swarm(SwarmConfig(sandbox=DaytonaProvider(api_key="...")))
|
|
22
|
+
|
|
23
|
+
# Or with Modal
|
|
24
|
+
from evolve import ModalProvider
|
|
25
|
+
swarm = Swarm(SwarmConfig(sandbox=ModalProvider()))
|
|
26
|
+
|
|
27
|
+
# Map: apply agent to each item
|
|
28
|
+
results = await swarm.map(
|
|
29
|
+
items=[{"doc.txt": "content1"}, {"doc.txt": "content2"}],
|
|
30
|
+
prompt="Analyze this document",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Filter: evaluate and filter items
|
|
34
|
+
critical = await swarm.filter(
|
|
35
|
+
items=results.success,
|
|
36
|
+
prompt="Evaluate severity",
|
|
37
|
+
schema=SeveritySchema,
|
|
38
|
+
condition=lambda x: x.severity == "critical",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Reduce: synthesize many into one
|
|
42
|
+
report = await swarm.reduce(
|
|
43
|
+
items=critical.success,
|
|
44
|
+
prompt="Create summary report",
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import asyncio
|
|
50
|
+
import json
|
|
51
|
+
import secrets
|
|
52
|
+
import warnings
|
|
53
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
54
|
+
|
|
55
|
+
from ..bridge import BridgeManager
|
|
56
|
+
from ..schema import is_pydantic_model, is_dataclass, to_json_schema, validate_and_parse
|
|
57
|
+
from ..config import AgentConfig, IntegrationsSetup
|
|
58
|
+
from ..utils import _encode_files_for_transport, _decode_files_from_transport, _filter_none
|
|
59
|
+
from ..prompts import JUDGE_PROMPT, JUDGE_USER_PROMPT, VERIFY_PROMPT, VERIFY_USER_PROMPT, REDUCE_PROMPT, RETRY_FEEDBACK_PROMPT, apply_template, build_file_tree
|
|
60
|
+
from ..retry import RetryConfig, execute_with_retry
|
|
61
|
+
from .types import (
|
|
62
|
+
FileMap,
|
|
63
|
+
SwarmConfig,
|
|
64
|
+
BestOfConfig,
|
|
65
|
+
VerifyConfig,
|
|
66
|
+
IndexedMeta,
|
|
67
|
+
ReduceMeta,
|
|
68
|
+
JudgeMeta,
|
|
69
|
+
VerifyMeta,
|
|
70
|
+
Prompt,
|
|
71
|
+
ItemInput,
|
|
72
|
+
SchemaType,
|
|
73
|
+
PipelineContext,
|
|
74
|
+
)
|
|
75
|
+
from .results import (
|
|
76
|
+
SwarmResult,
|
|
77
|
+
SwarmResultList,
|
|
78
|
+
ReduceResult,
|
|
79
|
+
BestOfResult,
|
|
80
|
+
BestOfInfo,
|
|
81
|
+
VerifyInfo,
|
|
82
|
+
is_swarm_result,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# CONSTANTS
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
MAX_CONCURRENCY = 100 # Cap to prevent resource exhaustion
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# SWARM CLASS
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
class Swarm:
|
|
98
|
+
"""Functional programming abstractions for AI agents.
|
|
99
|
+
|
|
100
|
+
Provides map, filter, reduce, and bestOf operations for parallel AI agent execution.
|
|
101
|
+
Uses a shared bridge process with multiple Evolve instances for efficiency.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, config: Optional[SwarmConfig] = None):
|
|
105
|
+
"""Initialize Swarm with configuration.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
config: SwarmConfig with agent, sandbox, concurrency settings
|
|
109
|
+
(optional - defaults to EVOLVE_API_KEY and E2B_API_KEY env vars)
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If concurrency exceeds MAX_CONCURRENCY
|
|
113
|
+
"""
|
|
114
|
+
config = config or SwarmConfig()
|
|
115
|
+
if config.concurrency > MAX_CONCURRENCY:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"concurrency={config.concurrency} exceeds max {MAX_CONCURRENCY}. "
|
|
118
|
+
f"For higher parallelism, scale horizontally with multiple processes."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.config = config
|
|
122
|
+
self.semaphore = asyncio.Semaphore(config.concurrency)
|
|
123
|
+
self.bridge = BridgeManager()
|
|
124
|
+
self._bridge_started = False
|
|
125
|
+
|
|
126
|
+
async def _ensure_bridge(self):
|
|
127
|
+
"""Ensure bridge is started."""
|
|
128
|
+
if not self._bridge_started:
|
|
129
|
+
await self.bridge.start()
|
|
130
|
+
self._bridge_started = True
|
|
131
|
+
|
|
132
|
+
# =========================================================================
|
|
133
|
+
# PUBLIC API
|
|
134
|
+
# =========================================================================
|
|
135
|
+
|
|
136
|
+
async def map(
|
|
137
|
+
self,
|
|
138
|
+
items: List[ItemInput],
|
|
139
|
+
prompt: Prompt,
|
|
140
|
+
system_prompt: Optional[str] = None,
|
|
141
|
+
schema: Optional[SchemaType] = None,
|
|
142
|
+
schema_options: Optional[Dict[str, Any]] = None,
|
|
143
|
+
agent: Optional[AgentConfig] = None,
|
|
144
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
145
|
+
skills: Optional[List[str]] = None,
|
|
146
|
+
integrations: Optional[IntegrationsSetup] = None,
|
|
147
|
+
best_of: Optional[BestOfConfig] = None,
|
|
148
|
+
verify: Optional[VerifyConfig] = None,
|
|
149
|
+
retry: Optional[RetryConfig] = None,
|
|
150
|
+
timeout_ms: Optional[int] = None,
|
|
151
|
+
name: Optional[str] = None,
|
|
152
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
153
|
+
) -> SwarmResultList:
|
|
154
|
+
"""Apply an agent to each item in parallel.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
items: List of items (FileMaps or SwarmResults from previous operation)
|
|
158
|
+
prompt: Task prompt (string or function(files, index) -> string)
|
|
159
|
+
system_prompt: Optional system prompt
|
|
160
|
+
schema: Optional Pydantic model or JSON Schema for structured output
|
|
161
|
+
schema_options: Optional validation options
|
|
162
|
+
agent: Optional agent override
|
|
163
|
+
mcp_servers: Optional MCP servers override (replaces swarm default)
|
|
164
|
+
skills: Optional skills override (replaces swarm default)
|
|
165
|
+
integrations: Optional Integrations override (replaces swarm default)
|
|
166
|
+
best_of: Optional bestOf configuration for N candidates + judge per item (mutually exclusive with verify)
|
|
167
|
+
verify: Optional verify configuration for LLM-as-judge quality verification with retry (mutually exclusive with best_of)
|
|
168
|
+
retry: Optional retry configuration for failed items
|
|
169
|
+
timeout_ms: Optional timeout in ms
|
|
170
|
+
name: Optional operation name for observability
|
|
171
|
+
_pipeline_context: Internal - pipeline context for observability (passed by Pipeline)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
SwarmResultList with results for each item
|
|
175
|
+
"""
|
|
176
|
+
await self._ensure_bridge()
|
|
177
|
+
operation_id = self._generate_operation_id()
|
|
178
|
+
timeout = timeout_ms or self.config.timeout_ms
|
|
179
|
+
retry = retry or self.config.retry
|
|
180
|
+
resolved_mcp_servers = mcp_servers if mcp_servers is not None else self.config.mcp_servers
|
|
181
|
+
resolved_skills = skills if skills is not None else self.config.skills
|
|
182
|
+
resolved_integrations = integrations if integrations is not None else self.config.integrations
|
|
183
|
+
|
|
184
|
+
# best_of and verify are mutually exclusive
|
|
185
|
+
if best_of and verify:
|
|
186
|
+
raise ValueError("map() cannot use both best_of and verify options simultaneously")
|
|
187
|
+
|
|
188
|
+
async def process_item(item: ItemInput, index: int) -> SwarmResult:
|
|
189
|
+
# bestOf has internal per-candidate and judge retry - don't double-wrap
|
|
190
|
+
if best_of:
|
|
191
|
+
return await self._execute_map_item_with_best_of(
|
|
192
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
193
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, best_of, retry, timeout,
|
|
194
|
+
name, _pipeline_context
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# verify has internal retry loop with feedback - don't double-wrap with retry
|
|
198
|
+
if verify:
|
|
199
|
+
return await self._execute_map_item_with_verify(
|
|
200
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
201
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, verify, timeout, retry,
|
|
202
|
+
name, _pipeline_context
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Wrap with retry if configured (simple map only)
|
|
206
|
+
if retry:
|
|
207
|
+
return await execute_with_retry(
|
|
208
|
+
lambda attempt: self._execute_map_item(
|
|
209
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
210
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, timeout, attempt,
|
|
211
|
+
name, _pipeline_context
|
|
212
|
+
),
|
|
213
|
+
retry,
|
|
214
|
+
item_index=index,
|
|
215
|
+
)
|
|
216
|
+
return await self._execute_map_item(
|
|
217
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
218
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, timeout, 1,
|
|
219
|
+
name, _pipeline_context
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
results = await asyncio.gather(*[
|
|
223
|
+
process_item(item, i) for i, item in enumerate(items)
|
|
224
|
+
])
|
|
225
|
+
|
|
226
|
+
return SwarmResultList.from_results(list(results))
|
|
227
|
+
|
|
228
|
+
async def filter(
|
|
229
|
+
self,
|
|
230
|
+
items: List[ItemInput],
|
|
231
|
+
prompt: str,
|
|
232
|
+
schema: SchemaType,
|
|
233
|
+
condition: Callable[[Any], bool],
|
|
234
|
+
schema_options: Optional[Dict[str, Any]] = None,
|
|
235
|
+
system_prompt: Optional[str] = None,
|
|
236
|
+
agent: Optional[AgentConfig] = None,
|
|
237
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
238
|
+
skills: Optional[List[str]] = None,
|
|
239
|
+
integrations: Optional[IntegrationsSetup] = None,
|
|
240
|
+
verify: Optional[VerifyConfig] = None,
|
|
241
|
+
retry: Optional[RetryConfig] = None,
|
|
242
|
+
timeout_ms: Optional[int] = None,
|
|
243
|
+
name: Optional[str] = None,
|
|
244
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
245
|
+
) -> SwarmResultList:
|
|
246
|
+
"""Two-step evaluation: agent assesses each item, then local condition applies threshold.
|
|
247
|
+
|
|
248
|
+
1. Agent sees context files, evaluates per prompt, outputs result.json matching schema
|
|
249
|
+
2. Condition function receives parsed data, returns true (success) or false (filtered)
|
|
250
|
+
|
|
251
|
+
Returns ALL items with status:
|
|
252
|
+
- "success": passed condition
|
|
253
|
+
- "filtered": evaluated but didn't pass condition
|
|
254
|
+
- "error": agent error
|
|
255
|
+
|
|
256
|
+
Use `.success` for passing items, `.filtered` for non-passing.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
items: List of items to filter
|
|
260
|
+
prompt: Evaluation prompt
|
|
261
|
+
schema: Pydantic model or JSON Schema (required for filter)
|
|
262
|
+
condition: Function(data) -> bool to determine pass/fail
|
|
263
|
+
schema_options: Optional validation options
|
|
264
|
+
system_prompt: Optional system prompt
|
|
265
|
+
agent: Optional agent override
|
|
266
|
+
mcp_servers: Optional MCP servers override (replaces swarm default)
|
|
267
|
+
skills: Optional skills override (replaces swarm default)
|
|
268
|
+
integrations: Optional Integrations override (replaces swarm default)
|
|
269
|
+
verify: Optional verify configuration for LLM-as-judge quality verification with retry
|
|
270
|
+
retry: Optional retry configuration for failed items
|
|
271
|
+
timeout_ms: Optional timeout in ms
|
|
272
|
+
name: Optional operation name for observability
|
|
273
|
+
_pipeline_context: Internal - pipeline context for observability (passed by Pipeline)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
SwarmResultList with all items (success, filtered, or error status)
|
|
277
|
+
"""
|
|
278
|
+
await self._ensure_bridge()
|
|
279
|
+
operation_id = self._generate_operation_id()
|
|
280
|
+
timeout = timeout_ms or self.config.timeout_ms
|
|
281
|
+
retry = retry or self.config.retry
|
|
282
|
+
resolved_mcp_servers = mcp_servers if mcp_servers is not None else self.config.mcp_servers
|
|
283
|
+
resolved_skills = skills if skills is not None else self.config.skills
|
|
284
|
+
resolved_integrations = integrations if integrations is not None else self.config.integrations
|
|
285
|
+
|
|
286
|
+
async def process_item(item: ItemInput, index: int) -> SwarmResult:
|
|
287
|
+
# verify has internal retry loop with feedback - don't double-wrap with retry
|
|
288
|
+
if verify:
|
|
289
|
+
return await self._execute_filter_item_with_verify(
|
|
290
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
291
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, verify, timeout, retry,
|
|
292
|
+
name, _pipeline_context
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Wrap with retry if configured
|
|
296
|
+
if retry:
|
|
297
|
+
return await execute_with_retry(
|
|
298
|
+
lambda attempt: self._execute_filter_item(
|
|
299
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
300
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, timeout, attempt,
|
|
301
|
+
name, _pipeline_context
|
|
302
|
+
),
|
|
303
|
+
retry,
|
|
304
|
+
item_index=index,
|
|
305
|
+
)
|
|
306
|
+
return await self._execute_filter_item(
|
|
307
|
+
item, prompt, index, operation_id, system_prompt, schema,
|
|
308
|
+
schema_options, agent, resolved_mcp_servers, resolved_skills, resolved_integrations, timeout, 1,
|
|
309
|
+
name, _pipeline_context
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
evaluated = await asyncio.gather(*[
|
|
313
|
+
process_item(item, i) for i, item in enumerate(items)
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
# Apply condition and set status accordingly
|
|
317
|
+
results: List[SwarmResult] = []
|
|
318
|
+
for r in evaluated:
|
|
319
|
+
if r.status == "error":
|
|
320
|
+
results.append(r)
|
|
321
|
+
elif r.data is not None:
|
|
322
|
+
try:
|
|
323
|
+
if condition(r.data):
|
|
324
|
+
results.append(r) # success
|
|
325
|
+
else:
|
|
326
|
+
# Didn't pass condition → filtered
|
|
327
|
+
results.append(SwarmResult(
|
|
328
|
+
status="filtered",
|
|
329
|
+
data=r.data,
|
|
330
|
+
files=r.files,
|
|
331
|
+
meta=r.meta,
|
|
332
|
+
verify=r.verify,
|
|
333
|
+
))
|
|
334
|
+
except Exception as e:
|
|
335
|
+
# Condition threw → error (preserve raw_data if present)
|
|
336
|
+
results.append(SwarmResult(
|
|
337
|
+
status="error",
|
|
338
|
+
data=None,
|
|
339
|
+
files=r.files,
|
|
340
|
+
meta=r.meta,
|
|
341
|
+
error=f"Condition function threw: {e}",
|
|
342
|
+
raw_data=getattr(r, 'raw_data', None),
|
|
343
|
+
))
|
|
344
|
+
else:
|
|
345
|
+
results.append(r)
|
|
346
|
+
|
|
347
|
+
return SwarmResultList.from_results(results)
|
|
348
|
+
|
|
349
|
+
async def reduce(
|
|
350
|
+
self,
|
|
351
|
+
items: List[ItemInput],
|
|
352
|
+
prompt: str,
|
|
353
|
+
system_prompt: Optional[str] = None,
|
|
354
|
+
schema: Optional[SchemaType] = None,
|
|
355
|
+
schema_options: Optional[Dict[str, Any]] = None,
|
|
356
|
+
agent: Optional[AgentConfig] = None,
|
|
357
|
+
mcp_servers: Optional[Dict[str, Any]] = None,
|
|
358
|
+
skills: Optional[List[str]] = None,
|
|
359
|
+
integrations: Optional[IntegrationsSetup] = None,
|
|
360
|
+
verify: Optional[VerifyConfig] = None,
|
|
361
|
+
retry: Optional[RetryConfig] = None,
|
|
362
|
+
timeout_ms: Optional[int] = None,
|
|
363
|
+
name: Optional[str] = None,
|
|
364
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
365
|
+
) -> ReduceResult:
|
|
366
|
+
"""Synthesize many items into one.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
items: List of items to reduce
|
|
370
|
+
prompt: Synthesis prompt
|
|
371
|
+
system_prompt: Optional system prompt
|
|
372
|
+
schema: Optional Pydantic model or JSON Schema
|
|
373
|
+
schema_options: Optional validation options
|
|
374
|
+
agent: Optional agent override
|
|
375
|
+
mcp_servers: Optional MCP servers override (replaces swarm default)
|
|
376
|
+
skills: Optional skills override (replaces swarm default)
|
|
377
|
+
integrations: Optional Integrations override (replaces swarm default)
|
|
378
|
+
verify: Optional verify configuration for LLM-as-judge quality verification with retry
|
|
379
|
+
retry: Optional retry configuration
|
|
380
|
+
timeout_ms: Optional timeout in ms
|
|
381
|
+
name: Optional operation name for observability
|
|
382
|
+
_pipeline_context: Internal - pipeline context for observability (passed by Pipeline)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
ReduceResult with synthesized output
|
|
386
|
+
"""
|
|
387
|
+
await self._ensure_bridge()
|
|
388
|
+
operation_id = self._generate_operation_id()
|
|
389
|
+
timeout = timeout_ms or self.config.timeout_ms
|
|
390
|
+
retry = retry or self.config.retry
|
|
391
|
+
resolved_mcp_servers = mcp_servers if mcp_servers is not None else self.config.mcp_servers
|
|
392
|
+
resolved_skills = skills if skills is not None else self.config.skills
|
|
393
|
+
resolved_integrations = integrations if integrations is not None else self.config.integrations
|
|
394
|
+
|
|
395
|
+
# Collect files and track original indices
|
|
396
|
+
all_files: List[FileMap] = []
|
|
397
|
+
indices: List[int] = []
|
|
398
|
+
|
|
399
|
+
for i, item in enumerate(items):
|
|
400
|
+
all_files.append(self._get_files(item))
|
|
401
|
+
indices.append(self._get_index(item, i))
|
|
402
|
+
|
|
403
|
+
# Build context: item_0/, item_1/, etc.
|
|
404
|
+
context: FileMap = {}
|
|
405
|
+
for i, files in enumerate(all_files):
|
|
406
|
+
for file_name, content in files.items():
|
|
407
|
+
context[f"item_{indices[i]}/{file_name}"] = content
|
|
408
|
+
|
|
409
|
+
# Build reduce system prompt (context structure + user's system_prompt)
|
|
410
|
+
file_tree = build_file_tree(context)
|
|
411
|
+
reduce_context_prompt = apply_template(REDUCE_PROMPT, {"fileTree": file_tree})
|
|
412
|
+
final_system_prompt = (
|
|
413
|
+
f"{reduce_context_prompt}\n\n{system_prompt}"
|
|
414
|
+
if system_prompt
|
|
415
|
+
else reduce_context_prompt
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Build meta (sandboxId/tag updated after execution)
|
|
419
|
+
def build_meta(result: Dict[str, Any], error_retry: Optional[int] = None, verify_retry: Optional[int] = None) -> ReduceMeta:
|
|
420
|
+
return ReduceMeta(
|
|
421
|
+
operation_id=operation_id,
|
|
422
|
+
operation="reduce",
|
|
423
|
+
tag=result["tag"],
|
|
424
|
+
sandbox_id=result["sandbox_id"],
|
|
425
|
+
swarm_name=self.config.tag,
|
|
426
|
+
operation_name=name,
|
|
427
|
+
error_retry=error_retry,
|
|
428
|
+
verify_retry=verify_retry,
|
|
429
|
+
input_count=len(items),
|
|
430
|
+
input_indices=indices,
|
|
431
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Shared execution logic
|
|
435
|
+
async def execute_once(prompt_to_use: str, tag_prefix: str, error_retry: Optional[int] = None, attempt_index: Optional[int] = None) -> ReduceResult:
|
|
436
|
+
# Calculate verify_retry from attempt_index (matches TS SDK pattern)
|
|
437
|
+
verify_retry = attempt_index - 1 if attempt_index and attempt_index > 1 else None
|
|
438
|
+
|
|
439
|
+
# Build observability for JSONL
|
|
440
|
+
observability = _filter_none({
|
|
441
|
+
'swarm_name': self.config.tag,
|
|
442
|
+
'operation_name': name,
|
|
443
|
+
'operation_id': operation_id,
|
|
444
|
+
'operation': 'reduce',
|
|
445
|
+
'role': 'worker',
|
|
446
|
+
'error_retry': error_retry,
|
|
447
|
+
'verify_retry': verify_retry,
|
|
448
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
async with self.semaphore:
|
|
452
|
+
result = await self._execute(
|
|
453
|
+
context=context,
|
|
454
|
+
prompt=prompt_to_use,
|
|
455
|
+
system_prompt=final_system_prompt,
|
|
456
|
+
schema=schema,
|
|
457
|
+
schema_options=schema_options,
|
|
458
|
+
agent=agent,
|
|
459
|
+
mcp_servers=resolved_mcp_servers,
|
|
460
|
+
skills=resolved_skills,
|
|
461
|
+
integrations=resolved_integrations,
|
|
462
|
+
tag_prefix=tag_prefix,
|
|
463
|
+
timeout=timeout,
|
|
464
|
+
observability=observability,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
meta = build_meta(result, error_retry, verify_retry)
|
|
468
|
+
|
|
469
|
+
if result.get("error"):
|
|
470
|
+
return ReduceResult(
|
|
471
|
+
status="error",
|
|
472
|
+
data=None,
|
|
473
|
+
files=result["files"],
|
|
474
|
+
meta=meta,
|
|
475
|
+
error=result["error"],
|
|
476
|
+
raw_data=result.get("raw_data"),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return ReduceResult(
|
|
480
|
+
status="success",
|
|
481
|
+
data=result["data"],
|
|
482
|
+
files=result["files"],
|
|
483
|
+
meta=meta,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
base_tag = f"{self.config.tag}-reduce"
|
|
487
|
+
|
|
488
|
+
# verify has internal retry loop with feedback - don't double-wrap with retry
|
|
489
|
+
if verify:
|
|
490
|
+
return await self._run_with_verification(
|
|
491
|
+
worker_fn=lambda current_prompt, tag_prefix, attempt_index=None: execute_once(
|
|
492
|
+
current_prompt, tag_prefix, None, attempt_index # Pass attempt_index for verify_retry
|
|
493
|
+
),
|
|
494
|
+
original_prompt=prompt,
|
|
495
|
+
input_files=context,
|
|
496
|
+
verify_config=verify,
|
|
497
|
+
mcp_servers=resolved_mcp_servers,
|
|
498
|
+
skills=resolved_skills,
|
|
499
|
+
integrations=resolved_integrations,
|
|
500
|
+
timeout=timeout,
|
|
501
|
+
system_prompt=final_system_prompt,
|
|
502
|
+
schema=schema,
|
|
503
|
+
operation_id=operation_id,
|
|
504
|
+
base_tag=base_tag,
|
|
505
|
+
retry=retry,
|
|
506
|
+
name=name,
|
|
507
|
+
_pipeline_context=_pipeline_context,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Wrap with retry if configured
|
|
511
|
+
if retry:
|
|
512
|
+
async def execute_fn(attempt: int = 1) -> ReduceResult:
|
|
513
|
+
tag_prefix = f"{base_tag}-er{attempt - 1}" if attempt > 1 else base_tag
|
|
514
|
+
error_retry = attempt - 1 if attempt > 1 else None
|
|
515
|
+
return await execute_once(prompt, tag_prefix, error_retry)
|
|
516
|
+
return await execute_with_retry(execute_fn, retry)
|
|
517
|
+
|
|
518
|
+
return await execute_once(prompt, base_tag)
|
|
519
|
+
|
|
520
|
+
async def best_of(
|
|
521
|
+
self,
|
|
522
|
+
item: ItemInput,
|
|
523
|
+
prompt: str,
|
|
524
|
+
config: BestOfConfig,
|
|
525
|
+
system_prompt: Optional[str] = None,
|
|
526
|
+
schema: Optional[SchemaType] = None,
|
|
527
|
+
schema_options: Optional[Dict[str, Any]] = None,
|
|
528
|
+
retry: Optional[RetryConfig] = None,
|
|
529
|
+
timeout_ms: Optional[int] = None,
|
|
530
|
+
name: Optional[str] = None,
|
|
531
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
532
|
+
) -> BestOfResult:
|
|
533
|
+
"""Run N candidates on the same task, judge picks the best.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
item: Single item to process
|
|
537
|
+
prompt: Task prompt
|
|
538
|
+
config: BestOf configuration (n, judge_criteria, mcp_servers, etc.)
|
|
539
|
+
system_prompt: Optional system prompt
|
|
540
|
+
schema: Optional Pydantic model or JSON Schema
|
|
541
|
+
schema_options: Optional validation options
|
|
542
|
+
retry: Optional retry configuration for candidates and judge
|
|
543
|
+
timeout_ms: Optional timeout in ms
|
|
544
|
+
name: Optional operation name for observability
|
|
545
|
+
_pipeline_context: Internal - pipeline context for observability (passed by Pipeline)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
BestOfResult with winner, candidates, and judge info
|
|
549
|
+
"""
|
|
550
|
+
await self._ensure_bridge()
|
|
551
|
+
retry = retry or self.config.retry
|
|
552
|
+
|
|
553
|
+
# Resolve n
|
|
554
|
+
n = config.n or (len(config.task_agents) if config.task_agents else None)
|
|
555
|
+
if n is None:
|
|
556
|
+
raise ValueError("bestOf requires n or task_agents")
|
|
557
|
+
if n < 2:
|
|
558
|
+
raise ValueError("bestOf requires n >= 2")
|
|
559
|
+
|
|
560
|
+
operation_id = self._generate_operation_id()
|
|
561
|
+
timeout = timeout_ms or self.config.timeout_ms
|
|
562
|
+
input_files = self._get_files(item)
|
|
563
|
+
|
|
564
|
+
# Resolve MCP servers, skills, and integrations for candidates and judge
|
|
565
|
+
candidate_mcp_servers = config.mcp_servers if config.mcp_servers is not None else self.config.mcp_servers
|
|
566
|
+
judge_mcp_servers = config.judge_mcp_servers if config.judge_mcp_servers is not None else config.mcp_servers if config.mcp_servers is not None else self.config.mcp_servers
|
|
567
|
+
candidate_skills = config.skills if config.skills is not None else self.config.skills
|
|
568
|
+
judge_skills = config.judge_skills if config.judge_skills is not None else config.skills if config.skills is not None else self.config.skills
|
|
569
|
+
candidate_integrations = config.integrations if config.integrations is not None else self.config.integrations
|
|
570
|
+
judge_integrations = config.judge_integrations if config.judge_integrations is not None else config.integrations if config.integrations is not None else self.config.integrations
|
|
571
|
+
|
|
572
|
+
# Run candidates (semaphore acquired inside _execute_best_of_candidate)
|
|
573
|
+
async def run_candidate(candidate_index: int) -> SwarmResult:
|
|
574
|
+
if retry:
|
|
575
|
+
result = await execute_with_retry(
|
|
576
|
+
lambda attempt: self._execute_best_of_candidate(
|
|
577
|
+
input_files=input_files,
|
|
578
|
+
prompt=prompt,
|
|
579
|
+
candidate_index=candidate_index,
|
|
580
|
+
operation_id=operation_id,
|
|
581
|
+
config=config,
|
|
582
|
+
mcp_servers=candidate_mcp_servers,
|
|
583
|
+
skills=candidate_skills,
|
|
584
|
+
integrations=candidate_integrations,
|
|
585
|
+
system_prompt=system_prompt,
|
|
586
|
+
schema=schema,
|
|
587
|
+
schema_options=schema_options,
|
|
588
|
+
timeout=timeout,
|
|
589
|
+
attempt=attempt,
|
|
590
|
+
name=name,
|
|
591
|
+
_pipeline_context=_pipeline_context,
|
|
592
|
+
),
|
|
593
|
+
retry,
|
|
594
|
+
item_index=0, # standalone bestOf uses item_index=0
|
|
595
|
+
)
|
|
596
|
+
else:
|
|
597
|
+
result = await self._execute_best_of_candidate(
|
|
598
|
+
input_files=input_files,
|
|
599
|
+
prompt=prompt,
|
|
600
|
+
candidate_index=candidate_index,
|
|
601
|
+
operation_id=operation_id,
|
|
602
|
+
config=config,
|
|
603
|
+
mcp_servers=candidate_mcp_servers,
|
|
604
|
+
skills=candidate_skills,
|
|
605
|
+
integrations=candidate_integrations,
|
|
606
|
+
system_prompt=system_prompt,
|
|
607
|
+
schema=schema,
|
|
608
|
+
schema_options=schema_options,
|
|
609
|
+
timeout=timeout,
|
|
610
|
+
name=name,
|
|
611
|
+
_pipeline_context=_pipeline_context,
|
|
612
|
+
)
|
|
613
|
+
# Call callback after candidate completes
|
|
614
|
+
if config.on_candidate_complete:
|
|
615
|
+
config.on_candidate_complete(0, candidate_index, result.status if result.status != "filtered" else "success")
|
|
616
|
+
return result
|
|
617
|
+
|
|
618
|
+
candidates = await asyncio.gather(*[
|
|
619
|
+
run_candidate(i) for i in range(n)
|
|
620
|
+
])
|
|
621
|
+
candidates = list(candidates)
|
|
622
|
+
|
|
623
|
+
# Run judge (semaphore acquired inside _execute_best_of_judge)
|
|
624
|
+
# Judge uses default retry (status === "error"), not custom retry_on
|
|
625
|
+
if retry:
|
|
626
|
+
# Create a copy of retry config without custom retry_on for judge
|
|
627
|
+
judge_retry = RetryConfig(
|
|
628
|
+
max_attempts=retry.max_attempts,
|
|
629
|
+
backoff_ms=retry.backoff_ms,
|
|
630
|
+
backoff_multiplier=retry.backoff_multiplier,
|
|
631
|
+
retry_on=None, # Use default (status == "error")
|
|
632
|
+
)
|
|
633
|
+
judge = await execute_with_retry(
|
|
634
|
+
lambda attempt: self._execute_best_of_judge(
|
|
635
|
+
input_files=input_files,
|
|
636
|
+
task_prompt=prompt,
|
|
637
|
+
candidates=candidates,
|
|
638
|
+
config=config,
|
|
639
|
+
mcp_servers=judge_mcp_servers,
|
|
640
|
+
skills=judge_skills,
|
|
641
|
+
integrations=judge_integrations,
|
|
642
|
+
timeout=timeout,
|
|
643
|
+
system_prompt=system_prompt,
|
|
644
|
+
schema=schema,
|
|
645
|
+
operation_id=operation_id,
|
|
646
|
+
attempt=attempt,
|
|
647
|
+
name=name,
|
|
648
|
+
_pipeline_context=_pipeline_context,
|
|
649
|
+
),
|
|
650
|
+
judge_retry
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
judge = await self._execute_best_of_judge(
|
|
654
|
+
input_files=input_files,
|
|
655
|
+
task_prompt=prompt,
|
|
656
|
+
candidates=candidates,
|
|
657
|
+
config=config,
|
|
658
|
+
mcp_servers=judge_mcp_servers,
|
|
659
|
+
skills=judge_skills,
|
|
660
|
+
integrations=judge_integrations,
|
|
661
|
+
timeout=timeout,
|
|
662
|
+
system_prompt=system_prompt,
|
|
663
|
+
schema=schema,
|
|
664
|
+
operation_id=operation_id,
|
|
665
|
+
name=name,
|
|
666
|
+
_pipeline_context=_pipeline_context,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
first_success = next((i for i, c in enumerate(candidates) if c.status == "success"), -1)
|
|
670
|
+
winner_index = judge["winner"] if judge["winner"] is not None else (first_success if first_success >= 0 else 0)
|
|
671
|
+
|
|
672
|
+
# Call judge callback
|
|
673
|
+
if config.on_judge_complete:
|
|
674
|
+
config.on_judge_complete(0, winner_index, judge.get("reasoning", ""))
|
|
675
|
+
|
|
676
|
+
judge_meta = JudgeMeta(
|
|
677
|
+
operation_id=operation_id,
|
|
678
|
+
operation="bestof-judge",
|
|
679
|
+
tag=judge["tag"],
|
|
680
|
+
sandbox_id=judge["sandbox_id"],
|
|
681
|
+
swarm_name=self.config.tag,
|
|
682
|
+
operation_name=name,
|
|
683
|
+
candidate_count=n,
|
|
684
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
return BestOfResult(
|
|
688
|
+
winner=candidates[winner_index] if winner_index < len(candidates) else candidates[0],
|
|
689
|
+
winner_index=winner_index,
|
|
690
|
+
judge_reasoning=judge.get("reasoning", "Judge failed to provide reasoning"),
|
|
691
|
+
judge_meta=judge_meta,
|
|
692
|
+
candidates=candidates,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
async def close(self):
|
|
696
|
+
"""Close the bridge connection."""
|
|
697
|
+
if self._bridge_started:
|
|
698
|
+
await self.bridge.stop()
|
|
699
|
+
self._bridge_started = False
|
|
700
|
+
|
|
701
|
+
async def __aenter__(self):
|
|
702
|
+
"""Async context manager entry - ensures bridge is started."""
|
|
703
|
+
await self._ensure_bridge()
|
|
704
|
+
return self
|
|
705
|
+
|
|
706
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
707
|
+
"""Async context manager exit - closes the bridge."""
|
|
708
|
+
await self.close()
|
|
709
|
+
return False
|
|
710
|
+
|
|
711
|
+
# =========================================================================
|
|
712
|
+
# PRIVATE: EXECUTION
|
|
713
|
+
# =========================================================================
|
|
714
|
+
|
|
715
|
+
async def _execute(
|
|
716
|
+
self,
|
|
717
|
+
context: FileMap,
|
|
718
|
+
prompt: str,
|
|
719
|
+
system_prompt: Optional[str],
|
|
720
|
+
schema: Optional[SchemaType],
|
|
721
|
+
schema_options: Optional[Dict[str, Any]],
|
|
722
|
+
agent: Optional[AgentConfig],
|
|
723
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
724
|
+
skills: Optional[List[str]],
|
|
725
|
+
integrations: Optional[IntegrationsSetup],
|
|
726
|
+
tag_prefix: str,
|
|
727
|
+
timeout: int,
|
|
728
|
+
observability: Optional[Dict[str, Any]] = None,
|
|
729
|
+
) -> Dict[str, Any]:
|
|
730
|
+
"""Execute a single agent task."""
|
|
731
|
+
instance_id = f"{tag_prefix}-{secrets.token_hex(4)}"
|
|
732
|
+
|
|
733
|
+
# Build agent config (merges override with base config)
|
|
734
|
+
agent_config = self._build_agent_config(agent)
|
|
735
|
+
|
|
736
|
+
# Convert schema to JSON Schema
|
|
737
|
+
json_schema = to_json_schema(schema)
|
|
738
|
+
|
|
739
|
+
# Build init params with _filter_none to exclude None values
|
|
740
|
+
# TS SDK resolves defaults from env vars when not provided
|
|
741
|
+
init_params = _filter_none({
|
|
742
|
+
# Agent config (optional - TS SDK resolves from env vars)
|
|
743
|
+
'agent_type': agent_config.type if agent_config else None,
|
|
744
|
+
'api_key': agent_config.api_key if agent_config else None,
|
|
745
|
+
'provider_api_key': agent_config.provider_api_key if agent_config else None,
|
|
746
|
+
'oauth_token': agent_config.oauth_token if agent_config else None,
|
|
747
|
+
'provider_base_url': agent_config.provider_base_url if agent_config else None,
|
|
748
|
+
'model': agent_config.model if agent_config else None,
|
|
749
|
+
'reasoning_effort': agent_config.reasoning_effort if agent_config else None,
|
|
750
|
+
'max_context_size': agent_config.max_context_size if agent_config else None,
|
|
751
|
+
# Sandbox (optional - TS SDK auto-resolves from EVOLVE_API_KEY/E2B_API_KEY/DAYTONA_API_KEY)
|
|
752
|
+
'sandbox_provider': {'type': self.config.sandbox.type, 'config': self.config.sandbox.config} if self.config.sandbox else None,
|
|
753
|
+
# Other settings
|
|
754
|
+
'workspace_mode': self.config.workspace_mode,
|
|
755
|
+
'session_tag_prefix': tag_prefix,
|
|
756
|
+
'system_prompt': system_prompt,
|
|
757
|
+
'schema': json_schema,
|
|
758
|
+
'schema_options': schema_options,
|
|
759
|
+
'context': _encode_files_for_transport(context) if context else None,
|
|
760
|
+
'mcp_servers': mcp_servers,
|
|
761
|
+
'skills': skills,
|
|
762
|
+
'integrations': integrations.to_dict() if integrations else None,
|
|
763
|
+
'observability': observability,
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
files: FileMap = {}
|
|
767
|
+
data: Any = None
|
|
768
|
+
error: Optional[str] = None
|
|
769
|
+
raw_data: Optional[str] = None
|
|
770
|
+
sandbox_id = ""
|
|
771
|
+
tag = tag_prefix
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
# Create instance
|
|
775
|
+
await self.bridge.create_instance(instance_id, init_params)
|
|
776
|
+
|
|
777
|
+
# Run prompt
|
|
778
|
+
run_result = await self.bridge.run_on_instance(
|
|
779
|
+
instance_id,
|
|
780
|
+
prompt,
|
|
781
|
+
timeout_ms=timeout,
|
|
782
|
+
call_timeout_s=(timeout / 1000) + 60, # Add buffer for RPC overhead
|
|
783
|
+
)
|
|
784
|
+
sandbox_id = run_result.get('sandbox_id', '')
|
|
785
|
+
|
|
786
|
+
# Get output
|
|
787
|
+
output = await self.bridge.get_output_on_instance(instance_id, recursive=True)
|
|
788
|
+
files = _decode_files_from_transport(output.get('files', {}))
|
|
789
|
+
|
|
790
|
+
if run_result.get('exit_code', 0) != 0:
|
|
791
|
+
error = f"Agent exited with code {run_result.get('exit_code')}"
|
|
792
|
+
elif json_schema:
|
|
793
|
+
# Validate result.json against schema
|
|
794
|
+
raw_json = files.get('result.json')
|
|
795
|
+
if raw_json is not None:
|
|
796
|
+
if isinstance(raw_json, bytes):
|
|
797
|
+
raw_json = raw_json.decode('utf-8')
|
|
798
|
+
|
|
799
|
+
if is_pydantic_model(schema) or is_dataclass(schema):
|
|
800
|
+
# Pydantic model or dataclass - validate and return instance
|
|
801
|
+
try:
|
|
802
|
+
strict = schema_options.get('mode') == 'strict' if schema_options else False
|
|
803
|
+
data = validate_and_parse(raw_json, schema, strict=strict)
|
|
804
|
+
except Exception as e:
|
|
805
|
+
error = f"Schema validation failed: {e}"
|
|
806
|
+
raw_data = raw_json
|
|
807
|
+
else:
|
|
808
|
+
# JSON Schema dict - use TS validation result
|
|
809
|
+
data = output.get('data')
|
|
810
|
+
if output.get('error'):
|
|
811
|
+
error = output['error']
|
|
812
|
+
if output.get('raw_data'):
|
|
813
|
+
raw_data = output['raw_data']
|
|
814
|
+
else:
|
|
815
|
+
error = "Schema provided but agent did not create output/result.json"
|
|
816
|
+
else:
|
|
817
|
+
data = files
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
error = str(e)
|
|
821
|
+
# Try to capture partial output even on failure (e.g., timeout)
|
|
822
|
+
try:
|
|
823
|
+
output = await self.bridge.get_output_on_instance(instance_id, recursive=True)
|
|
824
|
+
files = _decode_files_from_transport(output.get('files', {}))
|
|
825
|
+
except Exception:
|
|
826
|
+
pass # Sandbox may already be gone
|
|
827
|
+
finally:
|
|
828
|
+
# Always cleanup
|
|
829
|
+
try:
|
|
830
|
+
await self.bridge.kill_instance(instance_id)
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
"files": files,
|
|
836
|
+
"data": data,
|
|
837
|
+
"tag": tag,
|
|
838
|
+
"sandbox_id": sandbox_id,
|
|
839
|
+
"error": error,
|
|
840
|
+
"raw_data": raw_data,
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
# =========================================================================
|
|
844
|
+
# PRIVATE: MAP
|
|
845
|
+
# =========================================================================
|
|
846
|
+
|
|
847
|
+
async def _execute_map_item(
|
|
848
|
+
self,
|
|
849
|
+
item: ItemInput,
|
|
850
|
+
prompt: Prompt,
|
|
851
|
+
index: int,
|
|
852
|
+
operation_id: str,
|
|
853
|
+
system_prompt: Optional[str],
|
|
854
|
+
schema: Optional[SchemaType],
|
|
855
|
+
schema_options: Optional[Dict[str, Any]],
|
|
856
|
+
agent: Optional[AgentConfig],
|
|
857
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
858
|
+
skills: Optional[List[str]],
|
|
859
|
+
integrations: Optional[IntegrationsSetup],
|
|
860
|
+
timeout: int,
|
|
861
|
+
attempt: int = 1,
|
|
862
|
+
name: Optional[str] = None,
|
|
863
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
864
|
+
) -> SwarmResult:
|
|
865
|
+
"""Execute a single map item."""
|
|
866
|
+
files = self._get_files(item)
|
|
867
|
+
error_retry = attempt - 1 if attempt > 1 else None
|
|
868
|
+
tag_prefix = (
|
|
869
|
+
f"{self.config.tag}-map-{index}-er{error_retry}"
|
|
870
|
+
if error_retry
|
|
871
|
+
else f"{self.config.tag}-map-{index}"
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
try:
|
|
875
|
+
prompt_str = self._resolve_prompt(prompt, files, index)
|
|
876
|
+
except Exception as e:
|
|
877
|
+
return self._build_error_result(
|
|
878
|
+
f"Prompt function threw: {e}",
|
|
879
|
+
IndexedMeta(
|
|
880
|
+
operation_id=operation_id, operation="map", tag=tag_prefix,
|
|
881
|
+
sandbox_id="", swarm_name=self.config.tag, operation_name=name,
|
|
882
|
+
item_index=index, **self._pipeline_context_to_meta(_pipeline_context)
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Build observability for JSONL
|
|
887
|
+
observability = _filter_none({
|
|
888
|
+
'swarm_name': self.config.tag,
|
|
889
|
+
'operation_name': name,
|
|
890
|
+
'operation_id': operation_id,
|
|
891
|
+
'operation': 'map',
|
|
892
|
+
'item_index': index,
|
|
893
|
+
'role': 'worker',
|
|
894
|
+
'error_retry': error_retry,
|
|
895
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
async with self.semaphore:
|
|
899
|
+
result = await self._execute(
|
|
900
|
+
context=files,
|
|
901
|
+
prompt=prompt_str,
|
|
902
|
+
system_prompt=system_prompt,
|
|
903
|
+
schema=schema,
|
|
904
|
+
schema_options=schema_options,
|
|
905
|
+
agent=agent,
|
|
906
|
+
mcp_servers=mcp_servers,
|
|
907
|
+
skills=skills,
|
|
908
|
+
integrations=integrations,
|
|
909
|
+
tag_prefix=tag_prefix,
|
|
910
|
+
timeout=timeout,
|
|
911
|
+
observability=observability,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
meta = IndexedMeta(
|
|
915
|
+
operation_id=operation_id,
|
|
916
|
+
operation="map",
|
|
917
|
+
tag=result["tag"],
|
|
918
|
+
sandbox_id=result["sandbox_id"],
|
|
919
|
+
swarm_name=self.config.tag,
|
|
920
|
+
operation_name=name,
|
|
921
|
+
error_retry=error_retry,
|
|
922
|
+
item_index=index,
|
|
923
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
return self._build_result(result, meta)
|
|
927
|
+
|
|
928
|
+
async def _execute_map_item_with_best_of(
|
|
929
|
+
self,
|
|
930
|
+
item: ItemInput,
|
|
931
|
+
prompt: Prompt,
|
|
932
|
+
index: int,
|
|
933
|
+
operation_id: str,
|
|
934
|
+
system_prompt: Optional[str],
|
|
935
|
+
schema: Optional[SchemaType],
|
|
936
|
+
schema_options: Optional[Dict[str, Any]],
|
|
937
|
+
agent: Optional[AgentConfig],
|
|
938
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
939
|
+
skills: Optional[List[str]],
|
|
940
|
+
integrations: Optional[IntegrationsSetup],
|
|
941
|
+
best_of_config: BestOfConfig,
|
|
942
|
+
retry: Optional[RetryConfig],
|
|
943
|
+
timeout: int,
|
|
944
|
+
name: Optional[str] = None,
|
|
945
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
946
|
+
) -> SwarmResult:
|
|
947
|
+
"""Execute a single map item with bestOf."""
|
|
948
|
+
files = self._get_files(item)
|
|
949
|
+
tag_prefix = f"{self.config.tag}-map-{index}"
|
|
950
|
+
|
|
951
|
+
try:
|
|
952
|
+
prompt_str = self._resolve_prompt(prompt, files, index)
|
|
953
|
+
except Exception as e:
|
|
954
|
+
return self._build_error_result(
|
|
955
|
+
f"Prompt function threw: {e}",
|
|
956
|
+
IndexedMeta(
|
|
957
|
+
operation_id=operation_id, operation="map", tag=tag_prefix,
|
|
958
|
+
sandbox_id="", swarm_name=self.config.tag, operation_name=name,
|
|
959
|
+
item_index=index, **self._pipeline_context_to_meta(_pipeline_context)
|
|
960
|
+
)
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
n = best_of_config.n or (len(best_of_config.task_agents) if best_of_config.task_agents else None)
|
|
964
|
+
if n is None or n < 2:
|
|
965
|
+
return self._build_error_result(
|
|
966
|
+
"bestOf requires n >= 2 or task_agents with at least 2 elements",
|
|
967
|
+
IndexedMeta(
|
|
968
|
+
operation_id=operation_id, operation="map", tag=tag_prefix,
|
|
969
|
+
sandbox_id="", swarm_name=self.config.tag, operation_name=name,
|
|
970
|
+
item_index=index, **self._pipeline_context_to_meta(_pipeline_context)
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# Resolve MCP servers, skills, and integrations: bestOf config overrides operation-level
|
|
975
|
+
candidate_mcp_servers = best_of_config.mcp_servers if best_of_config.mcp_servers is not None else mcp_servers
|
|
976
|
+
judge_mcp_servers = best_of_config.judge_mcp_servers if best_of_config.judge_mcp_servers is not None else best_of_config.mcp_servers if best_of_config.mcp_servers is not None else mcp_servers
|
|
977
|
+
candidate_skills = best_of_config.skills if best_of_config.skills is not None else skills
|
|
978
|
+
judge_skills = best_of_config.judge_skills if best_of_config.judge_skills is not None else best_of_config.skills if best_of_config.skills is not None else skills
|
|
979
|
+
candidate_integrations = best_of_config.integrations if best_of_config.integrations is not None else integrations
|
|
980
|
+
judge_integrations = best_of_config.judge_integrations if best_of_config.judge_integrations is not None else best_of_config.integrations if best_of_config.integrations is not None else integrations
|
|
981
|
+
|
|
982
|
+
# Run candidates in parallel (semaphore acquired inside _execute_best_of_candidate)
|
|
983
|
+
async def run_candidate(candidate_index: int) -> SwarmResult:
|
|
984
|
+
if retry:
|
|
985
|
+
result = await execute_with_retry(
|
|
986
|
+
lambda attempt: self._execute_best_of_candidate(
|
|
987
|
+
input_files=files,
|
|
988
|
+
prompt=prompt_str,
|
|
989
|
+
candidate_index=candidate_index,
|
|
990
|
+
operation_id=operation_id,
|
|
991
|
+
config=best_of_config,
|
|
992
|
+
mcp_servers=candidate_mcp_servers,
|
|
993
|
+
skills=candidate_skills,
|
|
994
|
+
integrations=candidate_integrations,
|
|
995
|
+
system_prompt=system_prompt,
|
|
996
|
+
schema=schema,
|
|
997
|
+
schema_options=schema_options,
|
|
998
|
+
timeout=timeout,
|
|
999
|
+
parent_index=index,
|
|
1000
|
+
attempt=attempt,
|
|
1001
|
+
name=name,
|
|
1002
|
+
_pipeline_context=_pipeline_context,
|
|
1003
|
+
),
|
|
1004
|
+
retry,
|
|
1005
|
+
item_index=index, # map item index
|
|
1006
|
+
)
|
|
1007
|
+
else:
|
|
1008
|
+
result = await self._execute_best_of_candidate(
|
|
1009
|
+
input_files=files,
|
|
1010
|
+
prompt=prompt_str,
|
|
1011
|
+
candidate_index=candidate_index,
|
|
1012
|
+
operation_id=operation_id,
|
|
1013
|
+
config=best_of_config,
|
|
1014
|
+
mcp_servers=candidate_mcp_servers,
|
|
1015
|
+
skills=candidate_skills,
|
|
1016
|
+
integrations=candidate_integrations,
|
|
1017
|
+
system_prompt=system_prompt,
|
|
1018
|
+
schema=schema,
|
|
1019
|
+
schema_options=schema_options,
|
|
1020
|
+
timeout=timeout,
|
|
1021
|
+
parent_index=index,
|
|
1022
|
+
name=name,
|
|
1023
|
+
_pipeline_context=_pipeline_context,
|
|
1024
|
+
)
|
|
1025
|
+
# Call callback after candidate completes
|
|
1026
|
+
if best_of_config.on_candidate_complete:
|
|
1027
|
+
best_of_config.on_candidate_complete(index, candidate_index, result.status if result.status != "filtered" else "success")
|
|
1028
|
+
return result
|
|
1029
|
+
|
|
1030
|
+
candidates = list(await asyncio.gather(*[
|
|
1031
|
+
run_candidate(i) for i in range(n)
|
|
1032
|
+
]))
|
|
1033
|
+
|
|
1034
|
+
# Run judge (semaphore acquired inside _execute_best_of_judge)
|
|
1035
|
+
# Judge uses default retry (status === "error"), not custom retry_on
|
|
1036
|
+
if retry:
|
|
1037
|
+
judge_retry = RetryConfig(
|
|
1038
|
+
max_attempts=retry.max_attempts,
|
|
1039
|
+
backoff_ms=retry.backoff_ms,
|
|
1040
|
+
backoff_multiplier=retry.backoff_multiplier,
|
|
1041
|
+
retry_on=None,
|
|
1042
|
+
)
|
|
1043
|
+
judge = await execute_with_retry(
|
|
1044
|
+
lambda attempt: self._execute_best_of_judge(
|
|
1045
|
+
input_files=files,
|
|
1046
|
+
task_prompt=prompt_str,
|
|
1047
|
+
candidates=candidates,
|
|
1048
|
+
config=best_of_config,
|
|
1049
|
+
mcp_servers=judge_mcp_servers,
|
|
1050
|
+
skills=judge_skills,
|
|
1051
|
+
integrations=judge_integrations,
|
|
1052
|
+
timeout=timeout,
|
|
1053
|
+
system_prompt=system_prompt,
|
|
1054
|
+
schema=schema,
|
|
1055
|
+
operation_id=operation_id,
|
|
1056
|
+
parent_index=index,
|
|
1057
|
+
attempt=attempt,
|
|
1058
|
+
name=name,
|
|
1059
|
+
_pipeline_context=_pipeline_context,
|
|
1060
|
+
),
|
|
1061
|
+
judge_retry
|
|
1062
|
+
)
|
|
1063
|
+
else:
|
|
1064
|
+
judge = await self._execute_best_of_judge(
|
|
1065
|
+
input_files=files,
|
|
1066
|
+
task_prompt=prompt_str,
|
|
1067
|
+
candidates=candidates,
|
|
1068
|
+
config=best_of_config,
|
|
1069
|
+
mcp_servers=judge_mcp_servers,
|
|
1070
|
+
skills=judge_skills,
|
|
1071
|
+
integrations=judge_integrations,
|
|
1072
|
+
timeout=timeout,
|
|
1073
|
+
system_prompt=system_prompt,
|
|
1074
|
+
schema=schema,
|
|
1075
|
+
operation_id=operation_id,
|
|
1076
|
+
parent_index=index,
|
|
1077
|
+
name=name,
|
|
1078
|
+
_pipeline_context=_pipeline_context,
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
first_success = next((i for i, c in enumerate(candidates) if c.status == "success"), -1)
|
|
1082
|
+
winner_index = judge["winner"] if judge["winner"] is not None else (first_success if first_success >= 0 else 0)
|
|
1083
|
+
winner = candidates[winner_index] if winner_index < len(candidates) else candidates[0]
|
|
1084
|
+
|
|
1085
|
+
# Call judge callback with map item index
|
|
1086
|
+
if best_of_config.on_judge_complete:
|
|
1087
|
+
best_of_config.on_judge_complete(index, winner_index, judge.get("reasoning", ""))
|
|
1088
|
+
|
|
1089
|
+
judge_meta = JudgeMeta(
|
|
1090
|
+
operation_id=operation_id,
|
|
1091
|
+
operation="bestof-judge",
|
|
1092
|
+
tag=judge["tag"],
|
|
1093
|
+
sandbox_id=judge["sandbox_id"],
|
|
1094
|
+
swarm_name=self.config.tag,
|
|
1095
|
+
operation_name=name,
|
|
1096
|
+
candidate_count=n,
|
|
1097
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Return winner with bestOf info
|
|
1101
|
+
return SwarmResult(
|
|
1102
|
+
status=winner.status,
|
|
1103
|
+
data=winner.data,
|
|
1104
|
+
files=winner.files,
|
|
1105
|
+
meta=IndexedMeta(
|
|
1106
|
+
operation_id=operation_id,
|
|
1107
|
+
operation="map",
|
|
1108
|
+
tag=winner.meta.tag,
|
|
1109
|
+
sandbox_id=winner.meta.sandbox_id,
|
|
1110
|
+
swarm_name=self.config.tag,
|
|
1111
|
+
operation_name=name,
|
|
1112
|
+
item_index=index,
|
|
1113
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1114
|
+
),
|
|
1115
|
+
error=winner.error,
|
|
1116
|
+
raw_data=winner.raw_data,
|
|
1117
|
+
best_of=BestOfInfo(
|
|
1118
|
+
winner_index=winner_index,
|
|
1119
|
+
judge_reasoning=judge.get("reasoning", "Judge failed to provide reasoning"),
|
|
1120
|
+
judge_meta=judge_meta,
|
|
1121
|
+
candidates=candidates,
|
|
1122
|
+
),
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
async def _execute_map_item_with_verify(
|
|
1126
|
+
self,
|
|
1127
|
+
item: ItemInput,
|
|
1128
|
+
prompt: Prompt,
|
|
1129
|
+
index: int,
|
|
1130
|
+
operation_id: str,
|
|
1131
|
+
system_prompt: Optional[str],
|
|
1132
|
+
schema: Optional[SchemaType],
|
|
1133
|
+
schema_options: Optional[Dict[str, Any]],
|
|
1134
|
+
agent: Optional[AgentConfig],
|
|
1135
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1136
|
+
skills: Optional[List[str]],
|
|
1137
|
+
integrations: Optional[IntegrationsSetup],
|
|
1138
|
+
verify_config: VerifyConfig,
|
|
1139
|
+
timeout: int,
|
|
1140
|
+
retry: Optional[RetryConfig] = None,
|
|
1141
|
+
name: Optional[str] = None,
|
|
1142
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1143
|
+
) -> SwarmResult:
|
|
1144
|
+
"""Execute a single map item with verification."""
|
|
1145
|
+
files = self._get_files(item)
|
|
1146
|
+
base_tag = f"{self.config.tag}-map-{index}"
|
|
1147
|
+
|
|
1148
|
+
try:
|
|
1149
|
+
prompt_str = self._resolve_prompt(prompt, files, index)
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
return self._build_error_result(
|
|
1152
|
+
f"Prompt function threw: {e}",
|
|
1153
|
+
IndexedMeta(
|
|
1154
|
+
operation_id=operation_id, operation="map", tag=base_tag,
|
|
1155
|
+
sandbox_id="", swarm_name=self.config.tag, operation_name=name,
|
|
1156
|
+
item_index=index, **self._pipeline_context_to_meta(_pipeline_context)
|
|
1157
|
+
)
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
# Worker function that executes map item (tag_prefix managed by _run_with_verification)
|
|
1161
|
+
async def worker_fn(current_prompt: str, tag_prefix: str, attempt_index: Optional[int] = None) -> SwarmResult:
|
|
1162
|
+
verify_retry = attempt_index - 1 if attempt_index and attempt_index > 1 else None
|
|
1163
|
+
|
|
1164
|
+
# Build observability for JSONL
|
|
1165
|
+
observability = _filter_none({
|
|
1166
|
+
'swarm_name': self.config.tag,
|
|
1167
|
+
'operation_name': name,
|
|
1168
|
+
'operation_id': operation_id,
|
|
1169
|
+
'operation': 'map',
|
|
1170
|
+
'item_index': index,
|
|
1171
|
+
'role': 'worker',
|
|
1172
|
+
'verify_retry': verify_retry,
|
|
1173
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1176
|
+
async with self.semaphore:
|
|
1177
|
+
result = await self._execute(
|
|
1178
|
+
context=files,
|
|
1179
|
+
prompt=current_prompt,
|
|
1180
|
+
system_prompt=system_prompt,
|
|
1181
|
+
schema=schema,
|
|
1182
|
+
schema_options=schema_options,
|
|
1183
|
+
agent=agent,
|
|
1184
|
+
mcp_servers=mcp_servers,
|
|
1185
|
+
skills=skills,
|
|
1186
|
+
integrations=integrations,
|
|
1187
|
+
tag_prefix=tag_prefix,
|
|
1188
|
+
timeout=timeout,
|
|
1189
|
+
observability=observability,
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
meta = IndexedMeta(
|
|
1193
|
+
operation_id=operation_id,
|
|
1194
|
+
operation="map",
|
|
1195
|
+
tag=result["tag"],
|
|
1196
|
+
sandbox_id=result["sandbox_id"],
|
|
1197
|
+
swarm_name=self.config.tag,
|
|
1198
|
+
operation_name=name,
|
|
1199
|
+
verify_retry=verify_retry,
|
|
1200
|
+
item_index=index,
|
|
1201
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
return self._build_result(result, meta)
|
|
1205
|
+
|
|
1206
|
+
# Run with verification loop
|
|
1207
|
+
return await self._run_with_verification(
|
|
1208
|
+
worker_fn=worker_fn,
|
|
1209
|
+
original_prompt=prompt_str,
|
|
1210
|
+
input_files=files,
|
|
1211
|
+
verify_config=verify_config,
|
|
1212
|
+
mcp_servers=mcp_servers,
|
|
1213
|
+
skills=skills,
|
|
1214
|
+
integrations=integrations,
|
|
1215
|
+
timeout=timeout,
|
|
1216
|
+
system_prompt=system_prompt,
|
|
1217
|
+
schema=schema,
|
|
1218
|
+
operation_id=operation_id,
|
|
1219
|
+
base_tag=base_tag,
|
|
1220
|
+
retry=retry,
|
|
1221
|
+
item_index=index,
|
|
1222
|
+
name=name,
|
|
1223
|
+
_pipeline_context=_pipeline_context,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
# =========================================================================
|
|
1227
|
+
# PRIVATE: FILTER
|
|
1228
|
+
# =========================================================================
|
|
1229
|
+
|
|
1230
|
+
async def _execute_filter_item(
|
|
1231
|
+
self,
|
|
1232
|
+
item: ItemInput,
|
|
1233
|
+
prompt: str,
|
|
1234
|
+
index: int,
|
|
1235
|
+
operation_id: str,
|
|
1236
|
+
system_prompt: Optional[str],
|
|
1237
|
+
schema: SchemaType,
|
|
1238
|
+
schema_options: Optional[Dict[str, Any]],
|
|
1239
|
+
agent: Optional[AgentConfig],
|
|
1240
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1241
|
+
skills: Optional[List[str]],
|
|
1242
|
+
integrations: Optional[IntegrationsSetup],
|
|
1243
|
+
timeout: int,
|
|
1244
|
+
attempt: int = 1,
|
|
1245
|
+
name: Optional[str] = None,
|
|
1246
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1247
|
+
) -> SwarmResult:
|
|
1248
|
+
"""Execute a single filter item."""
|
|
1249
|
+
original_files = self._get_files(item)
|
|
1250
|
+
error_retry = attempt - 1 if attempt > 1 else None
|
|
1251
|
+
tag_prefix = (
|
|
1252
|
+
f"{self.config.tag}-filter-{index}-er{error_retry}"
|
|
1253
|
+
if error_retry
|
|
1254
|
+
else f"{self.config.tag}-filter-{index}"
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# Build observability for JSONL
|
|
1258
|
+
observability = _filter_none({
|
|
1259
|
+
'swarm_name': self.config.tag,
|
|
1260
|
+
'operation_name': name,
|
|
1261
|
+
'operation_id': operation_id,
|
|
1262
|
+
'operation': 'filter',
|
|
1263
|
+
'item_index': index,
|
|
1264
|
+
'role': 'worker',
|
|
1265
|
+
'error_retry': error_retry,
|
|
1266
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
async with self.semaphore:
|
|
1270
|
+
result = await self._execute(
|
|
1271
|
+
context=original_files,
|
|
1272
|
+
prompt=prompt,
|
|
1273
|
+
system_prompt=system_prompt,
|
|
1274
|
+
schema=schema,
|
|
1275
|
+
schema_options=schema_options,
|
|
1276
|
+
agent=agent,
|
|
1277
|
+
mcp_servers=mcp_servers,
|
|
1278
|
+
skills=skills,
|
|
1279
|
+
integrations=integrations,
|
|
1280
|
+
tag_prefix=tag_prefix,
|
|
1281
|
+
timeout=timeout,
|
|
1282
|
+
observability=observability,
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
meta = IndexedMeta(
|
|
1286
|
+
operation_id=operation_id,
|
|
1287
|
+
operation="filter",
|
|
1288
|
+
tag=result["tag"],
|
|
1289
|
+
sandbox_id=result["sandbox_id"],
|
|
1290
|
+
swarm_name=self.config.tag,
|
|
1291
|
+
operation_name=name,
|
|
1292
|
+
error_retry=error_retry,
|
|
1293
|
+
item_index=index,
|
|
1294
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
# Filter passes through ORIGINAL files, not output
|
|
1298
|
+
return self._build_result(result, meta, files_override=original_files)
|
|
1299
|
+
|
|
1300
|
+
async def _execute_filter_item_with_verify(
|
|
1301
|
+
self,
|
|
1302
|
+
item: ItemInput,
|
|
1303
|
+
prompt: str,
|
|
1304
|
+
index: int,
|
|
1305
|
+
operation_id: str,
|
|
1306
|
+
system_prompt: Optional[str],
|
|
1307
|
+
schema: SchemaType,
|
|
1308
|
+
schema_options: Optional[Dict[str, Any]],
|
|
1309
|
+
agent: Optional[AgentConfig],
|
|
1310
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1311
|
+
skills: Optional[List[str]],
|
|
1312
|
+
integrations: Optional[IntegrationsSetup],
|
|
1313
|
+
verify_config: VerifyConfig,
|
|
1314
|
+
timeout: int,
|
|
1315
|
+
retry: Optional[RetryConfig] = None,
|
|
1316
|
+
name: Optional[str] = None,
|
|
1317
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1318
|
+
) -> SwarmResult:
|
|
1319
|
+
"""Execute a single filter item with verification."""
|
|
1320
|
+
original_files = self._get_files(item)
|
|
1321
|
+
base_tag = f"{self.config.tag}-filter-{index}"
|
|
1322
|
+
|
|
1323
|
+
# Worker function that executes filter item (tag_prefix managed by _run_with_verification)
|
|
1324
|
+
async def worker_fn(current_prompt: str, tag_prefix: str, attempt_index: Optional[int] = None) -> SwarmResult:
|
|
1325
|
+
verify_retry = attempt_index - 1 if attempt_index and attempt_index > 1 else None
|
|
1326
|
+
|
|
1327
|
+
# Build observability for JSONL
|
|
1328
|
+
observability = _filter_none({
|
|
1329
|
+
'swarm_name': self.config.tag,
|
|
1330
|
+
'operation_name': name,
|
|
1331
|
+
'operation_id': operation_id,
|
|
1332
|
+
'operation': 'filter',
|
|
1333
|
+
'item_index': index,
|
|
1334
|
+
'role': 'worker',
|
|
1335
|
+
'verify_retry': verify_retry,
|
|
1336
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
async with self.semaphore:
|
|
1340
|
+
result = await self._execute(
|
|
1341
|
+
context=original_files,
|
|
1342
|
+
prompt=current_prompt,
|
|
1343
|
+
system_prompt=system_prompt,
|
|
1344
|
+
schema=schema,
|
|
1345
|
+
schema_options=schema_options,
|
|
1346
|
+
agent=agent,
|
|
1347
|
+
mcp_servers=mcp_servers,
|
|
1348
|
+
skills=skills,
|
|
1349
|
+
integrations=integrations,
|
|
1350
|
+
tag_prefix=tag_prefix,
|
|
1351
|
+
timeout=timeout,
|
|
1352
|
+
observability=observability,
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
meta = IndexedMeta(
|
|
1356
|
+
operation_id=operation_id,
|
|
1357
|
+
operation="filter",
|
|
1358
|
+
tag=result["tag"],
|
|
1359
|
+
sandbox_id=result["sandbox_id"],
|
|
1360
|
+
swarm_name=self.config.tag,
|
|
1361
|
+
operation_name=name,
|
|
1362
|
+
verify_retry=verify_retry,
|
|
1363
|
+
item_index=index,
|
|
1364
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
# Filter passes through ORIGINAL files, not output
|
|
1368
|
+
return self._build_result(result, meta, files_override=original_files)
|
|
1369
|
+
|
|
1370
|
+
# Run with verification loop
|
|
1371
|
+
return await self._run_with_verification(
|
|
1372
|
+
worker_fn=worker_fn,
|
|
1373
|
+
original_prompt=prompt,
|
|
1374
|
+
input_files=original_files,
|
|
1375
|
+
verify_config=verify_config,
|
|
1376
|
+
mcp_servers=mcp_servers,
|
|
1377
|
+
skills=skills,
|
|
1378
|
+
integrations=integrations,
|
|
1379
|
+
timeout=timeout,
|
|
1380
|
+
system_prompt=system_prompt,
|
|
1381
|
+
schema=schema,
|
|
1382
|
+
operation_id=operation_id,
|
|
1383
|
+
base_tag=base_tag,
|
|
1384
|
+
retry=retry,
|
|
1385
|
+
item_index=index,
|
|
1386
|
+
name=name,
|
|
1387
|
+
_pipeline_context=_pipeline_context,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
# =========================================================================
|
|
1391
|
+
# PRIVATE: VERIFY
|
|
1392
|
+
# =========================================================================
|
|
1393
|
+
|
|
1394
|
+
async def _run_with_verification(
|
|
1395
|
+
self,
|
|
1396
|
+
worker_fn: Callable[[str, str, Optional[int]], Any], # async function(prompt, tag_prefix, attempt_index) -> result
|
|
1397
|
+
original_prompt: str,
|
|
1398
|
+
input_files: FileMap,
|
|
1399
|
+
verify_config: VerifyConfig,
|
|
1400
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1401
|
+
skills: Optional[List[str]],
|
|
1402
|
+
integrations: Optional[IntegrationsSetup],
|
|
1403
|
+
timeout: int,
|
|
1404
|
+
system_prompt: Optional[str],
|
|
1405
|
+
schema: Optional[SchemaType],
|
|
1406
|
+
operation_id: str,
|
|
1407
|
+
base_tag: str,
|
|
1408
|
+
retry: Optional[RetryConfig] = None,
|
|
1409
|
+
item_index: int = 0,
|
|
1410
|
+
name: Optional[str] = None,
|
|
1411
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1412
|
+
) -> Any:
|
|
1413
|
+
"""Shared verification loop for map, filter, and reduce.
|
|
1414
|
+
|
|
1415
|
+
Runs worker function, verifies output, retries with feedback if needed.
|
|
1416
|
+
|
|
1417
|
+
Args:
|
|
1418
|
+
worker_fn: Async function that executes the worker with (prompt, tag_prefix, attempt_index)
|
|
1419
|
+
original_prompt: The original user prompt
|
|
1420
|
+
input_files: Input files for the worker
|
|
1421
|
+
verify_config: Verification configuration
|
|
1422
|
+
mcp_servers: MCP servers for verifier (resolved from operation or swarm)
|
|
1423
|
+
timeout: Timeout in ms
|
|
1424
|
+
system_prompt: Optional system prompt
|
|
1425
|
+
schema: Optional schema
|
|
1426
|
+
operation_id: Operation ID for metadata
|
|
1427
|
+
base_tag: Base tag for worker/verifier
|
|
1428
|
+
retry: Optional retry config for verifier error retry
|
|
1429
|
+
item_index: Item index for callbacks (default: 0 for reduce)
|
|
1430
|
+
name: Operation name for observability
|
|
1431
|
+
_pipeline_context: Pipeline context for observability
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
Result with verify info attached
|
|
1435
|
+
"""
|
|
1436
|
+
# Resolve verifier MCP servers, skills, and integrations
|
|
1437
|
+
verifier_mcp_servers = verify_config.verifier_mcp_servers if verify_config.verifier_mcp_servers is not None else mcp_servers
|
|
1438
|
+
verifier_skills = verify_config.verifier_skills if verify_config.verifier_skills is not None else skills
|
|
1439
|
+
verifier_integrations = verify_config.verifier_integrations if verify_config.verifier_integrations is not None else integrations
|
|
1440
|
+
max_attempts = verify_config.max_attempts
|
|
1441
|
+
|
|
1442
|
+
current_prompt = original_prompt
|
|
1443
|
+
last_result = None
|
|
1444
|
+
verify_attempts = 0
|
|
1445
|
+
|
|
1446
|
+
while verify_attempts < max_attempts:
|
|
1447
|
+
verify_attempts += 1
|
|
1448
|
+
|
|
1449
|
+
# Build worker tag: base_tag, base_tag-vr1, base_tag-vr2, etc. (vr = verify retry)
|
|
1450
|
+
worker_tag = f"{base_tag}-vr{verify_attempts - 1}" if verify_attempts > 1 else base_tag
|
|
1451
|
+
|
|
1452
|
+
# Run worker (with error retry if configured)
|
|
1453
|
+
# Worker keeps retry_on (user-specified condition) and gets -er{n} tag suffix for error retries
|
|
1454
|
+
if retry:
|
|
1455
|
+
async def worker_with_retry(retry_attempt: int = 1):
|
|
1456
|
+
tag = f"{worker_tag}-er{retry_attempt - 1}" if retry_attempt > 1 else worker_tag
|
|
1457
|
+
return await worker_fn(current_prompt, tag, verify_attempts)
|
|
1458
|
+
worker_result = await execute_with_retry(worker_with_retry, retry)
|
|
1459
|
+
else:
|
|
1460
|
+
worker_result = await worker_fn(current_prompt, worker_tag, verify_attempts)
|
|
1461
|
+
|
|
1462
|
+
# If worker failed even after retries, return immediately
|
|
1463
|
+
if worker_result.status == "error":
|
|
1464
|
+
# Call worker callback with error status
|
|
1465
|
+
if verify_config.on_worker_complete:
|
|
1466
|
+
verify_config.on_worker_complete(item_index, verify_attempts, "error")
|
|
1467
|
+
return worker_result
|
|
1468
|
+
|
|
1469
|
+
# Call worker callback with success status
|
|
1470
|
+
if verify_config.on_worker_complete:
|
|
1471
|
+
verify_config.on_worker_complete(item_index, verify_attempts, "success")
|
|
1472
|
+
|
|
1473
|
+
last_result = worker_result
|
|
1474
|
+
|
|
1475
|
+
# Run verification (verifier tag = worker_tag-verify, with error retry like judge)
|
|
1476
|
+
if retry:
|
|
1477
|
+
async def verify_with_retry(retry_attempt: int = 1):
|
|
1478
|
+
return await self._execute_verify(
|
|
1479
|
+
input_files=input_files,
|
|
1480
|
+
output_files=worker_result.files,
|
|
1481
|
+
task_prompt=current_prompt,
|
|
1482
|
+
config=verify_config,
|
|
1483
|
+
mcp_servers=verifier_mcp_servers,
|
|
1484
|
+
skills=verifier_skills,
|
|
1485
|
+
integrations=verifier_integrations,
|
|
1486
|
+
timeout=timeout,
|
|
1487
|
+
system_prompt=system_prompt,
|
|
1488
|
+
schema=schema,
|
|
1489
|
+
operation_id=operation_id,
|
|
1490
|
+
worker_tag=worker_tag,
|
|
1491
|
+
retry_attempt=retry_attempt,
|
|
1492
|
+
name=name,
|
|
1493
|
+
_pipeline_context=_pipeline_context,
|
|
1494
|
+
)
|
|
1495
|
+
# Use retry but ignore custom retry_on (like judge)
|
|
1496
|
+
retry_config = RetryConfig(
|
|
1497
|
+
max_attempts=retry.max_attempts,
|
|
1498
|
+
backoff_ms=retry.backoff_ms,
|
|
1499
|
+
backoff_multiplier=retry.backoff_multiplier,
|
|
1500
|
+
)
|
|
1501
|
+
verification = await execute_with_retry(verify_with_retry, retry_config)
|
|
1502
|
+
else:
|
|
1503
|
+
verification = await self._execute_verify(
|
|
1504
|
+
input_files=input_files,
|
|
1505
|
+
output_files=worker_result.files,
|
|
1506
|
+
task_prompt=current_prompt,
|
|
1507
|
+
config=verify_config,
|
|
1508
|
+
mcp_servers=verifier_mcp_servers,
|
|
1509
|
+
skills=verifier_skills,
|
|
1510
|
+
integrations=verifier_integrations,
|
|
1511
|
+
timeout=timeout,
|
|
1512
|
+
system_prompt=system_prompt,
|
|
1513
|
+
schema=schema,
|
|
1514
|
+
operation_id=operation_id,
|
|
1515
|
+
worker_tag=worker_tag,
|
|
1516
|
+
name=name,
|
|
1517
|
+
_pipeline_context=_pipeline_context,
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
# Call verifier callback
|
|
1521
|
+
if verify_config.on_verifier_complete:
|
|
1522
|
+
verify_config.on_verifier_complete(
|
|
1523
|
+
item_index,
|
|
1524
|
+
verify_attempts,
|
|
1525
|
+
bool(verification.get("passed")),
|
|
1526
|
+
verification.get("feedback"),
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
# Build verify meta
|
|
1530
|
+
verify_meta = VerifyMeta(
|
|
1531
|
+
operation_id=operation_id,
|
|
1532
|
+
operation="verify",
|
|
1533
|
+
tag=verification["tag"],
|
|
1534
|
+
sandbox_id=verification["sandbox_id"],
|
|
1535
|
+
swarm_name=self.config.tag,
|
|
1536
|
+
operation_name=name,
|
|
1537
|
+
attempts=verify_attempts,
|
|
1538
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
# If verification passed, return result with verify info
|
|
1542
|
+
if verification.get("passed"):
|
|
1543
|
+
# Create a new result with verify info attached
|
|
1544
|
+
return self._attach_verify_info(
|
|
1545
|
+
worker_result,
|
|
1546
|
+
VerifyInfo(
|
|
1547
|
+
passed=True,
|
|
1548
|
+
reasoning=verification.get("reasoning", ""),
|
|
1549
|
+
verify_meta=verify_meta,
|
|
1550
|
+
attempts=verify_attempts,
|
|
1551
|
+
)
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
# If verification failed and we have attempts left, rebuild prompt with feedback
|
|
1555
|
+
if verify_attempts < max_attempts:
|
|
1556
|
+
feedback = verification.get("feedback") or verification.get("reasoning") or "Output did not meet criteria"
|
|
1557
|
+
current_prompt = self._build_retry_prompt_with_feedback(original_prompt, feedback)
|
|
1558
|
+
|
|
1559
|
+
# Max retries exceeded - return last result with error status and verify info
|
|
1560
|
+
# Use last worker tag for consistency
|
|
1561
|
+
last_worker_tag = f"{base_tag}-vr{verify_attempts - 1}" if verify_attempts > 1 else base_tag
|
|
1562
|
+
verify_meta = VerifyMeta(
|
|
1563
|
+
operation_id=operation_id,
|
|
1564
|
+
operation="verify",
|
|
1565
|
+
tag=f"{last_worker_tag}-verifier",
|
|
1566
|
+
sandbox_id="",
|
|
1567
|
+
swarm_name=self.config.tag,
|
|
1568
|
+
operation_name=name,
|
|
1569
|
+
attempts=verify_attempts,
|
|
1570
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
return self._attach_verify_info(
|
|
1574
|
+
last_result,
|
|
1575
|
+
VerifyInfo(
|
|
1576
|
+
passed=False,
|
|
1577
|
+
reasoning="Max verification retries exceeded",
|
|
1578
|
+
verify_meta=verify_meta,
|
|
1579
|
+
attempts=verify_attempts,
|
|
1580
|
+
),
|
|
1581
|
+
force_error=True
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
async def _execute_verify(
|
|
1585
|
+
self,
|
|
1586
|
+
input_files: FileMap,
|
|
1587
|
+
output_files: FileMap,
|
|
1588
|
+
task_prompt: str,
|
|
1589
|
+
config: VerifyConfig,
|
|
1590
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1591
|
+
skills: Optional[List[str]],
|
|
1592
|
+
integrations: Optional[IntegrationsSetup],
|
|
1593
|
+
timeout: int,
|
|
1594
|
+
system_prompt: Optional[str],
|
|
1595
|
+
schema: Optional[SchemaType],
|
|
1596
|
+
operation_id: str,
|
|
1597
|
+
worker_tag: str,
|
|
1598
|
+
retry_attempt: int = 1,
|
|
1599
|
+
name: Optional[str] = None,
|
|
1600
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1601
|
+
) -> Dict[str, Any]:
|
|
1602
|
+
"""Execute verifier to check if output meets criteria."""
|
|
1603
|
+
# Verifier tag = worker_tag-verifier, with -er{n} suffix for error retries
|
|
1604
|
+
error_retry = retry_attempt - 1 if retry_attempt > 1 else None
|
|
1605
|
+
tag_prefix = (
|
|
1606
|
+
f"{worker_tag}-verifier-er{error_retry}"
|
|
1607
|
+
if error_retry
|
|
1608
|
+
else f"{worker_tag}-verifier"
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
# Build verify context
|
|
1612
|
+
context = self._build_verify_context(
|
|
1613
|
+
input_files=input_files,
|
|
1614
|
+
task_prompt=task_prompt,
|
|
1615
|
+
output_files=output_files,
|
|
1616
|
+
system_prompt=system_prompt,
|
|
1617
|
+
schema=schema,
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
# Build verify system prompt
|
|
1621
|
+
file_tree = build_file_tree(context)
|
|
1622
|
+
verify_system_prompt = apply_template(VERIFY_PROMPT, {
|
|
1623
|
+
"criteria": config.criteria,
|
|
1624
|
+
"fileTree": file_tree,
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
# Verify schema (always JSON Schema dict for simplicity)
|
|
1628
|
+
verify_schema = {
|
|
1629
|
+
"type": "object",
|
|
1630
|
+
"properties": {
|
|
1631
|
+
"passed": {"type": "boolean"},
|
|
1632
|
+
"reasoning": {"type": "string"},
|
|
1633
|
+
"feedback": {"type": "string"},
|
|
1634
|
+
},
|
|
1635
|
+
"required": ["passed", "reasoning"],
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
# Build observability for JSONL
|
|
1639
|
+
observability = _filter_none({
|
|
1640
|
+
'swarm_name': self.config.tag,
|
|
1641
|
+
'operation_name': name,
|
|
1642
|
+
'operation_id': operation_id,
|
|
1643
|
+
'operation': 'verify',
|
|
1644
|
+
'role': 'verifier',
|
|
1645
|
+
'error_retry': error_retry,
|
|
1646
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
async with self.semaphore:
|
|
1650
|
+
result = await self._execute(
|
|
1651
|
+
context=context,
|
|
1652
|
+
prompt=VERIFY_USER_PROMPT,
|
|
1653
|
+
system_prompt=verify_system_prompt,
|
|
1654
|
+
schema=verify_schema,
|
|
1655
|
+
schema_options=None,
|
|
1656
|
+
agent=config.verifier_agent,
|
|
1657
|
+
mcp_servers=mcp_servers,
|
|
1658
|
+
skills=skills,
|
|
1659
|
+
integrations=integrations,
|
|
1660
|
+
tag_prefix=tag_prefix,
|
|
1661
|
+
timeout=timeout,
|
|
1662
|
+
observability=observability,
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
passed = None
|
|
1666
|
+
reasoning = "Verification completed"
|
|
1667
|
+
feedback = None
|
|
1668
|
+
|
|
1669
|
+
if result.get("data") and not result.get("error"):
|
|
1670
|
+
data = result["data"]
|
|
1671
|
+
if isinstance(data, dict):
|
|
1672
|
+
passed = data.get("passed")
|
|
1673
|
+
reasoning = data.get("reasoning", reasoning)
|
|
1674
|
+
feedback = data.get("feedback")
|
|
1675
|
+
elif result.get("raw_data"):
|
|
1676
|
+
# Validation failed but we have raw data - try to extract
|
|
1677
|
+
try:
|
|
1678
|
+
raw = json.loads(result["raw_data"])
|
|
1679
|
+
passed = bool(raw.get("passed"))
|
|
1680
|
+
reasoning = raw.get("reasoning", reasoning)
|
|
1681
|
+
feedback = raw.get("feedback")
|
|
1682
|
+
except Exception:
|
|
1683
|
+
warnings.warn(
|
|
1684
|
+
f"Verify validation failed: {result.get('error')}",
|
|
1685
|
+
stacklevel=2
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
return {
|
|
1689
|
+
"status": "success" if passed is not None else "error",
|
|
1690
|
+
"passed": passed,
|
|
1691
|
+
"reasoning": reasoning,
|
|
1692
|
+
"feedback": feedback,
|
|
1693
|
+
"tag": result["tag"],
|
|
1694
|
+
"sandbox_id": result["sandbox_id"],
|
|
1695
|
+
"error": None if passed is not None else "Verifier failed to produce valid decision",
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
def _build_verify_context(
|
|
1699
|
+
self,
|
|
1700
|
+
input_files: FileMap,
|
|
1701
|
+
task_prompt: str,
|
|
1702
|
+
output_files: FileMap,
|
|
1703
|
+
system_prompt: Optional[str],
|
|
1704
|
+
schema: Optional[SchemaType],
|
|
1705
|
+
) -> FileMap:
|
|
1706
|
+
"""Build verify context containing worker task info and output to verify."""
|
|
1707
|
+
# Start with shared worker_task structure
|
|
1708
|
+
context = self._build_evaluator_context(
|
|
1709
|
+
input_files=input_files,
|
|
1710
|
+
task_prompt=task_prompt,
|
|
1711
|
+
system_prompt=system_prompt,
|
|
1712
|
+
schema=schema,
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
# Add output files to verify
|
|
1716
|
+
for name, content in output_files.items():
|
|
1717
|
+
context[f"worker_output/{name}"] = content
|
|
1718
|
+
|
|
1719
|
+
return context
|
|
1720
|
+
|
|
1721
|
+
def _build_evaluator_context(
|
|
1722
|
+
self,
|
|
1723
|
+
input_files: FileMap,
|
|
1724
|
+
task_prompt: str,
|
|
1725
|
+
system_prompt: Optional[str],
|
|
1726
|
+
schema: Optional[SchemaType],
|
|
1727
|
+
) -> FileMap:
|
|
1728
|
+
"""Build evaluator context (shared by judge and verify).
|
|
1729
|
+
|
|
1730
|
+
Creates worker_task/ structure with input files, prompts, schema.
|
|
1731
|
+
"""
|
|
1732
|
+
context: FileMap = {}
|
|
1733
|
+
|
|
1734
|
+
if system_prompt:
|
|
1735
|
+
context["worker_task/system_prompt.txt"] = system_prompt
|
|
1736
|
+
context["worker_task/user_prompt.txt"] = task_prompt
|
|
1737
|
+
|
|
1738
|
+
json_schema = to_json_schema(schema)
|
|
1739
|
+
if json_schema:
|
|
1740
|
+
context["worker_task/schema.json"] = json.dumps(json_schema, indent=2)
|
|
1741
|
+
|
|
1742
|
+
for name, content in input_files.items():
|
|
1743
|
+
context[f"worker_task/input/{name}"] = content
|
|
1744
|
+
|
|
1745
|
+
return context
|
|
1746
|
+
|
|
1747
|
+
@staticmethod
|
|
1748
|
+
def _build_retry_prompt_with_feedback(original_prompt: str, feedback: str) -> str:
|
|
1749
|
+
"""Build a retry prompt with verifier feedback."""
|
|
1750
|
+
return apply_template(RETRY_FEEDBACK_PROMPT, {
|
|
1751
|
+
"originalPrompt": original_prompt,
|
|
1752
|
+
"feedback": feedback,
|
|
1753
|
+
})
|
|
1754
|
+
|
|
1755
|
+
def _attach_verify_info(
|
|
1756
|
+
self,
|
|
1757
|
+
result: Any,
|
|
1758
|
+
verify_info: VerifyInfo,
|
|
1759
|
+
force_error: bool = False,
|
|
1760
|
+
) -> Any:
|
|
1761
|
+
"""Attach verify info to a result, creating a new result object."""
|
|
1762
|
+
if isinstance(result, SwarmResult):
|
|
1763
|
+
return SwarmResult(
|
|
1764
|
+
status="error" if force_error else result.status,
|
|
1765
|
+
data=result.data,
|
|
1766
|
+
files=result.files,
|
|
1767
|
+
meta=result.meta,
|
|
1768
|
+
error=result.error,
|
|
1769
|
+
raw_data=result.raw_data,
|
|
1770
|
+
best_of=result.best_of,
|
|
1771
|
+
verify=verify_info,
|
|
1772
|
+
)
|
|
1773
|
+
elif isinstance(result, ReduceResult):
|
|
1774
|
+
return ReduceResult(
|
|
1775
|
+
status="error" if force_error else result.status,
|
|
1776
|
+
data=result.data,
|
|
1777
|
+
files=result.files,
|
|
1778
|
+
meta=result.meta,
|
|
1779
|
+
error=result.error,
|
|
1780
|
+
raw_data=result.raw_data,
|
|
1781
|
+
verify=verify_info,
|
|
1782
|
+
)
|
|
1783
|
+
else:
|
|
1784
|
+
# Fallback - just set verify attribute
|
|
1785
|
+
result.verify = verify_info
|
|
1786
|
+
if force_error:
|
|
1787
|
+
result.status = "error"
|
|
1788
|
+
return result
|
|
1789
|
+
|
|
1790
|
+
# =========================================================================
|
|
1791
|
+
# PRIVATE: BESTOF
|
|
1792
|
+
# =========================================================================
|
|
1793
|
+
|
|
1794
|
+
async def _execute_best_of_candidate(
|
|
1795
|
+
self,
|
|
1796
|
+
input_files: FileMap,
|
|
1797
|
+
prompt: str,
|
|
1798
|
+
candidate_index: int,
|
|
1799
|
+
operation_id: str,
|
|
1800
|
+
config: BestOfConfig,
|
|
1801
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1802
|
+
skills: Optional[List[str]],
|
|
1803
|
+
integrations: Optional[IntegrationsSetup],
|
|
1804
|
+
system_prompt: Optional[str],
|
|
1805
|
+
schema: Optional[SchemaType],
|
|
1806
|
+
schema_options: Optional[Dict[str, Any]],
|
|
1807
|
+
timeout: int,
|
|
1808
|
+
parent_index: Optional[int] = None,
|
|
1809
|
+
attempt: int = 1,
|
|
1810
|
+
name: Optional[str] = None,
|
|
1811
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1812
|
+
) -> SwarmResult:
|
|
1813
|
+
"""Execute a single bestOf candidate."""
|
|
1814
|
+
base_tag = (
|
|
1815
|
+
f"{self.config.tag}-map-{parent_index}-bestof-cand-{candidate_index}"
|
|
1816
|
+
if parent_index is not None
|
|
1817
|
+
else f"{self.config.tag}-bestof-cand-{candidate_index}"
|
|
1818
|
+
)
|
|
1819
|
+
error_retry = attempt - 1 if attempt > 1 else None
|
|
1820
|
+
tag_prefix = f"{base_tag}-er{error_retry}" if error_retry else base_tag
|
|
1821
|
+
|
|
1822
|
+
# Get agent override for this candidate
|
|
1823
|
+
candidate_agent = config.task_agents[candidate_index] if config.task_agents and candidate_index < len(config.task_agents) else None
|
|
1824
|
+
|
|
1825
|
+
# Build observability for JSONL (matches TS: operation="map" with role="candidate")
|
|
1826
|
+
observability = _filter_none({
|
|
1827
|
+
'swarm_name': self.config.tag,
|
|
1828
|
+
'operation_id': operation_id,
|
|
1829
|
+
'operation': 'map',
|
|
1830
|
+
'item_index': parent_index,
|
|
1831
|
+
'role': 'candidate',
|
|
1832
|
+
'candidate_index': candidate_index,
|
|
1833
|
+
'error_retry': error_retry,
|
|
1834
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1837
|
+
# Acquire semaphore here (inside retry loop) so it's released during backoff
|
|
1838
|
+
async with self.semaphore:
|
|
1839
|
+
result = await self._execute(
|
|
1840
|
+
context=input_files,
|
|
1841
|
+
prompt=prompt,
|
|
1842
|
+
system_prompt=system_prompt,
|
|
1843
|
+
schema=schema,
|
|
1844
|
+
schema_options=schema_options,
|
|
1845
|
+
agent=candidate_agent,
|
|
1846
|
+
mcp_servers=mcp_servers,
|
|
1847
|
+
skills=skills,
|
|
1848
|
+
integrations=integrations,
|
|
1849
|
+
tag_prefix=tag_prefix,
|
|
1850
|
+
timeout=timeout,
|
|
1851
|
+
observability=observability,
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
meta = IndexedMeta(
|
|
1855
|
+
operation_id=operation_id,
|
|
1856
|
+
operation="bestof-cand",
|
|
1857
|
+
tag=result["tag"],
|
|
1858
|
+
sandbox_id=result["sandbox_id"],
|
|
1859
|
+
swarm_name=self.config.tag,
|
|
1860
|
+
operation_name=name,
|
|
1861
|
+
error_retry=error_retry,
|
|
1862
|
+
candidate_index=candidate_index,
|
|
1863
|
+
item_index=candidate_index, # candidate index for this operation
|
|
1864
|
+
**self._pipeline_context_to_meta(_pipeline_context),
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
return self._build_result(result, meta)
|
|
1868
|
+
|
|
1869
|
+
async def _execute_best_of_judge(
|
|
1870
|
+
self,
|
|
1871
|
+
input_files: FileMap,
|
|
1872
|
+
task_prompt: str,
|
|
1873
|
+
candidates: List[SwarmResult],
|
|
1874
|
+
config: BestOfConfig,
|
|
1875
|
+
mcp_servers: Optional[Dict[str, Any]],
|
|
1876
|
+
skills: Optional[List[str]],
|
|
1877
|
+
integrations: Optional[IntegrationsSetup],
|
|
1878
|
+
timeout: int,
|
|
1879
|
+
system_prompt: Optional[str],
|
|
1880
|
+
schema: Optional[SchemaType],
|
|
1881
|
+
operation_id: str,
|
|
1882
|
+
parent_index: Optional[int] = None,
|
|
1883
|
+
attempt: int = 1,
|
|
1884
|
+
name: Optional[str] = None,
|
|
1885
|
+
_pipeline_context: Optional[PipelineContext] = None,
|
|
1886
|
+
) -> Dict[str, Any]:
|
|
1887
|
+
"""Execute bestOf judge.
|
|
1888
|
+
|
|
1889
|
+
Returns a dict with status field for retry compatibility.
|
|
1890
|
+
"""
|
|
1891
|
+
base_tag = (
|
|
1892
|
+
f"{self.config.tag}-map-{parent_index}-bestof-judge"
|
|
1893
|
+
if parent_index is not None
|
|
1894
|
+
else f"{self.config.tag}-bestof-judge"
|
|
1895
|
+
)
|
|
1896
|
+
error_retry = attempt - 1 if attempt > 1 else None
|
|
1897
|
+
tag_prefix = f"{base_tag}-er{error_retry}" if error_retry else base_tag
|
|
1898
|
+
|
|
1899
|
+
# Build judge context
|
|
1900
|
+
context = self._build_judge_context(
|
|
1901
|
+
input_files=input_files,
|
|
1902
|
+
task_prompt=task_prompt,
|
|
1903
|
+
candidates=candidates,
|
|
1904
|
+
system_prompt=system_prompt,
|
|
1905
|
+
schema=schema,
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
# Build judge system prompt
|
|
1909
|
+
file_tree = build_file_tree(context)
|
|
1910
|
+
judge_system_prompt = apply_template(JUDGE_PROMPT, {
|
|
1911
|
+
"candidateCount": str(len(candidates)),
|
|
1912
|
+
"criteria": config.judge_criteria,
|
|
1913
|
+
"fileTree": file_tree,
|
|
1914
|
+
})
|
|
1915
|
+
|
|
1916
|
+
# Judge schema (always JSON Schema dict for simplicity)
|
|
1917
|
+
judge_schema = {
|
|
1918
|
+
"type": "object",
|
|
1919
|
+
"properties": {
|
|
1920
|
+
"winner": {"type": "integer", "minimum": 0, "maximum": len(candidates) - 1},
|
|
1921
|
+
"reasoning": {"type": "string"},
|
|
1922
|
+
},
|
|
1923
|
+
"required": ["winner", "reasoning"],
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
# Build observability for JSONL (matches TS: operation="map" with role="judge")
|
|
1927
|
+
observability = _filter_none({
|
|
1928
|
+
'swarm_name': self.config.tag,
|
|
1929
|
+
'operation_id': operation_id,
|
|
1930
|
+
'operation': 'map',
|
|
1931
|
+
'item_index': parent_index,
|
|
1932
|
+
'role': 'judge',
|
|
1933
|
+
'error_retry': error_retry,
|
|
1934
|
+
**self._pipeline_context_to_observability(_pipeline_context),
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
# Acquire semaphore here (inside retry loop) so it's released during backoff
|
|
1938
|
+
async with self.semaphore:
|
|
1939
|
+
result = await self._execute(
|
|
1940
|
+
context=context,
|
|
1941
|
+
prompt=JUDGE_USER_PROMPT,
|
|
1942
|
+
system_prompt=judge_system_prompt,
|
|
1943
|
+
schema=judge_schema,
|
|
1944
|
+
schema_options=None,
|
|
1945
|
+
agent=config.judge_agent,
|
|
1946
|
+
mcp_servers=mcp_servers,
|
|
1947
|
+
skills=skills,
|
|
1948
|
+
integrations=integrations,
|
|
1949
|
+
tag_prefix=tag_prefix,
|
|
1950
|
+
timeout=timeout,
|
|
1951
|
+
observability=observability,
|
|
1952
|
+
)
|
|
1953
|
+
|
|
1954
|
+
winner = None
|
|
1955
|
+
reasoning = "Judge failed to provide reasoning"
|
|
1956
|
+
|
|
1957
|
+
if result.get("data") and not result.get("error"):
|
|
1958
|
+
data = result["data"]
|
|
1959
|
+
if isinstance(data, dict):
|
|
1960
|
+
winner = data.get("winner")
|
|
1961
|
+
reasoning = data.get("reasoning", reasoning)
|
|
1962
|
+
elif result.get("raw_data"):
|
|
1963
|
+
# Validation failed but we have raw data - extract reasoning and default winner to 0
|
|
1964
|
+
try:
|
|
1965
|
+
raw = json.loads(result["raw_data"])
|
|
1966
|
+
warnings.warn(
|
|
1967
|
+
f"Judge returned invalid winner {raw.get('winner')}, defaulting to candidate 0",
|
|
1968
|
+
stacklevel=2
|
|
1969
|
+
)
|
|
1970
|
+
winner = 0
|
|
1971
|
+
reasoning = raw.get("reasoning", reasoning)
|
|
1972
|
+
except Exception:
|
|
1973
|
+
warnings.warn(
|
|
1974
|
+
f"Judge validation failed: {result.get('error')}",
|
|
1975
|
+
stacklevel=2
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
return {
|
|
1979
|
+
"status": "success" if winner is not None else "error",
|
|
1980
|
+
"winner": winner,
|
|
1981
|
+
"reasoning": reasoning,
|
|
1982
|
+
"tag": result["tag"],
|
|
1983
|
+
"sandbox_id": result["sandbox_id"],
|
|
1984
|
+
"error": None if winner is not None else "Judge failed to produce valid decision",
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
def _build_judge_context(
|
|
1988
|
+
self,
|
|
1989
|
+
input_files: FileMap,
|
|
1990
|
+
task_prompt: str,
|
|
1991
|
+
candidates: List[SwarmResult],
|
|
1992
|
+
system_prompt: Optional[str],
|
|
1993
|
+
schema: Optional[SchemaType],
|
|
1994
|
+
) -> FileMap:
|
|
1995
|
+
"""Build judge context containing worker task info and candidate outputs."""
|
|
1996
|
+
# Start with shared worker_task structure
|
|
1997
|
+
context = self._build_evaluator_context(
|
|
1998
|
+
input_files=input_files,
|
|
1999
|
+
task_prompt=task_prompt,
|
|
2000
|
+
system_prompt=system_prompt,
|
|
2001
|
+
schema=schema,
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
# Add candidate outputs
|
|
2005
|
+
for i, c in enumerate(candidates):
|
|
2006
|
+
if c.status == "error":
|
|
2007
|
+
context[f"candidate_{i}/_failed.txt"] = f"STATUS: FAILED\n\nError: {c.error or 'Unknown error'}"
|
|
2008
|
+
for name, content in c.files.items():
|
|
2009
|
+
context[f"candidate_{i}/{name}"] = content
|
|
2010
|
+
|
|
2011
|
+
return context
|
|
2012
|
+
|
|
2013
|
+
# =========================================================================
|
|
2014
|
+
# PRIVATE: UTILITIES
|
|
2015
|
+
# =========================================================================
|
|
2016
|
+
|
|
2017
|
+
def _generate_operation_id(self) -> str:
|
|
2018
|
+
"""Generate a unique operation ID."""
|
|
2019
|
+
return secrets.token_hex(8)
|
|
2020
|
+
|
|
2021
|
+
def _pipeline_context_to_meta(self, ctx: Optional[PipelineContext]) -> Dict[str, Any]:
|
|
2022
|
+
"""Extract pipeline context fields for meta objects."""
|
|
2023
|
+
if not ctx:
|
|
2024
|
+
return {}
|
|
2025
|
+
return _filter_none({
|
|
2026
|
+
'pipeline_run_id': ctx.pipeline_run_id,
|
|
2027
|
+
'pipeline_step_index': ctx.pipeline_step_index,
|
|
2028
|
+
})
|
|
2029
|
+
|
|
2030
|
+
def _pipeline_context_to_observability(self, ctx: Optional[PipelineContext]) -> Dict[str, Any]:
|
|
2031
|
+
"""Extract pipeline context fields for observability dict."""
|
|
2032
|
+
if not ctx:
|
|
2033
|
+
return {}
|
|
2034
|
+
return _filter_none({
|
|
2035
|
+
'pipeline_run_id': ctx.pipeline_run_id,
|
|
2036
|
+
'pipeline_step_index': ctx.pipeline_step_index,
|
|
2037
|
+
})
|
|
2038
|
+
|
|
2039
|
+
def _get_files(self, item: ItemInput) -> FileMap:
|
|
2040
|
+
"""Extract files from an item (FileMap or SwarmResult)."""
|
|
2041
|
+
if is_swarm_result(item):
|
|
2042
|
+
files = dict(item.files)
|
|
2043
|
+
# Rename result.json → data.json for clarity when used as input
|
|
2044
|
+
if "result.json" in files:
|
|
2045
|
+
files["data.json"] = files.pop("result.json")
|
|
2046
|
+
return files
|
|
2047
|
+
return item
|
|
2048
|
+
|
|
2049
|
+
def _get_index(self, item: ItemInput, fallback: int) -> int:
|
|
2050
|
+
"""Get index from item (for SwarmResult) or use fallback."""
|
|
2051
|
+
if is_swarm_result(item):
|
|
2052
|
+
return item.meta.item_index
|
|
2053
|
+
return fallback
|
|
2054
|
+
|
|
2055
|
+
def _resolve_prompt(self, prompt: Prompt, files: FileMap, index: int) -> str:
|
|
2056
|
+
"""Resolve prompt (string or callable) to string."""
|
|
2057
|
+
return prompt(files, index) if callable(prompt) else prompt
|
|
2058
|
+
|
|
2059
|
+
def _build_agent_config(self, override: Optional[AgentConfig]) -> Optional[AgentConfig]:
|
|
2060
|
+
"""Build agent config with optional override.
|
|
2061
|
+
|
|
2062
|
+
If override provided, merge with base config (keys inherited from base).
|
|
2063
|
+
If no override and no base config, return None (TS SDK resolves from env).
|
|
2064
|
+
"""
|
|
2065
|
+
base = self.config.agent
|
|
2066
|
+
if override:
|
|
2067
|
+
return AgentConfig(
|
|
2068
|
+
type=override.type,
|
|
2069
|
+
api_key=base.api_key if base else None,
|
|
2070
|
+
provider_api_key=base.provider_api_key if base else None,
|
|
2071
|
+
oauth_token=base.oauth_token if base else None,
|
|
2072
|
+
provider_base_url=base.provider_base_url if base else None,
|
|
2073
|
+
model=override.model,
|
|
2074
|
+
reasoning_effort=override.reasoning_effort,
|
|
2075
|
+
# Inherited like the other base-only fields (TS spreads the base
|
|
2076
|
+
# config under the override, so this must survive too).
|
|
2077
|
+
max_context_size=base.max_context_size if base else None,
|
|
2078
|
+
)
|
|
2079
|
+
return base
|
|
2080
|
+
|
|
2081
|
+
def _build_result(
|
|
2082
|
+
self,
|
|
2083
|
+
result: Dict[str, Any],
|
|
2084
|
+
meta: IndexedMeta,
|
|
2085
|
+
files_override: Optional[FileMap] = None,
|
|
2086
|
+
) -> SwarmResult:
|
|
2087
|
+
"""Build SwarmResult from execution result."""
|
|
2088
|
+
files = files_override if files_override is not None else result["files"]
|
|
2089
|
+
|
|
2090
|
+
if result.get("error"):
|
|
2091
|
+
return SwarmResult(
|
|
2092
|
+
status="error",
|
|
2093
|
+
data=None,
|
|
2094
|
+
files=files,
|
|
2095
|
+
meta=meta,
|
|
2096
|
+
error=result["error"],
|
|
2097
|
+
raw_data=result.get("raw_data"),
|
|
2098
|
+
)
|
|
2099
|
+
|
|
2100
|
+
return SwarmResult(
|
|
2101
|
+
status="success",
|
|
2102
|
+
data=result["data"],
|
|
2103
|
+
files=files,
|
|
2104
|
+
meta=meta,
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
def _build_error_result(self, error: str, meta: IndexedMeta) -> SwarmResult:
|
|
2108
|
+
"""Build error SwarmResult."""
|
|
2109
|
+
return SwarmResult(
|
|
2110
|
+
status="error",
|
|
2111
|
+
data=None,
|
|
2112
|
+
files={},
|
|
2113
|
+
meta=meta,
|
|
2114
|
+
error=error,
|
|
2115
|
+
)
|
|
2116
|
+
|