One safety rule
Every agent loop needs a budget before it starts. If the budget is used up, stop and return a safe status instead of asking the model one more time.
Do not copy older tutorials with unbounded while loops. Current agent frameworks expose max-turn, model-call, and tool-call limits because runaway loops are a normal failure mode.
What runaway means
A runaway agent keeps calling tools without getting closer to a final answer. It may repeat the same search, retry a broken API call, or ask the model again after every weak result.
In a hackathon project, this can burn credits, spam APIs, hit rate limits, or make your demo hang in front of judges.
Set four budgets
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Guard the loop
This is a plain JavaScript pattern you can use even if your SDK has its own max-turn option. The idea is the same: check the budget before each model call and before each tool call.
const limits = {
maxTurns: 8,
maxToolCalls: 12,
maxMs: 30000,
maxFailures: 2
};
async function runAgent(userInput) {
const startedAt = Date.now();
const state = {
turns: 0,
toolCalls: 0,
failures: 0,
messages: [{ role: "user", content: userInput }]
};
while (true) {
if (state.turns >= limits.maxTurns) {
return { status: "stopped", reason: "max_turns", state };
}
if (state.toolCalls >= limits.maxToolCalls) {
return { status: "stopped", reason: "max_tool_calls", state };
}
if (Date.now() - startedAt > limits.maxMs) {
return { status: "stopped", reason: "timeout", state };
}
if (state.failures >= limits.maxFailures) {
return { status: "stopped", reason: "too_many_failures", state };
}
state.turns += 1;
const response = await callModel(state.messages);
if (response.final) {
return { status: "completed", answer: response.final, state };
}
for (const call of response.toolCalls) {
if (state.toolCalls >= limits.maxToolCalls) {
return { status: "stopped", reason: "max_tool_calls", state };
}
state.toolCalls += 1;
try {
const result = await executeTool(call);
state.messages.push({ role: "tool", name: call.name, content: JSON.stringify(result) });
} catch (err) {
state.failures += 1;
state.messages.push({ role: "tool", name: call.name, content: JSON.stringify({ error: err.message }) });
}
}
}
}Use SDK limits too
If your SDK has built-in turn limits, use them. OpenAI’s JS runner throws MaxTurnsExceededError when maxTurns is reached, unless maxTurns is null. OpenAI’s Python runner raises MaxTurnsExceeded when max_turns is exceeded, and max_turns=None disables the limit.
Keep your own app-level limits as well. They make your repo easier to review because judges can see exactly how your agent stops.
Null is not safe
A common mistake is leaving limits empty while testing and forgetting to restore them. A demo prompt like “keep trying until it works” can then run for a long time.
Avoid unlimited settings unless you are running a controlled local experiment. For a public hackathon repo, choose small defaults and document them.
Watch concurrency
Parallel tool calls can be useful, but they also multiply damage. If five tool calls run at once and each retries, your simple agent becomes noisy fast.
Use concurrency controls when available. The OpenAI JS SDK documents toolExecution.maxFunctionToolConcurrency, and the Python SDK separates provider-side parallel_tool_calls from SDK-side max_function_tool_concurrency.
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done