Unified ManufacturingData Architecture

UMDA Tools

Agent Designer

Design domain specific AI agents that exchange canonical objects instead of free text, and coordinate them through a centralized LLM Router. Import the canonical model from the CDM Designer (or define it here), scope each agent to the entities it owns, and lay out how the router classifies a request and hands it to the right domain. Built on the same UMDA backbone as the UNS and CDM Designers.

Loading the designer… if this message does not go away after a few seconds, the page did not finish downloading: refresh to reload it.

Work the tabs left to right: define the canonical model, staff the agents, wire the router, then validate, visualize, and hand off.

'; return b; } function snakeName(s){ return String(s||"agent").toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"") || "agent"; } function jsonSchemaDoc(){ const ents = touchedEntities(); const defs = {}; ents.forEach(e => { const sig = (state.model.entitySignals||{})[e] || []; let payload; if(sig.length){ const props = {}; sig.forEach(nm => props[nm] = { description: "signal " + nm }); payload = { type:"object", description:"entity attributes, from the imported model signals", properties:props, additionalProperties:true }; } else { payload = { type:"object", description:"entity attributes, model these to your specification", additionalProperties:true }; } defs[e] = { type:"object", title: entityLabel(e), description: "Canonical " + entityLabel(e) + " object exchanged between agents.", required:["entity","id"], properties:{ entity:{ const:e }, id:{ type:"string", description:"stable identifier" }, timestamp:{ type:"string", format:"date-time" }, payload: payload }, additionalProperties:true }; }); const schema = { "$schema":"https://json-schema.org/draft/2020-12/schema", "$id":"https://umda.info/agent-contract/" + safeName(), title: state.model.name + " canonical contract", description:"Handoff objects exchanged between agents. Generated by the UMDA Agent Designer.", type:"object", properties:{ envelope:{ type:"object", required:["from","entity","object"], properties:{ from:{ type:"string", description:"writing agent" }, to:{ type:"string", description:"target agent or domain, optional" }, entity:{ enum: ents }, object:{ oneOf: ents.map(e => ({ "$ref":"#/$defs/" + e })) } } } }, "$defs": defs }; return JSON.stringify(schema, null, 2); } function agentSystemPrompt(a){ const reads = (a.reads||[]).map(entityLabel), writes = (a.writes||[]).map(entityLabel), p = agentParent(a); const dom = domainName(rootDomainId(a)) || "unassigned"; let s = "# " + a.name + "\n\n"; s += "You are " + a.name + ", a " + (p?"worker":"lead") + " agent in the " + dom + " domain of the " + state.model.name + " system.\n\n"; if(a.purpose) s += "Purpose: " + a.purpose + "\n\n"; s += "You exchange canonical objects, not free text. "; s += writes.length ? ("You are the single writer of these canonical entities: " + writes.join(", ") + ". ") : "You do not write canonical entities. "; if(reads.length) s += "You may read: " + reads.join(", ") + ". "; s += "\n\n"; if(p) s += "You report to " + p.name + " and must stay within the " + dom + " domain scope.\n\n"; if((a.tools||[]).length) s += "Tools available: " + a.tools.join(", ") + ".\n\n"; if((a.grounding||[]).length) s += "Sources of truth: " + a.grounding.map(g => (SOURCE_BY_ID[g.type]?SOURCE_BY_ID[g.type].label:g.type) + (g.detail?(" (" + g.detail + ")"):"")).join(", ") + ".\n\n"; if(a.guardrails) s += "Guardrails: " + a.guardrails + "\n\n"; const lv = agentLevel(a); if(lv !== null) s += "Autonomy level " + lv + " (" + LEVEL_BY_ID[lv].short + "): " + LEVEL_BY_ID[lv].prompt + "\n\n"; if(a.tier && TIER_BY_ID[a.tier]) s += "Model tier: " + TIER_BY_ID[a.tier].label + ".\n\n"; return s.trim(); } function systemPromptsText(){ let out = "# System prompt scaffolds for " + state.model.name + "\n\nGenerated by the UMDA Agent Designer. One scaffold per agent, canonical model context injected. Edit before use.\n"; state.model.agents.forEach(a => { out += "\n---\n\n" + agentSystemPrompt(a) + "\n"; }); return out; } /* ---------- agent qualification pack: the "Earning Autonomy" method made executable. One protocol per agent — risk ceiling, shadow-mode log template, promotion acceptance criteria written BEFORE the evidence arrives, demotion triggers, MOC-style sign-off. The Feedback Data Layer is the evidence base the whole path runs on. ---------- */ function qualLevelExtras(next){ if(next === 2) return ["The approval workflow has a named owner, a response-time expectation, and coverage across shifts."]; if(next === 3) return [ "The oversight queue has a named owner who actually watches it.", "Alerting is tuned well enough to be trusted — an approval queue nobody genuinely evaluates is a rubber stamp, the most dangerous configuration on the scale.", "The intervention path works at two in the morning.", "The authority bounds are enforced at the interface (the transaction layer: MES, CMMS, scheduling), not just promised in the prompt."]; if(next === 4) return ["The guardrails have a named owner.", "A standing audit cadence is agreed and on the calendar."]; return ["A person evaluates every recommendation, and every evaluation is logged as evidence."]; } function agentQualSection(a){ const p = agentParent(a), lv = agentLevel(a), dom = domainName(rootDomainId(a)) || "unassigned"; const writes = (a.writes || []).map(entityLabel).join(", ") || "none"; const toolList = (a.tools || []).join(", ") || "none"; const next = lv === null ? 1 : Math.min(4, lv + 1); const atCeil = lv === 4; let md = "## " + a.name + " — qualification protocol\n\n"; md += "| | |\n|---|---|\n"; md += "| Role | " + (p ? ("Worker under " + p.name) : "Lead agent") + " · " + dom + " domain |\n"; md += "| Current autonomy | " + (lv === null ? "Not set — set it on the Agents tab before granting any authority" : ("Level " + lv + " · " + LEVEL_BY_ID[lv].short + " (" + LEVEL_BY_ID[lv].note + ")")) + " |\n"; md += "| This protocol covers | " + (lv === null ? "Shadow mode → Level 1 entry" : (atCeil ? "The standing audit at Level 4" : ("Promotion: Level " + lv + " → Level " + next + " (" + LEVEL_BY_ID[next].short + ")"))) + " |\n"; md += "| Action classes | Writes: " + writes + " · Tools: " + toolList + " |\n"; md += "| Guardrails | " + (a.guardrails || "None written — write them before Level 3: unwritten bounds are not bounds") + " |\n\n"; md += "### 1. Risk ceiling — decide it before capability does\n\n"; md += "Score the four factors for this agent's action classes and record the ceiling: the highest level it may ever be a candidate for. Some cells are simply closed (a regulator expecting a person to own a decision is a design constraint, not an obstacle).\n\n"; md += "| Factor | Assessment (fill in) |\n|---|---|\n| Consequence severity — worst credible outcome | |\n| Reversibility — and how fast a wrong action is DETECTED | |\n| Blast radius — one machine, one line, one site, or the network | |\n| Regulatory weight — validated system, GxP, electronic records | |\n| **Agreed ceiling** | **Level ___** |\n\n"; md += "### 2. Shadow-mode log — the evidence base starts here\n\n"; md += "Run the agent against real operations with no authority. Its proposals are logged next to what the humans actually did and what happened afterward:\n\n"; md += "| # | Date / shift | Situation & inputs | Agent proposal (with its reasoning) | Human action | Agree? | Severity class | Outcome / notes |\n|---|---|---|---|---|---|---|---|\n| 1 | | | | | | | |\n| 2 | | | | | | | |\n| 3 | | | | | | | |\n\n"; md += "Overrides are the most valuable records in this log — each captures a case where human and agent judgment diverged. Review every one; do not just count them. In production this log is the Feedback Data Layer's job, captured continuously instead of by hand.\n\n"; if(atCeil){ md += "### 3. Standing audit — Level 4 holds only as long as the evidence does\n\n"; md += "- [ ] Audit cadence: every ______ (on the calendar, with a named owner)\n"; md += "- [ ] Each audit reviews: action volume, error profile vs. the band it was promoted on, guardrail hits, anything near the edges of authority\n"; md += "- [ ] Guardrail owner re-confirms the bounds still match the process\n\n"; } else { md += "### 3. Promotion acceptance criteria — Level " + (lv === null ? 0 : lv) + " → " + next + " (write these before the evidence arrives)\n\n"; md += "Promotion is a qualification decision, made against criteria set in advance — the same discipline as equipment IQ/OQ/PQ. The exact numbers belong to this plant and this risk class; the discipline of setting them now belongs to every plant.\n\n"; md += "- [ ] Minimum decision volume: ______ decisions (large enough to cover the plant's real variety)\n"; md += "- [ ] Observation window: ______ (spans at least one full demand cycle — month-end pressure and a holiday week; an agent observed only during a calm quarter has not been observed)\n"; md += "- [ ] Agreement rate ≥ ______ %\n"; md += "- [ ] Zero disagreements in the highest-severity class\n"; md += "- [ ] Every override reviewed in writing\n"; md += "- [ ] The error profile is small, random, and low-cost — not systematic in one direction, and not concentrated where stakes are highest\n"; md += "- [ ] Behavior at the edges of its authority reviewed (edge behavior predicts beyond-edge behavior)\n"; qualLevelExtras(next).forEach(x => { md += "- [ ] " + x + "\n"; }); md += "- [ ] Reviewed under management of change, with Quality and EHS at the table where their domains are touched\n\n"; } md += "### 4. Demotion triggers — named in the same document that grants the authority\n\n"; md += "Any of these drops the agent one level while the cause is understood. No one has to win an argument first — an organization that can only ratchet autonomy upward cannot respond to evidence.\n\n"; md += "- A quality event traceable to an agent action\n"; md += "- The error profile drifts outside the band it was promoted on\n"; md += "- A step change in the process — a new product introduction, a reformulation, an equipment replacement — invalidates the history it was judged against\n"; md += "- The oversight queue goes unwatched\n\n"; md += "### 5. Sign-off\n\n"; md += "| Role | Name | Date |\n|---|---|---|\n| Operations owner | | |\n| Quality (where touched) | | |\n| EHS (where touched) | | |\n| System owner" + ((state.model.governance && state.model.governance.owner) ? (" — " + state.model.governance.owner) : "") + " | | |\n"; return md; } function qualificationPackText(){ let out = "# Agent qualification pack — " + state.model.name + "\n\n"; out += "Generated by the UMDA Agent Designer (https://umda.info/tools/agent-designer.html). One protocol per agent: autonomy is earned one level at a time, against acceptance criteria written before the evidence comes in, with demotion as real as promotion. "; out += "The Feedback Data Layer is the evidence base this path runs on — every approval, override, and outcome below is FDL data. "; out += "Method: the Earning Autonomy whitepaper (https://umda.info/earning-autonomy.html).\n"; state.model.agents.forEach(a => { out += "\n---\n\n" + agentQualSection(a); }); if(!state.model.agents.length) out += "\nNo agents yet — add them on the Agents tab, set each one's autonomy level, then regenerate this pack.\n"; return out; } function n8nWorkflowJson(){ const leads = topAgents(); const nodes = [], conns = {}; nodes.push({ parameters:{ path: safeName() + "-router", options:{} }, name:"Request In", type:"n8n-nodes-base.webhook", typeVersion:1, position:[240,300], id:"node_webhook" }); nodes.push({ parameters:{ rules:{ values: state.model.router.intents.map(it => ({ outputKey: it.name })) } }, name:"LLM Router", type:"n8n-nodes-base.switch", typeVersion:1, position:[480,300], id:"node_router" }); conns["Request In"] = { main:[[{ node:"LLM Router", type:"main", index:0 }]] }; conns["LLM Router"] = { main:[] }; let i = 0; leads.forEach(a => { const nm = a.name; nodes.push({ parameters:{ values:{ string:[{ name:"agent", value: nm }, { name:"domain", value: domainName(rootDomainId(a)) || "" }, { name:"writes", value:(a.writes||[]).map(entityLabel).join(", ") }] } }, name:nm, type:"n8n-nodes-base.set", typeVersion:1, position:[760, 180 + i*160], id:"node_lead_" + i }); conns["LLM Router"].main.push([{ node:nm, type:"main", index:0 }]); i++; }); return JSON.stringify({ name: state.model.name + " router", nodes:nodes, connections:conns, active:false, settings:{}, version:1 }, null, 2); } function langGraphPy(){ const leads = topAgents(); let s = '"""LangGraph starter for ' + state.model.name + ', generated by the UMDA Agent Designer."""' + "\n"; s += "from langgraph.graph import StateGraph, END\n"; s += "from typing import TypedDict\n\n"; s += "class State(TypedDict):\n request: str\n intent: str\n result: dict\n\n"; s += "def router(state):\n # classify state['request'] into an intent, then route to the owning agent\n return state\n\n"; leads.forEach(a => { const fn = snakeName(a.name); s += "def " + fn + "(state):\n # " + (a.purpose||a.name) + "\n # writes: " + ((a.writes||[]).map(entityLabel).join(", ")||"none") + "\n return state\n\n"; }); s += "graph = StateGraph(State)\n"; s += "graph.add_node('router', router)\n"; leads.forEach(a => { s += "graph.add_node('" + snakeName(a.name) + "', " + snakeName(a.name) + ")\n"; }); s += "graph.set_entry_point('router')\n"; s += "# add conditional edges from router to each agent based on the classified intent\n"; leads.forEach(a => { s += "graph.add_edge('" + snakeName(a.name) + "', END)\n"; }); s += "app = graph.compile()\n"; return s; } function mcpServerStub(){ let s = '"""MCP server stub for ' + state.model.name + ', generated by the UMDA Agent Designer."""' + "\n"; s += "from mcp.server.fastmcp import FastMCP\n\n"; s += 'mcp = FastMCP("' + state.model.name + '")\n\n'; state.model.agents.forEach(a => { const fn = snakeName(a.name); s += "@mcp.tool()\n"; s += "def " + fn + "(request: str) -> dict:\n"; s += ' """' + (a.purpose||a.name) + " Writes: " + ((a.writes||[]).map(entityLabel).join(", ")||"none") + '"""' + "\n"; s += " # implement this agent\n return {}\n\n"; }); s += 'if __name__ == "__main__":\n mcp.run()\n'; return s; } function handoffReadme(){ const a = assessFleet(), doms = state.model.domains, agents = state.model.agents, R = state.model.router, g = state.model.governance || {}; let md = "# " + (state.model.name || "Agent system") + "\n\n"; md += "A UMDA agent system. Domain agents exchange canonical CDM objects, coordinated by a centralized LLM Router.\n\n"; md += "Readiness: " + a.overall + " of 100 (" + grade(a.overall) + ").\n\n"; md += "## At a glance\n"; md += "- Domains: " + doms.length + "\n- Agents: " + agents.length + " (" + topAgents().length + " leads)\n- Intents: " + R.intents.length + "\n- Canonical entities governed: " + governedEntities().length + "\n\n"; md += "## Domains and owners\n"; doms.forEach(d => { const lead = ownerAgents(d.id)[0]; md += "- " + d.name + " (owner: " + (d.owner||"unassigned") + ", lead agent: " + (lead?lead.name:"none") + ", " + (d.entities||[]).length + " entities)\n"; }); md += "\n## Governance\n"; md += "System owner: " + (g.owner||"not set") + ". Version " + (g.version||"n/a") + ", reviewed " + (g.cadence||"n/a") + ".\n"; if((g.federation||"").trim()) md += "\n" + g.federation + "\n"; md += "\n## Artifacts in this handoff\n"; md += "Generated from the Export tab of the UMDA Agent Designer:\n\n"; md += "- Design file (.json): the full design, reloadable in the tool.\n"; md += "- Agent cards (.json): one card per agent with its CDM contract.\n"; md += "- Router config (.json): intents, routes, fallback, and arbitration.\n"; md += "- Contract schema (.json): JSON Schema for the canonical handoff envelope.\n"; md += "- System prompt scaffolds (.md): one prompt per agent.\n"; md += "- Agent qualification pack (.md): per agent, the risk ceiling, shadow-mode log, promotion criteria, and demotion triggers — how each agent earns its autonomy level.\n"; md += "- Framework starters: n8n (.json), LangGraph (.py), MCP server (.py). Skeletons to build on, not runnable systems.\n"; md += "- Architecture diagram (.svg), executive one-pager (.html), and validation report (.md).\n"; return md; } function renderExport(v){ let h = pageIntro("Export and handoff", "Save your work, share it, or load a design someone sent you. The richer exports, agent cards, router config, schemas, and framework starters, arrive as the tabs fill in.", ["Save a .json to keep or reload", "Copy a share link that reopens this design", "Load a .json someone sent you"]); h += '
'; h += '

Design file

Save and reload the whole design as JSON.

'; h += '

Share link

A link that reopens this exact design in the browser.

'; h += '
'; h += '

System exports

'; h += '
'; h += '

Agent cards

One card per agent: purpose, domain, reads and writes in CDM terms, and tools.

'; h += '

Router config

Intents, the domains and agents each routes to, the fallback, and the arbitration note.

'; h += '

Intent table

A flat intent to domain to agent table for a sheet.

'; h += '

Architecture diagram

The router, the domain agents, and the canonical model, as SVG.

'; h += '
'; h += '

Contract and framework exports

'; h += '

The framework starters are valid skeletons to build on, not runnable systems. They give you the structure, the agent stubs, and the wiring to fill in.

'; h += '
'; h += '

Contract schema

JSON Schema for the canonical handoff envelope and one definition per entity in use.

'; h += '

System prompt scaffolds

One prompt per agent with its domain, reads and writes, tools, grounding, and guardrails injected.

'; h += '

Agent qualification pack

The Earning Autonomy method, executable: per agent, a risk-ceiling worksheet, a shadow-mode log, promotion acceptance criteria written before the evidence, demotion triggers, and an MOC-style sign-off.

'; h += '

n8n workflow

A starter workflow: a webhook into a router switch, branching to one node per lead agent.

'; h += '

LangGraph graph

A Python StateGraph scaffold with a router entry point and a node per lead agent.

'; h += '

MCP server stub

A FastMCP stub exposing each agent as a tool, with its purpose and writes in the docstring.

'; h += '
'; h += '

Leadership

'; h += '
'; h += '

Executive one-pager

A standalone HTML summary: stats, the diagram, top findings, ownership, and governance.

'; h += '

Handoff README

A plain markdown overview of the design, its owners, and every artifact in the package, to hand to a team.

'; h += '
'; h += '

Architecture preview

'; h += '
' + svgArchitecture(false) + '
'; v.innerHTML = h; v.querySelector("#dlJson").addEventListener("click", () => download(safeName()+"-agent-system.json", designJson(), "application/json")); v.querySelector("#cpShare").addEventListener("click", () => copyText(shareLink(), "Share link copied")); const ll = v.querySelector("#loadJsonLbl"); if(ll) ll.addEventListener("keydown", (e) => { if(e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return; e.preventDefault(); const inp = v.querySelector("#loadJson"); if(inp && typeof inp.click === "function") inp.click(); }); v.querySelector("#loadJson").addEventListener("change", (e) => { const f = e.target.files && e.target.files[0]; if(!f) return; const rd = new FileReader(); rd.onload = () => { try { loadDesign(JSON.parse(rd.result)); toast("Design loaded"); render(); } catch(err){ toast("That file did not load: " + err.message); } }; rd.readAsText(f); }); v.querySelector("#dlCards").addEventListener("click", () => download(safeName()+"-agent-cards.json", agentCardsJson(), "application/json")); v.querySelector("#cpCards").addEventListener("click", () => copyText(agentCardsJson(), "Agent cards copied")); v.querySelector("#dlRouter").addEventListener("click", () => download(safeName()+"-router-config.json", routerConfigJson(), "application/json")); v.querySelector("#cpRouter").addEventListener("click", () => copyText(routerConfigJson(), "Router config copied")); v.querySelector("#dlIntents").addEventListener("click", () => download(safeName()+"-intent-table.csv", intentTableCsv(), "text/csv")); v.querySelector("#dlSvg").addEventListener("click", () => download(safeName()+"-architecture.svg", svgArchitecture(true), "image/svg+xml")); v.querySelector("#dlSchema").addEventListener("click", () => download(safeName()+"-contract.schema.json", jsonSchemaDoc(), "application/json")); v.querySelector("#cpSchema").addEventListener("click", () => copyText(jsonSchemaDoc(), "Contract schema copied")); v.querySelector("#dlPrompts").addEventListener("click", () => download(safeName()+"-system-prompts.md", systemPromptsText(), "text/markdown")); v.querySelector("#dlQual").addEventListener("click", () => download(safeName()+"-qualification-pack.md", qualificationPackText(), "text/markdown")); v.querySelector("#cpQual").addEventListener("click", () => copyText(qualificationPackText(), "Qualification pack copied")); v.querySelector("#cpPrompts").addEventListener("click", () => copyText(systemPromptsText(), "System prompts copied")); v.querySelector("#dlN8n").addEventListener("click", () => download(safeName()+"-n8n-workflow.json", n8nWorkflowJson(), "application/json")); v.querySelector("#dlLang").addEventListener("click", () => download(safeName()+"-langgraph.py", langGraphPy(), "text/x-python")); v.querySelector("#dlMcp").addEventListener("click", () => download(safeName()+"-mcp-server.py", mcpServerStub(), "text/x-python")); v.querySelector("#dlExec").addEventListener("click", () => download(safeName()+"-one-pager.html", execOnePagerDoc(), "text/html")); const cpIntents = v.querySelector("#cpIntents"); if(cpIntents) cpIntents.addEventListener("click", () => copyText(intentTableCsv(), "Intent table copied")); const cpN8n = v.querySelector("#cpN8n"); if(cpN8n) cpN8n.addEventListener("click", () => copyText(n8nWorkflowJson(), "n8n workflow copied")); const cpLang = v.querySelector("#cpLang"); if(cpLang) cpLang.addEventListener("click", () => copyText(langGraphPy(), "LangGraph scaffold copied")); const cpMcp = v.querySelector("#cpMcp"); if(cpMcp) cpMcp.addEventListener("click", () => copyText(mcpServerStub(), "MCP server stub copied")); const cpExec = v.querySelector("#cpExec"); if(cpExec) cpExec.addEventListener("click", () => copyText(execOnePagerDoc(), "One-pager HTML copied")); const dlReadme = v.querySelector("#dlReadme"); if(dlReadme) dlReadme.addEventListener("click", () => download(safeName()+"-README.md", handoffReadme(), "text/markdown")); const cpReadme = v.querySelector("#cpReadme"); if(cpReadme) cpReadme.addEventListener("click", () => copyText(handoffReadme(), "Handoff README copied")); } /* ---------- focus preservation across re-renders ---------- */ function focusSelector(el){ if(!el || !el.tagName) return null; const tag = String(el.tagName).toLowerCase(); if(!/^(input|textarea|select|button)$/.test(tag)) return null; if(el.id) return "#" + el.id; let sel = tag; const cls = (typeof el.className === "string" ? el.className : "").trim().split(/\s+/).filter(Boolean); cls.forEach(c => { if(/^[A-Za-z0-9_-]+$/.test(c)) sel += "." + c; }); const ds = el.dataset || {}; Object.keys(ds).forEach(k => { const attr = "data-" + k.replace(/[A-Z]/g, m => "-" + m.toLowerCase()); sel += '[' + attr + '="' + String(ds[k]).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]'; }); return sel; } function captureFocus(){ const ae = (typeof document !== "undefined" && document.activeElement) ? document.activeElement : null; const view = elView(); if(!ae || !view || typeof view.contains !== "function" || !view.contains(ae)) return null; const sel = focusSelector(ae); if(!sel) return null; const f = { sel: sel }; try { if(typeof ae.selectionStart === "number"){ f.s = ae.selectionStart; f.e = ae.selectionEnd; } } catch(e){} return f; } function restoreFocus(f){ if(!f) return; const view = elView(); if(!view || typeof view.querySelector !== "function") return; let el = null; try { el = view.querySelector(f.sel); } catch(e){ return; } if(!el || typeof el.focus !== "function") return; try { el.focus({ preventScroll: true }); } catch(e){ try { el.focus(); } catch(e2){} } if(typeof f.s === "number" && typeof el.setSelectionRange === "function"){ try { el.setSelectionRange(f.s, f.e); } catch(e){} } } /* ---------- render dispatch ---------- */ function render(){ var _bn=document.getElementById("bootnote"); if(_bn)_bn.remove(); // the app is alive: clear the static loading note const _focus = captureFocus(); renderTabs(); const v = elView(); if(state.tab==="start") renderStart(v); else if(state.tab==="cdm") renderCdm(v); else if(state.tab==="agents") renderAgents(v); else if(state.tab==="router") renderRouter(v); else if(state.tab==="grounding") renderGrounding(v); else if(state.tab==="validate") renderValidate(v); else if(state.tab==="visualize") renderVisualize(v); else if(state.tab==="governance") renderGovernance(v); else if(state.tab==="export") renderExport(v); appendGlossary(v); restoreFocus(_focus); autosave(); } /* ---------- boot ---------- */ document.addEventListener("keydown", e => { const tag = (e.target && e.target.tagName) || ""; if(/^(INPUT|TEXTAREA|SELECT)$/.test(tag) || (e.target && e.target.isContentEditable)) return; if((e.ctrlKey || e.metaKey) && !e.shiftKey && (e.key==="z" || e.key==="Z")){ e.preventDefault(); undo(); } else if((e.ctrlKey || e.metaKey) && (e.key==="y" || e.key==="Y" || (e.shiftKey && (e.key==="z" || e.key==="Z")))){ e.preventDefault(); redo(); } }); loadProjects(); if(loadFromHash()){ toast("Loaded a shared design"); } else if(!state.projectId && !state.model.domains.length && !state.model.agents.length){ loadExample(); } render(); /* ---------- cross-tool handoff: the CDM or UNS Designer left a model here via localStorage (same origin), replacing the download-a-file-then-upload-it dance. The key is consumed on import or dismissal, and ignored after an hour so a forgotten handoff can't ambush a later session. ---------- */ (function(){ var h = null; try{ h = JSON.parse(localStorage.getItem("umda_handoff") || "null"); }catch(e){} if(!h || h.to !== "agent-designer" || !h.payload) return; if(Date.now() - (h.at || 0) > 36e5){ try{ localStorage.removeItem("umda_handoff"); }catch(e){} return; } var bar = document.createElement("div"); bar.className = "handoff-bar"; bar.setAttribute("role", "status"); bar.innerHTML = 'Model received from the ' + esc(h.from || "another designer") + '' + (h.name ? (': ' + esc(h.name)) : '') + ' — import it into this design?' + '' + ''; var tabs = document.getElementById("tabs"); tabs.parentNode.insertBefore(bar, tabs); function done(){ try{ localStorage.removeItem("umda_handoff"); }catch(e){} if(bar.parentNode) bar.parentNode.removeChild(bar); } document.getElementById("hbImport").addEventListener("click", function(){ try{ pushUndo(); var n = importEntitiesFromUns(h.payload); state.tab = "cdm"; done(); render(); toast(n ? ("Imported " + n + " entit" + (n === 1 ? "y" : "ies") + " from the " + (h.from || "designer")) : "No new entities in the handoff (already in the catalog)"); }catch(err){ toast("Import failed: " + err.message); } }); document.getElementById("hbLater").addEventListener("click", done); })(); /* ---------- shared plant workspace: one plant identity across the UNS / CDM / Agent designers. This tool contributes the agents section (readiness score + agent cards with autonomy levels). ---------- */ const WS_TOOL = "agent"; function wsRead(){ try{ const w = JSON.parse(localStorage.getItem("umda_plant") || "null"); return (w && typeof w === "object") ? w : null; }catch(e){ return null; } } function wsWrite(w){ try{ localStorage.setItem("umda_plant", JSON.stringify(w)); }catch(e){} } function wsReady(){ return state.model.agents.length > 0 && !fleetFindings().some(c => c.sev === "high"); } function wsDesignName(){ return state.model.name || ""; } function wsFragment(){ const a = assessFleet(), agents = state.model.agents, leads = topAgents().length, g = state.model.governance || {}; let md = "## AI Agents & Router\n\n" + "System: **" + (state.model.name || "unnamed") + "** — " + agents.length + " agent" + (agents.length === 1 ? "" : "s") + " (" + leads + " lead" + (leads === 1 ? "" : "s") + ") across " + state.model.domains.length + " domains; " + state.model.router.intents.length + " routed use cases. Readiness **" + a.overall + "/100 (" + grade(a.overall) + ")**." + (g.owner ? (" System owner: " + g.owner + ".") : "") + "\n"; if(agents.length){ md += "\n### Agent cards\n\n"; agents.forEach(ag => { const p = agentParent(ag), lv = agentLevel(ag); md += "- **" + ag.name + "** (" + (p ? ("worker under " + p.name) : "lead") + ", " + (domainName(rootDomainId(ag)) || "unassigned") + ")" + (lv !== null ? (" — autonomy Level " + lv + " · " + LEVEL_BY_ID[lv].short) : "") + (ag.tier && TIER_BY_ID[ag.tier] ? (", " + TIER_BY_ID[ag.tier].label + " tier") : "") + ". " + "Writes: " + ((ag.writes || []).map(entityLabel).join(", ") || "none") + "." + (ag.guardrails ? (" Guardrails: " + ag.guardrails + ".") : "") + "\n"; }); } return md; } let _wsLast = 0; function wsUpdate(){ const w = wsRead(); if(!w || !w.name) return; const now = Date.now(); if(now - _wsLast < 1500) return; _wsLast = now; try{ w.tools = w.tools || {}; w.tools[WS_TOOL] = { status: wsReady() ? "ready" : "in progress", design: wsDesignName(), at: now, fragment: wsFragment() }; wsWrite(w); wsStrip(); }catch(e){} } function wsBlueprintMd(){ const w = wsRead(); if(!w || !w.name) return ""; const order = [["edge","Edge Intelligence Hub","Edge Node Planner"],["uns","Unified Namespace","UNS Designer"],["cdm","Common Data Model","CDM Designer"],["agent","AI Agents & Router","Agent Designer"]]; let md = "# UMDA Blueprint — " + w.name + "\n\n"; md += "Generated " + new Date().toISOString().slice(0,10) + " by the UMDA design tools (https://umda.info/tools.html). Sensor to agent, end to end: the edge that earns the data, the namespace that names it, the model that types it, and the agents that consume it.\n\n"; md += "| Layer | Tool | Status | Design |\n|---|---|---|---|\n"; order.forEach(o => { const r = w.tools && w.tools[o[0]]; md += "| " + o[1] + " | " + o[2] + " | " + (r ? (r.status === "ready" ? "Ready" : "In progress") : "Not started") + " | " + ((r && r.design) || "—") + " |\n"; }); order.forEach(o => { const r = w.tools && w.tools[o[0]]; md += "\n---\n\n" + (r && r.fragment ? r.fragment : "## " + o[1] + "\n\n_Not designed yet: open the " + o[2] + " and save a design while this workspace is active._") + "\n"; }); return md; } function wsStrip(){ let bar = document.getElementById("plantStrip"); if(!bar){ bar = document.createElement("div"); bar.id = "plantStrip"; bar.className = "plant-strip"; const tabs = document.getElementById("tabs"); tabs.parentNode.insertBefore(bar, tabs); } const w = wsRead(); const chip = (k, lbl) => { const r = w && w.tools && w.tools[k]; const s = !r ? "—" : (r.status === "ready" ? "✓" : "in progress"); return '' + lbl + ' ' + s + ''; }; if(!w || !w.name){ bar.innerHTML = 'Shared plant workspace:One name across the UNS, CDM and Agent designers — each shows the others’ progress, and any of them can export the combined UMDA Blueprint.'; bar.querySelector("#psStart").addEventListener("click", () => { const v = bar.querySelector("#psName").value.trim(); if(!v){ toast("Give the plant a name first"); return; } wsWrite({ name: v, tools: {} }); _wsLast = 0; wsUpdate(); wsStrip(); toast("Workspace started: the designers now all contribute to “" + v + "”"); }); bar.querySelector("#psName").addEventListener("keydown", (e) => { if(e.key === "Enter") bar.querySelector("#psStart").click(); }); return; } bar.innerHTML = 'Plant:' + esc(w.name) + '' + chip("uns","UNS") + chip("cdm","CDM") + chip("agent","Agents") + chip("edge","Edge") + ''; bar.querySelector("#psBlueprint").addEventListener("click", () => { const md = wsBlueprintMd(); if(!md) return; download((w.name.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"") || "plant") + "-umda-blueprint.md", md, "text/markdown"); toast("Blueprint downloaded: the whole plant design in one document"); }); bar.querySelector("#psRename").addEventListener("click", () => { const v = prompt("Plant name (empty removes the shared workspace):", w.name); if(v === null) return; const nv = v.trim(); if(!nv){ try{ localStorage.removeItem("umda_plant"); }catch(e){} wsStrip(); toast("Workspace removed"); return; } w.name = nv; wsWrite(w); wsStrip(); }); } wsUpdate(); wsStrip();