synth-ai 0.2.13.dev2__py3-none-any.whl → 0.2.14__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (110) hide show
  1. examples/multi_step/configs/README_verilog_rl.md +77 -0
  2. examples/multi_step/configs/VERILOG_REWARDS.md +90 -0
  3. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +183 -0
  4. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  5. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  6. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +5 -4
  7. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  8. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  9. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  10. examples/multi_step/configs/verilog_rl_lora.toml +190 -0
  11. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  12. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  13. examples/multi_step/readme.md +48 -0
  14. examples/multi_step/verilog_rl_lora.md +218 -0
  15. examples/qwen_coder/configs/coder_lora_30b.toml +1 -1
  16. examples/sft/evaluate.py +2 -0
  17. examples/sft/generate_traces.py +2 -0
  18. examples/swe/task_app/grpo_swe_mini.py +1 -0
  19. examples/swe/task_app/hosted/rollout.py +2 -0
  20. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  21. examples/task_apps/crafter/CREATE_SFT_DATASET.md +273 -0
  22. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  23. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +174 -0
  24. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +268 -0
  25. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  26. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  27. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  28. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  29. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  30. examples/task_apps/crafter/task_app/__init__.py +3 -0
  31. examples/task_apps/crafter/task_app/grpo_crafter.py +306 -8
  32. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +10 -0
  33. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +16 -3
  34. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +17 -2
  35. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +25 -3
  36. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +52 -1
  37. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +111 -13
  38. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +156 -0
  39. examples/task_apps/enron/filter_sft.toml +5 -0
  40. examples/task_apps/enron/tests/__init__.py +2 -0
  41. examples/task_apps/enron/tests/integration/__init__.py +2 -0
  42. examples/task_apps/enron/tests/integration/test_enron_eval.py +2 -0
  43. examples/task_apps/enron/tests/unit/__init__.py +2 -0
  44. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  45. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  46. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +415 -0
  47. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +29 -0
  48. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +2 -0
  49. examples/task_apps/pokemon_red/task_app.py +199 -6
  50. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +2 -0
  51. examples/task_apps/sokoban/filter_sft.toml +5 -0
  52. examples/task_apps/sokoban/tests/__init__.py +2 -0
  53. examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
  54. examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
  55. examples/task_apps/verilog/eval_groq_qwen32b.toml +8 -4
  56. examples/task_apps/verilog/filter_sft.toml +5 -0
  57. examples/task_apps/verilog/task_app/grpo_verilog.py +258 -23
  58. examples/task_apps/verilog/tests/__init__.py +2 -0
  59. examples/task_apps/verilog/tests/integration/__init__.py +2 -0
  60. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +2 -0
  61. examples/task_apps/verilog/tests/unit/__init__.py +2 -0
  62. examples/warming_up_to_rl/groq_test.py +2 -0
  63. examples/warming_up_to_rl/run_local_rollout.py +2 -0
  64. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -0
  65. examples/warming_up_to_rl/run_local_rollout_parallel.py +2 -0
  66. examples/warming_up_to_rl/run_local_rollout_traced.py +2 -0
  67. examples/warming_up_to_rl/run_rollout_remote.py +2 -0
  68. synth_ai/api/models/supported.py +1 -0
  69. synth_ai/cli/__init__.py +46 -13
  70. synth_ai/cli/_modal_wrapper.py +3 -2
  71. synth_ai/cli/recent.py +1 -1
  72. synth_ai/cli/status.py +1 -1
  73. synth_ai/cli/task_apps.py +354 -143
  74. synth_ai/cli/traces.py +1 -1
  75. synth_ai/cli/tui.py +57 -0
  76. synth_ai/cli/turso.py +1 -1
  77. synth_ai/cli/watch.py +1 -1
  78. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  79. synth_ai/environments/examples/crafter_classic/environment.py +1 -1
  80. synth_ai/environments/examples/verilog/engine.py +76 -10
  81. synth_ai/judge_schemas.py +8 -8
  82. synth_ai/task/__init__.py +11 -1
  83. synth_ai/task/apps/__init__.py +1 -0
  84. synth_ai/task/config.py +257 -0
  85. synth_ai/task/contracts.py +15 -2
  86. synth_ai/task/rubrics/__init__.py +3 -0
  87. synth_ai/task/rubrics/loaders.py +22 -3
  88. synth_ai/task/rubrics/scoring.py +3 -0
  89. synth_ai/task/trace_correlation_helpers.py +315 -0
  90. synth_ai/task/validators.py +144 -0
  91. synth_ai/tracing_v3/abstractions.py +3 -3
  92. synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
  93. synth_ai/tracing_v3/session_tracer.py +16 -6
  94. synth_ai/tracing_v3/storage/base.py +29 -29
  95. synth_ai/tracing_v3/storage/config.py +3 -3
  96. synth_ai/tracing_v3/turso/daemon.py +8 -7
  97. synth_ai/tracing_v3/turso/native_manager.py +63 -40
  98. synth_ai/tracing_v3/utils.py +3 -3
  99. synth_ai/tui/__init__.py +5 -0
  100. synth_ai/tui/__main__.py +13 -0
  101. synth_ai/tui/cli/__init__.py +1 -0
  102. synth_ai/tui/cli/query_experiments.py +164 -0
  103. synth_ai/tui/cli/query_experiments_v3.py +164 -0
  104. synth_ai/tui/dashboard.py +906 -0
  105. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/METADATA +1 -1
  106. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/RECORD +110 -71
  107. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/WHEEL +0 -0
  108. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/entry_points.txt +0 -0
  109. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/licenses/LICENSE +0 -0
  110. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.14.dist-info}/top_level.txt +0 -0
@@ -34,6 +34,7 @@ from synth_ai.task.contracts import (
34
34
  from synth_ai.task.datasets import TaskDatasetRegistry, TaskDatasetSpec
35
35
  from synth_ai.task.rubrics import load_rubric
36
36
  from synth_ai.task.server import ProxyConfig, RubricBundle, TaskAppConfig
37
+ from synth_ai.task.validators import normalize_inference_url
37
38
  from synth_ai.task.tracing_utils import (
38
39
  build_tracer_factory,
39
40
  resolve_sft_output_dir,
@@ -45,7 +46,36 @@ from synth_ai.tracing_v3.session_tracer import SessionTracer
45
46
  logger = logging.getLogger(__name__)
46
47
 
47
48
  _HERE = Path(__file__).resolve()
48
- REPO_ROOT = _HERE.parents[4]
49
+
50
+
51
+ def _resolve_repo_root() -> Path:
52
+ """Find synth-ai repo root, checking env var and parent traversal."""
53
+ candidates: list[Path] = []
54
+ env_root = os.getenv("SYNTH_AI_REPO_ROOT")
55
+ if env_root:
56
+ candidates.append(Path(env_root).expanduser())
57
+
58
+ # Try Modal mount point
59
+ candidates.append(Path("/opt/synth_ai_repo"))
60
+
61
+ # Traverse up from current file
62
+ current = _HERE
63
+ for _ in range(6):
64
+ current = current.parent
65
+ candidates.append(current)
66
+ if (current / "synth_ai").is_dir() and (current / "examples").is_dir():
67
+ return current
68
+
69
+ # Return first existing candidate
70
+ for candidate in candidates:
71
+ if candidate.is_dir() and (candidate / "synth_ai").exists():
72
+ return candidate
73
+
74
+ # Fallback to current parent structure (may not work in Modal)
75
+ return _HERE.parent.parent.parent.parent
76
+
77
+
78
+ REPO_ROOT = _resolve_repo_root()
49
79
 
50
80
  DATASET_SPEC = TaskDatasetSpec(
51
81
  id="verilog_eval_v2",
@@ -161,23 +191,6 @@ def _base_task_info(dataset: VerilogDataset) -> TaskInfo:
161
191
  )
162
192
 
163
193
 
164
- def _normalize_inference_url(url: str | None) -> str:
165
- candidate = (url or DEFAULT_INFERENCE_URL).strip()
166
- if not candidate:
167
- candidate = DEFAULT_INFERENCE_URL
168
- if candidate.endswith("/v1/chat/completions"):
169
- return candidate
170
- if candidate.endswith("/chat/completions"):
171
- return candidate
172
- if candidate.endswith("/v1"):
173
- return f"{candidate.rstrip('/')}/chat/completions"
174
- if candidate.endswith("/v1/"):
175
- return f"{candidate.rstrip('/')}/chat/completions"
176
- if candidate.endswith("/chat"):
177
- return f"{candidate.rstrip('/')}/completions"
178
- if candidate.endswith("/chat/"):
179
- return f"{candidate.rstrip('/')}/completions"
180
- return f"{candidate.rstrip('/')}/v1/chat/completions"
181
194
 
182
195
 
183
196
  def _format_file_previews(files: dict[str, str]) -> str:
@@ -336,7 +349,7 @@ class VerilogLLMAgent:
336
349
  max_tokens: int,
337
350
  ) -> None:
338
351
  self.instructions = instructions.strip()
339
- self.inference_url = _normalize_inference_url(inference_url)
352
+ self.inference_url = normalize_inference_url(inference_url, default=DEFAULT_INFERENCE_URL)
340
353
  self.model = model or DEFAULT_MODEL
341
354
  self.temperature = temperature
342
355
  self.max_tokens = max_tokens
@@ -349,7 +362,16 @@ class VerilogLLMAgent:
349
362
  if not api_key:
350
363
  raise RuntimeError("GROQ_API_KEY is not configured for Verilog inference.")
351
364
  self.headers["Authorization"] = f"Bearer {api_key.strip()}"
352
- elif "openai" in lowered:
365
+ # If target is Synth backend (any deployment), use SYNTH_API_KEY
366
+ elif any(pattern in lowered for pattern in [
367
+ "synth-backend", "synth.run", "agent-learning",
368
+ "localhost:8000", "127.0.0.1:8000"
369
+ ]):
370
+ api_key = os.getenv("SYNTH_API_KEY")
371
+ if not api_key:
372
+ raise RuntimeError("SYNTH_API_KEY is not configured for Verilog inference with Synth backend.")
373
+ self.headers["Authorization"] = f"Bearer {api_key.strip()}"
374
+ elif "openai" in lowered or "api.openai.com" in lowered:
353
375
  api_key = os.getenv("OPENAI_API_KEY")
354
376
  if not api_key:
355
377
  raise RuntimeError("OPENAI_API_KEY is not configured for Verilog inference.")
@@ -574,6 +596,21 @@ async def rollout_executor(
574
596
  total_reward = 0.0
575
597
  final_observation: dict[str, Any] | None = None
576
598
  truncated_due_to_limit = False
599
+
600
+ # Log episode start
601
+ problem_id = getattr(instance, "problem_id", "unknown")
602
+ logger.info("=" * 80)
603
+ logger.info(f"[EPISODE START] run_id={request.run_id}")
604
+ logger.info(f" Problem ID: {problem_id}")
605
+ logger.info(f" Policy: {policy_id}")
606
+ logger.info(f" Model: {policy_model}")
607
+ logger.info(f" Max steps: {max_steps}")
608
+ logger.info(f" Temperature: {temperature}")
609
+ logger.info(f" Max tokens: {max_tokens}")
610
+ if instructions:
611
+ instructions_preview = instructions[:150] + "..." if len(instructions) > 150 else instructions
612
+ logger.info(f" Instructions: {instructions_preview}")
613
+ logger.info("=" * 80)
577
614
  code_dirty = False
578
615
  last_compile_success = False
579
616
  simulate_since_last_compile = False
@@ -648,7 +685,7 @@ async def rollout_executor(
648
685
  and not code_dirty
649
686
  )
650
687
  if skip_env_step:
651
- reward_last = -0.01
688
+ reward_last = 0.0 # No reward for blocked operations
652
689
  total_reward += reward_last
653
690
  current_observation = dict(current_observation)
654
691
  current_observation["reward_last"] = reward_last
@@ -669,6 +706,23 @@ async def rollout_executor(
669
706
  or current_observation.get("task_completed")
670
707
  )
671
708
  truncated_flag = bool(current_observation.get("truncated"))
709
+
710
+ # Log what the environment returned
711
+ print(f"\n{'='*80}")
712
+ print(f"[STEP {step_index}] TOOL CALL:")
713
+ print(f" Tool: {env_call.tool}")
714
+ print(f" Args: {env_call.args}")
715
+ print(f"\n[STEP {step_index}] ENVIRONMENT RESPONSE:")
716
+ print(f" Reward: {reward_last:.4f} (cumulative: {total_reward:.4f})")
717
+ print(f" Task completed: {step_observation.get('task_completed')}")
718
+ print(f" Done: {done_flag} | Truncated: {truncated_flag}")
719
+ if 'compile_status' in step_observation and step_observation.get('compile_status'):
720
+ print(f" Compile status:\n{step_observation.get('compile_status')}")
721
+ if 'simulate_status' in step_observation and step_observation.get('simulate_status'):
722
+ print(f" Simulate status:\n{step_observation.get('simulate_status')}")
723
+ if 'files' in step_observation:
724
+ print(f" Files: {list(step_observation.get('files', {}).keys())}")
725
+ print(f"{'='*80}\n")
672
726
 
673
727
  executed_tool_name = str(primary_call["tool"])
674
728
  normalized_executed_tool = executed_tool_name.strip().lower()
@@ -698,10 +752,40 @@ async def rollout_executor(
698
752
  {"tool_name": call["tool"], "arguments": call["args"]}
699
753
  for call in tool_calls
700
754
  ]
755
+
756
+ # Print tool calls for debugging
757
+ logger.info(f"[STEP {step_index}] Tool calls executed:")
758
+ for call in tool_calls:
759
+ tool_name = call["tool"]
760
+ args = call["args"]
761
+ # Truncate long arguments for readability
762
+ if "code" in args or "content" in args:
763
+ args_preview = {k: (v[:100] + "..." if isinstance(v, str) and len(v) > 100 else v)
764
+ for k, v in args.items()}
765
+ else:
766
+ args_preview = args
767
+ logger.info(f" └─ {tool_name}({args_preview})")
768
+
769
+ # Log reward details for debugging
770
+ logger.info(f"[STEP {step_index}] Reward details:")
771
+ logger.info(f" └─ reward_last: {reward_last:.4f}")
772
+ logger.info(f" └─ total_reward: {total_reward:.4f}")
773
+ logger.info(f" └─ skip_env_step: {skip_env_step}")
774
+ if not skip_env_step:
775
+ logger.info(f" └─ obs.task_completed: {current_observation.get('task_completed', False)}")
776
+ logger.info(f" └─ obs.compile_status: {current_observation.get('compile_status', 'N/A')}")
777
+ logger.info(f" └─ obs.simulate_status: {current_observation.get('simulate_status', 'N/A')}")
778
+ logger.info(f" └─ obs.terminated: {current_observation.get('terminated', False)}")
779
+ else:
780
+ logger.info(f" └─ (blocked operation - no env step)")
781
+
701
782
  step_info = {
702
783
  "assistant_message": assistant_text,
703
784
  "model_response": raw_response,
704
785
  "llm_request": request_payload,
786
+ "meta": {
787
+ "inference_url": policy_config.get("inference_url") or resolved_inference, # CRITICAL: Required by RL trainer for trace extraction (must have ?cid=...)
788
+ },
705
789
  }
706
790
  if override_info:
707
791
  step_info["auto_override"] = override_info
@@ -756,6 +840,9 @@ async def rollout_executor(
756
840
  "model_response": raw_response,
757
841
  "llm_request": request_payload,
758
842
  "error": error_text,
843
+ "meta": {
844
+ "inference_url": policy_config.get("inference_url") or resolved_inference, # CRITICAL: Required by RL trainer
845
+ },
759
846
  }
760
847
  steps.append(
761
848
  RolloutStep(
@@ -797,6 +884,25 @@ async def rollout_executor(
797
884
  },
798
885
  )
799
886
 
887
+ # Extract inference_url from policy config (REQUIRED for RL trace correlation)
888
+ # The trainer injects this with ?cid=trace_xxxxx parameter for trace linking
889
+ final_inference_url = policy_config.get("inference_url")
890
+ if not isinstance(final_inference_url, str) or not final_inference_url.strip():
891
+ # Fallback to agent's inference_url if not in policy config
892
+ final_inference_url = agent.inference_url
893
+ logger.warning(
894
+ "VERILOG_ROLLOUT: inference_url not found in policy_config, using agent.inference_url run_id=%s url=%s",
895
+ request.run_id,
896
+ final_inference_url,
897
+ )
898
+ else:
899
+ logger.info(
900
+ "VERILOG_ROLLOUT: using inference_url from policy_config run_id=%s url=%s has_cid=%s",
901
+ request.run_id,
902
+ final_inference_url,
903
+ "?cid=" in final_inference_url,
904
+ )
905
+
800
906
  trajectory = RolloutTrajectory(
801
907
  env_id=str(env_id),
802
908
  policy_id=str(policy_id),
@@ -810,11 +916,11 @@ async def rollout_executor(
810
916
  "total_reward": final_total_reward,
811
917
  "task_completed": bool(final_observation.get("task_completed")),
812
918
  "policy_model": policy_model,
813
- "inference_url": agent.inference_url,
919
+ "inference_url": final_inference_url,
814
920
  },
815
921
  },
816
922
  length=len(steps),
817
- inference_url=agent.inference_url, # NEW: Required for trace correlation
923
+ inference_url=final_inference_url, # CRITICAL: Must contain ?cid=... for trace correlation
818
924
  decision_samples=None,
819
925
  )
820
926
 
@@ -836,6 +942,133 @@ async def rollout_executor(
836
942
  }
837
943
  }
838
944
 
945
+ # Build pipeline_metadata (required for RL training)
946
+ pipeline_metadata = {
947
+ "reward_score": final_total_reward,
948
+ "policy_id": policy_id,
949
+ "inference_url": final_inference_url, # CRITICAL: Must be at top level for RL trainer (expects ?cid=...)
950
+ "inference": {
951
+ "provider": "groq",
952
+ "model": policy_model,
953
+ "url": final_inference_url, # Use final_inference_url (has ?cid=...)
954
+ },
955
+ "env_name": env_id,
956
+ "task_id": getattr(instance, "problem_id", None),
957
+ "task_split": getattr(instance, "split", "val"),
958
+ }
959
+
960
+ # Log episode summary with reward breakdown
961
+ compile_status = final_observation.get("compile_status", "N/A")
962
+ simulate_status = final_observation.get("simulate_status", "N/A")
963
+ task_completed = bool(final_observation.get("task_completed", False))
964
+
965
+ logger.info("=" * 80)
966
+ logger.info(f"[EPISODE COMPLETE] run_id={request.run_id}")
967
+ logger.info(f" Steps taken: {len(steps)}")
968
+ logger.info(f" Total reward: {final_total_reward:.3f}")
969
+ logger.info(f" Task completed: {task_completed}")
970
+ logger.info(f" Compile status: {compile_status}")
971
+ logger.info(f" Simulate status: {simulate_status}")
972
+ logger.info(f" Done/Truncated: {final_done}/{final_truncated}")
973
+ logger.info(f" Problem ID: {getattr(instance, 'problem_id', 'N/A')}")
974
+
975
+ # DEBUG: Log each step's reward for RL debugging
976
+ print(f"\n[REWARD DEBUG] Step-by-step breakdown:")
977
+ for idx, step in enumerate(steps):
978
+ print(f" Step {idx}: reward={step.reward:.4f} tool_calls={[tc.get('tool_name') for tc in step.tool_calls]}")
979
+ print(f"[REWARD DEBUG] Final observation keys: {list(final_observation.keys())}")
980
+ print(f"[REWARD DEBUG] Final obs total_reward: {final_observation.get('total_reward')}")
981
+ print(f"[REWARD DEBUG] Metrics outcome_score: {metrics.outcome_score}")
982
+ print(f"[REWARD DEBUG] Metrics mean_return: {metrics.mean_return}")
983
+
984
+ # Reward breakdown for debugging
985
+ logger.info("\n[REWARD BREAKDOWN]")
986
+ compile_count = sum(1 for s in steps if any(tc.get("tool_name") == "compile" for tc in s.tool_calls))
987
+ simulate_count = sum(1 for s in steps if any(tc.get("tool_name") == "simulate" for tc in s.tool_calls))
988
+ submit_count = sum(1 for s in steps if any(tc.get("tool_name") == "submit" for tc in s.tool_calls))
989
+ write_count = sum(1 for s in steps if any(tc.get("tool_name") == "write_file" for tc in s.tool_calls))
990
+
991
+ logger.info(f" Tool usage: write_file={write_count}, compile={compile_count}, simulate={simulate_count}, submit={submit_count}")
992
+
993
+ # Show per-step rewards
994
+ step_rewards = [s.reward for s in steps]
995
+ nonzero_rewards = [r for r in step_rewards if r != 0.0]
996
+ logger.info(f" Step rewards: {step_rewards}")
997
+ if nonzero_rewards:
998
+ logger.info(f" Non-zero rewards: {nonzero_rewards}")
999
+ else:
1000
+ logger.info(f" ⚠️ ALL REWARDS ZERO! Possible reasons:")
1001
+ logger.info(f" - No successful compiles (compile reward = 0.01)")
1002
+ logger.info(f" - No successful simulations (simulate reward = 0.1)")
1003
+ logger.info(f" - No successful submits (submit reward = 1.0)")
1004
+ logger.info(f" - Check if task_completed={task_completed}")
1005
+ logger.info(f" - Check compile_status='{compile_status}'")
1006
+ logger.info(f" - Check simulate_status='{simulate_status}'")
1007
+ logger.info("=" * 80)
1008
+
1009
+ # Log for debugging RL training
1010
+ logger.info(
1011
+ "VERILOG_ROLLOUT: pipeline_metadata run_id=%s reward=%.3f inference_url=%s",
1012
+ request.run_id,
1013
+ final_total_reward,
1014
+ final_inference_url,
1015
+ )
1016
+
1017
+ # DEBUG: Log what we're returning to the RL trainer
1018
+ print(f"\n[RETURN DEBUG] Trajectory structure being returned:")
1019
+ print(f" trajectory.steps count: {len(steps)}")
1020
+ print(f" trajectory.final.reward: {trajectory.final.get('reward') if trajectory.final else 'None'}")
1021
+ print(f" trajectory.length: {trajectory.length}")
1022
+ print(f" metrics.outcome_score: {metrics.outcome_score}")
1023
+ print(f" metrics.mean_return: {metrics.mean_return}")
1024
+ print(f" metrics.episode_returns: {metrics.episode_returns}")
1025
+ print(f" pipeline_metadata.reward_score: {pipeline_metadata.get('reward_score')}")
1026
+
1027
+ # ASSERTIONS: Validate RL-required fields before returning
1028
+ # These catch structural issues early (before they reach the backend trainer)
1029
+ # Only enforce for RL mode, not EVAL mode
1030
+ is_rl_mode = hasattr(request, 'mode') and str(getattr(request, 'mode', '')).lower() == 'rl'
1031
+
1032
+ assert isinstance(pipeline_metadata, dict), (
1033
+ f"VERILOG_ROLLOUT_VALIDATION: pipeline_metadata must be dict, got {type(pipeline_metadata).__name__}"
1034
+ )
1035
+ assert "inference_url" in pipeline_metadata, (
1036
+ f"VERILOG_ROLLOUT_VALIDATION: pipeline_metadata missing 'inference_url' (REQUIRED for RL training)"
1037
+ )
1038
+ assert isinstance(pipeline_metadata["inference_url"], str), (
1039
+ f"VERILOG_ROLLOUT_VALIDATION: pipeline_metadata['inference_url'] must be string, got {type(pipeline_metadata['inference_url']).__name__}"
1040
+ )
1041
+ # Only require ?cid= for RL mode (not needed for EVAL)
1042
+ if is_rl_mode:
1043
+ assert "?cid=" in pipeline_metadata["inference_url"], (
1044
+ f"VERILOG_ROLLOUT_VALIDATION: pipeline_metadata['inference_url'] must contain '?cid=' for trace correlation in RL mode. "
1045
+ f"Got: {pipeline_metadata['inference_url'][:100]}"
1046
+ )
1047
+
1048
+ # Validate each step has meta.inference_url (backend expects this nested structure)
1049
+ for step_idx, step in enumerate(steps):
1050
+ step_dict = step if isinstance(step, dict) else (step.model_dump() if hasattr(step, "model_dump") else {})
1051
+ step_info = step_dict.get("info", {})
1052
+ assert isinstance(step_info, dict), (
1053
+ f"VERILOG_ROLLOUT_VALIDATION: step[{step_idx}].info must be dict, got {type(step_info).__name__}"
1054
+ )
1055
+ step_meta = step_info.get("meta", {})
1056
+ assert isinstance(step_meta, dict), (
1057
+ f"VERILOG_ROLLOUT_VALIDATION: step[{step_idx}].info.meta must be dict, got {type(step_meta).__name__}"
1058
+ )
1059
+ assert "inference_url" in step_meta, (
1060
+ f"VERILOG_ROLLOUT_VALIDATION: step[{step_idx}].info.meta missing 'inference_url' (REQUIRED for RL training)"
1061
+ )
1062
+ assert isinstance(step_meta["inference_url"], str), (
1063
+ f"VERILOG_ROLLOUT_VALIDATION: step[{step_idx}].info.meta['inference_url'] must be string, got {type(step_meta['inference_url']).__name__}"
1064
+ )
1065
+
1066
+ logger.info(
1067
+ "VERILOG_ROLLOUT_VALIDATION: ✓ All RL-required fields present run_id=%s steps=%d",
1068
+ request.run_id,
1069
+ len(steps),
1070
+ )
1071
+
839
1072
  return RolloutResponse(
840
1073
  run_id=request.run_id,
841
1074
  trajectories=[trajectory],
@@ -844,6 +1077,7 @@ async def rollout_executor(
844
1077
  aborted=False,
845
1078
  ops_executed=len(steps),
846
1079
  trace=trace_payload,
1080
+ pipeline_metadata=pipeline_metadata,
847
1081
  )
848
1082
 
849
1083
 
@@ -917,6 +1151,7 @@ register_task_app(
917
1151
  "python-dotenv>=1.0.1",
918
1152
  "datasets>=2.10.0",
919
1153
  ),
1154
+ apt_packages=("iverilog",), # Icarus Verilog compiler and simulator (provides iverilog and vvp)
920
1155
  extra_local_dirs=(
921
1156
  (str(REPO_ROOT), "/opt/synth_ai_repo"),
922
1157
  (str(REPO_ROOT / "synth_ai"), "/opt/synth_ai_repo/synth_ai"),
@@ -1,2 +1,4 @@
1
1
  # Verilog task app tests
2
2
 
3
+
4
+
@@ -1,2 +1,4 @@
1
1
  # Integration tests for Verilog task app
2
2
 
3
+
4
+
@@ -177,3 +177,5 @@ def test_verilog_eval_with_groq(verilog_server: str) -> None:
177
177
  # Check that we got a meaningful outcome score
178
178
  assert "outcome" in result.stdout.lower() or "mean_return" in result.stdout.lower()
179
179
 
180
+
181
+
@@ -1,2 +1,4 @@
1
1
  # Unit tests for Verilog task app
2
2
 
3
+
4
+
@@ -47,8 +47,10 @@ async def run(args: argparse.Namespace) -> None:
47
47
 
48
48
  inference_url = args.inference_url or f"{args.base_url.rstrip('/')}/proxy/groq"
49
49
 
50
+ from synth_ai.task.contracts import RolloutMode
50
51
  request = RolloutRequest(
51
52
  run_id=args.run_id,
53
+ mode=RolloutMode.EVAL,
52
54
  env=RolloutEnvSpec(env_name="crafter", seed=args.seed, config={"seed": args.seed}),
53
55
  policy=RolloutPolicySpec(
54
56
  policy_name="groq-smoke",
@@ -42,8 +42,10 @@ def build_rollout_request(
42
42
  trace_format=trace_format,
43
43
  return_trace=return_trace,
44
44
  )
45
+ from synth_ai.task.contracts import RolloutMode
45
46
  return RolloutRequest(
46
47
  run_id=run_id,
48
+ mode=RolloutMode.EVAL,
47
49
  env=RolloutEnvSpec(env_name="crafter", seed=seed, config={}),
48
50
  policy=RolloutPolicySpec(policy_name="crafter-react", config=policy_config),
49
51
  ops=ops,
@@ -33,12 +33,14 @@ def build_rollout_request(
33
33
  "Authorization": f"Bearer {api_key}",
34
34
  },
35
35
  }
36
+ from synth_ai.task.contracts import RolloutMode
36
37
  return RolloutRequest(
37
38
  run_id=run_id,
38
39
  env=RolloutEnvSpec(env_name="crafter", seed=seed, config={}),
39
40
  policy=RolloutPolicySpec(policy_name="crafter-react", config=policy_config),
40
41
  ops=ops,
41
42
  record=RolloutRecordConfig(trajectories=True),
43
+ mode=RolloutMode.EVAL,
42
44
  on_done="reset",
43
45
  safety=RolloutSafetyConfig(),
44
46
  )
@@ -46,12 +46,14 @@ def build_rollout_request(
46
46
  trace_format=trace_format,
47
47
  return_trace=return_trace,
48
48
  )
49
+ from synth_ai.task.contracts import RolloutMode
49
50
  return RolloutRequest(
50
51
  run_id=run_id,
51
52
  env=RolloutEnvSpec(env_name="crafter", seed=seed, config={}),
52
53
  policy=RolloutPolicySpec(policy_name="crafter-react", config=policy_config),
53
54
  ops=ops,
54
55
  record=record_cfg,
56
+ mode=RolloutMode.EVAL,
55
57
  on_done="reset",
56
58
  safety=RolloutSafetyConfig(),
57
59
  )
@@ -53,12 +53,14 @@ def build_rollout_request(
53
53
  trace_format=trace_format,
54
54
  )
55
55
 
56
+ from synth_ai.task.contracts import RolloutMode
56
57
  return RolloutRequest(
57
58
  run_id=run_id,
58
59
  env=RolloutEnvSpec(env_name="crafter", seed=seed, config={}),
59
60
  policy=RolloutPolicySpec(policy_name="crafter-react", config=policy_config),
60
61
  ops=ops,
61
62
  record=record,
63
+ mode=RolloutMode.EVAL,
62
64
  on_done="reset",
63
65
  safety=RolloutSafetyConfig(),
64
66
  )
@@ -60,12 +60,14 @@ def build_request(
60
60
  for _ in range(max(llm_calls, 1)):
61
61
  ops.extend(["agent", "env"])
62
62
 
63
+ from synth_ai.task.contracts import RolloutMode
63
64
  return RolloutRequest(
64
65
  run_id=run_id,
65
66
  env=RolloutEnvSpec(env_name="crafter", seed=seed, config={}),
66
67
  policy=RolloutPolicySpec(policy_name="crafter-react", config=policy_config),
67
68
  ops=ops,
68
69
  record=RolloutRecordConfig(trajectories=True),
70
+ mode=RolloutMode.EVAL,
69
71
  on_done="reset",
70
72
  safety=RolloutSafetyConfig(),
71
73
  )
@@ -36,6 +36,7 @@ QWEN3_CODER_MODELS: list[str] = [
36
36
  # Training support sets
37
37
  RL_SUPPORTED_MODELS: frozenset[str] = frozenset(
38
38
  {
39
+ "Qwen/Qwen3-0.6B",
39
40
  "Qwen/Qwen3-1.7B",
40
41
  "Qwen/Qwen3-4B",
41
42
  "Qwen/Qwen3-4B-Thinking-2507",
synth_ai/cli/__init__.py CHANGED
@@ -22,26 +22,26 @@ except Exception:
22
22
  pass
23
23
 
24
24
  try:
25
- from ._typer_patch import patch_typer_make_metavar
25
+ from synth_ai.cli._typer_patch import patch_typer_make_metavar
26
26
 
27
27
  patch_typer_make_metavar()
28
28
  except Exception:
29
29
  pass
30
30
 
31
31
 
32
- from .root import cli # new canonical CLI entrypoint
32
+ from synth_ai.cli.root import cli # new canonical CLI entrypoint
33
33
 
34
34
  # Register subcommands from this package onto the group
35
35
  # Deprecated/legacy commands intentionally not registered: watch/experiments, balance, calc,
36
36
  # man, recent, status, traces
37
37
  try:
38
- from . import demo as _demo
38
+ from synth_ai.cli import demo as _demo
39
39
 
40
40
  _demo.register(cli)
41
41
  except Exception:
42
42
  pass
43
43
  try:
44
- from . import turso as _turso
44
+ from synth_ai.cli import turso as _turso
45
45
 
46
46
  _turso.register(cli)
47
47
  except Exception:
@@ -54,20 +54,53 @@ except Exception:
54
54
  pass
55
55
 
56
56
 
57
- from .task_apps import task_app_group
58
-
59
- cli.add_command(task_app_group, name="task-app")
57
+ # Import task_app_group conditionally
58
+ try:
59
+ from synth_ai.cli.task_apps import task_app_group
60
+ cli.add_command(task_app_group, name="task-app")
61
+ except Exception:
62
+ # Task app functionality not available
63
+ pass
60
64
 
61
65
 
62
66
  try:
63
- from . import task_apps as _task_apps
67
+ # Make task_apps import more robust to handle missing optional dependencies
68
+ import importlib
69
+ task_apps_module = importlib.import_module('synth_ai.cli.task_apps')
70
+ task_apps_module.register(cli)
71
+ except (ImportError, ModuleNotFoundError, TypeError, RuntimeError) as e:
72
+ # Task apps module not available (missing optional dependencies)
73
+ # This is expected - silently skip
74
+ pass
64
75
 
65
- _task_apps.register(cli)
76
+ # Register TUI command - make import completely isolated
77
+ def _register_tui_command():
78
+ """Register TUI command only when called, not during CLI startup."""
79
+ try:
80
+ # Import TUI only when the command is actually used
81
+ from synth_ai.cli.tui import register as tui_register
82
+ tui_register(cli)
83
+ except Exception:
84
+ # TUI not available - this is expected if dependencies are missing
85
+ pass
86
+
87
+ # Add TUI command as a lazy-registered command
88
+ try:
89
+ # Try to import and register immediately for normal cases
90
+ from synth_ai.cli.tui import register as tui_register
91
+ tui_register(cli)
66
92
  except Exception:
93
+ # If that fails, add a lazy registration that will only happen when called
94
+ # For now, just skip - the command won't be available but CLI won't crash
67
95
  pass
68
96
 
69
- cli.add_command(task_app_group.commands["serve"], name="serve")
70
- cli.add_command(task_app_group.commands["deploy"], name="deploy")
71
-
72
- cli.add_command(task_app_group.commands["modal-serve"], name="modal-serve")
97
+ # Add task app commands if available
98
+ try:
99
+ if 'task_app_group' in locals() and hasattr(task_app_group, 'commands'):
100
+ cli.add_command(task_app_group.commands["serve"], name="serve")
101
+ cli.add_command(task_app_group.commands["deploy"], name="deploy")
102
+ cli.add_command(task_app_group.commands["modal-serve"], name="modal-serve")
103
+ except Exception:
104
+ # Task app commands not available
105
+ pass
73
106
  # Top-level 'info' alias removed; use `synth-ai task-app info` instead
@@ -6,7 +6,7 @@ import sys
6
6
  def main() -> int:
7
7
  # Apply Typer compatibility patch before Modal CLI bootstraps Click/Typer internals.
8
8
  try:
9
- from ._typer_patch import patch_typer_make_metavar
9
+ from synth_ai.cli._typer_patch import patch_typer_make_metavar
10
10
 
11
11
  patch_typer_make_metavar()
12
12
  except Exception:
@@ -20,7 +20,8 @@ def main() -> int:
20
20
  else:
21
21
  sys.argv = ["modal"]
22
22
 
23
- return modal_main()
23
+ result = modal_main()
24
+ return result if result is not None else 0
24
25
 
25
26
 
26
27
  if __name__ == "__main__":
synth_ai/cli/recent.py CHANGED
@@ -12,7 +12,7 @@ from rich import box
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
- from ._storage import load_storage
15
+ from synth_ai.cli._storage import load_storage
16
16
 
17
17
  if TYPE_CHECKING: # pragma: no cover - typing only
18
18
  import pandas as pd
synth_ai/cli/status.py CHANGED
@@ -12,7 +12,7 @@ from rich.console import Console
12
12
  from rich.panel import Panel
13
13
  from rich.table import Table
14
14
 
15
- from ._storage import load_storage
15
+ from synth_ai.cli._storage import load_storage
16
16
 
17
17
 
18
18
  async def _db_stats(db_url: str) -> dict: