|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Draw the README architecture diagram, docs/assets/architecture.svg. |
| 3 | +
|
| 4 | +Edit the layout below and run this script; never edit the SVG by hand. The SVG |
| 5 | +embeds IBM Plex Sans from Web's @fontsource dependency, so GitHub and the |
| 6 | +website render the same text metrics: install Web's packages first |
| 7 | +(`npm ci --prefix apps/web`). |
| 8 | +""" |
| 9 | + |
| 10 | +import base64 |
| 11 | +from html import escape |
| 12 | +from pathlib import Path |
| 13 | +import struct |
| 14 | +import zlib |
| 15 | + |
| 16 | +ROOT = Path(__file__).resolve().parent.parent |
| 17 | +OUTPUT = ROOT / "docs/assets/architecture.svg" |
| 18 | +FONTS = ROOT / "apps/web/node_modules/@fontsource/ibm-plex-sans/files" |
| 19 | +WIDTH, HEIGHT = 1560, 1270 |
| 20 | + |
| 21 | +# Text roles: font weight and size in px. |
| 22 | +TYPE = { |
| 23 | + "rt": (600, 17), # region title |
| 24 | + "rs": (400, 14), # region note |
| 25 | + "t1": (600, 17), # card title |
| 26 | + "t2": (400, 14), # card text and free notes |
| 27 | + "pl": (600, 12), # protocol pill |
| 28 | + "lg": (400, 13.5), # legend |
| 29 | +} |
| 30 | +PILL_HEIGHT, PILL_PADDING = 22, 22 |
| 31 | +RADIUS = {"canvas": 18, "region": 10, "card": 7, "pill": 11} |
| 32 | + |
| 33 | +# Colors follow the README banner: sand control plane, sea-mist compute, |
| 34 | +# sunlit E2B, olive user machine and stone storage. |
| 35 | +CSS = """ |
| 36 | +.canvas{fill:#f9f5ee} |
| 37 | +.region{stroke-width:1.2} |
| 38 | +.control{fill:#f3eadb;stroke:#d5be97}.control+g .band{fill:#ece0ca} |
| 39 | +.compute{fill:#e9eff0;stroke:#a6bdc3}.compute+g .band{fill:#dce7e9} |
| 40 | +.sandbox{fill:#f7fafa;stroke:#6f929d}.sandbox+g .band{fill:#e8f0f1} |
| 41 | +.managed{fill:#fbf1de;stroke:#d6a457;stroke-dasharray:6 5}.managed+g .band{fill:#f6e5c4} |
| 42 | +.user{fill:#eef0e4;stroke:#b0b68b}.user+g .band{fill:#e3e7d3} |
| 43 | +.storage{fill:#f1ebe2;stroke:#bfaa8e}.storage+g .band{fill:#e8dfd1} |
| 44 | +.external{fill:#f4f2ee;stroke:#cbc3b7}.external+g .band{fill:#ebe7e0} |
| 45 | +.sep{stroke:rgba(60,48,30,.14);stroke-width:1} |
| 46 | +.card{fill:#fffdf9;stroke:#dcd2c3;stroke-width:1} |
| 47 | +.rt,.t1{fill:#1f1c17}.rs,.t2,.lg{fill:#675e51} |
| 48 | +.pill rect{fill:#24211c}.pl{fill:#f7f1e6} |
| 49 | +.line{fill:none;stroke:#5b5346;stroke-width:1.4}.dash{stroke-dasharray:6 5} |
| 50 | +.arrow,.dot{fill:#5b5346} |
| 51 | +text{text-rendering:geometricPrecision;font-kerning:normal} |
| 52 | +""" |
| 53 | + |
| 54 | + |
| 55 | +def woff_advances(data): |
| 56 | + """Return units per em and a code point to advance width map of a WOFF 1.0 font.""" |
| 57 | + tables = {} |
| 58 | + for i in range(struct.unpack_from(">H", data, 12)[0]): |
| 59 | + tag, offset, length, original, _ = struct.unpack_from(">4s4I", data, 44 + 20 * i) |
| 60 | + raw = data[offset:offset + length] |
| 61 | + tables[tag.decode()] = zlib.decompress(raw) if length < original else raw |
| 62 | + units = struct.unpack_from(">H", tables["head"], 18)[0] |
| 63 | + count = struct.unpack_from(">H", tables["hhea"], 34)[0] |
| 64 | + widths = struct.unpack_from(f">{2 * count}H", tables["hmtx"])[0::2] |
| 65 | + cmap = tables["cmap"] |
| 66 | + for i in range(struct.unpack_from(">H", cmap, 2)[0]): |
| 67 | + platform, encoding, offset = struct.unpack_from(">HHI", cmap, 4 + 8 * i) |
| 68 | + if (platform, encoding) == (3, 1) and struct.unpack_from(">H", cmap, offset)[0] == 4: |
| 69 | + break |
| 70 | + else: |
| 71 | + raise ValueError("font has no Unicode BMP character map") |
| 72 | + segments = struct.unpack_from(">H", cmap, offset + 6)[0] // 2 |
| 73 | + ends = struct.unpack_from(f">{segments}H", cmap, offset + 14) |
| 74 | + starts = struct.unpack_from(f">{segments}H", cmap, offset + 16 + 2 * segments) |
| 75 | + deltas = struct.unpack_from(f">{segments}H", cmap, offset + 16 + 4 * segments) |
| 76 | + ranges_at = offset + 16 + 6 * segments |
| 77 | + ranges = struct.unpack_from(f">{segments}H", cmap, ranges_at) |
| 78 | + advances = {} |
| 79 | + for k in range(segments): |
| 80 | + for code in range(starts[k], min(ends[k], 0xFFFE) + 1): |
| 81 | + glyph = code |
| 82 | + if ranges[k]: |
| 83 | + glyph = struct.unpack_from(">H", cmap, ranges_at + 2 * k + ranges[k] + 2 * (code - starts[k]))[0] |
| 84 | + if not glyph: |
| 85 | + continue |
| 86 | + glyph = (glyph + deltas[k]) & 0xFFFF |
| 87 | + advances[code] = widths[min(glyph, count - 1)] |
| 88 | + return units, advances |
| 89 | + |
| 90 | + |
| 91 | +class Diagram: |
| 92 | + def __init__(self): |
| 93 | + self.parts = [] |
| 94 | + self.fonts = {} |
| 95 | + self.overflows = [] |
| 96 | + for weight in {weight for weight, _ in TYPE.values()}: |
| 97 | + data = (FONTS / f"ibm-plex-sans-latin-{weight}-normal.woff").read_bytes() |
| 98 | + self.fonts[weight] = (data, *woff_advances(data)) |
| 99 | + |
| 100 | + def measure(self, s, role): |
| 101 | + weight, size = TYPE[role] |
| 102 | + _, units, advances = self.fonts[weight] |
| 103 | + missing = [c for c in s if ord(c) not in advances] |
| 104 | + if missing: |
| 105 | + raise ValueError(f"IBM Plex Sans Latin has no glyph for {missing!r} in {s!r}") |
| 106 | + return sum(advances[ord(c)] for c in s) / units * size |
| 107 | + |
| 108 | + def text(self, x, y, s, role, anchor="start", bounds=None): |
| 109 | + width = self.measure(s, role) |
| 110 | + if bounds: |
| 111 | + left = {"start": x, "middle": x - width / 2, "end": x - width}[anchor] |
| 112 | + if left < bounds[0] or left + width > bounds[1]: |
| 113 | + self.overflows.append(s) |
| 114 | + a = "" if anchor == "start" else f' text-anchor="{anchor}"' |
| 115 | + self.parts.append(f'<text x="{x:g}" y="{y:g}" class="{role}"{a}>{escape(s)}</text>') |
| 116 | + return width |
| 117 | + |
| 118 | + def card(self, x, y, w, h, title, text): |
| 119 | + self.parts.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{RADIUS["card"]}" class="card"/>') |
| 120 | + self.text(x + 16, y + h / 2 - 3, title, "t1", bounds=(x, x + w - 12)) |
| 121 | + self.text(x + 16, y + h / 2 + 18, text, "t2", bounds=(x, x + w - 12)) |
| 122 | + |
| 123 | + def region(self, x, y, w, h, tone, title, note=""): |
| 124 | + """A deployment location or sandbox with a 40 px title band.""" |
| 125 | + r = RADIUS["region"] |
| 126 | + self.parts.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}" class="region {tone}"/><g>') |
| 127 | + self.parts.append(f'<path class="band" d="M{x},{y + 40} V{y + r} Q{x},{y} {x + r},{y} ' |
| 128 | + f'H{x + w - r} Q{x + w},{y} {x + w},{y + r} V{y + 40} Z"/>') |
| 129 | + self.parts.append(f'<path d="M{x},{y + 40} H{x + w}" class="sep"/>') |
| 130 | + width = self.text(x + 16, y + 27, title, "rt", bounds=(x, x + w - 16)) |
| 131 | + if note: |
| 132 | + self.text(x + w - 16, y + 27, note, "rs", "end", bounds=(x + 40 + width, x + w)) |
| 133 | + self.parts.append("</g>") |
| 134 | + |
| 135 | + def line(self, d, arrows=True, dashed=False): |
| 136 | + markers = ' marker-start="url(#arrow)" marker-end="url(#arrow)"' if arrows else "" |
| 137 | + self.parts.append(f'<path d="{d}" class="line{" dash" if dashed else ""}"{markers}/>') |
| 138 | + |
| 139 | + def pill(self, cx, cy, label, rotate=False): |
| 140 | + w = self.measure(label, "pl") + PILL_PADDING |
| 141 | + h = PILL_HEIGHT |
| 142 | + if rotate: |
| 143 | + self.parts.append(f'<g transform="translate({cx} {cy}) rotate(-90)">') |
| 144 | + cx = cy = 0 |
| 145 | + self.parts.append(f'<g class="pill"><rect x="{cx - w / 2:.1f}" y="{cy - h / 2:g}" width="{w:.1f}" ' |
| 146 | + f'height="{h}" rx="{RADIUS["pill"]}"/><text x="{cx:g}" y="{cy + TYPE["pl"][1] * 0.36:.1f}" ' |
| 147 | + f'text-anchor="middle" class="pl">{escape(label)}</text></g>') |
| 148 | + if rotate: |
| 149 | + self.parts.append("</g>") |
| 150 | + |
| 151 | + def svg(self): |
| 152 | + if self.overflows: |
| 153 | + raise ValueError(f"text overflows its box: {self.overflows!r}") |
| 154 | + css = [CSS.strip()] |
| 155 | + for weight, (data, _, _) in sorted(self.fonts.items()): |
| 156 | + css.append(f'@font-face{{font-family:"OAC Plex";font-weight:{weight};' |
| 157 | + f'src:url(data:font/woff;base64,{base64.b64encode(data).decode()}) format("woff")}}') |
| 158 | + family = '"OAC Plex",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif' |
| 159 | + for role, (weight, size) in TYPE.items(): |
| 160 | + css.append(f".{role}{{font-family:{family};font-size:{size}px;font-weight:{weight}}}") |
| 161 | + return ( |
| 162 | + "<!-- Generated by scripts/generate-architecture-diagram.py; edit that script and run it again. -->\n" |
| 163 | + f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {WIDTH} {HEIGHT}" width="{WIDTH}" height="{HEIGHT}" ' |
| 164 | + 'role="img" aria-labelledby="title"><title id="title">OpenAgentCore deployment and protocol boundaries</title>' |
| 165 | + f'<style>{"".join(css)}</style><defs><marker id="arrow" viewBox="0 0 10 10" refX="8.6" refY="5" ' |
| 166 | + 'markerWidth="7" markerHeight="7" markerUnits="userSpaceOnUse" orient="auto-start-reverse">' |
| 167 | + '<path d="M1,1.5 L9,5 L1,8.5 Z" class="arrow"/></marker></defs>' |
| 168 | + f'<rect width="{WIDTH}" height="{HEIGHT}" rx="{RADIUS["canvas"]}" class="canvas"/>' |
| 169 | + + "".join(self.parts) + "</svg>\n" |
| 170 | + ) |
| 171 | + |
| 172 | + |
| 173 | +def draw(d): |
| 174 | + # Callers and their Core APIs. |
| 175 | + for x, title, text, api in [ |
| 176 | + (80, "Applications / official SDK", "Submit work · stream results", "P1 · Agents API /v1"), |
| 177 | + (560, "Web console / operators", "Configure the platform · view status", "P2 · Core API /core/v1"), |
| 178 | + (1040, "Nodes / Runtime connections", "Machine identity · register and connect", "P3 · Machine API /api/v1"), |
| 179 | + ]: |
| 180 | + d.card(x, 28, 440, 66, title, text) |
| 181 | + d.line(f"M{x + 220},94 V194") |
| 182 | + d.pill(x + 220, 145, api) |
| 183 | + |
| 184 | + d.region(80, 194, 1400, 260, "control", "Control server · OpenAgentCore", "Web, Core and the database can share one host or run separately") |
| 185 | + d.card(104, 254, 952, 76, "Core · scheduling and state", "Accepts work, selects where it runs, controls lifecycles and records results") |
| 186 | + d.card(1104, 254, 352, 76, "PostgreSQL", "Sessions · Turns · configuration") |
| 187 | + d.line("M1056,292 H1104") |
| 188 | + d.card(104, 362, 420, 64, "Sandbox Provider adapter", "Create / suspend / resume compute") |
| 189 | + d.card(548, 362, 432, 64, "Runtime connections", "Dispatch work / receive events") |
| 190 | + d.card(1004, 362, 452, 64, "Workspace FS adapter", "Manages file storage and mount bindings on its own") |
| 191 | + |
| 192 | + # Every execution location connects through the same protocols. |
| 193 | + for cx in (400, 950, 1330): |
| 194 | + d.line(f"M{cx},454 V618") |
| 195 | + d.pill(400, 489, "P4 · Sandbox Provider · compute lifecycle") |
| 196 | + d.pill(400, 528, "P5 · Node · host management") |
| 197 | + d.pill(400, 567, "P7 · Runtime · work and events") |
| 198 | + d.pill(950, 508, "P4 · Sandbox Provider") |
| 199 | + d.pill(950, 557, "P7 · Runtime") |
| 200 | + d.pill(1330, 534, "P7 · Runtime") |
| 201 | + |
| 202 | + d.region(80, 618, 640, 302, "compute", "Compute servers × N · microsandbox") |
| 203 | + d.card(104, 672, 592, 52, "sandbox node · host manager", "Manages compute from outside the sandboxes") |
| 204 | + d.region(104, 758, 592, 138, "sandbox", "microsandbox VM × N", "Isolation boundary") |
| 205 | + d.card(128, 810, 544, 62, "Runtime + Harness + tools", "Agent loop runs in the sandbox · workspace on independent FS") |
| 206 | + |
| 207 | + d.region(780, 618, 340, 302, "managed", "E2B managed platform") |
| 208 | + d.region(800, 672, 300, 224, "sandbox", "E2B Sandbox") |
| 209 | + d.card(820, 734, 260, 62, "Runtime + Harness", "Agent and tools in the sandbox") |
| 210 | + d.text(950, 841, "Provider-owned filesystem", "t2", "middle") |
| 211 | + d.text(950, 868, "No independent FS yet", "t2", "middle") |
| 212 | + |
| 213 | + d.region(1180, 618, 300, 302, "user", "Your machine · self_hosted") |
| 214 | + d.card(1200, 734, 260, 62, "Runtime + Harness", "You install and maintain it") |
| 215 | + d.text(1330, 841, "Local files and tools", "t2", "middle") |
| 216 | + d.text(1330, 868, "Core does not create or reclaim it", "t2", "middle") |
| 217 | + |
| 218 | + # File I/O and model traffic leave the execution locations directly. |
| 219 | + d.line("M240,920 V1060") |
| 220 | + d.text(258, 976, "NFS · file reads, writes and mount", "t2") |
| 221 | + d.line("M550,920 V1010 H1330 V920", arrows=False) |
| 222 | + d.line("M950,920 V1060") |
| 223 | + d.parts.append('<circle cx="950" cy="1010" r="3.5" class="dot"/>') |
| 224 | + d.pill(1040, 1010, "P13 · Model protocol") |
| 225 | + d.text(1190, 998, "HTTP MCP", "t2") |
| 226 | + |
| 227 | + d.region(80, 1060, 640, 156, "storage", "Independent storage server", "Deployed apart from compute servers") |
| 228 | + d.card(104, 1114, 352, 76, "Persistent workspaces · NFS", "Files remain after compute is released") |
| 229 | + d.card(472, 1114, 224, 76, "Memory snapshots", "Separate retention period") |
| 230 | + d.region(780, 1060, 700, 156, "external", "External services", "Reached directly from execution environments") |
| 231 | + d.card(804, 1114, 312, 76, "Model providers", "Native model APIs") |
| 232 | + d.card(1140, 1114, 316, 76, "Remote MCP servers", "HTTP MCP") |
| 233 | + |
| 234 | + d.line("M80,410 H40 V1148 H80", dashed=True) |
| 235 | + d.pill(40, 785, "P14 · Workspace filesystem · storage lifecycle and binding", rotate=True) |
| 236 | + x = 80 |
| 237 | + for item in ["Solid: communication or mount", "Dashed: independent storage management", |
| 238 | + "Outer frame: deployment location", "Inner frame: sandbox isolation boundary"]: |
| 239 | + x += d.text(x, 1250, item, "lg", bounds=(80, WIDTH - 80)) + 2 * TYPE["lg"][1] |
| 240 | + |
| 241 | + |
| 242 | +def main(): |
| 243 | + diagram = Diagram() |
| 244 | + draw(diagram) |
| 245 | + OUTPUT.write_text(diagram.svg()) |
| 246 | + |
| 247 | + |
| 248 | +if __name__ == "__main__": |
| 249 | + main() |
0 commit comments