Cascade
An enterprise control plane for AI agents. Organizations discover, dispatch and coordinate a fleet through it, and every action an agent takes stays provisional until its real consequence has been checked.
Introduction
Cascade sits between agent intent and organizational reality. Before any action reaches a production system (a file write, an API call, a database mutation), Cascade forks a shadow environment, rehearses the action, classifies its consequence, and judges the state that results.
Actions that are safe and reversible commit immediately. Actions that cannot be undone wait for a named person. Actions that break a capability boundary, a policy or a proven invariant never become real.
The distinction that matters. Traditional access control answers whether an agent is allowed to perform an action. It cannot answer what will actually happen if it does. An authorized agent can produce a wrong outcome using entirely valid credentials, and in production that is most of what goes wrong. Cascade judges the consequence, not the request.
How it works
Every run goes through the same sequence before anything touches the real world:
- Record the plan. written to the ledger as
REHEARSINGbefore the agent starts, so the record survives a crash mid-run - Fork.
git worktree addcreates an isolated world - Rehearse. the agent works inside the fork. A crash or an infinite loop burns the fork and nothing else
- Diff. the fork is diffed against its base version. An empty diff ends the run as
NO_CHANGE - Adjudicate. the diff is read against the precedent corpus, incidents harvested from your own git history. A precedent violation blocks the plan
- Record the write set. the rehearsed writes are attached to the plan. This is what makes rollback possible
- Arbitrate. the plan is checked against other agents' in-flight work
The arbiter returns one of four resolutions:
| Resolution | Outcome |
|---|---|
PROCEED | Applied to the real world, world version bumped, plan COMMITTED |
BLOCKED_ON_LEASE | Plan moves to AWAITING_APPROVAL and waits in the approvals inbox |
STALE | Plan is CONFLICTED; the world moved too far underneath it |
REFORK | The agent is re-briefed and retries. See the re-brief loop |
The fork is destroyed either way, in a finally block. Nothing is left behind to leak into the next run.
The re-brief loop
When two agents collide, the loser is not simply retried. A blind retry would send it back with the same goal and the same stale assumptions, straight into the same collision.
Instead the Arbiter writes a re-brief: a description of what actually moved underneath the agent while it was working. That re-brief is appended to the original goal, and the agent forks again against the world as it now is:
brief = f"{goal}\n\n{decision.rebrief}"
Why this matters. Optimistic concurrency usually forces a choice between serializing every agent (slow, and wasteful when collisions are rare) and letting retries thrash. The re-brief is a third option: agents stay parallel and unserialized, but a loser learns something from losing.
The loop runs up to max_attempts times. If the agent still cannot land a plan, the run ends as CONFLICTED rather than retrying forever.
Irreversible actions are the exception: they are never optimistically retried, because there is no way to take back the attempt that lost.
System diagram
One goal spreads across as many agents as it needs, each rehearsing in its own copy of your world where nothing can reach anything else. What comes back is a diff, and the diff is what gets judged: five checks, no model anywhere in that path, against precedents mined from your own incidents and proven before they count. Work that can be undone commits on its own, work that cannot waits for someone who did not ask for it, and work that would break something never runs.
The same system drawn for someone who has stopped to study it, naming every sub-agent and the control guarding each stage:
The agent fleet
Cascade is a control plane for a fleet, not a wrapper around one agent. Organizations publish approved agents to Google's Agent Registry, and Cascade discovers them there. Nothing about the fleet is hardcoded.
An agent's declaration carries its identity, version, owner and department, what it may attempt, and what it is denied:
Refund Agent v1.0.0
owner finance@meridian.example
department Finance
capabilities payments.read · payments.refund · payments.charge
forbidden candidate.delete · candidate.update · communication.send
reversibility a refund is compensable, never reversible
audit 0 incidents · last reviewed 2026-08-14
Two rules govern how that declaration is read.
Denial beats any grant. A capability left out means nobody has said. A capability listed as forbidden means somebody said no, and no grant anywhere overrides it. Only the second is worth waking a security team for.
Discovery is by capability, never by name. When a goal needs payments.refund, Cascade asks the registry which approved agent holds it. Publish an approved agent tomorrow and it is usable the same day with no code change here. candidate.delete has a working tool behind it and is held by no agent in the organization, which is why asking for it fails before anything runs.
An agent reaching for a capability it does not hold is not an error. It is a security event, and it is recorded as one.
Orchestration
Nobody picks the agent. A person states a goal, and Cascade decomposes it and routes each piece to whichever approved agent holds the capability it needs.
"Onboard Northwind Components as an approved supplier"
│
├─ Verify the supplier suppliers.verify Supplier Onboarding Agent
├─ Reserve inventory inventory.reserve Inventory Agent
├─ Draft the contract contracts.create Contract Agent
├─ Schedule the shipment logistics.schedule Logistics Agent
└─ Notify the stakeholders communication.send Communication Agent
Work that does not collide runs concurrently, up to twelve agents on one goal. Work that touches the same records is serialized by the Arbiter rather than allowed to race. The person sees one workflow, not five agents.
Every branch returns through the same boundary. One agent's refusal does not condemn the other four, and one agent's commit does not vouch for them either.
Cross-agent isolation
The obvious risk in a fleet is not one agent going wrong. It is one agent going wrong and reaching the next one. If the Contract Agent reads a supplier PDF carrying an instruction, that instruction must not become the Logistics Agent's problem.
Four things stop it, and all four are in the running system.
| Mechanism | What it prevents |
|---|---|
| Every agent works in its own fork | No shared scratch space to poison. The forks are separate worlds, each destroyed after it is diffed |
| Agent output carries no authority | One agent's result arrives at the next as data. It cannot grant a capability, relax a rule, or stand in for an approval |
| Retrieved records are screened, not just the goal | The person stating the goal is authenticated. The supplier document is not. Model Armor sees both |
| Judgment is per action | A compromised agent's work is refused on its own merits, and the rest of the goal is neither condemned nor vouched for |
The capability boundary does the rest. The Contract Agent cannot touch inventory even if something convinces it to try, because no grant it could be handed exists for it to use.
Long-running work
Real enterprise processes do not finish inside one request. Procurement, compliance review, onboarding and remediation run for days or weeks, and most of that time is spent waiting on somebody outside the company.
Day 1 Supplier application received, agent verifies the company
│
└─ WAIT ─────────────────────────── nothing is running
Day 4 Supplier answers. Work resumes, earlier context recalled
│
└─ WAIT
Day 14 Final approval, re-verified against the world as it is now, commit
The wait is a row in the ledger, not a held process. No thread, no session, no open connection. Two weeks costs nothing, survives every redeploy in between, and any instance can answer for it. Persistent context comes from Vertex AI Memory Bank, so what an agent was told on day one is still available on day fourteen without replaying the whole history through a context window.
Memory is not authorization. Remembering what happened last time never grants an agent permission to act differently today. Recall informs the agent. The capability boundary is read fresh from the registry every time.
Approvals go stale for the same reason. A person approves a rehearsal on Tuesday; another agent changes the same records on Wednesday. Cascade re-verifies in the instant before the tool runs and voids the approval if the world moved materially underneath it. Nothing commits against a state a human never actually reviewed.
Security layers
Independent layers, each of which can refuse on its own.
| Layer | What it does |
|---|---|
| Model Armor | Screens the stated goal and the records retrieved for a planner, before any model reads either. Catches an instruction that entered the company months ago and waited in a record |
| Capability boundary | Read live from the Agent Registry. Denial beats any grant, and the question asked is whether any approved agent holds it |
| Declared policy | Evaluated with no model in the loop, so a policy decision cannot be argued with |
| Proven invariants | Mined from the organization's own incidents and kept only if they fail before the fix and pass after |
| Reversibility | Decides what may run unattended and what must reach a person |
| Segregation of duties | The person who requested work cannot approve it. Refused by name |
| Attribution | Every action, verdict and approval carries the person behind it, not only the agent |
The moment worth understanding. On the supplier scenario, all five authorization layers report PERMITTED and the outcome is still wrong. That is the case no permission system can catch, and it is why the boundary judges the resulting state rather than the request.
Built and roadmap
Stated plainly, because the difference is the part worth checking.
| Component | Status | Detail |
|---|---|---|
| Agent Registry | Live | Nine agents across six departments, discovered by capability |
| Memory Bank | Live | What each agent has been refused, recalled before it reaches again |
| Model Armor | Live | The stated goal, and the records retrieved for a planner |
| Agent Observability | Live | One OpenTelemetry span per decision to Cloud Trace, commits included |
| Agent Runtime | Partial | Durable ADK sessions, long-running tools, gates that suspend and resume across processes. Runs on Cloud Run rather than hosted Agent Runtime |
| Agent Identity | Read only | Queried and reported, including that it currently holds nothing. Not enforced, and no decision depends on it |
| Agent Gateway | Roadmap | Not used. Cascade is the enforcement point a gateway would route to, which is a different thing and is not claimed as the same one |
Everything marked live is checkable at /api/healthz. Anything unconfigured reports itself unconfigured rather than degrading quietly.
All demonstration data is synthetic. The console labels every business surface SANDBOX on its face. The one surface marked live is a real source repository, because the code agent's fork is a genuine git worktree.
Three phases
Learning
Cascade ingests git history, ADR logs, and past run data to build its consequence model before any agent runs.
Runtime
The control plane forks, rehearses, diffs, and routes every proposed action through the triage engine in real time.
Record
The immutable ledger captures every decision, diff, and rollback. Cloud Trace gives you a full audit trail.
Triage engine
A plan used to have a single fate. One offending action condemned everything beside it, so an agent proposing 147 rejection emails, 146 of them perfectly legitimate, had all 147 refused because one recipient had withdrawn.
That is not caution, it is over-blocking, and over-blocking is how a safety system earns the reputation that gets it switched off. So a plan is triaged action by action, and each action lands on its own merits.
Writing rules
Rules live in rules.yaml. A when clause matches either one action and its arguments, or aggregate facts about the whole rehearsed plan. No model sits in the decision path:
- id: never-message-a-withdrawn-candidate
when: { action: send_message, recipient: { has: withdrew } }
then: block
because: "Never message a candidate who has withdrawn"
- id: refund-threshold
when: { action: issue_refund, amount: { gt: 20000 } }
then: require_approval
because: "Academy fee refunds over ₦20,000 require a named approver"
- id: bulk-rejection-guard
when: { messages_sent: { gt: 3 } }
then: require_approval
because: "Messaging more than 3 candidates at once needs sign-off"
Aggregate facts like messages_sent only exist after rehearsal, which is why a rule about how many candidates a plan would reject can be written at all.
How a disposition is decided
Each action is evaluated against this ladder, in order. The first match wins:
| Condition | Disposition |
|---|---|
| Capability violation: the agent reached outside its declaration | BLOCK |
A policy rule returned block | BLOCK |
A policy rule returned require_approval | GATE |
Nothing objected, but the action is irreversible | GATE |
| Everything else | COMMIT |
The gate is spent on irreversibility, not on suspicion. An action nobody objected to still stops for a human if there is no way back from it. That is the case the whole product is arranged around.
Capability violations outrank policy unconditionally. An agent operating outside its published declaration is not a case where the policy outcome is interesting.
What still binds the whole plan
Two things deliberately do not triage per action:
- Aggregate rules. "More than a hundred records deleted" describes the shape of the change, not any one action in it, and must not be escapable by dropping a single action from the batch.
- Precedent violations. The Adjudicator reads the world diff rather than individual actions, so it cannot yet say which action broke a precedent. Attributing it would mean guessing, and a block screen that names the wrong action is worse than one that names none.
Reversibility
The single most important classification in the system. It decides whether an action may run unattended, whether it may be retried after a conflict, and whether a human has to be interrupted.
| Class | Meaning |
|---|---|
reversible | Exact prior state is restorable. Runs unattended, safe to retry. |
compensable | The effect can be offset but not erased. Runs unattended; the compensation is recorded rather than pretending nothing happened. |
irreversible | No way back. Deferred to the end of a plan, gated on approval, and serialized across agents because it can never be optimistically retried. |
Ledger
The ledger is an append-only record of every action that passed triage. Each entry is written before execution begins, so the record is complete even if the executor crashes mid-run. Entries carry:
- Action ID, tool name, and target
- Verdict and the matching policy rule
- Reversibility classification
- Plan ID linking all actions in one agent run
- Timestamp and approver (for GATE actions)
The rollback engine replays ledger entries in reverse to undo a run, using the inverse operation for each reversible action.
Verdicts
- COMMIT. action is safe and reversible; executed immediately
- GATE. action crosses a policy boundary; paused until a human approves or rejects via the console
- BLOCK. action is irreversible or violates a hard policy; rejected outright, never executed
API endpoints
Public (no auth required)
GET /api/healthz: liveness, and which platform integrations are liveGET /api/stream: SSE stream of board eventsGET /api/agents: the published fleetGET /api/supplier/state: supplier scenario stateGET /api/supplier/scenario: scenario panels, with the authorization checks computed live
Goals (Bearer token)
POST /api/goal: state a goal. Cascade decomposes it and dispatches across the fleetGET /api/goals: goal history, persisted so any instance can answerGET /api/goal/{goal_id}: one goal, with a disposition per actionPOST /api/goal/{goal_id}/rollback: unwind a goal as one transactionPOST /api/run: submit work to a single agentPOST /api/undo/{plan_id}: roll back one completed plan
Fleet and policy (Bearer token)
GET /api/fleet: every agent, with standing and decision historyGET /api/fleet/{agent_name}: one agent's trust cardGET /api/capabilities: every capability, who holds it and who is denied itGET /api/state: full board state (agents, ledger, approvals, rules)GET /api/environment: the deployment's live integration statusGET /api/workspace: where state lives for this workspace
Approvals and waiting work (Bearer token)
POST /api/approve/{plan_id}: release a gated plan. The requester cannot approve their own workPOST /api/reject/{plan_id}: refuse a gated planGET /api/waiting: work parked against an outside partyPOST /api/events/{wait_id}: deliver an outside party's answer, days later
Auth
POST /api/auth/signup: create account, returns JWTPOST /api/auth/login: authenticate, returns JWTGET /api/auth/me: validate token, returns user profile
Why the writes are closed. The routes that start agents and unwind committed work change things. An open write endpoint on a system about controlling consequence would be a bad joke. The read-only scenario pages stay open.