先做一個假設:你拿最熟的 HTTP 去叫另一個 agent。設定檔寫一個網址,POST 一段 prompt,等它回話;等不下去就斷線,網址打不通就當它掛了。連線在,對方就在,這是我拿來對照的假設,不是哪個專案或哪個人的實際做法。

Synadia 的 Agent Protocol 沒有給你這個直覺。它的 README 說,Codex、OpenCode、Claude Code、OpenClaw、PI、Hermes、Flue,或你自己寫的 agent,都可以把自己註冊成一個叫 agents 的 NATS micro service。agent 的位址是一串主題名稱,不是指向某一台的網址;所有 agent 掛在同一個 NATS 上,靠主題互相找。

先交代我做了什麼:我讀了 README、核心規格(簽章那一章只讀到概要),以及 GitHub 和 npm 的元資料。沒有裝 SDK、沒有啟動 NATS server、沒有跑任何 nats 指令,SDK 原始碼也沒讀。下面每一句「規格要求」都只代表規格這樣寫,我沒有驗證實作是不是照做。

我想把同一個動作,「呼叫一個 agent 並等它做完」,拆成幾步,逐步對照上面那個 HTTP 假設和這份規格的做法。對照下來,每一步由誰負責都不一樣。

找人:規格禁止你自己拼地址

照 HTTP 假設,地址是你寫死的。規格的地址是一串主題:agents.{verb}.{agent}.{owner}.{name},verb 有 prompt、hb、status、attachments 四種。格式公開,你大概會想:自己拼一個 agents.prompt.claude-code.alice.main 送出去不就好了?

規格明講不行:”Callers therefore MUST learn endpoint subjects from $SRV.INFO.agents (§4); they MUST NOT construct endpoint subjects from identity alone.” 找人的流程變成先問,README 給的指令是 nats req '$SRV.INFO.agents' '' --replies=0 --timeout=2s,看誰回話。唯一的例外是心跳主題,它是固定的,所以可以直接訂 agents.hb.*.*.*,不用先問。

有個小坑。規格說 full-form 和縮寫的 subject token 在 NATS 萬用字元下不會互相比對,也就是 claude-code 和 cc 互相看不到(附錄 C 的慣例縮寫是 cc 對應 claude-code、oc 對應 openclaw)。換句話說,用其中一種寫法訂的萬用字元,比對不到另一種寫法註冊的 agent。

版本升級也是同一種安靜的壞法。0.3 把 verb 提到 agents. 之後,0.2 的格式是 agents.{a}.{o}.{n}。規格寫得很直接:”There is no v0.2 ↔ v0.3 compatibility shim.” 規格允許混版本部署,只是彼此在探索時看不到對方,因為 prompt 的主題根本不重疊。規格沒提錯誤訊息,所以我推測你只會看到一份少了人的名單(這是推測,我沒驗證)。

另外,同一組 agent、owner、name 如果跑了多個實例,prompt 端點註冊在名為 "agents" 的 queue group 裡,NATS micro 框架會做負載平衡。一則 prompt 只會有一個實例收到。

講完了嗎:沒人替你說「連線結束」

照 HTTP 假設,收尾是 TCP 和狀態碼替你講的。這份規格要求的做法瑣碎得多。

回應端第一則訊息必須是一個 ack 狀態 chunk,規格的限制是:”before any response or query chunks and before any work that introduces observable latency (e.g. a model call).” 也就是說,呼叫端要在模型還沒開始想之前,就先收到「有人接了」。

結尾則必須是一則零位元組、沒有任何 NATS header 的訊息:”Every response stream MUST end with a zero-byte body message carrying no NATS headers.” 出錯時更囉唆,先送一則帶 Nats-Service-Error-Code、Nats-Service-Error header 的訊息,再補一則空的終止,規格說 “Error-terminated streams always end with two messages”。

最有意思的是 NATS core 只保證至多一次:”NATS core messaging is at-most-once; individual chunks - including the terminator - MAY be lost silently.” 連那則空的終止訊息本身都可能悄悄掉掉。所以呼叫端必須替每條串流設 inactivity timeout,規格建議的值是 “60 seconds since the last observed chunk”。

差別像打電話和在辦公室喊話。打電話時,線路斷了你立刻知道;喊話時,喊出去沒有回音,你分不出是對方還沒說完,還是話根本沒送到。規格把「分不出來」這件事直接交給呼叫端,用計時器兜底。至於至少一次的投遞,它列在 out of scope 裡,寫的是 “JetStream-backed at-least-once streaming”。

不要了:這一步,規格沒有人兜底

照 HTTP 假設,要取消很簡單,斷線就好。

這份規格的原句是:”The protocol defines no cancellation signal.” 呼叫端要取消,做法是放掉 reply subscription。然後呢?”Agents are not notified of cancellation and continue until natural completion.”

你把話筒放下,對方沒有聽到掛斷聲,繼續把整段話講完,只是沒人在聽。規格只說會繼續到自然結束。繼續跑的這段時間會不會繼續花 token、繼續呼叫後面的工具,規格沒寫,下面是我的推讀:這要看那個 agent 當時接了什麼,有工具在手的話,我猜它會照樣用。

我的立場是:對會花錢的長任務,這個選擇我現階段不會直接拿去用,除非 host 端自己有上限,例如單次執行的時間或花費封頂。沒有取消訊號,取消這件事還是得有人處理,只是規格在這一步沒有指定任何人。

會讓我改變想法的條件有兩個。其一,我去讀 SDK 原始碼,發現 host 端本來就自帶逾時或預算上限,那麼規格不定義取消,只是把這一層留給實作,我的擔心就小很多。其二,規格走到 1.0 前加進取消訊號。規格自己說 0.x “is explicitly unstable”,所以第二種不是不可能。

中途被問一句:問的人有計時器,回的人沒有

照 HTTP 假設,一問一答很單純:我問、你答。agent 做到一半卻可能要反問人,例如「這個檔案要覆蓋嗎」。

規格的做法是 agent 在串流裡送一個 {"type":"query","data":{"id","reply_subject","prompt"}} chunk,呼叫端對那個 reply_subject 發布恰好一則回覆。聽起來合理,有意思的是逾時那一段。規格寫 “The agent chooses its own reply timeout.”,也就是等多久由 agent 決定。逾時之後 agent 有兩條路:(a) 以錯誤結束串流;(b) “proceed with a harness-defined default and continue emitting chunks. In case (b), the caller receives no signal.”

最後那句很重。如果走 (b),你這邊沒人通知你「剛剛那題沒人答,我自己選了預設值」。如果你答得比 agent 的逾時晚,你以為自己的回答正在被處理,其實對方早就往下跑了。這是規格白紙黑字允許的行為。我沒讀 SDK 原始碼,不知道有沒有實作這麼做,只知道它的安全性完全取決於那個預設值選得好不好。

把每一步的兜底擺在一起

呼叫端想知道的事 HTTP 式的假設(我的對照) 規格怎麼寫 誰兜底
對方在哪 設定檔裡的網址 先問 $SRV.INFO.agents,不得自行拼 subject 呼叫端
有人接了嗎 連線建起來 第一個 chunk 必須是 ack agent
講完了嗎 回應結束、連線關閉 零位元組終止訊息,但可能掉,呼叫端須設 inactivity timeout(建議 60 秒) 呼叫端
不要了 斷線 無取消訊號,放掉 subscription,agent 跑到自然結束 規格沒指定
對方活著嗎 請求打不通才知道 心跳,建議 30 秒,3 倍間隔沒聽到算離線 呼叫端
對方是誰 網址加憑證 交給 NATS 設定;可選 Agent-Sender 簽章;回話者的身分沒被證明 NATS 設定

表裡「活著嗎」那一列還沒講。心跳走 agents.hb.{agent}.{owner}.{name},pub/sub 沒有回覆,建議間隔 “30 seconds”,離線門檻是 “3 × interval_s since last observed heartbeat”,而且是依 instance_id 各自計算。規格對這個主題的描述是 “every agent instance emits at the configured cadence regardless of caller activity”,也就是一條固定頻率的廣播,不管有沒有人在呼叫。0.3 另外加了 status 端點,回覆格式和心跳相同,用意是讓呼叫端 “bootstrap their liveness tracker without waiting a full interval”。要看當下是否可達,規格建議用 micro service 的 ping(nats req '$SRV.PING.agents' '' --replies=0 --timeout=2s),而且先訂閱心跳再 ping。

對方是誰:身分這一步,缺口寫得很誠實

照 HTTP 假設,身分靠網址加憑證。這份規格的第一句話是:”Authentication and authorization are delegated to NATS server configuration.” 接著:”The protocol defines no pairing, allowlisting, or handshake.” 端到端加密也被排到之後的修訂,”deferred to a future revision”。

§13 有一個選用的擴充。呼叫端用 user NKEY 簽一個 Agent-Sender header,信任分成 verified、claimed、absent 三級,接收端的鐵律是 “A receiver MUST NOT grant anything on a claimed identity.”。已驗證的身分是 user,規格說 “The protocol grants nothing on account.”。

我最想讀的是這一句:這一章只證明呼叫端是誰給接收端看,沒有證明回話的人是誰給呼叫端看。原文是 “Any connection in the receiver’s account with the right permissions can join the agents queue group on a prompt subject and answer in the agent’s place”。同一個帳號裡,權限對的任何連線,都能加入那個 queue group,冒充 agent 回話。而且身分只有在 TLS 之下才有意義:”Identity is meaningful only over TLS to a server whose certificate the client verifies.”

簽章的細節,包括簽什麼內容、replay 視窗多長,我只讀到概要,不寫。

還在變的東西