
A Practical Guide to a 50-Line Deep Seek Agent in JavaScript
Why a 50-line agent is worth building
I like small agent loops because they surface failure modes quickly. You avoid framework glue, and you can see exactly where the model is allowed to act, where it has to stop, and what happens when its output is messy.
The useful part of a tiny agent is not that it is clever. It is that it makes the control flow plain:
- the model receives a goal
- it can ask for a tool
- your code runs the tool
- the model gets the result
- the loop ends when the model says it is done
That is enough to prototype internal helpers, safe lookup agents, or a command assistant for narrow tasks.
What the agent actually needs to do
Prompt, model call, and response parsing
A minimal agent needs three things:
- a system prompt that defines the job and the allowed tools
- a model call that returns structured text
- a parser that can tell whether the model is done or wants a tool
The parser is where people usually gloss over the hard part. In practice, the model will sometimes return valid JSON, sometimes extra text, and sometimes something that looks structured but still fails to parse. If your agent cannot recover from that, it breaks on the first odd response.
Tool boundary and when to stop
The other key decision is the tool boundary. The model should not be able to call arbitrary functions. I usually define a small set of named tools and a hard stop condition:
searchfor lookupread_filefor local inspectiondonefor final output
If the agent asks for anything else, reject it. If it loops more than a few times, stop it. That is the difference between a bounded workflow and an accidental runaway process.
Minimal JavaScript implementation
Environment setup and API client
Here is the shape I start with. It is intentionally plain.
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const tools = {
search: async ({ query }) => {
return `results for: ${query}`;
},
};
const systemPrompt = `You are a small agent.
Use only the available tools.
Return JSON with either { "tool": "...", "args": {...} } or { "final": "..." }.`;A small loop for reasoning and tool calls
The loop below is enough for a useful prototype. It keeps the state in memory and stops after a few rounds.
async function runAgent(task) {
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: task },
];
for (let step = 0; step < 5; step += 1) {
const response = await client.chat.completions.create({
model: "deepseek-chat",
messages,
temperature: 0,
});
const text = response.choices[0]?.message?.content ?? "";
let data;
try {
data = JSON.parse(text);
} catch {
messages.push({
role: "user",
content: "Your last reply was not valid JSON. Return only JSON.",
});
continue;
}
if (data.final) return data.final;
if (!tools[data.tool]) {
return `رفض unsupported tool: ${data.tool}`;
}
const result = await tools[data.tool](data.args || {});
messages.push({ role: "assistant", content: text });
messages.push({ role: "tool", content: String(result), tool_call_id: data.tool });
}
throw new Error("agent exceeded step limit");
}The parts that break first
Truncation, malformed JSON, and runaway loops
The first breakage is usually output truncation. If the model hits a token limit mid-object, JSON.parse fails and the whole loop becomes brittle. I treat that as normal, not exceptional.
The second breakage is malformed JSON. Even with a tight prompt, the model may add commentary or drop quotes. The fix is not “trust it more.” The fix is to reprompt with a very specific correction message and keep the loop bounded.
The third breakage is the agent that never decides. That happens when the prompt encourages reasoning but not termination. A hard maximum step count is mandatory.
| Failure mode | What you see | What to do |
|---|---|---|
| Truncation | JSON cut off halfway | Retry once, then stop |
| Malformed JSON | Extra prose or invalid syntax | Reprompt for strict JSON only |
| Runaway loop | Repeated tool requests | Cap steps and add stop rules |
Cost control and timeout handling
A tiny agent gets expensive when it retries too much or uses a chatty model for every step. I keep three limits in place:
- max steps per task
- max tool calls per task
- timeout for each tool call
If a tool hangs, the agent should fail closed. Do not let the model block your runtime waiting on a network call that never returns.
Hardening the pattern without bloating it
Schema checks and safe tool design
This is where the 50-line version usually becomes a real system. I would not add a big framework first. I would add schema checks.
Use a validator for the tool payload before execution. If the model says search, then args.query should be a string and nothing else. Keep tools narrow and deterministic. The more side effects a tool has, the more damage a bad prompt can do.
Do not expose filesystem writes, shell execution, or admin APIs in the first version. A bad agent prompt should be annoying, not destructive.
Logging, retries, and observability
You need logs for:
- the prompt sent
- the raw model output
- the parsed tool request
- the tool result
- why the loop stopped
Without that, debugging is guesswork. I also retry only on transient transport failures, not on every bad model response. If the model output is invalid three times in a row, that is a product signal, not a network glitch.
What to keep if you turn this into production code
Keep the loop small, but move the risky parts into explicit boundaries:
- strict input and output schemas
- a hard step limit
- per-tool timeouts
- a deny-by-default tool registry
- structured logs
- replayable traces for debugging
If you need user-facing reliability, add approval steps before any side-effecting tool runs. The model can suggest an action; your app should still own the final decision.
Conclusion
A 50-line agent is useful because it makes the control surface visible. You can see exactly where parsing fails, where tools need validation, and where the loop needs to stop. That clarity is the point.
If you keep the first version narrow, a small JavaScript agent is a solid way to learn the shape of model-driven workflows without hiding the hard parts behind a framework.


