Last post I said the planning turned out harder and more interesting than the doing. This is me paying that off.
Quick recap so this stands on its own. I build a CLI where you type a sentence and an LLM picks one action out of hundreds of apps and runs it on your real accounts. Last post was about direct mode, the get-out-of-my-way mode, and the two things it has to get right every time: which action, and which account. This post is about the other mode. Plan mode. The one that's supposed to be the careful, safe one where the agent shows you what it's going to do before it does it.
I figured plan mode would be the easy half. You don't even execute anything, you just write down the steps. How hard can writing down steps be. It turned out to be most of the months.
What plan mode is
By default you're in direct mode, and the composer tells you so. There's a little control sitting right there, and if you want to flip to plan mode you click it. That's the whole trigger. The agent never sniffs your request and decides on its own that this one feels risky, because that would be unpredictable and you'd never know which mode you were in. It's your call, every time.
The contract is the one most people already know from other tools. In plan mode the agent makes a plan, you read it, you change what you want or you approve it, and only then does it go and execute. Nothing touches your accounts while you're still looking at the plan. That's the promise. Most of this post is what it took to make that promise actually true, because the first few versions of it were lying to you in one way or another.
It started inside the main agent, and that was the first mistake
The first version didn't have a planner at all. I had the main agent do both jobs. You'd drop a keyword in your message and that flipped the same agent into planning behavior, it would go research and hand you a plan, and in direct mode that same agent would just do the work. One agent, two modes.
That didn't hold. The two jobs pull in opposite directions. Direct mode wants to act, plan mode wants to hold back and think, and asking one system prompt to be both meant it was good at neither. It would start planning and then drift into doing, or it would be in direct mode and get weirdly cautious. The modes interfered with each other.
So I tore plan mode out of the main agent entirely and made it a separate agent with its own system prompt, written from scratch. This one only researches and produces a plan. It cannot execute anything, that's not a rule I asked it to follow, it's just not wired to. Once planning is its own agent with one job, it stops fighting itself. That was redo number one, and it's the move everything else sits on top of.
Then it planned for things it had never looked at
The separate planner had a worse problem. It made plans up.
You'd ask for something inside an app, say some work on a Salesforce org, and it would confidently hand back a clean plan. The plan looked great. The plan was fiction. It didn't know what custom objects that org actually had, what fields were already there, what the data looked like, so it filled the gaps with assumptions and wrote those assumptions down as steps. Plans built on guesses break the moment they touch reality.
The fix was to stop letting it plan blind. I gave the planner a set of read-only tools, the non-destructive ones, so before it writes a single step it goes and looks. It reads what's in the org, checks what exists, and plans against what's actually there instead of what it imagined. Research first, plan second. Obvious in hindsight, but the first version genuinely skipped it and just talked.
Then it researched too little and planned too eagerly
Giving it tools didn't make it use them well. The planner was eager. It wanted to emit a plan, that was the satisfying thing to do, so it would do the bare minimum of looking and rush to the output. It also wouldn't ask you anything, even when it clearly should have, because asking felt like a delay.
I needed it to slow down and ask the right questions before committing to a plan. But there's a trap there. If you make an agent ask questions, it asks bad ones. It asks you things it could have figured out, or things so basic that answering them is annoying, and now the user is doing data entry for the agent. Nobody wants that.
So I didn't cut the questions, I changed their shape. The planner still asks, but every question comes with a recommended answer already filled in, one I supply based on what we already know. So instead of typing out an answer, you're confirming or nudging one. You glance at it, it's usually right, you move on. You only stop and think on the ones that actually need you. That kept the questions, which made the plans real, without turning the user into the planner's research assistant.
The two agents couldn't talk, so I made the plan carry the conversation
Splitting the planner off solved the interference problem and created a new one. The planner and the main agent are different agents, different sessions, different memories. All the good stuff, the research, the findings, the assumptions, the back-and-forth about what you actually wanted, all of that happened inside the planner. The main agent was never in that room.
So when it came time to execute, I was handing the main agent a list of steps with none of the reasoning behind them. It didn't know why a step was there, what we'd assumed, what we'd ruled out. It was being told what to do with no idea why, which is exactly how you get an executor that does the letter of the plan and misses the point.
The fix was to make the planner's output do double duty. It doesn't just emit steps, it packs the assumptions and the risks and the reasoning into the handoff in a concise form. So the main agent picks up the plan and inherits most of the context that produced it, call it ninety percent of what we worked out together. It knows what it's doing and why, not just the steps.
What you actually see when you read a plan
Here's where the post 1 promise gets paid. Last time I said plan mode is the answer to the wrong-account problem because you see every step before it runs, including which account it touches. This is that.
The plan is a list of steps in plain English. Where it helps, it gets specific, so for the Salesforce case it names the actual custom objects and fields it's going to touch rather than saying "update some records." Each step tells you what it's going to interact with, and it tells you the blast radius, like how many records or how many people this is going to affect. And critically, each step shows you the alias of the connection it'll run on. Not the raw id, the alias from last post, but enough that you can see this step is going to Client A's org and that one is going to Client B's.
And when you've got more than one connection that fits, the thing that caused the original wrong-org bug, plan mode doesn't guess. It gives you a selectable pick, which connection do you mean, right there in the plan. So the disambiguation that direct mode could only do when the model knew it was unsure now happens up front, in front of you, as a choice you make. That's the gap from post 1, closed. A wrong account shows up as a line you can read before anything runs, not as damage you find afterward.
Deciding what's destructive, in two layers
A plan is only safe to approve if the dangerous steps are actually marked dangerous, so the agent stops on them. The question is how you know which ones those are.
For our predefined actions it's the nature of the action. Adding, deleting, renaming, that kind of thing gets marked destructive automatically, and when execution reaches a destructive step it stops and asks you before running it. Human in the loop, at the exact step that matters.
The hard case is the raw API fallback from post 1, the escape hatch where there's no predefined action and the agent just makes a direct call. There's nothing to tag there, it's a method and a URL. So two things handle it. First, the agent judges by intent, it knows what the call is actually going to do, and a call gets flagged destructive based on that even when it's a GET that happens to do something dangerous. The verb doesn't decide it, the effect does. Second, under that there's a command classifier that looks at the whole command and rules it safe or not. I ran it across twelve hundred different commands and it sits around ninety-nine percent. So the model's judgment is the smart layer and the classifier is the boring reliable layer beneath it, and a step has to get past both to be treated as harmless.
The plan isn't a flat list, and it isn't blindly trusted
A couple of things make the plan more than a checklist.
Steps carry criticality. Every step is a must, a should, or a could, and that decides what happens when one fails.
| Criticality | What it means | On failure |
|---|---|---|
| must | the core of the plan, the reason you're here | the plan failed, full stop |
| should | important, but not load-bearing | you choose: retry it, or move on |
| could | optional, like backfilling some old records | you get told, the plan carries on |
So a failure halfway through doesn't mean the same thing every time, it means whatever the importance of that step says it means.
Steps also carry evidence. The planner doesn't just research and forget, it attaches the evidence for what it found to the step. So at execution, when it stops to confirm the next step, it shows you the evidence from the one before, and you can actually see what happened rather than trusting that it happened. And if you push back, if you say that step didn't really go through, retry it, it doesn't just blindly redo it. It checks. It can go query the platform and look at the real state, and it'll come back and tell you no, this is already done, you're wrong. It prefers the data over your claim, which is the right call, because the data is the thing that's actually true.
And the plan is a graph, not a line. The planner emits dependencies, this step needs these other steps first, and steps that don't depend on each other can run in parallel while the dependent ones wait their turn. When a destructive step pauses for your confirmation, the whole plan pauses, not just that branch.
That whole picture, the evidence and the criticality and the dependencies, is also why a stale plan isn't a disaster. Research happens at plan time and execution happens later, and the world can move in between. When it does, the agent isn't stuck. It can pause on a step whose evidence no longer matches reality and tell you the thing you approved isn't true anymore. You can cancel a running plan whenever you want. And you can go back to the planner and replan, as many times as you need in the same chat, because the planner keeps its context across the session and can redraw the plan from the new reality. The main agent even gets a say, it can push through an outdated plan by adapting, or it can throw it back and ask for a fresh one. The plan you approved is a starting point, not a contract you're locked into.
Where it still falls short
Same as last time, I'd rather tell you what this doesn't buy you than let you assume it's airtight.
Prompt injection is still real. The agent reads emails and docs and pages, and if one of those smuggles in an instruction, plan mode doesn't magically stop that. What it does is box it in. The agent has no general execution, it can only act through the defined actions and the connections you authorized, so an injected instruction can't make it do something off the menu, it's stuck inside the same narrow surface as everything else. And in plan mode the whole thing is on the table for you to read before it runs. So injection is bounded, by what the agent is even able to do and by you reviewing it. It is not solved, and I'm not going to pretend it is.
And the planner can still pick the wrong app or the wrong connection. A confidently wrong planner writes a clean, convincing plan for the wrong target, and nothing inside the planner catches that, because it doesn't know it's wrong. The thing that catches it is you, reading the plan, seeing the alias, seeing which org each step is pointed at.
Which is the honest version of the whole post. Both of the gaps that are left, injection and the wrong pick, come down to the human actually reading the plan. Plan mode moves the safety from trust the model to trust the human to look, and that's a real improvement, but it's only worth as much as a user who doesn't just hit approve on reflex. The structural pieces carry their share, the bounded action surface, the alias work from post 1, the classifier under the destructive checks. Review carries the part structure can't reach. If you rubber-stamp the plan, you've handed back the one guarantee plan mode actually gives you.
What I took from it
Separate the agent that thinks from the agent that acts. The whole thing started working once planning and doing stopped being the same system trying to be two things. One agent that only plans, one that only executes, each good at its one job.
Don't let an agent plan against a world it hasn't looked at. The planner that made things up wasn't dumb, it just had no eyes. Read-only research before it writes a single step did more for plan quality than any amount of prompting the plan to be careful.
If you make an agent ask questions, prefill the answers. Questions make the plan real, but raw questions make the user do the work. A recommended answer they can confirm keeps the rigor and skips the data entry.
Trust verified evidence over the confident claim, including the user's. The step that checks the platform and says "no, this is already done" is more useful than the one that politely redoes whatever you tell it to. Make the data the tiebreaker.
And know exactly what your safety actually rests on. Plan mode's structural guarantees are narrow and real. Everything past them rests on a person reading the plan. That's fine, as long as you're honest that it's true, and you don't build the rest of the system pretending the human is a backstop that never blinks.
The doing took an afternoon. The planning is what the rest of the months were for, and I think it's the more interesting half.
Top comments (10)
The "plan mode moves safety from trust the model to trust the human to look" line is the load-bearing honest stage, and it's the part a lot of agent-plan-mode writing skips. Once you say it out loud, the whole design has to start defending against review fatigue, which is the symmetric failure mode to alert fatigue in SRE. A user who reads forty plans a day starts rubber-stamping, and the structural guarantees that depend on review collapse silently.
One extension worth flagging on the 99% classifier: how the 1200 commands were chosen matters more than the rate. If they came from production traffic, the test set is conditioned on what users already type, which means the classifier is being measured on the part of the command space it's seen and missing the part it hasn't. The thing that breaks it in the wild is the command nobody thought to test against, because nobody thought to write it yet. Planted-fault sampling against adversarial command shapes (deliberately constructed to evade classification) is the version that gives you false-negative signal instead of observed precision.
The "data over user claim" tiebreaker is the part I keep coming back to. Inverting the trust-the-author default takes more discipline than agent designs typically allow.
On the sampling, good news, it's better than production traffic, and worth being precise about. The twelve hundred didn't come from what users typed. They came from our actions repository, every action our apps register, and we generated combinations off that set and ran the tests across those chunks, plus the custom API calls on top.
The thing that makes that matter: the command is never written by the user. The user instructs in plain language, the AI writes the actual command, and the AI can only write from the registered action set. So the command space isn't open ended, it's bounded by what our own AI is even able to emit, and the 1200 are combinations sampled out of exactly that bounded space. So it's not observed precision on the slice users happen to hit, it's coverage of the space the AI can actually produce.
Where your point still bites is the custom API call tail, because that's the part that isn't a registered action, so that's the open ended bit where the AI isn't picking from a fixed menu. Adversarial, planted-fault sampling is the right move there specifically, on the raw calls. Inside the action set, the combinations basically are the space, not a sample of it.
The review fatigue point I can't wriggle out of the same way, and it's the honest weak spot of the whole design. Everything past the structural guarantees rests on a human actually reading, and a human reading their fortieth plan is not reading. The thing I think helps is not flattening every step to the same weight, so the destructive and high-impact steps grab the eye and the routine ones recede, so the limited attention goes where a wrong call actually hurts. That doesn't solve it, it just spends the attention better. The real fix is not making someone review forty plans a day in the first place.
And on the data-over-user tiebreaker, thanks, that's the one I was most unsure about shipping, so it's good to hear it land. Overruling the user feels wrong to build, right up until the agent catches something real by sticking to the data, and that's the moment it stopped feeling rude and started feeling correct. Appreciate you calling it out specifically.
The action-space sampling is the right source and worth naming precisely: you are testing the capability boundary, not the query distribution. Those two can diverge badly in production — a user who discovers a path the repository did not register is the gap the twelve hundred misses. That is not a criticism of the approach, it is the residual surface the approach cannot see by construction.
The dual-gate framing makes the asymmetric cost argument cleaner than a single classifier. Intent layer reasons about what the call actually does in context; classifier pattern-matches on form. Two different failure modes, not just two chances to catch the same thing.
Where they could still fail together: an adversarial input that frames a destructive call as an obvious benign one. If the intent layer training included confident framing as a harmlessness signal, a convincing enough description routes both gates wrong simultaneously. Is that handled at training, or is there a third check that does not share the framing context?
Right on the first half, capability boundary, not query distribution, and the unregistered path a user discovers is exactly the surface the twelve hundred can't see by construction. No argument there.
On the gates failing together: the thing that saves it is that the two gates don't read the same input. The intent layer reasons over the call in context, the description, what it's meant to do, and that's the layer your framing attack lands on, because framing is language and that layer is reading language. The classifier underneath never sees any of that. It gets the raw call, method, URL, body, and rules on the form of the thing, not the story told about it. So a confident benign description can walk past the intent layer, and the classifier is still staring at a literal DELETE or a literal destructive payload that the framing never got to touch. That's the check that doesn't share the framing context, it's the whole reason it sits under there as the boring layer instead of the two gates being one smarter model with two prompts. Two inputs, not two passes over one input.
Where you can still get both at once: a call that's destructive in effect but benign in form. Innocuous-looking endpoint, ordinary-looking payload, damage happens server-side. Now the framing's clean for the intent layer and the form's clean for the classifier, and neither catches it because neither has anything to catch. That one's real and I don't have a gate for it. It falls to the blast radius on the step and the human reading the plan, which is the part I already said is load-bearing and thin.
Worth narrowing it though: this only exists on the raw API tail. For the predefined actions, destructive is tagged by the nature of the action, not judged off any description, so there's no framing to attack. Add, delete, rename is destructive whether you describe it sweetly or not. The framing attack only has somewhere to live on the raw calls, the same open-ended tail you pointed at for adversarial sampling. So it's one surface to harden, not two.
"Destructive in effect, benign in form" is the real surface, and the gate that closes it has to read neither the framing nor the form — it has to read the execution semantics.
Both existing gates check what they can see before the call fires. The intent layer sees the story, the classifier sees the shape. Neither gate sees the server-side effect, because the effect only exists after execution. The gate that could close it is post-execution: something that reads the state delta and asks whether that delta was in scope for what was requested. That's a different architecture — it doesn't block the call, it invalidates the step and triggers rollback or escalation. For the raw API tail that's probably the only viable close, and it's expensive because the system has to define "in scope" precisely enough to diff against.
The predefined action tagging is the right move for the bounded surface. The raw tail is the genuine open problem.
The delta comparison is the part I actually like out of all of this. Reading the state change and asking whether it was in scope is the right question, more than anything that happens before the call. The reversibility point underneath it is the other half I keep landing on, and the two stack badly.
Where I get stuck is the same place you put the cost. You need something to diff the real delta against, an expected delta, and on the raw tail I don't have one. The predefined actions could give me that, they declare what they write, so "in scope" is a real definition there. But the raw call is raw exactly because nobody declared its semantics, so there's nothing to compare the result to. The automated version of the gate needs a spec the raw tail doesn't come with.
And even where I could read the delta, reversibility decides whether reading it helps. Rolling back only exists where I own the substrate, my own database. A write to someone else's CRM, or an email that's already gone, has no after to undo. So the worst case is the call that's both undeclared, so I can't say what it should have done, and irreversible, so even catching it after the fact buys me nothing. No spec going in, no undo coming out.
Honestly, the only close I've got past that is the same human reading the diff, which is the load-bearing-human problem again, just moved downstream. Better to look at the realized change than the predicted plan, but it's still a person reading their fortieth thing of the day, so I don't think it actually solves it. It's the weak spot wearing a different hat.
So I'm genuinely open on this one. How would you get an expected delta for an undeclared call, or close the irreversible case, without putting a human back in the loop? That's the piece I don't have a clean answer for.
The two problems stack into exactly one class, and naming that class is the close. Undeclared and irreversible is the only combination with no spec going in and no undo coming out. Everything else has a cheaper gate: a declared call gets the delta diff, a reversible call gets the rollback. So the move isn't to build a gate for the raw tail, it's to refuse it: an undeclared call runs only where the substrate is reversible, and undeclared-plus-irreversible stops before execution rather than getting read after. That shrinks the load-bearing-human problem instead of moving it downstream. The human isn't reading the realized diff of everything, they're reading only the calls that are both undeclared and irreversible, because that intersection is the one place no automated close exists. Rare is what makes a human actually read the fortieth thing, and this makes it rare by construction. The residue I'd concede: you still have to classify reversibility correctly, and "someone else's CRM" versus "my database" is a judgment the system can get wrong. But that's a bounded property of the endpoint, declared once per integration, not a story told per call. It's the one input the framing attack can't reach, same reason the classifier sits under the intent layer.
The number I keep looking at is the 99% on the command classifier across twelve hundred commands, because that's about a dozen it called wrong, and the whole safety story hangs on which way those dozen broke. A safe call flagged destructive just costs an extra confirmation. A destructive call waved through as harmless is a deleted Salesforce object before the human ever sees the pause. Do you know the split there, and does the intent layer on top reliably catch the dangerous ones the classifier misses? That asymmetry feels like the real spec hiding behind the headline number.
That asymmetry is the right thing to be looking at, and you're right that the headline number hides it. A safe call flagged destructive is a free extra confirm, a destructive call waved through is the actual disaster, so those two errors are not worth the same and I don't treat them as the same.
Two things on it. First, the classifier isn't the only gate. The intent layer sits above it, the model reasoning about what the call actually does, and a step has to clear both to be treated as harmless. The whole reason there are two layers is that asymmetry, the classifier is the cheap fast pass and the intent read is there to catch the dangerous thing it shrugs at. When the two disagree, it stops.
Second, the honest part: I've been quoting aggregate accuracy, not the false-negative rate on its own, and you've correctly spotted that the false-negative rate is the only number that actually matters for safety. I owe a cleaner breakdown of which way those misses broke, and I'd rather over-flag and eat the extra confirms than under-flag even once. That's the bias I want the system to have, and measuring it properly instead of quoting one number is the next thing.
This matches what most people find once they get past the demo. Execution is the part agents are good at; planning is where the judgment lives, and it's the part you can't fully hand off. A good plan is mostly deciding what not to do and which constraints are non-negotiable, and that's exactly the context the agent doesn't have unless you give it. The leverage is spending your effort on the plan you can still veto cheaply, because course-correcting after execution starts costs far more than rejecting a bad plan up front.