[{"data":1,"prerenderedAt":1408},["ShallowReactive",2],{"blog:/blog/multi-agent-coordination-layer":3},{"id":4,"title":5,"author":6,"body":7,"category":1397,"date":1398,"description":1399,"excerpt":1400,"extension":1401,"meta":1402,"navigation":141,"path":1403,"readTime":1404,"seo":1405,"stem":1406,"__hash__":1407},"blog/blog/multi-agent-coordination-layer.md","Why Multi-Agent Systems Need a Coordination Layer","Henco Burger",{"type":8,"value":9,"toc":1390},"minimark",[10,14,17,22,25,185,188,191,195,198,205,211,227,241,245,248,288,314,318,325,1332,1335,1365,1369,1372,1377,1380,1386],[11,12,13],"p",{},"The AI agent landscape has a gap. We have incredible LLMs, solid frameworks for building individual agents (LangChain, CrewAI, AutoGen), and growing demand for multi-agent systems that can tackle complex workflows. What we don't have is good coordination infrastructure.",[11,15,16],{},"Most teams building multi-agent systems end up reinventing the same coordination plumbing: task queues on Redis, state management on a database, custom WebSocket servers for real-time updates, and bespoke logging for observability. This is undifferentiated heavy lifting that distracts from the actual agent logic.",[18,19,21],"h2",{"id":20},"the-problem-with-direct-orchestration","The Problem with Direct Orchestration",[11,23,24],{},"The simplest multi-agent pattern is direct orchestration: a coordinator calls agents in sequence, passing outputs from one to the next. It looks clean in a demo.",[26,27,28],"code-tabs",{},[29,30,36],"pre",{"className":31,"code":32,"filename":33,"language":34,"meta":35,"style":35},"language-typescript shiki shiki-themes github-light github-dark","// The naive approach: direct function calls\nconst research = await researchAgent.run(query);\nconst draft = await draftAgent.run(research);\nconst review = await reviewAgent.run(draft);\nconst final = await publishAgent.run(review);\n\n// Problems:\n// - Sequential, no parallelism\n// - No visibility into what's happening\n// - No way to retry a single step\n// - No shared state between agents\n// - No human approval before publish\n// - If a worker crashes mid-step, the step is lost\n","TypeScript","typescript","",[37,38,39,48,76,96,116,136,143,149,155,161,167,173,179],"code",{"__ignoreMap":35},[40,41,44],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sJ8bj","// The naive approach: direct function calls\n",[40,49,51,55,59,62,65,69,73],{"class":42,"line":50},2,[40,52,54],{"class":53},"szBVR","const",[40,56,58],{"class":57},"sj4cs"," research",[40,60,61],{"class":53}," =",[40,63,64],{"class":53}," await",[40,66,68],{"class":67},"sVt8B"," researchAgent.",[40,70,72],{"class":71},"sScJk","run",[40,74,75],{"class":67},"(query);\n",[40,77,79,81,84,86,88,91,93],{"class":42,"line":78},3,[40,80,54],{"class":53},[40,82,83],{"class":57}," draft",[40,85,61],{"class":53},[40,87,64],{"class":53},[40,89,90],{"class":67}," draftAgent.",[40,92,72],{"class":71},[40,94,95],{"class":67},"(research);\n",[40,97,99,101,104,106,108,111,113],{"class":42,"line":98},4,[40,100,54],{"class":53},[40,102,103],{"class":57}," review",[40,105,61],{"class":53},[40,107,64],{"class":53},[40,109,110],{"class":67}," reviewAgent.",[40,112,72],{"class":71},[40,114,115],{"class":67},"(draft);\n",[40,117,119,121,124,126,128,131,133],{"class":42,"line":118},5,[40,120,54],{"class":53},[40,122,123],{"class":57}," final",[40,125,61],{"class":53},[40,127,64],{"class":53},[40,129,130],{"class":67}," publishAgent.",[40,132,72],{"class":71},[40,134,135],{"class":67},"(review);\n",[40,137,139],{"class":42,"line":138},6,[40,140,142],{"emptyLinePlaceholder":141},true,"\n",[40,144,146],{"class":42,"line":145},7,[40,147,148],{"class":46},"// Problems:\n",[40,150,152],{"class":42,"line":151},8,[40,153,154],{"class":46},"// - Sequential, no parallelism\n",[40,156,158],{"class":42,"line":157},9,[40,159,160],{"class":46},"// - No visibility into what's happening\n",[40,162,164],{"class":42,"line":163},10,[40,165,166],{"class":46},"// - No way to retry a single step\n",[40,168,170],{"class":42,"line":169},11,[40,171,172],{"class":46},"// - No shared state between agents\n",[40,174,176],{"class":42,"line":175},12,[40,177,178],{"class":46},"// - No human approval before publish\n",[40,180,182],{"class":42,"line":181},13,[40,183,184],{"class":46},"// - If a worker crashes mid-step, the step is lost\n",[11,186,187],{},"This approach breaks down as soon as you need any of the following: parallel execution across multiple workers, visibility into what each agent is doing, the ability to retry a single failed step, shared state that all agents can read and write, human approval gates before high-stakes actions, or resilience when a worker process crashes.",[11,189,190],{},"These aren't edge cases. They're table stakes for production multi-agent systems.",[18,192,194],{"id":193},"why-pubsub-is-the-right-primitive","Why Pub/Sub Is the Right Primitive",[11,196,197],{},"Publish/subscribe messaging solves the coordination problem at the right level of abstraction. Here's why:",[11,199,200,204],{},[201,202,203],"strong",{},"Decoupling."," Publishers don't know about subscribers. An orchestrator dispatches a task to a topic. Any worker subscribed to that topic with matching capabilities picks it up. You can add, remove, or restart workers without changing the orchestrator.",[11,206,207,210],{},[201,208,209],{},"Fan-out."," A single event can reach multiple interested parties: the worker that processes it, the dashboard that displays it, the logger that records it, and the human who might need to approve the result.",[11,212,213,216,217,220,221,226],{},[201,214,215],{},"Durable delivery for workers."," Put workers in a load-balanced group, connected as ",[37,218,219],{},"agent"," actors (the actor type whose broker session persists), and the broker hands each task to one member of the group and holds the tasks dispatched while the group was scaled to zero until a member comes back. Tasks dispatched while no worker was listening are not lost; see ",[222,223,225],"a",{"href":224},"/docs/concepts/replay","Replay and Durable Delivery"," for the precise contract, including what it does not cover. Shared state is published retained, so an agent that joins late reads the current plan instead of waiting for the next change. None of this is exactly-once delivery: a task can, in principle, be seen twice, so keep task handlers idempotent.",[11,228,229,232,233,236,237,240],{},[201,230,231],{},"Presence."," Built-in presence tracking means you always know which agents are online and what each one can do, because every agent advertises its capabilities as presence data. With persistent presence, a scaled-to-zero agent stays discoverable with a status of ",[37,234,235],{},"offline"," or ",[37,238,239],{},"waking"," and is woken when work arrives. No custom health check infrastructure needed. (Presence does not report load: \"idle\" or \"overloaded\" is something an agent would have to publish itself.)",[18,242,244],{"id":243},"six-patterns-as-coordination-primitives","Six Patterns as Coordination Primitives",[11,246,247],{},"We identified six patterns that cover the coordination needs of multi-agent systems:",[249,250,251,258,264,270,276,282],"ol",{},[252,253,254,257],"li",{},[201,255,256],{},"Handoff"," - Task dispatch and result collection with capability-based routing",[252,259,260,263],{},[201,261,262],{},"Inbox"," - Direct agent-to-agent messaging for point-to-point communication",[252,265,266,269],{},[201,267,268],{},"Blackboard"," - Shared state with versioning (last writer wins) so all agents see the same world",[252,271,272,275],{},[201,273,274],{},"Observe"," - Real-time event stream for monitoring, logging, and debugging",[252,277,278,281],{},[201,279,280],{},"Approve"," - Human-in-the-loop gates for high-stakes decisions",[252,283,284,287],{},[201,285,286],{},"Tools"," - Remote tool invocation so agents can share capabilities",[11,289,290,291,294,295,294,298,294,301,294,304,294,307,294,310,313],{},"Each pattern maps to specific topics in the room (",[37,292,293],{},"tasks",", ",[37,296,297],{},"results",[37,299,300],{},"state",[37,302,303],{},"events",[37,305,306],{},"inbox",[37,308,309],{},"tools",[37,311,312],{},"approval",") and a typed message envelope. But you interact with them through a high-level SDK that hides the pub/sub mechanics.",[18,315,317],{"id":316},"what-coordination-actually-looks-like","What Coordination Actually Looks Like",[11,319,320,321,324],{},"Here's the same content pipeline from earlier, but with proper coordination infrastructure. The orchestrator and each worker connect with their own actor tokens (the broker never delivers a message back to the actor that published it), attach ",[37,322,323],{},"@nolag/agents"," to the connection, and meet in a room.",[26,326,327,1025],{},[29,328,331],{"className":31,"code":329,"filename":330,"language":34,"meta":35,"style":35},"import { NoLag } from \"@nolag/js-sdk\";\nimport { NoLagAgents, Handoff, Blackboard, Approve, Observe } from \"@nolag/agents\";\n\n// APP_SLUG is the slug returned when you created the app from the\n// nolag-agents-sdk blueprint (it has a random suffix). Create the\n// content-pipeline room in the control plane first: rooms never exist implicitly.\nconst client = NoLag(ORCHESTRATOR_TOKEN);\nconst agents = new NoLagAgents({\n  client,\n  appName: APP_SLUG,\n  agentId: \"orchestrator\",\n  presence: { name: \"orchestrator\", role: \"orchestrator\" },\n});\nawait client.connect();\nawait agents.ready();\nconst room = agents.room(\"content-pipeline\");\n\n// Shared state visible to all agents (retained, so late joiners see it)\nconst blackboard = new Blackboard(room, agents.agentId);\nblackboard.set(\"plan\", {\n  steps: [\"research\", \"draft\", \"review\", \"publish\"],\n  current: 0,\n});\n\n// Dispatch to any worker advertising the capability, and wait for its result.\n// dispatch() throws if no connected agent advertises the capability.\nconst handoff = new Handoff(room);\nasync function run(capability: string, payload: Record\u003Cstring, unknown>) {\n  const result = await handoff.dispatch(capability, payload, { waitForResult: true });\n  if (!result || result.status !== \"success\") throw new Error(`${capability} failed`);\n  return result.payload;\n}\n\nconst research = await run(\"research\", { query });\nconst draft = await run(\"draft\", { research });\nconst review = await run(\"review\", { draft });\n\n// Human gate before publish\nconst approve = new Approve(room, agents.agentId);\nconst { decision, reason } = await approve.request(\"publish\", review, { urgency: \"high\" });\nif (decision === \"approved\") {\n  await run(\"publish\", { review });\n} else {\n  log(\"publish\", decision, reason);\n}\n\n// Full observability: everything agents emit on the room's events topic\nnew Observe(room, agents.agentId).on((event) => log(event.category, event.severity, event.payload));\n","Orchestrator",[37,332,333,351,365,369,374,379,384,405,423,428,439,449,464,469,484,497,519,524,530,548,565,592,603,608,613,619,625,643,691,719,769,778,784,789,809,829,849,854,860,877,919,937,952,964,977,982,987,993],{"__ignoreMap":35},[40,334,335,338,341,344,348],{"class":42,"line":43},[40,336,337],{"class":53},"import",[40,339,340],{"class":67}," { NoLag } ",[40,342,343],{"class":53},"from",[40,345,347],{"class":346},"sZZnC"," \"@nolag/js-sdk\"",[40,349,350],{"class":67},";\n",[40,352,353,355,358,360,363],{"class":42,"line":50},[40,354,337],{"class":53},[40,356,357],{"class":67}," { NoLagAgents, Handoff, Blackboard, Approve, Observe } ",[40,359,343],{"class":53},[40,361,362],{"class":346}," \"@nolag/agents\"",[40,364,350],{"class":67},[40,366,367],{"class":42,"line":78},[40,368,142],{"emptyLinePlaceholder":141},[40,370,371],{"class":42,"line":98},[40,372,373],{"class":46},"// APP_SLUG is the slug returned when you created the app from the\n",[40,375,376],{"class":42,"line":118},[40,377,378],{"class":46},"// nolag-agents-sdk blueprint (it has a random suffix). Create the\n",[40,380,381],{"class":42,"line":138},[40,382,383],{"class":46},"// content-pipeline room in the control plane first: rooms never exist implicitly.\n",[40,385,386,388,391,393,396,399,402],{"class":42,"line":145},[40,387,54],{"class":53},[40,389,390],{"class":57}," client",[40,392,61],{"class":53},[40,394,395],{"class":71}," NoLag",[40,397,398],{"class":67},"(",[40,400,401],{"class":57},"ORCHESTRATOR_TOKEN",[40,403,404],{"class":67},");\n",[40,406,407,409,412,414,417,420],{"class":42,"line":151},[40,408,54],{"class":53},[40,410,411],{"class":57}," agents",[40,413,61],{"class":53},[40,415,416],{"class":53}," new",[40,418,419],{"class":71}," NoLagAgents",[40,421,422],{"class":67},"({\n",[40,424,425],{"class":42,"line":157},[40,426,427],{"class":67},"  client,\n",[40,429,430,433,436],{"class":42,"line":163},[40,431,432],{"class":67},"  appName: ",[40,434,435],{"class":57},"APP_SLUG",[40,437,438],{"class":67},",\n",[40,440,441,444,447],{"class":42,"line":169},[40,442,443],{"class":67},"  agentId: ",[40,445,446],{"class":346},"\"orchestrator\"",[40,448,438],{"class":67},[40,450,451,454,456,459,461],{"class":42,"line":175},[40,452,453],{"class":67},"  presence: { name: ",[40,455,446],{"class":346},[40,457,458],{"class":67},", role: ",[40,460,446],{"class":346},[40,462,463],{"class":67}," },\n",[40,465,466],{"class":42,"line":181},[40,467,468],{"class":67},"});\n",[40,470,472,475,478,481],{"class":42,"line":471},14,[40,473,474],{"class":53},"await",[40,476,477],{"class":67}," client.",[40,479,480],{"class":71},"connect",[40,482,483],{"class":67},"();\n",[40,485,487,489,492,495],{"class":42,"line":486},15,[40,488,474],{"class":53},[40,490,491],{"class":67}," agents.",[40,493,494],{"class":71},"ready",[40,496,483],{"class":67},[40,498,500,502,505,507,509,512,514,517],{"class":42,"line":499},16,[40,501,54],{"class":53},[40,503,504],{"class":57}," room",[40,506,61],{"class":53},[40,508,491],{"class":67},[40,510,511],{"class":71},"room",[40,513,398],{"class":67},[40,515,516],{"class":346},"\"content-pipeline\"",[40,518,404],{"class":67},[40,520,522],{"class":42,"line":521},17,[40,523,142],{"emptyLinePlaceholder":141},[40,525,527],{"class":42,"line":526},18,[40,528,529],{"class":46},"// Shared state visible to all agents (retained, so late joiners see it)\n",[40,531,533,535,538,540,542,545],{"class":42,"line":532},19,[40,534,54],{"class":53},[40,536,537],{"class":57}," blackboard",[40,539,61],{"class":53},[40,541,416],{"class":53},[40,543,544],{"class":71}," Blackboard",[40,546,547],{"class":67},"(room, agents.agentId);\n",[40,549,551,554,557,559,562],{"class":42,"line":550},20,[40,552,553],{"class":67},"blackboard.",[40,555,556],{"class":71},"set",[40,558,398],{"class":67},[40,560,561],{"class":346},"\"plan\"",[40,563,564],{"class":67},", {\n",[40,566,568,571,574,576,579,581,584,586,589],{"class":42,"line":567},21,[40,569,570],{"class":67},"  steps: [",[40,572,573],{"class":346},"\"research\"",[40,575,294],{"class":67},[40,577,578],{"class":346},"\"draft\"",[40,580,294],{"class":67},[40,582,583],{"class":346},"\"review\"",[40,585,294],{"class":67},[40,587,588],{"class":346},"\"publish\"",[40,590,591],{"class":67},"],\n",[40,593,595,598,601],{"class":42,"line":594},22,[40,596,597],{"class":67},"  current: ",[40,599,600],{"class":57},"0",[40,602,438],{"class":67},[40,604,606],{"class":42,"line":605},23,[40,607,468],{"class":67},[40,609,611],{"class":42,"line":610},24,[40,612,142],{"emptyLinePlaceholder":141},[40,614,616],{"class":42,"line":615},25,[40,617,618],{"class":46},"// Dispatch to any worker advertising the capability, and wait for its result.\n",[40,620,622],{"class":42,"line":621},26,[40,623,624],{"class":46},"// dispatch() throws if no connected agent advertises the capability.\n",[40,626,628,630,633,635,637,640],{"class":42,"line":627},27,[40,629,54],{"class":53},[40,631,632],{"class":57}," handoff",[40,634,61],{"class":53},[40,636,416],{"class":53},[40,638,639],{"class":71}," Handoff",[40,641,642],{"class":67},"(room);\n",[40,644,646,649,652,655,657,661,664,667,669,672,674,677,680,683,685,688],{"class":42,"line":645},28,[40,647,648],{"class":53},"async",[40,650,651],{"class":53}," function",[40,653,654],{"class":71}," run",[40,656,398],{"class":67},[40,658,660],{"class":659},"s4XuR","capability",[40,662,663],{"class":53},":",[40,665,666],{"class":57}," string",[40,668,294],{"class":67},[40,670,671],{"class":659},"payload",[40,673,663],{"class":53},[40,675,676],{"class":71}," Record",[40,678,679],{"class":67},"\u003C",[40,681,682],{"class":57},"string",[40,684,294],{"class":67},[40,686,687],{"class":57},"unknown",[40,689,690],{"class":67},">) {\n",[40,692,694,697,700,702,704,707,710,713,716],{"class":42,"line":693},29,[40,695,696],{"class":53},"  const",[40,698,699],{"class":57}," result",[40,701,61],{"class":53},[40,703,64],{"class":53},[40,705,706],{"class":67}," handoff.",[40,708,709],{"class":71},"dispatch",[40,711,712],{"class":67},"(capability, payload, { waitForResult: ",[40,714,715],{"class":57},"true",[40,717,718],{"class":67}," });\n",[40,720,722,725,728,731,734,737,740,743,746,749,752,754,757,759,762,764,767],{"class":42,"line":721},30,[40,723,724],{"class":53},"  if",[40,726,727],{"class":67}," (",[40,729,730],{"class":53},"!",[40,732,733],{"class":67},"result ",[40,735,736],{"class":53},"||",[40,738,739],{"class":67}," result.status ",[40,741,742],{"class":53},"!==",[40,744,745],{"class":346}," \"success\"",[40,747,748],{"class":67},") ",[40,750,751],{"class":53},"throw",[40,753,416],{"class":53},[40,755,756],{"class":71}," Error",[40,758,398],{"class":67},[40,760,761],{"class":346},"`${",[40,763,660],{"class":67},[40,765,766],{"class":346},"} failed`",[40,768,404],{"class":67},[40,770,772,775],{"class":42,"line":771},31,[40,773,774],{"class":53},"  return",[40,776,777],{"class":67}," result.payload;\n",[40,779,781],{"class":42,"line":780},32,[40,782,783],{"class":67},"}\n",[40,785,787],{"class":42,"line":786},33,[40,788,142],{"emptyLinePlaceholder":141},[40,790,792,794,796,798,800,802,804,806],{"class":42,"line":791},34,[40,793,54],{"class":53},[40,795,58],{"class":57},[40,797,61],{"class":53},[40,799,64],{"class":53},[40,801,654],{"class":71},[40,803,398],{"class":67},[40,805,573],{"class":346},[40,807,808],{"class":67},", { query });\n",[40,810,812,814,816,818,820,822,824,826],{"class":42,"line":811},35,[40,813,54],{"class":53},[40,815,83],{"class":57},[40,817,61],{"class":53},[40,819,64],{"class":53},[40,821,654],{"class":71},[40,823,398],{"class":67},[40,825,578],{"class":346},[40,827,828],{"class":67},", { research });\n",[40,830,832,834,836,838,840,842,844,846],{"class":42,"line":831},36,[40,833,54],{"class":53},[40,835,103],{"class":57},[40,837,61],{"class":53},[40,839,64],{"class":53},[40,841,654],{"class":71},[40,843,398],{"class":67},[40,845,583],{"class":346},[40,847,848],{"class":67},", { draft });\n",[40,850,852],{"class":42,"line":851},37,[40,853,142],{"emptyLinePlaceholder":141},[40,855,857],{"class":42,"line":856},38,[40,858,859],{"class":46},"// Human gate before publish\n",[40,861,863,865,868,870,872,875],{"class":42,"line":862},39,[40,864,54],{"class":53},[40,866,867],{"class":57}," approve",[40,869,61],{"class":53},[40,871,416],{"class":53},[40,873,874],{"class":71}," Approve",[40,876,547],{"class":67},[40,878,880,882,885,888,890,893,896,899,901,904,907,909,911,914,917],{"class":42,"line":879},40,[40,881,54],{"class":53},[40,883,884],{"class":67}," { ",[40,886,887],{"class":57},"decision",[40,889,294],{"class":67},[40,891,892],{"class":57},"reason",[40,894,895],{"class":67}," } ",[40,897,898],{"class":53},"=",[40,900,64],{"class":53},[40,902,903],{"class":67}," approve.",[40,905,906],{"class":71},"request",[40,908,398],{"class":67},[40,910,588],{"class":346},[40,912,913],{"class":67},", review, { urgency: ",[40,915,916],{"class":346},"\"high\"",[40,918,718],{"class":67},[40,920,922,925,928,931,934],{"class":42,"line":921},41,[40,923,924],{"class":53},"if",[40,926,927],{"class":67}," (decision ",[40,929,930],{"class":53},"===",[40,932,933],{"class":346}," \"approved\"",[40,935,936],{"class":67},") {\n",[40,938,940,943,945,947,949],{"class":42,"line":939},42,[40,941,942],{"class":53},"  await",[40,944,654],{"class":71},[40,946,398],{"class":67},[40,948,588],{"class":346},[40,950,951],{"class":67},", { review });\n",[40,953,955,958,961],{"class":42,"line":954},43,[40,956,957],{"class":67},"} ",[40,959,960],{"class":53},"else",[40,962,963],{"class":67}," {\n",[40,965,967,970,972,974],{"class":42,"line":966},44,[40,968,969],{"class":71},"  log",[40,971,398],{"class":67},[40,973,588],{"class":346},[40,975,976],{"class":67},", decision, reason);\n",[40,978,980],{"class":42,"line":979},45,[40,981,783],{"class":67},[40,983,985],{"class":42,"line":984},46,[40,986,142],{"emptyLinePlaceholder":141},[40,988,990],{"class":42,"line":989},47,[40,991,992],{"class":46},"// Full observability: everything agents emit on the room's events topic\n",[40,994,996,999,1002,1005,1008,1011,1014,1016,1019,1022],{"class":42,"line":995},48,[40,997,998],{"class":53},"new",[40,1000,1001],{"class":71}," Observe",[40,1003,1004],{"class":67},"(room, agents.agentId).",[40,1006,1007],{"class":71},"on",[40,1009,1010],{"class":67},"((",[40,1012,1013],{"class":659},"event",[40,1015,748],{"class":67},[40,1017,1018],{"class":53},"=>",[40,1020,1021],{"class":71}," log",[40,1023,1024],{"class":67},"(event.category, event.severity, event.payload));\n",[29,1026,1029],{"className":31,"code":1027,"filename":1028,"language":34,"meta":35,"style":35},"import { NoLag } from \"@nolag/js-sdk\";\nimport { NoLagAgents, Handoff, Observe } from \"@nolag/agents\";\n\n// Its own token (an `agent` actor, so its session persists), and its capabilities\n// advertised as presence so dispatch() can find it\nconst client = NoLag(WORKER_TOKEN, { loadBalance: true, loadBalanceGroup: \"researchers\" });\nconst agents = new NoLagAgents({\n  client,\n  appName: APP_SLUG,\n  agentId: \"researcher-1\",\n  presence: { name: \"researcher-1\", role: \"agent\", capabilities: [\"research\"] },\n});\nawait client.connect();\nawait agents.ready();\nconst room = agents.room(\"content-pipeline\");\n\nconst observe = new Observe(room, agents.agentId);\n\n// Only tasks whose capability matches are delivered to this handler\nnew Handoff(room).onTask([\"research\"], async (task, respond) => {\n  observe.emit(\"research\", { taskId: task.taskId, status: \"started\" });\n  const findings = await researchAgent.run(task.payload.query);\n  respond(\"success\", { findings });\n  observe.emit(\"research\", { taskId: task.taskId, status: \"done\" });\n});\n","Worker",[37,1030,1031,1043,1056,1060,1065,1070,1098,1112,1116,1124,1133,1152,1156,1166,1176,1194,1198,1213,1217,1222,1260,1280,1298,1311,1328],{"__ignoreMap":35},[40,1032,1033,1035,1037,1039,1041],{"class":42,"line":43},[40,1034,337],{"class":53},[40,1036,340],{"class":67},[40,1038,343],{"class":53},[40,1040,347],{"class":346},[40,1042,350],{"class":67},[40,1044,1045,1047,1050,1052,1054],{"class":42,"line":50},[40,1046,337],{"class":53},[40,1048,1049],{"class":67}," { NoLagAgents, Handoff, Observe } ",[40,1051,343],{"class":53},[40,1053,362],{"class":346},[40,1055,350],{"class":67},[40,1057,1058],{"class":42,"line":78},[40,1059,142],{"emptyLinePlaceholder":141},[40,1061,1062],{"class":42,"line":98},[40,1063,1064],{"class":46},"// Its own token (an `agent` actor, so its session persists), and its capabilities\n",[40,1066,1067],{"class":42,"line":118},[40,1068,1069],{"class":46},"// advertised as presence so dispatch() can find it\n",[40,1071,1072,1074,1076,1078,1080,1082,1085,1088,1090,1093,1096],{"class":42,"line":138},[40,1073,54],{"class":53},[40,1075,390],{"class":57},[40,1077,61],{"class":53},[40,1079,395],{"class":71},[40,1081,398],{"class":67},[40,1083,1084],{"class":57},"WORKER_TOKEN",[40,1086,1087],{"class":67},", { loadBalance: ",[40,1089,715],{"class":57},[40,1091,1092],{"class":67},", loadBalanceGroup: ",[40,1094,1095],{"class":346},"\"researchers\"",[40,1097,718],{"class":67},[40,1099,1100,1102,1104,1106,1108,1110],{"class":42,"line":145},[40,1101,54],{"class":53},[40,1103,411],{"class":57},[40,1105,61],{"class":53},[40,1107,416],{"class":53},[40,1109,419],{"class":71},[40,1111,422],{"class":67},[40,1113,1114],{"class":42,"line":151},[40,1115,427],{"class":67},[40,1117,1118,1120,1122],{"class":42,"line":157},[40,1119,432],{"class":67},[40,1121,435],{"class":57},[40,1123,438],{"class":67},[40,1125,1126,1128,1131],{"class":42,"line":163},[40,1127,443],{"class":67},[40,1129,1130],{"class":346},"\"researcher-1\"",[40,1132,438],{"class":67},[40,1134,1135,1137,1139,1141,1144,1147,1149],{"class":42,"line":169},[40,1136,453],{"class":67},[40,1138,1130],{"class":346},[40,1140,458],{"class":67},[40,1142,1143],{"class":346},"\"agent\"",[40,1145,1146],{"class":67},", capabilities: [",[40,1148,573],{"class":346},[40,1150,1151],{"class":67},"] },\n",[40,1153,1154],{"class":42,"line":175},[40,1155,468],{"class":67},[40,1157,1158,1160,1162,1164],{"class":42,"line":181},[40,1159,474],{"class":53},[40,1161,477],{"class":67},[40,1163,480],{"class":71},[40,1165,483],{"class":67},[40,1167,1168,1170,1172,1174],{"class":42,"line":471},[40,1169,474],{"class":53},[40,1171,491],{"class":67},[40,1173,494],{"class":71},[40,1175,483],{"class":67},[40,1177,1178,1180,1182,1184,1186,1188,1190,1192],{"class":42,"line":486},[40,1179,54],{"class":53},[40,1181,504],{"class":57},[40,1183,61],{"class":53},[40,1185,491],{"class":67},[40,1187,511],{"class":71},[40,1189,398],{"class":67},[40,1191,516],{"class":346},[40,1193,404],{"class":67},[40,1195,1196],{"class":42,"line":499},[40,1197,142],{"emptyLinePlaceholder":141},[40,1199,1200,1202,1205,1207,1209,1211],{"class":42,"line":521},[40,1201,54],{"class":53},[40,1203,1204],{"class":57}," observe",[40,1206,61],{"class":53},[40,1208,416],{"class":53},[40,1210,1001],{"class":71},[40,1212,547],{"class":67},[40,1214,1215],{"class":42,"line":526},[40,1216,142],{"emptyLinePlaceholder":141},[40,1218,1219],{"class":42,"line":532},[40,1220,1221],{"class":46},"// Only tasks whose capability matches are delivered to this handler\n",[40,1223,1224,1226,1228,1231,1234,1237,1239,1242,1244,1246,1249,1251,1254,1256,1258],{"class":42,"line":550},[40,1225,998],{"class":53},[40,1227,639],{"class":71},[40,1229,1230],{"class":67},"(room).",[40,1232,1233],{"class":71},"onTask",[40,1235,1236],{"class":67},"([",[40,1238,573],{"class":346},[40,1240,1241],{"class":67},"], ",[40,1243,648],{"class":53},[40,1245,727],{"class":67},[40,1247,1248],{"class":659},"task",[40,1250,294],{"class":67},[40,1252,1253],{"class":659},"respond",[40,1255,748],{"class":67},[40,1257,1018],{"class":53},[40,1259,963],{"class":67},[40,1261,1262,1265,1268,1270,1272,1275,1278],{"class":42,"line":567},[40,1263,1264],{"class":67},"  observe.",[40,1266,1267],{"class":71},"emit",[40,1269,398],{"class":67},[40,1271,573],{"class":346},[40,1273,1274],{"class":67},", { taskId: task.taskId, status: ",[40,1276,1277],{"class":346},"\"started\"",[40,1279,718],{"class":67},[40,1281,1282,1284,1287,1289,1291,1293,1295],{"class":42,"line":594},[40,1283,696],{"class":53},[40,1285,1286],{"class":57}," findings",[40,1288,61],{"class":53},[40,1290,64],{"class":53},[40,1292,68],{"class":67},[40,1294,72],{"class":71},[40,1296,1297],{"class":67},"(task.payload.query);\n",[40,1299,1300,1303,1305,1308],{"class":42,"line":605},[40,1301,1302],{"class":71},"  respond",[40,1304,398],{"class":67},[40,1306,1307],{"class":346},"\"success\"",[40,1309,1310],{"class":67},", { findings });\n",[40,1312,1313,1315,1317,1319,1321,1323,1326],{"class":42,"line":610},[40,1314,1264],{"class":67},[40,1316,1267],{"class":71},[40,1318,398],{"class":67},[40,1320,573],{"class":346},[40,1322,1274],{"class":67},[40,1324,1325],{"class":346},"\"done\"",[40,1327,718],{"class":67},[40,1329,1330],{"class":42,"line":615},[40,1331,468],{"class":67},[11,1333,1334],{},"The difference: workers can be scaled independently (every worker in a load-balanced group receives a share of the tasks rather than a copy), every event a worker emits is observable, shared state is accessible to all agents, human approval gates are built in, and a worker that restarts drains the tasks its group missed while it was away.",[11,1336,1337,1338,1340,1341,1343,1344,1347,1348,1351,1352,1354,1355,294,1358,236,1361,1364],{},"Two honest details. ",[37,1339,256],{}," does not narrate itself: nothing appears on the ",[37,1342,303],{}," topic unless an agent calls ",[37,1345,1346],{},"observe.emit",", which is why the worker above emits at each step. And ",[37,1349,1350],{},"approve.request"," resolves with a ",[37,1353,887],{}," of ",[37,1356,1357],{},"approved",[37,1359,1360],{},"rejected",[37,1362,1363],{},"deferred",", so the orchestrator has to handle all three rather than a boolean.",[18,1366,1368],{"id":1367},"nolag-as-the-substrate","NoLag as the Substrate",[11,1370,1371],{},"NoLag was built as real-time messaging infrastructure for chat, notifications, and IoT. It turns out that the same primitives - topics, rooms, presence, QoS, and access control - map directly to multi-agent coordination needs.",[11,1373,1374,1376],{},[37,1375,323],{}," is a high-level SDK that wraps these primitives into the six patterns above. Under the hood, it uses the same WebSocket infrastructure that carries NoLag's chat, notification and IoT traffic. You get load-balanced dispatch, durable catch-up for worker groups, presence-based discovery, retained shared state and per-topic ACL without building any of it yourself.",[11,1378,1379],{},"The key insight: agent coordination is a real-time messaging problem. And real-time messaging is a solved problem - if you use the right infrastructure.",[11,1381,1382],{},[222,1383,1385],{"href":1384},"/agents","Learn more about @nolag/agents →",[1387,1388,1389],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":35,"searchDepth":50,"depth":50,"links":1391},[1392,1393,1394,1395,1396],{"id":20,"depth":50,"text":21},{"id":193,"depth":50,"text":194},{"id":243,"depth":50,"text":244},{"id":316,"depth":50,"text":317},{"id":1367,"depth":50,"text":1368},"AI Agents","2026-05-19","Multi-agent AI systems need more than an LLM API. They need coordination infrastructure: task dispatch, shared state, approval gates, and observability. Here is why pub/sub is the right primitive.",null,"md",{},"/blog/multi-agent-coordination-layer","11 min read",{"title":5,"description":1399},"blog/multi-agent-coordination-layer","z6AWvofhR9AGR83-7_LdrniQhq_gLrzE8Ea7XHMmq54",1789869417482]