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 เรียก
Readsource + test file. SDK คืน content - Turn 3: Claude เรียก
Editpatch source +Bashre-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 commandGlob— หาไฟล์ตาม patternGrep— 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 สำหรับ resumeAssistantMessage— emit หลัง Claude response ทุกครั้ง รวม final text-only. มี text + tool-call blockUserMessage— emit หลัง tool execution — tool result ส่งกลับ ClaudeStreamEvent— โผล่เมื่อเปิด 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
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
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 tripmaxBudgetUsd— หยุด loop เมื่อ running cost เกิน thresholdeffort— คุมว่า 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 เดียวที่ finalresulttext populateerror_max_turns— loop ชนmaxTurnscaperror_max_budget_usd— ชน budget caperror_during_execution— interrupt (API failure, cancel request)error_max_structured_output_retries— structured-output validation fail เกิน retry limit
Habit สำคัญ: check subtype ก่อน อ่าน result — result มีเฉพาะบน success. ทุก subtype (error) ยังมี total_cost_usd, usage, num_turns, session_id — track cost + resume ที่หยุดได้แม้ fail
# 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 ก่อน ship —
max_turns+maxBudgetUsd. failure mode ต่างกัน ใช้ทั้งคู่ - Branch on result subtype — check
subtypeก่อนอ่านresult. Handleerror_max_turns+error_max_budget_usdชัด. Capturesession_idresume ได้ - 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) · อ่านต้นฉบับ