Skip to content

2.4. Sessions

What is a session?

A session is one conversation keyed by application, user, and session id. It stores events and state needed for later turns. It is not the incident database, runbook library, audit log, or a global memory shared by every user.

Why does persistence matter?

An in-memory session is fine for a short unit test. An A2A server can receive long-running tasks and restart independently from its clients, so losing all session/task state on every process restart is a correctness bug.

The course server uses one SQLite URL and one deliberately single-connection engine for both ADK sessions and A2A tasks:

database_url = f"sqlite+aiosqlite:///{settings.state_dir / 'runtime.db'}"
session_service = DatabaseSessionService(
    db_url=database_url,
    pool_size=1,
    max_overflow=0,
    connect_args={"timeout": 30},
)
runner = Runner(agent=root_agent, app_name="ops-copilot", session_service=session_service)
task_engine = session_service.db_engine
task_store = DatabaseTaskStore(engine=task_engine)

SQLite permits only one writer at a time. Sharing a one-connection pool queues the short session/task transactions inside this single process instead of letting independent engines race into intermittent lock errors. SQLite is appropriate for a single-replica course lab; multiple replicas or stronger durability would require a shared database and migration/backup plan.

Who closes runtime resources?

The app owns its runner, session service, SQLAlchemy engine, and task store. A lifespan hook closes them on shutdown:

@asynccontextmanager
async def lifespan(_: Starlette):
    try:
        yield
    finally:
        await runtime.close()

Explicit ownership prevents leaked connections in tests, restarts, and deployment termination.

Why separate bind and advertised addresses?

The server keeps its listener and advertised address separate. Host development binds AGENT_A2A_BIND_HOST=127.0.0.1; Kubernetes explicitly overrides it to 0.0.0.0. The agent card always advertises the callable AGENT_A2A_PROTOCOL/HOST/PORT, never a wildcard listener.

url=f"{settings.a2a_protocol}://{settings.a2a_host}:{settings.a2a_port}/"

Kubernetes supplies a service DNS name while the process listens on the pod interface; host development binds loopback and advertises localhost.

How is a runaway A2A loop bounded?

AGENT_A2A_MAX_LLM_CALLS defaults to 12 and is validated from 1 through 100. The A2A request converter preserves request metadata while replacing ADK's broad default call budget with this bound. The budget limits resource use; it does not replace gateway rate limits, timeouts, or task cancellation.

How do you inspect the session server without calling a model?

cd agents/python
mise run a2a

In another terminal:

curl -fsS http://localhost:8080/.well-known/agent-card.json
ls -l .state/runtime.db

Fetching the card creates the application and persistent stores but does not submit a model task.

How do you reset course state?

mise run data:reset

This removes .state/, including runtime sessions/tasks and the writable incident copy. The next run recreates it from the committed seed. Never use that reset pattern for a production database.

What is the session checkpoint?

Start and stop mise run a2a, confirm shutdown exits cleanly, restart it, and confirm the agent card is still available. Then run mise run data:reset and verify the committed ../data/incidents.db remains unchanged in Git.