Let an agent do ML research while you sleep: autoresearch on Amazon Bedrock AgentCore
Turn a GPU into an overnight ML researcher: an Amazon Bedrock AgentCore agent edits a training script, runs five-minute experiments on EC2, keeps only what improves, and remembers what worked across nights.
Let an agent do ML research while you sleep: autoresearch on Amazon Bedrock AgentCore
Move an overnight ML experiment loop off your workstation and onto a managed agent runtime, a GPU with no SSH, and a memory that lasts longer than one night.
TL;DR
The open-source autoresearch project has an AI agent edit a small GPT training script, train for five minutes, keep the change if validation loss improves, and repeat until morning. This post rebuilds that loop on AWS:
- Amazon Bedrock AgentCore Runtime hosts a LangGraph agent that runs for up to eight hours per session.
- Claude on Amazon Bedrock proposes each experiment as a small, exact edit to
train.py.
- An EC2 GPU instance runs the training, driven only through SSM Run Command: no SSH, no inbound ports.
- AgentCore Memory turns every experiment into lasting insights, so Tuesday's run builds on Monday's.
- DynamoDB, S3 and CloudWatch hold results, logs and metrics. EventBridge Scheduler and an AWS Budget keep the GPU from running up a bill.
The walkthrough below covers the key code for each piece.
What autoresearch is
The autoresearch repo is a compact, single-GPU GPT training setup, plus instructions that let a coding agent run research unattended. Three files matter.
prepare.py holds the data, tokenizer and evaluation, and is never edited. train.py holds the model, the optimizer (Muon + AdamW) and the training loop; it's the only file the agent edits. program.md holds the agent's instructions; it's the file the human edits.Training always stops after five minutes. The score is
val_bpb (validation bits per byte; lower is better), which doesn't depend on vocabulary size, so architecture and optimizer changes compare fairly. The agent edits, commits, trains, keeps the commit if val_bpb improved or resets it if not, and goes again: about 12 experiments an hour, roughly 100 a night.Upstream, the agent is a coding assistant on your own machine. That ties you to one workstation, and the agent's memory ends when its context window does.
What changes on AWS
| autoresearch, local | This build |
|---|---|---|
Agent | Coding-agent CLI on a workstation | LangGraph agent on AgentCore Runtime, Claude via Bedrock |
GPU | Whatever is under your desk | An EC2 GPU instance, reached only through SSM |
Instructions | program.md | Built-in default, overridable by uploading program.md to S3 |
Start and stop | Manual | CLI call or nightly schedule, plus a STOP flag |
Results | results.tsv | DynamoDB, results.tsv in S3, CloudWatch metrics |
Lessons learned | Gone with the session | AgentCore Memory, carried across nights |
Cost controls | None | Auto-stop, a morning safety stop, an AWS Budget |
Architecture
Plain text
EventBridge Scheduler (nightly, optional) EventBridge Scheduler (07:30 safety stop)│ │▼ ▼Operator CLI ──► Kickoff Lambda ──► AgentCore Runtime (LangGraph agent) ──SSM Run Command──► EC2 GPU box│ │ │ (uv, git,│ │ └──► Amazon Bedrock (Claude) train.py)│ └──► AgentCore Memory (insights) │└──► DynamoDB (results) · S3 (train.py, logs, results.tsv) ◄┘
Four design choices shape the code:
- The model proposes; code decides. Upstream, the coding agent runs git, reads the log and judges the result itself. Here the model only proposes the next edit. Committing, training, parsing
val_bpband keep-or-discard are ordinary code, so every decision is reproducible and the model can't talk itself into a worse number.
- Edits, not rewrites. The model returns search-and-replace blocks, and each
<old>block must matchtrain.pyexactly once. Edits are cheap in output tokens and easy to review. When one doesn't apply, the error goes back to the model.
- A GPU with no front door. The instance has no inbound rules. Every action is a shell script sent through SSM and run as the unprivileged
ubuntuuser. SSM truncates output at 24,000 characters, sotrain.pyandrun.logmove through S3.
- Everything is resumable. Results and the branch tip go to DynamoDB after every experiment, so a session restarted with the same run tag picks up where the last one stopped.
Walkthrough
1. The rules, as plain functions
The keep-or-discard rules are pure, unit-tested functions. A run counts as a crash unless it prints a numeric
val_bpb . The first successful run becomes the baseline, and after that only a strictly lower val_bpb is kept. An edit is rejected unless its <old> text appears exactly once.agent/autoresearch/core.py (excerpt) Python
def parse_run_output(text: str) -> RunResult:"""Parse the output of the remote experiment script.The script prints `===EXIT=== <code>`, then the summary metrics grepped fromrun.log, then (only when val_bpb is missing) the tail of run.log after`===TAIL===`. A run without a numeric val_bpb counts as a crash."""exit_match = re.search(r"===EXIT===\s+(-?\d+)", text)exit_code = int(exit_match.group(1)) if exit_match else Nonemetrics = {name: float(value) for name, value in METRIC_RE.findall(text)}tail = text.split("===TAIL===", 1)[1].strip() if "===TAIL===" in text else ""if "val_bpb" not in metrics:return RunResult(crashed=True, exit_code=exit_code, log_tail=tail[-4000:])return RunResult(crashed=False,val_bpb=metrics["val_bpb"],peak_vram_mb=metrics.get("peak_vram_mb", 0.0),exit_code=exit_code,)def decide(result: RunResult, best_bpb: float | None) -> str:"""Return 'keep', 'discard' or 'crash'.The first successful run becomes the baseline. After that, a change is keptonly if it strictly lowers val_bpb."""if result.crashed:return "crash"if best_bpb is None:return "keep"return "keep" if result.val_bpb < best_bpb else "discard"def parse_proposal(text: str) -> dict:"""Parse the model's response into {'description', 'hypothesis', 'edits'}."""descriptions = _tag("description", text)if not descriptions or not descriptions[0].strip():raise ProposalError("missing <description>")hypotheses = _tag("hypothesis", text)edits = []for i, block in enumerate(_tag("edit", text)):old, new = _tag("old", block), _tag("new", block)if len(old) != 1 or len(new) != 1:raise ProposalError(f"edit {i}: needs exactly one <old> and one <new>")edits.append({"old": old[0], "new": new[0]})if not edits:raise ProposalError("no <edit> blocks found")return {"description": _one_line(descriptions[0], 100),"hypothesis": _one_line(hypotheses[0], 600) if hypotheses else "","edits": edits,}def apply_edits(source: str, edits: list[dict]) -> str:"""Apply exact search/replace edits. Each `old` must match exactly once."""if not edits:raise EditError("no edits to apply")updated = sourcefor i, edit in enumerate(edits):old, new = edit.get("old"), edit.get("new")if not isinstance(old, str) or not old.strip() or not isinstance(new, str):raise EditError(f"edit {i}: 'old' must be non-empty text and 'new' must be text")count = updated.count(old)if count == 0:raise EditError(f"edit {i}: the <old> text was not found in train.py")if count > 1:raise EditError(f"edit {i}: the <old> text matches {count} places; include more context")updated = updated.replace(old, new, 1)if updated == source:raise EditError("the edits did not change train.py")return updated
2. The loop
The loop is a three-node LangGraph state machine: propose → execute → evaluate, with a conditional edge back to
propose . It ends on the time budget, the experiment budget, ten failures in a row, or a STOP flag in S3. The first experiment is always the unmodified baseline. On keep, the branch advances; on discard or crash, the box resets to the last kept commit. If the model can't produce an edit that applies in three tries, the experiment is logged as invalid and never runs.LangGraph stops a graph after 25 steps by default, and a night takes hundreds, so the runner invokes the graph with a much higher
recursion_limit .agent/autoresearch/graph.py (excerpt) Python
def build_graph(deps: Deps):def propose(state: ResearchState) -> dict:if not state["history"]:log.info("experiment 1: baseline")baseline = {"description": "baseline", "hypothesis": "unmodified train.py", "edits": []}return {"proposal": baseline, "candidate": state["head"]}source = deps.box.read_train_py(state["run_tag"])insights = deps.memory.recall(RECALL_QUERY)feedback = ""for attempt in range(1, MAX_PROPOSAL_ATTEMPTS + 1):prompt = build_user_prompt(gpu=deps.gpu,best_bpb=state.get("best_bpb"),history=state["history"],insights=insights,last_crash=state.get("last_crash", ""),train_py=source,feedback=feedback,)text = deps.researcher.propose(deps.program, prompt)try:proposal = parse_proposal(text)updated = apply_edits(source, proposal["edits"])except (ProposalError, EditError) as err:log.warning("proposal attempt %d rejected: %s", attempt, err)feedback = (f"Your previous response was rejected: {err}. Follow the response format ""exactly and copy each <old> block verbatim from the current train.py.")continueseq = state["seq"] + 1commit = deps.box.commit_train_py(state["run_tag"], seq, updated, proposal["description"])log.info("experiment %d: %s (%s)", seq, proposal["description"], commit)return {"proposal": proposal, "candidate": commit}return {"proposal": None, "candidate": None}def execute(state: ResearchState) -> dict:if not state.get("proposal"):return {"result": None}output = deps.box.run_experiment(state["run_tag"], state["seq"] + 1, state["candidate"])return {"result": parse_run_output(output).to_dict()}def evaluate(state: ResearchState) -> dict:seq = state["seq"] + 1prev_best = state.get("best_bpb")head, best = state["head"], prev_bestproposal = state.get("proposal")if proposal is None: # the model never produced an applicable editresult = RunResult(crashed=True)status, commit = "invalid", headproposal = {"description": "(no valid edit produced)", "hypothesis": ""}else:result = RunResult(**state["result"])status, commit = decide(result, prev_best), state["candidate"]if status == "keep":head, best = commit, result.val_bpbelif commit != head:deps.box.reset_to(head) # discard: rewind the branchrow = {"seq": seq,"commit": commit,"status": status,"val_bpb": result.val_bpb,"memory_gb": result.memory_gb,"description": proposal["description"],"hypothesis": proposal.get("hypothesis", ""),"ts": int(time.time()),}history = [*state["history"], row]_persist(deps, state["run_tag"], row, history, head, best, prev_best, result)log.info("experiment %d -> %s val_bpb=%.6f best=%s", seq, status, result.val_bpb, best)return {"seq": seq,"head": head,"best_bpb": best,"history": history,"last_crash": result.log_tail if status == "crash" else "","proposal": None,"candidate": None,"result": None,}def route(state: ResearchState) -> str:reason = stop_reason(deps, state)if reason:log.info("stopping: %s", reason)return ENDdeps.store.refresh_lock(deps.lock_owner)return "propose"graph = StateGraph(ResearchState)graph.add_node("propose", propose)graph.add_node("execute", execute)graph.add_node("evaluate", evaluate)graph.set_entry_point("propose")graph.add_edge("propose", "execute")graph.add_edge("execute", "evaluate")graph.add_conditional_edges("evaluate", route, {"propose": "propose", END: END})return graph.compile()
3. Driving the GPU through SSM
Every action on the box goes through one helper that base64-encodes the script, so quoting can't break it, and runs it as
ubuntu inside the checkout. Training runs under timeout . Its summary lines come back between markers that parse_run_output reads, and the full log goes to S3.agent/autoresearch/gpu.py (excerpt) Python
def _as_user(self, body: str) -> str:script = ("set -euo pipefail\n"'export PATH="$HOME/.local/bin:$PATH"\n'f"cd {shlex.quote(self.cfg.repo_dir)}\n" + body)encoded = base64.b64encode(script.encode()).decode()return f'sudo -u {self.cfg.gpu_user} -H bash -lc "$(echo {encoded} | base64 -d)"'def run(self, body: str, timeout_s: int = 300, as_root: bool = False) -> str:"""Run a script on the box and return stdout. Raises on non-zero exit."""resp = self.ssm.send_command(InstanceIds=[self.cfg.instance_id],DocumentName="AWS-RunShellScript",Parameters={"commands": [body if as_root else self._as_user(body)],"executionTimeout": [str(timeout_s)],},TimeoutSeconds=300,Comment="autoresearch",)command_id = resp["Command"]["CommandId"]deadline = time.time() + timeout_s + 300time.sleep(2)while time.time() < deadline:try:inv = self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.cfg.instance_id)except self.ssm.exceptions.InvocationDoesNotExist:time.sleep(3)continueif inv["Status"] in _PENDING:time.sleep(10)continueif inv["Status"] != "Success":raise RemoteCommandError(f"{inv['Status']} (exit {inv.get('ResponseCode')}): "f"{inv.get('StandardErrorContent', '')[-2000:]}")return inv.get("StandardOutputContent", "")raise RemoteCommandError(f"command {command_id} did not finish in time")def run_experiment(self, run_tag: str, seq: int, commit: str) -> str:"""Run one fixed-budget training job; return the marker-delimited summary."""timeout_s = self.cfg.experiment_timeout_slog_key = f"runs/{run_tag}/logs/{seq:04d}-{commit}.log"body = (# Kill leftovers from an interrupted run. The [p] trick stops pkill# from matching this script's own command line."pkill -f '[p]ython.*train\\.py' || true\n""rm -f run.log\n""set +e\n"f"timeout --kill-after=30 {timeout_s} uv run train.py > run.log 2>&1\n""code=$?\n""set -e\n"'echo "===EXIT=== $code"\n''echo "===METRICS==="\n'"grep -E '^(val_bpb|peak_vram_mb|training_seconds|num_params_M):' run.log || true\n"'echo "===TAIL==="\n'"grep -q '^val_bpb:' run.log || tail -n 40 run.log\n"+ self._cp("run.log", self._s3_uri(log_key)).rstrip("\n")+ " || true\n")return self.run(body, timeout_s=timeout_s + 300)
4. Memory that outlives the night
After each experiment, the agent writes a two-turn event to AgentCore Memory: what it tried and why, then what happened. The memory's semantic strategy turns those events into insight records under
/autoresearch/{actorId}/insights/ . Before each proposal, the agent retrieves the most relevant insights and adds them to the prompt. A session on a new branch next week still knows which learning-rate change crashed on this GPU.agent/autoresearch/store.py (excerpt) Python
class ExperimentMemory:"""Long-term memory of what worked, backed by AgentCore Memory.Each finished experiment is written as a short conversational event. Thememory's semantic strategy distils those events into insight records thatlater sessions (on any night) retrieve before proposing a change."""def __init__(self, cfg: Config):self.client = boto3.client("bedrock-agentcore", region_name=cfg.region)self.memory_id = cfg.memory_idself.actor_id = cfg.actor_idself.namespace = f"/autoresearch/{cfg.actor_id}/insights/"def record(self, run_tag: str, experiment: str, outcome: str) -> None:try:self.client.create_event(memoryId=self.memory_id,actorId=self.actor_id,sessionId=run_tag,eventTimestamp=datetime.now(timezone.utc),payload=[{"conversational": {"role": "USER", "content": {"text": experiment}}},{"conversational": {"role": "ASSISTANT", "content": {"text": outcome}}},],)except ClientError as err:log.warning("memory write failed: %s", err)def recall(self, query: str, top_k: int = 8) -> list[str]:try:resp = self.client.retrieve_memory_records(memoryId=self.memory_id,namespace=self.namespace,searchCriteria={"searchQuery": query, "topK": top_k},)except ClientError as err:log.warning("memory read failed: %s", err)return []texts = []for record in resp.get("memoryRecordSummaries", []):text = (record.get("content") or {}).get("text", "").strip()if text:texts.append(text)return texts
5. The prompt
The system prompt plays the part of
program.md . It restates the upstream rules and adds two things for the cloud: a strict response format, and an instruction to make train.py run on this GPU first when no baseline has succeeded. Upload your own program.md to the bucket to replace it without redeploying.agent/autoresearch/prompts.py (excerpt) Python
DEFAULT_PROGRAM = """\You are an autonomous ML researcher. You improve a small single-GPU GPTtraining script, train.py, one experiment at a time.Objective: the lowest val_bpb (validation bits per byte; lower is better).Training always stops after a fixed 5-minute budget, so you are optimisingwhat this GPU can learn in 5 minutes.You may change anything in train.py: architecture, optimizer, learning ratesand schedules, batch size, model size, the training loop. You may not changeprepare.py or the evaluation, and you may not add dependencies.Constraints and taste:- The script must run without crashing and finish within the time budget.- Some extra VRAM is fine for a real gain; don't exceed the GPU's memory.- Prefer simplicity. A tiny gain that adds a lot of hacky code is not worthit; the same result with less code is a win.- Learn from the history and the insights. Don't repeat ideas that alreadyfailed unless your variant is meaningfully different. Combine near-missesand try bolder ideas when progress stalls.- If there is no successful run yet, your only goal is to make train.py runon this GPU (for example: lower DEPTH or batch size, shorten sequences, orreplace a kernel the GPU does not support with a PyTorch-native one).- If the last experiment crashed because of a simple bug, you may fix it.Respond with ONE experiment in exactly this format:<description>one line, at most 80 characters</description><hypothesis>why this should lower val_bpb</hypothesis><edit><old>text copied verbatim from train.py; it must match exactly one place</old><new>the replacement text</new></edit>You may use several <edit> blocks. Keep each <old> short but unique."""
6. A long-running agent on AgentCore Runtime
AgentCore Runtime ends a session that reports itself idle for 15 minutes and caps every session at 8 hours. The entrypoint returns right away and runs the loop in a background thread registered with
add_async_task . The SDK then reports HealthyBusy on /ping until complete_async_task is called. A DynamoDB lease stops two sessions from sharing the GPU, and the runner stops the instance when the session ends.agent/main.py (excerpt) Python
@app.entrypointdef invoke(payload: dict) -> dict:payload = payload or {}action = payload.get("action", "start")try:params = RunParams.from_payload(payload)except ValueError as err:return {"error": str(err)}cfg = Config.from_env()store = ResultsStore(cfg)if action == "status":state, rows = store.load(params.run_tag)return {"run_tag": params.run_tag, "state": state, "recent": rows[-10:]}if action != "start":return {"error": f"unknown action: {action}"}owner = f"{params.run_tag}-{uuid.uuid4().hex[:8]}"if not store.acquire_lock(owner):return {"status": "busy", "message": "another research session is using the GPU"}task_id = app.add_async_task("autoresearch_session", {"run_tag": params.run_tag})def worker() -> None:try:run_session(cfg, params, owner, store)except Exception:log.exception("research session %s failed", params.run_tag)finally:app.complete_async_task(task_id)threading.Thread(target=worker, name=f"autoresearch-{params.run_tag}", daemon=True).start()return {"status": "started","run_tag": params.run_tag,"max_hours": params.max_hours,"max_experiments": params.max_experiments,}
agent/autoresearch/runner.py (excerpt) Python
def run_session(cfg: Config, params: RunParams, lock_owner: str, store: ResultsStore) -> dict:box = GpuBox(cfg)try:box.ensure_ready()box.clear_stop(params.run_tag)saved, rows = store.load(params.run_tag)head = box.checkout(params.run_tag, reset_to=saved["head"] if saved else None)done = saved["experiments"] if saved else 0researcher = Researcher(cfg.model_id, cfg.region)deps = Deps(box=box,store=store,memory=ExperimentMemory(cfg),researcher=researcher,metrics=Metrics(cfg),program=box.load_program(DEFAULT_PROGRAM),gpu=cfg.gpu_description,lock_owner=lock_owner,)state = {"run_tag": params.run_tag,"head": head,"best_bpb": saved.get("best_bpb") if saved else None,"seq": done,"history": rows,"last_crash": "","deadline": time.time() + params.max_hours * 3600 - SAFETY_MARGIN_S,"max_experiments": done + params.max_experiments,}log.info("session %s: resuming after %d experiments at %s", params.run_tag, done, head)final = build_graph(deps).invoke(state, config={"recursion_limit": 100_000})log.info("session %s finished: %d experiments, best val_bpb=%s, tokens in/out=%d/%d",params.run_tag,final["seq"],final.get("best_bpb"),researcher.input_tokens,researcher.output_tokens,)return finalfinally:store.release_lock(lock_owner)if cfg.stop_instance_on_finish:box.stop_instance()
7. Infrastructure
One AWS CDK stack deploys everything:
- a VPC with public subnets and no NAT gateway
- the GPU instance on the AWS Deep Learning Base GPU AMI, with a first-boot script that installs
uv, clones autoresearch and runsprepare.py
- the S3 bucket and the DynamoDB table
- the agent image, built for
linux/arm64
- a least-privilege execution role, scoped to one instance, one SSM document and one memory
- a kickoff Lambda function, a morning safety-stop schedule, an optional nightly schedule and an optional AWS Budget
AgentCore Memory and Runtime are declared as CloudFormation resources:
infra/autoresearch_stack.py (excerpt) Python
memory = CfnResource(self,"Memory",type="AWS::BedrockAgentCore::Memory",properties={"Name": "autoresearch_memory","Description": "What the autoresearch agent learned across nights","EventExpiryDuration": 90,"MemoryStrategies": [{"SemanticMemoryStrategy": {"Name": "experiment_insights","Description": "Facts about which train.py changes helped, hurt or crashed","NamespaceTemplates": ["/autoresearch/{actorId}/insights/"],}}],},)# ...runtime = CfnResource(self,"Runtime",type="AWS::BedrockAgentCore::Runtime",properties={"AgentRuntimeName": AGENT_NAME,"Description": "Autonomous train.py research loop (autoresearch)","AgentRuntimeArtifact": {"ContainerConfiguration": {"ContainerUri": image.image_uri}},"NetworkConfiguration": {"NetworkMode": "PUBLIC"},"ProtocolConfiguration": "HTTP","RoleArn": agent_role.role_arn,"LifecycleConfiguration": {"IdleRuntimeSessionTimeout": 900, "MaxLifetime": 28800},"EnvironmentVariables": {"AUTORESEARCH_REGION": r,"GPU_INSTANCE_ID": instance.instance_id,"GPU_DESCRIPTION": gpu_description,"ARTIFACT_BUCKET": bucket.bucket_name,"RESULTS_TABLE": table.table_name,"MEMORY_ID": memory.get_att("MemoryId").to_string(),"MODEL_ID": model_id,"REPO_DIR": REPO_DIR,"BASE_REF": repo_ref,"EXPERIMENT_TIMEOUT_S": "600","STOP_INSTANCE_ON_FINISH": "true",},},)
Running a night of research
An operator starts a session with a run tag, such as
nov05 , and a time budget. A small Lambda function starts the GPU instance and invokes the runtime, which replies at once and keeps working in the background. The agent creates the branch autoresearch/nov05 on the box, runs the baseline, and then loops until the budget runs out. Progress is visible throughout: each experiment lands in DynamoDB and as CloudWatch metrics, and results.tsv in S3 is rewritten after every run in the same format upstream uses.Stopping is just as simple. Writing a
STOP object to the bucket makes the agent finish the experiment in flight, record it, release the GPU lock and stop the instance. Starting again with the same tag resumes from the last kept commit. For recurring research, an EventBridge schedule starts a session every evening with a date-based tag. Each night gets its own branch, and all nights share one memory.Lessons for building your own
- Size the GPU before the first night. autoresearch is tuned for an H100, and its defaults may not run on a 24 GB A10G (
g5.xlarge). This build turns that into the model's first task: if the baseline crashes, it must maketrain.pyfit, and the first successful run becomes the baseline. For numbers you can compare with upstream, use a single-H100p5.4xlargeand check capacity in your Region first. The upstream README lists the right knobs for smaller GPUs.
- Measure your cadence. Five minutes of training plus startup, evaluation and a model call usually comes to fewer than 12 experiments an hour.
- Plan around the eight-hour session cap. One runtime session covers one night. Use a schedule for recurring runs rather than trying to stretch a session.
- Check quotas early. Confirm your EC2 vCPU quota for the instance family and your Bedrock throughput for the model before you depend on either.
Cost considerations
Prices are on-demand list prices in
us-west-2 at the time of writing; check the pricing pages before relying on them.Model inference. Each experiment is one model call. With Claude Sonnet 4.5 at $3 per million input tokens and $15 per million output tokens, a turn of 4K in and 2K out costs about $0.042. This code sends the full
train.py and recent history every turn, so real input is higher. The runner logs token totals at the end of each session so you can measure yours.GPU compute is the biggest cost:
Instance | GPU | On-demand per hour | Per 8-hour night |
|---|---|---|---|
g6.xlarge | 1x L4 (24 GB) | $0.805 | $6.44 |
g5.xlarge | 1x A10G (24 GB) | $1.006 | $8.05 |
g5.2xlarge | 1x A10G (24 GB) | $1.212 | $9.70 |
Everything else. AgentCore Runtime bills per second for the vCPU and memory a session uses (about $0.0895 per vCPU-hour and $0.00945 per GB-hour at the time of writing), and this agent spends most of its time waiting on SSM. Memory, DynamoDB, S3, CloudWatch and Lambda stay small at about 100 experiments a night.
Levers that matter: stop the GPU (the agent stops it after each session, and a morning schedule stops it regardless); use Spot capacity, since a resumable session loses one experiment, not the night; use a smaller model for routine proposals; cache the stable parts of the prompt; and use Savings Plans if the GPU runs nightly.
Where this goes next
The autoresearch README hints at the next step: more agents. A supervisor hands out ideas, workers each own a GPU and a branch, a reviewer checks wins for flukes, and all of them share one AgentCore Memory. Four workers take you from about a dozen experiments an hour to about fifty. That's the next post.
Clean up
Because everything lives in one CloudFormation stack, deleting the stack removes the instance, runtime, memory, bucket and table. Set the table's removal policy to
RETAIN first if you want to keep the experiment history.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article