evolve-sdk 0.0.52.dev744__tar.gz → 0.0.52.dev756__tar.gz

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.
Files changed (43) hide show
  1. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/PKG-INFO +1 -1
  2. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/__init__.py +13 -2
  3. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/hosted.py +284 -20
  4. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve_sdk.egg-info/PKG-INFO +1 -1
  5. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/pyproject.toml +1 -1
  6. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/LICENSE +0 -0
  7. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/MANIFEST.in +0 -0
  8. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/README.md +0 -0
  9. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/bridge/__init__.py +0 -0
  10. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/bridge/dist/bridge.bundle.cjs +0 -0
  11. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/_http.py +0 -0
  12. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/agent.py +0 -0
  13. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/bridge.py +0 -0
  14. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/browser_credentials.py +0 -0
  15. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/browser_profiles.py +0 -0
  16. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/config.py +0 -0
  17. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/integrations.py +0 -0
  18. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/managed_secrets.py +0 -0
  19. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/pipeline/__init__.py +0 -0
  20. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/pipeline/pipeline.py +0 -0
  21. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/pipeline/types.py +0 -0
  22. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/__init__.py +0 -0
  23. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/agent_md/judge.md +0 -0
  24. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/agent_md/reduce.md +0 -0
  25. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/agent_md/verify.md +0 -0
  26. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/user/judge.md +0 -0
  27. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/user/retry_feedback.md +0 -0
  28. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/prompts/user/verify.md +0 -0
  29. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/results.py +0 -0
  30. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/retry.py +0 -0
  31. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/schema.py +0 -0
  32. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/sessions_client.py +0 -0
  33. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/storage_client.py +0 -0
  34. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/swarm/__init__.py +0 -0
  35. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/swarm/results.py +0 -0
  36. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/swarm/swarm.py +0 -0
  37. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/swarm/types.py +0 -0
  38. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve/utils.py +0 -0
  39. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve_sdk.egg-info/SOURCES.txt +0 -0
  40. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve_sdk.egg-info/dependency_links.txt +0 -0
  41. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve_sdk.egg-info/requires.txt +0 -0
  42. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/evolve_sdk.egg-info/top_level.txt +0 -0
  43. {evolve_sdk-0.0.52.dev744 → evolve_sdk-0.0.52.dev756}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolve-sdk
3
- Version: 0.0.52.dev744
3
+ Version: 0.0.52.dev756
4
4
  Summary: Pythonic SDK for multi-agent orchestration in E2B sandboxes
5
5
  Author-email: "Swarmlink, Inc." <brandomagnani@evolvingmachines.ai>
6
6
  License: Apache-2.0
@@ -38,6 +38,7 @@ from .hosted import (
38
38
  Agent,
39
39
  AgentArm,
40
40
  AgentCapability,
41
+ AgentDatasetStats,
41
42
  AgentInfo,
42
43
  AgentPage,
43
44
  AgentResult,
@@ -80,6 +81,9 @@ from .hosted import (
80
81
  JobEvent,
81
82
  JobFailure,
82
83
  JobPage,
84
+ JobRetryConfig,
85
+ JobRetryConfigInput,
86
+ JobStats,
83
87
  JobStatus,
84
88
  JobTaskRollup,
85
89
  JobTaskRollupPage,
@@ -385,7 +389,7 @@ async def list_checkpoints(
385
389
  await store.close()
386
390
 
387
391
 
388
- __version__ = '0.0.52.dev744'
392
+ __version__ = '0.0.52.dev756'
389
393
 
390
394
  __all__ = [
391
395
  # Main classes
@@ -480,9 +484,13 @@ __all__ = [
480
484
  'Task',
481
485
  'TaskProviderVerdict',
482
486
  'AgentArm',
487
+ 'AgentDatasetStats',
483
488
  'Job',
484
489
  'JobCounts',
485
490
  'JobFailure',
491
+ 'JobRetryConfig',
492
+ 'JobRetryConfigInput',
493
+ 'JobStats',
486
494
  'JobStatus',
487
495
  'TrialStatus',
488
496
  'EvalSandboxProvider',
@@ -601,7 +609,10 @@ __all__ = [
601
609
  'JudgeCompleteEvent',
602
610
  'EmitOption',
603
611
 
604
- # Retry
612
+ # Retry — the SWARM client-side retry helper. The hosted job auto-retry
613
+ # policy is a different shape with its own names: JobRetryConfig /
614
+ # JobRetryConfigInput above (the spec calls that pair RetryConfig /
615
+ # RetryConfigInput; the Python export is prefixed so the two never shadow).
605
616
  'RetryConfig',
606
617
  'OnItemRetryCallback',
607
618
  'execute_with_retry',
@@ -46,6 +46,7 @@ from typing import (
46
46
  Literal,
47
47
  NoReturn,
48
48
  Optional,
49
+ TypedDict,
49
50
  Union,
50
51
  get_args,
51
52
  )
@@ -161,6 +162,7 @@ HostedErrorCode = Literal[
161
162
  'import_not_found',
162
163
  'import_too_large',
163
164
  'invalid_archive',
165
+ 'unpinned_git_ref',
164
166
  'package_not_retained',
165
167
  'package_corrupt',
166
168
  'package_missing',
@@ -437,15 +439,22 @@ class TaskPage:
437
439
 
438
440
  @dataclass
439
441
  class UpstreamStatus:
440
- """Where a dataset's git source points now, versus what its active version
441
- was built from — the data behind a "new version available" badge.
442
+ """The active version's git provenance, plus the watch on a ref that can
443
+ move.
444
+
445
+ The provenance half — ``git_url`` + the requested ``ref`` + the resolved
446
+ commit (``current_commit``) + the repository subfolder (``path``) — says
447
+ what the active version was built from. The watch half (``latest_commit``,
448
+ ``moved``, ``checked_at``, …) is the data behind a "new version available"
449
+ badge; on a version pinned to a commit sha it stays at rest (None / False),
450
+ because a pin cannot move and nothing checks one.
442
451
 
443
452
  Nothing here imports anything by itself — a new version is always a row you
444
453
  create, or ``auto_import`` creates.
445
454
  """
446
- #: The ref the active version was imported from.
455
+ #: The ref the active version was imported from, exactly as requested.
447
456
  ref: str
448
- #: The commit the active version was built from.
457
+ #: The commit the active version was built from (the resolved sha).
449
458
  current_commit: str
450
459
  #: Where the ref points upstream now; None when the last check failed.
451
460
  latest_commit: Optional[str]
@@ -459,6 +468,16 @@ class UpstreamStatus:
459
468
  error: Optional[str]
460
469
  #: Whether a moved upstream automatically imports a new version.
461
470
  auto_import: bool = False
471
+ #: The repository the active version was imported from, userinfo (an
472
+ #: embedded token) stripped. None on an older server or an unparseable url.
473
+ git_url: Optional[str] = None
474
+ #: The repository subfolder the corpus was read from; None = the
475
+ #: repository root (or an older server).
476
+ path: Optional[str] = None
477
+ #: The newest commit a local version already exists for, whether or not it
478
+ #: is the active one; None before any import recorded one (or an older
479
+ #: server).
480
+ acked_commit: Optional[str] = None
462
481
 
463
482
 
464
483
  @dataclass
@@ -781,6 +800,149 @@ class JobFailure:
781
800
  message: str
782
801
 
783
802
 
803
+ class AgentDatasetStats(TypedDict, total=False):
804
+ """Per-(agent, model, dataset) statistics — one ``stats['evals']`` group.
805
+
806
+ The evals key format is ``{agent}__{model}__{dataset}`` — the dataset ref
807
+ is always the LAST ``__`` segment, which is where Harbor-compatible
808
+ readers recover it — with the platform extension of an ``__{effort}``
809
+ segment inserted BEFORE the dataset when a declared reasoning effort is
810
+ part of the arm identity: ``{agent}__{model}__{effort}__{dataset}``.
811
+
812
+ A ``TypedDict``: at runtime this IS the plain wire dict, read by key like
813
+ always — the class only teaches type checkers the keys. Every key is
814
+ optional, like the TypeScript interface's ``?`` fields.
815
+ """
816
+ #: Trials that produced a rewards map — rewarded, not merely settled.
817
+ n_trials: int
818
+ #: Trials carrying ``exception_info`` — indeterminate and cancelled included.
819
+ n_errors: int
820
+ #: Metric results (a mean entry per arm today: the primary reward averaged
821
+ #: over EVERY trial of the group, unrewarded trials counting 0); open objects.
822
+ metrics: List[Dict[str, Any]]
823
+ #: pass@k for this group, k (as a string — JSON object keys always are) to
824
+ #: a value in [0, 1]; ``{}`` when the group cannot answer. Read it with
825
+ #: :func:`pass_at_k`, which returns sorted numeric points.
826
+ pass_at_k: Dict[str, float]
827
+ #: reward key -> reward value -> trial identifiers.
828
+ reward_stats: Dict[str, Dict[str, List[str]]]
829
+ #: exception type -> trial identifiers.
830
+ exception_stats: Dict[str, List[str]]
831
+
832
+
833
+ class JobStats(TypedDict, total=False):
834
+ """Aggregate statistics of a job. Progress counters, token totals, and
835
+ measured cost. The ``n_*`` counters are CUMULATIVE, Harbor-style: errored
836
+ trials are a subset of completed, cancelled a subset of errored — a
837
+ cancelled trial counts in all three. The disjoint per-status breakdown
838
+ rides ``Job.trials.by_status``. ``cost_usd`` is what the trials actually
839
+ spent so far — reporting, never a gate (enforcement is the per-trial cap).
840
+
841
+ A ``TypedDict``: at runtime this IS the plain wire dict, read by key like
842
+ always (``stats.get('cost_usd')``, ``stats['evals']``) — the class only
843
+ teaches type checkers the keys. Every key is optional, like the
844
+ TypeScript interface's ``?`` fields.
845
+ """
846
+ #: Cumulative: every trial that produced a result — errored and cancelled included.
847
+ n_completed_trials: int
848
+ #: Cumulative: every completed trial carrying ``exception_info``, cancelled included.
849
+ n_errored_trials: int
850
+ n_running_trials: int
851
+ n_pending_trials: int
852
+ #: A subset of ``n_errored_trials``.
853
+ n_cancelled_trials: int
854
+ n_retries: int
855
+ #: Keyed ``{agent}__{model}__{dataset}`` — dataset ref last, optional
856
+ #: effort segment before it.
857
+ evals: Dict[str, AgentDatasetStats]
858
+ #: Total input tokens (cache included); None until recorded.
859
+ n_input_tokens: Optional[int]
860
+ n_cache_tokens: Optional[int]
861
+ n_output_tokens: Optional[int]
862
+ #: Measured spend across settled trials — the WHOLE model bill, agent and
863
+ #: judge together; None before any settled.
864
+ cost_usd: Optional[float]
865
+ #: Sum of the job's per-trial GPU compute ESTIMATES (each trial's
866
+ #: ``gpu_cost['estimate_usd']``) — a SEPARATE labeled figure, never merged
867
+ #: into ``cost_usd`` (metered model spend). None when no trial of the job
868
+ #: carries an estimate; a real $0 (a GPU trial that provably never booted
869
+ #: a sandbox) keeps the sum non-None.
870
+ gpu_cost_usd: Optional[float]
871
+ #: The judge share of ``cost_usd``, itemized: what the trials'
872
+ #: verifier-phase judge keys spent. 0 for a job with no judge-enabled
873
+ #: tasks; None before anything settled, like ``cost_usd``.
874
+ judge_cost_usd: Optional[float]
875
+
876
+
877
+ class JobRetryConfigInput(TypedDict, total=False):
878
+ """Auto-retry policy INPUT — ``jobs().start(retry=...)``, the spec's
879
+ ``RetryConfigInput`` schema, Harbor's RetryConfig vocabulary verbatim.
880
+
881
+ ONE NAMING NOTE: Harbor, the spec, and the TypeScript SDK call this pair
882
+ ``RetryConfig`` / ``RetryConfigInput``. The Python SDK exports them as
883
+ ``JobRetryConfig`` / ``JobRetryConfigInput`` because ``evolve.RetryConfig``
884
+ already names the Swarm client-side retry helper (``max_attempts``,
885
+ ``backoff_ms``, ...) — two unrelated shapes never share one exported name.
886
+
887
+ A ``TypedDict``: pass the plain dict you always passed — the class only
888
+ teaches type checkers the keys. Every key is optional, and an omitted key
889
+ takes Harbor's own default with ONE named deviation: ``max_retries``
890
+ omitted takes the PLATFORM fleet default (published as
891
+ ``limits['job']['default_max_retries']`` on ``meta()``; 2 unless the
892
+ operator tuned it) rather than Harbor's 0 — infrastructure errors on a
893
+ hosted fleet retry automatically. Send ``{'max_retries': 0}`` to turn
894
+ retries off.
895
+
896
+ Inside the policy, an EXPLICIT ``'exclude_exceptions': None`` is not the
897
+ same as leaving the key out: None turns exclusions off entirely (Harbor's
898
+ own None semantics), while an omitted key keeps Harbor's default
899
+ non-retryable set. ``include_exceptions`` has no such split: None, an
900
+ omitted key, and ``[]`` all mean no filter.
901
+ """
902
+ #: Maximum automatic retries per trial (0-10). Omitted = fleet default; 0 = off.
903
+ max_retries: int
904
+ #: Exception types to retry on. None, omitted, or ``[]`` = no filter.
905
+ include_exceptions: Optional[List[str]]
906
+ #: Exception types to NOT retry on; wins over ``include_exceptions``.
907
+ #: Omitted = Harbor's default non-retryable set; an EXPLICIT None turns
908
+ #: exclusions off entirely.
909
+ exclude_exceptions: Optional[List[str]]
910
+ #: Multiplier for exponential backoff wait time (default 1.0).
911
+ wait_multiplier: float
912
+ #: Minimum wait in seconds between retries (default 1.0).
913
+ min_wait_sec: float
914
+ #: Maximum wait in seconds between retries (default 60.0; platform cap 3600).
915
+ max_wait_sec: float
916
+
917
+
918
+ class JobRetryConfig(TypedDict):
919
+ """The RESOLVED auto-retry policy a job runs under — the spec's
920
+ ``RetryConfig`` schema, echoed on every job body as ``Job.retry``: the
921
+ caller's values or the defaults of the day, resolved at create and
922
+ stored, so the row always states the policy it executes. Backoff between
923
+ attempts is Harbor's formula:
924
+ min(min_wait_sec x wait_multiplier^attempt, max_wait_sec).
925
+
926
+ A ``TypedDict``: at runtime this IS the plain wire dict, read by key like
927
+ always (``job.retry['max_retries']``) — the class only teaches type
928
+ checkers the keys. Every key is REQUIRED, like the spec's own required
929
+ list: the mapper reads an older server that sends no policy as the
930
+ retries-off policy with Harbor's defaults — exactly how such a server
931
+ behaves — so every field is always present.
932
+
933
+ Named ``JobRetryConfig`` rather than the spec's ``RetryConfig`` because
934
+ ``evolve.RetryConfig`` is the (unrelated) Swarm retry helper — see
935
+ :class:`JobRetryConfigInput` for the input half and the naming note.
936
+ """
937
+ max_retries: int
938
+ #: None = no include filter (everything the exclude set admits).
939
+ include_exceptions: Optional[List[str]]
940
+ exclude_exceptions: List[str]
941
+ wait_multiplier: float
942
+ min_wait_sec: float
943
+ max_wait_sec: float
944
+
945
+
784
946
  @dataclass
785
947
  class Job:
786
948
  """THE job body — the same shape from start, get, list rows, cancel,
@@ -807,8 +969,21 @@ class Job:
807
969
  #: The RESOLVED auto-retry policy this job runs under — Harbor's
808
970
  #: RetryConfig field names (``max_retries``, ``include_exceptions``,
809
971
  #: ``exclude_exceptions``, ``wait_multiplier``, ``min_wait_sec``,
810
- #: ``max_wait_sec``). A plain dict with the wire's own keys, read by key.
811
- retry: Dict[str, Any]
972
+ #: ``max_wait_sec``). Still the plain wire dict at runtime, read by key
973
+ #: :class:`JobRetryConfig` types the keys, and every field is ALWAYS
974
+ #: present: an older server that sends no policy reads as the retries-off
975
+ #: policy with Harbor's defaults, exactly how such a server behaves.
976
+ retry: JobRetryConfig
977
+ #: The RESOLVED timeout multipliers this job's phases arm under —
978
+ #: Harbor's five flat JobConfig fields, echoed on every job body. The
979
+ #: global one is always a number (1.0 when the create request named
980
+ #: none); each phase field below is None when not overridden, meaning the
981
+ #: global one applies to that phase.
982
+ timeout_multiplier: float
983
+ agent_timeout_multiplier: Optional[float]
984
+ verifier_timeout_multiplier: Optional[float]
985
+ agent_setup_timeout_multiplier: Optional[float]
986
+ environment_build_timeout_multiplier: Optional[float]
812
987
  sandbox_provider: EvalSandboxProvider
813
988
  counts: JobCounts
814
989
  n_total_trials: int
@@ -820,8 +995,10 @@ class Job:
820
995
  #: GPU compute estimate, never merged into ``cost_usd``; ``evals`` keyed
821
996
  #: ``agent__model__dataset``, each group carrying its mean and its
822
997
  #: ``pass_at_k`` — read the latter with :func:`pass_at_k`).
823
- #: A plain dict with the wire's own keys, read by key, never constructed.
824
- stats: Dict[str, Any]
998
+ #: Still the plain wire dict at runtime, read by key, never constructed
999
+ #: :class:`JobStats` types the keys so ``stats['evals']`` and friends
1000
+ #: check instead of being ``Any``.
1001
+ stats: JobStats
825
1002
  #: Why the job FAILED, or None.
826
1003
  failure: Optional[JobFailure]
827
1004
  #: Provenance of a derived job; empty for an original one.
@@ -1477,6 +1654,9 @@ def _map_upstream(data: Any) -> Optional[UpstreamStatus]:
1477
1654
  checked_at=data.get('checked_at'),
1478
1655
  error=data.get('error'),
1479
1656
  auto_import=data.get('auto_import') is True,
1657
+ git_url=data.get('git_url'),
1658
+ path=data.get('path'),
1659
+ acked_commit=data.get('acked_commit'),
1480
1660
  )
1481
1661
 
1482
1662
 
@@ -1734,6 +1914,34 @@ def _map_source_job(data: Dict[str, Any]) -> SourceJob:
1734
1914
  )
1735
1915
 
1736
1916
 
1917
+ def _optional_float(value: Any) -> Optional[float]:
1918
+ """A wire number or None — anything else (an older server, garbage) is None."""
1919
+ return float(value) if isinstance(value, (int, float)) else None
1920
+
1921
+
1922
+ def _map_retry_config(data: Any) -> JobRetryConfig:
1923
+ """The resolved retry policy off a job body. Tolerant of an OLDER server
1924
+ that does not send one yet: the absent-field reading is the retries-off
1925
+ policy with Harbor's defaults — exactly how such a server behaves.
1926
+ """
1927
+ raw = data if isinstance(data, dict) else {}
1928
+
1929
+ def _number(value: Any, default: float) -> float:
1930
+ # bool is an int in Python; a boolean here is malformed, not a number.
1931
+ return value if isinstance(value, (int, float)) and not isinstance(value, bool) else default
1932
+
1933
+ include = raw.get('include_exceptions')
1934
+ exclude = raw.get('exclude_exceptions')
1935
+ return {
1936
+ 'max_retries': int(_number(raw.get('max_retries'), 0)),
1937
+ 'include_exceptions': include if isinstance(include, list) else None,
1938
+ 'exclude_exceptions': exclude if isinstance(exclude, list) else [],
1939
+ 'wait_multiplier': _number(raw.get('wait_multiplier'), 1.0),
1940
+ 'min_wait_sec': _number(raw.get('min_wait_sec'), 1.0),
1941
+ 'max_wait_sec': _number(raw.get('max_wait_sec'), 60.0),
1942
+ }
1943
+
1944
+
1737
1945
  def _map_job(data: Dict[str, Any]) -> Job:
1738
1946
  """The ONE job mapper — nothing conditional, because nothing is optional."""
1739
1947
  agents = data.get('agents')
@@ -1753,9 +1961,23 @@ def _map_job(data: Dict[str, Any]) -> Job:
1753
1961
  n_concurrent_trials=int(data.get('n_concurrent_trials', 0)),
1754
1962
  max_trial_spend_usd=float(data.get('max_trial_spend_usd', 0)),
1755
1963
  worst_case_spend_usd=float(data.get('worst_case_spend_usd', 0)),
1756
- # An older server sends no policy; {} reads as "no auto-retry", which
1757
- # is exactly how such a server behaves.
1758
- retry=data.get('retry') if isinstance(data.get('retry'), dict) else {},
1964
+ # Resolved with Harbor's defaults for anything absent an older
1965
+ # server that sends no policy reads as the retries-off policy, which
1966
+ # is exactly how such a server behaves; every field always present.
1967
+ retry=_map_retry_config(data.get('retry')),
1968
+ # Timeout multipliers: an older server sends none — 1.0 / None reads
1969
+ # as "every phase at 1.0", exactly how such a server behaves.
1970
+ timeout_multiplier=(
1971
+ float(data['timeout_multiplier'])
1972
+ if isinstance(data.get('timeout_multiplier'), (int, float))
1973
+ else 1.0
1974
+ ),
1975
+ agent_timeout_multiplier=_optional_float(data.get('agent_timeout_multiplier')),
1976
+ verifier_timeout_multiplier=_optional_float(data.get('verifier_timeout_multiplier')),
1977
+ agent_setup_timeout_multiplier=_optional_float(data.get('agent_setup_timeout_multiplier')),
1978
+ environment_build_timeout_multiplier=_optional_float(
1979
+ data.get('environment_build_timeout_multiplier')
1980
+ ),
1759
1981
  sandbox_provider=data.get('sandbox_provider', ''),
1760
1982
  counts=_map_counts(data.get('counts')),
1761
1983
  n_total_trials=int(data.get('n_total_trials', 0)),
@@ -1907,6 +2129,23 @@ def _map_trial_retry(data: Dict[str, Any]) -> TrialRetry:
1907
2129
  )
1908
2130
 
1909
2131
 
2132
+ def _map_provider_degrade(data: Any) -> Optional[Dict[str, str]]:
2133
+ """The wire degrade object, defensively: anything malformed answers None,
2134
+ never a crash and never a partial row; extra keys never ride through."""
2135
+ if not isinstance(data, dict):
2136
+ return None
2137
+ from_provider = data.get('from')
2138
+ to_provider = data.get('to')
2139
+ reason = data.get('reason')
2140
+ if not (
2141
+ isinstance(from_provider, str)
2142
+ and isinstance(to_provider, str)
2143
+ and isinstance(reason, str)
2144
+ ):
2145
+ return None
2146
+ return {'from': from_provider, 'to': to_provider, 'reason': reason}
2147
+
2148
+
1910
2149
  def _map_trial(data: Dict[str, Any]) -> Trial:
1911
2150
  retries = data.get('retries')
1912
2151
  return Trial(
@@ -1938,11 +2177,7 @@ def _map_trial(data: Dict[str, Any]) -> Trial:
1938
2177
  max_trial_spend_usd=data.get('max_trial_spend_usd'),
1939
2178
  sandbox_provider=data.get('sandbox_provider'),
1940
2179
  # Defensive: a malformed degrade object reads as None, never a crash.
1941
- sandbox_provider_degrade=(
1942
- data['sandbox_provider_degrade']
1943
- if isinstance(data.get('sandbox_provider_degrade'), dict)
1944
- else None
1945
- ),
2180
+ sandbox_provider_degrade=_map_provider_degrade(data.get('sandbox_provider_degrade')),
1946
2181
  # Same defensive rule; absent (an older server) reads as None too.
1947
2182
  gpu_cost=(
1948
2183
  data['gpu_cost'] if isinstance(data.get('gpu_cost'), dict) else None
@@ -3387,7 +3622,12 @@ class JobsClient:
3387
3622
  n_concurrent_trials: Optional[int] = None,
3388
3623
  max_trial_spend_usd: Optional[float] = None,
3389
3624
  sandbox_provider: Optional[str] = None,
3390
- retry: Optional[Dict[str, Any]] = None,
3625
+ retry: Optional[JobRetryConfigInput] = None,
3626
+ timeout_multiplier: Optional[float] = None,
3627
+ agent_timeout_multiplier: Optional[float] = None,
3628
+ verifier_timeout_multiplier: Optional[float] = None,
3629
+ agent_setup_timeout_multiplier: Optional[float] = None,
3630
+ environment_build_timeout_multiplier: Optional[float] = None,
3391
3631
  agent_env: Optional[Dict[str, str]] = None,
3392
3632
  verifier_env: Optional[Dict[str, str]] = None,
3393
3633
  idempotency_key: Optional[str] = None,
@@ -3408,10 +3648,12 @@ class JobsClient:
3408
3648
  case for the whole job. ``retry`` is the auto-retry policy in
3409
3649
  Harbor's RetryConfig vocabulary (``max_retries``,
3410
3650
  ``include_exceptions``, ``exclude_exceptions``, ``wait_multiplier``,
3411
- ``min_wait_sec``, ``max_wait_sec``); omitted, the server applies its
3651
+ ``min_wait_sec``, ``max_wait_sec``) a plain dict, typed
3652
+ :class:`JobRetryConfigInput`; omitted, the server applies its
3412
3653
  fleet defaults (infrastructure errors retry automatically — send
3413
3654
  ``{'max_retries': 0}`` to turn retries off), and the response echoes
3414
- the RESOLVED policy either way. Inside the policy, an EXPLICIT
3655
+ the RESOLVED policy either way as ``Job.retry``
3656
+ (:class:`JobRetryConfig`, every field present). Inside the policy, an EXPLICIT
3415
3657
  ``'exclude_exceptions': None`` is not the same as leaving the key
3416
3658
  out: None turns exclusions off entirely (everything the include set
3417
3659
  admits is retried — Harbor's own None semantics), while an omitted
@@ -3419,7 +3661,19 @@ class JobsClient:
3419
3661
  ``'include_exceptions'`` has no such split: None, an omitted key,
3420
3662
  and the empty list ``[]`` all mean no filter — Harbor's include
3421
3663
  check treats the empty set exactly like None, so ``[]`` never means
3422
- "retry nothing". ``agent_env`` / ``verifier_env`` are
3664
+ "retry nothing". The five ``*timeout_multiplier`` arguments are
3665
+ Harbor's timeout knobs verbatim: ``timeout_multiplier`` multiplies
3666
+ every TASK-DECLARED timeout for this job's runs (default 1.0;
3667
+ values below 1 shrink), and each phase-specific one —
3668
+ ``agent_timeout_multiplier``, ``verifier_timeout_multiplier``,
3669
+ ``agent_setup_timeout_multiplier``,
3670
+ ``environment_build_timeout_multiplier`` — overrides it for that
3671
+ phase. The task itself is never rewritten. Every multiplier must be
3672
+ greater than 0 and at most the published ceiling
3673
+ (``limits['job']['max_timeout_multiplier']`` on the capability
3674
+ document; 10 unless the fleet changes it) — an absurd value is
3675
+ refused at create with a typed message naming the bound.
3676
+ ``agent_env`` / ``verifier_env`` are
3423
3677
  pass-through slots injected into every agent / verifier run — sent
3424
3678
  verbatim; the server owns acceptance (refused where unsupported,
3425
3679
  never silently dropped). The hosted platform honors exactly two
@@ -3452,6 +3706,16 @@ class JobsClient:
3452
3706
  body['sandbox_provider'] = sandbox_provider
3453
3707
  if retry is not None:
3454
3708
  body['retry'] = retry
3709
+ if timeout_multiplier is not None:
3710
+ body['timeout_multiplier'] = timeout_multiplier
3711
+ if agent_timeout_multiplier is not None:
3712
+ body['agent_timeout_multiplier'] = agent_timeout_multiplier
3713
+ if verifier_timeout_multiplier is not None:
3714
+ body['verifier_timeout_multiplier'] = verifier_timeout_multiplier
3715
+ if agent_setup_timeout_multiplier is not None:
3716
+ body['agent_setup_timeout_multiplier'] = agent_setup_timeout_multiplier
3717
+ if environment_build_timeout_multiplier is not None:
3718
+ body['environment_build_timeout_multiplier'] = environment_build_timeout_multiplier
3455
3719
  if agent_env is not None:
3456
3720
  body['agent_env'] = agent_env
3457
3721
  if verifier_env is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolve-sdk
3
- Version: 0.0.52.dev744
3
+ Version: 0.0.52.dev756
4
4
  Summary: Pythonic SDK for multi-agent orchestration in E2B sandboxes
5
5
  Author-email: "Swarmlink, Inc." <brandomagnani@evolvingmachines.ai>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evolve-sdk"
7
- version = "0.0.52.dev744"
7
+ version = "0.0.52.dev756"
8
8
  description = "Pythonic SDK for multi-agent orchestration in E2B sandboxes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"