One Tool to Rule Them All: Building a Fast AI Agent for Real-Time Response

The Case for Simplicity: One Tool for Response Tasks

The speaker introduces the core argument: that stripping back agent complexity to a single tool produces faster, more effective results for response tasks. They contrast this with the industry trend of loading agents with many MCP servers and skills, explaining that in time-critical response situations, unnecessary tooling wastes precious seconds.

Optiva's Risk Team: The Problem Space

The speaker introduces Optiva's risk team, whose job is to monitor trading limits and prevent overexposure to any single asset, company, or country. When a limit breaches, the team must rapidly sift through petabytes of data using SQL to identify the cause and advise trading — all while the financial exposure clock is ticking.

The Cost of Slow Investigation and the AI Opportunity

The speaker quantifies the problem: pre-AI, human investigation of a breach took fifteen to twenty minutes at best, and could stretch to hours. The arrival of generative AI tools like Cursor and Claude Code prompted the team to explore whether an agent could dramatically accelerate this forensic work.

Lesson One: Agents Do Well with Generic Tools

The team's first iteration was a simple folder of markdown documentation covering Postgres commands, Databricks access, limit definitions, and data formats. Deployed in Cursor, this alone cut investigation times by half or more — a significant early win that proved the basic viability of the AI-assisted approach.

The Bash Bottleneck: Statelessness Slows Everything Down

Bash's lack of statefulness emerged as a key limitation. For every follow-up question, the agent would run a full new database round-trip — rewriting hundreds of lines of Python and regenerating plots — wasting time that the risk team could not afford. The speaker draws inspiration from Cloudflare's approach of converting MCP servers into TypeScript functions, and notes that LLMs have deep familiarity with the notebook paradigm of building incrementally toward an answer.

Lesson Two: A Persistent Python Sandbox Changes Everything

Switching to a single Python interpreter tool — with persistent state across turns — transformed agent performance. Without any special prompting, the agent naturally stored data in variables and reused it for follow-up queries, mirroring how a Jupyter Notebook builds toward an answer. Plot modifications and incremental data exploration became fast and natural.

Lesson Three: Back Pressure — Steering the Agent Before It Wastes Time

By studying execution traces, the team identified recurring failure patterns: timed-out queries, references to non-existent columns, and aimless schema exploration. Because the single-tool approach meant the tool was simply Python code, it was straightforward to add 'back pressure' — pre-flight checks that detect obvious mistakes, block oversized queries, and restrict table discovery to a controlled function — eliminating the most frustrating forms of agent muddling.

SQL Linting with Abstract Syntax Trees

As basic errors were eliminated, more subtle issues emerged — such as the agent confidently reporting nonsensical dollar figures from misconfigured aggregate queries. The team's solution was to parse the Python and SQL code into abstract syntax trees, then apply deterministic linting rules: for example, checking that GROUP BY clauses include the meaningful columns before allowing an aggregate query to execute. An escape hatch boolean lets the agent override checks when it has good reason to, keeping the system flexible.

AST Tricks and Takeaways

The speaker shares additional AST-based checks, including detecting when the agent uses the Python tool purely as a scratchpad without surfacing an answer to the user. They close with three key lessons: give the agent freedom with generic tools; provide a stateful scratchpad to build up answers notebook-style; design an intuitive function universe to reduce friction; and leverage code introspection tools like ASTs for advanced, scalable back pressure.

Today, I'll be talking you through, basically trying to build a story and convince you that when you're trying to build agents for response tasks, being, if you wanna make them fast and effective, if you strip back the complexity and think about just using one tool, you can accomplish some pretty impressive results. So yeah, like most of the time, like the whole industry has been pushing towards like, oh, how many MCP servers can you jam into your agent? Or like, I have like a 100, tens of thousands of skills. That can work really well to build something that's quite effective at solving a task.

But when you put it in a response situation, you really don't wanna waste time, like, going through some confluence or Jira backlog when what you really wanna do is search for logs or something. So, yeah, and I'll be showing this to you through essentially how we used this kind of design pattern for our risk team. So without further to do, I'll quickly introduce you to what Octavus risk team does. So the risk team is essentially responsible for monitoring risks across Optiva. So a bit of finance one zero one, like when you're monitoring risks, risk and trading kind of work together, and they're of like yin and yang to each other.

So trading left to their own devices would very quickly, love to punt, like, everything onto, like, Apple or something and probably lose a lot of money, but risks are there to sit and actually make sure that what Optima doing is doing with their book is seen. So, yeah, with that in mind, they monitor many, many risks. And their whole goal is to essentially make sure that we are not spending too much of our book or like exposing ourselves to too much of one thing, like too much of one company, too much of one country, etcetera, etcetera.

So, yeah, they have a big dashboard of risk that they monitor, and you can see sometimes one of these limits breach. So, when a limit breaches, essentially, all they're given is just a little bit of a graph showing, the history of that limit, and then we just get a big ding ding ding, like it's breached, and they get a time.

So now this is presented in front of them. They need to figure out very quickly what caused this breach. So Optiva for context, we generate, like, petabytes of data and gigabytes a second. So all this data will be flying past them, and they'll have to very quickly be able to filter this stuff out with SQL and essentially tell trading, like, what they have to do to scale down. And that is not an easy thing to do when you have that much data to look through.

Yeah. So and another thing that's going through at the back of their mind is that for every second that they spend on this, it is costing money. Like the time that we don't know essentially why the breach has occurred, trading will have to scale back or we could be exposed to some risk that we have no clue about and that is essentially not good. So before AI, humans would take possibly, like, twenty minutes, fifteen to twenty minutes, best case, but it can very quickly scale to very, very bad numbers, like up to hours.

So, yeah, this has been the way of things for a long time. And essentially, when these cool agents dropped off, we were thinking, Okay, once basically Genetic AI took off with Cursor and ClotCode and such, we were starting to think, Okay, maybe this could be a pretty good candidate for being able to look into these response tasks.

Like if we just dumped it into that data block and just asked it to figure out what's happening, maybe it can do a decent job. So yeah, I'll fast forward many weeks of experimentation, and I'll bring you to our first lesson. So the first lesson is that parts do pretty well with generic tools. So I kid you not, like, our first iteration was just a folder of markdown that explains, like, oh, this is the Postgres command. This is how you access Databricks to access this and that.

This is what a limit is. This is the kind of data we use. And, yeah, if we just gave that and then told it to go through its own devices and figure out what caused the breach, it actually did a pretty decent job. And we just bundled this folder and gave it to risk to run-in like cursor or whatever, and they loved it.

Like, it cut investigation times like like properly by around half or even more in some cases. So it was quite a good win. Yeah. And it was pretty great. But, of course, like, we could always do better, and we can go faster. So I'll put, like, a simple example up on the screen. Like, if we ask, like, follow-up questions, the agent was very, very keen to just, basically do another round trip to get the data that it wants.

And this is kinda like inherent to Bash. Like, Bash is a really good tool that we have. And before AI even existed, we've done a very, very good job at building up to having essentially a great text interface, which is like LLM's bread and butter, but it's not very stateful. So like the most the biggest frustrations we would have is like you might ask it to change a plot or like do some small modification and such.

The agent would be like, oh, sure. And then it just writes another thousand or 500 lines of Python or whatever to do another round trip to the database, modify the graph, and then just give it to you. And you can just feel the time being wasted while you're sitting there and trading's asking you many, many questions about why on earth is happening. So, yeah, Bash definitely didn't feel like the right medium for the end goal of what we were trying to build.

And yeah, we were pretty recently inspired by Cloudflare's code mode thing. I don't know if you guys heard about it, but they've been exploring essentially taking MCP servers and turning it into TypeScript functions. And that is quite a good idea, especially in this space as well. Like I know somewhere inside the model's brain that it is really good at handling notebooks.

Right? Notebooks essentially just build up from data to an answer at the end. So if we can somehow, like, grab into that space of the models, like, space and pull out that knowledge, then we could probably start building agents that actually follow notebook style, build up to an answer, and get things faster. So that leads up to the next lesson that we learned, which is bots do a lot better if they have some sort of persistent sandbox. So, yeah, we gave an agent basically a Python interpreter.

And spoiler, this is the one tool that we're gonna stick to. And it actually did way faster. Like, we didn't even have to spend much time teaching the agent to just save stuff in variables. It was, like, really natural to it, which probably makes sense because of how notebooks work. Right? So when we asked a follow-up, it was very easy to be like, oh, I already pulled that data.

Let me just do some quick query on it to just change that data up and give you a better answer. And, yeah, it worked really well. And the plotting issue also quickly disappeared. Like, was able to modify its plots and do a lot of really good work there. So yeah. And, like, theoretically, you could do some of this stuff in Bash, but it would have been a big waste of time.

We could tell it to, like, save stuff to files and refer to the files later. But apart from interpreter, it's just so much easier to build up this tooling, and we didn't even have to think too hard about it. It kinda just emerged out of it. So, yeah, this was great and all, and we're doing pretty fast, we still wanted to go faster.

And if you spend hours looking at the traces, which is like a great way to try and really get speed out of these things, you started to notice some patterns. So it might do these queries, and then it would just stumble around, do timed out queries, talk about columns that don't exist. And it was just incredibly frustrating to see it do this stuff and then eventually figure out the answer.

Like, you're coding, you can just go get a coffee, right, and just figure this stuff out. But when you're sitting there waiting to get an answer to trading, you really don't want to watch it do, like, basically just dumb, like, muddling around like this. And it's okay in some cases, but, yeah, like, when when you kinda just know the answer and, you just can put it in the context file, you can beg for it to just follow certain standards. But, of course, as we know, like, agents aren't always just gonna listen to you in the context file, so we wanted to do something better here.

And if you take a step back, like, all we really needed to do was to find a way to detect these timeouts early and just tell it what the schemas are. So another quick lesson that we learned is that all we have to do is to just try do some of these checks before it does the work.

Right? That's a pretty simple thing to do. But I think the breakthrough that we had with just having this one Python interpreter tool is that these things are actually really easy to do. Right? If we had the bash tool with Postgres or we just use the Postgres MCP, like, we have very little control to modify that thing to add all this back pressure.

And what I mean by back pressure is, like, steering it in the right direction before it wastes time doing something. So but when it's a Python tool, it is essentially just code. So you can just like write a function, add some quick check, and then just boom, you just have some very quick back pressure. And you can see we added some quick ones to just say like, oh, if the query is too large, like through some obvious means, then ban the query from being run and ask it to try again. And its only way to find tables was through a find table function so it couldn't waste time like playing around with Postgres information schema queries to try find tables.

And yeah, that worked pretty well to eliminate some of these issues. And we were getting to a better and better state where the agent was making, like, mistakes that I wouldn't pin my head against. But once we eliminated all of those, the mistakes that the agent was trying to make started to become very, very interesting. So we'll move on to the next thing we started seeing the agent do. So, yeah, it might do some task and it'll report that the answer is confidently, like, oh, we have $1,200,000,000,000 of cash delta. And obviously, that is nonsense.

Right? Like, I would love one day if Optiva controlled or just had a position of that much, but that's just not happening anytime soon. So, yeah, like, could go tell the agent, hey, dollars is a ridiculous number. Like, if you see that, just please think about it before telling me. But that is not scalable to, well, every sort of data source we might have. Like, some numbers might literally be one particular link, especially working with, like, nanoposics time or other random stuff.

So, yeah, we needed some sort of solution that was scalable here. But this was starting to become difficult. Like, we can't look at this stuff and then, like, run, like, checks on, like, as easily as we saw in the previous slide to just, see if the data makes sense or not. So yeah, it was very easy to maybe reach to a sub agent or something to say like, hey, can you validate this?

But sub agents, especially here, we really wanted to avoid them because that's just adding time. And having two agents having to wait for them to finish and then putting them together, just going to cost too much time. So yeah, we were assigned to hit some like, we we had a pretty good state, but if we wanted to go faster, we had to start thinking.

Like, how do we scale these kind of checks to a bigger and bigger complexity? So for context, like, we have dozen like, dozens and dozens of calculators and many, many tables. So how can we decide for each of those tables, like, how the best way to query is? And to address this problem, we took some inspiration to, like, how you generally test software.

Right? Like there's some there are many layers you can do it too. You can do like unit tests and you can just run it, of course. But another thing that we take for granted is that compilers and interpreters also do quite a lot of testing of your code. Like they're able to check if your code makes sense in the first place.

They will check for some obvious things, like you're trying to use a variable that isn't used. And if you think about it, like how does it do that? So it does that through actually parsing your code and turning it into something we call an abstract syntax tree. And if we just were able to grab that, maybe we could do some extra checks on top of it.

So the next thing that we were starting to play around with now was essentially playing around with the syntax tree. So we can take the Python code and the query that it's about to run, and we can turn it into this structure. So you can see that it eventually essentially turns into a tree that you can query and, like, look at different parts of it.

And you can see from the SQL part, you can very clearly see that the group clause just doesn't have anything. And it's not a big ask for us to like deterministically go through our tables and just document like what columns make sense to group by if you're doing an aggregate. So by doing that, we could essentially start to look at our queries and decide, Okay, we're about to do some sort of aggregate queries. Probably a good idea to make sure that there's a group by with the columns that matter.

So that was our next level of advanced back pressure. So it was almost like linting. We could link the SQL and just check that, Okay, what are the group by clauses? If they're obviously missing something, don't let the query go. It's just going to give a ridiculous number. And it was also important to us that these checks were not perfect. So sometimes the agent can make the decision in the moment that it knows what it's doing.

So we did always give it like an SK patch. Like if you wanna bypass a check, you can just pass some Boolean and we won't stop you from doing it. Yeah. And we're still experimenting with the Python area. Python can be very, very broad on what it can do. Some interesting ones that we found were that when you give the agent just one tool, it loves to use it for everything, even just noting random crap like it's its scratch pad.

So sometimes you would do that, and then it would just won't tell the users, like, oh, great. I did the answer. And then that's all they would see. They were like, where's the answer? And they would have to, like, go, like, explain it to it to the user again. So we would have simple checks like, oh, if it's just if the code is literally just print statements, which you can very easily check with an AST, then, like, which are probably into the user that you probably wanna tell the user something.

And yeah. So it is quite awesome, like, all the little things you can grab through these trees, and we're still trying to think about more and more ways we could use this to better steer our agents to, like, make the right decision at the right time. And, yeah. And we still wanna go faster, of course, but we're still trying to find, ways to use these, but this is currently where we've landed.

Yeah. So takeaways is, yeah, like, the first lesson that we learned is, if we just give freedom, which is what we started with Cursor, it is able to do some pretty impressive results just by itself. And giving it a scratch pad, so some sort of persistence to save data goes a long way to being able to build up an answer, of like how it does in a Jupyter Notebook.

And making an intuitive universe, I think, is a good way to state the third lesson. Like if you are also just in a Bash terminal or Python interpreter, like, what functions would you want to solve it? Like, if you think about the amount of pain that it has to go, we sometimes just brush it off. But if you think about, oh, it has to write, like, all this extra stuff, you just make it a quick function call, which is very easy when it's just a Python interpreter.

It's just like a quick function. It can go pretty far to making the agent perform a lot faster. And of course, if you think beyond the code and what the code actually is, there's a lot of tools already built out there that's able to take code and do introspections on it. So an example is ASTs. So if you're able to use that and then grab insights out of it, you can do some even more advanced back pressure that you might not think about.

Yeah. So that concludes this talk. If you have any deeper questions, you can, visit us at our booth. Feel free to drop by. We're giving away a Claude Max subscription for whoever can solve our puzzle. That's, like, in the booth there. And, yeah, I'll

Optiver

Optiver logo with a stylized orange triangle.

One Tool to Rule Them All

Building a Fast AI Agent for Real-Time Response

RESPONSE AGENT

df[df.utilisation > 1.0].head(10)
df.groupby("underlying").cash_delta.sum()
breaches = df.query("utilisation > 1")
df.sort_values("max_util_pct").tail()
q95 = df.utilisation_pct.quantile(0.95)
tag = await find_book_tag(feedcode)
inst = pd.read_sql(SQL, equity_db)

An illustration of a simple robot icon. A code editor window displays several lines of Python code snippets.

One Tool to Rule Them All

Building a Fast AI Agent for Real-Time Response

Ryan Samarakoon

RESPONSE AGENT

df[df.utilisation > 1.0].head(10)
df.groupby("underlying").cash_delta.sum()
breaches = df.query("utilisation > 1")
df.sort_values("max_util_pct").tail()
q95 = df.utilisation_pct.quantile(0.95)
tag = await find_book_tag(feedcode)
inst = pd.read_sql(SQL, equity_db)

An illustration of a robot head.

A screenshot of a code editor showing Python code.

A white mouse cursor is visible on the dark blue background.

A day in the life of Risk

Risk are responsible for monitoring and responding to thousands of limits at Optiver

EQ-INDEX

S&P; 500 Cash Delta

45% UTILISATION

EQ-INDEX

EuroStoxx 50 Delta

64% UTILISATION

EQ-INDEX

Nikkei 225 Delta

40% UTILISATION

EQ-INDEX

FTSE 100 Delta

62% UTILISATION

EQ-INDEX

ASX 200 Delta

33% UTILISATION

EQ-INDEX

Hang Seng Delta

72% UTILISATION

EQ-VOL

SPX Vega

47% UTILISATION

EQ-VOL

SX5E Vega

50% UTILISATION

EQ-VOL

NKY Vega

37% UTILISATION

EQ-SINGLE

AAPL Gamma

89% UTILISATION

EQ-SINGLE

NVDA Gamma

85% UTILISATION

EQ-SINGLE

TSLA Gamma

62% UTILISATION

FX

EUR/USD Delta

28% UTILISATION

FX

USD/JPY Delta

45% UTILISATION

FX

GBP/USD Delta

17% UTILISATION

FX

AUD/USD Delta

58% UTILISATION

COMMOD

WTI Crude Delta

44% UTILISATION

COMMOD

Brent Vega

70% UTILISATION

COMMOD

Nat Gas Delta

74% UTILISATION

COMMOD

Gold Delta

32% UTILISATION

RATES

UST 10Y Duration

55% UTILISATION

RATES

Bund 10Y Duration

42% UTILISATION

RATES

JGB 10Y Duration

21% UTILISATION

CREDIT

IG Credit Spread

67% UTILISATION

24 small line graphs displaying time-series data for various financial instruments and risk metrics, each with a corresponding utilization percentage.

A day in the life of Risk

Risk are responsible for monitoring and responding to thousands of limits at Optiver

ENTERING A BREACH

EQ-INDEX S&P; 500 Cash Delta
78% UTILISATION
EQ-INDEX EuroStoxx 50 Delta
78% UTILISATION
EQ-INDEX Nikkei 225 Delta
51% UTILISATION
EQ-INDEX FTSE 100 Delta
48% UTILISATION
EQ-INDEX ASX 200 Delta
38% UTILISATION
EQ-INDEX Hang Seng Delta
83% UTILISATION
EQ-VOL SPX Vega
56% UTILISATION
EQ-VOL SNSE Vega
57% UTILISATION
EQ-VOL NKY Vega
32% UTILISATION
EQ-SINGLE AAPL Gamma
91% UTILISATION
EQ-SINGLE NVDA Gamma
89% UTILISATION
EQ-SINGLE TSLA Gamma
52% UTILISATION
FX EUR/USD Delta
30% UTILISATION
FX USD/JPY Delta
47% UTILISATION
FX GBP/USD Delta
18% UTILISATION
FX AUD/USD Delta
54% UTILISATION
COMMOD WTI Crude Delta
36% UTILISATION
COMMOD Brent Vega
59% UTILISATION
COMMOD Nat Gas Delta
92% UTILISATION
COMMOD Gold Delta
24% UTILISATION
RATES UST 10Y Duration
63% UTILISATION
RATES Bund 10Y Duration
40% UTILISATION
RATES JGB 10Y Duration
25% UTILISATION
CREDIT IG Credit Spread
77% UTILISATION
A dashboard displaying a grid of 24 small line graphs. Each graph represents a different financial instrument or market indicator, showing its trend over time. Below each graph is its current 'UTILISATION' percentage. The graph for S&P 500 Cash Delta shows a prominent red line, and the text "ENTERING A BREACH" is visible at the top left.

ENTERING A BREACH

S&P; 500 Cash Delta

Aggregate net delta exposure

A line chart showing S&P; 500 Cash Delta (aggregate net delta exposure) over time. The y-axis represents monetary values from $0M to $100M, and the x-axis represents time from 09:30 to 16:00. A horizontal dashed orange line is present at the $50M mark. A solid blue line shows delta exposure, starting slightly above $0M at 09:30 and briefly rising to approximately $15M-$20M before the plotted data ends.

Limit: $50M

Breach: 14:35

A line graph plots a value over time, with the y-axis representing monetary value in millions of dollars and the x-axis representing time from 09:30 to 16:00. A dashed horizontal line indicates a limit of $50M. The plotted value starts below $25M, steadily increases, crosses the $50M limit at 14:35, and continues to rise beyond $75M.

BREACH · 14:35 · EQ-IDX

What's behind the breach?

PORTFOLIO BOOK CASH & POSITION UTIL.
FX-G10 SPX-OPT -$20.0M -53,114 66%
FX-G10 ES-MICRO -$17.5M -54,696 35%
VOL-CARRY SPY-ETF +$13.9M +60,318 53%
EQ-IDX BASKET-1 -$9.3M -35,632 29%
FX-G10 BASKET-2 -$19.6M -51,888 32%

What's behind the breach?

PORTFOLIOBOOKCASH ΔPOSITIONUTIL.
FX-G10ES-MICRO-$17.5M-54,69635%
VOL-CARRYSPY-ETF+$13.9M+60,31853%
EQ-IDXBASKET-1-$9.3M-35,63229%
FX-G10BASKET-2-$19.6M-51,88832%
DISPNDX-FUT+$25.1M+28,58448%
EQ-SECTORNDX-OPT-$24.2M-87,99023%
EQ-IDXQQQ-ETF+$11.6M+49,81044%
DISPIWM-ETF-$27.7M-73,82530%
EQ-IDXRUT-FUT-$27.4M-44,75341%
EQ-SECTORNKY-FUT-$24.0M-8,00150%
EQ-IDXNKY-OPT-$20.8M-88,59333%
VOL-CARRYTPX-FUT-$2.0M-66,00632%
RATESHSI-FUT-$18.3M-31,30465%
EQ-IDXHSCEI-FUT-$20.7M-56,1279%
TIMESIDEQTYSYMBOLPXTRADER
14:34:39BUY36WBC AU22.08algo-vol-1
14:34:37BUY277ESU5172.22s.park
14:34:35BUY300HSCEIM58.81algo-vol-2
14:34:29BUY2686758 JT915.91k.tan
14:34:28BUY30005930 KS530.49auto-hedger
14:34:26BUY95HSCEIM515.72m.lee
14:34:20BUY156SPX 5450P23.01m.lee
14:34:13SELL105AS51U5960.40r.huang
14:34:09BUY98NKU5293.97r.huang
14:34:02BUY136ESU5194.66p.singh
14:33:57BUY87SPX 5450P25.93s.park
14:33:55BUY1532330 TT927.95s.park
14:33:48BUY278NQM5221.95j.tanaka
14:33:43SELL269CBA AU11.73auto-hedger
14:33:39BUY477SPX 5450P9.15d.wang
TIMETICKERPXCHG
14:34:47UNH488.42+0.82%
14:34:46MSFT441.82-0.56%
14:34:44DIS95.80+0.86%
14:34:42CVX156.75-0.72%
14:34:38COST855.78+0.69%
14:34:34ORCL142.67-0.24%
14:34:30MSFT440.74+0.66%
14:34:26GOOG180.45-0.83%
14:34:22NVDA132.99-0.37%
14:34:21JNJ153.09+0.79%
14:34:17BAC39.79-0.69%
14:34:13XOM113.37+0.80%
14:34:09TMO567.33-0.30%
14:34:06AAPL224.08+0.40%
14:34:03NFLX504.10-0.19%

Portfolio Overview

CategoryInstrumentP&L (Millions)P&L (Units)Percentage
FX-G10ETH-PERP+$19.0M+68,07355%
VOL-CARRYVIX-FUT+$7.8M+18,39310%
FX-G10VXN-FUT-$18.5M-86,93367%
FX-G

Financial Data Overview

Portfolio Summary

Instrument Related Instrument P&L ($) P&L (Units) Change (%)
EQ-IDX LS-MICRO +$5.9M +7,954 75%
FX-G10 SPY-ETF +$7.9M +84,083 6%
DISP BASKET-1 -$2.5M -39,762 29%

Recent Trades

Time Action Quantity & Instrument Price & Trader
14:30:07 BUY 124 E50S 1691.86 J.chen
14:30:02 BUY 27 HSIMS 550.89 s.park
14:29:58 SELL 210 NKU5 679.49 algo-vol-2

Market Quotes

Time Symbol Price Change (%)
14:31:19 WMT 70.24 -0.41%
14:31:18 GOOGL 179.27 +0.52%

THE PROBLEM

Time costs money.

Breaches need to be solved in minutes — or we risk trading outside our appetite and being forced to scale down.

THE PROBLEM

Time costs money.

Breaches need to be solved in minutes – or we risk trading outside our appetite and being forced to scale down.

money.

Breaches need to be solved in minutes — or we risk trading outside our appetite and being forced to scale down.

Humans can take 15–20 minutes to find the root cause. How fast can an agent do it?

APPROACH 1 BASH

Lesson 1: Bots do well with generic tools

Hand the agent psql and a shell — it works the breach the same way you would.

bash • session

Investigate this breach detailed in breach.txt. You can explore the data with psql. Login details are in /pg/secrets using your bash tool.

AGENT

  • > Bash
An orange robot icon with a square head and visible gears. Three small colored circles (red, yellow, green) are shown, resembling traffic light controls for a terminal window.

APPROACH 1 BASH

Lesson 1: Bots do well with generic tools

Hand the agent psql and a shell — it works the breach the same way you would.

bash session

Investigate this breach detailed in breach.txt. You can explore the data with psql (login details are in ~/.pgpass) using your bash tool.

AGENT

Bash

An icon of an orange robot head is shown with the label "AGENT" below it, and "Bash" listed as an option under AGENT.
  • The breach happened because auto-hedger booked +$32.1M of SPX-FUT delta into the MACRO-DELTA portfolio between 13:00 and 13:42, chasing the ESM5 rally.
  • Who else has been trading in this portfolio?
$ psql -c "SELECT trader, COUNT(*), SUM(ABS(delta)) FROM trades WHERE book='MACRO-DELTA' AND ts::date = today GROUP BY trader ORDER BY 3 DESC"

auto-hedger 412 . k.tan 188 . algo-vol-1 73 . m.lee 41 . j.park 18

  • Today MACRO-DELTA was traded by auto-hedger (412), k.tan (188), algo-vol-1 (73), m.lee (41) and j.park (18).

APPROACH 2 · PYTHON

Lesson 2: Bots do better if they have a sandbox with persistency

Load once into a dataframe – every follow-up answers from memory.

python · session

Lesson 2: Bots do better if they have a sandbox with persistency

Load once into a dataframe – every follow-up answers from memory.

python - session

Investigate this breach detailed in breach.txt. You have a Python sandbox with pandas and a connection to the same data – go.

>>> df = pd.read_sql("SELECT * FROM breach_window WHERE date=today", conn)

loaded 2.4M rows • positions >< trades >< prices

An icon of a person's silhouette with a question mark is displayed next to an instruction.

A red robot head icon labeled "AGENT" with "Python" below it is in the bottom right corner.

>>> df[df.book=="MACRO-DELTA"].groupby("trader").size().sort_values(ascending=False)
auto-hedger 412
k.tan 188
algo-vol-1 73
m.lee 41
j.park 18

Today MACRO-DELTA was traded by auto-hedger (412), k.tan (188), algo-vol-1 (73), m.lee (41) and j.park (18) – answered straight from the loaded dataframe.

> python - session
df = pd.read_sql("SELECT * FROM trades WHERE book='SPX-FUT'", conn)
A screenshot of a terminal window displaying an interactive Python session, with a command using Pandas to read SQL data into a DataFrame.
> python · session
>>> pd.read_sql("SELECT * FROM trades WHERE book='SPX-FUT'", conn)
Screenshot of a Python interactive session showing a pandas read_sql query.
>> pd.read_sql("SELECT * FROM trades WHERE book='SPX-FUT'", conn)
... still running ...
Screenshot of a Python interactive session environment, labeled "python session," displaying a pandas read_sql query that is still running.

UndefinedColumn: column "qty" does not exist

LINE 1: SELECT trader, SUM(qty) FROM trades WHERE trade_date = '2026...

Sorry – those calls were a mess. Let me check column names against the schema and add date filters before I hit the database again.

Lesson 3: Backpressure is a lot easier in a Python interpreter

Tiny wrappers turn schema discovery and runaway queries into things the agent can act on.

TOOL DEFINITIONS

def query_db(sql: str, max_rows: int = 200_000) -> pd.DataFrame:
    n = fast_estimate_returned_rows(sql)
    if n > max_rows:
        raise QueryTooLarge(
            f"would return {n:,} rows (limit {max_rows:,})."
            "Filter the query to a smaller time period."
        )
    return pd.read_sql(sql, conn)
def find_table(name: str) -> None:
    closest_match = find_closest_match_table(name)
    if closest_match is None:
        raise NoMatch(f"no table was found which matches {name!r}.")
    print(schema(closest_match))
    print(table_stats(closest_match))

Lesson 3: Backpressure is a lot easier in a Python interpreter

Tiny wrappers turn schema discovery and runaway queries into things the agent can act on.

</> TOOL DEFINITIONS

# tools/db.py
def query_db(sql: str, max_rows: int = 200_000) -> pd.DataFrame:
    n = fast_estimate_returned_rows(sql)
    if n > max_rows:
        raise QueryTooLarge(
            f"would return {n:,} rows (limit {max_rows:,})."
            " Filter the query to a smaller time period."
        )
    return pd.read_sql(sql, conn)

def find_table(name: str) -> None:
    closest_match = find_closest_match_table(name)
    if closest_match is None:
        raise NoMatch(f"no table was found which matches {name!r}.")
    print(schema(closest_match))
    print(table_stats(closest_match))

Lesson 3: Backpressure is a lot easier in a Python interpreter

Tiny wrappers turn schema discovery and runaway queries into things the agent can act on.

TOOL DEFINITIONS

# tools/db.py
def query_db(sql: str, max_rows: int = 200_000) -> pd.DataFrame:
    n = fast_estimate_returned_rows(sql)
    if n > max_rows:
        raise QueryTooLarge(
            f"would return {n:,} rows (limit {max_rows:,}))"
            " Filter the query to a smaller time period."
        )
    return pd.read_sql(sql, conn)
def find_table(name: str) -> None:
    closest_match = find_closest_match_table(name)
    if closest_match is None:
        raise NoMatch(f"no table was found which matches {name!r}.")
    print(schema(closest_match))
    print(table_stats(closest_match))

AGENT - PYTHON

>>> find_table("trades")
✓ schema(trades) ‣ trade_date:date · book:text · trader:text · ticker:text · quantity:int · px:float · delta:float
table_stats(trades) ‣ 142,318 rows · trade_date 2025-12-01 – 2026-06-03
print(table_stats(closest_match))
A white mouse cursor is visible in the center of the screen.

> python · session

>>> df = query_db("SELECT trader, ts, delta FROM trades WHERE date = today")

_> python · session

>>> df = query_db("SELECT trader, ts, delta FROM trades WHERE date = today")
Screenshot of a console or code editor displaying a Python command.

python + session

>>> df = query_db("SELECT trader, ts, delta FROM trades WHERE date = today")

142,518 rows - within budget

A white mouse cursor is visible in the center of the screen.

THE NEXT PROBLEM

How do you scale to thousands of investigation scenarios, all with their own small nuances?

THE NEXT PROBLEM

How do you scale to thousands of investigation scenarios, all with their own small nuances?

Of course, listing out all possible mistakes to our agent won't cut it.

investigation scenarios, all with their own small nuances?

Of course, listing out all possible mistakes to our agent won't cut it.

A white mouse cursor is visible in the center of the screen.

LESSON 4When all your tools are code, you can leverage the dark arts

Every query and every Python call goes through a parser first – the Abstract Syntax Tree exposes the structure so we can read, lint, or rewrite what the agent typed before it ever runs.

PYTHON AST

total = df.delta.sum()
  • Module
    • Assign
      • Name 'total'
      • Call
        • Attribute 'sum'
          • Attribute 'delta'
            • Name 'df'

SQL AST

SELECT trader, SUM(delta) FROM trades WHERE date = today;
  • SelectStmt
    • targetList
      • ColumnRef 'trader'
      • FuncCall 'sum'
        • ColumnRef 'delta'
    • fromClause
      • RangeVar 'trades'
    • whereClause
      • A_Expr '='
        • ColumnRef 'date'
        • ColumnRef 'today'
    • groupClause [...]

LESSON 4

When all your tools are code, you can leverage the dark arts

Every query and every Python call goes through a parser first – the Abstract Syntax Tree exposes the structure so we can read, lint, or rewrite what the agent typed before it ever runs.

PYHTON AST

total = df.delta.sum()

SQL AST

SELECT trader, SUM(delta) FROM trades WHERE date = today;

A diagram showing the Abstract Syntax Tree for the Python code `total = df.delta.sum()`. The tree branches from 'Module' to 'Assign' (Name 'total'), then to 'Call' (Attribute 'sum', Attribute 'delta', Name 'df').

A diagram showing the Abstract Syntax Tree for the SQL query `SELECT trader, SUM(delta) FROM trades WHERE date = today;`. The tree branches from 'SelectStmt' to 'targetList' (ColumnRef 'trader', FuncCall 'sum' with ColumnRef 'delta'), 'fromClause' (RangeVar 'trades'), 'whereClause' (A_Expr '=' with ColumnRef 'date' and ColumnRef 'today'), and an empty 'groupClause'.

groupClause [] - BUG

LESSON 4 • IN ACTION

Linting tools before they're called

The same parser the database uses, run on every query and every snippet — with table metadata it spots double-counted aggregates and nudges the agent away from print-only answers.

// LINTER DEFINITIONS

// STORES / METADATA


import ast, pglast

def lint_sql(sql) -> Issue | None:
    tree = pglast.parse_sql(sql)[0].stmt
    if has_agg(tree) and (load := first_table_missing_groupby(tree)):
        return Issue(
            f"{load.table} snapshotted by {{bad.snapshot_key's}}",
            ...
        )
  

LESSON 4 IN ACTION

Linting tools before they're called

The same parser the database uses, run on every query and every snippet — with table metadata it spots double-counted aggregates and nudges the agent away from print-only answers.

LINTER DEFINITIONS

# tools/lints.py
import ast, pglast

def lint_sql(sql) -> Issue | None:
	tree = pglast.parse_sql(sql)[0].stmt
	if has_agg(tree) and (not first_table_missing_groupby(tree)):
		return Issue(
			f"{"bad.table"} snapshotted by {"bad.snapshot_keys"};"
			"aggregating without grouping double-counts rows."
		)
def lint_python(src) -> Hint | None:
	body = ast.parse(src).body
	if body and all(is_print(s) for s in body):
		return Hint("Code only prints - return the value so the user sees it.")
def lint_dataframes(src) -> list[Issue]:
	issues = []
	for node in ast.walk(ast.parse(src)):
		if not isinstance(node, ast.Call):
			continue
		df = dataframe_passed_to(node)       # A None if not a known df
		if df and df.row_count > 10_000:
			issues.append(Issue(
				f"{ast.unparse(node.func)}({df.name}) — {df.name} has"
				f"'{df.row_count}' rows. Aggregate or sample first."
			))

AGENT PYTHON

Linting tools before they're called

The same parser the database uses, run on every query and every snippet — with table metadata it spots double-counted aggregates and nudges the agent away from print-only answers.

LINTER DEFINITIONS

# tools/lints.py
import ast, pglast

def lint_sql(sql) -> Issue | None:
    tree = pglast.parse_sql(sql)[0].stmt
    if has_agg(tree) and (bad := first_table_missing_groupby(tree)):
        return Issue(
            f"{bad.table} snapshotted by {{bad.snapshot_keys}}."
            " aggregating without grouping double-counts rows."
        )
def lint_python(src) -> Hint | None:
    body = ast.parse(src).body
    if body and all(is_print(s) for s in body):
        return Hint("Code only prints - return the value so the user sees it.")
def lint_dataframes(src) -> list[Issue]:
    issues = []
    for node in ast.walk(ast.parse(src)):
        if not isinstance(node, ast.Call):
            continue
        df = dataframe_passed_to_node             # None if not a known df
        if df and df.row_count > 10_000:
            issues.append(Issue(
                f"{ast.unparse(node.func)}({df.name}) -- {df.name} has "
                f"{df.row_count:,} rows. Aggregate or sample first."
            ))
    return issues

AGENT PYTHON

>>> query_db("SELECT trader, SUM(delta) FROM trades WHERE date=today")

[A] lint_sql: trades snapshotted by {ts}; aggregating without grouping double-counts rows.

df = dataframe_passed_to_node() # none if not a known df
if df and df.row_count > 10_000:
    issues.append(Issue(
        f"{ast.unparse(node.func)}({df.name}) - {df.name} has "
        f"{df.row_count:,} rows. Aggregate or sample first."
    ))
return issues

WRAP-UP

Takeaways

  • Freedom does wonders LESSON 1

    Hand the agent the same generic tools you reach for – psql, a shell, pandas – and it works the breach the same way you would. The less bespoke the toolbox, the further it can take you.

  • Give it a scratchpad LESSON 2

    A Python sandbox that holds a dataframe between turns means every follow-up answers from memory instead of doing another round-trip to the database — fast, cheap, and the agent stays in flow.

  • Make your universe intuitive LESSON 3

    Tiny wrappers – query_db with backpressure, find_table for schema discovery – turn runaway queries and missing context into polite errors the agent can act on instead of timeouts and apologies.

  • Think beyond the code LESSON 4

    When everything is just a call to agent.query(), or agent.execute(), what gets stored in the query context and shown to the user? How can this reveal intent and provide transparency?

When everything the agent touches is code, parse it before it runs. ASTs let us catch double-counted aggregates, oversized dataframes, or print-only answers - class-level mistakes one rule fixes forever.

Technologies & Tools

  • Bash
  • Claude Code
  • Cursor
  • Databricks
  • Jupyter Notebook
  • MCP
  • PostgreSQL
  • Python
  • SQL
  • TypeScript

Concepts & Methods

  • Abstract Syntax Tree
  • SQL linting
  • Unit testing

Organisations & Products

  • Claude Max
  • Cloudflare
  • Confluence
  • Jira