The Bajoran Engineer

Back to the bench: updating the flower shop agents

Earlier this week I wrote about wearing two hats at AGNTCon + MCPCon and ended with a list of things I wanted to try in my flower shop demo. Then I went home and built. This is what happened, including the parts that didn't go the way I said they would.

Back to the bench header

These are my notes as a builder. They aren't positions of the Agentic AI Foundation.

Earlier this week I wrote about wearing two hats at AGNTCon + MCPCon and ended with a list of things I wanted to try in my flower shop demo. Then I went home and built. This is what happened, including the parts that didn't go the way I said they would.

The flower shop agents are my reference project for Wildflower Software clients: a coordinator, four narrow workers (orders, inventory, marketing and books), and an approval gate that stops anything risky until the owner says yes. The first versions were deliberately simple, so the pattern was easy to read. The plan was always to move the demo onto the stack I use for client work once the pattern was clear. This round I did that, and kept how the agents behave the same.

#Moving the demo onto PydanticAI

The demo started with a short, hand-written loop that called the Anthropic API directly. It sent a task to the model, ran the tools the model asked for, and went around again. That made the pattern easy to see in a reference project, but it isn't how I build for clients. My agents run on PydanticAI, so the demo now does too.

Two things stay in the demo's own code: which tools each worker can see, and which actions wait for the owner. Those are the rules that make the system safe to use, and they shouldn't depend on a framework setting. In PydanticAI an agent gets its tools from a toolset, and you can wrap toolsets inside each other. So the allow-list became a filter on the MCP toolset, and the approval gate became a small wrapper of my own around that. A risky call gets queued and never reaches the server.

The shop's tools didn't move. They're still a FastMCP server, and PydanticAI talks to it through FastMCP's own client, so the demo's code for translating tool definitions went away too.

The tests moved too. They used to fake the Anthropic API. Now they use PydanticAI's FunctionModel, a scripted fake that answers however I tell it to, and real model calls are blocked. Every guarantee from before still passes: read-only tools run immediately, risky ones get queued, a rejected action never runs, and a server can't mark its own tool as safe.

#Every run goes through Prefect now

I moved my agent orchestration to Prefect last month, and the flower shop demo now runs the same way. PydanticAI and Prefect connect through something PydanticAI calls PrefectDurability. Inside a flow, every model call and every MCP tool call becomes its own Prefect task with retries. When a run fails, I can open it and see exactly which step broke.

One detail worth knowing if you try this: when I tried to keep the MCP calls out of Prefect tasks, PydanticAI refused. Once durability is on, it won't run an MCP call outside a task. That turned out to fit my gate nicely. Approved calls run as tasks, and queued calls never become tasks at all, because they never reach the server.

The honest gap is the wait for a person. A queued approval sits in memory until the owner answers at a terminal. If the process stops first, the queue is gone, though the audit log still has the record. So yes, the steps are durable. The approvals aren't, yet.

#The first agent that runs on its own

In my always-on agents post I said turning my agents on would mostly be a change to what starts a run. I got to test that.

Every Monday morning, an agent writes the shop owner a short note on how sales are going. I used the rule that made my LCARS demo work: let the model decide what to say, and let code do the math. A script compares the last four weeks with the four before and the same weeks last year, and flags days that look unusual for that day of the week. The model writes the note from that output. Then code checks the note before it's saved: every number has to come from the script, placeholder data has to say it's placeholder data, and it can ask the owner at most one question. The note is saved as a draft. Nothing gets sent.

If the model invents a number, the note goes back with the reason, and if it keeps failing, the run fails. I'd much rather see a failed run in Prefect than send the owner a text with a number nobody can trace.

Deploying it surprised me. I planned to use a Render cron job, since it runs once a week for a couple of minutes. But Render cron jobs can't attach a disk, so they can't reach the SQLite file. It ended up on a small background worker with a disk, waiting for the schedule. Small detail, but it's exactly the kind of thing that decides what a client pays every month.

#The whole team on my laptop

In the two-hats post I said one agent would get a small local model. What I actually built is a --local flag that runs the whole team on one. llama.cpp's llama-server accepts the same API format as Anthropic's, so PydanticAI just needs a different address.

I ran it with Qwen3.5 9B, the same model I use in LCARS, and asked which stems need to sell first and whether to place a wholesale order. The inventory worker called the shop's real tools, the coordinator wrote the answer, and it took about four minutes with no API key.

It also told the owner a "priority sale flag" was waiting for her approval. That flag didn't exist. Nothing had been queued, because the model never called a tool that could queue anything. Small models make things up like this on multi-step work, and this is why the rules live in code. A model can describe something wrong. It can't make an unapproved action happen. Picking which single agent should get the small model is still on my list, and now testing candidates is one flag away.

#What I'm (not) taking from the new MCP spec

I also said I'd try features from the new MCP spec. I haven't yet, but I did decide which ones belong in this demo, and one of them isn't where I expected.

Trace context, yes, and soon. The new spec lets a traceparent travel with each request. FastMCP already reads and writes it whenever OpenTelemetry is running, and PydanticAI already traces its runs. So when I turn on OpenTelemetry, I should get one trace from the owner's question through the agents and into the tool call on the MCP server, with very little new code. I'll find out.

Stateless HTTP, later. Today the tools run in the same process as the agents, so I don't need it. When the shop's texting assistant arrives, two services will need the same tools, and a stateless server will make that easy to host.

Tasks, but not for approvals. My first thought was that MCP Tasks would fix my disappearing approvals. Then I thought about where the approval would live. A pending approval as a Task means the server holds it, and the whole design of this demo is that the server describes tools and my code decides what's safe. So approvals will stay on my side, stored in a database, with a Prefect run that can wait as long as the owner needs. Tasks will come in when I have a tool that's genuinely slow.

#Some context from my day job

I work on open source technical programs at AAIF, so I couldn't help noticing how much of this lines up with work happening there. The Workflows & Process Integration working group voted in a human-approval reference architecture in September, where an agent proposes, a person approves and a controlled executor acts. That's the flower shop's shape, and reading it after building mine gave me better words for the pieces. The repo also has an AGENTS.md, and the sales note is an Agent Skill, both AAIF projects. And the trace work I'm about to do crosses the same agent-to-tool boundary the Observability & Traceability working group is studying.

None of that drove the build. I built what the shop needed. But having a front row seat means I know which problems are open, and that's useful when I'm deciding what to build next.

#What's next

  • OpenTelemetry traces from the owner's question all the way into the MCP server
  • Approvals that survive a restart
  • Evals for each worker
  • Running the agents in a sandbox

The code is in wildflower-software/flower-shop-agents, and the engineering details are on the Wildflower Software blog in What We Handed to a Framework, and What We Kept. I'll write up what works and what breaks.

About 2 bottles of water (โ‰ˆ1.1 L)

Estimated water used by the AI tools that helped research and draft this post, counted in 500 mL bottles. AI providers don't publish per-chat figures, so this sits between two public benchmarks.

How it's calculated: ~300 prompt-equivalents ร— โˆš(0.26 mL ร— 50 mL) = 300 ร— 3.6 mL โ‰ˆ 1.1 L โ‰ˆ 2 bottles

  • 0.26 mL: Google's figure for a median Gemini text prompt.
  • 50 mL: Mistral's figure for a ~400-token Le Chat response.
  • A geometric mean weighs the two evenly, even though they're about 200ร— apart.

Both figures are self-reported, exclude model training, and count evaporative cooling as water consumed.

Further reading: - How much power and water does AI use? (PCWorld) - Google games numbers to make AI look less thirsty (The Register)

If this post helped, you can support my open source work on Ko-fi.