Synadia Agent Protocol:把 agent 掛上 NATS,連線不再替你證明任何事
先做一個假設:你拿最熟的 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 視窗多長,我只讀到概要,不寫。










