Three scheduled AI agents that cooperate over durable shared memory to take GitHub issues all the way to reviewed pull requests — and then grade themselves.
github.com/unionai-oss/flyte-agent-loop
Prompt engineering shapes one call. Loop engineering shapes what happens across many calls, over time.
Agents run on a schedule, coordinate through durable state, and get better from their own graded history — no human in the inner loop.
Humans only file issues and review PRs. The system runs the rest.
| Pipeline | Cadence | What it does |
|---|---|---|
| builder | */5 min | Claims an open issue via a “dibs” comment, implements it (tests / examples / docs), has a verifier sub-agent check the work, then opens a PR — or files decomposed issues. |
| reviewer | */5 min | Claims an open agent PR, reads its comments, makes + verifies fixes, pushes them, and releases the claim for follow-ups. |
| distiller | */10 min | A distiller Agent dedupes & consolidates run history into a high-signal “lessons” memory, and publishes metrics, the memory filesystem, and per-run reasoning traces as a Flyte report. |
# builder / reviewer --reads context--> MemoryStore # builder / reviewer --writes records--> MemoryStore # GitHub repo --issues / PRs--> builder / reviewer # builder --opens PR or files issues--> GitHub repo # reviewer --pushes fixes--> GitHub repo # distiller --reads records / distills lessons--> MemoryStore
Agent is aflyte.ai.agents.Agent wraps the model + a tool-calling loop. Give it instructions, a model, and tools; .run() drives the loop and returns an AgentResult.
from flyte.ai.agents import Agent agent = Agent( name="issue-builder", instructions="Implement a GitHub issue…", model="claude-sonnet-4-5", tools=[read_issue, stage_file, …], max_turns=40, parallel_tool_calls=False, ) result = await agent.run.aio("Implement #42") result.summary # final text · also .error .attempts
Hand the agent any callable. A @env.task tool dispatches as a durable, tracked sub-action — retries, caching, and a place in the run's timeline — so the agent's tool calls are first-class Flyte executions.
The model sees the type hints + docstring as the tool schema.
# durable: each call is a tracked action @env.task async def read_issue(number: int) -> dict: """Fetch an issue's title and body.""" with _client() as gh: return gh.get_issue(number) # or a lightweight in-process tool @tool def stage_file(path: str, content: str) -> str: """Stage one file for the change.""" stage.files[path] = content return f"staged {path}"
MemoryStore is a keyed, versioned filesystem on object storage. Agents read and write it between executions — this is the state that makes a loop out of independent runs.
Read-only prefixes + an audit log keep multi-writer memory safe.
from flyte.ai.agents import MemoryStore store = await MemoryStore.get_or_create.aio( key="agent-loop-shared", ) await store.write_json.aio("runs/42.json", record) digest = await store.read_text.aio( "context/digest.md", default="", ) await store.save.aio() # persist to object storage
Wrap the agent in a scheduled @env.task and it becomes self-running: cron triggers, injected secrets, containerized execution, and a live report UI — no extra infra. flyte deploy activates it.
import flyte env = flyte.TaskEnvironment( name="agent-loop", secrets=[flyte.Secret("github-token")], ) @env.task(report=True, triggers=[flyte.Trigger( name="every_5m", automation=flyte.Cron("*/5 * * * *"), )]) async def builder() -> RunRecord: result = await agent.run.aio(prompt) flyte.report.log(f"opened PR …") return record
Stage each file with stage_file, then submit_implementation. The pipeline commits and opens the PR.
Scoped to what the issue asks for — tests when behavior changes, docs when user-facing.
Read a markdown spec, stage_issue(key, title, body, depends_on) per work item, then submit_decomposition.
The pipeline creates issues in dependency order & wires a depends-on DAG — independents run in parallel, dependents wait.
claim an eligible open issue
agent stages a plan (read-only tools)
strict sub-agent, up to 3 tries
durable task opens PR / files issues
Claims an open agent PR and always performs its own code review — human comments are extra guidance, not a prerequisite.
/flyte-agent-loop ….Success / verification / error rates over the full history. Empty “no-work” runs are pruned from memory.
A distiller Agent dedupes new runs into lessons.md — max signal per token — fed back to builder & reviewer as context.
A Flyte report: metrics, the whole memory filesystem, and each run’s flat sub-action “reasoning trace”.
Only new records trigger an Agent call — no new runs, no token spend, memory untouched.
Before working a target, a run posts an invisible HTML-comment marker. Overlapping cron fires parse the markers and skip anything already claimed. Claims expire (TTL) and can be released.
A pure state machine over comments + an explicit now — fully unit-tested, no network.
<!-- flyte-agent-loop:dibs v1 op=claim kind=issue agent=<id> run=<run> until=<iso8601> -->
Latest marker per kind wins; a release marker or an expired
until frees the target. Claims are re-entrant for their owner.
Agents are given read-only tools. They never touch the repo directly — they stage a proposal, a separate strict verifier checks it, and only then does a durable pipeline task perform the write.
Builder / reviewer agent stages files or sub-issues. No side effects.
A stricter agent returns {"verified":bool,"notes":…}. Fails feed back for up to 3 retries.
@env.task tools open the PR / push fixes / file issues as tracked, reproducible actions.
Emitting an entire change as one JSON object overflows the model’s max output tokens and truncates into invalid JSON — silently dropping the run.
Instead the agent calls stage_file(path, content) once per file. With parallel_tool_calls=False, each turn is bounded by the largest single file, and state accumulates race-free in a ChangeStage the pipeline owns.
stage_file("src/foo.py", …) # turn 1 stage_file("tests/test_foo.py", …) # turn 2 submit_implementation( branch="agent/issue-42-foo", title="…", body="…", summary="…", ) # pipeline reads stage.to_plan() directly — # no free-text parsing to truncate.
<key>-runsOne JSON file per run at runs/<ts>_<run>.json.
Writers: builder & reviewer — each to a unique path. Reader: distiller.
<key>-contextcontext/digest.md + the ingest/state.json ledger.
Writer: distiller only. Readers: builder & reviewer.
MemoryStore.save() uploads the whole local root — so a single shared store would let a builder’s
save() clobber a newer digest. Splitting by writer removes the cross-writer race: -runs
writers only ever touch unique paths, and -context has exactly one writer.
The Flyte Devbox is a full single-node Flyte cluster in Docker — tasks run in containers, with schedules and reports, just like a remote tenant.
Deploying activates three cron triggers; the reports show every execution.
# 1. start a local Flyte cluster flyte start devbox # 2. add secrets flyte create secret github-token --value … flyte create secret anthropic-api-key --value … # 3. deploy + activate schedules export FLYTE_AGENT_REPO=your-org/sandbox python -m flyte_agent_loop.deploy # or fire one run right now python -m flyte_agent_loop.deploy --run builder
Pure state machines + parsers, GitHub client against an in-memory MockTransport. No cluster, network, or LLM key. CI on Py 3.11–3.13.
Every pipeline wraps its flow in try/except: on any error it logs, releases the dibs, and records an error run instead of crashing.
The GitHub client retries transient failures (timeouts, 5xx, 429) with exponential backoff, so a flaky connection self-heals.
Stages are grouped with flyte.group(...) so a run’s timeline reads as discrete phases — claim → build → verify → open_pr — rather than a flat list of actions.
api.github.com is an infra fix, not a code one.A complete loop-engineering reference on Flyte 2 — small enough to read in an afternoon, real enough to run against your repo.
github.com/unionai-oss/flyte-agent-loop
docs/architecture.md · Union.ai · Flyte 2