第一部|最小閉環 · 第 2 章
訊息、Transcript 與 Provider Adapter
理解無狀態模型如何藉由 transcript 延續工作,以及 provider wire format 為何不能直接進入核心。
模型是無狀態的。Agent 能延續工作的唯一原因,是 host 保存了可重播的 transcript,並把它重新送給模型。
四種角色
| Role | 用途 | 關鍵限制 |
|---|---|---|
system |
Agent 身分、規則、skills metadata | 由 trusted host 組裝 |
user |
目標、後續指令、compact summary | 不可假裝成 tool result |
assistant |
final text 或 tool calls | 每個 tool call 必須有對應 result |
tool |
外部能力的觀察結果 | tool_call_id 必須精確配對 |
合法 Transcript 是硬限制
{"role":"assistant","content":null,"tool_calls":[
{"id":"call_1","type":"function","function":{"name":"read","arguments":"{\"path\":\"README.md\"}"}}
]}
{"role":"tool","tool_call_id":"call_1","content":"..."}
如果只保存 assistant tool call,卻在 crash 或取消時遺失 result,下一次 provider request 可能直接拒絕整段 transcript。這也是 tiny-agent 在 tool 中斷時,會替尚未開始或結果未知的 calls 補上固定 synthetic result 的原因。
Provider Adapter 必須正規化
OpenRouter wire response 可能包含 reasoning、refusal、reasoning_details 等
provider-specific 欄位。它們不能原樣寫入 closed Session schema。TypeScript 曾以
choice.message as Message 假裝完成轉型,結果 strict reducer 以 INVALID_FACT 拒絕。
function normalizeAssistantMessage(value: unknown): Message {
// 驗證 role、content 與每個 tool call
// 只建立 canonical fields,不回傳 provider object
return {
role: "assistant",
content,
...(toolCalls ? { tool_calls: toolCalls } : {}),
};
}
Type assertion 只影響編譯器,不會移除 runtime 欄位。正確 seam 是:
provider wire shape
→ validate + normalize
→ canonical assistant message
→ Agent / Session
Stop Reason 也是狀態
不能只看「有沒有 tool calls」。Provider 的 finish_reason 至少要映射成:
stop:正常 final answer。toolUse:tool calls 已完整產生,可進入 dispatch。length:輸出被截斷;其中 tool arguments 不可執行。- provider error:保存 failure,不偽造 assistant completion。
若 length 伴隨 tool calls,Agent 會寫入固定的 truncated synthetic results;這保護
transcript,也避免執行不完整 JSON arguments。
Usage 是每次 Physical Request 的帳
type Usage = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};
OpenRouter 的 prompt tokens可能已包含cache tokens,因此 tiny-agent正規化為:
input = prompt_tokens - cacheRead - cacheWrite
cacheHitRate = cacheRead / (input + cacheRead + cacheWrite)
model.completed.cacheHitRate 表示單次 request,可找出某一輪 cache 崩潰;run/session 的 rate 則由累積
counters 重算。不要把「最後一輪 rate」和「整段累積 tokens」混在一起。
親手驗證
用既有的offline regression test重播真實provider wire
shape;它會加入reasoning、refusal與額外tool-call欄位,然後確認Session只留下canonical
message:
npm --prefix typescript test -- \
--test-name-pattern="normalizes provider-only"
npm --prefix typescript test -- \
--test-name-pattern="rejects malformed provider assistant"
第一組應成功完成且可重新開啟Session;第二組應保存一次usage與failed
outcome,而不是讓INVALID_FACT洩漏到CLI。接著閱讀test中的mock response,逐欄標出哪些屬於provider
wire、哪些能進入canonical transcript。
合法transcript保證了資料「長什麼樣」,但沒保證「誰能把它變成真正的檔案讀寫或指令執行」——這是下一章的問題。