Scope: This document is for AI coding agents contributing to the Agent Starter Pack repository itself (the template generator). For guidance on working with generated projects, see llm.txt.
This document provides essential guidance, architectural insights, and best practices for AI coding agents tasked with modifying the Google Cloud Agent Starter Pack. Adhering to these principles is critical for making safe, consistent, and effective changes.
- Preserve and Isolate: Your primary objective is surgical precision. Modify only the code segments directly related to the user's request. Preserve all surrounding code, comments, and formatting. Do not rewrite entire files or functions to make a small change.
- Follow Conventions: This project relies heavily on established patterns. Before writing new code, analyze the surrounding files to understand and replicate existing conventions for naming, templating logic, and directory structure.
- Template-First Mindset: ASP is a template generator. The CLI should remain lean with good defaults. Most features belong in templates, not CLI code.
- Search Comprehensively: A single change often requires updates in multiple places. When modifying configuration, variables, or infrastructure, you must search across the entire repository, including:
agent_starter_pack/base_templates/(core templates by language)agent_starter_pack/deployment_targets/(environment-specific overrides).github/and.cloudbuild/(CI/CD workflows)docs/(user-facing documentation)
Template processing follows this hierarchy (later layers override earlier ones):
| Layer | Directory | Purpose |
|---|---|---|
| 1. Base | agent_starter_pack/base_templates/<language>/ |
Core Jinja scaffolding (Python, Go, more coming) |
| 2. Deployment | agent_starter_pack/deployment_targets/ |
Environment overrides (cloud_run, gke, agent_engine) |
| 3. Frontend | agent_starter_pack/frontends/ |
UI-specific files |
| 4. Agent | agent_starter_pack/agents/*/ |
Agent-specific logic and configurations |
Rule: Always place changes in the correct layer. Check if deployment targets need corresponding updates.
agent_starter_pack/
├── agents/ # Agent-specific files
│ ├── adk/ # Base ADK agent (Python)
│ ├── adk_a2a/ # A2A-enabled ADK agent
│ ├── adk_go/ # Base ADK agent (Go)
│ ├── adk_live/ # Real-time multimodal agent
│ ├── agentic_rag/ # RAG agent
│ └── langgraph/ # LangGraph-based agent
├── base_templates/ # Core Jinja templates by language
│ ├── python/ # Python project template
│ │ ├── {{cookiecutter.agent_directory}}/
│ │ ├── deployment/
│ │ ├── tests/
│ │ └── Makefile
│ └── go/ # Go project template
├── deployment_targets/ # Environment-specific overrides
│ ├── agent_engine/ # Agent Engine deployment
│ ├── cloud_run/ # Cloud Run deployment
│ └── gke/ # GKE Autopilot deployment
├── frontends/ # UI templates
└── cli/ # CLI implementation
├── commands/ # create, setup-cicd, enhance, etc.
└── utils/ # Template processing, helpers
| Change Type | Where to Modify | Also Check |
|---|---|---|
| Affects ALL generated projects | base_templates/<language>/ |
Deployment targets for conflicts |
| Deployment-specific logic | deployment_targets/<target>/ |
Base templates for shared code |
| Agent-specific feature | agents/<agent>/ |
Other agents for consistency |
| New CLI flag/command | cli/commands/ |
cli/utils/ for shared logic |
| CI/CD changes | Both .github/ AND .cloudbuild/ |
Keep in sync |
| Documentation | docs/ |
README.md for overview changes |
- Variable resolution from
cookiecutter.json - File copying (base → deployment → frontend → agent overlays)
- Jinja2 rendering of file content
- File/directory name rendering (Jinja in filenames)
Changes often require coordinated updates:
- Configuration:
templateconfig.yaml→cookiecutter.json→ rendered templates - CI/CD:
.github/workflows/↔.cloudbuild/(must stay in sync) - Infrastructure: Base terraform → deployment target overrides
Template changes require a specific workflow because you're modifying Jinja templates, not regular source files.
Note: This workflow applies to both Python and Go templates. Both use Jinja templating with the same patterns (
{{cookiecutter.*}},{% if %}, etc.).
uv run agent-starter-pack create mytest -p -s -y -d cloud_run --output-dir targetFlags explained:
-p/--prototype: Minimal project (no CI/CD or Terraform)-s/--skip-checks: Skip GCP/Vertex AI verification-y/--auto-approve: Skip all confirmation prompts-d: Deployment target--output-dir target: Output to target/ (gitignored)
cd target/mytest && git init && git add . && git commit -m "Initial"This creates a baseline for tracking your changes with git diff.
- Make changes directly in
target/mytest/ - Test immediately:
make lint, run the code, check output - Iterate until the change works correctly
- Use
git diffto see exactly what you changed
- Find the source template:
find agent_starter_pack -name "filename.py" -type f - Apply your changes to the template file
- Add Jinja conditionals if the change is conditional
- Use
{%- -%}whitespace control carefully
# Test your target combination
_TEST_AGENT_COMBINATION="adk,cloud_run,--session-type,in_memory" make lint-templated-agents
# Test alternate agent with same deployment
_TEST_AGENT_COMBINATION="adk_live,cloud_run" make lint-templated-agents
# Test same agent with alternate deployment
_TEST_AGENT_COMBINATION="adk,agent_engine" make lint-templated-agents- Jinja templates are harder to debug than rendered code
- Generated projects give immediate feedback on syntax errors
- Backporting ensures you understand exactly what changed
- Cross-combination testing catches conditional logic bugs
The template development workflow above applies to all languages. Here are language-specific details:
| Language | Agent(s) | Linter | Test Command | Status |
|---|---|---|---|---|
| Python | adk, adk_a2a, adk_live, agentic_rag, langgraph, custom_a2a | ruff, ty |
make lint |
Production |
| Go | adk_go | golangci-lint |
make lint |
Production |
| Java | (coming soon) | - | - | In development |
| TypeScript | (coming soon) | - | - | In development |
Python example:
uv run agent-starter-pack create mytest -a adk -d cloud_run -p -s -y --output-dir targetGo example:
uv run agent-starter-pack create mytest -a adk_go -d cloud_run -p -s -y --output-dir targetNote: These rules apply to both Python and Go templates. Both languages use the same Jinja2 templating patterns.
The starter pack uses Cookiecutter to generate project scaffolding from templates customized with Jinja2. Understanding the rendering process is key to avoiding errors.
Multi-Phase Template Processing:
- Cookiecutter Variable Substitution: Replacement of
{{cookiecutter.variable_name}}placeholders - Jinja2 Logic Execution: Conditional blocks (
{% if %}), loops ({% for %}) - File/Directory Name Templating: Jinja2 in filenames is rendered
Every opening Jinja block must have a corresponding closing block.
{% if ... %}requires{% endif %}{% for ... %}requires{% endfor %}{% raw %}requires{% endraw %}
{% if cookiecutter.deployment_target == 'cloud_run' %}
# Cloud Run specific content
{% endif %}Distinguish between substitution and logic:
- Substitution (in file content):
{{ cookiecutter.project_name }} - Logic (in
if/forblocks):{% if cookiecutter.session_type == 'cloud_sql' %}
Jinja is sensitive to whitespace. Use hyphens to control newlines:
{%-removes whitespace before the block-%}removes whitespace after the block{%- -%}removes whitespace on both sides
{%- if cookiecutter.some_option %}
option = true
{%- endif %}{%- if cookiecutter.agent_name == "adk_live" %}
# Agent-specific logic
{%- elif cookiecutter.deployment_target == "cloud_run" %}
# Deployment-specific logic
{%- endif %}Jinja2 whitespace control is the #1 source of linting failures. Understanding these patterns is essential.
Problem: Python requires blank lines to separate third-party imports from project imports. Conditional imports must handle this correctly.
Wrong - Creates extra blank line:
from opentelemetry.sdk.trace import TracerProvider, export
{% if cookiecutter.session_type == "agent_engine" %}
from vertexai import agent_engines
{% endif %}
from app.app_utils.gcs import create_bucket_if_not_existsCorrect - Exactly one blank line:
from opentelemetry.sdk.trace import TracerProvider, export
{% if cookiecutter.session_type == "agent_engine" -%}
from vertexai import agent_engines
{% endif %}
{%- if cookiecutter.is_a2a %}
from {{cookiecutter.agent_directory}}.agent import app as adk_app
{% endif %}
from {{cookiecutter.agent_directory}}.app_utils.gcs import create_bucket_if_not_existsKey points:
- Use
{%- -%}to control BOTH sides when needed - The blank line AFTER the conditional import goes INSIDE the if block when needed
- Test BOTH when condition is true AND false
Problem: Ruff enforces line length limits. Long import statements must be split.
Wrong - Too long:
from app.app_utils.typing import Feedback, InputChat, Request, dumps, ensure_valid_configCorrect - Split with parentheses:
from app.app_utils.typing import (
Feedback,
InputChat,
Request,
dumps,
ensure_valid_config,
)Problem: Ruff requires exactly ONE newline at the end of every file.
Wrong - No newline:
agent_engine = AgentEngineApp(project_id=project_id)
{%- endif %}Wrong - Extra newline:
agent_engine = AgentEngineApp(project_id=project_id)
{%- endif %}
Correct - Exactly one:
agent_engine = AgentEngineApp(project_id=project_id)
{%- endif %}Key for nested conditionals:
agent_engine = AgentEngineApp(
app=adk_app,
artifact_service_builder=artifact_service_builder,
)
{%- endif -%}
{% else %}
import loggingNotice {%- endif -%} to prevent blank line before the else block.
# Remove whitespace BEFORE the tag
{%- if condition %}
# Remove whitespace AFTER the tag
{% if condition -%}
# Remove whitespace on BOTH sides
{%- if condition -%}
# Typical pattern for conditional imports
{% if condition -%}
import something
{% endif %}
# Typical pattern for conditional code blocks with blank line before
{%- if condition %}
some_code()
{%- endif %}
# Pattern for preventing blank line between consecutive conditionals
{%- endif -%}
{%- if next_condition %}Critical Principle: Template changes can affect MULTIPLE agent/deployment combinations. Test across combinations when making template modifications.
| Dimension | Options |
|---|---|
| Agents | adk, adk_a2a, adk_go, adk_live, agentic_rag, langgraph |
| Deployments | cloud_run, gke, agent_engine |
| Session types | in_memory, cloud_sql, agent_engine |
| Features | data_ingestion, frontend_type |
- Your target combination
- One alternate agent with same deployment target
- One alternate deployment target with same agent
IMPORTANT: Only run linting when explicitly requested by the user. Do not proactively lint unless asked.
# Linting a specific combination
_TEST_AGENT_COMBINATION="agent,target,--param,value" make lint-templated-agents
# Testing a specific combination
_TEST_AGENT_COMBINATION="agent,target,--param,value" make test-templated-agents# Cloud Run combinations (Python)
_TEST_AGENT_COMBINATION="adk,cloud_run,--session-type,in_memory" make lint-templated-agents
_TEST_AGENT_COMBINATION="adk_live,cloud_run,--session-type,in_memory" make lint-templated-agents
# Agent Engine combinations (Python)
_TEST_AGENT_COMBINATION="adk,agent_engine" make lint-templated-agents
_TEST_AGENT_COMBINATION="adk_live,agent_engine" make lint-templated-agents
_TEST_AGENT_COMBINATION="langgraph,agent_engine" make lint-templated-agents
# GKE combinations (Python)
_TEST_AGENT_COMBINATION="adk,gke,--session-type,in_memory" make lint-templated-agents
# Go template testing
_TEST_AGENT_COMBINATION="adk_go,cloud_run" make lint-templated-agents
# Go template testing (GKE)
_TEST_AGENT_COMBINATION="adk_go,gke" make lint-templated-agents
# With session type variations
_TEST_AGENT_COMBINATION="adk,cloud_run,--session-type,agent_engine" make lint-templated-agentsBefore committing ANY template change:
# 1. Test the specific combination you're working on
_TEST_AGENT_COMBINATION="adk,cloud_run,--session-type,in_memory" make lint-templated-agents
# 2. Test related combinations (same deployment, different agents)
_TEST_AGENT_COMBINATION="adk_live,cloud_run,--session-type,in_memory" make lint-templated-agents
# 3. Test alternate code paths (different deployment, session types)
_TEST_AGENT_COMBINATION="adk,cloud_run,--session-type,agent_engine" make lint-templated-agents
_TEST_AGENT_COMBINATION="adk,agent_engine" make lint-templated-agents
# 4. If modifying deployment target files, test all agents with that target
# For agent_engine changes:
_TEST_AGENT_COMBINATION="adk,agent_engine" make lint-templated-agents
_TEST_AGENT_COMBINATION="adk_live,agent_engine" make lint-templated-agents
_TEST_AGENT_COMBINATION="langgraph,agent_engine" make lint-templated-agentsGolden Rule: After ANY template change affecting imports, conditionals, or file endings, test AT LEAST 3 combinations:
- The target combination
- An alternate agent with same deployment
- An alternate deployment with same agent
# Look for the diff output in the error message
--- app/fast_api_app.py
+++ app/fast_api_app.py
@@ -21,6 +21,7 @@
from opentelemetry import trace
from vertexai import agent_engines
+
from app.app_utils.gcs import create_bucket_if_not_existsThe + line shows what Ruff WANTS to add. In this case, it wants a blank line after agent_engines.
# Generated files are in target/
cat target/project-name/app/fast_api_app.py | head -30# Find the template source
find agent_starter_pack -name "fast_api_app.py" -type f- If
{% if condition %}exists, test with condition true AND false - Use different agent combinations to toggle different conditions
| Error | Cause | Fix |
|---|---|---|
| Missing blank line between imports | Conditional import without proper spacing | Add blank line inside {% if %} block with correct {%- -%} control |
| Extra blank line between imports | Jinja block creating unwanted newline | Use {%- endif -%} to strip both sides |
| Missing newline at end of file | Template ends without final newline | Ensure template has exactly one blank line at end |
| Extra blank line at end of file | Multiple newlines or {% endif %} creating extra line |
Use {%- endif -%} pattern |
| Line too long | Import statement exceeds limit | Split into multi-line with parentheses |
-
agent_engine_app.py(deployment_targets/agent_engine/)- Multiple conditional paths (adk_live, adk_a2a, regular)
- End-of-file newline issues
-
fast_api_app.py(deployment_targets/cloud_run/)- Conditional imports (session_type, is_a2a)
- Long import lines
- Complex nested conditionals
-
Any file with
{% if cookiecutter.agent_name == "..." %}- Different agents trigger different code paths
- Must test multiple agent types
The project maintains parallel CI/CD implementations. Any change to CI/CD logic must be applied to both.
- GitHub Actions: Configured in
.github/workflows/. Uses${{ vars.VAR_NAME }}for repository variables. - Google Cloud Build: Configured in
.cloudbuild/. Uses${_VAR_NAME}for substitution variables.
When adding a new variable or secret, ensure it is configured correctly for both systems in the Terraform scripts that manage them (e.g., github_actions_variable resource and Cloud Build trigger substitutions).
The project uses a single, unified application service account (app_sa) across all deployment targets to simplify IAM management.
- Do not create target-specific service accounts (e.g.,
cloud_run_sa) - Define roles for this account in
app_sa_roles - Reference this account consistently in all Terraform and CI/CD files
Use consistent and clear naming for Terraform resources. When referencing resources, especially those created conditionally or with for_each, ensure the reference is also correctly keyed.
# Creation
resource "google_service_account" "app_sa" {
for_each = local.deploy_project_ids # e.g., {"staging" = "...", "prod" = "..."}
account_id = "${var.project_name}-app"
# ...
}
# Correct Reference
# In a Cloud Run module for the staging environment
service_account = google_service_account.app_sa["staging"].email<type>: <concise summary in imperative mood>
<detailed explanation of the change>
- Why the change was needed
- What was the root cause
- How the fix addresses it
Types: fix, feat, refactor, docs, test, chore
Title: Brief, descriptive summary (50-60 chars)
Fix Cloud Build service account permission for GitHub PAT secret access
Description:
## Summary
- Key change 1 (what was added/modified)
- Key change 2
- Key change 3
## Problem
Clear description of the issue, including:
- Error messages or symptoms
- Why it was failing
- Context about when/where it occurs
## Solution
Explanation of how the changes fix the problem:
- What resources/files were modified
- Why this approach was chosen
- Any dependencies or sequencing requirementsCommit:
Fix Cloud Build service account permission for GitHub PAT secret access
Add IAM binding to grant Cloud Build service account the secretAccessor
role for the GitHub PAT secret. This resolves permission errors when
Terraform creates Cloud Build v2 connections in E2E tests.
The CLI setup already grants this permission via gcloud, but the
Terraform configuration was missing this binding, causing failures when
Terraform runs independently.
PR Description:
## Summary
- Grant Cloud Build service account `secretmanager.secretAccessor` role
- Add proper dependency to Cloud Build v2 connection resource
## Problem
E2E tests failed when Terraform attempted to create Cloud Build v2 connections:Error: could not access secret with service account: generic::permission_denied
The CLI setup grants this permission via gcloud, but Terraform
configuration lacked the IAM binding.
## Solution
Added `google_secret_manager_secret_iam_member` resource to grant the
Cloud Build service account permission to access the GitHub PAT secret
before creating the connection.
- Concise but complete: Provide enough context for reviewers
- Problem-first: Explain the "why" before the "what"
- Professional tone: Avoid mentions of AI tools or assistants
- Jinja Syntax: All
{% if %}and{% for %}blocks correctly closed? - Variable Consistency:
cookiecutter.variables spelled correctly? - Cross-Target Impact: Base template changes checked against deployment targets?
- CI/CD Parity: Changes applied to both GitHub Actions and Cloud Build?
- Multi-Agent Testing: Tested with different agent types and configurations?
# Quick prototype project (no CI/CD, no Terraform, no prompts)
uv run agent-starter-pack create mytest -p -s -y -d agent_engine --output-dir target
# Flags explained:
# -p / --prototype : Minimal project (no CI/CD or Terraform)
# -s / --skip-checks: Skip GCP/Vertex AI verification
# -y / --auto-approve: Skip all confirmation prompts
# -d : Deployment target
# --output-dir target: Output to target/ (gitignored)# Agent Engine + prototype (fastest)
uv run agent-starter-pack create test-$(date +%s) -p -s -y -d agent_engine --output-dir target
# Cloud Run with session type
uv run agent-starter-pack create test-$(date +%s) -p -s -y -d cloud_run --session-type in_memory --output-dir target
# GKE with session type
uv run agent-starter-pack create test-$(date +%s) -p -s -y -d gke --session-type in_memory --output-dir target
# Full project with CI/CD
uv run agent-starter-pack create test-$(date +%s) -s -y -d agent_engine --cicd-runner google_cloud_build --output-dir target| File | Purpose |
|---|---|
agent_starter_pack/cli/commands/create.py |
Main create command, CLI flags, shared options |
agent_starter_pack/cli/utils/template.py |
Template processing, process_template(), CI/CD runner prompt |
agent_starter_pack/base_templates/python/pyproject.toml |
Generated project metadata, [tool.agent-starter-pack] section |
agent_starter_pack/base_templates/python/Makefile |
Generated project Makefile targets |
uvfor Python: Primary tool for dependency management and CLI execution
- Hardcoded URLs: Use relative paths for frontend connections
- Missing Conditionals: Wrap agent-specific code in proper
{% if %}blocks - Dependency Conflicts: Some agents lack certain extras (e.g., adk_live + lint)
- makefile_hashes.json: Can cause merge conflicts during active development - regenerate if needed
Generated projects store creation context in pyproject.toml:
[tool.agent-starter-pack]
# Metadata
name = "my-project"
base_template = "adk"
asp_version = "0.25.0"
[tool.agent-starter-pack.create_params]
# CLI params used during creation - used by enhance command
deployment_target = "cloud_run"
session_type = "in_memory"
cicd_runner = "skip"The create_params section enables the enhance command to recreate identical scaffolding with the locked ASP version.