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. +

+ + + Code-Exec: a C# snippet typed into an agent tool and the result it returns + +

+ +

+ CI + NuGet: com.IvanMurzak.CodeExec + NuGet: com.IvanMurzak.CodeExec.Roslyn + License: PolyForm Shield 1.0.0 + Powers ai-game.dev +

+ +

+ 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. +

+ +

+ Contract · + Golden cases · + NuGet +

+ +## 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 + +

+ + + A snippet is prepared and compiled, runs inside the host under guards, and comes back as a value, a runtime error or a timeout + +

+ +`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 + +

+ + + One contract and golden cases; a C# runtime for .NET hosts and a Python runtime for Unreal and embedded interpreters + +

+ +| | 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 @@ +Code-Exec architecture: one contract and golden cases, a C# runtime for .NET hosts and a Python runtime for Unreal and embedded interpretersAgent tool callcode · timeoutMsOne contractspec/contract.md+ fixtures/goldenC# runtimeCore + Roslyn adapterPython runtimecode_exec, stdlib onlyUnity · Godot · .NETMono · CoreCLR · net48Unreal Editorany embedded PythonResultJSONOne request/response shape and one set of golden cases; both runtimes are tested against them. 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 @@ +Code-Exec architecture: one contract and golden cases, a C# runtime for .NET hosts and a Python runtime for Unreal and embedded interpretersAgent tool callcode · timeoutMsOne contractspec/contract.md+ fixtures/goldenC# runtimeCore + Roslyn adapterPython runtimecode_exec, stdlib onlyUnity · Godot · .NETMono · CoreCLR · net48Unreal Editorany embedded PythonResultJSONOne request/response shape and one set of golden cases; both runtimes are tested against them. 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 @@ +How a snippet runs: prepared and compiled, run inside the host under guards, and returned as a value, a runtime error or a timeoutAGENTPREPARERUN, INSIDE THE HOSTRESULTsnippet.cssb.Append(…);sb.ToString()int F(int n) => 1 + F(n - 1);F(10_000_000)while (true){ }Preparewrap statementsauto-import namesinject guardscompile · your RoslynRunhost's main threadown load contextstack + time budgetConvertValueConverter✓ valueab42RuntimeRecursion too deepTimeoutbudget spent⛔ stack guard⛔ time budgetRunaway recursion and endless loops stop the run, never the host; the next snippet runs normally. 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 @@ +How a snippet runs: prepared and compiled, run inside the host under guards, and returned as a value, a runtime error or a timeoutAGENTPREPARERUN, INSIDE THE HOSTRESULTsnippet.cssb.Append(…);sb.ToString()int F(int n) => 1 + F(n - 1);F(10_000_000)while (true){ }Preparewrap statementsauto-import namesinject guardscompile · your RoslynRunhost's main threadown load contextstack + time budgetConvertValueConverter✓ valueab42RuntimeRecursion too deepTimeoutbudget spent⛔ stack guard⛔ time budgetRunaway recursion and endless loops stop the run, never the host; the next snippet runs normally. 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 @@ +Code-Exec: a C# snippet typed into an agent tool and the result it returns{ }( )=>[ ];Code-ExecRun code inside the live hostShort C# and Python snippets from an AI agent,one contract, guarded, result back as text.C#PythonRoslyn 3.8 → 5.xscript-execute · C#› var sb = new StringBuilder("ab"); class Helper { public static int Twice(int v) => v * 2; } sb.Append(Helper.Twice(21)); sb.ToString() // last expression = resultcompiling against the host …✓ success value ab42 autoImported System.Text compile 609 ms · execute 2 ms 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 @@ +Code-Exec: a C# snippet typed into an agent tool and the result it returns{ }( )=>[ ];Code-ExecRun code inside the live hostShort C# and Python snippets from an AI agent,one contract, guarded, result back as text.C#PythonRoslyn 3.8 → 5.xscript-execute · C#› var sb = new StringBuilder("ab"); class Helper { public static int Twice(int v) => v * 2; } sb.Append(Helper.Twice(21)); sb.ToString() // last expression = resultcompiling against the host …✓ success value ab42 autoImported System.Text compile 609 ms · execute 2 ms 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'{title}{style(p)}' + f'' + f'{body}') + + +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")