On this page
  1. PHP2Shell in one minute
  2. The Agent Workflow Architecture
  3. Five design patterns
  4. 1. Route each request to the right agent
  5. 2. Share facts through state
  6. 3. Run the response in order
  7. 4. Fan out the analysis
  8. 5. Loop until the plan passes review
  9. How do you trace an agent at scale?
  10. How do you evaluate an agent's behaviour on every run?
  11. How do you manage context and cost at scale?
  12. Reading and writing state
  13. Keeping context bounded
  14. Caching and quota
  15. At scale: make the cache hit, measure it, evaluate every run
  16. Make the cache hit on purpose
  17. Measure the hit rate
  18. Evaluate in a dedicated session, every change
  19. Context and caching on Gemini 3.8 Flash
  20. Designed with production in mind
  21. What's next
  22. Disclaimers
  23. Sources

PHP2Shell response: an AI agent campaign with Google ADK

A firefighting solution to detect the PHP CVE fast and act: a team of agents that finds every exposed PHP site, ranks it and plans the patch, traced, evaluated and kept within budget.

ai-agents multi-agent google-adk security evaluation opentelemetry

While we are still fighting through the remediation of the findings from Mythos (Project Glasswing), a new alert affects almost every website: PHP2Shell.

On 14 October, Bank of America's chief executive said the bank had brought in about 1,000 contractors to fix software flaws that AI models exposed, Anthropic's Mythos among them (TNW). A week later, PHP shipped emergency patches for two critical flaws, CVE-2026-109618 and CVE-2026-109619: unauthenticated remote code execution, rated 9.2 (Wiz, The Hacker News). PHP runs 69.2% of the websites whose server-side language is known (W3Techs), and Palo Alto Networks' telemetry shows over 22.6 million exposed instances that are potentially vulnerable (Unit 42).

We need rapid CVE detection, and responding has to keep up. This post builds a team of agents with Google's Agent Development Kit (ADK) that runs the first hours of a PHP2Shell response, and holds it to the bar a real campaign needs.

A story from left to right. On the left, a flood of blank amber alert cards piles up around a faceless silhouette at a desk. In the middle, five small friendly robot agents sort the flood through a funnel into a short stack of three ranked cards. On the right, the silhouette sits calm at a tidy desk, with a rose approval seal with a check mark above the ranked stack and safe teal web page windows behind.

What you get:

  • A working PHP2Shell response team, with the code on GitHub.
  • Five agent patterns, each with its campaign rule: routing, shared state, steps in order, fan out and gather, and a review loop.
  • How to trace the agents at scale, with the OpenTelemetry spans ADK already emits.
  • How to evaluate their behaviour, by checking the path they took, not only the plan they wrote.
  • How to keep context and cost in check, so a campaign across a large fleet stays fast and affordable.

PHP2Shell in one minute

  • What: two flaws in the PHP interpreter. CVE-2026-109618 is memory corruption during out-of-memory handling; CVE-2026-109619 is a use-after-free involving state kept between requests. Either can allow unauthenticated remote code execution (Tenable).
  • Fixed in: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, released on 21 October. End-of-life branches get no official release; 7.4 has minimal source backports.
  • Urgency: CISA and Canada's Cyber Centre call for immediate patching, and exploitation attempts were reported around disclosure (SecurityWeek). The picture moves fast: on 25 October Unit 42 withdrew its attribution of an intrusion to PHP2Shell, which is one more reason a response plan gets reviewed before it runs. Status as of 26 October.
  • The three questions every team asks: are we affected, which sites first, and what's the plan?

The Agent Workflow Architecture

The campaign answers those three questions. An intake agent routes a quick question to an advisory analyst or an asset lookup, and hands a full response to a sequential team: read the advisory, match the fleet's PHP versions in code, analyse exposure and patches in parallel, draft and review the plan in a loop, and save the report. Every tool is read-only, and a person approves each change.

The PHP2Shell response team: an intake agent routes to two quick-answer agents or a sequential response team that reads the advisory, matches versions in code, analyses exposure and patches in parallel, drafts and reviews the plan in a loop, and saves the report
The campaign: five ADK patterns, read-only tools, one bounded write, and a person approves every patch.
Screenshot of the ADK Dev UI agent graph: intake routes to asset_lookup, advisory_analyst and response_team; response_team runs advisory_reader, exposure_matcher, the analysis ParallelAgent with patch_analyst and exposure_analyst, the plan_room LoopAgent with planner and reviewer and their check_plan and approve_plan tools, and report_writer; the model version reads gemini-3.8-flash.
ADK draws the same team from the code: the Dev UI agent graph from my local run on Gemini 3.8 Flash.

Here is one real run of the agent workflow, replayed from its trace: the route, the version match in code, the two analysts in parallel, one review round, the approval and the saved report.

One real run on Gemini 3.8 Flash, replayed step by step from its trace: 19 steps, 12 model calls. Steps are in true order; spacing is not real time.

The code is on GitHub at jimmyaicoach/adk-php2shell-response: the agents, the version matching and plan checks as plain, tested Python, the public advisory facts with their sources, and a sample fleet of twelve sites. I've already run several campaigns like this in production at scale, as Claude Code workflows on my Scan-and-Action Framework (SAAF): see I built an Agent Framework Titan-Kronos.

Five design patterns

1. Route each request to the right agent

intake is the root agent. A question about the advisory goes to advisory_analyst; a question about one site goes to asset_lookup; a full response goes to response_team. The root reads each sub-agent's description and transfers the conversation to the one that fits, and the trace names the agent that answered.

Screenshot of the ADK Dev UI chat: asked what it can do, the intake agent offers three routes: advisory details, a single-site check, and a fleet-wide response.
The intake agent in the ADK Dev UI, offering its three routes.

In practice: the route decides what can happen next, so only safe, read-only agents sit behind a model's choice, and anything with consequences sits behind code.

2. Share facts through state

Every agent in the session shares one state. read_advisory saves the advisory; match_versions saves the sites that need action; the analysts save their reports; the planner saves the plan. Each write is an assignment, so ADK records it as an event with its state delta, tied to the call that made it.

In practice: facts come from tools, not from a model's memory, and every change is on record for the post-incident review.

3. Run the response in order

response_team is a SequentialAgent: read the advisory, match the fleet, analyse, plan, save. The order is fixed in code. Before each step, a callback checks that the step before it left its output in state, and stops the run with a clear message if it didn't.

In practice: a step that produced nothing never feeds the next one. The version match is plain Python: a site on 8.4.20 is affected, one on 8.4.27 is patched, and one on 8.1 needs an upgrade.

4. Fan out the analysis

analysis is a ParallelAgent. The exposure analyst ranks each affected site (P1 for internet-facing sites with customer or payment data) and the patch analyst works out each site's target version, both at the same time. Each saves its report under its own key, and the planner gathers both.

Fan out and gather: the affected sites feed a ParallelAgent; the exposure analyst and the patch analyst run at the same time, each saves one report under its own key, and the planner reads both
Fan out and gather: one key per branch.

In practice: one key per branch, so neither overwrites the other, and every priority traces back to data in the fleet inventory.

5. Loop until the plan passes review

plan_room is a LoopAgent with a cap of three rounds. The planner writes the plan as a table: priority, site, current and target version, owner, rollout and rollback. The reviewer calls check_plan, plain code that lists any site the plan misses, then approves or writes feedback for the next round.

The review loop: the planner writes the plan, the reviewer runs check_plan in code, approve_plan ends the loop only after the check passes, and after three rounds an unapproved plan is saved as NEEDS-REVIEW
The review loop: two exits, approval only after the code check passes.

In practice: the loop has two exits. approve_plan ends it, and code refuses that call until check_plan has passed on the current draft. The cap of three rounds is the other exit, and a plan that never passed is saved as NEEDS-REVIEW for a person.

How do you trace an agent at scale?

Export the spans ADK already opens, over OpenTelemetry, and read them per run. ADK's own code opens a span for each invocation, each agent, each model call and each tool call, and stamps the model-call spans with the standard GenAI attributes. A slow tool or a cache miss shows up under the exact call that caused it.

The spans one request produces: invocation, agent, model call, cache handling, generation and tool, with the ADK source line that opens each
The spans ADK opens for one request.

What a model-call span carries:

AttributeWhat it tells you
gen_ai.systemThe provider, set by ADK
gen_ai.request.modelWhich model this call used
gen_ai.request.top_p, max_tokensThe generation settings sent
gen_ai.usage.input_tokens, output_tokensThe tokens for this call: the cost of each step
gen_ai.usage.experimental.reasoning_tokensThinking tokens
gen_ai.usage.experimental.system_instruction_tokensHow much of the prompt is the instruction: the number to watch in the review loop
gen_ai.response.finish_reasonsA normal stop, or a cut-off

These follow the OpenTelemetry GenAI semantic conventions, as ADK 1.30.0 emits them, so any backend that understands them reads ADK traces the same way as traces from other agent frameworks.

Where the spans go is chosen at startup, in one of three routes:

Three export routes: the Dev UI by default, any OpenTelemetry backend over OTLP, or Google Cloud Trace and Logging
Where the spans go, chosen at startup.
  • The default: plain adk web keeps spans in memory for the Dev UI's Trace tab.
  • The portable route: set the standard OTEL_EXPORTER_OTLP_ENDPOINT, and the same spans go to any OpenTelemetry backend.
  • The Google Cloud route: a flag at startup or deploy sends them to Cloud Trace, with logs to Cloud Logging.

For the trace and trajectory analysis, send them over OTLP to the same backend as the rest of your systems, run the behaviour checks below on every run, and alert on cost per run, failure rate and the instruction's share of the prompt.

How do you evaluate an agent's behaviour on every run?

Check the path, not only the plan. A plan can look complete while the path behind it went wrong: a site matched by the model instead of the code, a plan approved before the check ran, a report saved from an empty plan. Each of these is visible in the trace, so each becomes a deterministic check that runs on every run, with no model in the loop.

PatternRuleCheck on the traceCatches
RoutingA full response reaches the teamThe first transfer for a fleet-wide request goes to response_teamA full response answered by a quick-answer agent
StateFacts come from toolsread_advisory ran, and its state delta holds the advisory, before any version is citedVersions quoted from model memory
Steps in orderCode decides "affected"match_versions ran, and affected_sites exists before the analysts startA model judging versions on its own
Steps in orderEach step builds on the lastBoth reports exist before the planner runs, and no step ran after a stopA step working from nothing
Fan outBoth branches ranBoth analyst spans exist, each with its own output keyA branch that failed quietly
LoopEnds for a reasonOne to three rounds; an approval follows a passing check_plan on the same draftAn approval with no check behind it
LoopBounded promptsThe instruction's share of tokens stays flat across roundsContext growing each round
WritesOne bounded writesave_report ran once, and its path is inside reports/A write anywhere else
CostWithin budgetCalls, tokens and time under the run's capQuota and cost blow-ups

Run these checks on a fixed set of test fleets, several runs each, against a baseline, and ship a change to the agents only when the path stays clean. The repo's end-to-end test automates nine checks on the trace: the route, the advisory read before versions are matched, code matching before the analysts, both branches, one to three loop rounds, approval only after check_plan passed on that draft, one bounded write, no stopped step, and the affected sites matching the code's answer. In three runs on Gemini 3.8 Flash against the sample fleet, all nine passed every time, with 12 model calls per run, 8 sites needing action, and the plan approved in its first review round. The prompt-size and budget checks in the table are next.

How do you manage context and cost at scale?

Keep each agent's context to what its job needs, and cap every run. Context that grows round after round is the usual reason an agent workflow gets slower and dearer as it goes.

Reading and writing state

  • Read it in an instruction with {key}, or {key?} when the key may be missing, and in code through the tool's or callback's context.
  • Write by assignment: state["k"] = v. ADK records each assignment as the event's state delta.
  • Append by reassigning the whole list: state["k"] = state.get("k", []) + [x]. Recent ADK versions also record in-place changes, but reassigning works on every version and is checked against the schema.
  • Save a whole reply with output_key. The analysts, the planner and the reviewer each use one, so each writes one key and the next round overwrites it.
  • Scope with prefixes: none for this session, user: for one user across sessions, app: for everyone, and temp: for one invocation only.
  • Write through the context, never to a fetched session's state. ADK's docs warn that writing there bypasses event tracking and can lose data.

Keeping context bounded

  • Work from state, not history. Every agent in the response team runs with include_contents="none": it sees its instruction and the state it needs, not the whole conversation.
  • Overwrite the draft, keep only the latest feedback. The plan and the feedback are single keys, so the third round's prompt is the same size as the first.
  • Set a token budget per run, and decide what the team does at the limit: stop, summarise or hand over to a person.

Caching and quota

  • Cache the large, stable part of the prompt, and measure whether it hits: see the scale section below.
  • Budget each run. One request fans out into many model calls, so cap steps, tokens and time, retry with back-off and a limit, and count every failure.

At scale: make the cache hit, measure it, evaluate every run

At one fleet, a run is a handful of model calls. At a thousand fleets a day, the same design is a cost line and a quality gate. Three things decide whether it holds up: whether the cache actually hits, whether you can see the hit rate, and whether every change to the agents is evaluated before it ships.

Make the cache hit on purpose

Don't count on the automatic cache for an agent workflow. Gemini caches repeated prompt beginnings on its own, but in my benchmark on Gemini 3.8 Flash, a new question sent as a separate request never hit it, even at 171,000 tokens; follow-up turns in the same conversation did, at up to 98%. Each agent in a workflow sends its own separate requests, so the automatic cache is a bonus, not a plan. Google's own words agree: implicit caching comes with "no cost saving guarantee" (Gemini API docs), and on Vertex AI it starts only at 6,144 tokens for Gemini 3.8 Flash (Vertex AI). When it does hit, cached tokens cost 90% less.

Turn on ADK's explicit context cache. ContextCacheConfig on the App makes ADK create the cache itself, so a hit doesn't depend on the automatic one. It caches the system instruction, the tools and the agent's earlier contents. Three settings control it: min_tokens (skip caching small requests), ttl_seconds (how long a cache lives) and cache_intervals (how many uses before it's refreshed). The first request is never cached: ADK decides from the previous request's token count. It applies to Gemini models only. Google calls explicit caching a guaranteed saving on the tokens that reference a live cache; on Vertex AI it starts at 4,096 tokens for most Gemini 3 models (the page lists no explicit minimum for 3.8 Flash), and a cache lives 60 minutes unless you set otherwise.

Keep the start of every request identical. A cache reuses a prefix, so anything that changes must come after it. Here, each agent keeps its fixed rules in static_instruction, sent first, and only the changing state (the affected sites, the reports, the plan) in instruction, sent after it. Put state in the middle of the system instruction and every call has a new prefix, and the cache never hits.

Cache what is big and stable. Below those minimums nothing is cached at all, so caching pays only when the stable part is large: a response runbook, a patch policy, a long tool catalogue, shared by every call. The agents' own rules are a few hundred tokens, so in my three runs on Gemini 3.8 Flash, each agent's prompt was 440 to 2,450 tokens, below the explicit cache's minimum, and the cache read nothing: 0 of about 8,200 prompt tokens per run. At scale, the runbook is what you cache.

Measure the hit rate

The proof is in the usage block, not a header. Every Gemini response reports prompt_token_count and cached_content_token_count; the hit rate is one divided by the other. The repo's end-to-end test prints it per agent for every run:

AgentModel callsPrompt tokensRead from cache
intake14410
advisory_reader27620
exposure_matcher25810
exposure_analyst17820
patch_analyst11,4060
planner11,5080
reviewer22,4440
report_writer24460

One run of three on Gemini 3.8 Flash, with ADK's explicit cache on: 12 model calls, 8,370 prompt tokens, none cached, because no agent's prompt reached the 4,096-token minimum we set for the explicit cache. The other two runs read the same.

On every run, track it per agent and alert when it drops. A prompt edit that moves state into the fixed part, a model change, or a cache that expired between runs all show up as a falling hit rate before they show up on the bill. Export the spans over OpenTelemetry, record the cached tokens beside the model-call span, and set the alert on the share, not the total.

Evaluate in a dedicated session, every change

Evaluate in its own environment. ADK's evaluation runs each test case in a fresh session, against a fixed set of cases, so one case can't leak state into the next and the campaign's own sessions stay untouched. Keep the evaluation on its own project or session store, with its own sample fleets.

Score the path as well as the answer. ADK scores two things by default: the tool trajectory (did the agent call the expected tools, in the expected order; matched exactly by default, with in-order and any-order options) and the final response against a reference. Here, the nine trajectory checks above run on every end-to-end run, and the build fails if any of them fails.

Measure cache and cost in the end-to-end run, not in the evaluation. adk eval builds its runner from the root agent rather than the App, so the App's cache setting doesn't apply there. Keep cost and hit-rate measurement in the end-to-end runs, and quality in the evaluation.

Run each case several times. A model's path varies between runs, so one pass proves little. Run each case more than once (in ADK 1.30 the Python AgentEvaluator runs each case twice by default) and gate on the worst run, not the average.

Context and caching on Gemini 3.8 Flash

The agents run on Gemini 3.8 Flash, and Google's documentation for it shapes five choices:

  • Caching thresholds: the automatic cache starts at 6,144 tokens on Vertex AI (4,096 per the Gemini API), and a cached token costs 90% less.
  • Thinking: set with thinking_level (low, medium or high), not a token budget.
  • Thought signatures: they must be sent back with function calls, or the call fails with a 400 error.
  • Sampling settings: ignored on 3.8 Flash; Google recommends leaving them at the defaults, so the agents set none.
  • Prompt order: the question goes after the data, so each request ends with the task line.

The context window is 1,048,576 tokens in and 65,536 out, far above what one run needs; the limit that matters at scale is cost, not size.

Sources: Google's Vertex AI context caching, Gemini API caching, Vertex AI thought signatures, Vertex AI thinking, Gemini 3 guide and the Gemini 3.8 Flash model page, read 2026-10-26.

Designed with production in mind

AreaWithout these design choicesWith them
FactsThe model recalls versions and advisoriesTools read the advisory and the inventory; code decides "affected"
OrderAgents hand work along and hopeA sequence with a check before every step
ReviewOne draft, judged by eyeA review loop: code checks coverage, approval only after the check, a cap of three rounds
WritesThe model picks a path and writesOne write, to reports/, with the folder and name set in code
TracingThe Dev UIOpenTelemetry spans exported over OTLP, alerts on cost and failure rate
EvaluationReading the repliesNine trajectory checks on every run, against a baseline
Context and costHistory grows each roundAgents work from state; a budget per run
PeopleThe output goes straight outRead-only tools; a person approves every patch

What's next

The agents read a sample inventory and stop at a reviewed plan. Next: connect a real asset inventory through MCP tools, hand an approved plan to a patching agent over A2A with a person's approval in between, and run the trajectory checks on live traces.

Disclaimers

  • The agents run against a sample fleet of twelve made-up sites and the public advisory facts. They read and plan; they change no server, and every change still needs a person's approval.
  • The advisory facts are from the sources below, read on 26 October 2026; check them against the PHP project's own advisory before acting.
  • The trajectory checks and the cache figures come from three end-to-end runs on Gemini 3.8 Flash against the sample fleet, on 26 October 2026; other fleets, prompts and models will differ.
  • The OpenTelemetry section was read from ADK 1.30.0's code. The OTLP and Cloud routes were read, not run.
  • This post and its code were written with the help of AI agents and reviewed before publishing.

Sources