Agentic Hub

How it works

Six stages, and what each one stops from going wrong

The model call is the easy part. Everything below exists because a model can hand you something that looks right and isn't, and something has to catch it. Every number on this page comes from the code it describes.

ticket
  │
  ├─ 1  intake ............ Zod-validated at the boundary
  │
  ├─ 2  requirements ...... every citation checked against the ticket's own words
  │
  ├─ 3  planning .......... steps cite the requirement ids they satisfy
  │
  ├─ 4  checks ............ objective, free, run before any judge
  │        └── fail ──► back to 3 (bounded)
  │
  ├─ 5  evaluation ........ rubric judge, evidence required per score
  │        └── below threshold ──► back to 3 (bounded)
  │
  └─ 6  approval .......... stops for a human
           └── rejected ──► back to 3, with their note as the instruction

What kind of system this is

A workflow, not an agent. It comes down to one question. Who decides what happens next, your code or the model?

The control flow is TypeScript. The model has no tools, doesn't pick what happens next, and doesn't decide when it's finished. It gets asked three questions, and I check every answer before sending the next one.

Each call starts fresh. Planning only knows about the requirements because my code puts them in the prompt. Nothing is remembered between calls.

This systemAn agent
Who sequences the workYour code, decided before the run startsThe model, decided during the run
ToolsNone. It returns text and that's it.Calls them whenever it wants: read, write, search, execute
TerminationThe state machine hits an end stateThe model decides it's done
What the trace looks likeA state machine you could draw before it runsA transcript you read afterward to find out what it did
Where an agent would come in. Writing the code. You can't plan those steps ahead of time. You don't know which files need reading, how many edits it will take, or what a failing test actually means, and the job is only finished once the tests pass, which it has to find out by trying. So the split would be a workflow up to approval and an agent after it, with the human gate in between. That's the last cheap moment before something slow and unpredictable starts. That half isn't built, and the run report says so.

The stages

Each one covers what it produces, what it stops from going wrong, what I can actually show for it, and the code behind it.

01

Ticket intake

Checks the ticket against a schema before anything else runs.

Why it works this way. Bad input should fail here instead of two model calls later. Acceptance criteria are optional on purpose, since a ticket without them is incomplete rather than invalid, and pointing that out is a later stage's job.

Rejects a ticket with an unknown priority, a missing title, or a non-object body.
The API uses the same schema as the form, so the two can't disagree about what a valid ticket is.
02

Requirement extraction

Turns the ticket text into explicit requirements, implied ones, open questions, and what's missing.

Why it works this way. A schema only checks the shape of the response. It can't tell you whether the content is true. A model can return perfectly valid JSON that cites a requirement the ticket never contained. So every explicit requirement has to include a quote, and I check that the quote really appears in the ticket. I ignore whitespace and capitalization, so a quote that runs across a line break still passes, but a reworded one gets rejected. Constrained decoding wouldn't help here, because the JSON was already valid.

Made-up quotes and reworded ones both get rejected in tests.
Implied requirements aren't allowed to cite a quote. If the ticket says it, it's explicit.
03

Implementation planning

Produces an approach, ordered steps, a test strategy, risks, and what's out of scope.

Why it works this way. Each step has to list the requirement ids it covers, which lets me check coverage in normal code instead of asking a model. It does open up a way to cheat, since a model could make up ids, so a step is only allowed to use ids that already exist.

The first live runs came back with step ids as numbers in all 3 cases. The schema caught every one and the repair loop fixed them.
Saying in the prompt that the id is a string removed a whole model call per run. 25% fewer calls, 32% cheaper, and the scores went up.
04

Deterministic checks

Six checks over the plan that don't need a model: coverage, test strategy, orphan steps, risks without mitigations, whether open questions carried through, and whether there are steps at all.

Why it works this way. If I can check something objectively, I check it in code. Asking a model whether every requirement is covered would be slower, cost money, and sometimes get the counting wrong. These run before the judge, so I never pay to evaluate a plan I already know is incomplete. Warnings don't block anything, since they're judgment calls and belong to the person reviewing.

Free and instant. No model call, no network.
A failed check sends the plan back with the reasons attached, before any judge sees it.
05

Rubric evaluation

An LLM judge scores 5 criteria from 1–5 against written anchors, with mandatory evidence per score. Passing threshold: 3.5.

Why it works this way. Only the parts that really need judgment go to the model. The judge has to score every criterion once. Without that rule it can skip the one it would score badly, and the average goes up. The JSON still looks fine, so you wouldn't notice unless you check.

A scoring that skips, repeats, or invents a criterion gets rejected.
A score with no evidence gets rejected. A number on its own tells you nothing.
06

Human approval

The run stops and waits. A person approves it, or rejects it with a reason.

Why it works this way. This is the part I think matters most. The workflow never approves itself. A model can write something that looks right faster than anyone can read it, so a person has to check it before the work gets built. I tried to make that check quick by handling the boring parts first. The quotes are already verified and the coverage is already counted, so the reviewer can spend their time on what only a person can catch. Rejecting doesn't kill the run either. It goes back to planning, and whatever the reviewer wrote becomes the instruction for the next plan.

Tried it live. Rejecting with "no rollback step, and the export is not paginated" came back with a plan that handled both.
The browser only holds a run id. The repair count lives on the server, so you can't get extra tries by editing the request.

Two repair mechanisms, deliberately separated

Both get called “retry”, but they fire for different reasons, feed back different things, and have different limits. Lumping them together hides what actually happened in a run.

Schema repair
Fires when: The model returned malformed or dishonest output.
Feeds back: The specific validation violations, appended with the model's own bad output.
bound: 2 repairs per stage
Replan
Fires when: Output was well-formed but rejected on its merits by the checks, the judge, or a human.
Feeds back: Failed checks, the weakest rubric criteria with their evidence, or the reviewer's note.
bound: 2 rounds per run
Every loop is bounded three independent ways.
—A hard attempt ceiling per stage.
—An exit the moment output validates.
—An immediate abort on a non-retryable provider error, so a rate limit never consumes repair budget meant for schema failures.

What a model judges, and what it does not

Anything I can check objectively gets checked in code. Requirement coverage is just counting, and asking a model to do it again would be slower, cost more, and sometimes get it wrong. These 5 criteria are the parts that really need judgment.

CriterionQuestion the judge answers
Approach soundnessIs the proposed approach a reasonable way to satisfy the requirements?
Unsupported assumptionsDoes the plan invent constraints, behaviour, or context the ticket and requirements never established?
Test strategy qualityWould the stated tests actually catch a regression in the behaviour being changed?
Scope disciplineDoes the plan stay within what was asked?
Risk awarenessDoes the plan identify the risks that actually matter for this change, including security and accessibility where relevant?
Stated limitations of the judge.
—Judges the plan, not the eventual implementation.
—Shares a model family with the planner, so it may accept its own style of reasoning too readily.
—Thresholds are not calibrated against human raters.
—Has no repository context, so it cannot verify that referenced files exist.

What running it actually taught me

Everything above was built against a mock, using responses I’d guessed at. These came out of real runs, and a few of them are mistakes in my own assumptions rather than anything the model did.

A working repair loop hid a prompt defect
Planning returned numeric step ids in every live run. The loop fixed it each time, so everything looked fine, but the prompt had never said the field was a string. That's what caused it. Fixing the prompt removed a whole model call per run. I could have just coerced the type in the schema instead, but that would have buried the bug and kept paying for the extra call forever.
Cost does not follow per-token pricing
Opus is 5x Haiku per token but 13x per run, because thinking is on by default and blows up the output. On the same ticket all three models landed on the same rubric average, so the extra money bought nothing I could measure.
An eval that passed when nothing ran
A coverage assertion checked that the check "did not fail", which is automatically true on a run that never got as far as validation. An eval that passes when nothing happened is worse than having no eval.
One sample is not a property
clarificationNeeded came back true for every fixture, so I figured it just over-flags and pinned that as a test. A later set of recordings had it get the answer right. So it's really just nondeterministic, and my first conclusion made exactly the mistake this project warns about.
A code review found a spend vector
The approval endpoint enforced the repair bound against a number the client supplied. A forged run with repairRounds -9999 was accepted and made real billable calls. Validating the shape would not have fixed it, since a client sending 0 forever still buys unlimited replans. The state had to move server-side.

Measured cost per run

One full run on the same ticket. These come from the same pricing table the app uses, so they can't drift from what the run report shows.

ModelCost per runNote
Haiku 4.5$0.017Measured ~28s per run. What the recordings and eval numbers use.
Sonnet 5$0.075Measured 4.4x Haiku's cost and ~45s per run.
Opus 5$0.220Measured 13x Haiku and ~98s per run. Thinking is on by default and counts toward output.

What would change at production scale

Worth writing down, because knowing what's missing is part of the job. This plans the work. It doesn't write the code.

—Durable run state, so a run survives a restart.
—A sandboxed executor and repository access, so plans become diffs.
—Per-tenant isolation, and a shared store so the spend ceiling is not per-instance.
—The eval suite gating prompt changes in CI against a tracked pass-rate baseline.
—Rubric scores calibrated against human raters. Right now the threshold is a guess with a decimal point.