Skip to content
Coding challengeintermediate · 2 min read · 2026-11-01

Build a Guarded TypeScript Agent Loop

Learn how to model a small autonomous-agent loop with explicit planning, deterministic tool calls, and guardrails. The dsh-superagent-prototype repository is used only as a code-reading reference, not as a production framework.

What you will learn

  • After this lesson you can separate an agent loop into planning, tool execution, and guardrail checks.
  • After this lesson you can represent tool names, agent state, and plans with TypeScript types.
  • After this lesson you can stop unsafe or invalid agent behavior with simple deterministic guards.
  • After this lesson you can read an agent prototype by looking for boundaries rather than copying architecture blindly.

The core idea: an agent loop is a controlled state machine

An autonomous agent loop usually repeats three actions: decide the next step, call a tool, then update state. The dangerous version is to let the model decide everything at runtime with no limits. The safer version is to make the loop explicit and bounded.

In this lesson, the “agent” is deterministic so the code can run in a sandbox. In a real LLM-backed system, the planner might be an LLM call, but the guardrails should still live in ordinary code: allowed tools, valid inputs, maximum steps, and clear errors.

Use the repo as a reading reference, not a guarantee

The provided evidence identifies deepseek-ai/dsh-superagent-prototype as a TypeScript repository titled “Towards a More Autonomous Agent System”. That is enough reason to read it for ideas, but not enough reason to assume its architecture is production-ready for your workload.

When reading any agent prototype, look for concrete boundaries: - Where is the task converted into a plan? - Which tools are callable, and how are their inputs validated? - What state is carried between steps? - How does the loop stop when something goes wrong?

A practical TypeScript shape for agent boundaries

The following non-runnable sketch is not an API from the repository. It is a compact checklist you can use while reading agent code. If you can locate similar concepts in a codebase, you will understand its safety model faster.

Challenge: build a tiny guarded triage agent

Your task is to implement a small TypeScript agent that triages support tickets. The goal string is parsed, the planner returns a fixed list of tool calls, each tool updates state, and guardrails reject invalid goals, unknown tools, and plans that are too long.

This example deliberately avoids network calls, files, randomness, and current time. That makes the loop easy to test and reason about before replacing the deterministic planner with a real model.

Why this pattern scales

The important design choice is not the specific ticket example. It is the boundary: planning produces data, tools execute code, and guardrails decide whether an action is allowed.

As your agent grows, this separation lets you log plans, test tools independently, replay failures, and add policy checks without rewriting the entire loop.

Code

TypeScript · Agent boundary checklist sketchIllustrative · not executed
type ToolName = "search" | "summarize" | "writeReport";

type AgentStep = {
  tool: ToolName;
  input: unknown;
};

type AgentState = {
  goal: string;
  stepsRun: number;
  notes: string[];
};

// Reading checklist:
// 1. Where are AgentStep values created?
// 2. Who validates step.input?
// 3. What stops the loop?
// 4. What state is persisted or exposed?

This is an illustrative TypeScript sketch for code reading. It is not claimed to be part of dsh-superagent-prototype.

Common mistakes

  • ✕ Letting the planner call arbitrary functions by string name.

    ✓ Use a typed union such as type ToolName = "getTicket" | "summarize" | "priority" and reject any tool outside an allow-list before execution.

  • ✕ Treating an autonomous-agent repository as a proven production framework just because it is trending.

    ✓ Read it as a reference: identify its loop, tool boundaries, state model, and failure handling, then validate those ideas in your own small implementation.

  • ✕ Mixing planning, tool execution, and final response formatting in one large function.

    ✓ Keep these concerns separate so each one can be tested and guarded independently.

  • ✕ Running an unbounded loop until the agent “seems done”.

    ✓ Add a maximum step count and return a clear error when the plan exceeds it.

Your challenge

Implement a TypeScript function runAgent(goal: string): string for ticket triage. It must parse goals like triage:A-1, create an explicit plan, execute only allowed tools, stop on the first error, and print one result line for each built-in test case.

Interview questions

What is the difference between an agent planner and a tool executor?

The planner decides the next actions as data, such as a list of tool calls. The executor performs the selected tool code after validation and updates agent state.

Why should tool names be modeled as a TypeScript union?

A union gives compile-time restriction to known tool names. It reduces accidental misspellings and makes switch handling easier to audit.

Why is a runtime allow-list still useful if TypeScript already checks ToolName?

TypeScript types disappear at runtime. If a plan comes from JSON, an LLM, or another process, runtime validation is still required.

What is the purpose of a maximum-step guardrail?

It prevents runaway loops, unexpectedly large plans, and excessive tool use. It also makes failures easier to test and explain.

How would this design change with a real LLM planner?

The planner implementation could call the model, but its output should still be parsed into typed data and checked against the same guardrails before any tool runs.

Practice

  1. Add a new assignOwner tool to the challenge solution and include it in the plan after priority calculation.
  2. Change the guardrail so only high-priority tickets may be assigned to an on-call owner.
  3. Make invalid ticket IDs fail before planning, not during getTicket.

Quick quiz

Q1. Why does the example keep `plan` separate from `runTool`?
Q2. Which guardrail prevents the agent from running an unexpectedly long plan?
Q3. What does the `ToolName` union mainly protect against?
Q4. How should a trending agent prototype repository be used in this lesson’s approach?

Key takeaways

  • Model an agent loop as planning, guarded execution, and state updates.
  • Treat tool calls as typed data before running code.
  • Use both TypeScript unions and runtime allow-lists for tool safety.
  • Always bound the loop with a maximum step count.
  • Read agent prototypes for design ideas, then validate those ideas in small deterministic programs.

References

See the full TypeScript learning path