Agentic Instincts: Coding Agents and their love for Pattern Matching

Agentic Instincts: Coding Agents and their love for Pattern Matching

A case study in why AI agents need eval-first engineering and how “coding agents” should code “agents”

The agent worked perfectly, right up until the user said the same thing differently, a failure mode I’ve seen cropping up one-too-many times when building “agents” with “coding agents” (ouroboros reference :D).

Take a workflow agent that’s supposed to understand a user, collect missing information, use tools, retrieve a file, create a case, or route work to a human when policy requires it. The first version mostly works. Then a real user phrases something in a way nobody predicted and it falls over. So a coding agent gets asked to fix it.

Too often the “fix” is a regex. Or a keyword list, or a substring check, or a tiny field resolver for that one specific case. Then tests get added to prove those strings match. The tests pass, the demo looks calmer, and the architecture has quietly gotten worse in a way that won’t show up until the next unexpected phrasing.

I have nothing against regex, for the record. It’s great for machine-owned formats like IDs, timestamps, provider status codes, JSON envelopes and known protocol strings. The problem starts when regex is used to decide what a human means. At that point the agent has quietly become a brittle parser with an LLM stapled to it.

The parser trap

Here’s the anti-pattern, and it’s what coding agents reliably reach for when you feed them a stack trace saying “hey, this field isn’t being parsed correctly from the user message”:

if (/​invoice|contract|file/​i.test(userMessage)) {

return sendRequestedFile();

}

The exact form varies. Sometimes it’s fuzzy matching, sometimes token overlap, sometimes a list of field names, sometimes a transcript check like “if the bot said this phrase, mark the flow complete”. It all comes from the same instinct, which is to convert an open-ended language problem into a deterministic string problem.

To be honest, the instinct is understandable. A lot of normal engineering rewards exactly this thing. Narrow the input, define the branch, add the test, turn ambiguity into code. But agent workflows exist precisely because the input is not stable enough to enumerate. Users misspell words, switch languages mid-sentence, interrupt themselves, refer to something from three turns ago, attach files without explaining them, ask side questions and use phrases no engineer predicted. Which is, you know, the entire reason we reached for an LLM in the first place.

Once you add one parser branch per failure, every new use case becomes code. Ten fields become ten resolvers, ten case types become ten keyword maps, fifty files become fifty fragile branches. The agent’s “intelligence” is now fenced in by whatever phrases someone remembered to hardcode.

The self-fulfilling test

The bigger problem is the test that grows around the parser.

Runtime code says: message contains “invoice” ⇒ file workflow.

The test says:

expect(message).toContain(“invoice”);

expect(response).toMatch(/​file/​i);

Now the test and the implementation share the same false assumption, and the system passes because both sides believe the user will say the magic word. A self-fulfilling prophecy, basically.

A unit test can prove deterministic code obeys a contract. A string-matching “agent test” usually proves only that the parser recognises phrases we already anticipated. It can’t tell you whether the agent understood the user, or used the right tool, or whether the business operation actually happened, or whether the answer was grounded in real state. For agents, transcript matching is about the lowest-signal kind of confidence you can buy.

Why coding agents reach for this

I don’t think coding agents do this because they’re incapable. They do it because the local reward landscape, the one carved out during RL post-training, points them there.

They get handed a concrete failure: “the flow got stuck when the user asked for a file” or “this form field was not filled”. The smallest patch is to recognise that phrase and force the desired branch. The smallest test is to assert that branch. It looks like engineering: condition, handler, test, green check. The good ol’ TDD loop.

But in an agent system, that patch bypasses the one part of the system that’s supposed to do semantic work, which is the model operating inside a harness with tools, schemas, durable state and feedback.

Test-first habits aren’t wrong here, just incomplete for agent behaviour. TDD was built for deterministic contracts, while agents are probabilistic, tool-using, stateful and often open-ended. If we don’t give coding agents an eval-first instinct, they’ll happily translate an agent failure into a parser task, because parser tasks are much easier to make green.

TDD is not the enemy

The lesson isn’t “stop writing tests”. It’s more like: test deterministic parts deterministically, evaluate agentic parts agentically.

There’s still plenty of ordinary software in an agent system.

  • Schema validation

  • Tool input validation

  • Authorization

  • Idempotency

  • Database writes

  • Queue transitions

  • API adapters

  • Payload construction

  • Error normalization

Semantic behaviour is a different animal though. If the question is “did the agent understand the user and take the right next step?”, then the eval should run the actual agent path and inspect model decisions, tool calls, state transitions, final operations and user-visible output.

The unit test asks “did this deterministic function obey its contract?”. The eval asks “did the agent, in this environment, with these tools and this state, solve the task in a way we’d accept in production?”. Very different questions.

A safer boundary

A solid workflow agent shouldn’t ask application code to infer user intent from raw language. A better split would be:

human language

-> model semantic decision through structured output or tool call

-> deterministic validation against schemas, catalogs, policy, and state

-> deterministic execution

-> grounded response from the actual operation result

The model decides what the user likely means. Code decides whether that decision is valid and executable.

For a file request, the backend should build a catalog:

type FileCatalogItem = {

id: string;

label: string;

metadata: Record<string, unknown>;

canDeliver: boolean;

};

The model receives the user request plus the catalog and returns structured output:

type FileSelection = {

selectedIds: string[];

confidence: number;

clarificationQuestion?: string;

reason: string;

};

Then code validates the selected IDs against the catalog and sends only files that actually exist and are deliverable. No hardcoded file type names anywhere. New file types require better metadata, not a new parser branch.

The same idea applies to dynamic forms. The backend provides the current field schema and known facts, the model maps messy user language into structured fields, and code validates field IDs, option values, attachments and payload shape. If something’s missing, the workflow asks for it. If the user suddenly goes “wait, what does this field even mean?”, the agent answers and carries on with the flow instead of abandoning it.

The scalable unit is not “one resolver per field”. It’s a typed field schema plus a field-collection workflow that can handle N fields.

Memory means durable state

People often talk about agent memory as if it means personality. In production workflows, memory is way more boring than that. It’s durable state.

The harness needs to know:

  • Which workflow is active

  • Which phase it is in

  • Which fields are collected

  • Which fields remain missing

  • Where each value came from

  • Which tool calls already happened

  • Which errors are retryable

  • Which action is awaiting confirmation

  • Which payload hash was confirmed

  • Whether a human route was already queued

If all of this lives in a process-local variable, the agent is fragile. A restart, a deploy, a duplicate webhook or a second server instance can erase the flow. Then the user answered but the agent forgot, or the tool succeeded but the response claims it failed, or the confirmation happened but isn’t tied to any payload.

Durable state is the agent’s working “memory”. A scratchpad isn’t a luxury, it’s how the harness stops the model from re-solving the same problem every single turn.

Good memory is structured, not an ever-growing transcript blob:

type WorkState = {

activeUseCase: string;

phase: “routing” | “collecting” | “answering_side_question” | “awaiting_confirmation” | “executing” | “done”;

collectedFacts: Array<{ fieldId: string; value: unknown; source: string }>;

missingFields: string[];

pendingAction?: { kind: string; payloadHash: string };

lastToolResults: Array<{ tool: string; status: “ok” | “retryable_error” | “fatal_error” }>;

};

Tools should be signal-ly

Another failure mode is tool pollution. The agent calls a backend tool and receives a huge response, so now the model has to dig through a pile of JSON, infer what matters, ignore irrelevant fields and recover from errors that were written for developers. Which is kind of backwards if you think about it. The model is there for semantic judgment, not to be a garbage collector for messy tool output.

Agent tools should return the shortest useful result for the next decision. They need precise names, clear input schemas, concise success output and errors the model can actually act on.

Bad tool result:

{

“error”: “500″,

“stack”: ”...”,

“data”: null

}

Better tool result:

{

“status”: “retryable_error”,

“reasonCode”: “file_service_unavailable”,

“agentHint”: “Do not claim the file was sent. Apologize briefly and offer to retry or route to a human.”

}

Eval-first development

An eval-first harness doesn’t start with “what exact phrase should the final answer contain?”. It starts with a scenario:

type EvalScenario = {

id: string;

initialState: FixtureState;

turns: UserSimulator | StaticTurns;

availableTools: ToolFixture[];

expectedOutcomes: ExpectedOutcome[];

rubric: SemanticRubric;

};

Then it runs the real path:

  1. Seed fixtures.

  2. Run the router.

  3. Run the selected workflow.

  4. Let the model call tools or return structured decisions.

  5. Execute tools through production-like validation.

  6. Persist state transitions.

  7. Capture a trace.

  8. Grade the outcome.

Deterministic graders check schema validity, selected IDs, allowed actions, queue state, persisted fields, confirmed payload hashes and tool-backed claims. Semantic graders check whether the response is helpful, grounded, in the user’s language and appropriate to the active workflow.

This is why I push back on the “evals are just tests with a trendy name” framing. A good eval measures the model plus the harness: tools, state, policies, traces and outcomes. It allows multiple valid trajectories, but not invalid business results.

The harness shape

The harness I want looks something like this:

Inbound message

-> Context Builder

-> Intent Router

-> Use Case Contract Registry

-> Data Resolver

-> Specialized Workflow or Agent

-> Tool Executor

-> Policy Guard

-> Confirmation Gate

-> Action Executor

-> Response Composer

-> Trace Recorder

-> Eval Verifier

The exact names matter way less than the ownership. The router owns semantic classification. The context builder owns what the model sees. The contract registry owns which use cases exist, what tools they may use, what policies apply and how they’re verified. The workflow owns state progression. The tool executor owns validation and side effects. The response composer owns user language grounded in actual results. The trace recorder owns observability.

Without those boundaries, every fix lands in the same giant service file and every new use case just adds entropy.

The skill file rule

If you use coding agents to build agents, give them explicit rules in their skill or instruction files:

  • Do not infer user intent with regex, keywords, substring checks, fuzzy matching or transcript patterns.

  • Use model structured output or tool calls for semantic decisions.

  • Use deterministic code for validation, execution, policy, persistence and audit.

  • Add new use cases by extending schemas, catalogs, tools, workflows and eval fixtures.

  • Evals must run the actual agent path and grade traces, tool calls, state, business outcomes and semantic quality.

  • If a failure seems to require a new string pattern, stop and redraw the agent boundary.

That last one is the important one. When a coding agent wants to add a regex, it’s usually telling you something useful, which is that the harness doesn’t give the model a clean way to make that decision. Maybe you need a structured router, or a catalog with stable IDs, or a less noisy tool response. Maybe the state isn’t durable. Maybe the eval is checking a transcript when it should be checking an outcome.

Final thoughts

The future of agent engineering is not one giant prompt, but it’s also not a thousand regexes hiding behind a friendly chat bubble. It’s “harness engineering” (as cliché as that may sound now.....) which means instructions, tools, environment, state and feedback arranged so the model can make semantic decisions and deterministic software can keep those decisions safe.

For agents, the question worth making green isn’t “did this exact phrase appear?”. It’s “did the agent understand, act, recover and leave the system in the right state?”.

Useful /​ supporting material