feat: SkillOpt — benchmark-driven prompt optimization via Harbor + YAML surface - #1013
Open
osilkin98 wants to merge 43 commits into
Open
feat: SkillOpt — benchmark-driven prompt optimization via Harbor + YAML surface#1013osilkin98 wants to merge 43 commits into
osilkin98 wants to merge 43 commits into
Conversation
…ation loop Implements the core SkillOpt training loop as a standalone module at factory/skillopt/, mapping Microsoft Research's SkillOpt approach to the factory's benchmark infrastructure. The loop has 6 stages: - REFLECT: Per-trace LLM analysis via claude -p (reflect.py) - AGGREGATE: Consolidate reflections into edit proposals (aggregate.py) - SELECT: Textual learning rate to bound edit magnitude (select.py) - UPDATE: Apply edits to workflow prompt_template strings (update.py) - EVALUATE: Re-run benchmark and compare before/after scores (loop.py) - ACCEPT/REVERT: Keep improvements, revert regressions (loop.py) Entry point: python -m factory.skillopt --benchmark swebench \ --workflow factory/workflow/contributed/swebench/workflow.py \ --results-dir benchmarks/results/ Closes #1008 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ecture) Complete rewrite of factory/skillopt/ to match Microsoft's SkillOpt architecture. The old PoC optimized prompt_template strings in Python source; the new architecture optimizes SKILL.md files directly with minibatch reflection, hierarchical tree-structured merging, LLM-driven edit ranking, and a DL-style training loop with rejected-edit buffer. New modules: types.py, adapter.py, skill.py, gate.py, clip.py, trainer.py New dirs: adapters/ (swebench + stubs), prompts/ (6 analyst/merge/ranking prompts) Rewritten: reflect.py (minibatch), aggregate.py (hierarchical merge), __main__.py Deleted: models.py, select.py, update.py, loop.py Closes #1008 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The adapter was calling run-harbor.sh without the required --all or --task flags, and didn't support limiting the number of tasks. Now: - rollout() passes --all --limit N --timeout 7200 --preserve - build_train_env/build_eval_env return the limit count directly (instances come from the Harbor dataset, not a pre-populated list) - Results collected from benchmarks/results/ (written by run-harbor.sh cleanup) - Trace dumps fetched from Langfuse for the reflect phase via langfuse_client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sentrux Quality ReportAbsoluteDiff (vs base branch) |
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (66.22%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #1013 +/- ##
==========================================
- Coverage 88.98% 87.36% -1.62%
==========================================
Files 124 139 +15
Lines 14575 15840 +1265
==========================================
+ Hits 12969 13839 +870
- Misses 1606 2001 +395 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Covers types.py (model validation, strict/forbid), skill.py (all edit ops, protected regions, patch application), gate.py (score selection, accept/reject logic), clip.py (passthrough and truncation fallback), adapter.py (abstract enforcement), and trainer.py (construction, _compute_score). No external services required. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover all previously-untested modules with mocked LLM/subprocess calls: - reflect.py: fmt_trajectory, fmt_minibatch, _call_llm, _extract_json, _parse_raw_patch, run_error/success_analyst, run_minibatch_reflect - aggregate.py: merge_patches, _hierarchical_merge, _parse_patch - clip.py: LLM-driven ranking paths (success, bad JSON, truncation) - trainer.py: full train loop with mock adapter (accept/reject/checkpoint) - swebench.py: build_train/eval_env, _parse_jobs_dir, _collect_results, _extract_trace_ids, rollout error paths - __main__.py: arg parsing, _load_adapter, unknown benchmark error - adapter stubs: all 4 raise NotImplementedError - adapter.py: reflect delegation, setup noop Also fix Python 3.9 compat: add `from __future__ import annotations` to factory/worktree.py and tests/conftest.py for `str | None` union syntax. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In overfit mode (--overfit), the trainer evaluates candidate skills on the same task distribution used for training instead of a separate eval split, enabling deliberate overfitting to a fixed task set. Rejected edits are fed back into reflect prompts via step_buffer_context so the LLM avoids re-proposing them. --results-from loads pre-existing rollout results for the first step baseline, avoiding redundant Harbor runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SwebenchAdapter: add instances field, use --include-task-name instead of --limit when instances are pinned - run-harbor.sh: accept and forward --include-task-name flags to Harbor - Gate: add accept_ties param (>= for current_score in overfit mode) - Trainer: pass accept_ties=overfit, summarize rejected edits (cap 2000 chars) instead of dumping full patch text - CLI: add --instances flag for comma-separated instance IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…collision bugs Two bugs prevented SkillOpt from iterating on the same pinned tasks: 1. Harbor dedup: back-to-back runs could produce empty results because _find_latest_result_file() might pick a stale file from a prior run. 2. Result file collision: newest *-swebench-full.json wasn't necessarily from the current run. Fix: _clean_result_files() deletes all *-swebench-full.json before each Harbor invocation, and rollout() now logs an error with diagnostics (instances, returncode, stderr tail) when results are empty. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only task_prompt_* slot values are mutable. The reflect phase now shows prompt slots (not full SKILL.md) to the LLM analyst and parses SlotEdit proposals, converting them to Edit(op=replace) for the unchanged aggregate/clip/skill pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…AML surface is active The YAML slot values don't appear verbatim in the rendered SKILL.md because the renderer transforms them. Instead of apply_patch (text search/replace), load the Python workflow definition, override prompt_template slots with the new values, and call the deterministic renderer to regenerate the SKILL.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rejects edits where the line-level diff magnitude exceeds the learning rate, preventing oversized prompt mutations from bypassing the gate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nforcement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The adapter was dropping prompt_slots and prompt_slots_text kwargs,
causing the analyst prompt to show {{PROMPT_SLOTS}} literally instead
of the actual slot values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The optimizer was rewriting entire prompts (19-26 line changes) every step, exceeding the learning rate of 5 and causing 100% rejection rate. Now the analyst prompts include the LEARNING_RATE constraint explicitly so the LLM knows to make small incremental changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… pipeline The aggregate/clip LLM calls can subtly modify edit target text (whitespace, truncation), causing exact-match validation to reject valid prompt edits. Now uses prefix matching on the first 200 chars with stripped whitespace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Steps with magnitude=0 were running full Harbor eval rollouts on unchanged skills, then "accepting" phantom improvements from random batch variance. Now rejects immediately when no actual prompt change occurred. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ranking LLM was reconstructing Edit objects from scratch instead of passing through the originals, producing empty/garbled edits (magnitude=0). Now the ranking prompt asks for selected_indices (0-based) and clip picks edits from the original patch by index. Also skips eval when no actual prompt changes occurred (magnitude=0). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add yaml_to_workflow() and workflow_to_yaml() to yaml_surface.py for closing the SkillOpt loop: edited YAML annotations can now be converted back into Pydantic Workflow objects for the deterministic executor. Key design decisions: - yaml_to_workflow accepts optional workflow param for testing with unregistered workflows - _strip_exporter_suffix removes Read/Write lines appended by skill_export to prevent duplication on re-export - TYPE_CHECKING import for Workflow avoids circular imports Includes 7 comprehensive round-trip tests covering: no-edit identity, prompt edits, multi-line edge cases, real swebench workflow, gate nodes, selective multi-node edits, and structural field preservation.
Replace the stub with a working adapter mirroring SwebenchAdapter but with featurebench-specific differences: exact filter style (no glob prefix), *-featurebench-full.json result files, 'featurebench' benchmark name in rollout, 'feature_implementation' task type, and the workflow-featurebench SKILL.md path. Update the test to verify actual behavior instead of expecting NotImplementedError. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- benchmarks/searchqa/: dataset loader (HF + built-in sample), SQuAD-style EM/F1 evaluator, task template with <answer> tags, post-eval script - SearchQAFactoryCeo agent in factory_harbor_agent.py - searchqa case in config.sh benchmark configuration - factory/skillopt/adapters/searchqa.py: SearchQAAdapter for SkillOpt training (hard=EM, soft=F1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Minimal 3-node pipeline (study → builder → auto_merge) with a seed prompt matching SkillOpt's empty QA skill. Uses sonnet model with 300s timeout for fast answer extraction from search passages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Counter-based F1 - dataset.py: load from kyunghyuncho/search_qa (HuggingFace), export train/val/test splits as JSONL with [DOC] separators, keep SAMPLE_DATA fallback - workflow.py: simplify to single builder AgentNode (no study/auto_merge), timeout 120s, model sonnet - evaluator.py: Counter-based token F1, add sub_em metric, extract_answer falls back to last non-empty line - test_workflow.py: updated assertions for 1 node, 0 edges - Add searchqa to QA_EXEMPT_WORKFLOWS in test suites - Add README.md for contributed workflow
… loader The old source (kyunghyuncho/search_qa with raw_jeopardy config) fails due to a legacy loading script. Switch to lucadiliello/searchqa which has the data pre-formatted with [DOC] separators. Map fields directly (key->id, question, context, answers). Remove hardcoded sample data fallback and test split (dataset only has train + validation). Exported: 117,384 train + 16,980 val = 134,364 total items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Harbor dependency SearchQA is text-in/text-out QA — routing through Harbor containers was massive overkill. The adapter now loads JSONL datasets directly, calls `claude -p` in parallel via ThreadPoolExecutor, and scores with the inline evaluator (EM/F1). Also registers searchqa in __main__.py and adds --dataset-dir CLI arg. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t LLM calls The rendered SKILL.md wraps the prompt inside factory agent --task "..." blocks. For direct claude -p calls, we need just the QA skill text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add --model sonnet flag to _call_claude() so the student model is always sonnet regardless of ANTHROPIC_MODEL env var - Strip task instructions from workflow prompt_template to match SkillOpt's exact blank seed format - Move task instructions (answer format, <answer> tags) into the adapter's rollout prompt where they belong Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed writes set from builder node so skill_export doesn't append "Write output to:" metadata to the prompt_template. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SkillOpt's edit_budget limits the NUMBER of edit operations (append/replace/ delete/insert_after) per step, not the total lines changed. Our implementation was measuring line-level diff which is the wrong abstraction — a single "append a 5-line rule" is 1 operation but 5+ lines. The clip step already enforces max_edits at the operation level. Removed the redundant line-level magnitude check from the trainer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SkillOpt evaluates on the entire validation split at every gate decision. We were using batch_size (40) which made scores noisy (each question = 2.5%). Now passes env_num=0 to build_eval_env which returns the full val set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…adroom Sonnet baseline is 87.5% EM — too high for the optimizer to improve. Haiku baseline is ~75% EM, giving 25 points of headroom. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…izer - Replace generic code-agent analyst prompts with QA-specific versions that include failure type categories (rule_missing, rule_wrong, rule_ignored, answer_format, other) and QA analysis process - Enrich SearchQA adapter rollout results with question, gold_answers, prediction, and trace_dump in extras dict - Set fail_reason to 'EM=0: predicted X but expected Y' instead of empty - Update fmt_minibatch_trajectories to render question, predicted answer, and gold answers from extras for the analyst to reason about Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 implementation of longitudinal skill refinement that runs at epoch boundaries, comparing rollout performance of the same samples under consecutive skill versions to identify regressions and persistent failures. - Create factory/skillopt/slow_update.py with field manipulation (inject, extract, replace), comparison pair building, and LLM-driven optimizer call - Create factory/skillopt/prompts/slow_update.md adapted for QA optimization - Add SlowUpdateResult model to types.py - Integrate _run_slow_update_epoch into trainer.py training loop - Add --slow-update CLI flag to __main__.py - Add 12 tests covering field manipulation, comparison pairs, and formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generate Harbor task directories (task.toml, instruction.md, gold.json, Dockerfile, test.sh) from SearchQA JSONL splits so the benchmark can run through `factory workflow run searchqa` inside Harbor containers. - Add benchmarks/searchqa/generate_harbor_tasks.py script - Generate 400 train + 200 val Harbor tasks under benchmarks/searchqa-harbor/ - Update config.sh to use BENCH_LOCAL_PATH for searchqa - Update SearchQA workflow prompt with task scaffolding (read instruction, write answer) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generated 400 train + 200 val task directories with task.toml, instruction.md, Dockerfile, and test.sh verifier. Fixed network_mode and gold.json path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Harbor expects /workspace/reward.txt with a float score. The test.sh was only printing to stdout without writing the reward file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Harbor expects reward at /logs/verifier/reward.json, not /workspace/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…irect claude calls - Replace direct `claude -p` invocations with `run-harbor.sh searchqa --all` - Model implementation on SwebenchAdapter: _clean_result_files, _find_latest_result_file, _collect_results, _parse_jobs_dir helpers - Pass skill content via SEARCHQA_SKILL_B64 env var (base64-encoded) to avoid shell escaping issues with multiline content - Modify SearchQA workflow to decode SEARCHQA_SKILL_B64 and use as prompt_template, falling back to the hardcoded default - Add SEARCHQA_SKILL_B64 pass-through in run-harbor.sh via --ae flag - Set FACTORY_GIT_REF to current git commit so container installs from our branch - Remove _call_claude, _extract_skill_prompt, _extract_answer, _load_jsonl Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The SKILL.md frontmatter (---\nname:...) was being passed as a CLI arg to claude, which interpreted --- as a flag. Now extracts just the prompt text before base64-encoding for the container env var. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…/answer) The env var override was replacing the ENTIRE prompt including the infrastructure instructions (read task, write answer.txt). Now the env var only overrides the skill rules section; the fixed instructions are always appended. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- rollout() detects split from env_manager tuple (build_eval_env returns ("val", limit))
- SEARCHQA_SPLIT env var controls which Harbor task directory is used
- config.sh reads SEARCHQA_SPLIT (defaults to train)
- Results enriched with verifier predicted/gold answers for reflect phase
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements a SkillOpt-style training loop that optimizes agent prompt instructions via workflow YAML annotations, with benchmark execution through Harbor containers using
factory workflow run. Includes SearchQA as the reference benchmark, validated against Microsoft's upstream SkillOpt results.Architecture
The system has two loops:
Inner loop (benchmark execution via Harbor):
factory workflow run searchqaexecutes inside a Harbor containerSEARCHQA_SKILL_B64env var + appends fixed task instructionsreward.jsonOuter loop (SkillOpt optimizer):
Key components
Workflow YAML surface (
factory/skillopt/yaml_surface.py):task_prompt_*slots are mutable; graph structure is frozenyaml_to_workflow()converts edited YAML → valid Pydantic Workflow forfactory workflow runworkflow_to_yaml()for the reverse pathHarbor integration (
factory/skillopt/adapters/searchqa.py):run-harbor.sh searchqa --allwithSEARCHQA_SKILL_B64env varSEARCHQA_SPLITenv var switches between train/val Harbor task directoriesRolloutResultfor reflect phaseFACTORY_GIT_REFensures containers install from current branchSearchQA benchmark (
benchmarks/searchqa-harbor/):lucadiliello/searchqa(Jeopardy QA)task.toml+instruction.md+Dockerfile+test.shverifier<answer>tags, compares to gold with SQuAD-style EM normalization, writes/logs/verifier/reward.json/workspace/answer.txtSlow update (
factory/skillopt/slow_update.py):Mapping to InnerLoop/CycleAnalyzer proposal (PR #1051)
Our system maps to PR #1051's abstractions:
InnerLoop.step()adapter.rollout()→run-harbor.sh→factory workflow runin containerCycleRecord.score_endEvaluator.parse()reward.json+ verifiertest-stdout.txtdirectivesSEARCHQA_SKILL_B64env var (edited skill → container)CycleAnalyzer_collect_results()+_extract_verifier_outputs()from Harbor jobs dirThree extensions needed in PR #1051 for this use case:
InnerLoopneeds to supportrun-harbor.shalongsidefactory ceoTraining results
SearchQA (Haiku student, Opus optimizer, direct
claude -pbaseline):Harbor-based training (1 epoch, in progress): validates the full loop through
factory workflow runinside containers.Module structure
types.pyyaml_surface.pyadapter.pyadapters/swebench.pyadapters/searchqa.pyreflect.pyaggregate.pyclip.pyskill.pygate.pyslow_update.pytrainer.pyprompts/SearchQA Harbor benchmark
benchmarks/searchqa-harbor/{train,val}/benchmarks/searchqa/generate_harbor_tasks.pybenchmarks/searchqa/evaluator.pybenchmarks/factory_harbor_agent.pySearchQAFactoryCeoagent classfactory/workflow/contributed/searchqa/workflow.pyTest plan
ruff checkcleanmypy --strictcleanyaml_to_workflow()produces valid Workflow accepted byWorkflowExecutor🤖 Generated with Claude Code