-
Notifications
You must be signed in to change notification settings - Fork 21
feat: add autoharness worker #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
2ba8bd6
feat: add autoharness worker — self-improving agent harness
rohitg00 f75fe0c
chore: add Apache-2.0 license to autoharness
rohitg00 b5c57ec
fix: use CARGO_PKG_VERSION in manifest test instead of hardcoded string
rohitg00 e122b4d
chore: remove redundant task Dockerfiles that only inherit base image
rohitg00 e2fa91c
chore: move LICENSE to repo root
rohitg00 1aa86c9
docs: rewrite README for autoharness — fix naming, remove stale refer…
rohitg00 7e0adfa
docs: add one-liner description below heading
rohitg00 c73712c
Merge branch 'main' into feat/autoharness
rohitg00 6644340
chore(autoharness): bump iii-sdk to >=0.11.0.dev9
rohitg00 420379a
chore(autoharness): bump iii-sdk to ==0.11.3
rohitg00 0eafd0e
fix(autoharness): CodeRabbit review follow-up
rohitg00 8d421ac
Delete LICENSE
rohitg00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| .git | ||
| data/ | ||
| tasks/*/logs/ | ||
| node_modules/ | ||
| target/ | ||
| __pycache__ | ||
| *.pyc | ||
| .env | ||
| workers/ |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| data/ | ||
| node_modules/ | ||
| __pycache__/ | ||
| *.pyc | ||
| .env | ||
| target/ | ||
| tasks/*/logs/ | ||
| .agent/ |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim | ||
|
|
||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| git \ | ||
| ca-certificates \ | ||
| curl \ | ||
| build-essential \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| nodejs \ | ||
| npm \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| RUN groupadd -g 1000 taskgroup && \ | ||
| useradd -u 1000 -g taskgroup -m taskuser && \ | ||
| mkdir -p /task/logs && \ | ||
| chown -R taskuser:taskgroup /task | ||
|
|
||
| WORKDIR /task | ||
| USER taskuser | ||
| ENV HOME=/home/taskuser | ||
|
|
||
| CMD ["sleep", "infinity"] |
Large diffs are not rendered by default.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| """ | ||
| autoharness — the agent harness file that the meta-agent modifies. | ||
|
|
||
| Everything above the FIXED ADAPTER line is fair game for the meta-agent. | ||
| Everything below is the Harbor integration and must not be modified | ||
| unless a human explicitly requests it. | ||
| """ | ||
|
|
||
| import os | ||
| import json | ||
| import asyncio | ||
| import subprocess | ||
| from agents import Agent, Runner, function_tool | ||
| from agents.run import RunResult | ||
|
|
||
|
|
||
| # ========================== EDITABLE SECTION ========================== | ||
|
|
||
| SYSTEM_PROMPT = """\ | ||
| You are an autonomous coding agent. You have access to a shell tool that | ||
| lets you run any command in the task container. | ||
|
|
||
| Approach: | ||
| 1. Read the task instruction carefully. | ||
| 2. Explore the environment (ls, cat files, check language/framework). | ||
| 3. Plan your approach before writing code. | ||
| 4. Implement the solution step by step. | ||
| 5. Verify your work by running tests or checking output. | ||
| 6. If something fails, read the error, diagnose, and fix. | ||
|
|
||
| Rules: | ||
| - Do not ask for help. You are autonomous. | ||
| - Do not give up. Try alternative approaches. | ||
| - Verify your solution before finishing. | ||
| """ | ||
|
|
||
| MODEL = "gpt-5" | ||
| MAX_TURNS = 30 | ||
|
|
||
|
|
||
| @function_tool | ||
| def run_shell(command: str) -> str: | ||
| """Execute a shell command and return combined stdout+stderr.""" | ||
| try: | ||
| task_cwd = os.environ.get("TASK_DIR", "/task") | ||
| result = subprocess.run( | ||
| command, shell=True, capture_output=True, text=True, timeout=120, | ||
| cwd=task_cwd, | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| output = result.stdout + result.stderr | ||
| total_tokens = 0 | ||
| estimated_cost = 0.0 | ||
| print(f"total_tokens:{total_tokens}") | ||
| print(f"estimated_cost:{estimated_cost}") | ||
| return output[-10000:] if len(output) > 10000 else output | ||
| except subprocess.TimeoutExpired: | ||
| return "ERROR: command timed out after 120 seconds" | ||
|
|
||
|
|
||
| def create_tools(): | ||
| return [run_shell] | ||
|
|
||
|
|
||
| def create_agent(): | ||
| return Agent( | ||
| name="harness-agent", | ||
| instructions=SYSTEM_PROMPT, | ||
| model=MODEL, | ||
| tools=create_tools(), | ||
| ) | ||
|
|
||
|
|
||
| async def run_task(instruction: str) -> RunResult: | ||
| agent = create_agent() | ||
| result = await Runner.run(agent, instruction, max_turns=MAX_TURNS) | ||
| return result | ||
|
|
||
|
|
||
| # --- FIXED ADAPTER BELOW --- do not modify unless human requests --- | ||
|
|
||
|
|
||
| def to_atif(result: RunResult, duration: float, instruction: str) -> dict: | ||
| steps = [] | ||
| for item in result.raw_responses: | ||
| step = { | ||
| "action": {"type": "message"}, | ||
| "observation": "", | ||
| } | ||
| if hasattr(item, "output"): | ||
| for output in item.output: | ||
| if hasattr(output, "type"): | ||
| if output.type == "function_call": | ||
| step["action"] = { | ||
| "type": "tool_call", | ||
| "tool": output.name, | ||
| "input": output.arguments, | ||
| } | ||
| elif output.type == "message": | ||
| step["observation"] = getattr(output, "content", "") | ||
| steps.append(step) | ||
|
|
||
| return { | ||
| "version": "atif-v1.6", | ||
| "steps": steps, | ||
| "metrics": { | ||
| "duration_seconds": round(duration, 1), | ||
| "turns": len(result.raw_responses), | ||
| "final_output": result.final_output[:2000] if result.final_output else "", | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| class HarnessAgent: | ||
| """Harbor BaseAgent adapter.""" | ||
|
|
||
| async def run(self, task_path: str) -> dict: | ||
| import time | ||
|
|
||
| instruction_file = os.path.join(task_path, "instruction.md") | ||
| with open(instruction_file) as f: | ||
| instruction = f.read() | ||
|
|
||
| # run_shell reads TASK_DIR from the environment to chdir into the | ||
| # task workspace. Propagate task_path here so `ls`, `cat`, test | ||
| # commands, and file writes hit the task dir even when HarnessAgent | ||
| # is invoked from an arbitrary working directory. | ||
| os.environ["TASK_DIR"] = task_path | ||
|
|
||
| start = time.time() | ||
| result = await run_task(instruction) | ||
| duration = time.time() - start | ||
|
|
||
| trajectory = to_atif(result, duration, instruction) | ||
|
|
||
| logs_dir = os.path.join(task_path, "logs", "agent") | ||
| os.makedirs(logs_dir, exist_ok=True) | ||
| with open(os.path.join(logs_dir, "trajectory.json"), "w") as f: | ||
| json.dump(trajectory, f, indent=2) | ||
|
|
||
| return trajectory | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import time | ||
|
|
||
| task_dir = os.environ.get("TASK_DIR", "/task") | ||
| instruction_file = os.path.join(task_dir, "instruction.md") | ||
|
|
||
| with open(instruction_file) as f: | ||
| instruction = f.read() | ||
|
|
||
| start = time.time() | ||
| result = asyncio.run(run_task(instruction)) | ||
| duration = time.time() - start | ||
|
|
||
| trajectory = to_atif(result, duration, instruction) | ||
|
|
||
| logs_dir = os.path.join(task_dir, "logs", "agent") | ||
| os.makedirs(logs_dir, exist_ok=True) | ||
| with open(os.path.join(logs_dir, "trajectory.json"), "w") as f: | ||
| json.dump(trajectory, f, indent=2) | ||
|
|
||
| print(json.dumps(trajectory["metrics"], indent=2)) | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| #!/bin/bash | ||
| set -euo pipefail | ||
|
|
||
| API="http://localhost:3111" | ||
| TAG="${1:-$(date +%b%d | tr '[:upper:]' '[:lower:]')}" | ||
|
|
||
| echo "==========================================" | ||
| echo " autoharness benchmark runner" | ||
| echo " tag: $TAG" | ||
| echo "==========================================" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 1. Check prerequisites | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| check() { | ||
| if ! command -v "$1" &>/dev/null; then | ||
| echo "ERROR: $1 not found. Install it first." | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| check iii | ||
| check curl | ||
| check jq | ||
|
|
||
| if ! curl -sf "$API/api/report/tags" >/dev/null 2>&1; then | ||
| echo "" | ||
| echo "iii-engine + orchestrator not running. Starting them..." | ||
| echo "" | ||
|
|
||
| cd "$(dirname "$0")" | ||
|
|
||
| iii --config iii-config.yaml & | ||
| III_PID=$! | ||
| sleep 2 | ||
|
|
||
| cd orchestrator | ||
| python3 orchestrator.py & | ||
| ORCH_PID=$! | ||
| sleep 3 | ||
| cd .. | ||
|
|
||
| echo "Started iii-engine (PID $III_PID) + orchestrator (PID $ORCH_PID)" | ||
|
|
||
| trap "kill $III_PID $ORCH_PID 2>/dev/null" EXIT | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "API: $API" | ||
| echo "" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 2. Setup experiment tag | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "--- Setting up tag: $TAG ---" | ||
| SETUP=$(curl -sf -X POST "$API/api/experiment/setup" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"tag\":\"$TAG\"}" 2>/dev/null || echo '{"body":{"error":"exists"}}') | ||
|
|
||
| echo "$SETUP" | jq -r '.body // .' 2>/dev/null || echo "$SETUP" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 3. List available tasks | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Available tasks ---" | ||
| TASKS=$(curl -sf "$API/api/task/list" | jq -r '.body.tasks[].name' 2>/dev/null) | ||
| echo "$TASKS" | ||
| TASK_COUNT=$(echo "$TASKS" | wc -l | tr -d ' ') | ||
| echo "Total: $TASK_COUNT tasks" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 4. Register baseline experiment | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Registering baseline experiment ---" | ||
| COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none") | ||
| REG=$(curl -sf -X POST "$API/api/experiment/register" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{ | ||
| \"tag\": \"$TAG\", | ||
| \"hypothesis\": \"Baseline harness — no modifications\", | ||
| \"description\": \"baseline\", | ||
| \"category\": \"other\", | ||
| \"commit_sha\": \"$COMMIT\" | ||
| }") | ||
|
|
||
| EXP_ID=$(echo "$REG" | jq -r '.body.experiment_id // empty') | ||
| echo "Experiment ID: $EXP_ID" | ||
|
|
||
| if [ -z "$EXP_ID" ]; then | ||
| echo "ERROR: Failed to register experiment" | ||
| echo "$REG" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 5. Run benchmark (all tasks) | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Running benchmark (all tasks, concurrency=4) ---" | ||
| echo "This may take a few minutes..." | ||
|
|
||
| BATCH=$(curl -sf -X POST "$API/api/task/batch" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"experiment_id\": \"$EXP_ID\", \"concurrency\": 4}" \ | ||
| --max-time 900 2>/dev/null || echo '{"body":{"error":"batch failed"}}') | ||
|
|
||
| PASSED=$(echo "$BATCH" | jq -r '.body.passed // 0') | ||
| TOTAL=$(echo "$BATCH" | jq -r '.body.total_tasks // 0') | ||
| SCORE=$(echo "$BATCH" | jq -r '.body.aggregate_score // 0') | ||
| DURATION=$(echo "$BATCH" | jq -r '.body.duration_seconds // 0') | ||
|
|
||
| echo "" | ||
| echo "Results: $PASSED/$TOTAL passed (score: $SCORE)" | ||
| echo "Duration: ${DURATION}s" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 6. Record completion | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Recording results ---" | ||
| COMPLETE=$(curl -sf -X POST "$API/api/experiment/complete" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{ | ||
| \"experiment_id\": \"$EXP_ID\", | ||
| \"passed\": $PASSED, | ||
| \"total_tasks\": $TOTAL, | ||
| \"aggregate_score\": $SCORE, | ||
| \"task_scores\": $(echo "$BATCH" | jq '.body.task_scores // {}'), | ||
| \"duration_seconds\": $DURATION | ||
| }") | ||
|
|
||
| STATUS=$(echo "$COMPLETE" | jq -r '.body.status // "unknown"') | ||
| ACTION=$(echo "$COMPLETE" | jq -r '.body.action // "unknown"') | ||
| echo "Status: $STATUS | Action: $ACTION" | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 7. Show summary | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Summary ---" | ||
| curl -sf -X POST "$API/api/report/summary" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"tag\": \"$TAG\"}" | jq '.body | { | ||
| tag, | ||
| best: .best, | ||
| stats: .stats, | ||
| strategy, | ||
| total_duration_minutes, | ||
| common_failures: (.common_failures | keys) | ||
| }' 2>/dev/null | ||
|
|
||
| # ------------------------------------------------------------------- | ||
| # 8. Get suggestions for next experiment | ||
| # ------------------------------------------------------------------- | ||
|
|
||
| echo "" | ||
| echo "--- Suggestions for next experiment ---" | ||
| curl -sf -X POST "$API/api/search/suggest" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"tag\": \"$TAG\"}" | jq '.body.suggestions[]' 2>/dev/null | ||
|
|
||
| echo "" | ||
| echo "==========================================" | ||
| echo " Benchmark complete!" | ||
| echo " Tag: $TAG" | ||
| echo " Passed: $PASSED/$TOTAL" | ||
| echo "==========================================" | ||
| echo "" | ||
| echo "Next steps:" | ||
| echo " 1. Edit agent.py (the editable section)" | ||
| echo " 2. Run: ./bench.sh $TAG" | ||
| echo " 3. The system auto-keeps or discards" | ||
| echo " 4. Repeat until score converges" | ||
| echo "" | ||
| echo "Or give program.md to a meta-agent:" | ||
| echo " claude -p program.md" |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What are the currently available OpenAI GPT models for the Agents SDK?💡 Result:
For the OpenAI Agents SDK (which uses the Responses API), the currently available GPT models listed in the API docs are:
gpt-5.4,gpt-5.4-mini, andgpt-5.4-nano. [1]Source(s): [1]
🏁 Script executed:
Repository: iii-hq/workers
Length of output: 529
Update model identifier to a complete, available version.
MODEL = "gpt-5"is an incomplete model identifier. Update to a complete gpt-5 variant like"gpt-5.4","gpt-5.4-mini", or"gpt-5.4-nano", whichever is appropriate for your use case.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional —
MODELlives in the editable section (the whole point of autoharness is that the meta-agent modifies this)."gpt-5"is a valid OpenAI alias that resolves to the latest gpt-5 variant. Pinning to a specific sub-version like"gpt-5.4"would make the baseline less portable. The meta-agent can (and should) experiment with model selection as part of the loop — it's one of the 12 tracked categories (model_selection).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rohitg00, that's a well-reasoned design choice. OpenAI's own docs confirm thatgpt-5is a valid alias with snapshots available, so using the short alias rather than a pinned version makes the baseline more forward-compatible. The meta-agent's ability to experiment withmodel_selectionas a tracked category is a great demonstration of the self-improving loop in action. Withdrawing my concern.🐇✨
✏️ Learnings added