Skip to content

Commit b12f182

Browse files
authored
Redraw the README architecture diagram (#713)
Replace the Mermaid rendering with the deployment and protocol-boundary diagram: control server, microsandbox compute servers, E2B, self-hosted machines, independent workspace storage and external services, with each protocol labeled on its connection. scripts/generate-architecture-diagram.py owns the layout and colors and writes docs/assets/architecture.svg with IBM Plex Sans from Web's @fontsource dependency embedded, so GitHub and the website render the same text metrics. It uses only the Python standard library and rejects text that overflows its box or lacks a glyph. The website no longer frames the image in a white box, since the diagram now carries its own background.
1 parent c222381 commit b12f182

4 files changed

Lines changed: 268 additions & 4 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ Provider bootstrap, Runtime images and Harness adapters must agree on these name
149149

150150
## Branding
151151

152-
Use OpenAgentCore for public project branding. The canonical mark is `docs/assets/openagentcore-logo.svg`; Web uses its outline with cropped transparent margins, theme-aware favicon colors and dark-surface inversion. The README banner is `docs/assets/openagentcore-banner.jpeg`. The `example/parsar/` workbench keeps its own name, logo and favicon. Preserve external repository URLs and data identifiers when changing display copy.
152+
Use OpenAgentCore for public project branding. The canonical mark is `docs/assets/openagentcore-logo.svg`; Web uses its outline with cropped transparent margins, theme-aware favicon colors and dark-surface inversion. The README banner is `docs/assets/openagentcore-banner.jpeg`. The architecture diagram that README and the website show, `docs/assets/architecture.svg`, is generated by `scripts/generate-architecture-diagram.py`; change the script and run it again. The `example/parsar/` workbench keeps its own name, logo and favicon. Preserve external repository URLs and data identifiers when changing display copy.
153153

154154
## OpenAgentCore name guard
155155

‎docs/assets/architecture.svg‎

Lines changed: 17 additions & 2 deletions
Loading
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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()

‎website/.vitepress/theme/components/Landing.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@ h3 { font-family: var(--l-display); }
898898
</style>
899899

900900
<style scoped>
901-
.architecture-visual { display: block; max-width: 980px; margin: 0 auto 20px; padding: 16px; background: #fff; border-radius: 4px; }
901+
.architecture-visual { display: block; max-width: 980px; margin: 0 auto 20px; }
902902
.architecture-visual img { display: block; width: 100%; height: auto; }
903903
.more { display: inline-block; }
904904
.session-flow { display: flex; align-items: center; justify-content: center; gap: 28px; padding: 32px 24px; margin: 24px 0; border: 1px solid var(--l-line-strong); background: var(--l-panel); }

0 commit comments

Comments
 (0)