feat(viz): add comm-deploy and tool-discovery embeds for dim 2 & 3
Browse filesReplace the placeholder visuals and markdown tables in Dimensions 2
(Communication & deployment) and 3 (Tool & action model) with paired
animated SVG embeds plus polished chip-tables.
Dimension 2:
- d3-comm-deploy.html: two-panel SVG (HTTP frameworks / In-process)
with Play/Pause + speed slider, animated request/response packets
showing the network hop on the HTTP side and the inline call on the
in-process side. Auto-plays on scroll-into-view.
- comm-deploy-matrix.html: chip-styled table with grouped bands
(Shape / Deployment / Trainer-side), clickable HF Spaces and
PrimeIntellect Hub links, ✓/—/N/A pill badges.
Dimension 3:
- d3-tool-discovery.html: 6 framework cards in a responsive grid,
each with a mini sequence diagram (Trainer ↔ Env) animating
discovery → invoke phases. HTTP cards show dashed wire arrows,
in-process show solid Python calls.
- tool-action-matrix.html: polished table with Kind pill (HTTP /
In-process), code chips for endpoints and decorators, single
green ✓ for the universal multi-turn capability.
Both sections also get tighter intro/bridge prose replacing the
old bullet lists.
|
@@ -21,55 +21,33 @@ The first thing any environment author runs into is the API surface. What do I s
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
-
###
|
| 25 |
|
| 26 |
*Where does it run and how do they talk?*
|
| 27 |
|
| 28 |
-
The most fundamental architectural split: does your environment run as a **separate HTTP server** or **inside the training process**?
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
| --- | --- | --- | --- | --- | --- | --- |
|
| 36 |
-
| **Type** | HTTP server | HTTP server | HTTP server | In-process | In-process | In-process |
|
| 37 |
-
| **Protocol** | JSON-RPC 2.0 (MCP) with WebSocket support | REST + SSE | REST + cookies | Python calls | Python calls | Python calls |
|
| 38 |
-
| **Docker** | ✅ | ✅ | ✅ | — | — | — |
|
| 39 |
-
| **HF Spaces** | ✅ [Deployed](https://huggingface.co/spaces/AdithyaSK/jupyter-agent-openenv) | ✅ [Deployed](https://huggingface.co/spaces/AdithyaSK/jupyter-agent-ors) | ✅ [Deployed](https://huggingface.co/spaces/AdithyaSK/jupyter-agent-nemo-gym) | N/A | N/A | N/A |
|
| 40 |
-
| **OpenReward** | — | ✅ | — | — | — | — |
|
| 41 |
-
| **PrimeIntellect Hub** | — | — | — | ✅ (via [Prime RL](https://github.com/PrimeIntellect-ai/prime-rl)) | — | — |
|
| 42 |
-
| **Scales independently** | ✅ | ✅ | ✅ | — | — | — |
|
| 43 |
-
| **Deps on trainer side** | `openenv-core` | `requests` only | `requests` only | Full package | Full package | Full package |
|
| 44 |
-
| **Auth** | None | `X-API-Key` | Cookies | N/A | N/A | N/A |
|
| 45 |
-
|
| 46 |
-
> **[Table polish]** Convert the ✅ / — / N/A cells into colored chip badges (green pill for ✅, gray outline for —, faded for N/A). For the "HF Spaces" row, render each ✅ as a clickable chip that opens the deployed Space in a new tab — links are in the source text but the table doesn't expose them well right now.
|
| 47 |
|
| 48 |
Note: Verifiers integrates with PrimeIntellect's training stack (Prime RL) and their managed environment hub, which hosts community-contributed environments for distributed RL training.
|
| 49 |
|
| 50 |
---
|
| 51 |
|
| 52 |
-
###
|
| 53 |
|
| 54 |
*How does the model interact with the environment?*
|
| 55 |
|
| 56 |
-
All
|
| 57 |
-
|
| 58 |
-
The key distinction: **where does the tool definition live, and how is it discovered?**
|
| 59 |
|
| 60 |
-
|
| 61 |
-
- **In-process frameworks**: Tools are defined as Python functions or methods. Each framework has its own registration pattern, explicit lists, `ToolGroup` classes, or environment wrappers.
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
| | OpenEnv | ORS | NeMo Gym | Verifiers | SkyRL Gym | GEM |
|
| 66 |
-
| --- | --- | --- | --- | --- | --- | --- |
|
| 67 |
-
| **Tool definition** | `@mcp.tool` decorator on FastMCP | `@tool` decorator + Pydantic model | FastAPI `app.post()` endpoint | Plain Python functions passed to `ToolEnv(tools=[...])` | Methods on `ToolGroup` subclasses | Environment wrappers (`ToolEnvWrapper`) |
|
| 68 |
-
| **Discovery** | MCP `tools/list` endpoint | `GET /tools` HTTP endpoint | OpenAPI schema (`GET /openapi.json`) | Explicit list at init time | `init_tool_groups()` registration | Wrapper composition |
|
| 69 |
-
| **Schema format** | MCP tool spec (JSON Schema) | ORS ToolSpec | OpenAPI JSON Schema | Python type hints → auto-converted | Python type hints → auto-converted | Wrapper-defined |
|
| 70 |
-
| **Multi-turn** | ✅ (trainer loops) | ✅ (trainer loops) | ✅ (trainer loops) | ✅ (trainer loops) | ✅ (trainer loops) | ✅ (trainer loops) |
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
---
|
| 75 |
|
|
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
+
### Communication & deployment
|
| 25 |
|
| 26 |
*Where does it run and how do they talk?*
|
| 27 |
|
| 28 |
+
The most fundamental architectural split: does your environment run as a **separate HTTP server** or **inside the training process**? Everything else, the protocol, the deployment target, what you have to install on the trainer side, falls out of this single choice. Hit Play below to watch one tool call travel through each shape.
|
| 29 |
|
| 30 |
+
<HtmlEmbed src="d3-comm-deploy.html" frameless />
|
| 31 |
|
| 32 |
+
The two patterns differ on three things that show up in practice. **Where it runs:** an HTTP framework lives on its own machine, often a cheap CPU box or a Hugging Face Space, while the in-process kind shares the training GPU node. **What you install on the trainer side:** HTTP only needs an SDK or `requests`, while in-process pulls the full framework package into the training venv. **How it scales:** HTTP scales by adding server replicas behind a load balancer, in-process scales by adding more identical training workers, each with its own copy of the env. The table below lines all of that up at a glance.
|
| 33 |
|
| 34 |
+
<HtmlEmbed src="comm-deploy-matrix.html" frameless />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
Note: Verifiers integrates with PrimeIntellect's training stack (Prime RL) and their managed environment hub, which hosts community-contributed environments for distributed RL training.
|
| 37 |
|
| 38 |
---
|
| 39 |
|
| 40 |
+
### Tool & action model
|
| 41 |
|
| 42 |
*How does the model interact with the environment?*
|
| 43 |
|
| 44 |
+
All six frameworks ultimately expose the same thing to the model, a list of callable tools with names, descriptions, and typed parameters. What differs is **where the tool definition lives** and **how the trainer discovers it**. HTTP frameworks define tools on the server and ship a discovery endpoint the client hits at runtime. In-process frameworks define tools as Python functions and register them when the env constructs. After discovery, both shapes look identical: send a tool call, get back an observation.
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
<HtmlEmbed src="d3-tool-discovery.html" frameless />
|
|
|
|
| 47 |
|
| 48 |
+
The animation makes the discovery moment visible, but the table below is where the per-framework details live: what you decorate, which endpoint serves the schema, and what schema format the model sees. Multi-turn is universal, every framework loops generation against tool calls. The differences are upstream of that.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
<HtmlEmbed src="tool-action-matrix.html" frameless />
|
| 51 |
|
| 52 |
---
|
| 53 |
|
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="comm-deploy-matrix" style="width:100%;margin:14px 0;"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.comm-deploy-matrix {
|
| 4 |
+
border: 1px solid var(--border-color);
|
| 5 |
+
border-radius: 12px;
|
| 6 |
+
background: var(--surface-bg);
|
| 7 |
+
overflow: hidden;
|
| 8 |
+
color: var(--text-color);
|
| 9 |
+
}
|
| 10 |
+
.comm-deploy-matrix__head {
|
| 11 |
+
display: flex; align-items: center; gap: 12px;
|
| 12 |
+
padding: 8px 12px;
|
| 13 |
+
border-bottom: 1px solid var(--border-color);
|
| 14 |
+
background: color-mix(in oklab, var(--muted-color) 4%, transparent);
|
| 15 |
+
flex-wrap: wrap;
|
| 16 |
+
}
|
| 17 |
+
.comm-deploy-matrix__title {
|
| 18 |
+
font-size: 10.5px;
|
| 19 |
+
font-weight: 800;
|
| 20 |
+
letter-spacing: 1px;
|
| 21 |
+
text-transform: uppercase;
|
| 22 |
+
color: var(--muted-color);
|
| 23 |
+
margin-right: auto;
|
| 24 |
+
}
|
| 25 |
+
.comm-deploy-matrix__legend {
|
| 26 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 27 |
+
font-size: 10.5px; color: var(--muted-color);
|
| 28 |
+
}
|
| 29 |
+
.comm-deploy-matrix__legend .item {
|
| 30 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* status pills */
|
| 34 |
+
.cdm-pill {
|
| 35 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 36 |
+
padding: 2px 9px;
|
| 37 |
+
font-size: 11px;
|
| 38 |
+
font-weight: 700;
|
| 39 |
+
border-radius: 999px;
|
| 40 |
+
line-height: 1.4;
|
| 41 |
+
white-space: nowrap;
|
| 42 |
+
}
|
| 43 |
+
.cdm-pill.yes {
|
| 44 |
+
background: color-mix(in oklab, #22c55e 14%, transparent);
|
| 45 |
+
color: #16a34a;
|
| 46 |
+
border: 1px solid color-mix(in oklab, #22c55e 35%, transparent);
|
| 47 |
+
}
|
| 48 |
+
.cdm-pill.no {
|
| 49 |
+
color: var(--muted-color);
|
| 50 |
+
border: 1px dashed color-mix(in oklab, var(--muted-color) 50%, transparent);
|
| 51 |
+
background: transparent;
|
| 52 |
+
}
|
| 53 |
+
.cdm-pill.na {
|
| 54 |
+
color: color-mix(in oklab, var(--muted-color) 70%, var(--text-color));
|
| 55 |
+
background: color-mix(in oklab, var(--muted-color) 6%, transparent);
|
| 56 |
+
border: 1px solid color-mix(in oklab, var(--border-color) 80%, transparent);
|
| 57 |
+
font-weight: 600;
|
| 58 |
+
}
|
| 59 |
+
a.cdm-pill.yes {
|
| 60 |
+
text-decoration: none;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
transition: filter .15s ease, transform .15s ease;
|
| 63 |
+
}
|
| 64 |
+
a.cdm-pill.yes:hover { filter: brightness(1.15); }
|
| 65 |
+
a.cdm-pill.yes::after {
|
| 66 |
+
content: "↗";
|
| 67 |
+
font-size: 10px;
|
| 68 |
+
margin-left: 1px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* table */
|
| 72 |
+
.comm-deploy-matrix__wrap { overflow-x: auto; }
|
| 73 |
+
.comm-deploy-matrix table {
|
| 74 |
+
width: 100%;
|
| 75 |
+
border-collapse: collapse;
|
| 76 |
+
font-size: 12px;
|
| 77 |
+
min-width: 720px;
|
| 78 |
+
}
|
| 79 |
+
.comm-deploy-matrix table thead th {
|
| 80 |
+
text-align: left;
|
| 81 |
+
font-size: 10.5px;
|
| 82 |
+
font-weight: 700;
|
| 83 |
+
text-transform: uppercase;
|
| 84 |
+
letter-spacing: 0.6px;
|
| 85 |
+
color: var(--muted-color);
|
| 86 |
+
padding: 10px 12px;
|
| 87 |
+
border-bottom: 1px solid var(--border-color);
|
| 88 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 89 |
+
white-space: nowrap;
|
| 90 |
+
}
|
| 91 |
+
.comm-deploy-matrix table thead th .swatch {
|
| 92 |
+
width: 7px; height: 7px;
|
| 93 |
+
border-radius: 50%;
|
| 94 |
+
background: var(--c, var(--muted-color));
|
| 95 |
+
display: inline-block;
|
| 96 |
+
margin-right: 5px;
|
| 97 |
+
vertical-align: middle;
|
| 98 |
+
}
|
| 99 |
+
.comm-deploy-matrix table tbody td {
|
| 100 |
+
padding: 10px 12px;
|
| 101 |
+
border-bottom: 1px solid color-mix(in oklab, var(--border-color) 60%, transparent);
|
| 102 |
+
vertical-align: middle;
|
| 103 |
+
color: var(--text-color);
|
| 104 |
+
}
|
| 105 |
+
.comm-deploy-matrix table tbody tr:last-child td { border-bottom: 0; }
|
| 106 |
+
.comm-deploy-matrix table tbody tr:hover td {
|
| 107 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 108 |
+
}
|
| 109 |
+
.comm-deploy-matrix table tbody td.row-name {
|
| 110 |
+
font-weight: 600;
|
| 111 |
+
color: var(--text-color);
|
| 112 |
+
white-space: nowrap;
|
| 113 |
+
}
|
| 114 |
+
.comm-deploy-matrix table tbody td code {
|
| 115 |
+
background: color-mix(in oklab, var(--muted-color) 10%, transparent);
|
| 116 |
+
border-radius: 3px;
|
| 117 |
+
padding: 0 5px;
|
| 118 |
+
font-size: 11px;
|
| 119 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 120 |
+
}
|
| 121 |
+
.cdm-text { font-size: 11.5px; color: var(--text-color); }
|
| 122 |
+
.cdm-text .muted { color: var(--muted-color); }
|
| 123 |
+
.cdm-group-band {
|
| 124 |
+
background: color-mix(in oklab, var(--muted-color) 5%, transparent);
|
| 125 |
+
}
|
| 126 |
+
.cdm-group-band td {
|
| 127 |
+
padding: 5px 12px;
|
| 128 |
+
font-size: 9.5px;
|
| 129 |
+
font-weight: 800;
|
| 130 |
+
text-transform: uppercase;
|
| 131 |
+
letter-spacing: 0.7px;
|
| 132 |
+
color: var(--muted-color);
|
| 133 |
+
}
|
| 134 |
+
</style>
|
| 135 |
+
<script>
|
| 136 |
+
(() => {
|
| 137 |
+
const bootstrap = () => {
|
| 138 |
+
const scriptEl = document.currentScript;
|
| 139 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 140 |
+
if (!(container && container.classList && container.classList.contains('comm-deploy-matrix'))) {
|
| 141 |
+
const cands = Array.from(document.querySelectorAll('.comm-deploy-matrix'))
|
| 142 |
+
.filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 143 |
+
container = cands[cands.length - 1] || null;
|
| 144 |
+
}
|
| 145 |
+
if (!container || (container.dataset && container.dataset.mounted === 'true')) return;
|
| 146 |
+
container.dataset.mounted = 'true';
|
| 147 |
+
|
| 148 |
+
const FRAMEWORKS = [
|
| 149 |
+
{ key: 'openenv', name: 'OpenEnv', color: '#3b82f6', kind: 'http' },
|
| 150 |
+
{ key: 'ors', name: 'ORS', color: '#a855f7', kind: 'http' },
|
| 151 |
+
{ key: 'nemo', name: 'NeMo Gym', color: '#22c55e', kind: 'http' },
|
| 152 |
+
{ key: 'verifs', name: 'Verifiers', color: '#ec4899', kind: 'inproc' },
|
| 153 |
+
{ key: 'skyrl', name: 'SkyRL Gym', color: '#f59e0b', kind: 'inproc' },
|
| 154 |
+
{ key: 'gem', name: 'GEM', color: '#14b8a6', kind: 'inproc' },
|
| 155 |
+
];
|
| 156 |
+
|
| 157 |
+
const yes = (label='Yes') => `<span class="cdm-pill yes">✓ ${label}</span>`;
|
| 158 |
+
const no = () => `<span class="cdm-pill no">—</span>`;
|
| 159 |
+
const na = () => `<span class="cdm-pill na">N/A</span>`;
|
| 160 |
+
const link = (label, href) => `<a class="cdm-pill yes" href="${href}" target="_blank" rel="noopener">${label}</a>`;
|
| 161 |
+
const txt = (s, muted='') => `<span class="cdm-text">${s}${muted ? `<span class="muted"> · ${muted}</span>` : ''}</span>`;
|
| 162 |
+
const code = (s) => `<code>${s}</code>`;
|
| 163 |
+
|
| 164 |
+
const ROWS = [
|
| 165 |
+
{ group: 'Shape' },
|
| 166 |
+
{ name: 'Type', cells: {
|
| 167 |
+
openenv: txt('HTTP server'),
|
| 168 |
+
ors: txt('HTTP server'),
|
| 169 |
+
nemo: txt('HTTP server'),
|
| 170 |
+
verifs: txt('In-process'),
|
| 171 |
+
skyrl: txt('In-process'),
|
| 172 |
+
gem: txt('In-process'),
|
| 173 |
+
}},
|
| 174 |
+
{ name: 'Protocol', cells: {
|
| 175 |
+
openenv: txt('JSON-RPC 2.0 (MCP)', 'WebSocket'),
|
| 176 |
+
ors: txt('REST + SSE'),
|
| 177 |
+
nemo: txt('REST + cookies'),
|
| 178 |
+
verifs: txt('Python calls'),
|
| 179 |
+
skyrl: txt('Python calls'),
|
| 180 |
+
gem: txt('Python calls'),
|
| 181 |
+
}},
|
| 182 |
+
|
| 183 |
+
{ group: 'Deployment' },
|
| 184 |
+
{ name: 'Docker', cells: {
|
| 185 |
+
openenv: yes(), ors: yes(), nemo: yes(),
|
| 186 |
+
verifs: no(), skyrl: no(), gem: no(),
|
| 187 |
+
}},
|
| 188 |
+
{ name: 'HF Spaces', cells: {
|
| 189 |
+
openenv: link('OpenEnv', 'https://huggingface.co/spaces/AdithyaSK/jupyter-agent-openenv'),
|
| 190 |
+
ors: link('ORS', 'https://huggingface.co/spaces/AdithyaSK/jupyter-agent-ors'),
|
| 191 |
+
nemo: link('NeMo', 'https://huggingface.co/spaces/AdithyaSK/jupyter-agent-nemo-gym'),
|
| 192 |
+
verifs: na(), skyrl: na(), gem: na(),
|
| 193 |
+
}},
|
| 194 |
+
{ name: 'OpenReward', cells: {
|
| 195 |
+
openenv: no(), ors: yes(), nemo: no(),
|
| 196 |
+
verifs: no(), skyrl: no(), gem: no(),
|
| 197 |
+
}},
|
| 198 |
+
{ name: 'PrimeIntellect Hub', cells: {
|
| 199 |
+
openenv: no(), ors: no(), nemo: no(),
|
| 200 |
+
verifs: link('Prime RL', 'https://github.com/PrimeIntellect-ai/prime-rl'),
|
| 201 |
+
skyrl: no(), gem: no(),
|
| 202 |
+
}},
|
| 203 |
+
{ name: 'Scales independently', cells: {
|
| 204 |
+
openenv: yes(), ors: yes(), nemo: yes(),
|
| 205 |
+
verifs: no(), skyrl: no(), gem: no(),
|
| 206 |
+
}},
|
| 207 |
+
|
| 208 |
+
{ group: 'Trainer-side' },
|
| 209 |
+
{ name: 'Deps on trainer', cells: {
|
| 210 |
+
openenv: code('openenv-core'),
|
| 211 |
+
ors: code('requests') + ' only',
|
| 212 |
+
nemo: code('requests') + ' only',
|
| 213 |
+
verifs: txt('Full package'),
|
| 214 |
+
skyrl: txt('Full package'),
|
| 215 |
+
gem: txt('Full package'),
|
| 216 |
+
}},
|
| 217 |
+
{ name: 'Auth', cells: {
|
| 218 |
+
openenv: txt('None'),
|
| 219 |
+
ors: code('X-API-Key'),
|
| 220 |
+
nemo: txt('Cookies'),
|
| 221 |
+
verifs: na(),
|
| 222 |
+
skyrl: na(),
|
| 223 |
+
gem: na(),
|
| 224 |
+
}},
|
| 225 |
+
];
|
| 226 |
+
|
| 227 |
+
const headerCells = FRAMEWORKS.map(f =>
|
| 228 |
+
`<th><span class="swatch" style="--c:${f.color};"></span>${f.name}</th>`
|
| 229 |
+
).join('');
|
| 230 |
+
|
| 231 |
+
const bodyHtml = ROWS.map(r => {
|
| 232 |
+
if (r.group) {
|
| 233 |
+
return `<tr class="cdm-group-band"><td colspan="${FRAMEWORKS.length + 1}">${r.group}</td></tr>`;
|
| 234 |
+
}
|
| 235 |
+
const cells = FRAMEWORKS.map(f => `<td>${r.cells[f.key]}</td>`).join('');
|
| 236 |
+
return `<tr><td class="row-name">${r.name}</td>${cells}</tr>`;
|
| 237 |
+
}).join('');
|
| 238 |
+
|
| 239 |
+
container.innerHTML = `
|
| 240 |
+
<div class="comm-deploy-matrix__head">
|
| 241 |
+
<span class="comm-deploy-matrix__title">Communication & deployment · 6 frameworks</span>
|
| 242 |
+
<span class="comm-deploy-matrix__legend">
|
| 243 |
+
<span class="item"><span class="cdm-pill yes" style="padding:1px 7px;">✓</span> ships</span>
|
| 244 |
+
<span class="item"><span class="cdm-pill no" style="padding:1px 8px;">—</span> no</span>
|
| 245 |
+
<span class="item"><span class="cdm-pill na" style="padding:1px 7px;">N/A</span> not applicable</span>
|
| 246 |
+
</span>
|
| 247 |
+
</div>
|
| 248 |
+
<div class="comm-deploy-matrix__wrap">
|
| 249 |
+
<table>
|
| 250 |
+
<thead>
|
| 251 |
+
<tr>
|
| 252 |
+
<th>Capability</th>
|
| 253 |
+
${headerCells}
|
| 254 |
+
</tr>
|
| 255 |
+
</thead>
|
| 256 |
+
<tbody>${bodyHtml}</tbody>
|
| 257 |
+
</table>
|
| 258 |
+
</div>
|
| 259 |
+
`;
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
if (document.readyState === 'loading') {
|
| 263 |
+
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
| 264 |
+
} else {
|
| 265 |
+
bootstrap();
|
| 266 |
+
}
|
| 267 |
+
})();
|
| 268 |
+
</script>
|
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-comm-deploy" style="width:100%;margin:14px 0;"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-comm-deploy {
|
| 4 |
+
position: relative;
|
| 5 |
+
border: 1px solid var(--border-color);
|
| 6 |
+
border-radius: 12px;
|
| 7 |
+
background: var(--surface-bg);
|
| 8 |
+
overflow: hidden;
|
| 9 |
+
color: var(--text-color);
|
| 10 |
+
--c-http: #3b82f6;
|
| 11 |
+
--c-inproc: #ec4899;
|
| 12 |
+
--c-flow: var(--primary-color, #6366f1);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* ── Header ── */
|
| 16 |
+
.d3-comm-deploy__header {
|
| 17 |
+
display: flex; flex-wrap: wrap; align-items: center;
|
| 18 |
+
gap: 12px 16px; padding: 12px 16px;
|
| 19 |
+
border-bottom: 1px solid var(--border-color);
|
| 20 |
+
}
|
| 21 |
+
.d3-comm-deploy__title {
|
| 22 |
+
font-size: 11px; font-weight: 800; letter-spacing: 1.2px;
|
| 23 |
+
text-transform: uppercase; color: var(--muted-color);
|
| 24 |
+
margin-right: auto;
|
| 25 |
+
}
|
| 26 |
+
.d3-comm-deploy__btn {
|
| 27 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 28 |
+
padding: 6px 12px; border-radius: 7px;
|
| 29 |
+
border: 1px solid var(--border-color);
|
| 30 |
+
background: var(--surface-bg); color: var(--text-color);
|
| 31 |
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
| 32 |
+
transition: border-color .12s ease, background .12s ease;
|
| 33 |
+
}
|
| 34 |
+
.d3-comm-deploy__btn:hover { border-color: var(--primary-color); }
|
| 35 |
+
.d3-comm-deploy__btn.primary {
|
| 36 |
+
border-color: var(--primary-color);
|
| 37 |
+
background: color-mix(in oklab, var(--primary-color) 12%, var(--surface-bg));
|
| 38 |
+
}
|
| 39 |
+
.d3-comm-deploy__btn svg { width: 12px; height: 12px; }
|
| 40 |
+
.d3-comm-deploy__speed {
|
| 41 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 42 |
+
font-size: 11px; color: var(--muted-color);
|
| 43 |
+
}
|
| 44 |
+
.d3-comm-deploy__speed input[type=range] {
|
| 45 |
+
width: 100px;
|
| 46 |
+
accent-color: var(--primary-color);
|
| 47 |
+
}
|
| 48 |
+
.d3-comm-deploy__speed-val {
|
| 49 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 50 |
+
color: var(--text-color); font-size: 11px;
|
| 51 |
+
min-width: 36px; text-align: right;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ── Body grid: two panels side by side ── */
|
| 55 |
+
.d3-comm-deploy__body {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 1fr;
|
| 58 |
+
gap: 0;
|
| 59 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 60 |
+
}
|
| 61 |
+
.d3-comm-deploy__panel {
|
| 62 |
+
padding: 14px 16px 16px 16px;
|
| 63 |
+
display: flex; flex-direction: column; gap: 10px;
|
| 64 |
+
min-width: 0;
|
| 65 |
+
}
|
| 66 |
+
.d3-comm-deploy__panel + .d3-comm-deploy__panel {
|
| 67 |
+
border-left: 1px solid var(--border-color);
|
| 68 |
+
}
|
| 69 |
+
@media (max-width: 720px) {
|
| 70 |
+
.d3-comm-deploy__body { grid-template-columns: 1fr; }
|
| 71 |
+
.d3-comm-deploy__panel + .d3-comm-deploy__panel {
|
| 72 |
+
border-left: 0;
|
| 73 |
+
border-top: 1px solid var(--border-color);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.d3-comm-deploy__panel-head {
|
| 78 |
+
display: flex; align-items: center; gap: 8px;
|
| 79 |
+
font-size: 11px; font-weight: 700;
|
| 80 |
+
letter-spacing: 0.6px; text-transform: uppercase;
|
| 81 |
+
color: var(--text-color);
|
| 82 |
+
}
|
| 83 |
+
.d3-comm-deploy__panel-head .swatch {
|
| 84 |
+
width: 8px; height: 8px; border-radius: 50%;
|
| 85 |
+
background: var(--c, var(--muted-color));
|
| 86 |
+
flex-shrink: 0;
|
| 87 |
+
}
|
| 88 |
+
.d3-comm-deploy__panel-head .tag {
|
| 89 |
+
margin-left: auto;
|
| 90 |
+
font-size: 10px; font-weight: 700;
|
| 91 |
+
color: var(--muted-color);
|
| 92 |
+
text-transform: uppercase;
|
| 93 |
+
letter-spacing: 0.4px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* ── SVG stage ── */
|
| 97 |
+
.d3-comm-deploy__stage {
|
| 98 |
+
position: relative;
|
| 99 |
+
aspect-ratio: 16 / 9;
|
| 100 |
+
border-radius: 10px;
|
| 101 |
+
background: var(--surface-bg);
|
| 102 |
+
border: 1px solid var(--border-color);
|
| 103 |
+
overflow: hidden;
|
| 104 |
+
min-height: 200px;
|
| 105 |
+
}
|
| 106 |
+
.d3-comm-deploy__stage svg {
|
| 107 |
+
width: 100%; height: 100%; display: block;
|
| 108 |
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
| 109 |
+
}
|
| 110 |
+
.d3-comm-deploy__stage .box {
|
| 111 |
+
fill: color-mix(in oklab, var(--muted-color) 5%, var(--surface-bg));
|
| 112 |
+
stroke: var(--border-color);
|
| 113 |
+
stroke-width: 1.2;
|
| 114 |
+
}
|
| 115 |
+
.d3-comm-deploy__stage .box.outer {
|
| 116 |
+
fill: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 117 |
+
stroke-dasharray: 0;
|
| 118 |
+
}
|
| 119 |
+
.d3-comm-deploy__stage .box.tinted-http {
|
| 120 |
+
fill: color-mix(in oklab, var(--c-http) 8%, var(--surface-bg));
|
| 121 |
+
stroke: color-mix(in oklab, var(--c-http) 35%, var(--border-color));
|
| 122 |
+
}
|
| 123 |
+
.d3-comm-deploy__stage .box.tinted-env {
|
| 124 |
+
fill: color-mix(in oklab, #22c55e 8%, var(--surface-bg));
|
| 125 |
+
stroke: color-mix(in oklab, #22c55e 35%, var(--border-color));
|
| 126 |
+
}
|
| 127 |
+
.d3-comm-deploy__stage .box.tinted-inproc {
|
| 128 |
+
fill: color-mix(in oklab, var(--c-inproc) 8%, var(--surface-bg));
|
| 129 |
+
stroke: color-mix(in oklab, var(--c-inproc) 35%, var(--border-color));
|
| 130 |
+
}
|
| 131 |
+
.d3-comm-deploy__stage .box.backend {
|
| 132 |
+
fill: color-mix(in oklab, var(--muted-color) 4%, transparent);
|
| 133 |
+
stroke: var(--border-color);
|
| 134 |
+
stroke-dasharray: 4 3;
|
| 135 |
+
}
|
| 136 |
+
.d3-comm-deploy__stage .label-title {
|
| 137 |
+
fill: var(--text-color);
|
| 138 |
+
font-size: 11px; font-weight: 700;
|
| 139 |
+
}
|
| 140 |
+
.d3-comm-deploy__stage .label-sub {
|
| 141 |
+
fill: var(--muted-color);
|
| 142 |
+
font-size: 9px; font-weight: 500;
|
| 143 |
+
}
|
| 144 |
+
.d3-comm-deploy__stage .arrow {
|
| 145 |
+
stroke: var(--muted-color);
|
| 146 |
+
stroke-width: 1.4;
|
| 147 |
+
fill: none;
|
| 148 |
+
}
|
| 149 |
+
.d3-comm-deploy__stage .arrow.dashed {
|
| 150 |
+
stroke-dasharray: 4 3;
|
| 151 |
+
}
|
| 152 |
+
.d3-comm-deploy__stage .arrow-label {
|
| 153 |
+
fill: var(--muted-color);
|
| 154 |
+
font-size: 8.5px;
|
| 155 |
+
font-weight: 600;
|
| 156 |
+
letter-spacing: 0.4px;
|
| 157 |
+
text-transform: uppercase;
|
| 158 |
+
}
|
| 159 |
+
/* travelling packets */
|
| 160 |
+
.d3-comm-deploy__stage .packet {
|
| 161 |
+
fill: var(--c, var(--c-flow));
|
| 162 |
+
opacity: 0;
|
| 163 |
+
}
|
| 164 |
+
.d3-comm-deploy.playing .packet {
|
| 165 |
+
animation-play-state: running;
|
| 166 |
+
}
|
| 167 |
+
.d3-comm-deploy:not(.playing) .packet {
|
| 168 |
+
animation-play-state: paused;
|
| 169 |
+
opacity: 0;
|
| 170 |
+
}
|
| 171 |
+
/* HTTP panel: long-distance packets */
|
| 172 |
+
.d3-comm-deploy .packet.http-out {
|
| 173 |
+
animation: dcd-http-out var(--dur, 1.6s) linear infinite;
|
| 174 |
+
}
|
| 175 |
+
.d3-comm-deploy .packet.http-in {
|
| 176 |
+
animation: dcd-http-in var(--dur, 1.6s) linear infinite;
|
| 177 |
+
animation-delay: calc(var(--dur, 1.6s) * 0.5);
|
| 178 |
+
}
|
| 179 |
+
@keyframes dcd-http-out {
|
| 180 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 181 |
+
8% { opacity: 1; }
|
| 182 |
+
92% { opacity: 1; }
|
| 183 |
+
100% { transform: translate(120px, 0); opacity: 0; }
|
| 184 |
+
}
|
| 185 |
+
@keyframes dcd-http-in {
|
| 186 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 187 |
+
8% { opacity: 1; }
|
| 188 |
+
92% { opacity: 1; }
|
| 189 |
+
100% { transform: translate(-120px, 0); opacity: 0; }
|
| 190 |
+
}
|
| 191 |
+
.d3-comm-deploy .packet.http-back {
|
| 192 |
+
animation: dcd-http-back var(--dur, 1.6s) linear infinite;
|
| 193 |
+
animation-delay: calc(var(--dur, 1.6s) * 0.25);
|
| 194 |
+
}
|
| 195 |
+
@keyframes dcd-http-back {
|
| 196 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 197 |
+
8% { opacity: 0.85; }
|
| 198 |
+
92% { opacity: 0.85; }
|
| 199 |
+
100% { transform: translate(0, 56px); opacity: 0; }
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* In-process panel: short hop packets */
|
| 203 |
+
.d3-comm-deploy .packet.ip-out {
|
| 204 |
+
animation: dcd-ip-out var(--dur, 1.6s) linear infinite;
|
| 205 |
+
}
|
| 206 |
+
.d3-comm-deploy .packet.ip-in {
|
| 207 |
+
animation: dcd-ip-in var(--dur, 1.6s) linear infinite;
|
| 208 |
+
animation-delay: calc(var(--dur, 1.6s) * 0.5);
|
| 209 |
+
}
|
| 210 |
+
@keyframes dcd-ip-out {
|
| 211 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 212 |
+
10% { opacity: 1; }
|
| 213 |
+
90% { opacity: 1; }
|
| 214 |
+
100% { transform: translate(40px, 0); opacity: 0; }
|
| 215 |
+
}
|
| 216 |
+
@keyframes dcd-ip-in {
|
| 217 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 218 |
+
10% { opacity: 1; }
|
| 219 |
+
90% { opacity: 1; }
|
| 220 |
+
100% { transform: translate(-40px, 0); opacity: 0; }
|
| 221 |
+
}
|
| 222 |
+
.d3-comm-deploy .packet.ip-back {
|
| 223 |
+
animation: dcd-ip-back var(--dur, 1.6s) linear infinite;
|
| 224 |
+
animation-delay: calc(var(--dur, 1.6s) * 0.25);
|
| 225 |
+
}
|
| 226 |
+
@keyframes dcd-ip-back {
|
| 227 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 228 |
+
10% { opacity: 0.85; }
|
| 229 |
+
90% { opacity: 0.85; }
|
| 230 |
+
100% { transform: translate(0, 60px); opacity: 0; }
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* ── Framework chips ── */
|
| 234 |
+
.d3-comm-deploy__chips {
|
| 235 |
+
display: flex; flex-wrap: wrap; gap: 6px;
|
| 236 |
+
}
|
| 237 |
+
.d3-comm-deploy__chip {
|
| 238 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 239 |
+
padding: 4px 9px;
|
| 240 |
+
font-size: 11px; font-weight: 600;
|
| 241 |
+
border-radius: 999px;
|
| 242 |
+
border: 1px solid color-mix(in oklab, var(--c, var(--border-color)) 35%, var(--border-color));
|
| 243 |
+
background: color-mix(in oklab, var(--c, transparent) 8%, transparent);
|
| 244 |
+
color: var(--text-color);
|
| 245 |
+
}
|
| 246 |
+
.d3-comm-deploy__chip .dot {
|
| 247 |
+
width: 6px; height: 6px; border-radius: 50%;
|
| 248 |
+
background: var(--c, var(--muted-color));
|
| 249 |
+
flex-shrink: 0;
|
| 250 |
+
}
|
| 251 |
+
.d3-comm-deploy__note {
|
| 252 |
+
font-size: 11.5px;
|
| 253 |
+
color: var(--muted-color);
|
| 254 |
+
line-height: 1.5;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
/* ── Caption ── */
|
| 258 |
+
.d3-comm-deploy__caption {
|
| 259 |
+
padding: 10px 16px;
|
| 260 |
+
border-top: 1px solid var(--border-color);
|
| 261 |
+
font-size: 11.5px;
|
| 262 |
+
color: var(--muted-color);
|
| 263 |
+
font-style: italic;
|
| 264 |
+
line-height: 1.55;
|
| 265 |
+
}
|
| 266 |
+
</style>
|
| 267 |
+
<script>
|
| 268 |
+
(() => {
|
| 269 |
+
const bootstrap = () => {
|
| 270 |
+
const scriptEl = document.currentScript;
|
| 271 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 272 |
+
if (!(container && container.classList && container.classList.contains('d3-comm-deploy'))) {
|
| 273 |
+
const cands = Array.from(document.querySelectorAll('.d3-comm-deploy'))
|
| 274 |
+
.filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 275 |
+
container = cands[cands.length - 1] || null;
|
| 276 |
+
}
|
| 277 |
+
if (!container || (container.dataset && container.dataset.mounted === 'true')) return;
|
| 278 |
+
container.dataset.mounted = 'true';
|
| 279 |
+
|
| 280 |
+
container.innerHTML = `
|
| 281 |
+
<div class="d3-comm-deploy__header">
|
| 282 |
+
<div class="d3-comm-deploy__title">Two architectures · same RL loop</div>
|
| 283 |
+
<button type="button" class="d3-comm-deploy__btn primary" data-act="play">
|
| 284 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="6,4 20,12 6,20"/></svg>
|
| 285 |
+
<span data-label>Play</span>
|
| 286 |
+
</button>
|
| 287 |
+
<label class="d3-comm-deploy__speed">
|
| 288 |
+
Speed
|
| 289 |
+
<input type="range" min="0.4" max="2" step="0.1" value="1" data-act="speed">
|
| 290 |
+
<span class="d3-comm-deploy__speed-val" data-speed-val>1.0×</span>
|
| 291 |
+
</label>
|
| 292 |
+
</div>
|
| 293 |
+
|
| 294 |
+
<div class="d3-comm-deploy__body">
|
| 295 |
+
|
| 296 |
+
<!-- HTTP PANEL -->
|
| 297 |
+
<section class="d3-comm-deploy__panel">
|
| 298 |
+
<header class="d3-comm-deploy__panel-head" style="--c: var(--c-http);">
|
| 299 |
+
<span class="swatch"></span>
|
| 300 |
+
HTTP frameworks
|
| 301 |
+
<span class="tag">trainer ↔ server</span>
|
| 302 |
+
</header>
|
| 303 |
+
|
| 304 |
+
<div class="d3-comm-deploy__stage">
|
| 305 |
+
<svg viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
|
| 306 |
+
<defs>
|
| 307 |
+
<marker id="dcd-arr-http" viewBox="0 0 10 10" refX="9" refY="5"
|
| 308 |
+
markerWidth="6" markerHeight="6" orient="auto" markerUnits="userSpaceOnUse">
|
| 309 |
+
<path d="M0,0 L10,5 L0,10 Z" fill="currentColor"/>
|
| 310 |
+
</marker>
|
| 311 |
+
</defs>
|
| 312 |
+
|
| 313 |
+
<!-- training process box -->
|
| 314 |
+
<rect class="box tinted-http" x="14" y="50" width="130" height="80" rx="10"/>
|
| 315 |
+
<text class="label-title" x="79" y="80" text-anchor="middle">Training process</text>
|
| 316 |
+
<text class="label-sub" x="79" y="96" text-anchor="middle">GPU node</text>
|
| 317 |
+
<text class="label-sub" x="79" y="108" text-anchor="middle">trainer + vLLM</text>
|
| 318 |
+
|
| 319 |
+
<!-- env server box -->
|
| 320 |
+
<rect class="box tinted-env" x="256" y="50" width="130" height="80" rx="10"/>
|
| 321 |
+
<text class="label-title" x="321" y="80" text-anchor="middle">Environment server</text>
|
| 322 |
+
<text class="label-sub" x="321" y="96" text-anchor="middle">CPU · FastAPI</text>
|
| 323 |
+
<text class="label-sub" x="321" y="108" text-anchor="middle">per-session state</text>
|
| 324 |
+
|
| 325 |
+
<!-- arrows between (action up, response down) -->
|
| 326 |
+
<g style="color: var(--muted-color);">
|
| 327 |
+
<path class="arrow" d="M144,75 L256,75" marker-end="url(#dcd-arr-http)"/>
|
| 328 |
+
<text class="arrow-label" x="200" y="68" text-anchor="middle">action / tool call</text>
|
| 329 |
+
<path class="arrow" d="M256,105 L144,105" marker-end="url(#dcd-arr-http)"/>
|
| 330 |
+
<text class="arrow-label" x="200" y="120" text-anchor="middle">obs · reward</text>
|
| 331 |
+
</g>
|
| 332 |
+
|
| 333 |
+
<!-- backend dashed -->
|
| 334 |
+
<rect class="box backend" x="261" y="160" width="120" height="44" rx="8"/>
|
| 335 |
+
<text class="label-title" x="321" y="184" text-anchor="middle">Backend</text>
|
| 336 |
+
<text class="label-sub" x="321" y="196" text-anchor="middle">E2B · Modal · custom</text>
|
| 337 |
+
<g style="color: var(--muted-color);">
|
| 338 |
+
<path class="arrow dashed" d="M321,130 L321,160" marker-end="url(#dcd-arr-http)"/>
|
| 339 |
+
</g>
|
| 340 |
+
|
| 341 |
+
<!-- packets -->
|
| 342 |
+
<circle class="packet http-out" cx="144" cy="75" r="3.5" style="fill: var(--c-http);"/>
|
| 343 |
+
<circle class="packet http-in" cx="256" cy="105" r="3.5" style="fill: #22c55e;"/>
|
| 344 |
+
<circle class="packet http-back" cx="321" cy="130" r="3" style="fill: var(--muted-color); opacity:0;"/>
|
| 345 |
+
</svg>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<div class="d3-comm-deploy__chips">
|
| 349 |
+
<span class="d3-comm-deploy__chip" style="--c:#3b82f6;"><span class="dot"></span>OpenEnv</span>
|
| 350 |
+
<span class="d3-comm-deploy__chip" style="--c:#a855f7;"><span class="dot"></span>ORS</span>
|
| 351 |
+
<span class="d3-comm-deploy__chip" style="--c:#22c55e;"><span class="dot"></span>NeMo Gym</span>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="d3-comm-deploy__note">
|
| 354 |
+
Two processes, one network hop on every tool call. Env can run on a CPU box or HF Space and scale by adding replicas.
|
| 355 |
+
</div>
|
| 356 |
+
</section>
|
| 357 |
+
|
| 358 |
+
<!-- IN-PROCESS PANEL -->
|
| 359 |
+
<section class="d3-comm-deploy__panel">
|
| 360 |
+
<header class="d3-comm-deploy__panel-head" style="--c: var(--c-inproc);">
|
| 361 |
+
<span class="swatch"></span>
|
| 362 |
+
In-process frameworks
|
| 363 |
+
<span class="tag">single python process</span>
|
| 364 |
+
</header>
|
| 365 |
+
|
| 366 |
+
<div class="d3-comm-deploy__stage">
|
| 367 |
+
<svg viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
|
| 368 |
+
<defs>
|
| 369 |
+
<marker id="dcd-arr-ip" viewBox="0 0 10 10" refX="9" refY="5"
|
| 370 |
+
markerWidth="6" markerHeight="6" orient="auto" markerUnits="userSpaceOnUse">
|
| 371 |
+
<path d="M0,0 L10,5 L0,10 Z" fill="currentColor"/>
|
| 372 |
+
</marker>
|
| 373 |
+
</defs>
|
| 374 |
+
|
| 375 |
+
<!-- outer training process box -->
|
| 376 |
+
<rect class="box outer" x="14" y="32" width="372" height="118" rx="12"/>
|
| 377 |
+
<text class="label-title" x="28" y="50" text-anchor="start" style="fill: var(--muted-color); font-size:9px; letter-spacing:0.5px; text-transform:uppercase;">Training process</text>
|
| 378 |
+
|
| 379 |
+
<!-- inner trainer box -->
|
| 380 |
+
<rect class="box tinted-http" x="32" y="62" width="140" height="76" rx="9"/>
|
| 381 |
+
<text class="label-title" x="102" y="92" text-anchor="middle">Trainer</text>
|
| 382 |
+
<text class="label-sub" x="102" y="108" text-anchor="middle">GRPO · vLLM</text>
|
| 383 |
+
<text class="label-sub" x="102" y="120" text-anchor="middle">GPU node</text>
|
| 384 |
+
|
| 385 |
+
<!-- inner env box -->
|
| 386 |
+
<rect class="box tinted-inproc" x="228" y="62" width="140" height="76" rx="9"/>
|
| 387 |
+
<text class="label-title" x="298" y="92" text-anchor="middle">Env (Python obj)</text>
|
| 388 |
+
<text class="label-sub" x="298" y="108" text-anchor="middle">step() · reset()</text>
|
| 389 |
+
<text class="label-sub" x="298" y="120" text-anchor="middle">same venv</text>
|
| 390 |
+
|
| 391 |
+
<!-- arrows between -->
|
| 392 |
+
<g style="color: var(--muted-color);">
|
| 393 |
+
<path class="arrow" d="M172,84 L228,84" marker-end="url(#dcd-arr-ip)"/>
|
| 394 |
+
<text class="arrow-label" x="200" y="78" text-anchor="middle">call</text>
|
| 395 |
+
<path class="arrow" d="M228,116 L172,116" marker-end="url(#dcd-arr-ip)"/>
|
| 396 |
+
<text class="arrow-label" x="200" y="130" text-anchor="middle">return</text>
|
| 397 |
+
</g>
|
| 398 |
+
|
| 399 |
+
<!-- backend dashed -->
|
| 400 |
+
<rect class="box backend" x="140" y="170" width="120" height="38" rx="8"/>
|
| 401 |
+
<text class="label-title" x="200" y="190" text-anchor="middle">Backend</text>
|
| 402 |
+
<text class="label-sub" x="200" y="202" text-anchor="middle">optional · E2B / sandbox</text>
|
| 403 |
+
<g style="color: var(--muted-color);">
|
| 404 |
+
<path class="arrow dashed" d="M298,138 L298,162 L260,162 L260,170" marker-end="url(#dcd-arr-ip)"/>
|
| 405 |
+
</g>
|
| 406 |
+
|
| 407 |
+
<!-- packets -->
|
| 408 |
+
<circle class="packet ip-out" cx="172" cy="84" r="3.5" style="fill: var(--c-inproc);"/>
|
| 409 |
+
<circle class="packet ip-in" cx="228" cy="116" r="3.5" style="fill: #22c55e;"/>
|
| 410 |
+
<circle class="packet ip-back" cx="298" cy="138" r="3" style="fill: var(--muted-color); opacity:0;"/>
|
| 411 |
+
</svg>
|
| 412 |
+
</div>
|
| 413 |
+
|
| 414 |
+
<div class="d3-comm-deploy__chips">
|
| 415 |
+
<span class="d3-comm-deploy__chip" style="--c:#ec4899;"><span class="dot"></span>Verifiers</span>
|
| 416 |
+
<span class="d3-comm-deploy__chip" style="--c:#f59e0b;"><span class="dot"></span>SkyRL Gym</span>
|
| 417 |
+
<span class="d3-comm-deploy__chip" style="--c:#14b8a6;"><span class="dot"></span>GEM</span>
|
| 418 |
+
</div>
|
| 419 |
+
<div class="d3-comm-deploy__note">
|
| 420 |
+
One process. Direct Python call. Fewer moving parts, but env shares the GPU node's CPU and dependency tree.
|
| 421 |
+
</div>
|
| 422 |
+
</section>
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="d3-comm-deploy__caption">
|
| 426 |
+
Same loop, two shapes. HTTP frameworks separate trainer and env so each can scale on its own machine class. In-process frameworks fold the env into the trainer for simpler local runs at the cost of resource isolation.
|
| 427 |
+
</div>
|
| 428 |
+
`;
|
| 429 |
+
|
| 430 |
+
// ── Controls ──
|
| 431 |
+
const playBtn = container.querySelector('[data-act="play"]');
|
| 432 |
+
const playLbl = container.querySelector('[data-label]');
|
| 433 |
+
const speedInput = container.querySelector('[data-act="speed"]');
|
| 434 |
+
const speedVal = container.querySelector('[data-speed-val]');
|
| 435 |
+
|
| 436 |
+
let playing = false;
|
| 437 |
+
const setPlaying = (next) => {
|
| 438 |
+
playing = next;
|
| 439 |
+
container.classList.toggle('playing', playing);
|
| 440 |
+
playLbl.textContent = playing ? 'Pause' : 'Play';
|
| 441 |
+
const icon = playBtn.querySelector('svg');
|
| 442 |
+
if (playing) {
|
| 443 |
+
icon.innerHTML = '<rect x="6" y="5" width="4" height="14" fill="currentColor"/><rect x="14" y="5" width="4" height="14" fill="currentColor"/>';
|
| 444 |
+
} else {
|
| 445 |
+
icon.innerHTML = '<polygon points="6,4 20,12 6,20" fill="currentColor"/>';
|
| 446 |
+
}
|
| 447 |
+
};
|
| 448 |
+
playBtn.addEventListener('click', () => setPlaying(!playing));
|
| 449 |
+
|
| 450 |
+
const updateSpeed = () => {
|
| 451 |
+
const s = parseFloat(speedInput.value);
|
| 452 |
+
const dur = (1.6 / s).toFixed(2) + 's';
|
| 453 |
+
container.style.setProperty('--dur', dur);
|
| 454 |
+
speedVal.textContent = s.toFixed(1) + '×';
|
| 455 |
+
};
|
| 456 |
+
speedInput.addEventListener('input', updateSpeed);
|
| 457 |
+
updateSpeed();
|
| 458 |
+
|
| 459 |
+
// start playing on first scroll-into-view, like the other embeds
|
| 460 |
+
const io = ('IntersectionObserver' in window) ? new IntersectionObserver((entries) => {
|
| 461 |
+
entries.forEach(e => {
|
| 462 |
+
if (e.isIntersecting && !playing) {
|
| 463 |
+
setPlaying(true);
|
| 464 |
+
io.disconnect();
|
| 465 |
+
}
|
| 466 |
+
});
|
| 467 |
+
}, { threshold: 0.4 }) : null;
|
| 468 |
+
if (io) io.observe(container); else setPlaying(true);
|
| 469 |
+
};
|
| 470 |
+
|
| 471 |
+
if (document.readyState === 'loading') {
|
| 472 |
+
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
| 473 |
+
} else {
|
| 474 |
+
bootstrap();
|
| 475 |
+
}
|
| 476 |
+
})();
|
| 477 |
+
</script>
|
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-tool-discovery" style="width:100%;margin:14px 0;"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-tool-discovery {
|
| 4 |
+
position: relative;
|
| 5 |
+
border: 1px solid var(--border-color);
|
| 6 |
+
border-radius: 12px;
|
| 7 |
+
background: var(--surface-bg);
|
| 8 |
+
overflow: hidden;
|
| 9 |
+
color: var(--text-color);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* ── Header ── */
|
| 13 |
+
.d3-tool-discovery__header {
|
| 14 |
+
display: flex; flex-wrap: wrap; align-items: center;
|
| 15 |
+
gap: 12px 16px; padding: 12px 16px;
|
| 16 |
+
border-bottom: 1px solid var(--border-color);
|
| 17 |
+
}
|
| 18 |
+
.d3-tool-discovery__title {
|
| 19 |
+
font-size: 11px; font-weight: 800; letter-spacing: 1.2px;
|
| 20 |
+
text-transform: uppercase; color: var(--muted-color);
|
| 21 |
+
margin-right: auto;
|
| 22 |
+
}
|
| 23 |
+
.d3-tool-discovery__btn {
|
| 24 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 25 |
+
padding: 6px 12px; border-radius: 7px;
|
| 26 |
+
border: 1px solid var(--border-color);
|
| 27 |
+
background: var(--surface-bg); color: var(--text-color);
|
| 28 |
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
| 29 |
+
transition: border-color .12s ease, background .12s ease;
|
| 30 |
+
}
|
| 31 |
+
.d3-tool-discovery__btn:hover { border-color: var(--primary-color); }
|
| 32 |
+
.d3-tool-discovery__btn.primary {
|
| 33 |
+
border-color: var(--primary-color);
|
| 34 |
+
background: color-mix(in oklab, var(--primary-color) 12%, var(--surface-bg));
|
| 35 |
+
}
|
| 36 |
+
.d3-tool-discovery__btn svg { width: 12px; height: 12px; }
|
| 37 |
+
.d3-tool-discovery__speed {
|
| 38 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 39 |
+
font-size: 11px; color: var(--muted-color);
|
| 40 |
+
}
|
| 41 |
+
.d3-tool-discovery__speed input[type=range] {
|
| 42 |
+
width: 100px;
|
| 43 |
+
accent-color: var(--primary-color);
|
| 44 |
+
}
|
| 45 |
+
.d3-tool-discovery__speed-val {
|
| 46 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 47 |
+
color: var(--text-color); font-size: 11px;
|
| 48 |
+
min-width: 36px; text-align: right;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* ── Grid of framework cards ── */
|
| 52 |
+
.d3-tool-discovery__grid {
|
| 53 |
+
display: grid;
|
| 54 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 55 |
+
gap: 0;
|
| 56 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 57 |
+
}
|
| 58 |
+
.td-card {
|
| 59 |
+
padding: 14px 14px 12px 14px;
|
| 60 |
+
border-right: 1px solid var(--border-color);
|
| 61 |
+
border-bottom: 1px solid var(--border-color);
|
| 62 |
+
display: flex; flex-direction: column; gap: 9px;
|
| 63 |
+
background: var(--surface-bg);
|
| 64 |
+
min-width: 0;
|
| 65 |
+
}
|
| 66 |
+
/* fix the trailing edges so the grid looks clean */
|
| 67 |
+
.td-card:nth-child(3n) { border-right: 0; }
|
| 68 |
+
.td-card:nth-last-child(-n+3):not(:nth-child(3n+2)):not(:nth-child(3n)) { border-bottom: 0; }
|
| 69 |
+
@media (max-width: 880px) {
|
| 70 |
+
.td-card { border-right: 0 !important; }
|
| 71 |
+
.td-card:nth-last-child(-n+2) { border-bottom: 0; }
|
| 72 |
+
}
|
| 73 |
+
@media (max-width: 580px) {
|
| 74 |
+
.td-card { border-bottom: 1px solid var(--border-color) !important; }
|
| 75 |
+
.td-card:last-child { border-bottom: 0 !important; }
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* card head */
|
| 79 |
+
.td-card__head {
|
| 80 |
+
display: flex; align-items: center; gap: 8px;
|
| 81 |
+
}
|
| 82 |
+
.td-card__head .swatch {
|
| 83 |
+
width: 9px; height: 9px;
|
| 84 |
+
border-radius: 50%;
|
| 85 |
+
background: var(--c, var(--muted-color));
|
| 86 |
+
flex-shrink: 0;
|
| 87 |
+
}
|
| 88 |
+
.td-card__head .name {
|
| 89 |
+
font-weight: 700;
|
| 90 |
+
font-size: 12.5px;
|
| 91 |
+
color: var(--text-color);
|
| 92 |
+
}
|
| 93 |
+
.td-card__head .kind {
|
| 94 |
+
margin-left: auto;
|
| 95 |
+
font-size: 9px;
|
| 96 |
+
font-weight: 700;
|
| 97 |
+
letter-spacing: 0.6px;
|
| 98 |
+
text-transform: uppercase;
|
| 99 |
+
padding: 2px 6px;
|
| 100 |
+
border-radius: 4px;
|
| 101 |
+
color: var(--muted-color);
|
| 102 |
+
background: color-mix(in oklab, var(--muted-color) 8%, transparent);
|
| 103 |
+
border: 1px solid var(--border-color);
|
| 104 |
+
}
|
| 105 |
+
.td-card__head .kind.http {
|
| 106 |
+
color: color-mix(in oklab, var(--c, #3b82f6) 90%, var(--text-color));
|
| 107 |
+
background: color-mix(in oklab, var(--c, #3b82f6) 12%, transparent);
|
| 108 |
+
border-color: color-mix(in oklab, var(--c, #3b82f6) 30%, var(--border-color));
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* sequence stage */
|
| 112 |
+
.td-stage {
|
| 113 |
+
position: relative;
|
| 114 |
+
aspect-ratio: 16 / 6;
|
| 115 |
+
border-radius: 8px;
|
| 116 |
+
background: color-mix(in oklab, var(--muted-color) 4%, transparent);
|
| 117 |
+
border: 1px solid var(--border-color);
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
min-height: 90px;
|
| 120 |
+
}
|
| 121 |
+
.td-stage svg {
|
| 122 |
+
width: 100%; height: 100%; display: block;
|
| 123 |
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
| 124 |
+
}
|
| 125 |
+
.td-stage .node {
|
| 126 |
+
fill: var(--surface-bg);
|
| 127 |
+
stroke: var(--border-color);
|
| 128 |
+
stroke-width: 1.2;
|
| 129 |
+
}
|
| 130 |
+
.td-stage .node.client {
|
| 131 |
+
fill: color-mix(in oklab, var(--muted-color) 6%, var(--surface-bg));
|
| 132 |
+
}
|
| 133 |
+
.td-stage .node.env {
|
| 134 |
+
fill: color-mix(in oklab, var(--c) 10%, var(--surface-bg));
|
| 135 |
+
stroke: color-mix(in oklab, var(--c) 35%, var(--border-color));
|
| 136 |
+
}
|
| 137 |
+
.td-stage .label-title {
|
| 138 |
+
fill: var(--text-color);
|
| 139 |
+
font-size: 10px; font-weight: 700;
|
| 140 |
+
}
|
| 141 |
+
.td-stage .label-sub {
|
| 142 |
+
fill: var(--muted-color);
|
| 143 |
+
font-size: 8.5px; font-weight: 500;
|
| 144 |
+
}
|
| 145 |
+
.td-stage .arrow {
|
| 146 |
+
stroke: var(--muted-color);
|
| 147 |
+
stroke-width: 1.3;
|
| 148 |
+
fill: none;
|
| 149 |
+
}
|
| 150 |
+
.td-stage .arrow.dashed { stroke-dasharray: 4 3; }
|
| 151 |
+
.td-stage .arrow-label {
|
| 152 |
+
fill: var(--text-color);
|
| 153 |
+
font-size: 8.5px;
|
| 154 |
+
font-weight: 600;
|
| 155 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 156 |
+
}
|
| 157 |
+
.td-stage .stage-tag {
|
| 158 |
+
fill: var(--muted-color);
|
| 159 |
+
font-size: 7.5px;
|
| 160 |
+
font-weight: 700;
|
| 161 |
+
letter-spacing: 0.5px;
|
| 162 |
+
text-transform: uppercase;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/* travelling packets — sequence: discovery out → discovery in → call out → call in */
|
| 166 |
+
.td-stage .packet {
|
| 167 |
+
fill: var(--c, var(--primary-color));
|
| 168 |
+
opacity: 0;
|
| 169 |
+
}
|
| 170 |
+
.d3-tool-discovery.playing .packet {
|
| 171 |
+
animation-play-state: running;
|
| 172 |
+
}
|
| 173 |
+
.d3-tool-discovery:not(.playing) .packet { animation-play-state: paused; opacity: 0; }
|
| 174 |
+
|
| 175 |
+
/* full sequence packet covering 4 phases over the duration */
|
| 176 |
+
.td-stage .packet.disc-out {
|
| 177 |
+
animation: td-disc-out var(--dur, 2.4s) linear infinite;
|
| 178 |
+
}
|
| 179 |
+
.td-stage .packet.disc-in {
|
| 180 |
+
animation: td-disc-in var(--dur, 2.4s) linear infinite;
|
| 181 |
+
}
|
| 182 |
+
.td-stage .packet.call-out {
|
| 183 |
+
animation: td-call-out var(--dur, 2.4s) linear infinite;
|
| 184 |
+
}
|
| 185 |
+
.td-stage .packet.call-in {
|
| 186 |
+
animation: td-call-in var(--dur, 2.4s) linear infinite;
|
| 187 |
+
}
|
| 188 |
+
@keyframes td-disc-out {
|
| 189 |
+
0% { transform: translate(0, 0); opacity: 0; }
|
| 190 |
+
1% { opacity: 1; }
|
| 191 |
+
23% { transform: translate(180px, 0); opacity: 1; }
|
| 192 |
+
24% { opacity: 0; }
|
| 193 |
+
100% { opacity: 0; }
|
| 194 |
+
}
|
| 195 |
+
@keyframes td-disc-in {
|
| 196 |
+
0%, 24% { opacity: 0; }
|
| 197 |
+
25% { transform: translate(0, 0); opacity: 1; }
|
| 198 |
+
48% { transform: translate(-180px, 0); opacity: 1; }
|
| 199 |
+
49% { opacity: 0; }
|
| 200 |
+
100% { opacity: 0; }
|
| 201 |
+
}
|
| 202 |
+
@keyframes td-call-out {
|
| 203 |
+
0%, 49% { opacity: 0; }
|
| 204 |
+
50% { transform: translate(0, 0); opacity: 1; }
|
| 205 |
+
73% { transform: translate(180px, 0); opacity: 1; }
|
| 206 |
+
74% { opacity: 0; }
|
| 207 |
+
100% { opacity: 0; }
|
| 208 |
+
}
|
| 209 |
+
@keyframes td-call-in {
|
| 210 |
+
0%, 74% { opacity: 0; }
|
| 211 |
+
75% { transform: translate(0, 0); opacity: 1; }
|
| 212 |
+
98% { transform: translate(-180px, 0); opacity: 1; }
|
| 213 |
+
99% { opacity: 0; }
|
| 214 |
+
100% { opacity: 0; }
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/* card footer table */
|
| 218 |
+
.td-card__rows {
|
| 219 |
+
display: grid;
|
| 220 |
+
grid-template-columns: auto 1fr;
|
| 221 |
+
gap: 3px 10px;
|
| 222 |
+
font-size: 10.5px;
|
| 223 |
+
line-height: 1.5;
|
| 224 |
+
}
|
| 225 |
+
.td-card__rows .k {
|
| 226 |
+
color: var(--muted-color);
|
| 227 |
+
font-weight: 600;
|
| 228 |
+
text-transform: uppercase;
|
| 229 |
+
letter-spacing: 0.4px;
|
| 230 |
+
font-size: 9.5px;
|
| 231 |
+
padding-top: 1px;
|
| 232 |
+
}
|
| 233 |
+
.td-card__rows .v {
|
| 234 |
+
color: var(--text-color);
|
| 235 |
+
overflow-wrap: anywhere;
|
| 236 |
+
}
|
| 237 |
+
.td-card__rows code {
|
| 238 |
+
background: color-mix(in oklab, var(--muted-color) 10%, transparent);
|
| 239 |
+
border-radius: 3px;
|
| 240 |
+
padding: 0 4px;
|
| 241 |
+
font-size: 10.5px;
|
| 242 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* caption */
|
| 246 |
+
.d3-tool-discovery__caption {
|
| 247 |
+
padding: 10px 16px;
|
| 248 |
+
border-top: 1px solid var(--border-color);
|
| 249 |
+
font-size: 11.5px;
|
| 250 |
+
color: var(--muted-color);
|
| 251 |
+
font-style: italic;
|
| 252 |
+
line-height: 1.55;
|
| 253 |
+
}
|
| 254 |
+
</style>
|
| 255 |
+
<script>
|
| 256 |
+
(() => {
|
| 257 |
+
const bootstrap = () => {
|
| 258 |
+
const scriptEl = document.currentScript;
|
| 259 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 260 |
+
if (!(container && container.classList && container.classList.contains('d3-tool-discovery'))) {
|
| 261 |
+
const cands = Array.from(document.querySelectorAll('.d3-tool-discovery'))
|
| 262 |
+
.filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 263 |
+
container = cands[cands.length - 1] || null;
|
| 264 |
+
}
|
| 265 |
+
if (!container || (container.dataset && container.dataset.mounted === 'true')) return;
|
| 266 |
+
container.dataset.mounted = 'true';
|
| 267 |
+
|
| 268 |
+
const FRAMEWORKS = [
|
| 269 |
+
{
|
| 270 |
+
key: 'openenv', name: 'OpenEnv', color: '#3b82f6', kind: 'http',
|
| 271 |
+
defn: '<code>@mcp.tool</code> on FastMCP',
|
| 272 |
+
discoveryLabel: 'tools/list',
|
| 273 |
+
callLabel: 'tools/call',
|
| 274 |
+
schema: 'MCP tool spec (JSON Schema)',
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
key: 'ors', name: 'ORS', color: '#a855f7', kind: 'http',
|
| 278 |
+
defn: '<code>@tool</code> + Pydantic',
|
| 279 |
+
discoveryLabel: 'GET /tools',
|
| 280 |
+
callLabel: 'POST /call',
|
| 281 |
+
schema: 'ORS ToolSpec',
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
key: 'nemo', name: 'NeMo Gym', color: '#22c55e', kind: 'http',
|
| 285 |
+
defn: '<code>app.post()</code> endpoints',
|
| 286 |
+
discoveryLabel: 'GET /openapi.json',
|
| 287 |
+
callLabel: 'POST /tool',
|
| 288 |
+
schema: 'OpenAPI JSON Schema',
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
key: 'verifs', name: 'Verifiers', color: '#ec4899', kind: 'inproc',
|
| 292 |
+
defn: 'Plain Python fns',
|
| 293 |
+
discoveryLabel: 'tools=[fn]',
|
| 294 |
+
callLabel: 'fn(args)',
|
| 295 |
+
schema: 'Type hints (auto)',
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
key: 'skyrl', name: 'SkyRL Gym', color: '#f59e0b', kind: 'inproc',
|
| 299 |
+
defn: '<code>ToolGroup</code> subclass',
|
| 300 |
+
discoveryLabel: 'init_tool_groups()',
|
| 301 |
+
callLabel: 'group.invoke()',
|
| 302 |
+
schema: 'Type hints (auto)',
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
key: 'gem', name: 'GEM', color: '#14b8a6', kind: 'inproc',
|
| 306 |
+
defn: '<code>ToolEnvWrapper</code>',
|
| 307 |
+
discoveryLabel: 'wrapper compose',
|
| 308 |
+
callLabel: 'env.step(call)',
|
| 309 |
+
schema: 'Wrapper-defined',
|
| 310 |
+
},
|
| 311 |
+
];
|
| 312 |
+
|
| 313 |
+
const cardSvg = (f) => `
|
| 314 |
+
<svg viewBox="0 0 280 110" preserveAspectRatio="xMidYMid meet">
|
| 315 |
+
<defs>
|
| 316 |
+
<marker id="td-arr-${f.key}" viewBox="0 0 10 10" refX="9" refY="5"
|
| 317 |
+
markerWidth="6" markerHeight="6" orient="auto" markerUnits="userSpaceOnUse">
|
| 318 |
+
<path d="M0,0 L10,5 L0,10 Z" fill="currentColor"/>
|
| 319 |
+
</marker>
|
| 320 |
+
</defs>
|
| 321 |
+
|
| 322 |
+
<!-- left node: Trainer -->
|
| 323 |
+
<rect class="node client" x="10" y="22" width="64" height="62" rx="8"/>
|
| 324 |
+
<text class="label-title" x="42" y="48" text-anchor="middle">Trainer</text>
|
| 325 |
+
<text class="label-sub" x="42" y="62" text-anchor="middle">${f.kind === 'http' ? 'client' : 'in-process'}</text>
|
| 326 |
+
|
| 327 |
+
<!-- right node: Env / tools -->
|
| 328 |
+
<rect class="node env" x="206" y="22" width="64" height="62" rx="8"/>
|
| 329 |
+
<text class="label-title" x="238" y="48" text-anchor="middle">${f.kind === 'http' ? 'Env server' : 'Env (Py)'}</text>
|
| 330 |
+
<text class="label-sub" x="238" y="62" text-anchor="middle">tools</text>
|
| 331 |
+
|
| 332 |
+
<!-- discovery arrow (top) -->
|
| 333 |
+
<g style="color: var(--muted-color);">
|
| 334 |
+
<path class="arrow ${f.kind === 'http' ? 'dashed' : ''}" d="M74,38 L206,38" marker-end="url(#td-arr-${f.key})"/>
|
| 335 |
+
<text class="arrow-label" x="140" y="32" text-anchor="middle">${f.discoveryLabel}</text>
|
| 336 |
+
</g>
|
| 337 |
+
<text class="stage-tag" x="140" y="14" text-anchor="middle">discover</text>
|
| 338 |
+
|
| 339 |
+
<!-- call arrow (bottom) -->
|
| 340 |
+
<g style="color: var(--c);">
|
| 341 |
+
<path class="arrow" d="M74,72 L206,72" marker-end="url(#td-arr-${f.key})"/>
|
| 342 |
+
<text class="arrow-label" x="140" y="88" text-anchor="middle">${f.callLabel}</text>
|
| 343 |
+
</g>
|
| 344 |
+
<text class="stage-tag" x="140" y="106" text-anchor="middle">invoke</text>
|
| 345 |
+
|
| 346 |
+
<!-- packets -->
|
| 347 |
+
<circle class="packet disc-out" cx="74" cy="38" r="3" style="fill: var(--muted-color);"/>
|
| 348 |
+
<circle class="packet disc-in" cx="206" cy="38" r="3" style="fill: var(--muted-color);"/>
|
| 349 |
+
<circle class="packet call-out" cx="74" cy="72" r="3.2" style="fill: var(--c);"/>
|
| 350 |
+
<circle class="packet call-in" cx="206" cy="72" r="3.2" style="fill: #22c55e;"/>
|
| 351 |
+
</svg>
|
| 352 |
+
`;
|
| 353 |
+
|
| 354 |
+
const cardsHtml = FRAMEWORKS.map(f => `
|
| 355 |
+
<article class="td-card" style="--c:${f.color};">
|
| 356 |
+
<div class="td-card__head">
|
| 357 |
+
<span class="swatch"></span>
|
| 358 |
+
<span class="name">${f.name}</span>
|
| 359 |
+
<span class="kind ${f.kind === 'http' ? 'http' : ''}">${f.kind === 'http' ? 'HTTP' : 'In-process'}</span>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="td-stage">${cardSvg(f)}</div>
|
| 362 |
+
<div class="td-card__rows">
|
| 363 |
+
<span class="k">def</span><span class="v">${f.defn}</span>
|
| 364 |
+
<span class="k">discover</span><span class="v"><code>${f.discoveryLabel}</code></span>
|
| 365 |
+
<span class="k">schema</span><span class="v">${f.schema}</span>
|
| 366 |
+
</div>
|
| 367 |
+
</article>
|
| 368 |
+
`).join('');
|
| 369 |
+
|
| 370 |
+
container.innerHTML = `
|
| 371 |
+
<div class="d3-tool-discovery__header">
|
| 372 |
+
<div class="d3-tool-discovery__title">Tool discovery & invocation · 6 frameworks</div>
|
| 373 |
+
<button type="button" class="d3-tool-discovery__btn primary" data-act="play">
|
| 374 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="6,4 20,12 6,20"/></svg>
|
| 375 |
+
<span data-label>Play</span>
|
| 376 |
+
</button>
|
| 377 |
+
<label class="d3-tool-discovery__speed">
|
| 378 |
+
Speed
|
| 379 |
+
<input type="range" min="0.4" max="2" step="0.1" value="1" data-act="speed">
|
| 380 |
+
<span class="d3-tool-discovery__speed-val" data-speed-val>1.0×</span>
|
| 381 |
+
</label>
|
| 382 |
+
</div>
|
| 383 |
+
<div class="d3-tool-discovery__grid">${cardsHtml}</div>
|
| 384 |
+
<div class="d3-tool-discovery__caption">
|
| 385 |
+
Same loop, two discovery shapes. HTTP frameworks fetch the tool list at runtime over the wire (dashed line). In-process frameworks register tools when the env constructs (solid Python call). After that, both look the same to the model: name + typed args in, observation out.
|
| 386 |
+
</div>
|
| 387 |
+
`;
|
| 388 |
+
|
| 389 |
+
// ── Controls ──
|
| 390 |
+
const playBtn = container.querySelector('[data-act="play"]');
|
| 391 |
+
const playLbl = container.querySelector('[data-label]');
|
| 392 |
+
const speedInput = container.querySelector('[data-act="speed"]');
|
| 393 |
+
const speedVal = container.querySelector('[data-speed-val]');
|
| 394 |
+
|
| 395 |
+
let playing = false;
|
| 396 |
+
const setPlaying = (next) => {
|
| 397 |
+
playing = next;
|
| 398 |
+
container.classList.toggle('playing', playing);
|
| 399 |
+
playLbl.textContent = playing ? 'Pause' : 'Play';
|
| 400 |
+
const icon = playBtn.querySelector('svg');
|
| 401 |
+
if (playing) {
|
| 402 |
+
icon.innerHTML = '<rect x="6" y="5" width="4" height="14" fill="currentColor"/><rect x="14" y="5" width="4" height="14" fill="currentColor"/>';
|
| 403 |
+
} else {
|
| 404 |
+
icon.innerHTML = '<polygon points="6,4 20,12 6,20" fill="currentColor"/>';
|
| 405 |
+
}
|
| 406 |
+
};
|
| 407 |
+
playBtn.addEventListener('click', () => setPlaying(!playing));
|
| 408 |
+
|
| 409 |
+
const updateSpeed = () => {
|
| 410 |
+
const s = parseFloat(speedInput.value);
|
| 411 |
+
const dur = (2.4 / s).toFixed(2) + 's';
|
| 412 |
+
container.style.setProperty('--dur', dur);
|
| 413 |
+
speedVal.textContent = s.toFixed(1) + '×';
|
| 414 |
+
};
|
| 415 |
+
speedInput.addEventListener('input', updateSpeed);
|
| 416 |
+
updateSpeed();
|
| 417 |
+
|
| 418 |
+
const io = ('IntersectionObserver' in window) ? new IntersectionObserver((entries) => {
|
| 419 |
+
entries.forEach(e => {
|
| 420 |
+
if (e.isIntersecting && !playing) {
|
| 421 |
+
setPlaying(true);
|
| 422 |
+
io.disconnect();
|
| 423 |
+
}
|
| 424 |
+
});
|
| 425 |
+
}, { threshold: 0.3 }) : null;
|
| 426 |
+
if (io) io.observe(container); else setPlaying(true);
|
| 427 |
+
};
|
| 428 |
+
|
| 429 |
+
if (document.readyState === 'loading') {
|
| 430 |
+
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
| 431 |
+
} else {
|
| 432 |
+
bootstrap();
|
| 433 |
+
}
|
| 434 |
+
})();
|
| 435 |
+
</script>
|
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="tool-action-matrix" style="width:100%;margin:14px 0;"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.tool-action-matrix {
|
| 4 |
+
border: 1px solid var(--border-color);
|
| 5 |
+
border-radius: 12px;
|
| 6 |
+
background: var(--surface-bg);
|
| 7 |
+
overflow: hidden;
|
| 8 |
+
color: var(--text-color);
|
| 9 |
+
}
|
| 10 |
+
.tool-action-matrix__head {
|
| 11 |
+
display: flex; align-items: center; gap: 12px;
|
| 12 |
+
padding: 8px 12px;
|
| 13 |
+
border-bottom: 1px solid var(--border-color);
|
| 14 |
+
background: color-mix(in oklab, var(--muted-color) 4%, transparent);
|
| 15 |
+
flex-wrap: wrap;
|
| 16 |
+
}
|
| 17 |
+
.tool-action-matrix__title {
|
| 18 |
+
font-size: 10.5px;
|
| 19 |
+
font-weight: 800;
|
| 20 |
+
letter-spacing: 1px;
|
| 21 |
+
text-transform: uppercase;
|
| 22 |
+
color: var(--muted-color);
|
| 23 |
+
margin-right: auto;
|
| 24 |
+
}
|
| 25 |
+
.tool-action-matrix__legend {
|
| 26 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 27 |
+
font-size: 10.5px; color: var(--muted-color);
|
| 28 |
+
}
|
| 29 |
+
.tool-action-matrix__legend .item {
|
| 30 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 31 |
+
}
|
| 32 |
+
.tam-pill {
|
| 33 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 34 |
+
padding: 2px 9px;
|
| 35 |
+
font-size: 11px;
|
| 36 |
+
font-weight: 700;
|
| 37 |
+
border-radius: 999px;
|
| 38 |
+
line-height: 1.4;
|
| 39 |
+
white-space: nowrap;
|
| 40 |
+
}
|
| 41 |
+
.tam-pill.yes {
|
| 42 |
+
background: color-mix(in oklab, #22c55e 14%, transparent);
|
| 43 |
+
color: #16a34a;
|
| 44 |
+
border: 1px solid color-mix(in oklab, #22c55e 35%, transparent);
|
| 45 |
+
}
|
| 46 |
+
.tam-pill.kind-http {
|
| 47 |
+
background: color-mix(in oklab, #3b82f6 12%, transparent);
|
| 48 |
+
color: color-mix(in oklab, #3b82f6 80%, var(--text-color));
|
| 49 |
+
border: 1px solid color-mix(in oklab, #3b82f6 30%, transparent);
|
| 50 |
+
}
|
| 51 |
+
.tam-pill.kind-inproc {
|
| 52 |
+
background: color-mix(in oklab, #ec4899 10%, transparent);
|
| 53 |
+
color: color-mix(in oklab, #ec4899 80%, var(--text-color));
|
| 54 |
+
border: 1px solid color-mix(in oklab, #ec4899 28%, transparent);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.tool-action-matrix__wrap { overflow-x: auto; }
|
| 58 |
+
.tool-action-matrix table {
|
| 59 |
+
width: 100%;
|
| 60 |
+
border-collapse: collapse;
|
| 61 |
+
font-size: 12px;
|
| 62 |
+
min-width: 760px;
|
| 63 |
+
}
|
| 64 |
+
.tool-action-matrix table thead th {
|
| 65 |
+
text-align: left;
|
| 66 |
+
font-size: 10.5px;
|
| 67 |
+
font-weight: 700;
|
| 68 |
+
text-transform: uppercase;
|
| 69 |
+
letter-spacing: 0.6px;
|
| 70 |
+
color: var(--muted-color);
|
| 71 |
+
padding: 10px 12px;
|
| 72 |
+
border-bottom: 1px solid var(--border-color);
|
| 73 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 74 |
+
white-space: nowrap;
|
| 75 |
+
}
|
| 76 |
+
.tool-action-matrix table thead th .swatch {
|
| 77 |
+
width: 7px; height: 7px;
|
| 78 |
+
border-radius: 50%;
|
| 79 |
+
background: var(--c, var(--muted-color));
|
| 80 |
+
display: inline-block;
|
| 81 |
+
margin-right: 5px;
|
| 82 |
+
vertical-align: middle;
|
| 83 |
+
}
|
| 84 |
+
.tool-action-matrix table tbody td {
|
| 85 |
+
padding: 10px 12px;
|
| 86 |
+
border-bottom: 1px solid color-mix(in oklab, var(--border-color) 60%, transparent);
|
| 87 |
+
vertical-align: middle;
|
| 88 |
+
color: var(--text-color);
|
| 89 |
+
font-size: 11.5px;
|
| 90 |
+
line-height: 1.5;
|
| 91 |
+
}
|
| 92 |
+
.tool-action-matrix table tbody tr:last-child td { border-bottom: 0; }
|
| 93 |
+
.tool-action-matrix table tbody tr:hover td {
|
| 94 |
+
background: color-mix(in oklab, var(--muted-color) 3%, transparent);
|
| 95 |
+
}
|
| 96 |
+
.tool-action-matrix table tbody td.row-name {
|
| 97 |
+
font-weight: 600;
|
| 98 |
+
color: var(--text-color);
|
| 99 |
+
white-space: nowrap;
|
| 100 |
+
font-size: 12px;
|
| 101 |
+
}
|
| 102 |
+
.tool-action-matrix table tbody td code {
|
| 103 |
+
background: color-mix(in oklab, var(--muted-color) 10%, transparent);
|
| 104 |
+
border-radius: 3px;
|
| 105 |
+
padding: 0 5px;
|
| 106 |
+
font-size: 11px;
|
| 107 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 108 |
+
}
|
| 109 |
+
</style>
|
| 110 |
+
<script>
|
| 111 |
+
(() => {
|
| 112 |
+
const bootstrap = () => {
|
| 113 |
+
const scriptEl = document.currentScript;
|
| 114 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 115 |
+
if (!(container && container.classList && container.classList.contains('tool-action-matrix'))) {
|
| 116 |
+
const cands = Array.from(document.querySelectorAll('.tool-action-matrix'))
|
| 117 |
+
.filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 118 |
+
container = cands[cands.length - 1] || null;
|
| 119 |
+
}
|
| 120 |
+
if (!container || (container.dataset && container.dataset.mounted === 'true')) return;
|
| 121 |
+
container.dataset.mounted = 'true';
|
| 122 |
+
|
| 123 |
+
const FRAMEWORKS = [
|
| 124 |
+
{ key: 'openenv', name: 'OpenEnv', color: '#3b82f6', kind: 'http' },
|
| 125 |
+
{ key: 'ors', name: 'ORS', color: '#a855f7', kind: 'http' },
|
| 126 |
+
{ key: 'nemo', name: 'NeMo Gym', color: '#22c55e', kind: 'http' },
|
| 127 |
+
{ key: 'verifs', name: 'Verifiers', color: '#ec4899', kind: 'inproc' },
|
| 128 |
+
{ key: 'skyrl', name: 'SkyRL Gym', color: '#f59e0b', kind: 'inproc' },
|
| 129 |
+
{ key: 'gem', name: 'GEM', color: '#14b8a6', kind: 'inproc' },
|
| 130 |
+
];
|
| 131 |
+
|
| 132 |
+
const yes = '<span class="tam-pill yes">✓</span>';
|
| 133 |
+
|
| 134 |
+
const ROWS = [
|
| 135 |
+
{ name: 'Tool definition', cells: {
|
| 136 |
+
openenv: '<code>@mcp.tool</code> on FastMCP',
|
| 137 |
+
ors: '<code>@tool</code> + Pydantic model',
|
| 138 |
+
nemo: 'FastAPI <code>app.post()</code> endpoint',
|
| 139 |
+
verifs: 'Plain Python fns into <code>ToolEnv(tools=[...])</code>',
|
| 140 |
+
skyrl: 'Methods on <code>ToolGroup</code> subclass',
|
| 141 |
+
gem: 'Env wrappers (<code>ToolEnvWrapper</code>)',
|
| 142 |
+
}},
|
| 143 |
+
{ name: 'Discovery', cells: {
|
| 144 |
+
openenv: 'MCP <code>tools/list</code>',
|
| 145 |
+
ors: 'HTTP <code>GET /tools</code>',
|
| 146 |
+
nemo: 'OpenAPI <code>GET /openapi.json</code>',
|
| 147 |
+
verifs: 'Explicit list at init',
|
| 148 |
+
skyrl: '<code>init_tool_groups()</code>',
|
| 149 |
+
gem: 'Wrapper composition',
|
| 150 |
+
}},
|
| 151 |
+
{ name: 'Schema format', cells: {
|
| 152 |
+
openenv: 'MCP tool spec (JSON Schema)',
|
| 153 |
+
ors: 'ORS ToolSpec',
|
| 154 |
+
nemo: 'OpenAPI JSON Schema',
|
| 155 |
+
verifs: 'Python type hints (auto)',
|
| 156 |
+
skyrl: 'Python type hints (auto)',
|
| 157 |
+
gem: 'Wrapper-defined',
|
| 158 |
+
}},
|
| 159 |
+
{ name: 'Multi-turn', cells: {
|
| 160 |
+
openenv: yes, ors: yes, nemo: yes,
|
| 161 |
+
verifs: yes, skyrl: yes, gem: yes,
|
| 162 |
+
}},
|
| 163 |
+
];
|
| 164 |
+
|
| 165 |
+
const headerCells = FRAMEWORKS.map(f =>
|
| 166 |
+
`<th><span class="swatch" style="--c:${f.color};"></span>${f.name}</th>`
|
| 167 |
+
).join('');
|
| 168 |
+
const kindRow = FRAMEWORKS.map(f => `<td><span class="tam-pill kind-${f.kind}">${f.kind === 'http' ? 'HTTP' : 'In-process'}</span></td>`).join('');
|
| 169 |
+
const rowsHtml = ROWS.map(r => {
|
| 170 |
+
const cells = FRAMEWORKS.map(f => `<td>${r.cells[f.key]}</td>`).join('');
|
| 171 |
+
return `<tr><td class="row-name">${r.name}</td>${cells}</tr>`;
|
| 172 |
+
}).join('');
|
| 173 |
+
|
| 174 |
+
container.innerHTML = `
|
| 175 |
+
<div class="tool-action-matrix__head">
|
| 176 |
+
<span class="tool-action-matrix__title">Tool & action model · 6 frameworks</span>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="tool-action-matrix__wrap">
|
| 179 |
+
<table>
|
| 180 |
+
<thead>
|
| 181 |
+
<tr><th>Capability</th>${headerCells}</tr>
|
| 182 |
+
</thead>
|
| 183 |
+
<tbody>
|
| 184 |
+
<tr><td class="row-name">Kind</td>${kindRow}</tr>
|
| 185 |
+
${rowsHtml}
|
| 186 |
+
</tbody>
|
| 187 |
+
</table>
|
| 188 |
+
</div>
|
| 189 |
+
`;
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
if (document.readyState === 'loading') {
|
| 193 |
+
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
| 194 |
+
} else {
|
| 195 |
+
bootstrap();
|
| 196 |
+
}
|
| 197 |
+
})();
|
| 198 |
+
</script>
|