Built on Flyte 2 · flyte.ai.agents

flyte-agent-loop

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.

builder · every 5m reviewer · every 5m distiller · every 10m

github.com/unionai-oss/flyte-agent-loop

The idea

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.

🔁 The loop

  • Schedule — cron triggers fire the agents continuously.
  • Cooperate — they claim work & share memory so they never collide or double-work.
  • Verify — every change is checked by a strict verifier before it lands.
  • Learn — a distiller folds run history into lessons fed back as context.

Humans only file issues and review PRs. The system runs the rest.

Three pipelines, one shared memory

The system at a glance

PipelineCadenceWhat 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.
Architecture

Agents cooperating over durable memory

flowchart TD mem["shared MemoryStore<br/>context/digest.md<br/>runs per-run JSON<br/>ingest/state.json ledger"] builder["builder<br/>every 5 min"] reviewer["reviewer<br/>every 5 min"] distiller["distiller<br/>every 10 min"] repo["GitHub repo"] mem -->|reads context| builder mem -->|reads context| reviewer builder -->|writes records| mem reviewer -->|writes records| mem repo -->|open issues and PRs| builder repo -->|open PRs and comments| reviewer builder -->|opens PR or files issues| repo reviewer -->|pushes fixes| repo mem -->|reads records| distiller distiller -->|distills lessons and report| mem
# 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
Flyte 2 crash course · 1/4

An Agent is a
tool-using loop

flyte.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.

Docs: Flyte docs → Build an agent

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
Flyte 2 crash course · 2/4

Tools are just
Flyte tasks

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}"
Flyte 2 crash course · 3/4

Durable memory
across runs

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
Flyte 2 crash course · 4/4

From agent to
autonomous service

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
Pipeline 1 · builder

One issue in → a PR or new issues out

🅰 Write code → pull request

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.

🅱 Decompose a spec → new issues

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.

01Dibs

claim an eligible open issue

02Propose

agent stages a plan (read-only tools)

03Verify

strict sub-agent, up to 3 tries

04Write

durable task opens PR / files issues

Pipeline 2 · reviewer

PRs that review
themselves

Claims an open agent PR and always performs its own code review — human comments are extra guidance, not a prerequisite.

  • Read — the PR diff and all its comments.
  • Fix — stage tightly-scoped changes.
  • Verify — confirm fixes are aligned with comments and correct.
  • Push + release — commit to the head branch, drop the claim for follow-ups.
  • Approve — once satisfied, it LGTMs; future runs stand down until a human comments /flyte-agent-loop ….
Pipeline 3 · distiller

The agents grade — and teach — themselves

📊 Evaluate

Success / verification / error rates over the full history. Empty “no-work” runs are pruned from memory.

🧠 Consolidate

A distiller Agent dedupes new runs into lessons.md — max signal per token — fed back to builder & reviewer as context.

🔎 Report

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.

Technique · cooperative locking

Dibs

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.

Technique · the safety invariant

Propose → Verify → Write

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.

propose

Builder / reviewer agent stages files or sub-issues. No side effects.

verify

A stricter agent returns {"verified":bool,"notes":…}. Fails feed back for up to 3 retries.

write

@env.task tools open the PR / push fixes / file issues as tracked, reproducible actions.

Technique · bounded outputs

Staging, not one giant JSON blob

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.
Technique · durable state

Two memory stores, disjoint writers

🗂 <key>-runs

One JSON file per run at runs/<ts>_<run>.json.

Writers: builder & reviewer — each to a unique path. Reader: distiller.

📓 <key>-context

context/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.

Run it

Local cluster,
zero to provision

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 --valueflyte 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
Built like production

Minimal, but not fragile

🧪 Hermetic tests

Pure state machines + parsers, GitHub client against an in-memory MockTransport. No cluster, network, or LLM key. CI on Py 3.11–3.13.

🛟 Graceful recovery

Every pipeline wraps its flow in try/except: on any error it logs, releases the dibs, and records an error run instead of crashing.

🔁 Retry & backoff

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.

Honest trade-offs

Deliberate simplifications

  • Dibs is best-effort — a TOCTOU window lets two runs claim at once; the TTL + agent id make it visible & self-healing, not impossible.
  • Distiller overlap — it isn’t dibs-protected; an over-long run can last-writer-win on the context store.
  • First page only — the client reads one page of issues/PRs/comments; a marker beyond it can be missed.
  • Unbounded growth — run files & processed ids accumulate; only empty runs are auto-pruned.
  • Blocking I/O — the GitHub client is synchronous; fine for single-flight tasks, not high fan-out.
  • Egress required — no path to api.github.com is an infra fix, not a code one.
Thanks

Agents that ship,
review, and learn.

A complete loop-engineering reference on Flyte 2 — small enough to read in an afternoon, real enough to run against your repo.

flyte.ai.agents.Agent MemoryStore Triggers · Cron Reports

github.com/unionai-oss/flyte-agent-loop

docs/architecture.md · Union.ai · Flyte 2

← → / space to navigate · f fullscreen
flyte-agent-loop · loop engineering with Flyte 2 1 / 19