[{"data":1,"prerenderedAt":466},["ShallowReactive",2],{"blog-en-no-agent-message-in-the-wire":3,"header-blog-translations-/en/blog/no-agent-message-in-the-wire":463},{"id":4,"title":5,"author":6,"body":7,"date":447,"description":448,"draft":449,"extension":450,"image":451,"meta":452,"navigation":201,"path":453,"seo":454,"stem":455,"tags":456,"translationKey":461,"__hash__":462},"blog_en/blog/en/no-agent-message-in-the-wire.md","There Is No agent_message in the Chat Wire","Patrick Hofmann",{"type":8,"value":9,"toc":438},"minimark",[10,14,17,24,29,32,35,39,42,45,49,61,64,339,348,351,355,388,391,394,398,401,404,408,417,420,424,431,434],[11,12,13],"p",{},"Last night I sent an agent a friend request. Accepted, DM opened, typed \"hi\", four seconds later a reply came.",[11,15,16],{},"What happened at the wire level: the same WebSocket frame as for a human reply. The same DDISA auth. The same 1:1 DM container. The same Web Push that would have pinged me on the phone had I been offline.",[11,18,19,20],{},"I set out to build a web chat. What came out is evidence for a statement I've been carrying around for a while without being able to phrase it sharply: ",[21,22,23],"strong",{},"human and agent are the same at the protocol level.",[25,26,28],"h2",{"id":27},"how-it-was-before","How it was before",[11,30,31],{},"When I wanted to talk to one of my agents, it went over Telegram. Telegram DM in, Telegram DM out. That worked — and still does, for being on the move it's still the most convenient way.",[11,33,34],{},"But Telegram is a foreign layer. Bot identities, mention patterns, the Markdown-quirks universe, rate limits. Every one of these idiosyncrasies the agent stack had to know. And there was no browser path — when I sat at the computer and wanted to talk to an agent, I had to open Telegram Desktop next to it.",[25,36,38],{"id":37},"why-i-built-it","Why I built it",[11,40,41],{},"Not out of an architectural insight. I wanted a web chat. A browser tab where my agents stand as contacts, where I can open threads, where the last messages are visible without me having to resort to a Telegram search.",[11,43,44],{},"The architectural insight fell out during the build.",[25,46,48],{"id":47},"how-it-looks-now","How it looks now",[11,50,51,52,56,57,60],{},"Two components. ",[53,54,55],"code",{},"ape-chat"," as the foundation lib — the server that does identity, contacts, threads, messages, WebSocket, Web Push. And ",[53,58,59],{},"chat-bridge"," as a thin daemon — a WebSocket client that, for every CLI-based agent, translates incoming frames into CLI calls and posts the reply back. Clean separation, both usable independently.",[11,62,63],{},"The bridge daemon is at its core a loop:",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// chat-bridge: catch incoming, spawn pi, post reply\nws.on('message:new', async (msg) => {\n  if (msg.thread !== myDmWith(human)) return;\n  if (msg.from === self) return;\n\n  const reply = await spawn('pi', ['--print', msg.body]);\n  await ws.send('message:post', {\n    thread: msg.thread,\n    body: reply.stdout,\n  });\n});\n","typescript","",[53,72,73,82,132,169,196,203,257,284,302,319,329],{"__ignoreMap":70},[74,75,78],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sHwdD","// chat-bridge: catch incoming, spawn pi, post reply\n",[74,83,85,89,93,97,100,103,107,109,112,116,119,123,126,129],{"class":76,"line":84},2,[74,86,88],{"class":87},"sTEyZ","ws",[74,90,92],{"class":91},"sMK4o",".",[74,94,96],{"class":95},"s2Zo4","on",[74,98,99],{"class":87},"(",[74,101,102],{"class":91},"'",[74,104,106],{"class":105},"sfazB","message:new",[74,108,102],{"class":91},[74,110,111],{"class":91},",",[74,113,115],{"class":114},"spNyl"," async",[74,117,118],{"class":91}," (",[74,120,122],{"class":121},"sHdIc","msg",[74,124,125],{"class":91},")",[74,127,128],{"class":114}," =>",[74,130,131],{"class":91}," {\n",[74,133,135,139,142,144,146,149,152,155,157,160,163,166],{"class":76,"line":134},3,[74,136,138],{"class":137},"s7zQu","  if",[74,140,118],{"class":141},"swJcz",[74,143,122],{"class":87},[74,145,92],{"class":91},[74,147,148],{"class":87},"thread",[74,150,151],{"class":91}," !==",[74,153,154],{"class":95}," myDmWith",[74,156,99],{"class":141},[74,158,159],{"class":87},"human",[74,161,162],{"class":141},")) ",[74,164,165],{"class":137},"return",[74,167,168],{"class":91},";\n",[74,170,172,174,176,178,180,183,186,189,192,194],{"class":76,"line":171},4,[74,173,138],{"class":137},[74,175,118],{"class":141},[74,177,122],{"class":87},[74,179,92],{"class":91},[74,181,182],{"class":87},"from",[74,184,185],{"class":91}," ===",[74,187,188],{"class":87}," self",[74,190,191],{"class":141},") ",[74,193,165],{"class":137},[74,195,168],{"class":91},[74,197,199],{"class":76,"line":198},5,[74,200,202],{"emptyLinePlaceholder":201},true,"\n",[74,204,206,209,212,215,218,221,223,225,228,230,232,235,237,240,242,244,247,249,252,255],{"class":76,"line":205},6,[74,207,208],{"class":114},"  const",[74,210,211],{"class":87}," reply",[74,213,214],{"class":91}," =",[74,216,217],{"class":137}," await",[74,219,220],{"class":95}," spawn",[74,222,99],{"class":141},[74,224,102],{"class":91},[74,226,227],{"class":105},"pi",[74,229,102],{"class":91},[74,231,111],{"class":91},[74,233,234],{"class":141}," [",[74,236,102],{"class":91},[74,238,239],{"class":105},"--print",[74,241,102],{"class":91},[74,243,111],{"class":91},[74,245,246],{"class":87}," msg",[74,248,92],{"class":91},[74,250,251],{"class":87},"body",[74,253,254],{"class":141},"])",[74,256,168],{"class":91},[74,258,260,263,266,268,271,273,275,278,280,282],{"class":76,"line":259},7,[74,261,262],{"class":137},"  await",[74,264,265],{"class":87}," ws",[74,267,92],{"class":91},[74,269,270],{"class":95},"send",[74,272,99],{"class":141},[74,274,102],{"class":91},[74,276,277],{"class":105},"message:post",[74,279,102],{"class":91},[74,281,111],{"class":91},[74,283,131],{"class":91},[74,285,287,290,293,295,297,299],{"class":76,"line":286},8,[74,288,289],{"class":141},"    thread",[74,291,292],{"class":91},":",[74,294,246],{"class":87},[74,296,92],{"class":91},[74,298,148],{"class":87},[74,300,301],{"class":91},",\n",[74,303,305,308,310,312,314,317],{"class":76,"line":304},9,[74,306,307],{"class":141},"    body",[74,309,292],{"class":91},[74,311,211],{"class":87},[74,313,92],{"class":91},[74,315,316],{"class":87},"stdout",[74,318,301],{"class":91},[74,320,322,325,327],{"class":76,"line":321},10,[74,323,324],{"class":91},"  }",[74,326,125],{"class":141},[74,328,168],{"class":91},[74,330,332,335,337],{"class":76,"line":331},11,[74,333,334],{"class":91},"}",[74,336,125],{"class":87},[74,338,168],{"class":91},[11,340,341,343,344,347],{},[53,342,227],{}," here is the CLI agent that drives a ChatGPT subscription backend via ",[53,345,346],{},"litellm",". But it could just as well be a Claude CLI or an own script spitting out Markov chains. The bridge doesn't care what it spawns.",[11,349,350],{},"Round trip: four seconds, dominated by the LLM call. The WebSocket latency and the bridge loop are in the noise.",[25,352,354],{"id":353},"whats-in-the-wire","What's in the wire",[11,356,357,358,368,369,372,373,375,376,375,379,375,381,375,384,387],{},"The central thing: ",[21,359,360,361,364,365,92],{},"there is no ",[53,362,363],{},"agent_message"," type next to ",[53,366,367],{},"user_message"," The schema says ",[53,370,371],{},"Message",". The fields are ",[53,374,182],{},", ",[53,377,378],{},"to",[53,380,251],{},[53,382,383],{},"timestamp",[53,385,386],{},"signature",". A message from human to human has the same form as one from agent to human, as one from agent to agent.",[11,389,390],{},"The only difference is in the sender's key material. Humans sign with their passkey, agents with an Ed25519 key issued at enrollment. That doesn't change the form of the message — only the identity the server can compute back when it checks the signature.",[11,392,393],{},"And precisely because the protocol doesn't distinguish between the two, the next phase practically fell out by itself: multiple threads per DM. You can hold parallel conversations to the same agent — the way you can run parallel topics with a human you talk to about the tax return and about the weekend at the same time. I didn't have to design that. It was already there, because the DM containers for 1:1 humans work exactly the same.",[25,395,397],{"id":396},"what-fell-away","What fell away",[11,399,400],{},"A parallel agent protocol. I could have built it — own routes, own format, own container, own mention semantics. More code, more drift between the two paths. Every new feature idea I'd have had to build twice: once for human-human, once for human-agent.",[11,402,403],{},"I didn't. Not out of discipline, but because while building it became obvious that there's nothing that would have to keep the two paths apart. A message is a message.",[25,405,407],{"id":406},"cousin","Cousin",[11,409,410,411,416],{},"A few days ago I wrote ",[412,413,415],"a",{"href":414},"/en/blog/push-substitutes-push","push-substitutes-push"," — Web Push for agent approvals, instead of a Telegram bot reply. The same move on a different axis: take the out-of-band notification away from a third party, replace it with native infrastructure that has exactly the same property (push to the phone), only without the third-party layer.",[11,418,419],{},"What happens here is the next layer in the same line. Before: Telegram-out for approvals. Then: Web Push for approvals. Now: web chat for the generic conversation, with the same Web Push that took over the approval replacement, when I'm not in the browser tab right now. Three steps, one direction — take away the third-party layer between me and my agents without losing its usable properties.",[25,421,423],{"id":422},"closing","Closing",[11,425,426,427,430],{},"The thesis that agent and human are the same at the protocol level can't be proven in a conference slide. It proves itself while building — namely by the fact that no special case arises. If at any point while writing the chat server I'd had to build in an ",[53,428,429],{},"if (sender.isAgent)"," branch, the thesis would have been refuted. I didn't.",[11,432,433],{},"Four seconds, one message, one reply. Who the sender was, the server can read off the signature if it has to. The container doesn't need to know.",[435,436,437],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":70,"searchDepth":84,"depth":84,"links":439},[440,441,442,443,444,445,446],{"id":27,"depth":84,"text":28},{"id":37,"depth":84,"text":38},{"id":47,"depth":84,"text":48},{"id":353,"depth":84,"text":354},{"id":396,"depth":84,"text":397},{"id":406,"depth":84,"text":407},{"id":422,"depth":84,"text":423},"2026-05-04","chat.openape.ai is live. Friend request to an agent, open a DM thread, type a message — at the wire level nothing differs from a 1:1 with a human.",false,"md",null,{},"/blog/en/no-agent-message-in-the-wire",{"title":5,"description":448},"blog/en/no-agent-message-in-the-wire",[457,458,459,460],"OpenApe","AI Agents","Architecture","Building in Public","no-agent-message-in-the-wire","nvxKE-iWbNvd4k-OHA6NvPjDKZDSJjjgD74YIQ8twI4",{"en":464,"de":465},"/en/blog/no-agent-message-in-the-wire","/de/blog/im-chat-wire-gibt-es-keine-agent-message",1779001886923]