aiml
SearchMy learning
Menu

Lesson 1 of 6Module 1

Scaffold, lint, and run an ADK project with Google Agents CLI

Scaffold a prototype ADK agent with agents-cli, inspect agents-cli-manifest.yaml, run agents-cli lint, and serve multi-turn ADK and A2A sessions locally.

Prerequisites

Python 3.12, Google Agents CLI (agents-cli), and basic familiarity with Google ADK agents and function tools.

By the end, you can

  • Scaffold an ADK prototype project with agents-cli scaffold create and explain each field in agents-cli-manifest.yaml.
  • Keep root_agent, App(name='app'), and agent_directory aligned across the ADK SSE server and the A2A agent card endpoint.
  • Validate code formatting and types with agents-cli lint and execute a multi-turn local session with agents-cli run --session-id.
Runtime
Python 3.12+, Google Agents CLI 1.11.0, and Vertex AI or a Gemini API key
Dependencies
google-agents-cli 1.11.0, google-adk 2.13.0

Lab code, tests and fixtures (ZIP, 8 KB)Quiz check your understanding

Progress is saved in this browser.

Create a docuctl documentation agent from an Agents CLI template, connect its documentation tool, and inspect a multi-turn local session. The downloaded lab also checks project naming, application alignment and the saved conversation without making a model request.

Problem

docuctl is a fictional documentation example, not a CLI you need to install. Commands in its documentation are data for the assistant to explain.

When you need this. Writing an ADK Agent in a single Python file lets you test prompts and tools, but moving that agent toward evaluation pipelines, Agent2Agent (A2A) discovery, and cloud deployment requires a standard project layout, HTTP entrypoint, and quality checks. In this lesson, you will scaffold the docuctl-docs project with Google Agents CLI (agents-cli 1.11.0), wire the docuctl documentation corpus into read_page, pass agents-cli lint, and run a multi-turn session against the local FastAPI and A2A server.

Concepts

Google Agents CLI (google-agents-cli, invoked as agents-cli) manages the full agent lifecycle—scaffolding, linting, running, evaluating, deploying, and publishing—around one project manifest (agents-cli-manifest.yaml). Rather than writing Terraform and CI workflows before your agent logic works, you start in prototype mode (--prototype) and add infrastructure later in place.

Prototype mode (--prototype)
Generates only the runnable Python application (app/agent.py, app/fast_api_app.py, pyproject.toml, Dockerfile, and tests/) and sets deployment_target: none and cicd_runner: skip in agents-cli-manifest.yaml.
Project manifest (agents-cli-manifest.yaml)
The single source of truth read by agents-cli info, run, eval, scaffold upgrade, and deploy to locate agent_directory (app), acli_version (1.11.0), and create_params.
root_agent vs App(name="app")
root_agent = Agent(name="docuctl_docs", ...) names the conversational agent and its A2A Agent Card identity; app = App(root_agent=root_agent, name="app") names the deployable ADK application container and must match agent_directory (app).
A2A Agent Card (.well-known/agent-card.json)
A machine-readable JSON document served at /a2a/app/.well-known/agent-card.json that advertises the agent’s JSON-RPC URL, supported modalities, and skills derived from its tools.

Architecture

QuestionADK runtimeGeminiread_pageAnswer+ sessionsendrequestresponseargumentspagereplyA2A agent carddiscoveryQuestionADKruntimeGeminiread_pageAnswer + sessioncallpageA2A agent card
The runtime sends a question to Gemini, executes read_page and returns the answer and session identifier.

The manifest connects the generated agent package to the local server. The agent reads the bundled documentation through read_page; ADK manages the session and model requests. The A2A card is a separate discovery surface for the same application. The local validators check project naming and manifest alignment; inspecting the running routes is a separate check.

Working code

Download the complete lesson lab, extract it, and open its agents-cli-create-run directory. It includes the source, tests and fixtures. With Python 3.12+ and uv installed, prepare the local environment:

Prepare the downloaded labTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt
LAB_DIR="$PWD"

Scaffold the prototype project

agents-cli creates the application wrapper, dependency configuration and manifest. The downloaded lab supplies the documentation tool and four fictional documentation pages. Keep the extracted lab directory separate from the generated project so that the setup commands have clear source and destination paths.

From the extracted lab, save its path, create the project, and copy the supplied agent package into the generated app directory:

Create the project and install the supplied agentTerminal · bash
LAB_DIR="$PWD"
agents-cli scaffold create docuctl-docs --agent adk --region us-central1 --prototype --agent-guidance-filename AGENTS.md -y
cp -R "$LAB_DIR/docuctl_docs/." docuctl-docs/app/
cd docuctl-docs
agents-cli install
agents-cli info

--prototype omits deployment and CI/CD configuration. It still produces a local application wrapper. Inspect agent_directory: app in the generated manifest; do not replace the generated project with the teaching lab’s files.

For the local Gemini API option, edit the generated .env: comment out the Vertex AI GOOGLE_* settings and set GEMINI_API_KEY to your own AI Studio key. Keep .env out of version control. The official authentication guide also describes Application Default Credentials for the Google Cloud option. Use one backend consistently.

Choose the Gemini modelTerminal · bash
export DOCS_MODEL=gemini-3.8-flash

The model ID is listed in the Gemini model catalog. Its listing establishes availability in that catalog, not successful execution with your credentials. Sending a question invokes Gemini and may incur charges; the offline test command below makes no model requests.

Wire the agent and manifest contract

Inside app/agent.py (packaged in this lab as docuctl_docs/agent.py), the scaffolded template defines two distinct objects that must stay separate:

  1. root_agent = Agent(name="docuctl_docs", ...): the ADK agent definition, whose name appears as the event author in conversation traces and as the top-level name in the A2A Agent Card.
  2. app = App(root_agent=root_agent, name="app"): the ADK application container, whose name must match agent_directory ("app").

Here is the complete agent.py implementation with the read_page tool over the four docuctl documentation files (config.md, deploy.md, rollback.md, and tokens.md) plus explicit contract helpers for project naming and route alignment:

docuctl_docs/agent.pySource · pythonDownload
"""The docuctl docs assistant scaffolded with Google Agents CLI 1.11.0."""

from __future__ import annotations

import os
import re
from pathlib import Path

from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.models import Gemini
from google.genai import types

DOCS = Path(__file__).resolve().parent / "docs"
PAGES = sorted(p.stem for p in DOCS.glob("*.md"))
MODEL = os.environ.get("DOCS_MODEL", "gemini-3.8-flash")
MAX_PROJECT_NAME_LENGTH = 26
PROJECT_NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$")

INSTRUCTION = """You answer questions from developers about docuctl, the team's deploy CLI.
Read the relevant page with read_page before you answer. Answers depend on the developer's
docuctl major version (1.x or 2.x). State the exact command and keep the answer under 80 words."""


def read_page(page: str) -> dict[str, str]:
    """Read one page of the docuctl documentation: config, deploy, rollback or tokens."""
    if page not in PAGES:
        return {"error": f"No page {page!r}. Pages: {', '.join(PAGES)}."}
    return {"page": page, "text": (DOCS / f"{page}.md").read_text(encoding="utf-8")}


def validate_project_name(project_name: str) -> str:
    """Apply the agents-cli scaffold create project-name rules (<=26 chars, lowercase/digits/hyphens)."""
    if len(project_name) > MAX_PROJECT_NAME_LENGTH:
        raise ValueError(
            f"Project name '{project_name}' exceeds {MAX_PROJECT_NAME_LENGTH} characters. "
            "Please use a shorter name."
        )
    if not PROJECT_NAME_RE.match(project_name):
        raise ValueError(
            f"Project name '{project_name}' must start with a lowercase letter and contain only "
            "lowercase letters, digits, and hyphens."
        )
    return project_name


def validate_manifest_contract(app_name: str, agent_directory: str) -> dict[str, str]:
    """Verify that App(name=...) matches agent_directory in agents-cli-manifest.yaml."""
    if app_name != agent_directory:
        raise ValueError(
            f"App(name={app_name!r}) does not match manifest agent_directory={agent_directory!r}: "
            f"ADK mounts /apps/{agent_directory}/... while A2A mounts /a2a/{app_name}."
        )
    return {
        "adk_session_route": f"/apps/{agent_directory}/users/cli-user/sessions",
        "a2a_card_route": f"/a2a/{app_name}/.well-known/agent-card.json",
    }



root_agent = Agent(
    name="docuctl_docs",
    model=Gemini(
        model=MODEL,
        retry_options=types.HttpRetryOptions(attempts=1),
    ),
    instruction=INSTRUCTION,
    tools=[read_page],
    generate_content_config=types.GenerateContentConfig(
        temperature=0.2,
        max_output_tokens=1024,
        thinking_config=types.ThinkingConfig(thinking_level="low"),
    ),
)
app = App(root_agent=root_agent, name="app")

Why does app = App(root_agent=root_agent, name="app") keep name="app" even when root_agent.name is "docuctl_docs"? When app/fast_api_app.py starts, get_fast_api_app(agents_dir=AGENT_DIR) discovers agent packages by directory name (app) and creates session routes under /apps/app/...; streaming requests go to /run_sse with app_name: "app" in the body. At the same time, the FastAPI lifespan function mounts A2A routes at /a2a/<app_name> using adk_app.name. Keeping App(name="app") aligned with agent_directory: app ensures both /apps/app/users/.../sessions and /a2a/app/.well-known/agent-card.json identify the same ADK application. The generated server constructs an A2A Runner separately from the API server’s runner lookup.

Lint and run a multi-turn session

Before starting the server, run agents-cli lint. This single command runs four checks across the project in sequence:

  1. uv run ruff check . --config pyproject.toml
  2. uv run ruff format . --check --config pyproject.toml
  3. uv run codespell
  4. uv run ty check

Once all four checks pass, start a persistent background server on port 18080 and send the first query with agents-cli run:

Run a two-turn local session and inspect the A2A Agent CardTerminal · bash
# Turn 1: start the local server on port 18080 and ask about production rollback
agents-cli run "I use docuctl 2.x. How do I roll production back to r41?" --start-server

# Inspect the live A2A Agent Card while the local server is running
curl -s http://127.0.0.1:18080/a2a/app/.well-known/agent-card.json | python3 -m json.tool

# Turn 2: paste the session ID printed above
export SESSION_ID="PASTE_THE_SESSION_ID_FROM_TURN_1"
agents-cli run "And on staging?" --session-id "$SESSION_ID"

# Stop the background server when finished
agents-cli run --stop-server

Keep the background server running between turns. Inspect whether the trace calls read_page("rollback"), whether the answer uses the requested environment, and whether the second response uses the same session. A model may call the tool again; session continuation does not guarantee a particular trajectory.

The Python file constructs a real ADK Agent, Gemini model and App; a missing SDK now fails at import rather than creating replacement classes. Its configuration caps output at 1,024 tokens and disables SDK retries. The supplied earlier CLI transcript shows tool calls and answers, but does not include provider response metadata sufficient to independently verify the exact model. No fresh Gemini run is claimed for this renamed lab.

Inspect the preserved earlier CLI example

The original record uses shipctl. Its contents are preserved unchanged.

Earlier CLI exampleExample output · jsonDownload
{
  "cli_version": "1.11.0",
  "project_name": "shipctl-docs",
  "root_agent_name": "shipctl_docs",
  "app_name": "app",
  "server_port": 18080,
  "session_id": "c3f4d38a-8ef4-46b5-85fb-e43956bab9b3",
  "lint": {
    "command": "agents-cli lint",
    "steps": [
      {"tool": "ruff check", "command": "uv run ruff check . --config pyproject.toml", "status": "passed"},
      {"tool": "ruff format", "command": "uv run ruff format . --check --config pyproject.toml", "status": "passed"},
      {"tool": "codespell", "command": "uv run codespell", "status": "passed"},
      {"tool": "ty", "command": "uv run ty check", "status": "passed"}
    ]
  },
  "turns": [
    {
      "turn": 1,
      "command": "agents-cli run \"I use shipctl 2.x. How do I roll production back to r41?\" --start-server",
      "user": "I use shipctl 2.x. How do I roll production back to r41?",
      "tool_call": {"name": "read_page", "args": {"page": "rollback"}},
      "tool_response_page": "rollback",
      "assistant": "To roll production back to release `r41` in shipctl 2.x, run:\n\n```bash\nshipctl rollback --env production --to r41\n```\n\nThis points production traffic to `r41` without rebuilding. Note that database migrations are not automatically rolled back.",
      "session_id": "c3f4d38a-8ef4-46b5-85fb-e43956bab9b3"
    },
    {
      "turn": 2,
      "command": "agents-cli run \"And on staging?\" --session-id c3f4d38a-8ef4-46b5-85fb-e43956bab9b3",
      "user": "And on staging?",
      "tool_call": null,
      "assistant": "To roll staging back to release `r41` in shipctl 2.x, run:\n\n```bash\nshipctl rollback --env staging --to r41\n```",
      "session_id": "c3f4d38a-8ef4-46b5-85fb-e43956bab9b3"
    }
  ],
  "agent_card": {
    "name": "shipctl_docs",
    "description": "An ADK Agent",
    "version": "0.1.0",
    "preferredTransport": "JSONRPC",
    "protocolVersion": "0.3",
    "url": "http://0.0.0.0:8000/a2a/app",
    "skills": [
      {
        "id": "shipctl_docs",
        "name": "model",
        "description": "An LLM-based agent",
        "tags": ["llm"]
      },
      {
        "id": "shipctl_docs-read_page",
        "name": "read_page",
        "description": "Read one page of the shipctl documentation: config, deploy, rollback or tokens.",
        "tags": ["llm", "tools"]
      }
    ]
  },
  "failures": {
    "name_too_long": {
      "command": "agents-cli scaffold create shipctl-documentation-assistant-prod --agent adk --prototype",
      "exit_code": 2,
      "error": "Project name 'shipctl-documentation-assistant-prod' exceeds 26 characters. Please use a shorter name."
    },
    "app_name_mismatch": {
      "app_name": "shipctl_docs",
      "agent_directory": "app",
      "error": "App(name='shipctl_docs') does not match manifest agent_directory='app': ADK mounts /apps/app/... while A2A mounts /a2a/shipctl_docs."
    }
  }
}

Failure cases

Run the offline test suite to verify the read_page tool, the recorded multi-turn session and A2A Agent Card, and the two failure cases:

Run the offline verification suiteTerminal · bash
cd "$LAB_DIR"
"$LAB_DIR/.venv/bin/python" -m unittest -v test_create_run.py

Two failure boundaries commonly trip up new agents-cli projects:

Project name exceeds 26 characters. Passing a long directory name such as agents-cli scaffold create docuctl-documentation-assistant-prod exits immediately with code 2:

Error: Project name 'docuctl-documentation-assistant-prod' exceeds 26 characters. Please use a shorter name.

agents-cli enforces a 26-character limit on project names because downstream GCP service accounts (which have a 30-character ID limit after adding suffixes) and Cloud Run resource identifiers derive their names from name in agents-cli-manifest.yaml.

Mismatch between App name and agent_directory. If you rename App(root_agent=root_agent, name="docuctl_docs") in app/agent.py while agent_directory: app remains in agents-cli-manifest.yaml, the FastAPI server mounts ADK routes under /apps/app/... (from the folder name) while binding the Runner and A2A endpoint to docuctl_docs (/a2a/docuctl_docs). The bundled validate_manifest_contract helper refuses that mismatch. This unit check does not launch a misconfigured server or establish a timeout. The CLI sends streaming requests to /run_sse, with the application name in the JSON body; it creates sessions through /apps/app/users/.../sessions. Keep App(name="app") matched to agent_directory, and put your agent’s descriptive identifier on root_agent = Agent(name="docuctl_docs", ...).

What happens when you change App(name='docuctl_docs') in app/agent.py while agent_directory remains 'app' in agents-cli-manifest.yaml?

Choose one answer, then check your reasoning.

Read the explanation

Keep App(name='app') aligned with agent_directory in agents-cli-manifest.yaml while setting root_agent = Agent(name='docuctl_docs', ...) for the agent's identity.

Deployment

This lesson stops at a local prototype. Keep the generated project and manifest together; later lessons add infrastructure and deployment configuration. Before handing the app to another environment, rerun its tests and inspect the session and A2A routes using that environment’s configuration.

Scaling

A local multi-turn session checks wiring, not concurrent capacity. Before increasing traffic, measure model latency, session-store behavior and server resource use under the intended request mix. Use persistent session storage when requests may reach different server processes.

Key takeaways

  • agents-cli scaffold create --prototype creates the runnable ADK + A2A application and agents-cli-manifest.yaml without premature Terraform or CI/CD files.
  • root_agent = Agent(name="docuctl_docs", ...) identifies the agent in traces and the A2A Agent Card, whereas app = App(root_agent=root_agent, name="app") must match agent_directory (app).
  • agents-cli run --start-server keeps the local FastAPI process alive on port 18080 so subsequent agents-cli run --session-id <uuid> calls reuse in-memory conversation state.

Best practices

  • Keep project names at 26 characters or fewer (^[a-z][a-z0-9-]{0,25}$) from day one so scaffold enhance never fails on GCP service-account length limits.
  • Run agents-cli info and agents-cli lint before starting the local server to catch manifest drift and type errors early.
  • Validate tool inputs against an explicit allowlist (PAGES). The read_page tool returns an error object listing the supported pages when a caller requests an unknown topic.

References

Commands reviewed against Google Agents CLI 1.11.0 and the linked official documentation on October 23, 2026.

Written by Arjun Prabhulal.

Module 1

Scaffold, run, and extend agent projects