Skip to content
VishalPatil
Projects / VAgents

VAgents

VAgents preview

A personal AI agents library built so the deterministic parts stay deterministic and the model is the only thing left guessing.

Stack used

  • Python
  • TypeScript
  • MCP
  • Agents
  • Git

Why I built it

Every AI prototype I started began the same way: a prompt wrapper, some tool invocation logic, a retry here, a bit of state handling there, and an evaluation loop bolted on at the end once the output started disappointing me. Each project got its own slightly different copy of all five. None of them were good, and none of them got better, because improving one never improved the others.

vagents is my answer to that. It's a personal agents library for building, orchestrating, and iterating on AI agent workflows, where the repeated parts are modules with interfaces instead of code I retype. It's open source, I'm the sole builder, and it's grounded in the development branch of VishalPatil18/vagents.

Most AI projects begin as experiments and stay that way. That's the problem I was actually solving.

The real failure mode

Scattered prompt files, duplicated utility code, weak failure handling, limited reproducibility. Individually each is survivable; together they make iteration expensive and productionisation risky. The cost isn't in writing the glue, it's in never being able to trust it.

What I was optimising for

I set three outcomes before writing code, and used them to settle arguments with myself later.

Reusability

Abstract the common agent behaviours (prompt assembly, tool execution, retries, output formatting) into reusable modules rather than per-project copies.

Reliability

Strict typing and predictable error pathways, so failure is a case I handled rather than a surprise I discover in a demo.

Explainability

Document architecture and decisions well enough that I can defend the tradeoffs in a design review months later, when I no longer remember making them.

Architecture

The project separates into three layers that evolve independently, with explicit contracts between them.

Layer separation

Interfaces and integration surfaces (TypeScript, ~26.1%)

Layered on where developer experience or presentation mattered more than runtime behaviour.

The boundary the outside world talks to - validated and typed before anything reaches orchestration.

Next step: typed contracts

Agent runtime and core logic (Python, ~59%)

The control layer deciding how model calls, tool calls, and state transitions happen over time.

Instruction layers assembled at runtime rather than duplicated per agent.

Each tool is a contract. The model chooses whether to call it, not what shape the call takes.

Raw runtime and provider failures mapped to one stable application-level taxonomy.

Next step: calls out

Outside the boundary

Treated as an unreliable network call: retries, timeouts, and defined degradation.

Failure is contained at the adapter so one bad tool cannot destabilise a run.

Long-form documentation is ~8.2% of the repo. Explainability is a shipped artifact, not a README afterthought.

  • Interface layer
  • Python core
  • Non-deterministic dependency
  • Documentation artifact

Three layers with explicit contracts, so each one can evolve without dragging the others along. Hover any block for the reasoning.

  • Agent runtime and core logic. Mostly Python, and the primary implementation language of the repo. Orchestration and runtime behaviour live here.
  • Interfaces and integration surfaces. TypeScript, plus API and UX utilities, layered on wherever developer experience or presentation mattered more than raw runtime concerns.
  • Documentation and explainability artifacts. Design notes, long-form technical documentation, and the decision logs. Docs are part of the product, not an afterthought.
  • Python
  • TypeScript
  • MCP
  • Agents
  • Git

The tech stack

LayerChoiceShare of the repo
Core languagePython (agent runtime and orchestration)~59%
Tooling and interfacesTypeScript~26.1%
Long-form technical docsTeX~8.2%
Presentation layerCSS~5.6%
Build, config, supporting filesMisc~1.1%

The language split is itself a design statement: this is a Python system with a TypeScript surface, not a full-stack app that happens to call a model. The TeX share is not accidental either. A meaningful fraction of the repo is written explanation.

The runtime flow

Runtime flow of a single agent request

Malformed input fails at the edge rather than three layers deep inside orchestration.

System, context, and task instructions layered and injected at runtime.

Retries and timeouts apply here; a tool failure is a handled case, not an exception that escapes.

Output shaped at one place, so formatting is not re-derived per agent.

Whatever failed underneath, the caller sees one vocabulary of errors.

The trace is what makes modular prompt composition debuggable at all.

Written once as the path every agent takes, instead of reimplemented per prototype with slightly different retry semantics.

Input arrives, context and prompt are assembled, the agent decides whether a tool is needed and invokes it, the response is synthesised, and the whole thing is persisted and logged. Written out like that it's obvious. The value is that it's written out once, as a path every agent takes, rather than reimplemented per prototype with slightly different retry semantics each time.

Deterministic contracts around a non-deterministic model

This is the guiding principle I'd defend hardest. The model is probabilistic and nothing I do changes that, so the engineering job is to make everything touching the model predictable.

  1. Validation boundaries at the edges

    Inputs are validated where they enter the system, so a malformed request fails at the boundary rather than three layers deep inside orchestration.

  2. Typed contracts between layers

    Each tool is a contract with typed inputs and outputs. The model can choose whether to call it; it cannot change the shape of the call.

  3. Centralised error utilities

    Heterogeneous runtime and provider failures are normalised into a stable application-level taxonomy through shared utilities rather than ad hoc try/except at every call site.

  4. Retries, timeouts, and graceful degradation

    External model and tool calls are treated as the unreliable network calls they are, with degradation behaviour defined rather than emergent.

The cost of normalising errors

Normalisation adds wrapper code, and there is a real temptation to skip it for a "quick" integration. I pay it anyway because the alternative is a caller that has to know the failure vocabulary of every provider it might transitively hit.

Tool calling as contract design

Tool calling is where an agent stops being a text generator and starts having consequences, so it's the layer I was strictest about. Every tool is a typed input/output interface with an adapter behind it, which keeps behaviour predictable and makes the tool independently testable without a model in the loop.

The tradeoff is honest: strict contracts remove ambiguity but cost real up-front schema and adapter work. For a one-off script that cost isn't worth it, which is precisely why one-off scripts kept failing me on edge cases.

Prompt architecture

Prompts are composed modularly. System, context, and task instructions are layered and injected at runtime rather than duplicated per agent. Editing shared instruction logic in one place beats editing it in six.

What modular prompts cost you

They're easier to maintain and harder to reason about end to end. Once instructions are assembled from parts, "what did the model actually see" stops being a question you can answer by opening a file, which is exactly why tracing matters more in this design than in a naive one.

Testing and the evaluation loop

Model quality is empirical, not guaranteed, so behaviour gets tested the same way logic does.

LevelWhat it covers
UnitPure logic modules, no model in the loop
IntegrationEndpoint and orchestration boundaries
API endpoint testsA standing requirement in this project, not optional
RegressionEvery previously fixed behaviour, so old failures stay fixed

On top of that sits the evaluation loop: run outputs against expected behaviour, find the failures, refine prompts and code, repeat. It's slow, and it's the only thing standing between a system that demos well and a system that holds up.

How I worked on it

Solo, with no tickets and no reviewers, which means process discipline has to replace external pressure.

Solo development loop

A change is argued for in writing before it is implemented.

Spec translated into milestones rather than one large landing.

Endpoint and orchestration boundaries carry the regression weight.

Model quality is empirical, so outputs are tested against expected behaviour and the prompts or code refined from the failures.

Key implementation decisions recorded as made. A log you can rewrite is a log that flatters you.

Forces each iteration's lesson to be articulated rather than absorbed.

No tickets and no reviewers, so the process has to supply the pressure that a team normally would.

  1. Specs first

    Spec-driven development against my own written specs, so a change is intentional before it's implemented.

  2. Incremental implementation with regression checks

    Milestones translated from the spec, each one guarded by the tests around it.

  3. Append-only decision log

    claude/context.md records key implementation decisions as they're made. It's append-only on purpose. A decision log you can rewrite is a decision log that flatters you.

  4. Teaching journal

    claude/learnings.md captures what each iteration taught me, which forces the lesson to be articulated rather than absorbed.

Design rationale

How is this maintainable instead of a one-off prototype? I started by naming the patterns I kept rewriting (prompt assembly, tool execution, retries, output formatting) and turned each into a module with a clear interface. Orchestration is deliberately separated from business logic so one can change without breaking the other. The rest is process: specs before implementation, and an append-only context log so design decisions survive longer than my memory of them. Maintainability here comes from explicit contracts plus discipline, not from folder structure alone.

How do you get reliability out of a non-deterministic system? By refusing to let the non-determinism spread. Inputs and outputs at each layer are validated and typed, errors are normalised through consistent utilities, and retries, timeouts, and fallback behaviour are first-class rather than afterthoughts. Tests at the API and orchestration boundaries catch regressions early. The model stays probabilistic; the system around it doesn't have to be.

Why build the library at all instead of shipping the agent you wanted? Because I'd already shipped several of those and none of them made the next one easier. Richer orchestration does increase complexity and failure surface. That tradeoff is real. But scattered scripts have the same failure surface without anything to point at when they break.

What does full ownership actually mean here? Problem framing, architecture, implementation, testing, docs, and shipping, all mine. No external review cycle meant I had to be explicit about tradeoffs and quality bars myself, in writing, which is where the specs and the two logs came from. The upside is diagnostic speed: there's no layer I have to go ask someone about.

What I'd scale next

Observability across agent runs, a proper benchmark suite so quality claims have numbers behind them, and multi-agent protocols. All three are the same wish stated three ways. I want to measure this system as rigorously as I currently structure it.