evolve-sdk 0.0.52.dev955__tar.gz → 0.0.52.dev965__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 (44) hide show
  1. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/PKG-INFO +1 -1
  2. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/__init__.py +17 -1
  3. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/hosted.py +305 -7
  4. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve_sdk.egg-info/PKG-INFO +1 -1
  5. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/pyproject.toml +1 -1
  6. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/LICENSE +0 -0
  7. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/MANIFEST.in +0 -0
  8. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/README.md +0 -0
  9. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/bridge/__init__.py +0 -0
  10. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/bridge/dist/bridge.bundle.cjs +0 -0
  11. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/_http.py +0 -0
  12. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/agent.py +0 -0
  13. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/bridge.py +0 -0
  14. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/browser_credentials.py +0 -0
  15. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/browser_profiles.py +0 -0
  16. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/config.py +0 -0
  17. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/integrations.py +0 -0
  18. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/managed_secrets.py +0 -0
  19. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/pipeline/__init__.py +0 -0
  20. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/pipeline/pipeline.py +0 -0
  21. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/pipeline/types.py +0 -0
  22. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/__init__.py +0 -0
  23. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/agent_md/judge.md +0 -0
  24. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/agent_md/reduce.md +0 -0
  25. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/agent_md/verify.md +0 -0
  26. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/user/judge.md +0 -0
  27. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/user/retry_feedback.md +0 -0
  28. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/prompts/user/verify.md +0 -0
  29. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/py.typed +0 -0
  30. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/results.py +0 -0
  31. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/retry.py +0 -0
  32. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/schema.py +0 -0
  33. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/sessions_client.py +0 -0
  34. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/storage_client.py +0 -0
  35. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/swarm/__init__.py +0 -0
  36. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/swarm/results.py +0 -0
  37. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/swarm/swarm.py +0 -0
  38. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/swarm/types.py +0 -0
  39. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve/utils.py +0 -0
  40. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve_sdk.egg-info/SOURCES.txt +0 -0
  41. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve_sdk.egg-info/dependency_links.txt +0 -0
  42. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve_sdk.egg-info/requires.txt +0 -0
  43. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/evolve_sdk.egg-info/top_level.txt +0 -0
  44. {evolve_sdk-0.0.52.dev955 → evolve_sdk-0.0.52.dev965}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolve-sdk
3
- Version: 0.0.52.dev955
3
+ Version: 0.0.52.dev965
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
@@ -45,6 +45,10 @@ from .hosted import (
45
45
  AgentPage,
46
46
  AgentResult,
47
47
  AgentsClient,
48
+ AnalysisCheck,
49
+ AnalysisFailure,
50
+ AnalyzeConfig,
51
+ AnalyzeConfigInput,
48
52
  ApiKey,
49
53
  AttemptPhase,
50
54
  AuthClient,
@@ -80,6 +84,7 @@ from .hosted import (
80
84
  ImportTaskFailure,
81
85
  ImportWarning,
82
86
  Job,
87
+ JobAnalysisStats,
83
88
  JobBuildExclusion,
84
89
  JobCounts,
85
90
  JobEvent,
@@ -119,7 +124,10 @@ from .hosted import (
119
124
  TraceEventPage,
120
125
  JobGrepGroup,
121
126
  JobGrepPage,
127
+ Rubric,
128
+ RubricCriterion,
122
129
  Trial,
130
+ TrialAnalysis,
123
131
  TrialFile,
124
132
  TrialFilePage,
125
133
  TrialPage,
@@ -407,7 +415,7 @@ async def list_checkpoints(
407
415
  await store.close()
408
416
 
409
417
 
410
- __version__ = '0.0.52.dev955'
418
+ __version__ = '0.0.52.dev965'
411
419
 
412
420
  __all__ = [
413
421
  # Main classes
@@ -510,6 +518,7 @@ __all__ = [
510
518
  'AgentArm',
511
519
  'AgentDatasetStats',
512
520
  'Job',
521
+ 'JobAnalysisStats',
513
522
  'JobBuildExclusion',
514
523
  'JobCounts',
515
524
  'JobFailure',
@@ -519,6 +528,13 @@ __all__ = [
519
528
  'JobSecretInline',
520
529
  'JobStats',
521
530
  'JobStatus',
531
+ 'AnalyzeConfig',
532
+ 'AnalyzeConfigInput',
533
+ 'AnalysisCheck',
534
+ 'AnalysisFailure',
535
+ 'Rubric',
536
+ 'RubricCriterion',
537
+ 'TrialAnalysis',
522
538
  'TrialStatus',
523
539
  'EvalSandboxProvider',
524
540
  'SpendSource',
@@ -180,6 +180,13 @@ HostedErrorCode = Literal[
180
180
  'concurrent_update',
181
181
  'regrade_source_ineligible',
182
182
  'no_regradable_trials',
183
+ # Analyze: a rubric that cannot rule an analysis (unknown keys, empty or
184
+ # duplicate criteria, bounds exceeded); one wave at a time (re-analysis is
185
+ # legal once the previous wave settles); a terminal job with no analyzable
186
+ # trial (every trial CANCELLED).
187
+ 'invalid_rubric',
188
+ 'analysis_already_running',
189
+ 'no_analyzable_trials',
183
190
  'import_not_found',
184
191
  'import_too_large',
185
192
  'invalid_archive',
@@ -1071,6 +1078,10 @@ class JobStats(TypedDict, total=False):
1071
1078
  #: measured (``judge_spend_source`` ``'assumed_cap'``). 0 on jobs with no
1072
1079
  #: judge-enabled tasks.
1073
1080
  n_unmeasured_judge_trials: int
1081
+ #: Aggregate of the job's trace analyses; None when no trial of this job
1082
+ #: has ever been analyzed. Never a fabricated empty object — absence of
1083
+ #: analysis is stated as None, here and on each trial.
1084
+ analysis: Optional['JobAnalysisStats']
1074
1085
 
1075
1086
 
1076
1087
  class JobRetryConfigInput(TypedDict, total=False):
@@ -1150,6 +1161,149 @@ JobSecretInline = TypedDict(
1150
1161
  )
1151
1162
 
1152
1163
 
1164
+ class RubricCriterion(TypedDict):
1165
+ """One analysis criterion — Harbor's RubricCriterion verbatim (their
1166
+ cli/quality_checker/models.py ``{name, description, guidance}``). The
1167
+ name becomes the key of the matching entry in ``checks``; the guidance is
1168
+ what the analyzer agent is instructed with. A plain dict at runtime, like
1169
+ every wire shape here.
1170
+ """
1171
+ #: Criterion identifier, snake_case (it keys the result's ``checks``).
1172
+ #: Harbor's defaults are ``reward_hacking`` and ``task_specification``.
1173
+ name: str
1174
+ #: What the criterion evaluates, one sentence.
1175
+ description: str
1176
+ #: Evaluation guidance handed to the analyzer agent — what evidence to
1177
+ #: read and what PASS / FAIL / NOT_APPLICABLE mean for this criterion.
1178
+ guidance: str
1179
+
1180
+
1181
+ class Rubric(TypedDict):
1182
+ """An analysis rubric — Harbor's Rubric shape (``{criteria: [...]}``,
1183
+ their cli/quality_checker/models.py). The criteria set is FROZEN into
1184
+ each analysis at enqueue: the stored result is validated against exactly
1185
+ this set — a missing or extra criterion is a stored typed failure, never
1186
+ a partial pass.
1187
+ """
1188
+ criteria: List[RubricCriterion]
1189
+
1190
+
1191
+ class AnalyzeConfigInput(TypedDict, total=False):
1192
+ """Trace-analysis configuration INPUT — Harbor's ``harbor analyze``
1193
+ vocabulary (their cli/analyze.py: ``--model``, ``--rubric``), the spec's
1194
+ ``AnalyzeConfigInput`` schema.
1195
+
1196
+ PRESENCE of this object is the switch: on ``jobs().start(analyze=...)``
1197
+ it arms the embedded trigger (each trial is analyzed server-side right
1198
+ after it settles; CANCELLED trials are skipped); ``{}`` is legal and
1199
+ means "all defaults" — claude-haiku-4-5 over Harbor's default rubric
1200
+ (reward_hacking, task_specification). The analyzer always runs the
1201
+ claude-code harness in its own sealed sandbox; its spend is capped per
1202
+ analysis and metered as its own line, never blended into the trial's own
1203
+ bill.
1204
+ """
1205
+ #: Model the analyzer agent runs — Harbor's ``--model``; the default is
1206
+ #: Harbor's own (claude-haiku-4-5) under its wire id on this platform's
1207
+ #: claude roster. Off-roster models are refused typed (``invalid_input``).
1208
+ model_name: str
1209
+ rubric: Rubric
1210
+
1211
+
1212
+ class AnalyzeConfig(TypedDict):
1213
+ """The RESOLVED trace-analysis policy — the caller's values or the
1214
+ defaults of the day, resolved at accept and stored (same law as
1215
+ ``JobRetryConfig``). Echoed as ``Job.analyze`` when the job was created
1216
+ with ``analyze``; each analysis additionally carries the exact pair IT
1217
+ ran under (``Trial.analysis['model_name']`` / ``['rubric']``), which a
1218
+ later manual re-analysis may have changed.
1219
+ """
1220
+ model_name: str
1221
+ rubric: Rubric
1222
+
1223
+
1224
+ class AnalysisCheck(TypedDict):
1225
+ """One criterion's verdict — Harbor's QualityCheckModel verbatim (their
1226
+ cli/quality_checker/models.py ``{explanation, outcome}``)."""
1227
+ #: ``'pass'`` | ``'fail'`` | ``'not_applicable'``.
1228
+ outcome: str
1229
+ #: The analyzer's rationale, citing trial evidence.
1230
+ explanation: str
1231
+
1232
+
1233
+ class AnalysisFailure(TypedDict):
1234
+ """Why an analysis FAILED — a stored typed failure, never a silent
1235
+ absence and never a fake pass."""
1236
+ #: Which part failed: ``invalid_result`` (the analyzer ran but its
1237
+ #: analysis.json failed validation — the message preserves every
1238
+ #: validator reason, one per line), ``inputs`` (the trial tree or task
1239
+ #: content could not be assembled), or an infrastructure stage of the
1240
+ #: analyzer run (``mint_key``, ``boot``, ``harness_install``, ``agent``,
1241
+ #: ``artifact_read``, ``lease_expired``, ...).
1242
+ phase: str
1243
+ message: str
1244
+
1245
+
1246
+ class TrialAnalysis(TypedDict):
1247
+ """One trace analysis of a trial — ``Trial.analysis``.
1248
+
1249
+ The result half is Harbor's AnalyzeResult verbatim (their
1250
+ analyze/models.py: ``summary``, ``checks`` keyed by criterion,
1251
+ ``estimated_cost_usd``; the enclosing trial is Harbor's ``trial_name``);
1252
+ the rest is provenance — which model and rubric THIS analysis ran under,
1253
+ its lifecycle status, and its typed failure when it failed.
1254
+
1255
+ ``estimated_cost_usd`` is the analyzer agent's OWN metered spend — its
1256
+ own line, never part of the trial's ``agent_result.cost_usd`` or the
1257
+ job's ``stats['cost_usd']``; the job aggregate is
1258
+ ``stats['analysis']['cost_usd']``. None when nothing was measured, never
1259
+ a fabricated 0. A plain wire dict at runtime.
1260
+ """
1261
+ id: str
1262
+ #: ``'queued'`` | ``'running'`` | ``'completed'`` | ``'failed'``. Every
1263
+ #: non-terminal analysis reaches ``completed`` or ``failed``; a worker
1264
+ #: death mid-run is reaped to a typed ``failed``.
1265
+ status: str
1266
+ model_name: str
1267
+ rubric: Rubric
1268
+ #: 3–5 sentence overview of the trial (Harbor's summary contract). None
1269
+ #: until completed.
1270
+ summary: Optional[str]
1271
+ #: One entry per rubric criterion, keys exactly the rubric's criterion
1272
+ #: names (the frozen-criteria law). None until completed.
1273
+ checks: Optional[Dict[str, AnalysisCheck]]
1274
+ estimated_cost_usd: Optional[float]
1275
+ #: Non-None exactly when status is ``'failed'``.
1276
+ failure: Optional[AnalysisFailure]
1277
+ #: When this analysis was enqueued.
1278
+ created_at: str
1279
+ #: When it settled; None while queued or running.
1280
+ finished_at: Optional[str]
1281
+
1282
+
1283
+ class JobAnalysisStats(TypedDict):
1284
+ """The job-level analysis aggregate — ``stats['analysis']``.
1285
+
1286
+ Harbor's job ``analysis.json`` is a flat list of per-trial results
1287
+ (their analyze/models.py AnalyzeReport); each trial's own result rides
1288
+ ``Trial.analysis``, and this object aggregates them. LATEST-per-trial: a
1289
+ re-analyzed trial contributes only its newest analysis, matching Harbor,
1290
+ where a re-run overwrites the trial directory's ``analysis.json``.
1291
+ """
1292
+ #: Trials whose latest analysis produced a valid result.
1293
+ n_completed: int
1294
+ #: Trials whose latest analysis is a stored typed failure.
1295
+ n_failed: int
1296
+ #: Trials whose latest analysis is still queued or running.
1297
+ n_pending: int
1298
+ #: Measured spend of the LATEST analyses summed — the analyzer's own
1299
+ #: metered line, never part of ``stats['cost_usd']``. None when no
1300
+ #: analysis recorded measured spend.
1301
+ cost_usd: Optional[float]
1302
+ #: Per-criterion outcome tally over the completed latest analyses, keyed
1303
+ #: by criterion name: ``{n_pass, n_fail, n_not_applicable}`` each.
1304
+ checks: Dict[str, Dict[str, int]]
1305
+
1306
+
1153
1307
  class JobRetryConfig(TypedDict):
1154
1308
  """The RESOLVED auto-retry policy a job runs under — the spec's
1155
1309
  ``RetryConfig`` schema, echoed on every job body as ``Job.retry``: the
@@ -1236,6 +1390,11 @@ class Job:
1236
1390
  #: present: an older server that sends no policy reads as the retries-off
1237
1391
  #: policy with Harbor's defaults, exactly how such a server behaves.
1238
1392
  retry: JobRetryConfig
1393
+ #: The resolved embedded-analysis policy the job was created with; None
1394
+ #: when the create named none (a later manual :meth:`JobsClient.analyze`
1395
+ #: does not rewrite it — the job row states what the CREATE asked for,
1396
+ #: each analysis states what IT ran under). Always None on a regrade job.
1397
+ analyze: Optional[AnalyzeConfig]
1239
1398
  #: The RESOLVED timeout multipliers this job's phases arm under —
1240
1399
  #: Harbor's five flat JobConfig fields, echoed on every job body. The
1241
1400
  #: global one is always a number (1.0 when the create request named
@@ -1585,6 +1744,13 @@ class Trial:
1585
1744
  #: serve (:class:`evolve.results.UsageReading`). None = the meter never
1586
1745
  #: answered, never zero.
1587
1746
  usage: Optional[UsageReading] = None
1747
+ #: The trial's LATEST trace analysis (:class:`TrialAnalysis`); None when
1748
+ #: the trial has never been analyzed — never a fabricated empty object. A
1749
+ #: re-analysis (same job, different rubric or model) replaces what this
1750
+ #: field serves, matching Harbor, where a re-run overwrites the trial
1751
+ #: directory's analysis.json; earlier analyses stay stored as the audit
1752
+ #: record.
1753
+ analysis: Optional[TrialAnalysis] = None
1588
1754
 
1589
1755
 
1590
1756
  @dataclass
@@ -2404,6 +2570,12 @@ def _map_job(data: Dict[str, Any]) -> Job:
2404
2570
  # server that sends no policy reads as the retries-off policy, which
2405
2571
  # is exactly how such a server behaves; every field always present.
2406
2572
  retry=_map_retry_config(data.get('retry')),
2573
+ # The resolved embedded-analysis policy, or None: a create that named
2574
+ # none, and an older server that sends nothing, both mean "no
2575
+ # embedded analysis" — exactly what None states.
2576
+ analyze=(
2577
+ data['analyze'] if isinstance(data.get('analyze'), dict) else None
2578
+ ),
2407
2579
  # Timeout multipliers: an older server sends none — 1.0 / None reads
2408
2580
  # as "every phase at 1.0", exactly how such a server behaves.
2409
2581
  timeout_multiplier=(
@@ -2606,6 +2778,12 @@ def _map_trial(data: Dict[str, Any]) -> Trial:
2606
2778
  # older servers and on every non-judge trial — None either way.
2607
2779
  judge_result=_map_judge_result(data.get('judge_result')),
2608
2780
  judge_spend_source=data.get('judge_spend_source'),
2781
+ # The trial's LATEST trace analysis. Defensive like gpu_cost: absent
2782
+ # (an older server, or a never-analyzed trial) and malformed both
2783
+ # read None — "never analyzed", never a fabricated empty object.
2784
+ analysis=(
2785
+ data['analysis'] if isinstance(data.get('analysis'), dict) else None
2786
+ ),
2609
2787
  environment_setup=_map_timing(data.get('environment_setup')),
2610
2788
  agent_setup=_map_timing(data.get('agent_setup')),
2611
2789
  agent_execution=_map_timing(data.get('agent_execution')),
@@ -4320,6 +4498,7 @@ class JobsClient:
4320
4498
  max_trial_spend_usd: Optional[float] = None,
4321
4499
  sandbox_provider: Optional[str] = None,
4322
4500
  retry: Optional[JobRetryConfigInput] = None,
4501
+ analyze: Optional[AnalyzeConfigInput] = None,
4323
4502
  timeout_multiplier: Optional[float] = None,
4324
4503
  agent_timeout_multiplier: Optional[float] = None,
4325
4504
  verifier_timeout_multiplier: Optional[float] = None,
@@ -4359,7 +4538,15 @@ class JobsClient:
4359
4538
  ``'include_exceptions'`` has no such split: None, an omitted key,
4360
4539
  and the empty list ``[]`` all mean no filter — Harbor's include
4361
4540
  check treats the empty set exactly like None, so ``[]`` never means
4362
- "retry nothing". The five ``*timeout_multiplier`` arguments are
4541
+ "retry nothing". ``analyze`` arms the EMBEDDED trace-analysis
4542
+ trigger (Harbor's ``harbor analyze`` vocabulary, the spec's
4543
+ AnalyzeConfigInput): PRESENCE is the switch — each trial is analyzed
4544
+ server-side right after it settles (CANCELLED trials are skipped),
4545
+ ``{}`` means "all defaults" (claude-haiku-4-5, Harbor's default
4546
+ rubric), and the response echoes the RESOLVED policy as
4547
+ ``Job.analyze`` (:class:`AnalyzeConfig`); omitted, no embedded
4548
+ analysis runs and :meth:`analyze` remains the manual door. The five
4549
+ ``*timeout_multiplier`` arguments are
4363
4550
  Harbor's timeout knobs verbatim: ``timeout_multiplier`` multiplies
4364
4551
  every TASK-DECLARED timeout for this job's runs (default 1.0;
4365
4552
  values below 1 shrink), and each phase-specific one —
@@ -4431,6 +4618,8 @@ class JobsClient:
4431
4618
  body['sandbox_provider'] = sandbox_provider
4432
4619
  if retry is not None:
4433
4620
  body['retry'] = retry
4621
+ if analyze is not None:
4622
+ body['analyze'] = analyze
4434
4623
  if timeout_multiplier is not None:
4435
4624
  body['timeout_multiplier'] = timeout_multiplier
4436
4625
  if agent_timeout_multiplier is not None:
@@ -4916,6 +5105,110 @@ class JobsClient:
4916
5105
  )
4917
5106
  return _map_job(raw)
4918
5107
 
5108
+ async def analyze(
5109
+ self,
5110
+ id: str,
5111
+ *,
5112
+ model_name: Optional[str] = None,
5113
+ rubric: Optional[Rubric] = None,
5114
+ ) -> Job:
5115
+ """Analyze a terminal job's trial traces (rubric-driven, Harbor's
5116
+ ``harbor analyze``), server-side.
5117
+
5118
+ For each trial the analyzer agent reads the trial's Harbor-shape tree
5119
+ plus its original task and rules every rubric criterion, storing the
5120
+ result on the trial (``Trial.analysis``) and the aggregate on the job
5121
+ (``stats['analysis']``). THE RESPONSE IS THE JOB, its analyses
5122
+ enqueued — analyses are not a separate resource; follow them with
5123
+ :meth:`watch_analysis`, or poll the job's trials. This is also the
5124
+ RE-analysis path: calling again (same job, different rubric or
5125
+ model) runs a fresh wave once the previous one has settled. Both
5126
+ arguments omitted means the defaults: claude-haiku-4-5 over Harbor's
5127
+ default rubric (reward_hacking, task_specification). CANCELLED
5128
+ trials are never analyzed.
5129
+
5130
+ The server owns every acceptance refusal, surfaced typed:
5131
+ ``job_not_terminal``, ``invalid_rubric`` (unknown keys named, empty
5132
+ or duplicate criteria, bounds), ``invalid_input`` (off-roster
5133
+ model), ``analysis_already_running`` (one wave at a time),
5134
+ ``no_analyzable_trials`` (every trial CANCELLED).
5135
+ """
5136
+ body: Dict[str, Any] = {}
5137
+ if model_name is not None:
5138
+ body['model_name'] = model_name
5139
+ if rubric is not None:
5140
+ body['rubric'] = rubric
5141
+ raw = await self._http.request_json(
5142
+ f'/api/jobs/{urllib.parse.quote(id)}/analyze', method='POST', body=body
5143
+ )
5144
+ return _map_job(raw)
5145
+
5146
+ async def watch_analysis(
5147
+ self,
5148
+ id: str,
5149
+ *,
5150
+ on_stats: Optional[Callable[[Job], None]] = None,
5151
+ poll_interval_s: float = 2.0,
5152
+ timeout_s: Optional[float] = None,
5153
+ ) -> Job:
5154
+ """Follow a job's analysis wave to its settled end.
5155
+
5156
+ Analyses have no event stream — the contract's own words: poll the
5157
+ job's trials to watch them settle — so this polls :meth:`get` until
5158
+ ``stats['analysis']`` reports nothing pending, and returns the final
5159
+ job; the per-trial results then ride the job's trials
5160
+ (``Trial.analysis``). ``on_stats`` fires on every observed change of
5161
+ the analysis tally (including the first non-None one seen), with the
5162
+ job body the observation came from. A still-None tally is the
5163
+ enqueue race after an accepted :meth:`analyze` and is watched
5164
+ through, never misread as "never analyzed" — so on a job that was
5165
+ NEVER analyzed this polls indefinitely (until ``timeout_s``): call
5166
+ it after :meth:`analyze`, as the CLI always does. It is the MANUAL
5167
+ wave's companion, not the embedded trigger's: on a still-RUNNING job
5168
+ created with ``analyze``, ``n_pending`` can touch 0 between trial
5169
+ settles, so the watch can return before every trial has been
5170
+ analyzed.
5171
+
5172
+ ``timeout_s`` bounds the whole watch and raises
5173
+ :class:`TimeoutError`. A rate limit or transient outage mid-watch is
5174
+ a delay, not an outcome: a 429/503 sleeps the server's
5175
+ ``retry_after_sec`` and keeps watching.
5176
+ """
5177
+ if poll_interval_s <= 0:
5178
+ raise ValueError('poll_interval_s must be positive')
5179
+ deadline = time.monotonic() + timeout_s if timeout_s is not None else None
5180
+ last_tally: Optional['tuple[int, int, int]'] = None
5181
+ while True:
5182
+ try:
5183
+ job = await self.get(id)
5184
+ except EvolveAPIError as error:
5185
+ if error.status not in (429, 503):
5186
+ raise
5187
+ if deadline is not None and time.monotonic() >= deadline:
5188
+ raise TimeoutError(
5189
+ f'watch_analysis({id!r}) timed out after {timeout_s}s'
5190
+ ) from error
5191
+ await asyncio.sleep(
5192
+ max(error.retry_after_sec or 0.0, poll_interval_s)
5193
+ )
5194
+ continue
5195
+ analysis = job.stats.get('analysis')
5196
+ if isinstance(analysis, dict):
5197
+ tally = (
5198
+ int(analysis.get('n_completed', 0)),
5199
+ int(analysis.get('n_failed', 0)),
5200
+ int(analysis.get('n_pending', 0)),
5201
+ )
5202
+ if tally != last_tally:
5203
+ last_tally = tally
5204
+ if on_stats is not None:
5205
+ on_stats(job)
5206
+ if tally[2] == 0:
5207
+ return job
5208
+ if deadline is not None and time.monotonic() >= deadline:
5209
+ raise TimeoutError(f'watch_analysis({id!r}) timed out after {timeout_s}s')
5210
+ await asyncio.sleep(poll_interval_s)
5211
+
4919
5212
  async def download(
4920
5213
  self,
4921
5214
  id: str,
@@ -4926,11 +5219,16 @@ class JobsClient:
4926
5219
  standard job-directory layout (deterministic bytes).
4927
5220
 
4928
5221
  The archive extracts to ``job-<id>/`` with ``config.json``,
4929
- ``result.json`` (stats incl. ``pass_at_k``), and per trial its
4930
- ``config.json``, ``result.json``, ``agent/trajectory.json`` (the
4931
- normalized ATIF trajectory), ``agent/{stdout,stderr}.log``,
4932
- ``verifier/test-stdout.txt``, ``verifier/reward.json`` and
4933
- ``exception.txt`` — absent artifacts are absent files.
5222
+ ``lock.json``, ``result.json`` (stats incl. ``pass_at_k``) and
5223
+ ``job.log``, and per trial its ``config.json``, ``lock.json``,
5224
+ ``result.json`` (``step_results`` on multi-step trials),
5225
+ ``trial.log``, ``agent/trajectory.json`` (the normalized ATIF
5226
+ trajectory), ``agent/{stdout,stderr}.log``, ``agent/sessions/``,
5227
+ ``verifier/test-stdout.txt``, ``verifier/reward.json``, the raw
5228
+ ``verifier/reward.txt`` (only when the grader wrote one),
5229
+ ``steps/<name>/verifier/reward.json`` (multi-step trials only),
5230
+ ``exception.txt``, and ``artifacts/`` with its always-present
5231
+ ``manifest.json`` — absent artifacts are absent files.
4934
5232
 
4935
5233
  Returns the archive bytes — verified against the response's
4936
5234
  Content-Length and, when the server states one, its digest — or, when
@@ -5358,7 +5656,7 @@ class HostedEvolve:
5358
5656
 
5359
5657
  @property
5360
5658
  def jobs(self) -> JobsClient:
5361
- """Jobs: start, watch, compare, resume, retry, regrade, download."""
5659
+ """Jobs: start, watch, compare, resume, retry, regrade, analyze, download."""
5362
5660
  if self._jobs is None:
5363
5661
  self._jobs = JobsClient(self._config)
5364
5662
  return self._jobs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolve-sdk
3
- Version: 0.0.52.dev955
3
+ Version: 0.0.52.dev965
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.dev955"
7
+ version = "0.0.52.dev965"
8
8
  description = "Pythonic SDK for multi-agent orchestration in E2B sandboxes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"