Revisions and traffic splitting on Agent Runtime

Revisions and traffic splitting on Agent Runtime

I want to walk through something that was enabled recently on Agent Runtime… we can finally ship a new version of an agent without redeploying the whole thing (should we cry of happiness together?). New version of an agent, same running engine, same URL, same everything around it.

To make the walkthrough concrete I built two versions of a small research agent (to ship it twice): (v1) a fast version that uses the built-in Google Search tool, and a (v2) slow-but-thorough one that uses the Deep Research agent as a tool call.

All the code (the sample agent and the deploy/revision/traffic scripts) used in this blog post lives in one repo: agentic-research-revisions-demo. I’ll link the individual files as they come up, too.

A heads-up before we start: revisions and traffic splitting are pre-GA at the time of writing (the API is v1beta1), and a couple other things I use here (Deep Research and agent identity) are preview/experimental too. It all works today, but check the docs before you build production on it, because names and shapes can still move. Docs for revisions/traffic on Agent Runtime:
Manage revisions and traffic  |  Gemini Enterprise Agent Platform  |  Google Cloud Documentation.

Why Agent Runtime?

You might tell me “Dani, but revisions and traffic splitting is something trivial on any container deployment service” and yes, you would be right, but Agent Runtime (previously Agent Engine) is a service specialized to deploy agents, and it’s a rapidly evolving product, and until recently it didn’t support revisions, but now it does! So, yes, let me be excited.

And your follow-up might be “Dani, but why not just use a basic container service like Cloud Run if Agent Engine is currently changing?” Session and memory management, man, they’re so incredibly tricky; and let’s not get started with code-execution sandboxes, observability, IAM/VPC-SC plumbing…for me, an AI developer with not-so-vast networking and infra knowledge, it’s very nice to have this pre-built and managed, hence my preference for Agent Runtime (see image below for a better grasp of what the service encompasses).

Diagram from Ivan Nardini’s video, “Deploy and manage agents with Agent Engine”: managed runtime, sessions, memory, a code sandbox, evaluation, and observability; all with your pick of framework, tools, and models (basically, your own code).

One thing I’ve noticed is that as Agent Runtime evolves as a product it keeps giving the developer more and more flexibility (while managing the tricky stuff) e.g. you can set the CPU and memory and min/max instances, or bring your own service account, attach a private VPC, point it to specific KMS keys. You can even control the container image in three different ways: edit the Dockerfile the runtime builds, pass build args to that build, or skip the build entirely and hand it a prebuilt image URI (or have it build straight from a connected Git repo). So it’s managed sessions and managed scaling, but with a lot of flexibility on customization.

The agent I’m going to ship (v1 and v2)

I wanted a real thing to deploy, so I figured I’d quickly build a research briefing agent that, when I ask, goes and finds the latest on a topic (I’ve been cleansing from a mild YouTube addiction , so I wanted a way to learn about the latest on agents, evals, new products, and Google/tech updates without spending my day watching videos; so, perfect excuse to create a dedicated agent that does that for me).

To scaffold the agent’s files I used agents-cli, which just hit 1.0.0 (woooooo); it’s the fastest way I know to get a well-structured ADK project with all the config already in place (I previously used the agent starter pack; agents-cli is its successor, with the idea of removing the rigidness of the experience and making it skill-based, so you can use it yourself with the CLI commands or hand it to your coding agent of choice). Command to scaffold:

agents-cli scaffold create agentic-research --agent adk --prototype

That gave me an app/ with an agent.py and all the files surrounding it. What I did was swap the placeholder tool for my own (very simple) search tool and rewrite the instructions. The first version is deliberately simple (one grounded Google Search pass), synthesized into a briefing with the source links pulled out (the whole tool is app/tools.py):

# app/tools.py  (v1 — fast, grounded)
async def research_latest(topic: str) -> str:
    """Grounded web lookup: a quick briefing with clickable sources."""
    client = _client()  # bind it — see the "oh, by the way" below
    resp = await client.aio.models.generate_content(
        model="gemini-flash-latest",
        contents=f"Brief me on the latest about: {topic}. Recent items, be specific, cite sources.",
        config=types.GenerateContentConfig(
            tools=[types.Tool(google_search=types.GoogleSearch())],
        ),
    )
    # ...pull resp.candidates[0].grounding_metadata.grounding_chunks[].web.uri into a Sources list...
    return briefing_with_links

That answers in about ten seconds. Good enough to be useful, and very quick.

Create Engine (first deploy)

(Naming note, since you’ll see both: “Agent Runtime” is the current name for what was “Agent Engine.” The SDK still says agent_engines, and resource names still say reasoningEngines, for backward compatibility. Same thing.)

Agent Runtime deploys a container built from your project. agents-cli deploy will do this for you (it packages the project and Agent Runtime builds the image), and for a plain first deploy that’s exactly what I’d reach for. But because this post is about revisions and traffic, I’m using the Vertex SDK directly so all the pieces are visible (also, the CLI doesn’t directly expose traffic splitting…yet).

So, first, for creating an engine (which you can see in deploy_sdk.py, with a shared client and config in runtime_common.py) you would use:

import vertexai
from google.genai import types
from vertexai import agent_engines
from app.agent import root_agent

client = vertexai.Client(
    project=PROJECT, location="us-central1",           # the engine is regional
    http_options=types.HttpOptions(api_version="v1beta1"),
)

remote = client.agent_engines.create(
    agent=agent_engines.AdkApp(agent=root_agent),
    config={
        "display_name": "Agentic Research",
        "requirements": [...],
        "extra_packages": ["app"],
        # Latest Gemini is global-only, but the engine is regional...so we can pin the runtime's model calls to `global` with an env var.
        "env_vars": {"GOOGLE_CLOUD_LOCATION": "global"},
    },
)
print(remote.api_resource.name)
# e.g. projects/.../locations/us-central1/reasoningEngines/3690047334479036416

create() spins up a brand-new engine with its own resource_name and URL. That resource name is the thing everything else points at, so I save it off. This is a create, and you do it once.

Revisions (new versions)

Now, what if instead of a quick grounded lookup I wanted a proper Deep Research run? Lately I’ve been doing this trick of querying the Deep Research agent’s API, it’s a preview capability, but it is a lot better than the simple sequential WebFetchs agents sometimes make. The only catch is it takes 10-ish minutes, which is far too long to answer inside a single chat turn (so for v2 I designed the agent to kick the report off and hand back a handle to fetch when it’s done):

# app/tools.py  (v2 — deep, slow)
async def research_latest(topic: str) -> str:
    """Kick off a Deep Research report (thorough, ~10-15 min) and return a handle."""
    client = _client()  # bind it — see the "oh, by the way" below
    ix = await client.aio.interactions.create(
        agent="deep-research-preview-04-2026",
        input=f"Research assignment: {topic}. Recent research, launches, Google updates. Cite sources.",
        background=True,
    )
    return f"Started a deep research report. Interaction id: {ix.id} — fetch it in a few minutes."

Now, in the first deploy we called create(); intuitively, to create a revision we call update() with the resource name you already have (I copied deploy_sdk.py to update_revision.py and literally changed one thing: create() to update()):

remote = client.agent_engines.update(
    name="projects/.../reasoningEngines/3690047334479036416",  # the engine I already have
    agent=agent_engines.AdkApp(agent=root_agent),              # now the v2 code
    config={...},
)

Same engine, same resource name, same URL. What actually happens under the hood is that Agent Runtime takes an immutable snapshot (a revision) of the new code and everything that defines it. You’re not overwriting v1; the previous revision is still right there, which is what makes the next part possible.

A quick precision note on what counts as “a new version.” A revision gets created when you change a versioned field: the packaged code, the dependencies, the Python version, the env vars, the scaling settings, the container concurrency, the agent card. Things outside that list (like the traffic setting we’re about to use) apply across revisions without creating a new one.

Traffic splitting (between versions)

So now I have a couple of revisions of the same agent that behave completely differently. v1 answers in ten seconds with a simple grounded summary, while v2 gives us a far richer report but makes us wait several minutes. If this were a real user-facing agent, which one would people prefer? (That’s the textbook reason to split traffic.)

By default an engine runs in “always latest” mode, where the newest revision takes everything. To split it, you switch to a manual split and hand it percentages (traffic.py wraps list / split / reset):

client.agent_engines.update(
    name="projects/.../reasoningEngines/3690047334479036416",
    config={
        "traffic_config": {
            "traffic_split_manual": {
                "targets": [
                    {"runtime_revision_name": ".../runtimeRevisions/1", "percent": 90},
                    {"runtime_revision_name": ".../runtimeRevisions/2", "percent": 10},
                ]
            }
        }
    },
)

The targets live under traffic_configtraffic_split_manualtargets. Each target wants runtime_revision_name (the full resource path of the revision, ending in /runtimeRevisions/<id>) and percent (an integer). The percents have to add up to 100. And the whole thing is on the v1beta1 API. (In REST/JSON it’s the same shape in camelCase: trafficConfig.trafficSplitManual.targets[].{runtimeRevisionName, percent}, with trafficSplitAlwaysLatest {} for the default.)

Because traffic config isn’t a versioned field, setting it doesn’t ship any code or create a new revision, it just reroutes requests across revisions that already exist. You can move the dial back and forth as much as you like. Ninety-ten to start, then fifty-fifty once you trust it, then a hundred to the new one, all without a re-deploy.

Oh, btw (pt. two): my first split kept failing with a vague code 13 INTERNAL. The cause turned out to be capacity: a manual split runs both revisions warm at the same time, but I’d deployed with min_instances: 1 / max_instances: 1. With one instance the engine can’t stand up a second revision to send traffic to, so the whole update fails. Bumping instances so there’s room for every revision in the split fixed it instantly (I used min 2 / max 4). Instance counts are a versioned field, so that bump is itself a new revision.

And here’s the thing actually working…

I set the split 50/50 between the grounded revision and the deep one, then hit the same engine, with the same question, a dozen times. Seven requests came back with the instant grounded summary; five kicked off a deep research report (right around the 50/50 I asked for), all from one URL. That’s the whole pitch: I’m no longer guessing whether I want the fast answer or the thorough one, I can measure it, and turn the dial the moment the data says something.

Same engine, same question, two completely different answers depending on which revision the split routed you to:

Recap

The reason I’m bullish on this: you get managed sessions, managed scaling, and now managed versioning, but you didn’t trade away control to get them. In one config you set the CPU/memory and instance range, the concurrency, the service account and identity, private networking and CMEK, the Python version and dependencies, the env vars and secrets (even the container image itself!!). That combination is rare, and it’s why shipping the fifth revision of this agent felt like nothing at all.

And that’s the whole thing. Register once, revise as often as you want, and when you’re not sure which version is better, don’t argue about it, split the traffic and let your users tell you.


Everything above is true as of when I wrote it, but this area is moving fast and half of it is pre-GA, so here’s where I’d look to see if it’s drifted:

  • The manage revisions and traffic and deploy an agent docs are the source of truth for field names.
  • For agents-cli: use agents-cli info for your version, and agents-cli deploy --help; its last line points at the exact source file implementing the command, which is the honest way to check what it does and doesn’t support.

Thank you for reading! Feel free to reach out to talk more about this or any agentic-related stuff on LinkedIn. Truth is I take a while to answer, I batch-reply to my messages (close ones won’t let me lie), but you’ll eventually get a reply! I’m like v2 of this blog post’s agent :smiling_face_with_tear:. But jokes aside, thank you for reading, hope you find this useful and a nice read on this long weekend. Happy coding y’all.

4 Likes

I want to learn more