DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

Why Every Library in the Agent Stack Has Zero Runtime Dependencies

The choice

Every library in the agent stack ships with zero runtime dependencies. No requests. No httpx. No pydantic. No jsonschema. No tenacity. Nothing.

This was a deliberate design choice made at the start of the project. It holds for every library: prompt-replay, agentvet, llm-circuit-breaker, tool-side-effects-tag, and the rest.

This post explains why. It also explains what the choice costs, because there are real costs, and ignoring them would be dishonest.


The diamond dependency problem

Here is the scenario. Your project uses requests 2.28. You add agentvet. agentvet depends on requests 2.31. Pip resolves to 2.31 (or whichever wins). Your project tests against 2.28. You now have a version mismatch that you did not ask for.

Now multiply by 10 libraries. Each one brings its own opinion on requests, pydantic, anthropic, and whatever else. Your dependency graph is a graph in the graph theory sense. Diamond conflicts happen at the edges. The more libraries you add, the more edges, the higher the chance of a conflict.

Zero dependencies means zero edges from that library into your graph. The diamond problem cannot happen at a library that has no deps.

This matters especially for agent-stack libraries because you are likely to install several of them together. They are designed to compose. If they all had separate dep requirements, composing five libraries could easily produce five separate version conflicts.


No transitive license risk

Your company's legal team asks what open-source licenses your software uses. With zero-deps libraries, the answer is simple: just the library's own license.

With deps, you inherit the transitive dep tree. That tree can include packages you have never looked at. Those packages can have licenses you have not reviewed. A library that depends on requests inherits urllib3, certifi, idna, and charset-normalizer. Each has its own license. If any of them changes license in a new version, you are affected.

Zero deps breaks the transitive license chain at the library boundary.


Faster installs in restricted environments

Some deployment environments have restricted internet access. Some CI pipelines cache their pip installs aggressively. Some edge environments have limited disk space or network bandwidth.

A library with zero deps installs as a single wheel. No network calls to resolve transitive dependencies. No compatibility checks across the dep tree. The install is fast and deterministic.

This sounds minor. In practice it matters when you are building Docker images in CI, deploying to edge workers, or running in environments where every pip install is audited before it runs.


It forces simpler implementations

This is the one that surprised me. Zero deps is a constraint that pushes implementations toward simpler code.

When you cannot reach for requests, you write with http.client. http.client is verbose. So you think harder about whether the library actually needs to make HTTP calls at all. Often it does not. Often you can push the HTTP call out to the caller and have the library operate on the response data instead.

When you cannot reach for pydantic, you write explicit Python validation code. That code is more verbose. So you think harder about how complex your validation logic needs to be. Complex validation often reveals that your API surface is too large. Simpler APIs need less validation.

Here is an example. agentvet validates tool call arguments against a JSON Schema. The obvious implementation reaches for the jsonschema package. But jsonschema pulls in attrs, rpds-py, referencing, jsonschema-specifications, and more. That is too much for a utility library.

The zero-deps version uses pure Python:

def _validate_type(value, schema: dict, path: str) -> list:
    """Validate a single value against a JSON Schema type constraint."""
    errors = []
    expected_type = schema.get("type")

    if expected_type is None:
        return errors

    type_map = {
        "string": str,
        "number": (int, float),
        "integer": int,
        "boolean": bool,
        "array": list,
        "object": dict,
        "null": type(None),
    }

    expected_python_type = type_map.get(expected_type)
    if expected_python_type and not isinstance(value, expected_python_type):
        errors.append(
            f"{path}: expected {expected_type}, got {type(value).__name__}"
        )
    return errors


def _validate_string_constraints(value: str, schema: dict, path: str) -> list:
    errors = []
    if "minLength" in schema and len(value) < schema["minLength"]:
        errors.append(f"{path}: length {len(value)} < minLength {schema['minLength']}")
    if "maxLength" in schema and len(value) > schema["maxLength"]:
        errors.append(f"{path}: length {len(value)} > maxLength {schema['maxLength']}")
    if "enum" in schema and value not in schema["enum"]:
        errors.append(f"{path}: value not in enum {schema['enum']}")
    if "pattern" in schema:
        import re
        if not re.match(schema["pattern"], value):
            errors.append(f"{path}: value does not match pattern {schema['pattern']!r}")
    return errors


def validate_args(args: dict, schema: dict) -> list:
    """
    Validate a dict of tool call arguments against a JSON Schema.
    Returns a list of error strings. Empty list means valid.

    Supports: type, required, properties, minLength, maxLength, enum, pattern,
              minimum, maximum, items (arrays), additionalProperties.
    """
    errors = []
    properties = schema.get("properties", {})
    required = schema.get("required", [])

    for key in required:
        if key not in args:
            errors.append(f"root.{key}: required field missing")

    for key, value in args.items():
        if key not in properties:
            if not schema.get("additionalProperties", True):
                errors.append(f"root.{key}: additional property not allowed")
            continue

        field_schema = properties[key]
        path = f"root.{key}"
        errors.extend(_validate_type(value, field_schema, path))

        if isinstance(value, str):
            errors.extend(_validate_string_constraints(value, field_schema, path))
        elif isinstance(value, (int, float)):
            if "minimum" in field_schema and value < field_schema["minimum"]:
                errors.append(f"{path}: {value} < minimum {field_schema['minimum']}")
            if "maximum" in field_schema and value > field_schema["maximum"]:
                errors.append(f"{path}: {value} > maximum {field_schema['maximum']}")
        elif isinstance(value, list) and "items" in field_schema:
            for i, item in enumerate(value):
                errors.extend(_validate_type(item, field_schema["items"], f"{path}[{i}]"))

    return errors
Enter fullscreen mode Exit fullscreen mode

That is 65 lines. It handles the common cases. It does not handle $ref, allOf, anyOf, or if/then. If you need those, you probably need the real jsonschema package. But most tool call schemas do not need them.

The zero-deps constraint forced the question: "do we actually need the full JSON Schema spec here?" The answer was no. That forced a simpler interface.


What zero-deps costs

Honesty requires naming the costs.

More code to maintain. Every utility that a dep would provide, you write yourself. http.client is verbose compared to requests. Pure Python JSON validation is verbose compared to pydantic. The maintenance surface grows.

Sometimes less ergonomic. The requests library exists because http.client is painful. Pure Python validation is more error-prone than pydantic. Users expect ergonomic APIs. Sometimes a dep would deliver that ergonomics for free.

Missing features. The pure Python JSON Schema validator above does not support $ref or allOf. If your tool schemas use those, you are blocked. A jsonschema dep would unblock you instantly. The zero-deps version cannot easily support everything the spec defines.

Testing complexity. Well-tested deps like pydantic, requests, and jsonschema have their own test suites. When you reimplement their functionality, you own the correctness of that implementation. More code to test. More edge cases you might miss.


The tradeoff in one sentence

Zero deps makes the library safe to install anywhere, but it makes the library harder to build and maintain.

For composable utility libraries that live at the periphery of an agent system, that tradeoff is usually worth it. For a framework that is the center of your system, zero deps is likely too restrictive.


Install and quick-start

All libraries in the agent stack install the same way:

pip install agentvet
pip install llm-circuit-breaker
pip install tool-side-effects-tag
pip install prompt-replay
# etc.
Enter fullscreen mode Exit fullscreen mode

No extra dependency resolution. Each installs as a single wheel.


Sibling libraries in the agent stack

All of these have zero runtime dependencies:

Library What it does
agentvet Validate LLM tool call arguments against JSON Schema
llm-circuit-breaker Open/half-open/closed circuit breaker for LLM calls
tool-side-effects-tag Tag tools as READ/WRITE/IDEMPOTENT/DESTRUCTIVE
prompt-replay Record and replay LLM calls for prompt comparison
tool-secret-scrubber Strip secrets from tool call logs
llm-message-hash-py Canonical hash of LLM request payloads
llm-retry-py Exponential backoff retry for LLM calls
tool-output-truncate-py Truncate tool output to fit LLM context limits

What is next

Three areas where the zero-deps constraint might be relaxed in future:

First, an optional extras pattern. pip install agentvet[full] could add jsonschema for full JSON Schema spec coverage. The base install stays zero-deps. Users who need the full spec opt in explicitly.

Second, a build-time bundler. Bundle the subset of a dep that the library actually uses into the wheel itself. The user experience is zero-deps. The implementation gets to use the real library internally.

Third, a community-maintained version with deps. Keep the zero-deps version as the default. Maintain a separate -rich variant that adds ergonomic deps for users who want them and are OK with the dependency surface.

The zero-deps choice was made at the start and has held. It will continue to hold for the base installs. How much it relaxes through extras or bundling is an open question.

GitHub: MukundaKatta

Top comments (0)