T1 — ReAct loop với finish-reason-aware exit
finish_reason SAU khi tool execute xong — không phải trước — để tránh bỏ rơi tool call pending khi model trả stop không đúng thời điểm.Tổng quan Overview
ReAct (Reasoning + Acting) là pattern phổ biến nhất cho agent harness hiện đại: model suy luận, gọi tool, nhận kết quả, tiếp tục suy luận. Vòng lặp đơn giản về khái niệm — nhưng ẩn chứa nhiều edge case nguy hiểm khi làm việc với streaming API thực tế.
Điểm khác biệt cốt lõi của opencode: kiểm tra exit condition SAU khi tool đã execute xong,
không phải ngay khi nhận finish_reason từ model. Nhiều harness khác break ngay
khi thấy finishReason === "stop" — điều này tạo ra silent corruption khi model
vừa gọi tool VÀ vừa trả stop trong cùng một response (behavior có ở một số provider).
finish_reason: "stop" trong khi tool_calls array vẫn có
dữ liệu. Naive harness sẽ exit và bỏ rơi tool calls đó.
yield*) thay vì
async/await thuần — cho phép interrupt, retry, và structured concurrency mà không phải viết
try/catch phức tạp.
Phân tích code chi tiết Anatomy
Điều kiện thoát vòng lặp — ba guard cùng lúc
opencode kiểm tra ba điều kiện ĐỒNG THỜI trước khi break. Thiếu bất kỳ điều kiện nào cũng có thể gây ra lỗi:
session/prompt.ts — exit condition (runLoop)
{`
while (true) {
yield* status.set(sessionID, { type: "busy" })
let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
// Guard 1: lastAssistant?.finish — model đã có response chưa?
// Guard 2: !["tool-calls"].includes(lastAssistant.finish) — không còn tool pending?
// Guard 3: !hasToolCalls — không có tool call nào trong current response?
// Guard 4: lastUser.id < lastAssistant.id — assistant đã respond sau user chưa?
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastUser.id < lastAssistant.id
) {
yield* slog.info("exiting loop")
break
}
step++
const result = yield* handle.process(streamInput)
if (result === "stop") return "break"
if (result === "compact") yield* compaction.create(...)
}
`}
Điều kiện lastAssistant?.finish kiểm tra xem có phản hồi từ model chưa (tránh exit
ngay vòng đầu). Điều kiện !["tool-calls"].includes(...) chỉ exit khi finish reason
KHÔNG phải tool-calls. !hasToolCalls là double-check xem response hiện tại
có chứa tool call không. Cả bốn điều kiện phải thỏa mãn.
Step counter và compaction trigger
Biến step được tăng sau mỗi iteration (không phải trước exit check).
Nó phục vụ hai mục đích: (1) cung cấp context cho T5 (synthetic reminder chỉ chạy khi
step > 1), và (2) trigger compaction check sau mỗi bước.
session/prompt.ts — sau exit check
{`
step++ // tăng SAU khi check exit, không trước
const result = yield* handle.process(streamInput)
// result có thể là:
// "stop" → user cancel hoặc error không thể recover
// "compact" → context overflow, cần compact trước khi tiếp
// undefined → normal, tiếp tục loop
`}Effect Generator và structured concurrency
Toàn bộ loop chạy trong Effect Generator function (function* với yield*).
Điều này cho phép Effect runtime quản lý lifetime, interrupt, và resource cleanup mà không cần
code boilerplate.
Effect Generator pattern
{`
// Mỗi yield* là một "suspension point" — Effect có thể interrupt tại đây
// nếu user cancel hoặc có lỗi unrecoverable
yield* status.set(sessionID, { type: "busy" })
yield* slog.info("loop", { step })
`}Tương tác với các kỹ thuật khác Interaction
T1 là orchestrator trung tâm — nó gọi hầu hết các kỹ thuật khác trong nhóm A:
T2 (streaming demultiplexer) được gọi bởi handle.process() — T1 không trực tiếp
đụng vào stream. T3 (Deferred coordination) chạy bên trong T2 và T4 (interruption cleanup)
bọc ngoài cả pipeline. T5 (synthetic reminder) chạy trong build-messages phase trước khi
T1 gửi request mới. T6 (doom loop) feed signal vào T2, và T2 trả về flag cho T1 để stop.
Failure modes Failures
Failure 1: Exit sớm với tool calls pending
Đây là lỗi phổ biến nhất. Naive harness break ngay khi nhận finish_reason === "stop"
mà không kiểm tra xem có tool call chưa được execute không.
Naive implementation — sai
{`
// WRONG: exit ngay khi stop, không kiểm tra tool_calls
while (true) {
const resp = await llm.call(messages)
if (resp.finishReason === "stop") break // có thể bỏ rơi tool calls!
for (const tc of resp.toolCalls) {
messages.push(await executeToolCall(tc))
}
}
// Nếu model trả finish_reason=stop VÀ tool_calls=[...] cùng lúc
// → tool không được chạy → model nhận context thiếu
`}Failure 2: Không có doom loop guard
Nếu không tích hợp với T6, vòng lặp có thể chạy vô hạn khi model bị kẹt. opencode đặt
doom loop detection trong T2, nhưng T1 phải handle signal stop từ T2 (result === "stop").
Thiếu stop signal handling
{`
// Nếu handle.process() không trả về "stop" khi doom loop detected
// → T1 tiếp tục gọi model vô hạn
const result = yield* handle.process(streamInput)
// PHẢI check:
if (result === "stop") return "break"
`}Failure 3: Step counter không reset khi retry
Nếu step không được reset đúng cách sau retry, các kỹ thuật phụ thuộc vào
step (như T5) sẽ bị confused. opencode handle điều này bằng cách reset
context đầy đủ trong cleanup.
Step counter và retry
{`
// Nếu retry không reset step → T5 sẽ wrap ALL user messages
// ngay từ step đầu, không phải chỉ step > 1
// → prompt bloated không cần thiết
// opencode: step là local variable trong runLoop scope
// mỗi session invocation bắt đầu từ 0
let step = 0
`}So sánh với các harness khác Compare
| Harness | Loop style | Finish-reason guard | Điểm yếu |
|---|---|---|---|
| opencode | Effect Generator while(true) | Complex state machine, Effect learning curve | |
| Claude Code | Recursive call style | Khó trace control flow qua recursion | |
| Aider | Prompt-first loop | Gửi xong rồi mới check tools — sequential | |
| Cline | Sequential, one tool mỗi lần | Không hỗ trợ parallel tool execution | |
| AutoGPT | while loop + max_iterations | Count-based, không content-aware |
opencode là harness duy nhất trong danh sách batch nhiều tool calls trong 1 step VÀ có explicit finish-reason guard. Claude Code dùng recursion có thể làm điều tương đương nhưng flow ít tường minh hơn.
Implementation recipe Recipe
Minimal TypeScript implementation của finish-reason-aware loop, không cần Effect dependency:
finish-reason-aware-loop.ts
{`
interface LLMResponse {
finishReason: "stop" | "tool_calls" | "length" | "end_turn"
toolCalls: ToolCall[]
content: string
}
interface ToolCall {
id: string
name: string
input: unknown
}
async function runFinishAwareLoop(
messages: Message[],
callLLM: (msgs: Message[]) => Promise<LLMResponse>,
executeTool: (tc: ToolCall) => Promise<unknown>,
options = { maxSteps: 50 }
) {
let lastResponse: LLMResponse | null = null
let step = 0
while (true) {
// EXIT CHECK: tất cả điều kiện phải thỏa
const hasToolCalls = lastResponse?.toolCalls.length ?? 0 > 0
if (
lastResponse !== null &&
lastResponse.finishReason !== "tool_calls" &&
!hasToolCalls
) {
break
}
if (step >= options.maxSteps) {
throw new Error("Max steps exceeded — possible doom loop")
}
step++
// Gọi model
const response = await callLLM(messages)
messages.push({ role: "assistant", content: response.content })
lastResponse = response
// Execute ALL tool calls — không skip ngay cả khi finishReason=stop
if (response.toolCalls.length > 0) {
const results = await Promise.all(
response.toolCalls.map(async (tc) => ({
toolCallId: tc.id,
result: await executeTool(tc),
}))
)
messages.push({ role: "tool", toolResults: results })
}
}
return lastResponse
}
`}Tham khảo Refs
- IBM — What is a ReAct Agent? · Giải thích Reasoning+Acting pattern cơ bản
- ReAct: Synergizing Reasoning and Acting in Language Models (paper gốc) · Yao et al., 2022
- Martin Fowler — Harness Engineering · Overview về agent harness design
- Vercel AI SDK — streamText docs · finish_reason values và lifecycle hooks
- Anthropic — Claude Code auto mode · Loop design insights từ production harness
- anomalyco/opencode GitHub repo · Source code tham chiếu chính —
session/prompt.ts