Blog · Engineering

I don't call the agent for decisions. I call it for sentences.

By Michael Hairetis

I run two platforms on the same piece of infrastructure.

The first is an orchestration platform that runs autonomous data operations on a schedule. I wrote about the service underneath it in the previous post: a thin harness that puts an HTTP endpoint in front of a real coding agent, so anything upstream can ask for judgment by name and get an agent's answer back. Give it a role and a prompt, get that role's answer. That is the whole contract.

The second platform is a financial intelligence system that publishes a daily brief. It is the reason I now think the interesting question is not what can an agent decide. It is what happens when the agent's output is the product.

Those are very different bars, and I only found out by crossing one.

An orchestrator can be wrong. A publication cannot.

On the orchestration side, when an agent classifies a failure incorrectly the cost is a wasted retry. The system notices, tries something else, and nobody outside ever knows. The judgment is internal: load-bearing for the machinery, invisible to the world.

On the publishing side there is no such cushion. The agent writes a paragraph, the paragraph goes into a brief, the brief goes to readers. There is an operator in front of it, me, but I am reviewing prose rather than auditing a decision tree. A wrong classification announces itself. A plausible, well-written, subtly wrong sentence does not.

Same infrastructure. Completely different failure mode.

To date the platform holds 1,662 economic releases and 183 transcripts, and has published 55 briefs. Every release is read by three separate agent roles, which comes to roughly 5,100 analysis calls, completing at 97 to 99 percent depending on the role. Eleven roles are registered; eight stay warm.

Five of the eight resident roles write prose

Eight roles stay resident because they are on the critical path. Three wake on demand, do one job, and go away again. The split is purely about latency: a resident role is already primed and answers immediately, while one that wakes for a single job pays a few seconds of startup nobody is waiting on.

The eight warm roles: an author that writes the daily brief end to end and carries the byline; an editor whose only mandate is would a normal person understand this; a standing macro assessment so each day's events land in a context rather than in isolation; a tape analyst that turns market microstructure into prose a reader can follow; an operations manager that decides whether a failed collection job deserves a retry, a different approach, or my attention; and three readers of every incoming release.

That distribution is the whole point. Five of the eight exist to write or edit reader-facing prose, and exactly one is doing the internal decision-making that the orchestration platform uses this infrastructure for almost exclusively. Same endpoint. The shape of what runs on it is upside down.

Three agents read the same document and must not agree

This is the use I did not anticipate and now consider the main event.

When a release lands, a bank credit report, a Treasury auction result, a Fed speech, it does not get analysed once. It goes to three roles pointed at the identical source with different jobs.

That is not three prompts on one model. Each is a role with its own standing definition, and each stays resident so it accumulates context across everything it has read this week. The private analyst knows what it concluded on Tuesday. The writer knows the story it has been telling.

Build it as a single call and you get mush: one paragraph hedging between two audiences and useful to neither. The reason to have an agent behind the endpoint rather than a model is precisely that it can hold a role.

They read the primary sources themselves

Here is the part that deleted the most code.

729 of those releases carry a primary source document, a regulator's PDF, a scanned table, an agency report, with another 111 files attached on top. We do not extract them. No PDF text pipeline, no OCR step, no table parser, no per-format handler. The event record hands the agent the file paths and says: read these before you assess.

And it does. It opens the actual quarterly banking report and cites figures out of the tables.

I had written the extraction layer. I deleted it. Every format I would have had to handle specially, multi-column tables, scans, a chart with the number only in the axis label, is now just a file the agent opens. That is not a small saving. It removes the whole category of maintenance where a source changes its layout and your parser silently returns the wrong column.

Because the platform runs on a flat subscription rather than per-token billing, I also stopped truncating. There was a 4,000-character cap on how much of a release's payload got passed through. I removed it. The only thing that cap was ever protecting was a bill I am not being sent.

Every editorial bug I shipped was an agent following orders

This is the lesson, and it took a month of small embarrassments to see.

I kept finding defects in published prose. Internal shorthand in a reader-facing card. Database row identifiers in a published sentence, literally events/1042 in a paragraph a subscriber could read. Terms of art used with no gloss. A section quietly editorialising instead of reporting the day.

Every single time my first instinct was that the agent had drifted, misunderstood, or needed a firmer instruction. Every single time I was wrong.

The agents were doing exactly what their role definitions told them, to the letter. The problem was that each definition had been written for an internal audience and then quietly promoted to a reader-facing one.

The clearest case: the public writer had an instruction to faithfully preserve the source's framing. Perfectly reasonable. But the sources are Treasury and Federal Reserve releases, and they use the internal vocabulary, so the agent was being obedient when it passed that vocabulary straight through to readers. It was not drifting. It was following an order that had become wrong the moment its output started being published.

Another role had an explicit invariant to cite records by identifier rather than by name, because that was precise and useful when its only reader was me. When its output was later routed into a published summary, that same invariant produced events/1042 in the prose. The instruction never changed. The audience did.

So the fix is never "tell it to try harder." The fix is to work out who the role thinks it is writing for, and notice when that is no longer who reads it. I now treat every role definition as an editorial brief with a named audience, and when a role's output changes destination the definition gets rewritten rather than patched.

The failure mode nobody warns you about is not the agent going off-script. It is the agent following a script you wrote for a different reader and forgot to update.

Failure needed a taxonomy, not a retry

The other thing publishing forced on me: agent failures are not one thing, and treating them as one thing is how you ship a half-finished sentence.

Calls go through a queue that records how something failed, not just that it did. Across roughly 5,100 calls the failures sort into a handful of kinds: the service was unreachable, the answer came back in a shape we could not parse, the response was truncated, the output arrived malformed. About a hundred failures in total, and the categories matter far more than the count.

Unreachable is transient. Retry it.

But an output we cannot parse, or one that arrived partial, does not get retried automatically, and that is deliberate. In a publishing context a partial answer is worse than no answer, because a missing analysis is visibly missing, while a truncated one looks complete. It sits there reading like a finished thought with its conclusion silently amputated. Those go in a queue for a human.

The one that taught me the most produced genuinely good prose and simply omitted the label the parser needed. The agent had not failed at the work. It had failed at the handshake. Had I treated that as "the agent is unreliable" I would have drawn exactly the wrong conclusion and started rewriting the role. The work was fine. The contract between us was underspecified.

What it actually means

Two platforms, one endpoint, and the second taught me something the first could not.

Wrapping an agent so code can call it is not only a way to replace the functions that were always going to be wrong, the if chains and the regexes and the lookup tables. That is the version I understood first, and it is real. But you can also put an agent where a person used to sit, and the constraint that shows up then is not technical. It is editorial.

A daily publication needs someone who reads primary sources, holds a house voice, remembers what it said yesterday, and writes for a specific reader. That is a job description. It turns out you can fill it with a role definition and an HTTP call, and the moment you do, the hard part stops being the infrastructure and becomes the thing every editor has always had to get right: knowing who you are writing for.

This is the engine behind our builds. The architecture described here is what runs every OpenRed client automation — see the services page for what it does in plain terms, or book a free 30-minute call.

More from the OpenRed blog