M9:為什麼 proxy 要自己解析 ccr_retrieve?
headroom 的設計是這樣的:對話內容太長時,proxy 把舊訊息壓縮並存入 CCR store,
然後注入一個 ccr_retrieve 工具讓模型能主動召回。
這樣模型在需要時可以自己要求取回,而不是等 proxy 猜測何時該展開。
問題是:模型回傳「呼叫 ccr_retrieve」這個 tool_use 之後,誰來執行?
最直覺的答案是 client,但那意味著 client 要知道 CCR store 的存在、
知道怎麼查、知道把結果送回去。設計上 client 應該全程無感。
所以 proxy 得自己做:攔下 tool_use、查 store、組 follow-up 訊息、重問上游,
把真答案送回 client,client 看到的只是一個完整的回應。
server-side resolve loop 怎麼收
M9 的實作分兩部分。第一部分是 POST /ccr/retrieve 側信道端點:
與 /v1/messages 的 forward 路由共用同一個 Mutex<CcrStore>,
直接查 key,命中回 200 附上內容,查無回 404。
用顯式路由優先,不會誤觸上游的 forward fallback。
第二部分是回程的 resolve loop:gate 條件是「POST /v1/messages 且回應是 JSON(非 SSE)」,
只有在這個條件成立時才掃 tool_use。
extract_ccr_calls 的邏輯更嚴格:只有全部的 tool_use 都是 ccr_retrieve,
才接管處理;只要其中有 client 自己的工具,就整包放行、不動任何一個。
這個設計確保不會吃掉 client 原本期待自己處理的 tool_use。
確認攔截後:把原始 assistant turn 附上去、加一筆 user tool_result 給出 CCR 內容、
強制 stream: false 重呼上游,迴圈直到回應裡沒有 ccr_retrieve。
上限 MAX_RESOLVE_HOPS=8 防無限迴圈。普通 JSON 回應不重序列化,逐字節穿透。
這塊在學習版是全新設計——精讀解答本才發現,解答本的 Rust proxy 也沒在 proxy 層做 retrieve, store 只有 put/get,「honoring retrieval 在上游 Python runtime layer」。 M9 的 server-side resolve 是學習版自創,不是照抄。commit 和 memory 都標注了這一點, 避免日後誤以為這是原版設計。
精讀解答本:「SSE 不攔截」不是 gap,是哲學
M9 做完,下一個問題是 SSE 串流。工業版解答本的 proxy 在串流模式下不做 retrieve, 原本以為是遺漏、是下一個需要填的 gap。
精讀 sse/framing.rs(SseFramer)和 sse/anthropic.rs(AnthropicStreamState)之後,
答案變了。工業版的串流哲學是:bytes 原樣送給 client(byte-passthrough 是神聖不可侵犯的),
每個 chunk 另外 try_send 進一個有界 mpsc(深度 256),丟給獨立 spawned task 跑狀態機,
純粹做 telemetry 收集(usage / stop_reason / cache 指標)。
「never block on parser readiness、滿了就丟掉、不影響 byte path」。
SSE 串流裡之所以不攔截 ccr_retrieve,理由很清楚:
bytes 一旦送出去就收不回來了,要攔就必須 buffer 整個串流才能改寫,
那就不是串流了。工業版刻意不在串流路徑上做任何攔截,是架構抉擇,不是缺陷。
先讀才動手,省下了去做 buffer 攔截的那條死路。
M10:觀察式探針,不碰 byte path
精讀之後問攻法:要繞過 byte-passthrough 的限制硬做攔截,還是忠於解答本做旁觀?
選了觀察式——用 sse::SseCcrProbe 被動記錄串流裡出現的 ccr_retrieve,
一個 byte 都不動。
實作上,SseCcrProbe 復用 SseByteSplitter 切事件邊界,
按 index 追蹤 tool_use,input_json_delta 累積 partial JSON,
等到 content_block_stop 才解析出完整 key,認出 ccr_retrieve 就回報。
parse_event 把帶邊界的原始 chunk 剝成 (event_name, data),
多行 data 依 SSE 規範用 \n 串接——避免 Python 版本「跨 chunk 切到 emoji」的 1,946 次 parse fail 舊問題。
proxy 的 rechunk_sse 每個 chunk 同時餵進 probe 和往 client 送,
偵測到 ccr_retrieve 就記在 stderr 觀測線。
學習版是單 consumer 內聯(省掉 task/mpsc),
與工業版 spawned task + 有界 mpsc 的差異,誠實寫進 commit 和 README,不假裝等價。
這個 session 的方法論
精讀解答本能把「gap」重新定義成「抉擇」:原以為 SSE 不攔截是缺陷,讀完才知道工業版刻意如此。先讀才動手,省下做錯方向的成本。
無解答可抄時要明說:M9 的 server-side resolve 是學習版自創,解答本不在 proxy 層做這件事。commit 和 memory 標清楚,避免日後誤以為照抄。
誠實記差異:M10 單通道 tee vs 工業版 spawned task + 有界 mpsc,功能相近但架構不同。差異寫進 commit,不試圖掩蓋。
TDD 全程不例外:M9 新增 6 個測試(端點命中/404、resolve 閉環、foreign tool 放行、plain JSON byte-faithful、hop 上限);M10 新增 5 個測試(整餵/碎 chunk 跨邊界、ccr key 萃取、foreign tool、純文字、串流 byte-faithful)。全套 Rust 52 + Python 37,零 warning,parity 5 個 fixtures byte-for-byte 通過。