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.
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
+