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, andtests/) and setsdeployment_target: noneandcicd_runner: skipinagents-cli-manifest.yaml. - Project manifest (
agents-cli-manifest.yaml) - The single source of truth read by
agents-cli info,run,eval,scaffold upgrade, anddeployto locateagent_directory(app),acli_version(1.11.0), andcreate_params. root_agentvsApp(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 matchagent_directory(app).- A2A Agent Card (
.well-known/agent-card.json) - A machine-readable JSON document served at
/a2a/app/.well-known/agent-card.jsonthat advertises the agent’s JSON-RPC URL, supported modalities, and skills derived from its tools.
Architecture
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:
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:
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.
export DOCS_MODEL=gemini-3.8-flashThe 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:
root_agent = Agent(name="docuctl_docs", ...): the ADK agent definition, whosenameappears as the eventauthorin conversation traces and as the top-levelnamein the A2A Agent Card.app = App(root_agent=root_agent, name="app"): the ADK application container, whosenamemust matchagent_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:
"""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:
uv run ruff check . --config pyproject.tomluv run ruff format . --check --config pyproject.tomluv run codespelluv 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:
# 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-serverKeep 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.
{
"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:
cd "$LAB_DIR"
"$LAB_DIR/.venv/bin/python" -m unittest -v test_create_run.pyTwo 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", ...).
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 --prototypecreates the runnable ADK + A2A application andagents-cli-manifest.yamlwithout premature Terraform or CI/CD files.root_agent = Agent(name="docuctl_docs", ...)identifies the agent in traces and the A2A Agent Card, whereasapp = App(root_agent=root_agent, name="app")must matchagent_directory(app).agents-cli run --start-serverkeeps the local FastAPI process alive on port18080so subsequentagents-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 soscaffold enhancenever fails on GCP service-account length limits. - Run
agents-cli infoandagents-cli lintbefore starting the local server to catch manifest drift and type errors early. - Validate tool inputs against an explicit allowlist (
PAGES). Theread_pagetool 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.
- Google Agents CLI documentation: installation,
scaffold create,lint, andrunreference. - Agent Development Kit (ADK) Python docs:
Agent,App, and FastAPI runtime structure. - Agent2Agent (A2A) protocol specification:
.well-known/agent-card.jsondiscovery and JSON-RPC bindings.