KRIT.JUNSREE PERSONAL WEBSITE
MEDIUM

Agent Loop คือ While Loop ที่คุณไม่ต้องเขียนเอง (Claude Agent SDK Part 2)

Rick Hightower อธิบาย agent loop ไม่ใช่ magic แต่คือ while loop ธรรมดา — turn คืออะไร, 5 message type (System/Assistant/User/Stream/Result), parallel-vs-sequential rule (readOnlyHint), 3 dial คุม loop (maxTurns/maxBudgetUsd/effort), และวิธีอ่าน result subtype (success/error_max_turns/error_max_budget_usd/error_during_execution/error_max_structured_output_retries) พร้อม gotcha ห้าม break early + guard None ใน Python. Part 2 ของ 14-part series

By krit.junsree@gmail.com ◆ Jul 10, 2026 ◆ 20 MIN READ Beginner

☰ ON THIS PAGE

Rick Hightower อธิบาย agent loop ของ Claude Agent SDK — ไม่ใช่ magic แต่คือ while loop ธรรมดา. Part 2 ของซีรีส์ 14 part. อธิบาย turn คืออะไร, 5 message type, parallel vs sequential, 3 dial คุม loop, และวิธีอ่าน result subtype เพื่อรู้ว่า agent หยุดเพราะอะไร

When your agent does something surprising at 2 a.m., ‘it just works’ is not a debugging strategy. You need to know exactly what the loop did.

เรียนรู้: turn คืออะไร + condition เดียวที่ end loop, 5 message type ที่ SDK emit, ทำไม tool บางตัวรัน parallel บางตัว sequential, 3 dial กัน loop runaway drain budget, วิธีอ่าน result subtype รู้แน่นอนว่า run หยุดเพราะอะไร

Part 2 ของ “Building with the Claude Agent SDK” — 14-part guide

Turn คืออะไร

Turn = 1 round trip ใน loop: Claude produce output ที่รวม tool call → SDK execute tool → result feed กลับให้ Claude อัตโนมัติ. Feedback เกิดโดยไม่ hand control กลับให้ code คุณ. Turn ทำต่อจนกว่า Claude produce output ที่ ไม่มี tool call = loop end + คุณได้ final result

Loop ไม่จบด้วย timer หรือ token count — จบเมื่อ Claude ตัดสินใจว่าไม่มีอะไรจะขอต่อ. ทุกอย่างอื่นคือ stage ใน turn หรือวิธี override natural stopping point

Walk through Concrete Run

Prompt: “Fix the failing tests in the auth module.” SDK ส่ง prompt → emit system message + session metadata → loop เริ่ม

  • Turn 1: Claude เรียก Bash รัน test suite. SDK emit assistant message + tool call, รัน command, emit user message + output (3 failure)
  • Turn 2: Claude เรียก Read source + test file. SDK คืน content
  • Turn 3: Claude เรียก Edit patch source + Bash re-run test. Pass ทั้ง 3
  • Turn 4 (final): Claude produce text-only response ไม่มี tool call — “Fixed the auth bug, all three tests pass now.” SDK emit final assistant message + result message

4 turn — 3 with tool call + 1 final text. คำถามธรรมดา “what files are here?” = 1 turn ของ Glob. Open-ended task “refactor auth module + update tests” = chain dozen ของ turn. Loop เดียวกัน length ต่างกัน — length เป็น data dependent ไม่ใช่ที่คุณตั้งล่วงหน้า

Built-in Tool ที่ได้ฟรี

  • Read — เปิด + อ่านไฟล์
  • Edit — modify ไฟล์ในที่
  • Bash — รัน shell command
  • Glob — หาไฟล์ตาม pattern
  • Grep — search content

Enable ด้วยการ list ใน options — Claude ตัดสินใจเรียกตัวไหนเมื่อไหร่. Sequence Bash → Read → Edit → Bash — คุณไม่ script. Claude เลือกทีละ turn ตามที่ tool คืน

Parallel vs Sequential

Rule เกี่ยวกับ safety:

  • Read-only tool ทำ parallel ได้ — อ่าน repo เดียวกัน 3 call พร้อมกันไม่เปลี่ยนอะไร: Read, Glob, Grep, MCP tool ที่ mark read-only
  • Tool ที่เปลี่ยน state = sequential เพื่อไม่เหยียบกัน: Edit, Write, Bash

Claude ขออ่าน 5 ไฟล์พร้อมกัน = fan out parallel · ขอ edit 3 ไฟล์ = sequential

Custom tool default = sequential. Opt-in parallel ด้วย mark readOnlyHint (field name เดียวกันทั้ง TS + Python). Read overlap ได้ · Write ต้องรอ turn

5 Message Type ที่ Loop Emit

  • SystemMessage — session lifecycle. อันแรก subtype "init" = session metadata + session ID สำหรับ resume
  • AssistantMessage — emit หลัง Claude response ทุกครั้ง รวม final text-only. มี text + tool-call block
  • UserMessage — emit หลัง tool execution — tool result ส่งกลับ Claude
  • StreamEvent — โผล่เมื่อเปิด partial message — raw streaming delta (token-by-token UI)
  • ResultMessage — mark end of loop + final text + token usage + cost + session ID

Handle ไหน = ขึ้นกับสิ่งที่ build. สนใจแค่ outcome = watch ResultMessage. อยาก progress view = handle AssistantMessage

python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage

async def main():
    async for message in query(
        prompt="Review the failing tests in buggy-shop and fix the bug.",
        options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"]),
    ):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if hasattr(block, "text"):
                    print(block.text)            # Claude's reasoning
                elif hasattr(block, "name"):
                    print(f"Tool: {block.name}")  # a tool being called
        elif isinstance(message, ResultMessage):
            print(f"Done: {message.subtype}")

asyncio.run(main())
typescript
//TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Review the failing tests in buggy-shop and fix the bug.",
  options: { allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"] },
})) {
  if (message.type === "assistant") {
    for (const block of message.message.content) {
      if (block.type === "text") {
        console.log(block.text);          // Claude's reasoning
      } else if (block.type === "tool_use") {
        console.log(`Tool: ${block.name}`); // a tool being called
      }
    }
  } else if (message.type === "result") {
    console.log(`Done: ${message.subtype}`);
  }
}

10 บรรทัด. แทนที่จะ dump ทุก raw message — sort by type + print สิ่งที่คนอ่านจริง. Firehose → progress log

Gotcha: อย่า break ออกจาก loop ตอนเห็น result message. Trailing system event (prompt suggestion) อาจมาหลัง result — iterate stream จนจบ. Break early = transport ค้าง + cleanup truncate

คุม Loop: Turns, Budget, Effort

Natural stopping point (Claude ไม่มีอะไรจะขอต่อ) OK สำหรับ task ที่ scope ชัด — ไม่ OK สำหรับ “improve this codebase” ตอน 3 โมงเช้าที่คุณจ่าย. 3 dial:

  • maxTurns — cap จำนวน tool-use round trip
  • maxBudgetUsd — หยุด loop เมื่อ running cost เกิน threshold
  • effort — คุมว่า Claude reason หนักแค่ไหนต่อ turn. Lower = ถูก + token น้อย · Higher = reason ลึกสำหรับ debug ยาก

Turn + budget default = no limit — สำคัญสุดที่ต้องเปลี่ยนก่อน ship

Production: set budget เป็น default. Turn cap ป้องกัน loop ที่ลอง — budget cap ป้องกัน loop ที่ลองแพง. failure mode ต่างกัน ต้องมีทั้งคู่

บอกว่าหยุดเพราะอะไร

Payoff ของทั้งบทความ — เมื่อ loop end, ResultMessage บอกอะไรผ่าน field subtype. อ่านถูกต่างระหว่าง “agent failed” vs “agent ชน turn limit + fix ได้ 2 จาก 3 test — resume ต่อ”

  • success — Claude จบ normally. subtype เดียวที่ final result text populate
  • error_max_turns — loop ชน maxTurns cap
  • error_max_budget_usd — ชน budget cap
  • error_during_execution — interrupt (API failure, cancel request)
  • error_max_structured_output_retries — structured-output validation fail เกิน retry limit

Habit สำคัญ: check subtype ก่อน อ่าน resultresult มีเฉพาะบน success. ทุก subtype (error) ยังมี total_cost_usd, usage, num_turns, session_id — track cost + resume ที่หยุดได้แม้ fail

python
# Python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

async def run_agent():
    session_id = None
    async for message in query(
        prompt="Find and fix the bug causing test failures in buggy-shop.",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
            max_turns=30,        # protect against a loop that keeps trying
            effort="high",       # thorough reasoning for real debugging
        ),
    ):
        if isinstance(message, ResultMessage):
            session_id = message.session_id  # available on every subtype
            if message.subtype == "success":
                print(f"Done: {message.result}")
            elif message.subtype == "error_max_turns":
                print(f"Hit turn limit. Resume session {session_id} to continue.")
            elif message.subtype == "error_max_budget_usd":
                print("Hit budget limit.")
            else:
                print(f"Stopped: {message.subtype}")
            if message.total_cost_usd is not None:
                print(f"Cost: ${message.total_cost_usd:.4f}")

asyncio.run(run_agent())

Field ที่ควรรู้: stop_reason report ว่า model หยุด generate ทำไมบน final turn. ค่าที่ต้อง catch = refusal — Claude ปฏิเสธ request — สถานการณ์ต่างจาก hit limit ต้อง handle ต่าง

Gotcha: ใน Python — total_cost_usd + usage = optional อาจเป็น None บาง error path. Guard ก่อน format ไม่งั้น failed run จะ throw error ที่ 2 ซ้อน error แรก

ทำวันนี้

  • เพิ่ม progress view — แทน raw message dump ด้วย loop ที่ handle AssistantMessage + ResultMessage ตาม type. 10 บรรทัด
  • Set ทั้ง 2 cap ก่อน shipmax_turns + maxBudgetUsd. failure mode ต่างกัน ใช้ทั้งคู่
  • Branch on result subtype — check subtype ก่อนอ่าน result. Handle error_max_turns + error_max_budget_usd ชัด. Capture session_id resume ได้
  • Guard optional field — Python null-check total_cost_usd + usage ก่อน format
  • Catch refusal แยก — check stop_reason = refusal. ต่างจาก hit limit

Takeaway

Agent loop ฟังดู frontier concept — mechanically เกือบน่าเบื่อ: send prompt → let model call tool ทีละ turn → stop เมื่อมันหยุดขอ → อ่าน result subtype รู้ว่าจบยังไง. แค่นั้น

Value ของ SDK ไม่ใช่ loop ฉลาด — คือคุณไม่ต้องเขียน/maintain มัน + inspect ทุก stage ได้เมื่อพัง

Precision นี้ทำให้ agent เป็นสิ่งที่ put on call ได้ — Surprise ตอน 2 AM = ไม่ใช่ magic แต่คือ while loop


แหล่งที่มา: “Claude Agent SDK Agent Loop: The Agent Loop Is Just a While Loop You Did Not Have to Write” (Part 2 of 14) โดย Rick Hightower ตีพิมพ์ 7 มิ.ย. 2026 บน Towards AI (Medium) · อ่านต้นฉบับ

Leave a Reply

Your email address will not be published. Required fields are marked *