diff --git a/README.md b/README.md
index 153a634..3d04470 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,47 @@
-# Code-Exec
-
-Run short code snippets inside a live host process — game engines, editors, any .NET or Python host — with **one
-contract** for C# and Python. Built for AI agents: the agent writes only the statements it needs, the result comes
-back as text, and errors point at the agent's own lines.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Run short code snippets inside a live host process (game engines, editors, any .NET or Python host)
+ with one contract for C# and Python. Built for AI agents: the agent writes only the statements it
+ needs, the result comes back as text, and errors point at the agent's own lines.
+
+
+## Contents
+
+- [Why Code-Exec](#why-code-exec)
+- [How a snippet runs](#how-a-snippet-runs)
+- [Quick start](#quick-start)
+- [Snippets at a glance](#snippets-at-a-glance)
+- [Execution guards](#execution-guards)
+- [Runtimes](#runtimes)
+- [Build and test](#build-and-test)
+- [Documentation](#documentation)
+- [License](#license)
+
+## Why Code-Exec
+
+An agent that works on a running game or editor often needs one small piece of code: read a value, try an API, fix
+an object. Code-Exec runs that piece inside the host, next to the live objects, and answers in a shape an agent can
+read:
```csharp
using System.Text; // optional: own usings, aliases, `using static`
@@ -12,30 +51,40 @@ sb.Append(Helper.Twice(21));
sb.ToString() // last expression = the result → "ab42"
```
-```python
-import math
-x = 21
-math.floor(x * 2.5) # last expression = the result → "52"
+```text
+success true value ab42 autoImported System.Text compileMs 609 · executeMs 2
```
-| | C# (`dotnet/`) | Python (`python/`) |
-|---|---|---|
-| Hosts | Unity (Mono), Godot (.NET 8+), BRUTAL, any .NET Framework / .NET app | Unreal Editor (PythonScriptPlugin), any embedded interpreter |
-| Ships as | NuGet `com.IvanMurzak.CodeExec` + source-only `com.IvanMurzak.CodeExec.Roslyn` | the `code_exec` package, vendored by the host |
-| Dependencies | **none** — you bring your own `Microsoft.CodeAnalysis.CSharp` (3.8 or newer) | standard library only, Python 3.10+ |
-| Unloading | CoreCLR: each run in its own collectible `AssemblyLoadContext`; Mono: identical requests reuse one assembly | fresh namespace per run |
-
-The normative contract — request/response fields, snippet semantics, error kinds — is
-[`spec/contract.md`](spec/contract.md). Shared golden cases: [`fixtures/golden/`](fixtures/golden).
-
-## Why no Roslyn dependency
-
-Every host already has its own Roslyn constraints (Unity pins 4.8 because of `System.Collections.Immutable`; Godot and
-.NET 10 apps can use the latest). So the core library has no dependencies at all, and the Roslyn adapter ships as
-**source**: it compiles inside your project, against your Roslyn. CI checks that source against Roslyn 3.8, 4.0, 4.8,
-4.14 and 5.x, on .NET Framework 4.8, .NET 8 and .NET 10.
-
-## C# in two lines
+- **Statements, not boilerplate.** No class, no `Main`: usings, types and functions where the agent needs them, and
+ the last expression is the result. Unknown simple names are imported when exactly one namespace defines them.
+- **Errors in the agent's coordinates.** Diagnostics and stack frames name `snippet.cs` / `snippet.py` and the
+ agent's own lines, never the generated wrapper:
+ `snippet.cs(2,3): error CS1061: 'int' does not contain a definition for 'Lenght'`.
+- **Safe for the host.** Runaway recursion and endless loops stop the run with an error, never the process, also on
+ the host's main thread ([details](#execution-guards)).
+- **Any Roslyn, no dependency.** The core has no package dependencies; the Roslyn adapter ships as **source** and
+ compiles against the Roslyn your host already has (3.8 or newer).
+- **One contract, two runtimes.** C# and Python share one request/response shape and one set of golden cases
+ ([`spec/contract.md`](spec/contract.md), [`fixtures/golden/`](fixtures/golden)).
+
+## How a snippet runs
+
+
+
+
+
+
+
+
+`Prepare()` wraps the statements, resolves names, injects the guards and compiles against the assemblies the host
+already runs; it is safe on a background thread. `Run()` / `RunAsync()` invoke the snippet on the calling (main)
+thread. On CoreCLR each run gets its own collectible `AssemblyLoadContext`; on Mono identical requests reuse one
+assembly. The host's `ValueConverter` turns the result into something that outlives the snippet before its code is
+released (Unity: ReflectorNet JSON).
+
+## Quick start
+
+### C# (NuGet)
```xml
@@ -52,50 +101,121 @@ var runner = new CodeRunner(new RoslynCSharpCompiler(), new CodeRunnerOptions
ExecuteResult result = await runner.ExecuteAsync(new ExecuteRequest { Code = "Application.unityVersion" });
```
-- `Prepare()` compiles and is safe on a background thread; `Run()` / `RunAsync()` invoke on the calling (main) thread.
-- `include` takes files and folders (recursive): the agent's helper "micro-projects", compiled with the snippet.
-- Unknown simple names are imported automatically when exactly one namespace defines them (`autoImported`);
- ambiguous names are reported, never guessed. CoreLib's `Internal.*` namespaces are never imported.
-- `ValueConverter` turns the result into something that outlives the snippet (Unity: ReflectorNet JSON).
-- Execution guards ([§3.6](spec/contract.md#36-execution-guards)): runaway recursion stops with an error before a stack
- overflow could end the host, and loops stop with `Timeout` once the time budget is spent, also in synchronous code
- on the main thread. Budget: `TimeoutMs` on the request, else `DefaultTimeoutMs` (120 s), at most `MaxTimeoutMs`
- (0 = no cap); stack allowance `MaxStackBytes` (1 MiB); `ExecutionGuards = false` turns them off. Threads and tasks
- the snippet starts (`new Thread`, `ThreadPool`, `Task.Run`, `StartNew`) stop once its run has ended.
-- Agent tools: expose `timeoutMs` as a `timeout` argument, default to about 30 s, and cap it below the tool timeout
- ([§1](spec/contract.md#1-request)).
-- Output is limited before it reaches the agent ([§6](spec/contract.md#6-output-limits)): a text value, the logs (first
- and last lines kept, 2k per line) and the error, 20k characters each by default (`MaxValueChars`, `MaxLogChars`,
- `MaxLogLineChars`, `MaxErrorChars` on the request; 0 = no limit). Includes stay inside `IncludeRoot` unless `AllowIncludeOutsideRoot`.
-- Hosts that load assemblies from memory (Godot's game assembly) pass a `locationResolver` to
- `LoadedAssembliesReferenceProvider`, otherwise snippets cannot see those types. On CoreCLR a snippet binds to the
- copy the process already runs (any load context) before the default context is asked, so a host's
- `AssemblyLoadContext.Default.Resolving` handler cannot hand it a second copy.
-
-## Python
+`com.IvanMurzak.CodeExec` targets `netstandard2.0` (Unity / Mono, .NET Framework) and `net8.0`.
+`com.IvanMurzak.CodeExec.Roslyn` is source-only: it compiles inside your project against **your**
+`Microsoft.CodeAnalysis.CSharp`.
+
+### Python
```python
import code_exec
code_exec.run("x = 21\nx * 2", include=["helpers"], include_root=".ai-game-dev/snippets", imports=["unreal"])
-code_exec.run_b64(base64_json_request) # JSON in, JSON out: one string literal from C++
-code_exec.run("print('x')", max_log_chars=0) # the same limits as C# (JSON: maxLogChars, ...); 0 = no limit
-code_exec.run("while True: pass", timeout_ms=5000) # stopped by a watchdog: errorKind Timeout (JSON: timeoutMs)
+code_exec.run_b64(base64_json_request) # JSON in, JSON out: one string literal from C++
+code_exec.run("print('x')", max_log_chars=0) # the same limits as C# (JSON: maxLogChars, ...); 0 = no limit
+code_exec.run("while True: pass", timeout_ms=5000) # stopped by a watchdog: errorKind Timeout (JSON: timeoutMs)
code_exec.set_default_timeout_ms(30000) # the budget of requests without one (default 120000; 0 = none)
code_exec.set_max_timeout_ms(280000) # the most a request may ask for (default 0 = no cap)
code_exec.set_max_recursion_limit(3000) # Python < 3.12: the most a snippet's sys.setrecursionlimit sets (0 = no cap)
```
-## Development
+The `code_exec` package uses the standard library only (Python 3.10+); hosts vendor it.
+
+## Snippets at a glance
+
+```python
+import math
+x = 21
+math.floor(x * 2.5) # last expression = the result → "52"
+```
+
+| | |
+|---|---|
+| Result | the last expression (C# and Python), or `return` (C#); `returnType` names its static type |
+| Logs | `Console` / `print` / engine log hooks, as `{ level, message }` entries |
+| `include` | files and folders (recursive): the agent's helper "micro-projects", compiled or loaded with the snippet; they stay inside the include root unless the host allows otherwise |
+| Auto-import | unknown simple names are imported when exactly one namespace defines them (`autoImported`); ambiguous names are reported, never guessed; CoreLib's `Internal.*` namespaces are never imported |
+| Full-source mode | C#: `entry: { type, method }` compiles the code verbatim and invokes that method |
+| Parameters | C#: `{ name, type, value }` arguments with host types and already-deserialized values |
+| Output limits | value, logs (first and last lines kept, 2k per line) and error: 20k characters each by default; `0` = no limit ([§6](spec/contract.md#6-output-limits)) |
+| Error kinds | `None`, `InvalidRequest`, `Compile`, `EntryPointNotFound`, `Runtime`, `Timeout`, `NotAvailable` |
+
+Hosts that load assemblies from memory (Godot's game assembly) pass a `locationResolver` to
+`LoadedAssembliesReferenceProvider`, otherwise snippets cannot see those types. On CoreCLR a snippet binds to the copy
+the process already runs (any load context) before the default context is asked, so a host's
+`AssemblyLoadContext.Default.Resolving` handler cannot hand it a second copy.
+
+## Execution guards
+
+.NET cannot catch a stack overflow, and code on the host's main thread cannot be aborted from outside. So the C#
+runtime compiles guards into the agent's code ([§3.6](spec/contract.md#36-execution-guards)); Python uses a watchdog.
+
+| Guard | What happens |
+|---|---|
+| Recursion | stops with `System.InsufficientExecutionStackException: Recursion too deep` before a stack overflow could end the host; allowance `MaxStackBytes` (1 MiB) |
+| Time | loops, calls and blocking waits (`Join`, `Wait`, `Result`, `Sleep`, ...) check the clock; once the budget is spent the run stops with `Timeout`, also in synchronous code on the main thread |
+| Threads and tasks | work the snippet starts (`new Thread`, `ThreadPool`, `Task.Run`, `StartNew`) belongs to its run and stops once the run has ended |
+| Catch-alls | the agent's `catch (Exception)` lets a stop pass, so it reaches the runtime; `finally` blocks still run |
+
+Budget: `TimeoutMs` on the request, else `DefaultTimeoutMs` (120 s), at most `MaxTimeoutMs` (0 = no cap);
+`ExecutionGuards = false` turns the guards off. For agent tools: expose `timeoutMs` as a `timeout` argument, default
+to about 30 s, and cap it below the tool's own timeout ([§1](spec/contract.md#1-request)).
+
+## Runtimes
+
+
+
+
+
+
+
+
+| | C# (`dotnet/`) | Python (`python/`) |
+|---|---|---|
+| Hosts | Unity (Mono), Godot (.NET 8+), BRUTAL, any .NET Framework / .NET app | Unreal Editor (PythonScriptPlugin), any embedded interpreter |
+| Ships as | NuGet `com.IvanMurzak.CodeExec` + source-only `com.IvanMurzak.CodeExec.Roslyn` | the `code_exec` package, vendored by the host |
+| Dependencies | **none**: you bring your own `Microsoft.CodeAnalysis.CSharp` (3.8 or newer) | standard library only, Python 3.10+ |
+| Unloading | CoreCLR: each run in its own collectible `AssemblyLoadContext`; Mono: identical requests reuse one assembly | fresh namespace per run |
+| Guards | compiled into the snippet: stack, time, blocking waits, threads and tasks | watchdog for the time budget, capped recursion limit |
+| Golden cases | [`fixtures/golden/csharp.json`](fixtures/golden/csharp.json) | [`fixtures/golden/python.json`](fixtures/golden/python.json) |
+
+**Why no Roslyn dependency.** Every host already has its own Roslyn constraints (Unity pins 4.8 because of
+`System.Collections.Immutable`; Godot and .NET 10 apps can use the latest). So the core library has no dependencies
+at all, and the Roslyn adapter ships as source: it compiles inside your project, against your Roslyn. CI checks that
+source against Roslyn 3.8, 4.0, 4.8, 4.14 and 5.x, on .NET Framework 4.8, .NET 8 and .NET 10.
+
+Code-Exec powers the code execution tools of [AI Game Dev](https://ai-game.dev) for Unity, Unreal Engine and Godot.
+The library itself has no dependency on any engine, MCP or hosted service.
+
+## Build and test
+
+Requirements: .NET SDK 10 (and the .NET 8 runtime), Python ≥ 3.10.
```bash
scripts/test-all.sh # .NET tests, pack + package consumer, Python tests
ROSLYN_VERSIONS="3.8.0 4.8.0 5.9.0" scripts/test-all.sh
+dotnet test dotnet/CodeExec.Tests -p:RoslynVersion=4.8.0
dotnet test dotnet/CodeExec.Tests -p:IncludeNetFx=true -f net48 # Windows: the Mono-like path
+python -B -m unittest discover -s python/tests
scripts/pack.sh # pack + package shape checks, publishes nothing
+python3 scripts/readme-assets.py # regenerate the README art in docs/assets/
```
-Releases: `.github/workflows/release.yml`, dispatch only. See [`AGENTS.md`](AGENTS.md).
+Releases: [`.github/workflows/release.yml`](.github/workflows/release.yml), dispatch only.
+
+## Documentation
+
+| | |
+|---|---|
+| [`spec/contract.md`](spec/contract.md) | the normative contract: request and response fields, C# and Python snippet semantics, guards, output limits |
+| [`fixtures/golden/`](fixtures/golden) | shared golden cases both runtimes are tested against; [`fixtures/include/`](fixtures/include) the include trees they use |
+| [`dotnet/PACKAGE.md`](dotnet/PACKAGE.md) | the NuGet package readme |
+| [`dotnet/samples/`](dotnet/samples) | a strict package consumer and a source consumer (C# 9, `netstandard2.0`: how Unity vendors it) |
+| [`AGENTS.md`](AGENTS.md) | layout, contributor and agent rules |
+
+Repository layout: `spec/` · `fixtures/` · `dotnet/` (Core, Roslyn adapter, tests, samples) · `python/` (runtime and
+tests) · `scripts/` · `docs/assets/`.
## License
-[PolyForm Shield 1.0.0](LICENSE.md). Copyright (c) 2026 Ivan Murzak.
+[PolyForm Shield 1.0.0](LICENSE.md): free to use, including in commercial products, except to provide a product that
+competes with the licensor's products. Copyright (c) 2026 Ivan Murzak.
diff --git a/docs/assets/architecture-dark.svg b/docs/assets/architecture-dark.svg
new file mode 100644
index 0000000..f5d259f
--- /dev/null
+++ b/docs/assets/architecture-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/assets/architecture-light.svg b/docs/assets/architecture-light.svg
new file mode 100644
index 0000000..f60618c
--- /dev/null
+++ b/docs/assets/architecture-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/assets/flow-dark.svg b/docs/assets/flow-dark.svg
new file mode 100644
index 0000000..5f9481c
--- /dev/null
+++ b/docs/assets/flow-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/assets/flow-light.svg b/docs/assets/flow-light.svg
new file mode 100644
index 0000000..5c8108a
--- /dev/null
+++ b/docs/assets/flow-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/assets/hero-dark.svg b/docs/assets/hero-dark.svg
new file mode 100644
index 0000000..deac8d8
--- /dev/null
+++ b/docs/assets/hero-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/assets/hero-light.svg b/docs/assets/hero-light.svg
new file mode 100644
index 0000000..c51ef3c
--- /dev/null
+++ b/docs/assets/hero-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/scripts/readme-assets.py b/scripts/readme-assets.py
new file mode 100644
index 0000000..876b46c
--- /dev/null
+++ b/scripts/readme-assets.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""Generate the README art in docs/assets/: animated hero, run-flow and architecture SVGs (light + dark, shown through
+). Usage: python3 scripts/readme-assets.py [out-dir]
+
+The SVGs animate with SMIL only (no scripts, no external fonts: GitHub renders them through ). The snippets, values,
+error texts and timings drawn here are real output of the C# runtime (Roslyn 4.8, .NET 10); keep them in sync with the
+README and the contract.
+"""
+import os
+import sys
+
+OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "docs", "assets")
+MONO = "ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace"
+SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"
+
+PALETTES = {
+ "dark": dict(bg="#0d1117", card="#161b22", card2="#1c2330", bd="#30363d", tx="#e6edf3", mu="#8b949e",
+ ac="#39d0d8", vi="#a371f7", gr="#3fb950", orr="#f0883e", pk="#ff7b72", ye="#e3b341", g1="#3fb950", g2="#39d0d8"),
+ "light": dict(bg="#ffffff", card="#f6f8fa", card2="#eef2f6", bd="#d0d7de", tx="#1f2328", mu="#59636e",
+ ac="#08707a", vi="#7341c9", gr="#1a7f37", orr="#a94400", pk="#cf222e", ye="#8a5c00", g1="#1a7f37", g2="#08707a"),
+}
+CW = 0.602 # monospace advance per em
+
+
+def style(p):
+ return f""""""
+
+
+def svg(w, h, p, body, title):
+ return (f'')
+
+
+def kt(*xs):
+ return ";".join(f"{x:.4f}".rstrip("0").rstrip(".") if x not in (0, 1) else str(int(x)) for x in xs)
+
+
+def frame(W, H):
+ return [f'',
+ f'']
+
+
+def spans(parts):
+ return "".join(f'{t}' for c, t in parts)
+
+
+def plen(parts):
+ return sum(len(t.replace("<", "<").replace(">", ">").replace("&", "&")) for _, t in parts)
+
+
+# ---------------------------------------------------------------- hero
+def hero(p):
+ W, H, D = 1200, 320, 11.0
+ b = frame(W, H)
+ # faint background: brackets and a caret, pulsing
+ for i, (x, y, t) in enumerate(((530, 64, "{ }"), (560, 140, "( )"), (522, 214, "=>"), (566, 282, "[ ]"), (580, 92, ";"))):
+ b.append(f'{t}')
+ # wordmark
+ b.append(f'Code-Exec')
+ b.append(f'Run code inside the live host')
+ b.append(f'Short C# and Python snippets from an AI agent,')
+ b.append(f'one contract, guarded, result back as text.')
+ x = 62
+ for label, cls in (("C#", "vi"), ("Python", "ye"), ("Roslyn 3.8 → 5.x", "ac")):
+ w = 22 + len(label) * 7.9
+ b.append(f''
+ f'{label}')
+ x += w + 10
+ # terminal
+ tx0, ty0, tw, th = 610, 30, 560, 260
+ b.append(f'')
+ for i, c in enumerate(("pk", "ye", "gr")):
+ b.append(f'')
+ b.append(f'script-execute · C#')
+
+ lines = [
+ [("ac", "› "), ("vi", "var "), ("tx", "sb = "), ("vi", "new "), ("ac", "StringBuilder"), ("tx", "("), ("gr", '"ab"'), ("tx", ");")],
+ [("tx", " "), ("vi", "class "), ("ac", "Helper "), ("tx", "{ "), ("vi", "public static int "), ("tx", "Twice("),
+ ("vi", "int "), ("tx", "v) => v * "), ("or", "2"), ("tx", "; }")],
+ [("tx", " sb.Append(Helper.Twice("), ("or", "21"), ("tx", "));")],
+ [("tx", " sb.ToString()"), ("mu", " // last expression = result")],
+ ]
+ fs = 13.5
+ cw = fs * CW
+ lx = tx0 + 20
+ ly = [ty0 + 58 + i * 22 for i in range(len(lines))]
+ # typewriter windows on a shared loop
+ win = [(0.03, 0.12), (0.13, 0.27), (0.28, 0.35), (0.36, 0.42)]
+ end = 0.95
+ fade = f''
+ g = [f'{fade}']
+ for i, (parts, (ta, tb)) in enumerate(zip(lines, win)):
+ n = plen(parts)
+ w = n * cw + 4
+ b.append(f'')
+ g.append(f'{spans(parts)}')
+ g.append('')
+ b.extend(g)
+ # cursor follows the typing
+ cx, cy, ck = [lx], [ly[0] - 12], [0]
+ for i, (parts, (ta, tb)) in enumerate(zip(lines, win)):
+ n = plen(parts)
+ cx += [lx, lx + n * cw]
+ cy += [ly[i] - 12, ly[i] - 12]
+ ck += [ta, tb]
+ cx.append(cx[-1]); cy.append(cy[-1]); ck.append(1)
+ b.append(f''
+ f''
+ f''
+ f'')
+ # divider + "running" hint
+ dy = ty0 + 150
+ b.append(f'')
+ b.append(f'compiling against the host …'
+ f'')
+ # result (real output)
+ rows = [
+ [("gr", "✓ success "), ("mu", "value "), ("tx", "ab42")],
+ [("mu", " autoImported "), ("ac", "System.Text")],
+ [("mu", " compile "), ("or", "609"), ("mu", " ms · execute "), ("or", "2"), ("mu", " ms")],
+ ]
+ for i, parts in enumerate(rows):
+ a = 0.53 + i * 0.04
+ b.append(f'{spans(parts)}'
+ f'')
+ return svg(W, H, p, "".join(b), "Code-Exec: a C# snippet typed into an agent tool and the result it returns")
+
+
+# ---------------------------------------------------------------- run flow
+def flow(p):
+ W, H, D = 1200, 400, 12.0
+ b = frame(W, H)
+ CY = 196
+
+ def box(x, y, w, h, title, subs, tcls="tx"):
+ s = f''
+ top = y - (len(subs) * 17) / 2 - 2
+ s += f'{title}'
+ for k, sub in enumerate(subs):
+ s += f'{sub}'
+ return s
+
+ for x, t in ((30, "AGENT"), (240, "PREPARE"), (500, "RUN, INSIDE THE HOST"), (930, "RESULT")):
+ b.append(f'{t}')
+
+ # snippet box with cycling code
+ b.append(f'')
+ b.append(f'snippet.cs')
+ runs = [ # code lines, color, outcome row index, start time
+ (["sb.Append(…);", "sb.ToString()"], "gr", 0, 0.00),
+ (["int F(int n) =>", " 1 + F(n - 1);", "F(10_000_000)"], "pk", 1, 0.33),
+ (["while (true)", "{ }"], "ye", 2, 0.66),
+ ]
+ for code, c, _, t0 in runs:
+ t1 = t0 + 0.31
+ anim = f''
+ lines = "".join(f'{l.replace(">", ">")}' for k, l in enumerate(code))
+ b.append(f'{lines}{anim}')
+
+ b.append(box(240, CY, 210, 112, "Prepare", ["wrap statements", "auto-import names", "inject guards", "compile · your Roslyn"], "ac"))
+ b.append(box(500, CY, 210, 112, "Run", ["host's main thread", "own load context", "stack + time budget"], "vi"))
+ b.append(box(756, CY - 92, 130, 62, "Convert", ["ValueConverter"], "tx"))
+ outs = [(CY - 92, "gr", "✓ value", "ab42"), (CY + 8, "pk", "Runtime", "Recursion too deep"), (CY + 100, "ye", "Timeout", "budget spent")]
+ for y, c, k, v in outs:
+ b.append(f''
+ f'{k}'
+ f'{v}')
+ # guard markers in the run box
+ paths = {
+ "a": "M200,%d L240,%d" % (CY, CY),
+ "b": "M450,%d L500,%d" % (CY, CY),
+ "ok1": f"M710,{CY} C735,{CY} 731,{CY-92} 756,{CY-92}",
+ "ok2": f"M886,{CY-92} L930,{CY-92}",
+ "rt": f"M710,{CY} C820,{CY} 820,{CY+8} 930,{CY+8}",
+ "to": f"M710,{CY} C820,{CY} 820,{CY+100} 930,{CY+100}",
+ }
+ for d in paths.values():
+ b.append(f'')
+
+ def packet(d, ts, te, c):
+ return (f''
+ f'')
+
+ def glow(x, y, w, h, t, cls="sac", hold=0.05):
+ return (f''
+ f'')
+
+ for code, c, out, t0 in runs:
+ b.append(packet(paths["a"], t0 + 0.03, t0 + 0.05, c))
+ b.append(glow(240, CY, 210, 112, t0 + 0.05))
+ b.append(packet(paths["b"], t0 + 0.10, t0 + 0.12, c))
+ b.append(glow(500, CY, 210, 112, t0 + 0.12, "svi", 0.06))
+ if out == 0:
+ b.append(packet(paths["ok1"], t0 + 0.19, t0 + 0.22, c))
+ b.append(glow(756, CY - 92, 130, 62, t0 + 0.22, "sac", 0.02))
+ b.append(packet(paths["ok2"], t0 + 0.245, t0 + 0.26, c))
+ t_hit = t0 + 0.26
+ else:
+ b.append(packet(paths["rt" if out == 1 else "to"], t0 + 0.19, t0 + 0.24, c))
+ t_hit = t0 + 0.24
+ label = "stack guard" if out == 1 else "time budget"
+ b.append(f'⛔ {label}'
+ f'')
+ y = outs[out][0]
+ b.append(glow(930, y, 240, 52, t_hit, {"gr": "sgr", "pk": "spk", "ye": "sye"}[c], 0.05))
+ b.append(f''
+ f'Runaway recursion and endless loops stop the run, never the host; the next snippet runs normally.')
+ return svg(W, H, p, "".join(b), "How a snippet runs: prepared and compiled, run inside the host under guards, and returned as a value, a runtime error or a timeout")
+
+
+# ---------------------------------------------------------------- architecture
+def architecture(p):
+ W, H, D = 1200, 330, 7.0
+ b = frame(W, H)
+ CY = 158
+
+ def box(x, y, w, h, title, sub, tcls="tx"):
+ s = f''
+ s += f'{title}'
+ if sub:
+ s += f'{sub}'
+ return s
+
+ b.append(box(30, CY, 170, 70, "Agent tool call", "code · timeoutMs", "ac"))
+ b.append(box(260, CY, 210, 96, "One contract", "spec/contract.md", "vi"))
+ b.append(f'+ fixtures/golden')
+ rt = [(CY - 70, "C# runtime", "Core + Roslyn adapter", "vi"), (CY + 70, "Python runtime", "code_exec, stdlib only", "ye")]
+ for y, t, s, c in rt:
+ b.append(box(540, y, 230, 64, t, s, c))
+ hosts = [(CY - 70, "Unity · Godot · .NET", "Mono · CoreCLR · net48"), (CY + 70, "Unreal Editor", "any embedded Python")]
+ for y, t, s in hosts:
+ b.append(box(840, y, 200, 64, t, s, "tx"))
+ b.append(box(1080, CY, 100, 70, "Result", "JSON", "gr"))
+ b.append(f''
+ f'One request/response shape and one set of golden cases; both runtimes are tested against them.')
+ paths = ["M200,%d L260,%d" % (CY, CY)]
+ for y, *_ in rt:
+ paths.append(f"M470,{CY} C505,{CY} 505,{y} 540,{y}")
+ paths.append(f"M770,{y} L840,{y}")
+ paths.append(f"M1040,{y} C1060,{y} 1060,{CY} 1080,{CY}")
+ for d in paths:
+ b.append(f'')
+ seq = [(paths[0], 0.02, 0.12, "ac")]
+ for i, (y, _, _, c) in enumerate(rt):
+ seq.append((paths[1 + 3*i], 0.30, 0.42, c))
+ seq.append((paths[2 + 3*i], 0.52, 0.62, c))
+ seq.append((paths[3 + 3*i], 0.72, 0.82, "gr"))
+ for d, ts, te, c in seq:
+ b.append(f''
+ f'')
+ glows = [((260, CY, 210, 96), 0.12), ((1080, CY, 100, 70), 0.82)]
+ for y, *_ in rt:
+ glows.append(((540, y, 230, 64), 0.42))
+ for y, *_ in hosts:
+ glows.append(((840, y, 200, 64), 0.62))
+ for (x, y, w, h), t in glows:
+ b.append(f''
+ f'')
+ return svg(W, H, p, "".join(b), "Code-Exec architecture: one contract and golden cases, a C# runtime for .NET hosts and a Python runtime for Unreal and embedded interpreters")
+
+
+os.makedirs(OUT, exist_ok=True)
+for name, fn in (("hero", hero), ("flow", flow), ("architecture", architecture)):
+ for theme, pal in PALETTES.items():
+ with open(os.path.join(OUT, f"{name}-{theme}.svg"), "w", encoding="utf-8", newline="\n") as f:
+ f.write(fn(pal) + "\n")
+print("ok")