DEV Community

Sam Yang
Sam Yang

Posted on

The Dependency the Agent Invented: A Myth-Busting FAQ

Consider a reconstructed incident that shows up when teams treat an agentic loop as an architecture review. A backend pair watched their coding agent close six steps with a confident summary and a tidy pull request. The suite was green, the changelog mentioned Redis-backed sessions, and nobody had added Redis to compose, secrets, or the runbook. The miss was not a weak model so much as a missing freeze on what the repository was allowed to assume.

Developer glossaries often present planning, tools, memory, and loops as if those nouns were safety properties. Those nouns describe a control flow, not a signed agreement about services, environment variables, or network ports. The practical question is not whether the agent sounded agentic, but whether a new dependency appeared without a prior contract line. The rest of this FAQ restates four claims teams repeat, then offers a checker that fails the build when the contract is violated.

Claim: a finished agentic loop means the design was negotiated

Engineers often read a multi-step trace as evidence that the agent and the repository reached a shared understanding. The trace usually contains file reads, a patch, a test command, and a paragraph that restates the patch in calmer language. That sequence resembles a hiker who stops when the trees look familiar rather than a committee that recorded dissent. Green tests record that the new code agrees with itself, not that Redis, object storage, or a queue existed yesterday.

A corrected mental model treats the loop as a hypothesis generator whose writes remain untrusted until an assumption freeze passes. Names that were absent from the contract file before the first tool call are invented dependencies, even when the compiler is quiet. You can still use agentic planning; you just refuse to confuse a plan with a change-control record.

Claim: a successful tool call equals the intended side effect

Teams point at a tool result that returned exit code zero and conclude the world now matches the assistant's summary. Exit codes report that a process finished, not that the process mutated the object the prompt described. A formatter can succeed on a file that should not exist, and tests can pass against a stub the agent just wrote. The analogy is a shipping label that prints without proving the crate contained the parts on the invoice.

Evidence you can collect is boring on purpose: diff the working tree against a declared allowlist after every tool batch. If docker-compose.yml, .env.example, or infrastructure modules gained a service name, that name must already live in the contract. If the name is new, the loop did not discover architecture; it smuggled a prior from training data into your repo.

Claim: reading a file once keeps the agent grounded for the rest of the loop

Developers repeat that the agent has the codebase because an early step dumped a tree or opened README.md. A transcript is a snapshot with drift, more like a tourist map folded in a pocket than a live survey of the street. Later writes can add modules the early read never saw, and later reads can miss files the agent itself created. Grounding is a time series of checks, not a single observation captured at step one of the loop.

The corrected model is to re-hash the working tree after each batch of writes and compare it with the contract, not with the chat. Chat memory can narrate files that the index never staged, which is how invented caches survive a code review that only skims the summary. If your review process starts from the assistant's closing paragraph, you are reviewing literature, not the tree.

Claim: a large free-tier token budget removes the need for an assumption budget

A related claim says that once tokens are plentiful, the agent will eventually read enough files to stop inventing services. Token volume only measures how much text moved through the loop, not how many new runtime names were authorized. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option with an operator-stated ten million token allowance, which makes long loops feel inexpensive.

Use that server as a place to run the checker beside the agent, not as a reason to skip the contract file. Split the token allowance so the last pass must read assumptions/contract.yaml and the diff, even if the feature summary already sounds complete. If the ledger pass cannot fit, the loop was not cheap; it was unpaid review shifted onto the next human.

A proposed assumption ledger

The proposed checker is a local fixture, not a benchmark, and it has not been executed against a private production monorepo in this article. Copy it into a throwaway app that already has compose, tests, and a small API so you can see the failure mode. The contract is the customs form; the agent may pack the suitcase, but undeclared names should not cross the boundary. The script reads a unified git diff, then rejects service names, environment prefixes, ports, and imports that the YAML file does not list.

# assumptions/contract.yaml
# Proposed fixture: commit this before the agent starts a loop.
version: 1
allowed_compose_services:
  - api
  - postgres
  - worker
allowed_env_prefixes:
  - APP_
  - DATABASE_
allowed_ports:
  - 8000
  - 5432
allowed_python_imports:
  - fastapi
  - sqlalchemy
  - pydantic
  - psycopg
datastore_hints:
  redis: redis
  memcached: memcached
  mongo: mongodb
  elastic: elasticsearch
  kafka: kafka
  rabbitmq: rabbitmq
  minio: object-storage
  s3://: s3
Enter fullscreen mode Exit fullscreen mode
#!/usr/bin/env python3
"""Proposed fixture: fail a diff that invents undeclared runtime names.

Label: unexecuted example. Run in a copy of your repo. Do not point it
at production kubeconfigs, secret files, or live credentials.
"""
from __future__ import annotations

import re
import subprocess
import sys
from pathlib import Path

try:
    import yaml
except ImportError:
    sys.stderr.write('pip install pyyaml\n')
    raise SystemExit(2)

CONTRACT_PATH = Path('assumptions/contract.yaml')
ENV_ASSIGN = re.compile(r'\b([A-Z][A-Z0-9_]{2,})=')
PORT_PAIR = re.compile(r'\b(\d{2,5}):(\d{2,5})\b')
IMPORT_LINE = re.compile(r'^\s*(?:from|import)\s+([A-Za-z0-9_]+)')
COMPOSE_SERVICE = re.compile(r'^\s{2}([A-Za-z0-9_-]+):\s*$')


def git_diff() -> str:
    result = subprocess.run(
        ['git', 'diff', '--unified=0', 'main...HEAD'],
        check=False,
        capture_output=True,
        text=True,
    )
    return result.stdout


def added_lines(diff: str) -> list[str]:
    lines = []
    for line in diff.splitlines():
        if line.startswith('+') and not line.startswith('+++'):
            lines.append(line[1:])
    return lines


def main() -> int:
    contract = yaml.safe_load(CONTRACT_PATH.read_text())
    added = added_lines(git_diff())
    joined = '\n'.join(added)
    failures: list[str] = []

    allowed_services = set(contract['allowed_compose_services'])
    allowed_prefixes = tuple(contract['allowed_env_prefixes'])
    allowed_ports = {int(p) for p in contract['allowed_ports']}
    allowed_imports = set(contract['allowed_python_imports'])
    hints = contract['datastore_hints']

    for hint, label in hints.items():
        if hint.lower() in joined.lower():
            failures.append(f'invented datastore hint {label!r} via {hint!r}')

    for line in added:
        match = COMPOSE_SERVICE.match(line)
        if match and match.group(1) not in allowed_services:
            failures.append(f'invented compose service {match.group(1)!r}')
        for env_name in ENV_ASSIGN.findall(line):
            if not env_name.startswith(allowed_prefixes):
                failures.append(f'invented env {env_name!r}')
        for host_port, _container in PORT_PAIR.findall(line):
            port = int(host_port)
            if port not in allowed_ports:
                failures.append(f'invented host port {port}')
        imported = IMPORT_LINE.match(line)
        if imported and imported.group(1) not in allowed_imports:
            failures.append(f'invented import {imported.group(1)!r}')

    unique = sorted(set(failures))
    if unique:
        sys.stderr.write('assumption freeze failed\n')
        for item in unique:
            sys.stderr.write(f'- {item}\n')
        return 1
    sys.stdout.write('assumption freeze passed\n')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Run the commands from a clean branch so the diff window matches the loop you actually want to audit. If check_assumptions.py exits with a nonzero status, the agent invented a dependency even when pytest remains quiet. Keep the contract in the same pull request as the feature so reviewers see the freeze and the code together.

git checkout -b audit-invented-deps
python3 tools/check_assumptions.py; echo exit:$?
git diff --name-only main...HEAD
rg -n 'redis|memcached|mongo|elastic|kafka|s3://' -g '!assumptions/**' -g '!tools/**'
Enter fullscreen mode Exit fullscreen mode
# tests/test_assumption_contract.py
# Proposed test. Label: unexecuted example.
from pathlib import Path
import yaml

REQUIRED = (
    'allowed_compose_services',
    'allowed_env_prefixes',
    'allowed_ports',
    'allowed_python_imports',
    'datastore_hints',
)

def test_contract_has_required_keys():
    data = yaml.safe_load(Path('assumptions/contract.yaml').read_text())
    for key in REQUIRED:
        assert key in data and data[key], key
Enter fullscreen mode Exit fullscreen mode

When the checker fails, classify the hit before you widen the allowlist, because widening is how invented caches become permanent. If the name is a real product decision, add it to the contract in a separate commit with a human sentence of intent. If the name arrived from a model prior, delete the code and spend tokens on a narrower prompt that cites the contract path. Do not enlarge allowed_python_imports to silence a red build; that is how Redis became a session store in the reconstructed incident.

This approach will not help teams that lack a declared runtime, because a blank contract either blocks all work or rubber-stamps everything. Regular expressions over diffs miss generated manifests, helm templates, and configuration assembled at deploy time from concatenated strings. The ledger also ignores semantic debt: an allowed postgres service can still receive a schema the team cannot operate. Security review, load testing, and data-retention analysis remain outside the fixture, and green ledger output should not be sold as those reviews.

Skip this workflow if your organization already enforces infrastructure policy as code through a real admission controller. Skip it for throwaway prototypes that will never open a port, and skip it if nobody can edit the contract except the agent. An agent-owned contract is a diary, not a freeze, and it will happily legalize every invention after the fact.

Agentic language will keep spreading because it names a real control flow that many developers are seeing for the first time. The useful move is to keep the vocabulary and still demand a freeze on names that cost money, pages, or incident hours. If you already run a free-tier coding agent, commit the contract before the loop, then let the remaining tokens write code that can pass it.

Top comments (0)