Hey folks,
This series has covered three agents that run Tactix on their own: anomaly detection, critical insights, smart actions. They run on a schedule. They reason over store data and push narratives to operators. Nobody talks to them.
This post is about the fourth agent, the Co-Pilot, and it works on a fundamentally different principle. An operator opens it and asks a plain question:
"Why was Saturday slow at Downtown?"
"What's the forecast for the holiday weekend?"
"How's Downtown trending versus last month?"
And the thing that makes it interesting to build is what the agent does not have. It doesn't know the answer. It doesn't hold the data. What it has is a map of where every kind of answer lives, and the judgment to go fetch one from the right pieces, in the right order, for whatever it's handed.
That reframe is the whole post. The Co-Pilot isn't a chatbot with a database bolted on. It's a decision-maker sitting on top of a refined data estate, and its core skill is choosing what to fetch. Everything below follows that one thread: how the data gets refined into fetchable shape, how that shape becomes a set of tools, how the agent decides which tools to pull from, and how we keep those decisions both smart and safe.
Here's the whole thing.
Why fetching is the right mental model
It's tempting to picture a data agent as a smart thing that "has" your data and answers from it. That picture leads you straight into trouble: you end up dumping rows into a context window, the agent hallucinates over them, and it's slow and wrong.
The model that actually works is narrower and more honest. The agent holds almost nothing. It holds a question, a map of available tools, and a reasoning loop. When asked something, it figures out what facts it would need, fetches them one tool call at a time, and reasons over what comes back. It's less like a know-it-all and more like a sharp analyst dropped into an unfamiliar company with a really good filing system: they don't memorize the files, they know which drawer to open for which question.
Everything we built is in service of that: making the drawers well-organized (the data estate), labeling them clearly (the tool surface), and giving the analyst good instincts about which to open (the routing loop).
The estate the agent fetches from
The agent's intelligence is only as good as the structure underneath it, so start there. Back in post two I covered the medallion pipeline: raw data refined bronze to silver to a gold data mart. That gold layer isn't one big table. It's a set of purpose-built marts, each a pre-computed answer to one kind of question:
Performance — revenue, orders, labor, AOV, pre-aggregated by store and period
Forecasts — forward projections, and the same projections aligned against actuals
Comparable periods — day-of-week baselines and z-scores, so "was Saturday unusual?" is a lookup, not a calculation
Weather — forecasts, point-in-time actuals, and historical rain/snow revenue correlations
Events, channel, labor — holiday windows, channel mix, revenue per labor hour

Each gold mart is a pre-computed answer, exposed as one narrow MCP tool. The agent never touches raw rows.
The discipline that makes this work is from post two and it's load-bearing: the agent never touches raw rows and never computes a baseline itself. The analytical work already happened upstream, in dbt. By the time the agent shows up, every hard question is sitting in a table as a clean signal.
That distinction is worth making visual, because it's the single biggest reason the agent is fast and accurate.

Agents reason beautifully over clean signals and terribly over raw rows. The hard work already happened upstream.
Hand an LLM a CSV of point-of-sale transactions and ask it to find a pattern, and you get hallucinations and burned tokens. Hand it "this store did $X, a normal Saturday is $Y," and it reasons like an analyst. So we pay the cost upstream to make every mart a clean answer to a clean question.
From marts to a tool surface
A mart the agent can't reach is useless, so each one gets wrapped as a callable tool over MCP — the Model Context Protocol, the open standard for connecting agents to data and systems. This is the move that makes the whole thing tractable. Rather than hand the agent a database connection and hope, we expose a curated surface of narrow tools, each wrapping one mart with a clear purpose and a clear shape:
fetch_daily_performance_summary,fetch_comparable_periods,fetch_weather_actuals,fetch_historical_weather_impact,fetch_forecast_data,and a handful more.
Each does exactly one thing, returns structured JSON, and carries a description the agent reads to decide whether it's the right reach. That's the tool-design discipline from post one: a tool built for the question an operator actually asks beats a generic run_query that's really just a foot-gun. Raw SQL and unscoped store listing aren't exposed at all.
Two tools sit slightly apart, backed by Postgres instead of the warehouse, because some signals don't live in BigQuery: fetch_smart_actions and fetch_published_insights read the narratives the other three agents already produced. That one's worth pausing on.

The fourth agent stands on the first three. Their conclusions are just another thing it can fetch.
The Co-Pilot pulls the conclusions of the anomaly and smart-action agents as just another fetch, exactly the way it pulls revenue. The work the first three agents did becomes one more drawer the fourth one can open. So by the time the agent goes to work, the entire world is presented to it as a dozen-odd labeled doors, each into one refined slice of the estate. Its only job is to pick the right ones.
The fetching decision
Here's the part that impressed me to build. Between the question arriving and the answer streaming back, the agent runs a loop, and on each pass it makes a single call: do I have enough to answer, or do I need to fetch something, and if so, what?
It's not a script. Each step, the agent emits one move — call a tool, resolve a fuzzy store name, ask the user to clarify, or finalize — then reads the result and decides the next. Fetches chain. The output of one becomes the reason for the next.

Nobody scripted the sequence. The agent reasoned that 'slow' needs a number, then a baseline, then a cause, then a correlation — four marts, decided live.
Make it concrete. "Why was Saturday slow at Downtown?"
Resolve. "Slow" implies a comparison; "Saturday" needs a concrete date; "Downtown" is a name, not an ID. Lock the window and the store first.
Get the fact. Fetch daily performance for that Saturday. Revenue was $X.
Establish normal. Was $X actually low? Fetch comparable periods for the day-of-week baseline. Yes — well below.
Reach for cause. Fetch weather actuals — heavy rain. Then fetch historical weather impact to check whether rain correlates with dips at this store. It does.
Compose. Saturday ran below a normal Saturday, rain was recorded, and rain here has historically correlated with softer sales — with the date range stated plainly.
Nobody told the agent "for slowness questions, run these five tools." It reasoned that a slowness claim needs a number, then a baseline, then a candidate cause, then a correlation check, and pulled from four marts to get there. And crucially, the route changes with the question.

Same agent, same tools. The shape of the question decides the route: one fetch, a backward chain, or a forward-looking pull.
That contrast is the heart of it. "What's my revenue this week?" is a single fetch. "Why was Saturday slow?" is a backward-looking chain of four. "What should I expect for the holiday weekend?" routes the other direction entirely — forecast, events, weather forecast, labor — because now the agent is reasoning forward instead of backward. That directional split turns out to be the cleanest way to think about almost any question an operator asks.

Every question points the agent in a direction. Backward to explain what happened, forward to prepare for what's coming.
That routing — matching the shape of a question to the right subset of a dozen tools, in an order worked out live — is the agent. Everything else is plumbing in service of letting it make that call well.
We do give it help, so its reasoning stays grounded and it spends effort on routing rather than re-deriving basics. Before the loop runs, a fast pass pins down the messy parts of the question — which stores, which dates, what intent — using regex for the deterministic bits ("store 548," "week of June 15") and a small temperature-zero model for ambiguous date language ("the back half of last month").
That resolved context is injected as authoritative. And date windows get corrected deterministically outside the model, so a misremembered boundary never silently skews a fetch.
Where access control fits
Every fetch the agent makes is scoped before it runs — and notice this sits underneath the routing, not on top of it. The access layer from post three is what makes that possible.

Scope isn't a rule we ask the agent to follow. It's which doors exist for this user in the first place.
The agent reasons in store display names — "Downtown," "Airport" — and never sees the numeric IDs underneath, so it cannot route to a store it was never shown. A scoped client gates every tool call against the user's allow-list, which is re-fetched from the authoritative Postgres function on every single message rather than trusted from the client. And a final pass scrubs stray IDs from anything the agent emits, on the assumption that the model will eventually slip one in.
The framing that matters: scope isn't a rule we ask the smart agent to follow. It's a property of the tools it's allowed to call. The agent decides which data answers the question; the wrapper decides which it's even able to touch. Keeping those two concerns cleanly separated is exactly what lets the routing stay as flexible as it is without ever becoming a liability. A store manager and a multi-store owner run the identical agent — they just have different doors.
Sounding like an analyst, and showing the work
Two things turn correct routing into a product people trust.
First, voice. A correct answer that reads like a query result is a bad answer. "Revenue: $4,210. Labor: 28%." is true and useless. So the agent has a prompt layer devoted to sounding like an analyst, kept separate from the tool and scope logic so it can evolve on its own: context for every number, direction and magnitude for every trend, urgency ordering, a close the operator can act on, and brevity, because they're reading on a phone behind a counter. It adapts to the audience the routing already knows about — an executive gets financial-impact framing, a store manager gets operational detail.
Second, streaming. Because one question can fan out into four or five fetches, the answer takes real time, so we stream the process over Server-Sent Events, not just the result.

What the operator sees: the agent shows its fetches as it works, then answers like an analyst — with the date range it used.
The operator watches the agent decide: a plan of which stores it's checking, live tool chips as each fetch fires ("checking comparable periods…"), the resolved date range, then the answer typing itself out token by token. The routing becoming visible is what turns an unavoidable wait into a sense of the thing actually working a problem. We went with SSE over WebSockets for a boring, correct reason: the streaming is one-directional, and SSE passes straight through our Next.js proxies without ceremony.
Challenges
Choosing well, not just choosing. The hard problem in a routing agent isn't calling tools, it's calling the right ones in the right order without burning a dozen fetches on every question. Early on it over-fetched — grabbing weather for a question that was purely about labor — or under-fetched, answering "slow" with a bare number and no baseline.
The fixes were all upstream of the model: tool descriptions written for an operator's mental model rather than a schema, authoritative entity extraction so it isn't guessing at dates mid-reason, and deterministic redirects for a few known traps. The model does the routing; the scaffolding keeps it honest. The principle from Building Effective Agents holds: give the agent the simplest decision surface that still answers the question, and resist over-engineering the loop.
Ambiguity is the median question. With the scheduled agents, the input shape is known. Here, "How are we doing?" — no store, no date, no metric — is the normal case. The agent resolves what it can deterministically, and when it genuinely can't tell (two stores match "downtown"), it stops and asks rather than guessing.
A clarifying question feels slightly slower than an answer; it's infinitely better than a confident answer about the wrong store. Teaching a decision-making agent when not to decide turned out to be as important as the routing itself.
Takeaways

A few things I'd tell anyone building an agent that has to go get its own answers.
Refine before you expose. Each gold mart is a pre-computed answer to one kind of question. The agent reasons over clean signals, never raw rows, because the analytical work already happened upstream.
One narrow tool per mart. A curated MCP surface of purpose-built tools beats a database connection. The agent picks tools by reading their descriptions, so write those descriptions for the question, not the schema.
Let the agent route. The intelligence is matching question shape to the right subset of tools, in an order worked out live. Don't hard-script the sequence; give the model a clean decision surface and let it chain fetches on results.
Chain on results. The output of one fetch is the reason for the next. "Slow" becomes a number, then a baseline, then a weather check, then a correlation — four marts, decided step by step.
Agents can stand on agents. The Co-Pilot fetches the other three agents' conclusions as just another tool call. Published insights and smart actions are doors too.
Scope is a property of the tools, not a rule for the model. The agent decides what answers the question; the wrapper decides what it can touch. Keeping those separate is what lets routing stay flexible and safe at once.
Show the routing. Streaming the plan, the fetches, and the resolving date range turns a multi-call wait into visible progress.
If you're building an agent that has to decide where to go for its answers, I'd love to compare notes. Reach out.
Further reading
Anthropic, Building Effective Agents. the taxonomy behind a tool-using agent that routes rather than follows a script.
Anthropic, Writing Tools for AI Agents. why a curated surface of narrow, purpose-built tools beats raw query access — the spine of our MCP layer.
Model Context Protocol, Specification. the open standard we expose every mart through.
Anthropic, Effective Context Engineering for AI Agents. assembling the right scoped context for each routing decision.
Supabase, Row Level Security. the authoritative scope layer the tool wrapper enforces.
Until next time, Amit.
