Architecture

`adk-loop-lab` is built around a deterministic controller, not a free-running chat loop. The controller owns lifecycle order, checkpoints, and stop decisions, while adjacent modules for context, evaluation, memory, and tool governance provide reusable building blocks that are only partially integrated into the generic runtime today.

Seven-Phase Lifecycle

Rendering diagram...

The lifecycle mirrors the controller contract described in docs/architecture/overview.md: observe the current world, select the next bounded move, verify it, persist state, and only then evaluate whether continuing is justified. The architecture modules support richer context reconstruction and memory-aware execution, but the generic controller still takes a simpler path in a few places.

Three-Plane Separation

Rendering diagram...
  • Control plane: deterministic Python in loop/ handles phase ordering, budgets, and checkpointing.
  • Execution plane: adk/, context/, and tools/ translate a bounded action into model and tool work.
  • Data plane: state/, memory/, and events/ preserve authoritative facts, reusable lessons, and traces.

Module Map

Rendering diagram...

The package split is intentionally plain:

  • loop/ owns the lifecycle, policies, recovery, and checkpoints.
  • adk/ absorbs ADK version churn through agents, runner helpers, workflow helpers, and a compatibility layer.
  • context/ implements reusable prompt reconstruction from goal, constraints, state, memory, failures, and budget status, though the generic controller currently assembles simpler PLAN and REFLECT prompts directly.
  • memory/, state/, and events/ keep long-lived records inspectable and local-first.
  • evaluation/ includes deterministic validators and composite policy helpers, although the generic controller currently uses inline average-score and all-pass logic rather than the richer composite helper.

Deterministic Shell Principle

Models propose; deterministic code decides. `PLAN` and `REFLECT` may use model output. `DECIDE` does not. Lifecycle transitions and terminal outcomes belong to deterministic code paths, while some policy metadata such as tool approval and richer evaluation composition is implemented outside the generic controller's current enforcement path.

That principle gives the repo its core trust boundaries:

  • current facts come from state, not prompt memory
  • promotion helpers require evidence before lessons are verified
  • irreversible operations can be separated from routine reads and reversible writes
  • traces can be redacted without losing lifecycle visibility

Budget Enforcement

Budget exhaustion is a deterministic stop condition rather than a model judgment.

BudgetWhy it existsTerminal outcome
IterationsPrevent unbounded loopingBUDGET_EXHAUSTED
Model callsKeep agent spend explicitBUDGET_EXHAUSTED
Tool callsBound side effects and churnBUDGET_EXHAUSTED
Elapsed durationAvoid stale or hanging runsBUDGET_EXHAUSTED
Stagnation thresholdStop repeated non-progressSTAGNATED

In the current controller, model and tool counters are recorded after those calls complete and exhaustion is applied in stopping logic. The default stopping order is still explicit: fatal error, success, budget exhaustion, stagnation, duration exhaustion, otherwise continue.

Key Design Decisions

The architecture pages and ADRs line up around three durable choices:

  1. Google ADK is the execution surface. ADR 001 keeps agent construction and workflow composition in adk/ so the controller stays ADK-agnostic.
  2. SQLite FTS5 is the default memory backend. ADR 002 favors local inspectability and evidence-gated promotion over a mandatory vector store.
  3. Async-shaped sqlite3 adapters replace aiosqlite. ADR 003 preserves the async call shape without adding runtime fragility in local and sandboxed environments.

From there, the architecture stays conservative: keep the shell deterministic, keep state authoritative, keep verification in front of promotion, and keep every loop bounded.