Upstream ADK Impact Analysis & Generator Architecture¶
This document maps out how changes in the upstream google-adk package
propagate through adk-fluent’s meta-engineering pipeline, which parts of
the codebase absorb those changes automatically, and which require manual
intervention.
Architecture at a Glance¶
adk-fluent splits into two ownership planes:
┌─────────────────────────────────────────────────────────────────────┐
│ HANDCODED PLANE │
│ │
│ _base.py BuilderBase, operators (>> | * // @), COW │
│ _context.py C namespace — context engineering DSL │
│ _prompt.py P namespace — prompt composition DSL │
│ _transforms.py S namespace — state transform DSL │
│ _artifacts.py A namespace — artifact operations DSL │
│ _middleware.py M namespace — middleware composition DSL │
│ _tools.py T namespace — tool composition DSL │
│ _primitives.py FnAgent, TapAgent, FallbackAgent, RaceAgent… │
│ _primitive_builders.py tap(), gate(), race(), dispatch()… │
│ _routing.py Route — deterministic state-based routing │
│ _helpers.py run_one_shot, ChatSession, deep_clone… │
│ _ir.py TransformNode, TapNode, RouteNode… (hand IR) │
│ middleware.py RetryMiddleware, CostTracker, LatencyMiddleware │
│ patterns.py review_loop, map_reduce, cascade, fan_out_merge │
│ testing/ contracts.py, diagnosis.py │
│ seeds/seed.manual.toml Human-curated overrides │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ AUTO-GENERATED PLANE │
│ │
│ manifest.json ← scanner.py reads google-adk via reflection │
│ seeds/seed.toml ← seed_generator merges manifest + manual.toml │
│ agent.py (2) ← generator combines seed + manifest │
│ workflow.py (3) ← Pipeline, FanOut, Loop │
│ config.py (38) ← AgentConfig, LlmAgentConfig, RunConfig… │
│ tool.py (51) ← FunctionTool, GoogleSearchTool, MCPToolset… │
│ service.py (15) ← SessionService, ArtifactService, Memory… │
│ plugin.py (12) ← BasePlugin, LoggingPlugin… │
│ executor.py (5) ← CodeExecutor variants │
│ planner.py (3) ← BasePlanner, BuiltInPlanner │
│ runtime.py (3) ← App, Runner, InMemoryRunner │
│ _ir_generated.py ← AgentNode, SequenceNode… (frozen dataclasses)│
│ *.pyi stubs ← IDE autocomplete and type checking │
│ tests/generated/ ← Equivalence test scaffolds │
│ docs/generated/ ← API reference, migration guides │
│ CLAUDE.md, .cursor/rules, .windsurfrules, etc. │
│ │
│ Total: 132 builders across 9 modules │
└─────────────────────────────────────────────────────────────────────┘
The Meta-Engineering Pipeline¶
The generation pipeline has five stages. Each stage’s output feeds the next:
┌──────────────┐
│ google-adk │ (installed pip package)
└──────┬───────┘
│
┌────────────▼─────────────┐
Stage 1 │ scripts/scanner.py │ Reflection-based introspection
│ pkgutil.walk_packages() │ of all Pydantic BaseModel classes
└────────────┬─────────────┘
│
┌──────▼───────┐
│ manifest.json │ Machine truth: 443 classes, 779 fields
└──────┬───────┘
│
┌────────────▼─────────────┐ ┌───────────────────┐
Stage 2 │ scripts/seed_generator/ │◄───│ seed.manual.toml │
│ classifier → aliases → │ │ (human overrides) │
│ extras → emitter │ └───────────────────┘
└────────────┬─────────────┘
│
┌──────▼───────┐
│ seed.toml │ Human intent + machine truth merged
└──────┬───────┘
│
┌────────────▼─────────────┐
Stage 3 │ scripts/generator/ │
│ spec → ir_builders → │
│ module_builder → emit │
└────────────┬─────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ .py code │ │ .pyi stubs│ │ tests/ │
│ (builders)│ │ (types) │ │generated │
└──────────┘ └──────────┘ └──────────┘
Stage 4 scripts/doc_generator.py → docs/generated/
Stage 5 scripts/llms_generator.py → CLAUDE.md, editor rules
Stage 1: Scanner — The Reflection Engine¶
scripts/scanner.py uses pkgutil.walk_packages() to discover every
module under google.adk, then inspects every public class to extract:
Extracted |
How |
Used For |
|---|---|---|
Class names & qualnames |
|
Import paths in generated code |
Pydantic fields |
|
Builder setter methods |
Field types |
|
Type annotations in stubs |
Defaults |
|
Optional vs required detection |
Callbacks |
Heuristic: |
Additive |
Inheritance (MRO) |
|
Field origin tracking |
Init signatures |
|
Non-Pydantic class support |
Docstrings |
|
Generated API docs |
The scanner also has a diff engine (diff_manifests()) that compares
a previous manifest to the current one, reporting added/removed classes and
fields. This powers just diff and just diff-md.
Stage 2: Seed Generator — Classification & Policy¶
The seed generator transforms raw manifest data into builder specifications:
Classifier (
classifier.py): Tags each class by semantic role using MRO and naming conventions:BaseAgentin MRO →agentName ends with
Service→serviceName ends with
Tool/Toolset→toolName ends with
Config→configetc.
Builder-worthiness filter: Only tags in
{agent, config, runtime, executor, planner, service, plugin, tool}get builders. Classes taggedeval,auth, ordataare skipped.Field policy (
field_policy.py): Decides per-field behavior:skip: Parent references, internal fields → hidden from useradditive: Callback fields →.before_model(fn)appendslist_extend: List fields liketools,sub_agents→ extend semantics
Alias engine (
aliases.py): Derives ergonomic method names using morphological suffix rules:description→describe(suffixription→ribe)instruction→instruct(suffixruction→ruct)configuration→configure(suffixuration→ure)Plus semantic overrides:
output_key→outputs, etc.
Extras inference (
extras.py): Detects type-driven patterns and generates extra methods (e.g.,.tool(),.sub_agent(),.delegate()).Manual merge:
seed.manual.tomloverrides are applied last, providing human-curated renames (LlmAgent→Agent), optional constructor args, deprecated aliases, and hand-written extra method definitions.
Stage 3: Code Generator — IR to Python¶
The generator uses a two-phase approach:
Phase A: Spec → IR (ir_builders.py)
Each BuilderSpec is converted into a ClassNode containing:
_ALIASES,_CALLBACK_ALIASES,_ADDITIVE_FIELDSclass attributes_ADK_TARGET_CLASSpointing to the real ADK class__init__with positional constructor argsFluent setter methods for every field
Callback accumulation methods
Extra behavioral methods (
.tool(),.ask(),.stream(), etc.).build()terminal delegating to_safe_build()
Phase B: IR → Python (code_ir/emitters.py)
The emit_python() function renders ClassNode IR into formatted Python
source. The emitter integrates ruff formatting inline, so output is
always canonical.
Stage 4-5: Docs & LLM Context¶
Downstream generators read the same seed + manifest inputs to produce:
Sphinx-compatible API reference pages
Migration guides (ADK class → adk-fluent builder mapping)
Cookbook examples with side-by-side comparisons
LLM context files for every major AI coding tool
Impact Classification: What Happens When ADK Changes¶
Category 1: New Classes Added to ADK¶
Example: Google adds VertexAiSearchToolV2, a new tool class.
Impact: Fully automatic. Zero manual work.
Stage |
What Happens |
|---|---|
Scanner |
Discovers new class, adds to manifest.json |
Seed Generator |
Classifies as |
Code Generator |
Emits new |
Stubs |
New |
Tests |
New equivalence test scaffold |
Docs |
New API reference entry |
Action required: just all && just test — that’s it.
Exception: If the new class requires special treatment (custom
constructor, semantic renames, extra methods), add entries to
seeds/seed.manual.toml and re-run just seed && just generate.
Category 2: New Fields Added to Existing Classes¶
Example: Google adds reasoning_effort: str to LlmAgent.
Impact: Fully automatic.
Stage |
What Happens |
|---|---|
Scanner |
Captures new field in manifest.json under |
Seed Generator |
Includes field in builder spec; applies alias rules |
Code Generator |
Emits |
BuilderBase |
|
The new field is accessible two ways:
Via the generated explicit setter method (after regeneration)
Via
BuilderBase.__getattr__dynamic forwarding (works immediately even before regeneration, since it validates againstmodel_fields)
Action required: just all to get explicit methods and type stubs.
Category 3: Fields Removed from Existing Classes¶
Example: Google removes deprecated_field from RunConfig.
Impact: Partially automatic. May require manual cleanup.
Stage |
What Happens |
|---|---|
Scanner |
Field disappears from manifest.json |
Diff engine |
|
Seed Generator |
No longer includes field in spec |
Code Generator |
No longer emits setter method |
Type stubs |
Removed from |
Risk: User code calling .deprecated_field() will break at the
builder level (method not found) rather than at ADK construction time.
The diff engine’s "breaking": true flag surfaces this.
Action required:
just archive(saves current manifest asmanifest.previous.json)just scan && just diff-md(generates Markdown changelog)Review breaking changes
just all && just testUpdate user-facing migration guides if needed
Category 4: Fields Renamed in Existing Classes¶
Example: Google renames output_key → output_state_key on LlmAgent.
Impact: Requires manual intervention in seed.manual.toml.
Component |
Impact |
|---|---|
Scanner |
Sees removal of |
Seed generator |
Generates new alias for |
Existing aliases |
|
User code |
|
Action required:
Update alias in
seed.manual.toml:outputs = "output_state_key"Add deprecated alias to preserve backward compatibility:
[builders.Agent.deprecated_aliases] output_key = { field = "output_state_key", use = "outputs" }
just seed && just generate && just test
Category 5: Class Renamed or Moved¶
Example: Google moves google.adk.tools.function_tool.FunctionTool
to google.adk.tools.core.FunctionTool.
Impact: Automatic for the scanner (uses qualname), but seed.manual.toml
entries keyed by source_class need updating.
Component |
Impact |
|---|---|
Scanner |
Discovers class at new qualname |
Seed generator |
Matches by qualname; if manual entries used old qualname, they won’t match |
Generated imports |
Automatically use new import path |
Action required: Update source_class in seed.manual.toml if the
class was manually configured. The scanner falls back to matching by
short class name, so most cases resolve automatically.
Category 6: Inheritance Hierarchy Changes¶
Example: Google inserts a new base class EnhancedAgent between
BaseAgent and LlmAgent, adding new fields.
Impact: Mostly automatic.
Component |
Impact |
|---|---|
Scanner |
Captures new MRO chain; new inherited fields appear |
Classifier |
Still tags as |
Field origin |
|
Generator |
Emits setters for all fields (own + inherited) |
Action required: just all. New inherited fields get methods
automatically. If the new base class itself deserves a builder, the
classifier handles it.
Category 7: Callback Signature Changes¶
Example: Google changes before_model_callback from
Callable[[CallbackContext], Content] to
Callable[[CallbackContext, ModelConfig], Content].
Impact: Transparent to the generated code, but affects handcoded
callback composition in _base.py.
Component |
Impact |
|---|---|
Scanner |
Captures new type string |
Generator |
Updates type annotation in generated setter |
Type stubs |
Updated |
|
|
Action required:
just allfor type stub updatesReview
_base.py:_compose_callbacks()if composition logic is signature-sensitiveReview handcoded middleware in
middleware.pythat wraps callbacks
Category 8: New Pydantic Validators Added¶
Example: Google adds a validator that enforces
max_iterations >= 1 on LoopAgent.
Impact: Transparent to generators. Surfaced at .build() time.
Component |
Impact |
|---|---|
Scanner |
Captures validator names in manifest |
Generator |
No change needed (validators aren’t generated) |
|
Catches |
|
Pre-flight checks can catch this before build |
Action required: None. The existing error handling in BuilderBase._safe_build()
already wraps Pydantic ValidationError with user-friendly diagnostics.
Category 9: Breaking API Changes (Major Version Bump)¶
Example: ADK 2.0 replaces Pydantic v1 models with v2, renames
LlmAgent to Agent, restructures the module tree.
Impact: Significant manual work, but the pipeline absorbs most of it.
Component |
Impact |
|---|---|
Scanner |
Must handle Pydantic v2 API ( |
Scanner |
New module paths discovered automatically |
Seed generator |
Renames and overrides in |
|
|
|
Pydantic v2 |
Action required:
Update scanner if Pydantic introspection API changed
just scanto get new manifestReview
seed.manual.tomlfor stale entriesjust all && just testUpdate handcoded files that import ADK types directly
Handcoded Components: Upstream Sensitivity Matrix¶
Each handcoded file has different sensitivity to upstream changes:
File |
Sensitivity |
What Would Break It |
|---|---|---|
|
Medium |
Changes to Pydantic |
|
Low |
Changes to ADK’s |
|
Low |
Changes to ADK’s |
|
Low |
Changes to ADK’s session state API ( |
|
Low |
Changes to ADK’s artifact service API ( |
|
Low |
Changes to ADK’s callback protocol signatures |
|
Low |
Changes to |
|
High |
Changes to |
|
High |
Changes to |
|
Medium |
Changes to how |
|
Low |
Changes to agent class names (cosmetic only) |
|
Medium |
Changes to ADK’s callback/event lifecycle |
|
Low |
Composed from other handcoded components; transitive sensitivity |
|
High |
Direct ADK API surface adapter; any restructuring breaks it |
The Highest-Risk Handcoded Files¶
_primitives.py — Defines custom BaseAgent subclasses (FnAgent,
TapAgent, FallbackAgent, MapOverAgent, RaceAgent, etc.) that
override _run_async_impl(). Any change to ADK’s agent execution
protocol directly breaks these.
_helpers.py — Contains execution helpers (run_one_shot,
run_stream, ChatSession) that instantiate ADK Runner and
InMemorySessionService directly. Changes to these ADK APIs require
manual updates.
_base.py — The _prepare_build_config() method contains
logic that introspects ADK class fields at build time. Changes to
Pydantic’s model API or ADK’s field naming conventions affect this.
The __getattr__ dynamic forwarding validates against
_ADK_TARGET_CLASS.model_fields.
Generator Components: What Would We Change¶
When upstream ADK changes require generator modifications, here’s what to touch and why:
Scanner Changes (scripts/scanner.py)¶
Upstream Change |
Scanner Modification |
|---|---|
New inspection pattern (not Pydantic, not |
Add new |
New field metadata (e.g., field groups, access level) |
Extend |
Module restructuring |
No change — auto-discovery via |
New base class hierarchy |
No change — MRO is captured automatically |
Non-public API surfacing |
Update |
Seed Generator Changes (scripts/seed_generator/)¶
Upstream Change |
Seed Generator Modification |
|---|---|
New semantic category of classes |
Add tag to |
New field naming conventions |
Update |
New callback patterns |
Update |
New field policy needs |
Update |
New method generation patterns |
Add to |
Code Generator Changes (scripts/generator/)¶
Upstream Change |
Generator Modification |
|---|---|
New builder behavior pattern |
Add new |
New code IR node type needed |
Add to |
Change to build protocol |
Update |
New import resolution needs |
Update |
New type normalization |
Update |
Change to stub format |
Update |
Seed Manual Overrides (seeds/seed.manual.toml)¶
This is the human intent layer — the file where developers express decisions that can’t be inferred automatically:
# Rename ADK class → fluent builder name
[renames]
LlmAgent = "Agent"
SequentialAgent = "Pipeline"
ParallelAgent = "FanOut"
LoopAgent = "Loop"
# Optional constructor args (not required, but convenient)
[builders.Agent]
optional_constructor_args = ["model"]
# Hand-written extra methods
[[builders.Agent.extras]]
name = "tool"
signature = "(self, fn_or_tool: Any) -> Self"
behavior = "list_append"
target_field = "tools"
doc = "Add a tool (function or BaseTool instance)."
# Deprecated aliases with migration path
[builders.Agent.deprecated_aliases]
save_as = { field = "output_key", use = "writes" }
output_schema = { field = "output_schema", use = "returns" }
When ADK adds a class that needs special treatment beyond what the seed generator infers, add entries here.
Upgrade Runbook: Step-by-Step¶
When a new ADK version is released:
# 1. Archive current state for diff comparison
just archive
# 2. Upgrade ADK
pip install --upgrade google-adk
# 3. Scan the new ADK
just scan
# 4. Review what changed
just diff # JSON summary
just diff-md # Publishable Markdown changelog
# 5. Check if seed.manual.toml needs updates
# (renamed classes, moved fields, new extras)
# Edit seeds/seed.manual.toml if needed
# 6. Regenerate everything
just all
# 7. Verify
just test # Run full test suite
just typecheck # Verify type stubs
just ci # Full local CI
# 8. Review generated diff
git diff --stat
# 9. Commit
git add -A && git commit -m "chore: upgrade to google-adk X.Y.Z"
When just diff Reports Breaking Changes¶
If the diff output shows "breaking": true:
Check
removed_classes— do we have manual overrides for them?Check
removed_fields— do we have aliases pointing to them?Update
seed.manual.tomlto add deprecated aliases for removed fields (smooth migration for downstream users)Review handcoded files in the sensitivity matrix above
Run
just all && just testto verify
Design Principles of the Meta-Engineering Pattern¶
1. Machine Truth + Human Intent = Generated Code¶
The pipeline separates concerns:
manifest.json captures what ADK exposes (machine truth via reflection)
seed.manual.toml captures what we want (human intent via curation)
The generator combines them deterministically
This means upstream changes are absorbed by re-scanning (new machine truth), and human decisions persist across regenerations (stable human intent).
2. Idempotent & Deterministic¶
Running just generate twice produces identical output. This is enforced
by the just check-gen CI gate, which regenerates and diffs.
3. Escape Hatch: Dynamic Forwarding¶
BuilderBase.__getattr__ validates method calls against the ADK target
class’s model_fields at runtime. This means new ADK fields work
immediately even before regeneration — the generated explicit methods
are an optimization for discoverability and type safety, not a requirement.
4. IR-Based Code Generation¶
The generator doesn’t emit Python strings directly. It builds an
intermediate representation (ClassNode, MethodNode, etc. in
scripts/code_ir/nodes.py) and then emits from that IR. This enables:
Multiple output formats from the same IR (
.py,.pyi, tests)Structural validation before emission
Consistent formatting (ruff is applied to emitter output)
5. Progressive Enhancement¶
The codebase layers capabilities:
Auto-generated builders — zero-effort field access for any ADK class
Handcoded DSLs (S, C, P, A, M, T) — rich composition on top
Expression operators (>>, |, *, //, @) — concise syntax on top
Patterns (review_loop, cascade) — reusable architectures on top
Upstream changes only affect layer 1. Layers 2-4 are stable across ADK versions because they compose builders, not ADK internals directly.