Building Autonomous AI Workflows: LangChain vs Custom TypeScript Agentic Frameworks
Architecting autonomous AI agents: replacing complex LangChain abstractions with deterministic TypeScript tool loops, function schemas, and self-correction handlers.
By Uttam Thapa · · AI/ML
⚡ Executive Summary (TL;DR)
An agent is a loop, not a prompt: call the model, execute the tools it asks for, feed the results back, repeat until it stops asking. Written directly in
TypeScript that loop is about forty lines — and writing it yourself is what buys you the things production needs and frameworks tend to bury:
a hard step ceiling, a token budget, validated tool arguments, and errors fed back as data the model can recover from.
Figure 1: An agent run — several tool calls, each result folded back into the conversation before the next decision.
What Separates an Agent From a Prompt
A single completion maps one input to one output. An agent runs a loop where the model can act between turns:
goal → plan → call a tool → read the result → decide again → answer. That loop is the entire difference, and it is also the entire source of new
failure modes. A prompt can be wrong; a loop can be wrong repeatedly, expensively, and without stopping.
The Loop, in Full
export async function runAgent(goal: string, tools: Tool[], maxSteps = 5) {
const messages = [{ role: 'user', content: goal }];
for (let step = 0; step < maxSteps; step++) {
const response = await callOpenAIWithTools(messages, tools);
// No tool calls means the model considers the goal met.
if (!response.tool_calls) return response.content;
for (const toolCall of response.tool_calls) {
try {
const result = await executeTool(toolCall);
messages.push({ role: 'tool', content: JSON.stringify(result) });
} catch (err) {
// Errors are context, not crashes. Returned to the model, a failed
// call becomes something it can correct on the next step.
messages.push({ role: 'tool', content: JSON.stringify({ error: String(err) }) });
}
}
}
// Falling out of the loop is a real outcome and needs its own handling.
throw new AgentStepLimitError(goal, maxSteps);
}
Two lines carry most of the reliability. The maxSteps bound means a confused agent stops instead of spending your budget in a circle. And catching
tool errors into the message list rather than letting them propagate is what enables self-correction: a model told "that postcode was invalid" will usually try a
different one, where a thrown exception simply ends the run.
🚨 Hitting the step limit is not a bug to hide
Returning undefined when the loop runs out — as the naive version does — produces a silent failure that looks identical to a successful run with
no answer. Throw, log the message history, and surface it. The step limit being reached is the most informative signal an agent produces, and it is usually
telling you a tool description is unclear.
Tools Are an API You Are Designing for a Model
Agent quality is mostly tool quality. The model can only work with what your schema describes, and a vague description is indistinguishable to it from a missing
capability.
Describe the boundary
Say what the tool does and what it does not. "Searches products by name; does not check stock" prevents a whole class of misuse.
Validate every argument
Parse tool arguments with a schema before executing anything. Model output is untrusted input that happens to look structured.
Return errors it can use
{ error: "date must be ISO 8601" } is actionable. { error: "500" } teaches the model nothing.
Keep the tool count small. Beyond roughly a dozen, selection accuracy drops noticeably and the schema starts consuming meaningful context on every single call.
If you need more, group them behind a router tool rather than exposing all of them at once.
Framework or Your Own Loop
|
Agent framework |
Direct tool-calling loop |
| Time to first demo | Fast | An afternoon |
| Integrations | Large library included | You write what you need |
| Control of the loop | Through the framework's hooks | Total |
| Debugging a bad run | Through layers of abstraction | Read the message array |
| Cost predictability | Depends on hidden prompting | Every token is yours |
Frameworks earn their place when their integration library is the thing you actually want. When the hard part is the loop — retries, budgets, observability,
recovery — the abstraction is between you and the problem. The loop above is small enough that owning it costs less than learning someone else's escape hatches.
Making a Run Explainable
When an agent produces a wrong answer, the answer tells you nothing; the trajectory tells you everything. Persist the full message array for every run —
each model response, each tool call with its arguments, each result — keyed by a run id.
💡 Budget in tokens, not just steps
Step count is a poor proxy for cost, because the message history grows with every step and each call re-sends all of it. Track cumulative tokens and stop on
that too. It is the difference between an agent that occasionally costs more than expected and one that occasionally costs a hundred times more.
✅ Key takeaways
- ✓The agent is the loop. Owning it directly is roughly forty lines and buys full control.
- ✓Bound every run by both step count and token budget, and treat hitting the bound as a reportable outcome.
- ✓Feed tool errors back as messages. That is the entire self-correction mechanism.
- ✓Validate tool arguments with a schema. Structured output is still untrusted input.
- ✓Keep the toolset small and precisely described. Selection accuracy falls as the list grows.
- ✓Persist the trajectory. The message history is the only useful artefact when a run goes wrong.
See the same discipline applied to a narrower problem in the AI travel planner, where a
deterministic check validates every generated plan, and in
the product recommender, where the model ranks a set it is not allowed to invent.
Frequently asked questions
Should I use LangChain or build a custom TypeScript agent?
Use a framework when you want its integrations and are happy inside its abstractions. Build directly against the provider's tool-calling API when you need to control the loop, the retries and the observability — which most production agents eventually do.
How do you stop an autonomous agent looping forever?
Bound it explicitly: a maximum step count, a token budget, and a terminating condition checked after each step. An agent without a hard ceiling will find a way to spend it.
How do you make agent tool calls reliable?
Define tools with a strict schema, validate arguments before executing anything, and return structured errors the model can act on. A tool that throws an opaque exception gives the model nothing to recover from.
Home · Projects · Blog · Services · Résumé · Contact