From 32d40ff54341e80f80e181a33e22fd2e0a0ee510 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Tue, 30 Jun 2026 20:37:56 -0600 Subject: [PATCH] refactor(examples): collapse supervisor-loop's duplicate runners into one WORKER_BACKEND knob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example's own thesis is 'the worker backend is the ONLY knob' — but it had THREE runners: run-bridge.ts + run-sandbox.ts (~80% duplicate scaffolding differing only in backend: 'bridge' vs 'sandbox'), and run-supervisor-mcp.ts which THREW for sandbox despite headlining 'flip WORKER_BACKEND=sandbox, zero changes'. - shared.ts gains buildWorkerBackend() (returns the ExecutorConfig for bridge OR sandbox, constructing the SandboxClient when sandbox) + resolveSupervisorBrain() (router-if-key else scripted). The two swap seams. - run-bridge.ts + run-sandbox.ts (198L, 2 files) → one run.ts (67L). WORKER_BACKEND is the only knob. - run-supervisor-mcp.ts: deletes its local throwing workerBackend(), reuses buildWorkerBackend() — so the 'one knob' claim is now TRUE (sandbox works, no throw). - READMEs (example + catalog) updated to the single run.ts. Net: ~130 fewer lines, one fewer file, the thesis is now real. Verified: examples typecheck 0; lint clean; the offline supervisor-loop-example.test.ts (which imports shared.ts) still passes. --- examples/README.md | 2 +- examples/supervisor-loop/README.md | 30 ++--- examples/supervisor-loop/run-bridge.ts | 105 ------------------ examples/supervisor-loop/run-sandbox.ts | 93 ---------------- .../supervisor-loop/run-supervisor-mcp.ts | 38 +------ examples/supervisor-loop/run.ts | 64 +++++++++++ examples/supervisor-loop/shared.ts | 73 +++++++++++- 7 files changed, 156 insertions(+), 249 deletions(-) delete mode 100644 examples/supervisor-loop/run-bridge.ts delete mode 100644 examples/supervisor-loop/run-sandbox.ts create mode 100644 examples/supervisor-loop/run.ts diff --git a/examples/README.md b/examples/README.md index e8fdd854..b23228ab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -121,7 +121,7 @@ pnpm tsx examples/recursive-supervisor/recursive-supervisor.ts pnpm tsx examples/driver-loop/driver-loop.ts # SEE THE FOLD (offline) TANGLE_API_KEY=... pnpm tsx examples/supervise/supervise.ts # the one-call supervisor WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 \ - pnpm tsx examples/supervisor-loop/run-bridge.ts # same supervisor, local cli-bridge backend + WORKER_BACKEND=bridge WORKER_MODEL=opencode/... pnpm tsx examples/supervisor-loop/run.ts # one knob: bridge|sandbox TANGLE_API_KEY=... pnpm tsx examples/delegate/delegate.ts # delegate(intent), one call # Tier 2 — the runLoop kernel diff --git a/examples/supervisor-loop/README.md b/examples/supervisor-loop/README.md index 51b81d0d..5b48459b 100644 --- a/examples/supervisor-loop/README.md +++ b/examples/supervisor-loop/README.md @@ -8,8 +8,8 @@ see [`../supervise/`](../supervise/); the runners here add the per-backend worke | Runner | Worker backend | Command | |---|---|---| -| `run-bridge.ts` | local cli-bridge (real harness CLI) | `WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run-bridge.ts` | -| `run-sandbox.ts` | a real cloud box | `TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... pnpm tsx examples/supervisor-loop/run-sandbox.ts` | +| `run.ts` | local cli-bridge (real harness CLI) | `WORKER_BACKEND=bridge WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run.ts` | +| `run.ts` | a real cloud box | `WORKER_BACKEND=sandbox TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... pnpm tsx examples/supervisor-loop/run.ts` | | `run-supervisor-mcp.ts` | `WORKER_BACKEND` (`bridge`\|`sandbox`) over the coordination MCP | `WORKER_BACKEND=bridge pnpm dlx tsx examples/supervisor-loop/run-supervisor-mcp.ts` | ## Details @@ -19,12 +19,16 @@ the offline-vs-real driver brain, and the one-call run-context boilerplate. ### Files -- **`shared.ts`** — the demo fixtures the runners reuse: `demoGoal` + the deployable `demoCheck` - (the completion oracle), and `scriptedSupervisorChat` (a fixed `spawn → await → stop` brain so - the box/bridge wiring runs offline with no inference). -- **`run-sandbox.ts`** — `supervise()` with `backend: 'sandbox'`. Each worker is a coding harness in a real box. -- **`run-bridge.ts`** — `supervise()` with `backend: 'bridge'`. Each worker is a real harness CLI (claude-code / codex / opencode / kimi / gemini) fronted by the OpenAI-compatible bridge in `~/code/cli-bridge`. **The local path.** -- **`run-supervisor-mcp.ts`** — the real MCP path (below): a harness agent IS the supervisor and calls `spawn_agent` natively over the coordination MCP. +- **`shared.ts`** — the demo fixtures + the swap seam: `demoGoal` + the deployable `demoCheck` (the + completion oracle), `scriptedSupervisorChat` (a fixed `spawn → await → stop` brain so the wiring runs + offline with no inference), and the two functions that make the "one knob" real — **`buildWorkerBackend()`** + (returns the worker `ExecutorConfig` for `WORKER_BACKEND=bridge` or `sandbox`) and **`resolveSupervisorBrain()`** + (router brain if a key is present, else scripted). +- **`run.ts`** — the plain `supervise()` runner. **One file, one knob:** `WORKER_BACKEND=bridge` runs workers + through the local cli-bridge (real harness CLIs — claude-code / codex / opencode / kimi / gemini); flip to + `WORKER_BACKEND=sandbox` and the *same* supervisor drives workers in real Tangle boxes. +- **`run-supervisor-mcp.ts`** — the real MCP path (below): a harness agent IS the supervisor and calls + `spawn_agent` natively over the coordination MCP. Same `WORKER_BACKEND` knob (via `buildWorkerBackend`). ### Supervisor + coordinator MCP, workers on sandbox OR cli-bridge — swap `WORKER_BACKEND`, same code @@ -59,7 +63,7 @@ WORKER_BACKEND=sandbox SANDBOX_BASE_URL=https://... TANGLE_API_KEY=sk-... \ pnpm dlx tsx examples/supervisor-loop/run-supervisor-mcp.ts ``` -This is distinct from the `run-bridge.ts` / `run-sandbox.ts` runners below, which drive a +This is distinct from the plain `run.ts` runner above, which drives a **scripted/router `ToolLoopChat` brain** through `supervise()`. `run-supervisor-mcp.ts` has no driver brain at all — the harness itself reasons the spawn → await → stop loop via the MCP. @@ -71,8 +75,8 @@ resolves from `dist/`. | Backend | Command | Needs | |---|---|---| | **`router-tools`** | `TANGLE_API_KEY=sk-... pnpm tsx examples/supervise/supervise.ts` | `TANGLE_API_KEY`; optional `TANGLE_ROUTER_URL`, `MODEL` — the one-call entry (router brain + router-tools workers) | -| **`sandbox`** | `TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... pnpm tsx examples/supervisor-loop/run-sandbox.ts` | a real `SandboxClient` (key + base URL); optional `LOOP_HARNESS` (default `opencode`); driver defaults to router-brain, `DRIVER=scripted` for no driver inference | -| **`bridge`** (local) | `WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run-bridge.ts` | a running `~/code/cli-bridge` (base `http://127.0.0.1:3344`, no `/v1`, bearer optional/default `local`); `WORKER_MODEL` = `/`. Override `BRIDGE_URL` / `BRIDGE_BEARER` if you started it with auth. Set `TANGLE_API_KEY` + `DRIVER_MODEL` for a real driver brain (else scripted) | +| **`sandbox`** | `WORKER_BACKEND=sandbox TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... pnpm tsx examples/supervisor-loop/run.ts` | a real `SandboxClient` (key + base URL); optional `LOOP_HARNESS` (default `opencode`); driver defaults to router-brain, `DRIVER=scripted` for no driver inference | +| **`bridge`** (local) | `WORKER_BACKEND=bridge WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run.ts` | a running `~/code/cli-bridge` (base `http://127.0.0.1:3344`, no `/v1`, bearer optional/default `local`); `WORKER_MODEL` = `/`. Override `BRIDGE_URL` / `BRIDGE_BEARER` if you started it with auth. Set `TANGLE_API_KEY` + `DRIVER_MODEL` for a real driver brain (else scripted) | ### Test locally — the cli-bridge backend @@ -84,7 +88,7 @@ box. Start it, then point a worker at it: cd ~/code/cli-bridge && pnpm install && pnpm install:harness -- opencode && pnpm start # → http://127.0.0.1:3344 -WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run-bridge.ts +WORKER_BACKEND=bridge WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run.ts ``` The workflow is: **prove the supervisor topology against local harness CLIs (`bridge`), then @@ -101,7 +105,7 @@ pnpm test tests/loops/coordination-driver.test.ts tests/supervisor-loop-example. The supervisor drives through an injected `ToolLoopChat` brain (one driver-LLM turn). `supervise(..., { router })` (or `examples/supervise/supervise.ts`) uses **`routerBrain(cfg)`** so the supervisor's turns are real router tool-calls and the brain decides the loop itself. -`run-sandbox.ts`/`run-bridge.ts` default to a **scripted** brain (`scriptedSupervisorChat`, a +`run.ts` defaults to a **scripted** brain (`scriptedSupervisorChat`, a fixed `spawn → await → stop` plan) so the box/bridge wiring is the only moving part — the same offline seam the unit tests use — and opt into `routerBrain` when a key is present. Same brain, different seam. diff --git a/examples/supervisor-loop/run-bridge.ts b/examples/supervisor-loop/run-bridge.ts deleted file mode 100644 index fcf5c3e0..00000000 --- a/examples/supervisor-loop/run-bridge.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * The local-worker path — WORKERS run through the OpenAI-compatible bridge in - * ~/code/cli-bridge, so the heavy compute (each worker is a real local harness CLI: - * opencode / claude-code / codex / kimi / gemini) runs on your machine + subscription, - * zero cloud sandbox. The worker leaf is a RESUMABLE cli-bridge session — structurally - * identical to the sandbox executor's persistent box, just local. - * - * The supervisor is the canonical one-call `supervise()`; this runner supplies only the - * load-bearing cli-bridge seam (`backend: 'bridge'`) and the scripted-brain $0/offline story. - * - * ── The supervisor BRAIN is a separate dial ────────────────────────────────────────── - * The brain must emit `spawn_agent` via OpenAI tool-calling. cli-bridge fronts FULL agents - * (opencode etc.) that do their own internal tool-use and do NOT return raw `tool_calls`, so - * the brain CANNOT run through cli-bridge here. Two real options: - * • router — set TANGLE_API_KEY (+ DRIVER_MODEL, a tool-calling model). The boss is only a - * handful of decisions, so this stays cheap; workers stay free. - * • scripted — the default: a deterministic spawn→await→stop brain (no inference, $0), which - * still drives a live Scope. Proves the worker wiring end-to-end. - * For a 100%-local boss, run opencode AS the supervisor with the coordination MCP - * (`serveCoordinationMcp`) so it calls `spawn_agent` via its own tool-use — see run-supervisor-mcp.ts. - * - * Start the bridge (defaults to port 3344; no auth unless started with BRIDGE_BEARER): - * cd ~/code/cli-bridge && pnpm install && pnpm start # → http://127.0.0.1:3344 - * - * Run it (scripted boss, free workers — the headline): - * WORKER_MODEL=opencode/zai-coding-plan/glm-5.1 pnpm tsx examples/supervisor-loop/run-bridge.ts - */ - -import { type ExecutorConfig, routerBrain, supervise } from '@tangle-network/agent-runtime/loops' -import { demoCheck, demoGoal, scriptedSupervisorChat } from './shared' - -async function main(): Promise { - const bridgeUrl = process.env.BRIDGE_URL ?? 'http://127.0.0.1:3344' - const bridgeBearer = process.env.BRIDGE_BEARER ?? 'local' - const workerModel = process.env.WORKER_MODEL - if (!workerModel) { - console.error( - 'run-bridge needs WORKER_MODEL=/ the bridge can serve,\n' + - ' e.g. WORKER_MODEL=opencode/zai-coding-plan/glm-5.1\n' + - 'Start the bridge first:\n' + - ' cd ~/code/cli-bridge && pnpm install && pnpm start (→ http://127.0.0.1:3344)\n' + - 'No bridge handy? The offline wiring is covered by:\n' + - ' pnpm test tests/loops/coordination-driver.test.ts tests/supervisor-loop-example.test.ts', - ) - process.exit(1) - } - - // The worker leaf — a RESUMABLE cli-bridge session (the local twin of the sandbox box). - const backend: ExecutorConfig = { - backend: 'bridge', - bridgeUrl, - bridgeBearer, - model: workerModel, - timeoutMs: 180_000, - } - - // The supervisor BRAIN — router (real, tool-calling) if a key is present, else scripted ($0). - // NOT cli-bridge: full-agent harnesses don't return raw tool_calls (see header). - const routerKey = process.env.TANGLE_API_KEY - const driverModel = process.env.DRIVER_MODEL - const brain = - routerKey && driverModel - ? routerBrain({ - routerBaseUrl: process.env.ROUTER_BASE_URL ?? 'https://router.tangle.tools/v1', - routerKey, - model: driverModel, - }) - : scriptedSupervisorChat(1, 'bridge-solver') - const driverLabel = routerKey && driverModel ? `router(${driverModel})` : 'scripted' - - console.log( - `supervisor-loop · BRIDGE · worker=${workerModel} (cli-bridge) · driver=${driverLabel}`, - ) - - const result = await supervise( - { - name: 'supervisor', - harness: null, - systemPrompt: - 'You are a supervisor. Spawn one worker harness session to produce the required line, ' + - 'await it with await_event, and stop once a worker delivered (valid). Do not answer yourself.', - }, - demoGoal, - { - backend, - deliverable: { check: demoCheck, describe: 'worker delivers the goal' }, - brain, - budget: { maxIterations: 100, maxTokens: 1_000_000, maxUsd: 1 }, - perWorker: { maxIterations: 1, maxTokens: 100_000 }, - maxTurns: 12, - runId: 'supervisor-loop-bridge', - }, - ) - - console.log( - result.kind === 'winner' - ? `[OK] delivered: ${JSON.stringify(result.out)}` - : `[--] no winner (${result.reason}, ${result.downCount} down)`, - ) -} - -main().catch((err) => { - console.error(err instanceof Error ? (err.stack ?? err.message) : String(err)) - process.exit(1) -}) diff --git a/examples/supervisor-loop/run-sandbox.ts b/examples/supervisor-loop/run-sandbox.ts deleted file mode 100644 index aabcfa73..00000000 --- a/examples/supervisor-loop/run-sandbox.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * SANDBOXED SUPERVISOR — a supervisor that drives workers inside real Tangle sandbox boxes. - * - * The three-line shape: - * 1. the supervisor AUTHORS a worker `AgentProfile` (its standing instructions + harness), - * 2. each worker runs `runLoop` INSIDE a real box — `createExecutor({ backend: 'sandbox', - * harness, sandboxClient })` composes the kernel as a single-task leaf in a box running - * `harness` (opencode / claude-code / codex), - * 3. the supervisor reads each box's settled output and drives the next worker until the - * deliverable check passes. - * - * TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... pnpm tsx examples/supervisor-loop/run-sandbox.ts - * - * The supervisor is the canonical one-call `supervise()`; this runner supplies only the - * load-bearing sandbox seam — a real `SandboxClient` + `backend: 'sandbox'`. The WORKER BACKEND - * is the only knob: swap `backend: 'sandbox'` for `'bridge'` and the IDENTICAL supervisor drives - * local harness CLIs instead (see run-bridge.ts). - * - * The driver brain defaults to the router (the box key is already in hand); set DRIVER=scripted - * for the offline brain. For a fully offline, no-creds wiring check, see - * tests/loops/coordination-driver.test.ts and tests/supervisor-loop-example.test.ts. - */ - -import { - type ExecutorConfig, - type SandboxClient as RuntimeSandboxClient, - routerBrain, - supervise, -} from '@tangle-network/agent-runtime/loops' -import type { BackendType } from '@tangle-network/sandbox' -import { SandboxClient } from '@tangle-network/sandbox' -import { demoCheck, demoGoal, scriptedSupervisorChat } from './shared' - -async function main(): Promise { - const apiKey = process.env.TANGLE_API_KEY - const baseUrl = process.env.SANDBOX_BASE_URL - if (!apiKey || !baseUrl) { - console.error( - 'run-sandbox needs a real sandbox client: set TANGLE_API_KEY + SANDBOX_BASE_URL.\n' + - 'No box? The bridge backend runs the SAME supervisor against local harness CLIs:\n' + - ' cd ~/code/cli-bridge && pnpm install && pnpm install:harness -- opencode && pnpm start\n' + - ' WORKER_MODEL=opencode/anthropic/claude-sonnet-4-5 pnpm tsx examples/supervisor-loop/run-bridge.ts', - ) - process.exit(1) - } - - // The real Tangle sandbox client satisfies the runtime's `SandboxClient` port (it exposes - // `create(...)`), so it drops straight into the sandbox seam. - const sandboxClient = new SandboxClient({ apiKey, baseUrl }) as unknown as RuntimeSandboxClient - const harness = (process.env.LOOP_HARNESS ?? 'opencode') as BackendType - const backend: ExecutorConfig = { backend: 'sandbox', harness, sandboxClient, maxIterations: 1 } - - // Default to the router brain (the box key is already in hand); DRIVER=scripted for $0 offline. - const useRouterDriver = process.env.DRIVER !== 'scripted' - const routerBaseUrl = process.env.ROUTER_BASE_URL ?? 'https://router.tangle.tools/v1' - const driverModel = process.env.LOOP_MODEL ?? 'deepseek-v4-flash' - const brain = useRouterDriver - ? routerBrain({ routerBaseUrl, routerKey: apiKey, model: driverModel }) - : scriptedSupervisorChat(2, 'box-solver') - - console.log( - `supervisor-loop · SANDBOX · harness=${harness} · driver=${useRouterDriver ? `router(${driverModel})` : 'scripted'}`, - ) - - const result = await supervise( - { - name: 'supervisor', - harness: null, - systemPrompt: - 'You are a supervisor. Spawn worker coding sessions in boxes, await each, and stop on delivery.', - }, - demoGoal, - { - backend, - deliverable: { check: demoCheck, describe: 'worker delivers the goal' }, - brain, - budget: { maxIterations: 100, maxTokens: 2_000_000, maxUsd: 2 }, - perWorker: { maxIterations: 1, maxTokens: 200_000 }, - runId: 'supervisor-loop-sandbox', - }, - ) - - console.log( - result.kind === 'winner' - ? `[OK] delivered: ${JSON.stringify(result.out)}` - : `[--] no winner (${result.reason}, ${result.downCount} down)`, - ) -} - -main().catch((err) => { - console.error(err instanceof Error ? (err.stack ?? err.message) : String(err)) - process.exit(1) -}) diff --git a/examples/supervisor-loop/run-supervisor-mcp.ts b/examples/supervisor-loop/run-supervisor-mcp.ts index 0d698b3e..2b34463a 100644 --- a/examples/supervisor-loop/run-supervisor-mcp.ts +++ b/examples/supervisor-loop/run-supervisor-mcp.ts @@ -34,47 +34,13 @@ import { type Agent, createExecutorRegistry, createSupervisor, - type ExecutorConfig, InMemoryResultBlobStore, InMemorySpawnJournal, type Scope, serveCoordinationMcp, workerFromBackend, } from '@tangle-network/agent-runtime/loops' -import { demoCheck, expectedAnswer } from './shared' - -/** Build the worker-leaf `ExecutorConfig` for the chosen backend. THIS is the swap seam: the - * `backend` field, plus the matching per-backend seam fields. Nothing downstream cares which - * one it is — `workerFromBackend` injects the seam and returns a uniform spawnable worker. */ -function workerBackend(): ExecutorConfig { - const backend = process.env.WORKER_BACKEND ?? 'bridge' - if (backend === 'sandbox') { - throw new Error( - 'WORKER_BACKEND=sandbox needs a real SandboxClient (key + base URL). Construct it from\n' + - ' @tangle-network/sandbox and return { backend: "sandbox", sandboxClient } here — the\n' + - ' supervisor, MCP, spawn_agent, and the deployable check are identical to the bridge path.\n' + - 'Run the proven path: WORKER_BACKEND=bridge WORKER_MODEL=opencode/zai-coding-plan/glm-5.1', - ) - } - if (backend !== 'bridge') { - throw new Error(`WORKER_BACKEND must be "bridge" or "sandbox" (got ${JSON.stringify(backend)})`) - } - const model = process.env.WORKER_MODEL - if (!model) { - throw new Error( - 'WORKER_BACKEND=bridge needs WORKER_MODEL=/ the bridge can serve,\n' + - ' e.g. WORKER_MODEL=opencode/zai-coding-plan/glm-5.1\n' + - 'Start the bridge first: cd ~/code/cli-bridge && pnpm start (→ http://127.0.0.1:3344)', - ) - } - return { - backend: 'bridge', - bridgeUrl: process.env.BRIDGE_URL ?? 'http://127.0.0.1:3344', - bridgeBearer: process.env.BRIDGE_BEARER ?? 'local', - model, - timeoutMs: 180_000, - } -} +import { buildWorkerBackend, demoCheck, expectedAnswer } from './shared' /** The supervisor's standing instructions — it delegates, it does not solve. */ const supervisorTask = @@ -109,7 +75,7 @@ async function supervisorBridgeChat(opts: { mcpUrl: string }): Promise { } async function main(): Promise { - const backend = workerBackend() + const backend = buildWorkerBackend() const blobs = new InMemoryResultBlobStore() console.log( diff --git a/examples/supervisor-loop/run.ts b/examples/supervisor-loop/run.ts new file mode 100644 index 00000000..d9c15fbc --- /dev/null +++ b/examples/supervisor-loop/run.ts @@ -0,0 +1,64 @@ +/** + * supervisor-loop — the canonical one-call `supervise()` over a real worker backend, where the WORKER + * BACKEND is the ONLY knob. The SAME supervisor + brain + deployable check drive workers either through + * the local cli-bridge (real harness CLIs on your machine) or inside real Tangle sandbox boxes; flipping + * `WORKER_BACKEND` is the whole difference. + * + * The supervisor brain must emit `spawn_agent`/`await_event` via OpenAI tool-calling, so it runs on the + * router (real, tool-calling) when a key is present, else the scripted $0/offline brain — NOT cli-bridge + * (full-agent harnesses don't return raw tool_calls). All of that is in `shared.ts`. + * + * Run — local workers, free scripted brain (the headline, no cloud): + * WORKER_BACKEND=bridge WORKER_MODEL=opencode/zai-coding-plan/glm-5.1 \ + * pnpm tsx examples/supervisor-loop/run.ts + * (start the bridge first: cd ~/code/cli-bridge && pnpm install && pnpm start → http://127.0.0.1:3344) + * + * Run — sandbox workers (SAME code, one env flip): + * WORKER_BACKEND=sandbox TANGLE_API_KEY=sk-... SANDBOX_BASE_URL=https://... \ + * pnpm tsx examples/supervisor-loop/run.ts + * + * For the coordination-MCP variant (a supervisor driving workers via `spawn_agent` over a served MCP), + * see run-supervisor-mcp.ts. For a fully offline, no-creds wiring check: + * pnpm test tests/loops/coordination-driver.test.ts tests/supervisor-loop-example.test.ts + */ +import { supervise } from '@tangle-network/agent-runtime/loops' +import { buildWorkerBackend, demoCheck, demoGoal, resolveSupervisorBrain } from './shared' + +async function main(): Promise { + // THE ONE KNOB — bridge (local CLIs) or sandbox (real boxes). Everything below is identical. + const backend = buildWorkerBackend() + const { brain, label } = resolveSupervisorBrain(1, `${backend.backend}-solver`) + + console.log(`supervisor-loop · ${backend.backend.toUpperCase()} · driver=${label}`) + + const result = await supervise( + { + name: 'supervisor', + harness: null, + systemPrompt: + 'You are a supervisor. Spawn one worker session to produce the required line, await it with ' + + 'await_event, and stop once a worker delivered (valid). Do not answer yourself.', + }, + demoGoal, + { + backend, + deliverable: { check: demoCheck, describe: 'worker delivers the goal' }, + brain, + budget: { maxIterations: 100, maxTokens: 2_000_000, maxUsd: 2 }, + perWorker: { maxIterations: 1, maxTokens: 200_000 }, + maxTurns: 12, + runId: `supervisor-loop-${backend.backend}`, + }, + ) + + console.log( + result.kind === 'winner' + ? `[OK] delivered: ${JSON.stringify(result.out)}` + : `[--] no winner (${result.reason}, ${result.downCount} down)`, + ) +} + +main().catch((err) => { + console.error(err instanceof Error ? (err.stack ?? err.message) : String(err)) + process.exit(1) +}) diff --git a/examples/supervisor-loop/shared.ts b/examples/supervisor-loop/shared.ts index 6b156fa2..fa16b9d8 100644 --- a/examples/supervisor-loop/shared.ts +++ b/examples/supervisor-loop/shared.ts @@ -7,7 +7,14 @@ * these are only the per-example task + the offline brain it can be driven with. */ -import type { ToolLoopChat } from '@tangle-network/agent-runtime/loops' +import { + type ExecutorConfig, + type SandboxClient as RuntimeSandboxClient, + routerBrain, + type ToolLoopChat, +} from '@tangle-network/agent-runtime/loops' +import type { BackendType } from '@tangle-network/sandbox' +import { SandboxClient } from '@tangle-network/sandbox' /** The marker every runner asks its workers to emit; the check confirms it landed. */ export const expectedAnswer = 'ANSWER=42' @@ -85,3 +92,67 @@ export function scriptedSupervisorChat(workerCount: number, labelPrefix = 'solve }) } } + +/** + * Build the worker-leaf `ExecutorConfig` for the chosen backend — THE one swap seam the supervisor-loop + * thesis is about. `WORKER_BACKEND=bridge` (default) runs workers through the local cli-bridge (real + * harness CLIs on your machine, no cloud); `WORKER_BACKEND=sandbox` runs them in real Tangle boxes. + * Nothing downstream cares which — `workerFromBackend` injects the seam and returns a uniform worker. + */ +export function buildWorkerBackend(): ExecutorConfig { + const backend = process.env.WORKER_BACKEND ?? 'bridge' + if (backend === 'sandbox') { + const apiKey = process.env.TANGLE_API_KEY + const baseUrl = process.env.SANDBOX_BASE_URL + if (!apiKey || !baseUrl) { + throw new Error( + 'WORKER_BACKEND=sandbox needs a real sandbox client: set TANGLE_API_KEY + SANDBOX_BASE_URL.\n' + + ' No box? WORKER_BACKEND=bridge WORKER_MODEL=opencode/zai-coding-plan/glm-5.1 runs the SAME\n' + + ' supervisor against local harness CLIs (start the bridge: cd ~/code/cli-bridge && pnpm start).', + ) + } + // The real Tangle client satisfies the runtime's `SandboxClient` port (it exposes `create(...)`). + const sandboxClient = new SandboxClient({ apiKey, baseUrl }) as unknown as RuntimeSandboxClient + const harness = (process.env.LOOP_HARNESS ?? 'opencode') as BackendType + return { backend: 'sandbox', harness, sandboxClient, maxIterations: 1 } + } + if (backend !== 'bridge') { + throw new Error(`WORKER_BACKEND must be "bridge" or "sandbox" (got ${JSON.stringify(backend)})`) + } + const model = process.env.WORKER_MODEL + if (!model) { + throw new Error( + 'WORKER_BACKEND=bridge needs WORKER_MODEL=/ the bridge can serve,\n' + + ' e.g. WORKER_MODEL=opencode/zai-coding-plan/glm-5.1\n' + + ' Start the bridge first: cd ~/code/cli-bridge && pnpm start (→ http://127.0.0.1:3344)', + ) + } + return { + backend: 'bridge', + bridgeUrl: process.env.BRIDGE_URL ?? 'http://127.0.0.1:3344', + bridgeBearer: process.env.BRIDGE_BEARER ?? 'local', + model, + timeoutMs: 180_000, + } +} + +/** The supervisor BRAIN: the real router driver when a key + model are present, else the scripted $0 + * offline brain. (The brain is NEVER cli-bridge — full-agent harnesses don't return raw tool_calls.) */ +export function resolveSupervisorBrain( + workerCount: number, + labelPrefix: string, +): { brain: ToolLoopChat; label: string } { + const routerKey = process.env.TANGLE_API_KEY + const driverModel = process.env.DRIVER_MODEL ?? process.env.LOOP_MODEL + if (process.env.DRIVER !== 'scripted' && routerKey && driverModel) { + return { + brain: routerBrain({ + routerBaseUrl: process.env.ROUTER_BASE_URL ?? 'https://router.tangle.tools/v1', + routerKey, + model: driverModel, + }), + label: `router(${driverModel})`, + } + } + return { brain: scriptedSupervisorChat(workerCount, labelPrefix), label: 'scripted' } +}