Teaching AI Agents to Find Your Code

Share with:


I’ve been thinking about a problem that every large engineering organisation will hit as AI agents become a standard part of the development workflow. You give an agent a business requirement (“add a status field to the refund flow”) and it confidently modifies the wrong service. The code compiles, the tests pass, the PR looks reasonable, but it’s completely wrong. This isn’t a model capability problem. It’s a vocabulary problem. And it turns out we’ve known how to solve vocabulary problems in large systems for over two decades.


The Root Cause Is Ambiguity, Not Intelligence

A mature codebase is a graveyard of naming decisions made by different teams in different quarters, none of them ever reconciled. “Customer” means the aggregate in one service, a read model in another, and a third-party API response object in a third. “Invoice” and “billing” get used interchangeably in some parts of the system and mean completely different things in others. A human engineer learns these distinctions over months of context, but an agent has no institutional memory and only knows what’s in front of it. When the codebase holds several plausible answers to the question “which service handles this?”, the agent picks one. The pick is locally valid and globally wrong, which is the most expensive category of defect an organisation can manufacture at scale. The fix isn’t a smarter model. It’s a codebase that has decided what its own words mean and written those decisions down somewhere an agent can read.


The Idea: Federated Manifests, a Centralized Index, and a Dedicated Routing Agent

At the local level, each service needs structured documentation: a README that speaks in business language, defines the terms the service owns, explicitly calls out the terms it does not own, and maps business requirements to specific entry points in the code. But in a system with hundreds of services across hundreds of repos, local documentation alone is circular. The agent has to already know which service to look in before it can read the README, which is the problem we’re trying to solve. What bridges the gap is a centralized index, a single generated file that maps domain terms to services, aggregated from a small manifest that every service maintains. This index lives in its own dedicated repo. A dedicated routing agent is scoped to that repo and given one job: receive a business requirement, look up the relevant service, and return a structured answer. The parent agent that received the original requirement never loads the index at all. It delegates to the routing agent, gets back a service name, and spawns a new service agent scoped to that repo to do the actual work. This keeps each agent’s context focused. The routing agent carries only the index, the service agent carries only its repo, and the parent orchestrates without being burdened by either.


What Each Service Owns

Every service maintains a domain.yaml at its repo root. It’s a small file with a simple job: declare what domain concepts this service owns, and explicitly reject the synonyms it doesn’t.

service: payment-gateway
team: payments
description: "Processes card transactions, handles authorization and settlement"

domain:
  bounded_context: payments
  canonical_terms:
    - payment
    - transaction
    - authorization
    - settlement
    - charge
  rejected_synonyms:
    - invoice    # belongs to billing-service
    - refund     # belongs to refund-service
  related_concepts:
    - refund
    - chargeback
    - dispute

The rejected_synonyms field is the one most solutions miss. A service saying “we do not own invoice” is as useful to an agent as a service saying what it does own. At scale, the drift between how different teams name the same concept is precisely what causes agents to modify the wrong service. Recording rejections explicitly cuts that failure mode off at the source.

The bounded_context field groups services by business domain. Multiple services can share a context (payment-gateway and payment-scheduler both live in payments), which lets an agent reason about service relationships without needing a graph database.


What Each Service Documents

The README inside each service is where the service agent navigates once it has arrived. Three things make a README genuinely useful rather than decorative. First, a glossary of the ubiquitous language: the exact terms the business uses, defined precisely, with explicit callouts for terms the service does not own and where those live instead. Second, a plain-language description of business capabilities covering what workflows this service handles and what it does not, with no technical architecture or implementation detail. Third, an entry points table, which is the one most READMEs skip and the most valuable part. An agent that lands in the right service still needs to know where to start, and without explicit entry points it will infer one. Inference at this level produces code that compiles but enters the system at the wrong boundary.

## Entry Points

| Business Requirement       | File                    | Function                   |
|---------------------------|-------------------------|----------------------------|
| Authorize a payment        | src/authorization.py    | authorize_transaction()    |
| Process a charge           | src/gateway.py          | process_charge()           |
| Capture an authorization   | src/capture.py          | capture_authorization()    |

The folder structure inside the service should reinforce all of this. Organising by business capability (invoices/settlements/) rather than technical layer (controllers/services/models/) means an agent reading the directory tree gets the same signal as an agent reading the README.


The Centralized Index

A CI pipeline crawls every service repo, reads each domain.yaml, and writes a single aggregated index into the index repo. The index is generated and never edited by hand. When a conflict exists (twelve services all claiming “customer” is a real scenario), the index surfaces it explicitly rather than silently picking a winner. The routing agent returns all candidates with a recommended primary, and the parent agent decides whether to spawn one service agent or several in parallel.

The pipeline runs on merge to main in any service repo, nightly as a safety net, and on demand for bootstrapping. Coverage (the percentage of services with a manifest) should be tracked as a metric, treated the same way an engineering organisation treats test coverage.


Bootstrapping Without a Big Bang

The most common objection at scale is the cold start problem. You have hundreds of services with no manifests and no index, and you can’t add a manifest to every service before the system is useful. You don’t need to. The index is valuable at partial coverage because missing services simply don’t appear, making the routing agent’s failure mode “service not found” rather than “wrong service confidently returned.” Seed the twenty highest-traffic services manually, add the manifest to the service template so all new services get it automatically, make it part of onboarding, and backfill the rest over time prioritised by where agents are getting routing wrong most often. The system improves incrementally and stays useful throughout.


What This Is Not

This design doesn’t require a vector database. Semantic search over embeddings is a valid complementary layer once the index is large and fuzzy matching becomes necessary, but it requires infrastructure, is harder to debug, and produces failures that are difficult to trace. The manifest approach is deterministic first, and embeddings can be added later if the need is real. It doesn’t require a graph database either; the also_relevant field captures the service relationships an agent needs for routing without that infrastructure. And it is not auto-generated from code. Static analysis can supplement the manifest but can’t replace it, because domain intent is a human decision: which team owns a concept, which synonyms are deliberately rejected, which bounded context a service belongs to. Code analysis can observe what a service does. It cannot observe what the team decided it is supposed to mean, and that decision needs to be written down by a person.


The interesting thing about this whole approach is that none of it is new. Ubiquitous language, bounded contexts, and context mapping come from Eric Evans’ Domain-Driven Design, published in 2003. What’s new is the audience. That vocabulary was designed to remove the translation step between domain experts and engineers. It now has a second consumer, and that consumer is an AI agent with no institutional memory, no hallway conversations, and no ability to infer what the team actually meant. Everything it needs has to be in the artifact it’s pointed at. The manifest and the index are that artifact.

Share with:


Leave a Reply

Your email address will not be published. Required fields are marked *

*


The reCAPTCHA verification period has expired. Please reload the page.