Skip to content

Development

Day-to-day development workflow, code quality, testing, and Docker.

Important

Configure cloud resource values in .env after first deployment. See Getting Started for initial setup and Environment Variables for complete reference.

Quick Start

Local-only (cloud resource URIs commented out in .env):

uv run server                              # API at 127.0.0.1:8000 (SQLite sessions, in-memory storage)
SERVE_WEB_INTERFACE=TRUE uv run server     # With ADK web UI

Standard development (mirrors production, requires deployed cloud resource URIs configured in .env):

docker compose up --build --watch          # Cloud SQL (via IAP tunnel), Agent Engine, GCS

Both modes always export traces and structured logs to Cloud Trace and Cloud Logging. Set TELEMETRY_NAMESPACE in .env to a unique value (e.g., your name) so teammates can filter to your traces when reviewing experiments.

Prerequisites:
- .env file configured with required authentication and identification values (copy from .env.example)
- gcloud auth application-default login (for Vertex AI and telemetry)

See Getting Started for initial setup.

Environment Setup

Configure your local .env file after completing your first deployment. The deployed resources provide production-ready persistence for sessions, memory, and artifacts.

1. Create .env File

cp .env.example .env

2. Configure Environment Variables

Edit .env and configure required values. The .env.example file includes inline comments for each variable.

Required configuration:
- Google Cloud Vertex AI credentials
- Agent identification
- OpenTelemetry settings

Recommended (after first deployment):
- BASTION_INSTANCE - Bastion host name for IAP tunnel (docker-compose)
- BASTION_ZONE - Bastion host zone for IAP tunnel (docker-compose)
- SESSION_SERVICE_URI - Session persistence (Cloud SQL)
- MEMORY_SERVICE_URI - Memory persistence (Agent Engine)
- ARTIFACT_SERVICE_URI - Artifact storage

See Environment Variables for complete reference.

3. Verify Configuration

# Check auth
gcloud auth application-default login

# Verify with Docker Compose (starts server and connects to cloud resources configured above)
docker compose up --build --watch

See Environment Variables for complete reference.

Feature Branch Workflow

# Create branch (feat/, fix/, docs/, refactor/, test/)
git checkout -b feat/your-feature-name

# Develop locally
docker compose up --build --watch  # Standard (cloud resources, mirrors production)
# Or: uv run server                # Local-only with cloud resource URIs commented out in .env

# Quality checks before commit (100% coverage required)
# Pre-commit automates format/lint/typecheck if installed
uv run ruff format && uv run ruff check && uv run mypy
uv run pytest --cov --cov-report=term-missing

# Commit (Conventional Commits: 50 char title, list body)
git add . && git commit -m "feat: add new tool"

# Push and create PR
git push origin feat/your-feature-name
gh pr create  # Follow PR format: What, Why, How, Tests

# After merge to main, monitor deployment
gh run list --workflow=ci-cd.yml --limit 5
gh run view --log

GitHub Actions automatically builds, tests, and deploys to Cloud Run. Check job summary for deployment details.

Code Quality & Testing

Run format, lint, type check, and unit tests (100% coverage required) before every commit

Standards:
- Type Hints: Strict mypy, modern Python 3.13+ syntax (| unions, lowercase generics)
- Code Style: Ruff enforced (88-char lines, Path objects, security checks)
- Docstrings: Google-style format (args, returns, exceptions)
- Testing: 100% coverage on production code, exclusions for configuration modules, fixtures in conftest.py, test behaviors and errors

Pre-commit Hooks

Pre-commit runs format, lint, and type checks as a fix-in-place git hook on every git commit. It does not run pytest, so coverage validation still requires uv run pytest --cov.

Coverage spans Python (ruff, mypy), Terraform (terraform fmt), GitHub Actions workflows (actionlint), and the YAML/TOML/JSON config files.

Workflow linting runs in two places on purpose. The hook catches problems before you push; the actionlint job in ci.yml is the gate that actually blocks a merge, since a hook only fires for contributors who ran pre-commit install and --no-verify skips it. Both pin the same version and pass the same flags, so a clean commit means a clean gate. Bump them together.

terraform validate is deliberately not a hook: it needs terraform init per root, which is too slow for a commit. CI validates instead. And note that CI's terraform fmt -check runs against terraform/main only, so the hook is the sole formatting gate for terraform/bootstrap/**.

Local tooling: the Terraform hook calls the terraform binary, already required for infrastructure work. actionlint needs nothing installed — pre-commit builds it, using the Go toolchain on PATH if there is one and otherwise downloading one into its own cache.

One-time setup (writes .git/hooks/pre-commit):

uv run pre-commit install

Manual invocation:

uv run pre-commit run --all-files              # All hooks on all tracked files
uv run pre-commit run --files path/to/file     # Scope to specific files
uv run pre-commit run ruff-format              # Run a single hook

Hooks are configured in .pre-commit-config.yaml. pre-commit autoupdate is a no-op for the language: system hooks, so update those at their own source:

Hook Version comes from Update with
ruff, mypy uv.lock uv lock --upgrade
terraform fmt the terraform binary on PATH your system package manager
actionlint the pinned rev here and the docker://rhysd/actionlint:<ver> tag in ci.yml bump both together; tests/unit/test_pinned_versions.py fails if they drift
everything else the pinned rev in .pre-commit-config.yaml pre-commit autoupdate

Agent Evals

Run the deterministic agent eval gate before merging changes that touch agent behavior (instructions, tools, callbacks, model):

uv run pytest tests/eval -m "deterministic"  # real model inference, deterministic scoring; needs Vertex creds from .env

See Agent Evals for the full evaluation suite reference.

See Testing Strategy and Code Quality references for detailed patterns, tool usage, and exclusion strategies.

Docker Compose for Standard Development

Docker Compose reads .env values and mirrors the production Cloud Run architecture: IAP tunnel to bastion → Auth Proxy → Cloud SQL private IP, with file sync and auto-restart (~2-5s feedback loop).

# Start with watch mode (leave running)
docker compose up --build --watch

# Changes in src/ sync instantly
# Changes in pyproject.toml or uv.lock trigger rebuild

# Stop: Ctrl+C or docker compose down

Important

Docker Compose requires roles/iap.tunnelResourceAccessor on your Google account for IAP tunnel access. Grant via gcloud projects add-iam-policy-binding or GCP Console.

Key details:
- IAP tunnel container connects to bastion host running Auth Proxy, sharing app's localhost via network_mode: "service:app"
- App connects to localhost:5432 identically to Cloud Run
- Source files sync to container without rebuild (instant feedback)
- Loads .env automatically for configuration
- Multi-stage Dockerfile optimized with uv cache mounts (~80% faster rebuilds)
- Non-root container (~200MB final image)

Local-Only (uv run server)

uv run server reads .env like Docker Compose. With cloud resource URIs set, it connects to those services. For fully local development, comment out SESSION_SERVICE_URI, MEMORY_SERVICE_URI, and ARTIFACT_SERVICE_URI in .env. Sessions persist locally in SQLite (.adk/ directory). Traces and logs still export to Cloud Trace and Cloud Logging.

uv run server                              # Uses cloud resources if URIs set, local storage if unset
SERVE_WEB_INTERFACE=TRUE uv run server     # With ADK web UI
LOG_LEVEL=DEBUG uv run server              # Debug logging

You can also connect uv run server to Cloud SQL, Agent Engine, or GCS selectively by setting individual service URI environment variables. See Cloud Backend Options for manual IAP tunnel setup and selective backend options.

See Docker Compose Workflow and Dockerfile Strategy for details on watch mode, volumes, layer optimization, and security.

Common Tasks

Dependencies

# Add runtime dependency
uv add package-name

# Add dev dependency
uv add --group dev package-name

# Update all dependencies
uv lock --upgrade

# Update specific package
uv lock --upgrade-package package-name

# After updating pyproject.toml or uv.lock:
# - Locally: Restart server or Docker Compose (auto-rebuild with watch)
# - CI/CD: Commit both files together (required for --locked to pass)

Version Bump

When bumping version in pyproject.toml:

# Edit version in pyproject.toml
# Then update lockfile
uv lock

# Commit both together
git add pyproject.toml uv.lock
git commit -m "chore: bump version to X.Y.Z"

Why: CI uses uv sync --locked which will fail if lockfile is out of sync.

Custom GCP Services or IAM Roles

Need a new GCP API or WIF IAM role for your feature? Add it to the designated main module extension points — no bootstrap re-run or admin required.

  • terraform/main/services.tf — add to the services set to enable a GCP API
  • terraform/main/iam.tf — add to the wif_additional_roles set to grant the GitHub Actions WIF principal a new role

Add the roles or services AND the resources that use them in the same PR. The time_sleep resources handle propagation within the apply — declare depends_on on the correct sleep instance(s) for any resource that requires a newly-enabled service or role.

See Extending the Main Module for the full pattern, code examples, and rationale.

Test Deployed Service

Proxy Cloud Run service to test locally:

# Service name format: ${agent_name}-${environment}
gcloud run services proxy <service-name> \
  --project <project-id> \
  --region <region> \
  --port 8000

# Test
curl http://localhost:8000/health

# With web UI (if SERVE_WEB_INTERFACE=TRUE)
open http://localhost:8000

# Stop proxy: Ctrl+C

See Cloud Run proxy documentation.

Observability

Server Logs:
- Print to stdout (via LoggingPlugin callbacks)
- Basic request/response logging for immediate feedback

Opentelemetry Traces and Logs:
- Detailed traces → Cloud Trace
- Structured logs → Cloud Logging
- Full correlation between traces and logs

See Observability for querying, filtering, and trace analysis.

Project Structure

your-agent-name/
  src/your_agent_name/
    agent.py              # ADK App and LlmAgent configuration
    callbacks.py          # Agent lifecycle callbacks (logging, safety, tool augmentation)
    prompt.py             # Agent instructions (InstructionProvider pattern)
    config.py             # Typed env-var validation (Pydantic ServerEnv)
    observability.py      # OpenTelemetry initialization
    server.py             # FastAPI composition root (services, DI, routing)
    tools.py              # Custom FunctionTools for agent use
  tests/                  # Test suite (unit/integration/smoke/eval lanes)
    conftest.py           # Shared fixtures, test env setup (all lanes)
    unit/test_*.py        # Default lane; modules mirror source structure
  terraform/              # Infrastructure as code
    bootstrap/{env}       # One-time CI/CD setup per environment
    main/                 # Agent cloud resource deployment
  docs/                   # Documentation
  .env.example            # Environment template
  pyproject.toml          # Project configuration
  docker-compose.yml      # Local development
  Dockerfile              # Container image
  AGENTS.md               # AI Coding Agent instructions
  CLAUDE.md               # Imports AGENTS.md for Claude Code
  README.md               # Main documentation

Back to Documentation