Designing software systems for spatial agents
An agent answering spatial questions cannot call a simulator mid-conversation. The storage layer has to change shape first, and that turns one system into two.
.png)
An agent that answers questions about a spatial domain looks like a language problem and is mostly a storage problem. The model's job is to pick tools and phrase results. Everything that determines whether the system works at all happens underneath it, and most of it has to be decided before a single prompt is written.
The constraint
The domain here is forest biomass procurement. A planner asks where to site a facility, and answering means combining harvest cost, wildfire risk and transport distance across thousands of candidate locations.
There was already a system that did this. It ran the cost simulator directly, routed every origin-destination pair through a routing engine, and produced an exact answer in minutes to hours. That is a reasonable design for an analyst who submits a scenario and comes back later.
An agent cannot use it. Not because the numbers are wrong, but because the interaction model is incompatible in a way no amount of engineering around the edges fixes.
A single facility analysis needs tens of thousands of sequential simulator calls. A twenty-year projection needs an order of magnitude more. Every origin-destination distance is a routing call at 50 to 200 ms. A conversation has a budget of seconds and a user who will ask a follow-up question that changes one parameter.
So the first architectural decision is not about the agent. It is that the computation has to move from query time to build time, and once it does, the resulting system is different enough from the original that keeping both is easier than merging them.
What the agent needs from storage
Working backwards from the interaction gives three requirements, and they are more specific than "make it fast".
Answers must be reads, not runs. Anything the agent needs has to already exist as a row. That means precomputing the full cross product: every cluster, every treatment, every harvesting system. 165 million predictions, generated once, stored as a materialised view.
Everything read together must be stored together. A siting decision needs cost, fire risk and transport factor at the same location. Three separate lookups is three round trips and three chances to misalign. Cost and burn probability share one table keyed on cluster, so the joint read is a single bounded spatial query.
Coarse answers need their own storage. A question with no location in it cannot use a spatial filter. Aggregating 11.9 million rows on demand takes seconds, so the aggregates are precomputed too, at three levels of granularity, in their own tables.
The pattern underneath all three: decide the query shapes first, then design storage for exactly those shapes. This is the opposite of normalising and letting the planner work it out, and it is the right call only because the query shapes are known and few. Nine tools, each with one access pattern. If the system had to support arbitrary queries, none of this would apply.

The tiers, and what each is allowed to do
Tier 1, PostGIS. Three prediction surfaces with GiST spatial indexes. A cluster table holding cost and fire jointly, a transport factor lookup, and hex aggregates at three resolutions. This tier has no logic in it. It stores and it indexes.
Tier 2, orchestration. A service exposing a small number of endpoints, running the agent loop, holding session state, and caching. The nine tools live here. Four read from Tier 1, four compute on results already read, one calls a geocoder. Results are cached with lifetimes matched to how fast the underlying thing changes: 24 hours for demand, one hour for regional summaries, 30 minutes for cluster sets.
Tier 3, interface. Chat, map, and a summary panel. Two entry points, a map pin and a text box, both landing in the same pipeline. Tool progress streams as it happens rather than the user watching a spinner for twenty seconds.
The rule that keeps this maintainable is that the model never touches Tier 1. No generated SQL, no dynamic query construction. Every path to the database goes through a tool with a fixed query and typed arguments. That forecloses a class of failures entirely: no injection, no accidental full table scan, no query the DBA has never seen. It also caps what the system can do, which is the trade.
Two access patterns, two systems
The original simulation system was not replaced. Both run.
One performs live simulation for domain experts who need cluster-level precision and are willing to wait. The other queries precomputed views for planners who need an answer now and do not have forestry expertise. Same underlying data, opposite ends of the accuracy-latency curve.

Merging them was the obvious alternative and it is worse. A single system with a fast mode and a slow mode has to reconcile two data access patterns, two latency budgets and two error tolerances inside one codebase, and every feature has to be built twice anyway. Keeping them separate means each can be simple.
The dependency runs one way and is worth being clear about. The simulation system is the source of truth. It generated the training labels for the surrogate, it provides the ground truth the conversational system is validated against, and if its methods change, the prediction layers are stale. The fast system is a cached, approximated projection of the slow one, and it does not know when it has gone out of date.
What this costs
Precomputation forecloses parameters. Every prediction was generated under fixed economic assumptions. A parameter not baked in cannot be varied at query time. There is a way around this when the underlying function separates cleanly, but it is a property of the domain rather than something the architecture provides.
Two systems drift. No automated check confirms the prediction layers still match what the simulator would produce today. Consistency is maintained by regenerating them and by remembering to.
Aggregates go stale. Rebuilding the hex tables from 11.9 million rows takes minutes. Any change to the underlying layers leaves them briefly wrong, with nothing surfacing that.
The tool surface is the capability ceiling. Nine fixed tools means nine things the system can do. A question outside them cannot be answered by improvising, only by shipping a tenth tool. That is a deliberate choice and it will feel restrictive to anyone expecting an agent to be open-ended.
Cache invalidation is a hand-maintained rule. The agent reuses a cached cluster set when a parameter changes but the location does not. That rule is correct for the parameters currently exposed. Add one that silently affects retrieval and it fails quietly.
More on what the system can and cannot answer at /docs/scope and /docs/capabilities-overview.
References
PostGIS Development Team. PostGIS 3.4 Manual.
PostgreSQL Global Development Group. Materialized Views.
Yao, S. et al. (2023). ReAct: Synergizing Reasoning and Acting in Language Models. ICLR.
Redis Ltd. Redis.