diff --git a/examples/think-compare-runtimes/.dev.vars.example b/examples/think-compare-runtimes/.dev.vars.example
new file mode 100644
index 00000000..edd885ed
--- /dev/null
+++ b/examples/think-compare-runtimes/.dev.vars.example
@@ -0,0 +1,7 @@
+# Copy to .dev.vars for local development.
+#
+# wrangler dev / vite dev under colima or Docker Desktop does not
+# expose /dev/fuse to Workers Containers. This tells the Workspace
+# container to run wsd with its userspace shim locally. Production
+# leaves this unset so wsd uses real kernel FUSE when available.
+FUSE_MOUNT=shim
diff --git a/examples/think-compare-runtimes/.gitignore b/examples/think-compare-runtimes/.gitignore
new file mode 100644
index 00000000..ae2b0309
--- /dev/null
+++ b/examples/think-compare-runtimes/.gitignore
@@ -0,0 +1,31 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# wrangler files
+.wrangler
+.dev.vars*
+!.dev.vars.example
+.env*
+!.env.example
diff --git a/examples/think-compare-runtimes/Dockerfile.sandbox b/examples/think-compare-runtimes/Dockerfile.sandbox
new file mode 100644
index 00000000..f51abff9
--- /dev/null
+++ b/examples/think-compare-runtimes/Dockerfile.sandbox
@@ -0,0 +1,6 @@
+# Sandbox runtime image for the comparison example. This extends
+# the Sandbox SDK base image used by getSandbox(env.Sandbox, ...).
+
+FROM docker.io/cloudflare/sandbox:0.11.0
+
+EXPOSE 8080
diff --git a/examples/think-compare-runtimes/Dockerfile.workspace b/examples/think-compare-runtimes/Dockerfile.workspace
new file mode 100644
index 00000000..e8669e10
--- /dev/null
+++ b/examples/think-compare-runtimes/Dockerfile.workspace
@@ -0,0 +1,30 @@
+# Workspace runtime image for the comparison example. It runs the
+# wsd daemon so shell commands can see the same /workspace tree that
+# Workspace.fs stores in Durable Object storage.
+#
+# Base image is node:22-trixie-slim so the agent has node + npm
+# available out of the box for in-container verification (e.g. npm
+# install / npm test). It's also Debian-glibc, which is what the wsd
+# SEA binary is built against.
+#
+# FUSE_MOUNT=auto lets wsd use real kernel FUSE when available and
+# fall back to the userspace shim in local development environments
+# that do not expose /dev/fuse.
+
+FROM ghcr.io/cloudflare/workspace-wsd-linux-x64:0.0.0-alpha.7 AS wsd
+
+FROM --platform=linux/amd64 node:22-trixie-slim
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ca-certificates git fuse3 libfuse2t64 \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=wsd /usr/local/bin/wsd /usr/local/bin/wsd
+
+ENV PORT=8080
+ENV MOUNT_POINT=/workspace
+ENV FUSE_MOUNT=auto
+EXPOSE 8080
+
+ENTRYPOINT ["/usr/local/bin/wsd"]
diff --git a/examples/think-compare-runtimes/index.html b/examples/think-compare-runtimes/index.html
new file mode 100644
index 00000000..e6f7f640
--- /dev/null
+++ b/examples/think-compare-runtimes/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Think Runtime Comparison
+
+
+
+
+
+
diff --git a/examples/think-compare-runtimes/package.json b/examples/think-compare-runtimes/package.json
new file mode 100644
index 00000000..9708e7e2
--- /dev/null
+++ b/examples/think-compare-runtimes/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@cloudflare/example-think-compare-runtimes",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "build:wsd": "npm run build:docker --workspace @cloudflare/workspace-wsd",
+ "predev": "npm run build:wsd",
+ "predeploy": "npm run build:wsd",
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "test": "vitest run",
+ "typecheck": "tsc -b --pretty false",
+ "preview": "npm run build && vite preview",
+ "deploy": "npm run build && wrangler deploy",
+ "cf-typegen": "wrangler types"
+ },
+ "dependencies": {
+ "@cloudflare/kumo": "^2.4.1",
+ "@cloudflare/sandbox": "^0.11.0",
+ "@cloudflare/think": "^0.8.2",
+ "@cloudflare/workspace": "*",
+ "@cloudflare/workspace-rpc": "*",
+ "@phosphor-icons/react": "^2.1.10",
+ "@shikijs/core": "^4.2.0",
+ "@shikijs/engine-javascript": "^4.2.0",
+ "@shikijs/langs": "^4.2.0",
+ "@shikijs/themes": "^4.2.0",
+ "agents": "^0.14.1",
+ "ai": "^6.0.196",
+ "partyserver": "^0.5.6",
+ "partysocket": "^1.1.19",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-markdown": "^10.1.0",
+ "remark-gfm": "^4.0.1",
+ "shiki": "^4.2.0",
+ "workers-ai-provider": "^3.1.14",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "^1.39.2",
+ "@tailwindcss/vite": "^4.3.0",
+ "@testing-library/react": "^16.3.2",
+ "@types/node": "^25.9.1",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "jsdom": "^29.1.1",
+ "tailwindcss": "^4.3.0",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.14",
+ "vitest": "^4.1.7",
+ "wrangler": "^4.95.0"
+ }
+}
diff --git a/examples/think-compare-runtimes/shared/events.ts b/examples/think-compare-runtimes/shared/events.ts
new file mode 100644
index 00000000..20a531b9
--- /dev/null
+++ b/examples/think-compare-runtimes/shared/events.ts
@@ -0,0 +1,35 @@
+export type RuntimeId = "workspace" | "sandbox";
+export type EventRuntime = RuntimeId | "both";
+export type ExecutionTarget = "worker-shell" | "workspace-container" | "sandbox-container";
+
+export type RunEventKind =
+ | "run_started"
+ | "run_completed"
+ | "runtime_started"
+ | "runtime_completed"
+ | "runtime_failed"
+ | "runtime_note"
+ | "container_acquired"
+ | "container_released"
+ | "container_release_scheduled"
+ | "agent_message"
+ | "agent_message_delta"
+ | "agent_thinking_delta"
+ | "agent_step"
+ | "agent_tool_call"
+ | "agent_tool_result"
+ | "agent_tool_error"
+ | "tool_call"
+ | "tool_result"
+ | "tool_error";
+
+export interface RunEvent {
+ id: string;
+ runId: string;
+ sequence: number;
+ runtime: EventRuntime;
+ kind: RunEventKind;
+ title: string;
+ detail: string;
+ timestamp: string;
+}
diff --git a/examples/think-compare-runtimes/shared/fixture.test.ts b/examples/think-compare-runtimes/shared/fixture.test.ts
new file mode 100644
index 00000000..d855ea00
--- /dev/null
+++ b/examples/think-compare-runtimes/shared/fixture.test.ts
@@ -0,0 +1,191 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "./fixture";
+
+describe("comparisonFixture", () => {
+ test("defines a docs feature task for both runtimes", () => {
+ expect(comparisonFixture.root).toBe("/workspace/repo");
+ expect(comparisonFixture.files.map((file) => file.path)).toEqual([
+ "package.json",
+ "README.md",
+ "style-guide.md",
+ "docs-nav.json",
+ "feature-briefs/smart-request-policies.md",
+ "docs/workers/index.md",
+ "docs/workers/routing.md",
+ "docs/workers/security.md",
+ "docs/workers/examples/authenticated-api.md",
+ "docs/workers/examples/rate-limit.md",
+ "docs/_partials/beta-note.md",
+ "scripts/check-docs.mjs",
+ ]);
+ expect(comparisonFixture.task).toContain("Add documentation for Smart Request Policies");
+ expect(comparisonFixture.task).toContain("create a new Workers docs page");
+ expect(comparisonFixture.task).toContain("update the docs navigation");
+ });
+
+ test("provides source material for a file-first docs workflow", () => {
+ const brief = fileContents("feature-briefs/smart-request-policies.md");
+ const styleGuide = fileContents("style-guide.md");
+ const nav = fileContents("docs-nav.json");
+ const checker = fileContents("scripts/check-docs.mjs");
+
+ expect(brief).toContain("Smart Request Policies");
+ expect(brief).toContain("Enterprise report exports");
+ expect(styleGuide).toContain("Frontmatter");
+ expect(styleGuide).toContain("Workers docs style");
+ expect(nav).toContain("Workers");
+ expect(checker).toContain("docs/workers/smart-request-policies.md");
+ expect(checker).toContain("docs-nav.json");
+ });
+
+ test("reports all docs validation failures as repair instructions", () => {
+ const files = createFixtureFiles();
+ files.set(
+ `${comparisonFixture.root}/docs/workers/smart-request-policies.md`,
+ [
+ "---",
+ "title: Smart Request Policies",
+ "description: Configure Smart Request Policies.",
+ "lastUpdated: 2026-06-05",
+ "---",
+ "",
+ "# Smart Request Policies",
+ "",
+ "This draft intentionally misses several validation requirements.",
+ "",
+ "~~~ts",
+ "export default { async fetch() { return new Response('ok'); } };",
+ "~~~",
+ "",
+ ].join("\n"),
+ );
+
+ const result = runDocsCheck(files);
+
+ expect(result.exitCode).toBe(1);
+ expect(result.output).toContain("docs validation failed:");
+ expect(result.output).toContain('exact header name "x-bypass-token"');
+ expect(result.output).toContain('exact phrase "Enterprise report exports"');
+ expect(result.output).toContain('path "/workers/smart-request-policies/"');
+ expect(result.output).toContain('README.md must include "smart-request-policies"');
+ });
+
+ test("accepts docs that satisfy the validation contract", () => {
+ const files = createFixtureFiles();
+ files.set(
+ `${comparisonFixture.root}/docs/workers/smart-request-policies.md`,
+ [
+ "---",
+ "title: Smart Request Policies",
+ "description: Configure Smart Request Policies for Workers requests.",
+ "lastUpdated: 2026-06-05",
+ "---",
+ "",
+ "# Smart Request Policies",
+ "",
+ "Smart Request Policies evaluate method, path, header, and risk-signal rules before a Worker handler runs.",
+ "",
+ "Enterprise report exports can use a route-specific bypass token for scheduled jobs.",
+ "The Worker can check the `x-bypass-token` header before allowing sensitive export routes.",
+ "",
+ "~~~ts",
+ "export default { async fetch(request: Request, env: Env): Promise {",
+ " const url = new URL(request.url);",
+ " if (url.pathname === '/reports/export' && request.method !== 'GET') {",
+ " if (request.headers.get('x-bypass-token') !== env.EXPORT_BYPASS_TOKEN) {",
+ " return new Response('Policy denied', { status: 403 });",
+ " }",
+ " }",
+ " return fetch(request);",
+ "} };",
+ "~~~",
+ "",
+ ].join("\n"),
+ );
+ files.set(
+ `${comparisonFixture.root}/docs-nav.json`,
+ JSON.stringify(
+ {
+ sections: [
+ {
+ title: "Workers",
+ items: [
+ { title: "Smart Request Policies", path: "/workers/smart-request-policies/" },
+ ],
+ },
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+ files.set(
+ `${comparisonFixture.root}/README.md`,
+ "See docs/workers/smart-request-policies.md for Smart Request Policies.\n",
+ );
+
+ const result = runDocsCheck(files);
+
+ expect(result).toEqual({ exitCode: 0, output: "docs check passed" });
+ });
+});
+
+function createFixtureFiles(): Map {
+ return new Map(
+ comparisonFixture.files.map((file) => [
+ `${comparisonFixture.root}/${file.path}`,
+ file.contents,
+ ]),
+ );
+}
+
+function runDocsCheck(files: Map): { exitCode: number; output: string } {
+ const output: string[] = [];
+ const script = fileContents("scripts/check-docs.mjs").replace(
+ 'import { readFileSync } from "node:fs";',
+ "",
+ );
+ const readFileSync = (path: string): string => {
+ const contents = files.get(`${comparisonFixture.root}/${path}`);
+ if (contents === undefined) throw new Error(`ENOENT: ${path}`);
+ return contents;
+ };
+ const consoleLike = {
+ error: (message: string) => output.push(message),
+ log: (message: string) => output.push(message),
+ };
+ const processLike = {
+ exit(code: number) {
+ throw new DocsCheckExit(code);
+ },
+ };
+
+ try {
+ new Function("readFileSync", "console", "process", script)(
+ readFileSync,
+ consoleLike,
+ processLike,
+ );
+ return { exitCode: 0, output: output.join("\n") };
+ } catch (error) {
+ if (error instanceof DocsCheckExit) {
+ return { exitCode: error.code, output: output.join("\n") };
+ }
+ throw error;
+ }
+}
+
+class DocsCheckExit extends Error {
+ readonly code: number;
+
+ constructor(code: number) {
+ super(`docs check exited with ${code}`);
+ this.code = code;
+ }
+}
+
+function fileContents(path: string): string {
+ const file = comparisonFixture.files.find((candidate) => candidate.path === path);
+ expect(file, `missing fixture file ${path}`).toBeTruthy();
+ return file?.contents ?? "";
+}
diff --git a/examples/think-compare-runtimes/shared/fixture.ts b/examples/think-compare-runtimes/shared/fixture.ts
new file mode 100644
index 00000000..6c3785d4
--- /dev/null
+++ b/examples/think-compare-runtimes/shared/fixture.ts
@@ -0,0 +1,166 @@
+export interface FixtureFile {
+ path: string;
+ contents: string;
+}
+
+export interface ComparisonFixture {
+ root: string;
+ task: string;
+ files: FixtureFile[];
+}
+
+export const comparisonFixture: ComparisonFixture = {
+ root: "/workspace/repo",
+ task: [
+ "Add documentation for Smart Request Policies.",
+ "Use the feature brief, style guide, existing Workers docs, and examples to create a new Workers docs page, update the docs navigation, add a Worker example, and update the README.",
+ "Write the docs changes first; when the content is complete, run the available docs validation command and summarize what changed and how you verified it.",
+ ].join(" "),
+ files: [
+ {
+ path: "package.json",
+ contents: `${JSON.stringify(
+ {
+ scripts: {
+ check: "node scripts/check-docs.mjs",
+ },
+ dependencies: {},
+ devDependencies: {},
+ },
+ null,
+ 2,
+ )}\n`,
+ },
+ {
+ path: "README.md",
+ contents: `# Workers docs fixture\n\nThis repository is a small stand-in for Cloudflare Workers documentation. It contains existing docs pages, examples, navigation metadata, and feature briefs that should be used together when adding a new feature page.\n\n## Current sections\n\n- Workers overview\n- Routing\n- Security\n- Examples\n\nRun \`npm run check\` after documentation changes are complete to verify the new page, navigation entry, and example requirements.\n`,
+ },
+ {
+ path: "style-guide.md",
+ contents: `# Workers docs style guide\n\n## Voice\n\nWrite in a direct, helpful style for developers building on Cloudflare Workers. Prefer concrete examples over abstract platform language.\n\n## Frontmatter\n\nEvery docs page must begin with YAML frontmatter containing \`title\`, \`description\`, and \`lastUpdated\`. Use an ISO date for \`lastUpdated\`.\n\n## Workers docs style\n\n- Start with a short explanation of what the feature does.\n- Include a small Worker example when the feature affects request handling.\n- Mention beta limitations in a clearly labeled section.\n- Link to related Workers docs using relative links.\n- Avoid marketing claims such as "best", "magic", or "instant".\n`,
+ },
+ {
+ path: "docs-nav.json",
+ contents: `${JSON.stringify(
+ {
+ sections: [
+ {
+ title: "Workers",
+ items: [
+ { title: "Overview", path: "/workers/" },
+ { title: "Routing", path: "/workers/routing/" },
+ { title: "Security", path: "/workers/security/" },
+ ],
+ },
+ {
+ title: "Examples",
+ items: [
+ {
+ title: "Authenticated API",
+ path: "/workers/examples/authenticated-api/",
+ },
+ { title: "Rate limit", path: "/workers/examples/rate-limit/" },
+ ],
+ },
+ ],
+ },
+ null,
+ 2,
+ )}\n`,
+ },
+ {
+ path: "feature-briefs/smart-request-policies.md",
+ contents: `# Smart Request Policies\n\nSmart Request Policies let Workers evaluate incoming requests against declarative method, path, header, and risk-signal rules before application handlers run. The feature is in beta for Enterprise customers.\n\n## Audience\n\nDevelopers who maintain API Workers, internal tooling, and report export endpoints.\n\n## Core behavior\n\n- Policies run at the start of a Worker request.\n- Safe methods such as \`GET\` and \`HEAD\` can be allowed without a bypass token.\n- Mutating methods such as \`POST\`, \`PUT\`, \`PATCH\`, and \`DELETE\` should require an explicit bypass token when the route handles sensitive exports.\n- Enterprise report exports may use a route-specific bypass token for scheduled jobs.\n- Denied requests should return a short reason string suitable for logs.\n\n## Docs requirements\n\n- Create \`docs/workers/smart-request-policies.md\`.\n- Add the new page to the Workers section in \`docs-nav.json\`.\n- Include a Worker example that checks method, pathname, and an \`x-bypass-token\` header.\n- Mention that beta policies do not replace application authorization.\n- Update the repository README so maintainers can find the new page.\n\n## Related topics\n\n- Routing rules are documented in \`docs/workers/routing.md\`.\n- Security recommendations are documented in \`docs/workers/security.md\`.\n`,
+ },
+ {
+ path: "docs/workers/index.md",
+ contents: `---\ntitle: Workers overview\ndescription: Build serverless applications on Cloudflare's global network.\nlastUpdated: 2026-05-20\n---\n\n# Workers overview\n\nCloudflare Workers run JavaScript and TypeScript close to users. Workers can inspect requests, route traffic, call storage services, and generate responses without managing servers.\n\n## Common tasks\n\n- Route requests to different origins.\n- Protect APIs with request checks.\n- Transform responses at the edge.\n- Connect to Cloudflare storage and AI services.\n\nFor request routing details, see [Routing](./routing.md). For security patterns, see [Security](./security.md).\n`,
+ },
+ {
+ path: "docs/workers/routing.md",
+ contents: `---\ntitle: Workers routing\ndescription: Route requests in Workers using URL and method checks.\nlastUpdated: 2026-05-21\n---\n\n# Workers routing\n\nWorkers receive a \`Request\` object and can branch on method, pathname, headers, and other request metadata.\n\n~~~ts\nexport default {\n async fetch(request: Request): Promise {\n const url = new URL(request.url);\n\n if (request.method === "GET" && url.pathname === "/health") {\n return Response.json({ ok: true });\n }\n\n return new Response("Not found", { status: 404 });\n },\n};\n~~~\n\nKeep routing checks close to the code that handles the matching request.\n`,
+ },
+ {
+ path: "docs/workers/security.md",
+ contents: `---\ntitle: Workers security\ndescription: Apply request validation and authorization checks in Workers.\nlastUpdated: 2026-05-22\n---\n\n# Workers security\n\nWorkers can enforce lightweight request checks before calling application code. Use these checks together with application authorization and origin-side controls.\n\n## Recommendations\n\n- Validate methods before handling mutating routes.\n- Treat headers as untrusted input unless they are set by trusted infrastructure.\n- Return short denial reasons for logs without exposing sensitive policy details to callers.\n- Keep security checks explicit and easy to review.\n`,
+ },
+ {
+ path: "docs/workers/examples/authenticated-api.md",
+ contents: `---\ntitle: Authenticated API example\ndescription: Check an authorization header before proxying an API request.\nlastUpdated: 2026-05-23\n---\n\n# Authenticated API example\n\nThis Worker checks a bearer token before forwarding traffic to an API origin.\n\n~~~ts\nexport default {\n async fetch(request: Request, env: Env): Promise {\n const token = request.headers.get("authorization");\n\n if (token !== "Bearer " + env.API_TOKEN) {\n return new Response("Unauthorized", { status: 401 });\n }\n\n return fetch(request);\n },\n};\n~~~\n\nUse application-specific authorization for user and tenant decisions.\n`,
+ },
+ {
+ path: "docs/workers/examples/rate-limit.md",
+ contents: `---\ntitle: Rate limit example\ndescription: Apply a simple path-specific request limit in a Worker.\nlastUpdated: 2026-05-24\n---\n\n# Rate limit example\n\nThis example shows where request protection logic can run before application handlers.\n\n~~~ts\nexport default {\n async fetch(request: Request): Promise {\n const url = new URL(request.url);\n\n if (url.pathname.startsWith("/api/") && request.method !== "GET") {\n return new Response("Limited", { status: 429 });\n }\n\n return new Response("OK");\n },\n};\n~~~\n`,
+ },
+ {
+ path: "docs/_partials/beta-note.md",
+ contents: `> Beta features can change before general availability. Test policies in a staging environment before using them for production request handling.\n`,
+ },
+ {
+ path: "scripts/check-docs.mjs",
+ contents: `import { readFileSync } from "node:fs";
+
+const failures = [];
+
+function readRequired(path, purpose) {
+ try {
+ return readFileSync(path, "utf8");
+ } catch (error) {
+ failures.push(path + " is required for " + purpose + ". Create or repair this file, then rerun npm run check.");
+ return "";
+ }
+}
+
+function parseJson(path, source) {
+ try {
+ return JSON.parse(source);
+ } catch (error) {
+ failures.push(path + " must contain valid JSON. Repair the JSON syntax, then rerun npm run check.");
+ return {};
+ }
+}
+
+function assert(condition, message) {
+ if (!condition) {
+ failures.push(message);
+ }
+}
+
+const pagePath = "docs/workers/smart-request-policies.md";
+const page = readRequired(pagePath, "the Smart Request Policies docs page");
+const nav = parseJson("docs-nav.json", readRequired("docs-nav.json", "the Workers navigation entry"));
+const readme = readRequired("README.md", "the maintainer-facing page link");
+
+assert(page.startsWith("---\\n"), pagePath + " must start with YAML frontmatter containing title, description, and lastUpdated.");
+assert(page.includes("title:"), pagePath + " frontmatter must include title.");
+assert(page.includes("description:"), pagePath + " frontmatter must include description.");
+assert(page.includes("lastUpdated:"), pagePath + " frontmatter must include lastUpdated.");
+assert(page.includes("Smart Request Policies"), pagePath + " must describe Smart Request Policies by name.");
+assert(page.includes("x-bypass-token"), pagePath + ' must include the exact header name "x-bypass-token". Add it to the Worker example or policy explanation.');
+assert(page.includes("Enterprise report exports"), pagePath + ' must include the exact phrase "Enterprise report exports". Explain how scheduled Enterprise report exports can use a route-specific bypass token.');
+assert(page.includes("~~~ts") || page.includes("\`\`\`ts"), pagePath + ' must include a fenced TypeScript Worker example using ~~~ts or "three-backtick ts".');
+assert(!page.includes("TODO"), pagePath + " must not contain TODO placeholders. Replace placeholders with final docs content.");
+
+const sections = Array.isArray(nav.sections) ? nav.sections : [];
+const workers = sections.find((section) => section && section.title === "Workers");
+assert(workers, "docs-nav.json must contain the Workers section.");
+const workerItems = Array.isArray(workers?.items) ? workers.items : [];
+assert(
+ workerItems.some((item) => item && item.path === "/workers/smart-request-policies/"),
+ 'docs-nav.json must include a Workers item with path "/workers/smart-request-policies/".',
+);
+assert(
+ readme.includes("smart-request-policies"),
+ 'README.md must include "smart-request-policies" so maintainers can find the new page.',
+);
+
+if (failures.length > 0) {
+ console.error(["docs validation failed:", "", ...failures.map((failure, index) => String(index + 1) + ". " + failure)].join("\\n"));
+ process.exit(1);
+}
+
+console.log("docs check passed");
+`,
+ },
+ ],
+};
diff --git a/examples/think-compare-runtimes/src/App.test.tsx b/examples/think-compare-runtimes/src/App.test.tsx
new file mode 100644
index 00000000..27693f70
--- /dev/null
+++ b/examples/think-compare-runtimes/src/App.test.tsx
@@ -0,0 +1,368 @@
+// @vitest-environment jsdom
+
+import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { App } from "./App";
+
+vi.mock("partysocket/react", () => ({
+ usePartySocket: vi.fn(),
+}));
+
+describe("App", () => {
+ afterEach(() => {
+ cleanup();
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ });
+
+ test("renders a clean idle substrate instrument", () => {
+ vi.stubGlobal("fetch", vi.fn());
+
+ render();
+
+ expect(screen.getByText("Workspace / Sandbox")).toBeTruthy();
+ expect(screen.queryByText("Workspace vs Sandbox · same task · same model")).toBeNull();
+ expect(screen.queryByText("TASK")).toBeNull();
+ expect(screen.queryByText(/run-/)).toBeNull();
+ expect(screen.getByRole("button", { name: "START RUN" })).toBeTruthy();
+
+ const workspace = screen.getByLabelText("Workspace runtime wing");
+ const sandbox = screen.getByLabelText("Sandbox runtime wing");
+
+ expect(within(workspace).getByText("Workspace")).toBeTruthy();
+ expect(within(workspace).queryByText("VFS · dynamic worker · container escalation")).toBeNull();
+ expect(within(workspace).getAllByText("VFS").length).toBeGreaterThanOrEqual(1);
+ expect(within(workspace).getAllByText("Dynamic worker").length).toBeGreaterThanOrEqual(1);
+ expect(within(workspace).getAllByText("Container").length).toBeGreaterThanOrEqual(1);
+ expect(within(workspace).queryByText("Routing summary")).toBeNull();
+ expect(within(workspace).queryByText("Details")).toBeNull();
+ expect(within(workspace).queryByText("Check")).toBeNull();
+
+ expect(within(sandbox).getAllByText("Sandbox").length).toBeGreaterThanOrEqual(1);
+ expect(within(sandbox).queryByText("Container-native files and commands")).toBeNull();
+ });
+
+ test("starts a comparison run without exposing debug run IDs", async () => {
+ const fetchMock = vi.fn(async () =>
+ Response.json(
+ {
+ runId: "run-123",
+ socketPath: "/parties/compare-run/run-123",
+ events: [
+ event({
+ id: "run-123:0",
+ runId: "run-123",
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ title: "Comparison run started",
+ detail: "Both agents are starting.",
+ timestamp: new Date().toISOString(),
+ }),
+ ],
+ },
+ { status: 201 },
+ ),
+ );
+ vi.stubGlobal("fetch", fetchMock);
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+
+ await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/runs", { method: "POST" }));
+ expect(screen.queryByText("run-123")).toBeNull();
+ expect((screen.getByRole("button", { name: "STOP RUN" }) as HTMLButtonElement).disabled).toBe(
+ false,
+ );
+ });
+
+ test("stops and discards the visible run so a new run can start", async () => {
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ if (String(input) === "/api/runs/run-123/stop") {
+ return new Response(null, { status: 204 });
+ }
+ return Response.json(
+ {
+ runId: "run-123",
+ socketPath: "/parties/compare-run/run-123",
+ events: [
+ event({
+ id: "run-123:0",
+ runId: "run-123",
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: new Date().toISOString(),
+ }),
+ ],
+ },
+ { status: 201 },
+ );
+ });
+ vi.stubGlobal("fetch", fetchMock);
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+ const stopButton = await screen.findByRole("button", { name: "STOP RUN" });
+
+ fireEvent.click(stopButton);
+
+ await waitFor(() =>
+ expect(fetchMock).toHaveBeenCalledWith("/api/runs/run-123/stop", { method: "POST" }),
+ );
+ expect(screen.getByRole("button", { name: "START RUN" })).toBeTruthy();
+ expect(screen.queryByText(/Running ·/)).toBeNull();
+ });
+
+ test("updates running elapsed time before agents finish", async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-06-04T00:00:00.000Z"));
+ vi.stubGlobal(
+ "fetch",
+ sessionWithEvents([
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ ]),
+ );
+
+ render();
+ await act(async () => {
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+ await Promise.resolve();
+ });
+
+ act(() => {
+ vi.setSystemTime(new Date("2026-06-04T00:00:01.000Z"));
+ vi.advanceTimersByTime(1000);
+ });
+
+ expect(
+ within(screen.getByLabelText("Workspace runtime wing")).getByText(/Running · 00:02/i),
+ ).toBeTruthy();
+ });
+
+ test("renders substrate lanes and streamed thinking without raw event details", async () => {
+ vi.stubGlobal(
+ "fetch",
+ sessionWithEvents([
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/security.md" }),
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_message_delta",
+ title: "Think response stream",
+ detail: "Reading the related docs before editing.",
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ cwd: "/workspace/repo",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ stdout: "docs/workers/security.md:Smart Request Policies",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:05.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 6,
+ runtime: "sandbox",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "sandbox-container",
+ exitCode: 0,
+ stdout: "docs check passed",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:06.000Z",
+ }),
+ ]),
+ );
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+
+ const workspace = await screen.findByLabelText("Workspace runtime wing");
+ const sandbox = screen.getByLabelText("Sandbox runtime wing");
+
+ expect(within(workspace).getByLabelText("workspace substrate timeline")).toBeTruthy();
+ expect(
+ await within(workspace).findByText("Reading the related docs before editing."),
+ ).toBeTruthy();
+ const workspaceStream = within(workspace).getByLabelText("workspace agent work stream");
+ expect(workspaceStream.className).toContain("overflow-y-auto");
+ expect(within(workspace).getAllByText("Dynamic worker").length).toBeGreaterThanOrEqual(1);
+ expect(within(workspace).getByText("$ grep -R Smart docs")).toBeTruthy();
+ expect(within(workspace).getAllByText("dynamic worker").length).toBeGreaterThanOrEqual(1);
+ expect(within(workspace).getByText("exit 0")).toBeTruthy();
+ expect(within(workspace).queryByText("message")).toBeNull();
+ expect(within(workspace).queryByText("Command requested")).toBeNull();
+ expect(within(workspace).getAllByText("1").length).toBeGreaterThanOrEqual(2);
+ expect(within(workspace).queryByText("Routing summary")).toBeNull();
+ expect(within(workspace).queryByText("agent · tool · result")).toBeNull();
+
+ expect(within(sandbox).getByLabelText("sandbox substrate timeline")).toBeTruthy();
+ expect(within(sandbox).getAllByText("Sandbox").length).toBeGreaterThanOrEqual(1);
+ expect(within(sandbox).getByText("exit 0")).toBeTruthy();
+ expect(within(sandbox).queryByText("Check")).toBeNull();
+ expect(within(sandbox).queryByText("State")).toBeNull();
+ });
+
+ test("renders assistant response details as Markdown in the secondary transcript", async () => {
+ vi.stubGlobal(
+ "fetch",
+ sessionWithEvents([
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn complete",
+ detail:
+ "## Summary of Changes\n\nI modified `docs/workers/smart-request-policies.md`.\n\n- `npm run check` passed.",
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ ]),
+ );
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+
+ const workspace = await screen.findByLabelText("Workspace runtime wing");
+ expect(within(workspace).getByRole("heading", { name: "Summary of Changes" })).toBeTruthy();
+ expect(within(workspace).getByText("docs/workers/smart-request-policies.md")).toBeTruthy();
+ expect(within(workspace).getByText("npm run check")).toBeTruthy();
+ });
+
+ test("renders completed run status and capacity failure hints", async () => {
+ vi.stubGlobal(
+ "fetch",
+ sessionWithEvents([
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "runtime_completed",
+ title: "Workspace runtime completed",
+ timestamp: "2026-06-04T00:02:51.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "sandbox",
+ kind: "runtime_failed",
+ title: "Sandbox runtime failed",
+ detail: "3040: Capacity temporarily exceeded, please try again.",
+ timestamp: "2026-06-04T00:03:42.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "both",
+ kind: "run_completed",
+ title: "Comparison run complete",
+ timestamp: "2026-06-04T00:03:42.000Z",
+ }),
+ ]),
+ );
+
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "START RUN" }));
+
+ expect(
+ await within(screen.getByLabelText("Workspace runtime wing")).findByText(
+ /Completed · 02:50/i,
+ ),
+ ).toBeTruthy();
+ expect(screen.getByRole("button", { name: "RUN AGAIN" })).toBeTruthy();
+
+ const sandbox = screen.getByLabelText("Sandbox runtime wing");
+
+ expect(within(sandbox).getByText(/Failed · 03:40/i)).toBeTruthy();
+ expect(within(sandbox).getByText("Upstream model capacity; retry later.")).toBeTruthy();
+ });
+});
+
+function sessionWithEvents(events: ReturnType[]) {
+ return vi.fn(async () =>
+ Response.json(
+ {
+ runId: "run-456",
+ socketPath: "/parties/compare-run/run-456",
+ events,
+ },
+ { status: 201 },
+ ),
+ );
+}
+
+function event(overrides: Partial) {
+ return {
+ id: `run-1:${overrides.sequence ?? 0}`,
+ runId: "run-1",
+ sequence: overrides.sequence ?? 0,
+ runtime: overrides.runtime ?? "both",
+ kind: overrides.kind ?? "run_started",
+ title: overrides.title ?? "Event",
+ detail: overrides.detail ?? "Detail",
+ timestamp: overrides.timestamp ?? "1970-01-01T00:00:00.000Z",
+ ...overrides,
+ } as import("../shared/events").RunEvent;
+}
diff --git a/examples/think-compare-runtimes/src/App.tsx b/examples/think-compare-runtimes/src/App.tsx
new file mode 100644
index 00000000..b9188d73
--- /dev/null
+++ b/examples/think-compare-runtimes/src/App.tsx
@@ -0,0 +1,138 @@
+import { usePartySocket } from "partysocket/react";
+import { useEffect, useMemo, useRef, useState } from "react";
+import type { RunEvent } from "../shared/events";
+import { buildDashboardModel } from "./dashboard-model";
+import { applyRunMessage, type RunMessage } from "./run-state";
+import { RuntimeWing } from "./runtime-wing";
+import { TopBar } from "./top-bar";
+
+interface RunSessionResponse {
+ runId: string;
+ socketPath: string;
+ events: RunEvent[];
+}
+
+type StartState = "idle" | "starting" | "running" | "failed";
+
+export function App() {
+ const [runId, setRunId] = useState(null);
+ const [events, setEvents] = useState([]);
+ const [startState, setStartState] = useState("idle");
+ const [error, setError] = useState(null);
+ const [nowIso, setNowIso] = useState(() => new Date().toISOString());
+ const activeRunIdRef = useRef(null);
+
+ usePartySocket({
+ party: "compare-run",
+ room: runId ?? "idle",
+ enabled: runId !== null,
+ onMessage(message) {
+ const parsed = JSON.parse(String(message.data)) as RunMessage;
+ const activeRunId = activeRunIdRef.current;
+ if (!activeRunId || !messageBelongsToRun(parsed, activeRunId)) return;
+ setEvents((current) => applyRunMessage(current, parsed));
+ },
+ });
+
+ const dashboard = useMemo(() => buildDashboardModel(events, nowIso), [events, nowIso]);
+ const runLabel = runStatusLabel(startState, dashboard.run.status, dashboard.run.elapsedLabel);
+ const actionLabel =
+ startState === "starting"
+ ? "STARTING"
+ : startState === "running" && dashboard.run.status === "running"
+ ? "STOP RUN"
+ : runId
+ ? dashboard.run.actionLabel
+ : "START RUN";
+ const startDisabled = startState === "starting";
+
+ useEffect(() => {
+ if (dashboard.run.status !== "running" && startState !== "running") return;
+
+ setNowIso(new Date().toISOString());
+ const timer = setInterval(() => {
+ setNowIso(new Date().toISOString());
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [dashboard.run.status, startState]);
+
+ async function handleRunAction() {
+ if (startState === "running" && dashboard.run.status === "running") {
+ stopRun();
+ return;
+ }
+ await startRun();
+ }
+
+ async function startRun() {
+ activeRunIdRef.current = null;
+ setRunId(null);
+ setEvents([]);
+ setStartState("starting");
+ setError(null);
+ setNowIso(new Date().toISOString());
+
+ try {
+ const response = await fetch("/api/runs", { method: "POST" });
+
+ if (!response.ok) {
+ throw new Error(`Run request failed with ${response.status}`);
+ }
+
+ const session = (await response.json()) as RunSessionResponse;
+ activeRunIdRef.current = session.runId;
+ setRunId(session.runId);
+ setEvents(session.events);
+ setStartState("running");
+ } catch (cause) {
+ setStartState("failed");
+ setError(cause instanceof Error ? cause.message : String(cause));
+ }
+ }
+
+ function stopRun() {
+ const stoppedRunId = activeRunIdRef.current ?? runId;
+ activeRunIdRef.current = null;
+ setRunId(null);
+ setEvents([]);
+ setStartState("idle");
+ setError(null);
+ setNowIso(new Date().toISOString());
+
+ if (stoppedRunId) {
+ void fetch(`/api/runs/${encodeURIComponent(stoppedRunId)}/stop`, { method: "POST" });
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+function runStatusLabel(startState: StartState, status: string, elapsedLabel: string): string {
+ if (startState === "starting") return "STARTING";
+ if (startState === "failed" || status === "failed") return `FAILED · ${elapsedLabel}`;
+ if (status === "completed") return `DONE · ${elapsedLabel}`;
+ if (status === "running") return `RUN · ${elapsedLabel}`;
+ return "IDLE";
+}
+
+function messageBelongsToRun(message: RunMessage, runId: string): boolean {
+ if (message.type === "event") return message.event.runId === runId;
+ return message.events.every((event) => event.runId === runId);
+}
diff --git a/examples/think-compare-runtimes/src/auto-scroll-list.test.tsx b/examples/think-compare-runtimes/src/auto-scroll-list.test.tsx
new file mode 100644
index 00000000..29b2fc2c
--- /dev/null
+++ b/examples/think-compare-runtimes/src/auto-scroll-list.test.tsx
@@ -0,0 +1,62 @@
+// @vitest-environment jsdom
+
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { AutoScrollList } from "./auto-scroll-list";
+
+describe("AutoScrollList", () => {
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ });
+
+ test("scrolls to the bottom when its watch key changes", () => {
+ const scrollTo = vi.fn();
+ vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(480);
+ Object.defineProperty(HTMLElement.prototype, "scrollTo", {
+ configurable: true,
+ value: scrollTo,
+ });
+
+ const { rerender } = render(
+
+ first event
+ ,
+ );
+
+ expect(scrollTo).toHaveBeenLastCalledWith({ top: 480, behavior: "smooth" });
+
+ rerender(
+
+ first event
+ second event
+ ,
+ );
+
+ expect(scrollTo).toHaveBeenCalledTimes(2);
+ expect(scrollTo).toHaveBeenLastCalledWith({ top: 480, behavior: "smooth" });
+ });
+
+ test("keeps separate lists independently scrollable", () => {
+ const scrollTo = vi.fn();
+ vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(320);
+ Object.defineProperty(HTMLElement.prototype, "scrollTo", {
+ configurable: true,
+ value: scrollTo,
+ });
+
+ render(
+
+
+ workspace event
+
+
+ sandbox event
+
+
,
+ );
+
+ expect(scrollTo).toHaveBeenCalledTimes(2);
+ expect(scrollTo.mock.contexts[0]).not.toBe(scrollTo.mock.contexts[1]);
+ });
+});
diff --git a/examples/think-compare-runtimes/src/auto-scroll-list.tsx b/examples/think-compare-runtimes/src/auto-scroll-list.tsx
new file mode 100644
index 00000000..c279bce5
--- /dev/null
+++ b/examples/think-compare-runtimes/src/auto-scroll-list.tsx
@@ -0,0 +1,34 @@
+import { type ReactNode, useEffect, useRef } from "react";
+
+export function AutoScrollList({
+ ariaLabel,
+ children,
+ className,
+ watchKey,
+}: {
+ ariaLabel: string;
+ children: ReactNode;
+ className?: string;
+ watchKey: string | number;
+}) {
+ const listRef = useRef(null);
+
+ useEffect(() => {
+ void watchKey;
+ const list = listRef.current;
+ if (!list) return;
+
+ if (typeof list.scrollTo === "function") {
+ list.scrollTo({ top: list.scrollHeight, behavior: "smooth" });
+ return;
+ }
+
+ list.scrollTop = list.scrollHeight;
+ }, [watchKey]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/think-compare-runtimes/src/dashboard-model.test.ts b/examples/think-compare-runtimes/src/dashboard-model.test.ts
new file mode 100644
index 00000000..95b7146b
--- /dev/null
+++ b/examples/think-compare-runtimes/src/dashboard-model.test.ts
@@ -0,0 +1,226 @@
+import { describe, expect, test } from "vitest";
+import type { EventRuntime, RunEvent, RunEventKind } from "../shared/events";
+import { buildDashboardModel } from "./dashboard-model";
+
+describe("buildDashboardModel", () => {
+ test("derives idle telemetry before a run starts", () => {
+ const model = buildDashboardModel([], null);
+
+ expect(model.run.status).toBe("idle");
+ expect(model.run.actionLabel).toBe("START RUN");
+ expect(model.runtimes.workspace.container).toBe("asleep");
+ expect(model.runtimes.sandbox.container).toBe("off");
+ expect(model.runtimes.workspace.toolCalls).toBe(0);
+ expect(model.runtimes.sandbox.execCalls).toBe(0);
+ });
+
+ test("shows Sandbox cold boot while Workspace is already active", () => {
+ const model = buildDashboardModel(
+ [
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "read called",
+ detail: JSON.stringify({ name: "read", path: "/workspace/repo/src/index.ts" }),
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ ],
+ "2026-06-04T00:00:04.000Z",
+ );
+
+ expect(model.run.status).toBe("running");
+ expect(model.run.elapsedLabel).toBe("00:04");
+ expect(model.runtimes.workspace.status).toBe("running");
+ expect(model.runtimes.workspace.elapsedLabel).toBe("00:03");
+ expect(model.runtimes.workspace.toolCalls).toBe(1);
+ expect(model.runtimes.workspace.fileOps).toBe(1);
+ expect(model.runtimes.workspace.execCalls).toBe(0);
+ expect(model.runtimes.workspace.workerShellExecs).toBe(0);
+ expect(model.runtimes.workspace.containerExecs).toBe(0);
+ expect(model.runtimes.workspace.container).toBe("asleep");
+ expect(model.runtimes.sandbox.container).toBe("booting");
+ });
+
+ test("counts Workspace exec routing by execution target", () => {
+ const model = buildDashboardModel(
+ [
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "exec called",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ executionTarget: "worker-shell",
+ cwd: "/workspace/repo",
+ }),
+ timestamp: "2026-06-04T00:00:05.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "exec called",
+ detail: JSON.stringify({
+ command: "npm run check",
+ executionTarget: "workspace-container",
+ cwd: "/workspace/repo",
+ }),
+ timestamp: "2026-06-04T00:00:06.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "sandbox",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({ command: "npm test", cwd: "/workspace/repo" }),
+ timestamp: "2026-06-04T00:00:07.000Z",
+ }),
+ ],
+ "2026-06-04T00:00:08.000Z",
+ );
+
+ expect(model.runtimes.workspace.toolCalls).toBe(2);
+ expect(model.runtimes.workspace.execCalls).toBe(2);
+ expect(model.runtimes.workspace.workerShellExecs).toBe(1);
+ expect(model.runtimes.workspace.containerExecs).toBe(1);
+ expect(model.runtimes.workspace.validationStatus).toBe("passed");
+ expect(model.runtimes.workspace.container).toBe("awake");
+ expect(model.runtimes.sandbox.toolCalls).toBe(1);
+ expect(model.runtimes.sandbox.execCalls).toBe(1);
+ expect(model.runtimes.sandbox.containerExecs).toBe(1);
+ expect(model.runtimes.sandbox.container).toBe("awake");
+ });
+
+ test("uses the latest validation result", () => {
+ const model = buildDashboardModel(
+ [
+ event({
+ sequence: 0,
+ runtime: "sandbox",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ executionTarget: "sandbox-container",
+ exitCode: 1,
+ }),
+ }),
+ event({
+ sequence: 1,
+ runtime: "sandbox",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ executionTarget: "sandbox-container",
+ exitCode: 0,
+ }),
+ }),
+ ],
+ "2026-06-04T00:01:00.000Z",
+ );
+
+ expect(model.runtimes.sandbox.validationStatus).toBe("passed");
+ });
+
+ test("uses terminal timestamps for completed runs", () => {
+ const model = buildDashboardModel(
+ [
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "runtime_completed",
+ timestamp: "2026-06-04T00:02:51.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "sandbox",
+ kind: "runtime_completed",
+ timestamp: "2026-06-04T00:03:42.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "both",
+ kind: "run_completed",
+ timestamp: "2026-06-04T00:03:42.000Z",
+ }),
+ ],
+ "2026-06-04T00:10:00.000Z",
+ );
+
+ expect(model.run.status).toBe("completed");
+ expect(model.run.actionLabel).toBe("RUN AGAIN");
+ expect(model.run.elapsedLabel).toBe("03:42");
+ expect(model.runtimes.workspace.elapsedLabel).toBe("02:49");
+ expect(model.runtimes.sandbox.elapsedLabel).toBe("03:41");
+ });
+});
+
+function event(overrides: Partial & { sequence: number }): RunEvent {
+ return {
+ id: `run-1:${overrides.sequence}`,
+ runId: "run-1",
+ sequence: overrides.sequence,
+ runtime: (overrides.runtime ?? "both") as EventRuntime,
+ kind: (overrides.kind ?? "run_started") as RunEventKind,
+ title: overrides.title ?? "Event",
+ detail: overrides.detail ?? "Detail",
+ timestamp: overrides.timestamp ?? "1970-01-01T00:00:00.000Z",
+ };
+}
diff --git a/examples/think-compare-runtimes/src/dashboard-model.ts b/examples/think-compare-runtimes/src/dashboard-model.ts
new file mode 100644
index 00000000..25172760
--- /dev/null
+++ b/examples/think-compare-runtimes/src/dashboard-model.ts
@@ -0,0 +1,139 @@
+import type { RunEvent, RuntimeId } from "../shared/events";
+import { execObservationFacts, factsForRuntime, type RunEventFact } from "./run-event-facts";
+import { deriveRunSummary, type OverallRunStatus, type RuntimeRunStatus } from "./run-state";
+
+export type ContainerState = "off" | "booting" | "asleep" | "awake";
+
+export type ValidationStatus = "not-run" | "passed" | "failed";
+
+export interface RuntimeDashboardModel {
+ id: RuntimeId;
+ status: RuntimeRunStatus;
+ elapsedLabel: string;
+ toolCalls: number;
+ fileOps: number;
+ execCalls: number;
+ workerShellExecs: number;
+ containerExecs: number;
+ validationStatus: ValidationStatus;
+ container: ContainerState;
+ error: string | null;
+ events: RunEvent[];
+}
+
+export interface DashboardModel {
+ run: {
+ status: OverallRunStatus;
+ elapsedLabel: string;
+ actionLabel: "START RUN" | "RUN AGAIN";
+ };
+ runtimes: Record;
+}
+
+const runtimeIds: RuntimeId[] = ["workspace", "sandbox"];
+
+export function buildDashboardModel(events: RunEvent[], nowIso: string | null): DashboardModel {
+ const summary = deriveRunSummary(events);
+
+ return {
+ run: {
+ status: summary.status,
+ elapsedLabel: formatDuration(
+ summary.elapsedMs ?? runningElapsedMs(summary.startedAt, summary.completedAt, nowIso),
+ ),
+ actionLabel:
+ summary.status === "completed" || summary.status === "failed" ? "RUN AGAIN" : "START RUN",
+ },
+ runtimes: Object.fromEntries(
+ runtimeIds.map((runtime) => {
+ const runtimeSummary = summary.runtimes[runtime];
+ const facts = factsForRuntime(events, runtime, "runtimeOnly");
+ const execs = execObservationFacts(facts);
+ const workerShellExecs = execs.filter(
+ (fact) => fact.executionTarget === "worker-shell",
+ ).length;
+ const workspaceContainerExecs = execs.filter(
+ (fact) => fact.executionTarget === "workspace-container",
+ ).length;
+ const sandboxContainerExecs = execs.filter(
+ (fact) => fact.executionTarget === "sandbox-container",
+ ).length;
+ const containerExecs =
+ runtime === "workspace" ? workspaceContainerExecs : sandboxContainerExecs;
+
+ return [
+ runtime,
+ {
+ id: runtime,
+ status: runtimeSummary.status,
+ elapsedLabel: formatDuration(
+ runtimeSummary.elapsedMs ??
+ runningElapsedMs(runtimeSummary.startedAt, runtimeSummary.completedAt, nowIso),
+ ),
+ toolCalls: facts.filter((fact) => fact.phase === "call" && fact.tool !== null).length,
+ fileOps: facts.filter(isFileCall).length,
+ execCalls: execs.length,
+ workerShellExecs,
+ containerExecs,
+ validationStatus: validationStatus(facts),
+ container: containerState(runtime, runtimeSummary.status, facts, containerExecs),
+ error: runtimeSummary.error,
+ events: facts.map((fact) => fact.event),
+ },
+ ];
+ }),
+ ) as Record,
+ };
+}
+
+function runningElapsedMs(
+ startedAt: string | null,
+ completedAt: string | null,
+ nowIso: string | null,
+): number | null {
+ if (!startedAt || completedAt || !nowIso) return null;
+ const elapsed = Date.parse(nowIso) - Date.parse(startedAt);
+ return Number.isNaN(elapsed) ? null : Math.max(0, elapsed);
+}
+
+export function formatDuration(elapsedMs: number | null): string {
+ if (elapsedMs === null) return "--:--";
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+function isFileCall(fact: RunEventFact): boolean {
+ return (
+ fact.phase === "call" && (fact.tool === "read" || fact.tool === "write" || fact.tool === "edit")
+ );
+}
+
+function validationStatus(facts: RunEventFact[]): ValidationStatus {
+ const latestValidation = execObservationFacts(facts)
+ .filter((fact) => fact.validationCommand)
+ .at(-1);
+ if (!latestValidation) return "not-run";
+ return latestValidation.failed ? "failed" : "passed";
+}
+
+function containerState(
+ runtime: RuntimeId,
+ status: RuntimeRunStatus,
+ facts: RunEventFact[],
+ containerExecs: number,
+): ContainerState {
+ if (runtime === "workspace") {
+ return containerExecs > 0 ? "awake" : "asleep";
+ }
+
+ if (status === "idle") return "off";
+ if (
+ facts.some((fact) => fact.phase === "call" || fact.phase === "result") ||
+ containerExecs > 0
+ ) {
+ return "awake";
+ }
+ return "booting";
+}
diff --git a/examples/think-compare-runtimes/src/main.tsx b/examples/think-compare-runtimes/src/main.tsx
new file mode 100644
index 00000000..07abc654
--- /dev/null
+++ b/examples/think-compare-runtimes/src/main.tsx
@@ -0,0 +1,16 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App";
+import "./styles.css";
+
+const root = document.getElementById("root");
+
+if (!root) {
+ throw new Error("Root element not found");
+}
+
+createRoot(root).render(
+
+
+ ,
+);
diff --git a/examples/think-compare-runtimes/src/markdown-text.tsx b/examples/think-compare-runtimes/src/markdown-text.tsx
new file mode 100644
index 00000000..bee5aa26
--- /dev/null
+++ b/examples/think-compare-runtimes/src/markdown-text.tsx
@@ -0,0 +1,242 @@
+import { type ComponentProps, useEffect, useState } from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import type { HighlighterCore } from "shiki/core";
+
+export function MarkdownText({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+function Heading2(props: ComponentProps<"h2"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return (
+
+ );
+}
+
+function Heading3(props: ComponentProps<"h3"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return ;
+}
+
+function Paragraph(props: ComponentProps<"p"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return ;
+}
+
+function OrderedList(props: ComponentProps<"ol"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return (
+
+ );
+}
+
+function UnorderedList(props: ComponentProps<"ul"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return (
+
+ );
+}
+
+function ListItem(props: ComponentProps<"li"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return ;
+}
+
+function Strong(props: ComponentProps<"strong"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return ;
+}
+
+function Link(props: ComponentProps<"a"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return (
+
+ );
+}
+
+function Table(props: ComponentProps<"table"> & { node?: unknown }) {
+ const { node: _node, ...rest } = props;
+ return (
+
+ );
+}
+
+function Pre({ children }: ComponentProps<"pre"> & { node?: unknown }) {
+ return <>{children}>;
+}
+
+function Code({
+ children,
+ className,
+ node: _node,
+ ...props
+}: ComponentProps<"code"> & { node?: unknown }) {
+ const source = String(children ?? "");
+ const language = /language-([\w-]+)/.exec(className ?? "")?.[1];
+
+ if (language) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface HighlightToken {
+ content: string;
+ offset: number;
+ color?: string;
+}
+
+interface HighlightLine {
+ key: string;
+ tokens: HighlightToken[];
+}
+
+const supportedLanguages = new Set([
+ "bash",
+ "javascript",
+ "json",
+ "jsonc",
+ "js",
+ "jsx",
+ "markdown",
+ "md",
+ "sh",
+ "shell",
+ "shellscript",
+ "tsx",
+ "ts",
+ "typescript",
+]);
+
+let highlighter: Promise | null = null;
+
+function ShikiCodeBlock({ code, language }: { code: string; language: string }) {
+ const [lines, setLines] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ void getHighlighter()
+ .then((shiki) =>
+ shiki.codeToTokens(code, {
+ lang: normalizeLanguage(language),
+ theme: "github-dark-dimmed",
+ }),
+ )
+ .then((highlighted) => {
+ if (!cancelled) setLines(createHighlightLines(highlighted.tokens, code));
+ })
+ .catch(() => {
+ if (!cancelled) setLines(null);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [code, language]);
+
+ return (
+
+
+ {lines
+ ? lines.map((line) => (
+
+ {line.tokens.map((token) => (
+
+ {token.content}
+
+ ))}
+
+ ))
+ : code}
+
+
+ );
+}
+
+function getHighlighter(): Promise {
+ highlighter ??= Promise.all([
+ import("shiki/core"),
+ import("@shikijs/engine-javascript"),
+ import("@shikijs/langs/typescript"),
+ import("@shikijs/langs/javascript"),
+ import("@shikijs/langs/tsx"),
+ import("@shikijs/langs/shellscript"),
+ import("@shikijs/langs/json"),
+ import("@shikijs/langs/jsonc"),
+ import("@shikijs/langs/markdown"),
+ import("@shikijs/themes/github-dark-dimmed"),
+ ]).then(
+ ([core, engine, typescript, javascript, tsx, shellscript, json, jsonc, markdown, theme]) =>
+ core.createHighlighterCore({
+ themes: [theme.default],
+ langs: [
+ typescript.default,
+ javascript.default,
+ tsx.default,
+ shellscript.default,
+ json.default,
+ jsonc.default,
+ markdown.default,
+ ],
+ engine: engine.createJavaScriptRegexEngine(),
+ }),
+ );
+ return highlighter;
+}
+
+function normalizeLanguage(language: string): string {
+ const normalized = language.toLowerCase();
+ if (normalized === "js") return "javascript";
+ if (normalized === "ts") return "typescript";
+ if (normalized === "sh" || normalized === "shell" || normalized === "bash") return "shellscript";
+ if (normalized === "md") return "markdown";
+ return supportedLanguages.has(normalized) ? normalized : "text";
+}
+
+function createHighlightLines(tokens: HighlightToken[][], code: string): HighlightLine[] {
+ const sourceLines = code.split("\n");
+ let runningOffset = 0;
+
+ return tokens.map((line) => {
+ const key = line[0] ? `line-${line[0].offset}` : `line-${runningOffset}`;
+ const sourceLine = sourceLines.shift() ?? "";
+ runningOffset += sourceLine.length + 1;
+ return { key, tokens: line };
+ });
+}
diff --git a/examples/think-compare-runtimes/src/run-event-facts.test.ts b/examples/think-compare-runtimes/src/run-event-facts.test.ts
new file mode 100644
index 00000000..42e2998e
--- /dev/null
+++ b/examples/think-compare-runtimes/src/run-event-facts.test.ts
@@ -0,0 +1,124 @@
+import { describe, expect, test } from "vitest";
+import type { RunEvent } from "../shared/events";
+import { detailFieldsForEvent, factForEvent, factsForRuntime } from "./run-event-facts";
+
+describe("run-event-facts", () => {
+ test("normalizes tool names, phases, paths, commands, and execution targets", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ title: "Run started",
+ detail: "Starting.",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: JSON.stringify({ path: "/workspace/repo/docs-nav.json" }),
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ cwd: "/workspace/repo",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ stdout: "ok",
+ stderr: "",
+ }),
+ }),
+ event({
+ sequence: 3,
+ runtime: "sandbox",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "sandbox-container",
+ exitCode: 1,
+ stdout: "",
+ stderr: "Missing nav entry",
+ }),
+ }),
+ ];
+
+ expect(
+ factsForRuntime(events, "workspace", "runtimeOnly").map((fact) => fact.sequence),
+ ).toEqual([1, 2]);
+ expect(
+ factsForRuntime(events, "workspace", "runtimeOrShared").map((fact) => fact.sequence),
+ ).toEqual([0, 1, 2]);
+
+ const read = factForEvent(eventAt(events, 1));
+ expect(read.tool).toBe("read");
+ expect(read.phase).toBe("call");
+ expect(read.path).toBe("/workspace/repo/docs-nav.json");
+
+ const shell = factForEvent(eventAt(events, 2));
+ expect(shell.tool).toBe("exec");
+ expect(shell.phase).toBe("result");
+ expect(shell.command).toBe("grep -R Smart docs");
+ expect(shell.executionTarget).toBe("worker-shell");
+ expect(shell.exitCode).toBe(0);
+ expect(shell.validationCommand).toBe(false);
+
+ const validation = factForEvent(eventAt(events, 3));
+ expect(validation.executionTarget).toBe("sandbox-container");
+ expect(validation.validationCommand).toBe(true);
+ expect(validation.failed).toBe(true);
+ });
+
+ test("formats details through the same parsed fact", () => {
+ const fields = detailFieldsForEvent(
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ stdout: "ok\n",
+ path: "/workspace/repo/docs/index.md",
+ exitCode: 0,
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "workspace-container",
+ }),
+ }),
+ );
+
+ expect(fields).toEqual([
+ { label: "command", value: "npm run check" },
+ { label: "path", value: "/workspace/repo/docs/index.md" },
+ { label: "cwd", value: "/workspace/repo" },
+ { label: "executionTarget", value: "workspace-container" },
+ { label: "exitCode", value: "0" },
+ { label: "stdout", value: "ok\n" },
+ ]);
+ });
+});
+
+function eventAt(events: RunEvent[], index: number): RunEvent {
+ const item = events[index];
+ if (item === undefined) throw new Error(`Missing event at index ${index}`);
+ return item;
+}
+
+function event(overrides: Partial & { sequence: number }): RunEvent {
+ return {
+ id: `run-1:${overrides.sequence}`,
+ runId: "run-1",
+ sequence: overrides.sequence,
+ runtime: overrides.runtime ?? "workspace",
+ kind: overrides.kind ?? "runtime_note",
+ title: overrides.title ?? "Event",
+ detail: overrides.detail ?? "Detail",
+ timestamp: "1970-01-01T00:00:00.000Z",
+ } as RunEvent;
+}
diff --git a/examples/think-compare-runtimes/src/run-event-facts.ts b/examples/think-compare-runtimes/src/run-event-facts.ts
new file mode 100644
index 00000000..d32cd855
--- /dev/null
+++ b/examples/think-compare-runtimes/src/run-event-facts.ts
@@ -0,0 +1,207 @@
+import type { EventRuntime, ExecutionTarget, RunEvent, RuntimeId } from "../shared/events";
+
+export type RuntimeMatchPolicy = "runtimeOnly" | "runtimeOrShared";
+export type ToolName = "read" | "write" | "edit" | "exec";
+export type EventPhase = "call" | "result" | "error" | "message" | "lifecycle" | "note";
+export interface EventDetailField {
+ label: string;
+ value: string;
+}
+
+export interface RunEventFact {
+ event: RunEvent;
+ sequence: number;
+ runtime: EventRuntime;
+ phase: EventPhase;
+ tool: ToolName | null;
+ path: string | null;
+ command: string | null;
+ cwd: string | null;
+ executionTarget: ExecutionTarget | null;
+ exitCode: number | null;
+ validationCommand: boolean;
+ failed: boolean;
+ text: string | null;
+ detail: Record | null;
+}
+
+const toolNames: ToolName[] = ["read", "write", "edit", "exec"];
+const preferredDetailFields = [
+ "command",
+ "path",
+ "cwd",
+ "executionTarget",
+ "exitCode",
+ "stdout",
+ "stderr",
+ "error",
+];
+
+export function factForEvent(event: RunEvent): RunEventFact {
+ const detail = parseJsonObject(event.detail);
+ const command = stringField(detail, "command");
+ const path = stringField(detail, "path");
+ const exitCode = numberField(detail, "exitCode");
+ const phase = phaseForEvent(event);
+ const tool = toolForEvent(event, detail);
+
+ return {
+ event,
+ sequence: event.sequence,
+ runtime: event.runtime,
+ phase,
+ tool,
+ path,
+ command,
+ cwd: stringField(detail, "cwd"),
+ executionTarget: executionTargetForEvent(event, detail),
+ exitCode,
+ validationCommand: typeof command === "string" && /npm\s+run\s+check/.test(command),
+ failed: phase === "error" || (typeof exitCode === "number" && exitCode !== 0),
+ text: detail ? null : event.detail,
+ detail,
+ };
+}
+
+export function factsForRuntime(
+ events: RunEvent[],
+ runtime: RuntimeId,
+ policy: RuntimeMatchPolicy,
+): RunEventFact[] {
+ return events
+ .filter((event) => eventMatchesRuntime(event, runtime, policy))
+ .sort((left, right) => left.sequence - right.sequence)
+ .map(factForEvent);
+}
+
+export function eventMatchesRuntime(
+ event: RunEvent,
+ runtime: RuntimeId,
+ policy: RuntimeMatchPolicy,
+): boolean {
+ if (event.runtime === runtime) return true;
+ return policy === "runtimeOrShared" && event.runtime === "both";
+}
+
+export function detailFieldsForEvent(event: RunEvent): EventDetailField[] {
+ const fact = factForEvent(event);
+ if (!fact.detail) return [];
+ return orderedEntries(fact.detail).map(([label, value]) => ({
+ label,
+ value: stringifyFieldValue(value),
+ }));
+}
+
+export function execObservationFacts(facts: RunEventFact[]): RunEventFact[] {
+ const callsByCommand = new Map();
+ const results: RunEventFact[] = [];
+
+ for (const fact of facts) {
+ if (fact.tool !== "exec" || !fact.command) continue;
+ if (fact.phase === "result" || fact.phase === "error") {
+ results.push(fact);
+ } else if (fact.phase === "call") {
+ callsByCommand.set(fact.command, fact);
+ }
+ }
+
+ const resultCommands = new Set(results.map((fact) => fact.command));
+ const unpairedCalls = [...callsByCommand.values()].filter(
+ (fact) => !resultCommands.has(fact.command),
+ );
+ return [...results, ...unpairedCalls].sort((left, right) => left.sequence - right.sequence);
+}
+
+export function readableDetail(fact: RunEventFact): string {
+ if (!fact.detail) return fact.text ?? "";
+ const error = stringField(fact.detail, "error");
+ if (error) return error;
+ const message = stringField(fact.detail, "message");
+ if (message) return message;
+ if (fact.command) return fact.command;
+ if (fact.path) return trimWorkspaceRoot(fact.path);
+ return fact.event.detail;
+}
+
+export function trimWorkspaceRoot(path: string): string {
+ return path.replace(/^\/workspace\/repo\//, "");
+}
+
+function phaseForEvent(event: RunEvent): EventPhase {
+ if (event.kind === "agent_message") return "message";
+ if (event.kind.endsWith("_call")) return "call";
+ if (event.kind.endsWith("_result")) return "result";
+ if (event.kind.endsWith("_error") || event.kind === "runtime_failed") return "error";
+ if (event.kind.startsWith("run_") || event.kind.startsWith("runtime_")) return "lifecycle";
+ return "note";
+}
+
+function toolForEvent(event: RunEvent, detail: Record | null): ToolName | null {
+ const fromDetail = stringField(detail, "tool");
+ if (isToolName(fromDetail)) return fromDetail;
+ if (typeof stringField(detail, "command") === "string") return "exec";
+ const title = event.title.toLowerCase();
+ return toolNames.find((name) => title.includes(name)) ?? null;
+}
+
+function executionTargetForEvent(
+ event: RunEvent,
+ detail: Record | null,
+): ExecutionTarget | null {
+ const target = stringField(detail, "executionTarget");
+ if (isExecutionTarget(target)) return target;
+
+ const backend = stringField(detail, "backend");
+ if (event.runtime === "workspace" && backend === "shell") return "worker-shell";
+ if (event.runtime === "workspace" && backend === "container") return "workspace-container";
+ if (event.runtime === "sandbox" && toolForEvent(event, detail) === "exec") {
+ return "sandbox-container";
+ }
+ return null;
+}
+
+function parseJsonObject(detail: string): Record | null {
+ try {
+ const parsed = JSON.parse(detail) as unknown;
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : null;
+ } catch {
+ return null;
+ }
+}
+
+function orderedEntries(value: Record): [string, unknown][] {
+ const preferred = preferredDetailFields
+ .filter((field) => Object.hasOwn(value, field))
+ .map((field): [string, unknown] => [field, value[field]]);
+ const rest = Object.entries(value).filter(([field]) => !preferredDetailFields.includes(field));
+ return [...preferred, ...rest];
+}
+
+function stringField(value: Record | null, key: string): string | null {
+ const field = value?.[key];
+ return typeof field === "string" ? field : null;
+}
+
+function numberField(value: Record | null, key: string): number | null {
+ const field = value?.[key];
+ return typeof field === "number" ? field : null;
+}
+
+function stringifyFieldValue(value: unknown): string {
+ if (typeof value === "string") return value;
+ if (value === null) return "null";
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
+ return JSON.stringify(value);
+}
+
+function isToolName(value: string | null): value is ToolName {
+ return value !== null && toolNames.includes(value as ToolName);
+}
+
+function isExecutionTarget(value: string | null): value is ExecutionTarget {
+ return (
+ value === "worker-shell" || value === "workspace-container" || value === "sandbox-container"
+ );
+}
diff --git a/examples/think-compare-runtimes/src/run-state.test.ts b/examples/think-compare-runtimes/src/run-state.test.ts
new file mode 100644
index 00000000..728aa3cb
--- /dev/null
+++ b/examples/think-compare-runtimes/src/run-state.test.ts
@@ -0,0 +1,147 @@
+import { describe, expect, test } from "vitest";
+import type { EventRuntime, RunEvent, RunEventKind } from "../shared/events";
+import { applyRunMessage, deriveRunSummary } from "./run-state";
+
+describe("applyRunMessage", () => {
+ test("replaces history and appends live events", () => {
+ const historyEvent = event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ title: "Started",
+ detail: "Initial history",
+ });
+ const liveEvent = { ...historyEvent, id: "run-1:1", sequence: 1 };
+
+ const withHistory = applyRunMessage([], {
+ type: "history",
+ events: [historyEvent],
+ });
+ const withLiveEvent = applyRunMessage(withHistory, {
+ type: "event",
+ event: liveEvent,
+ });
+
+ expect(withHistory).toEqual([historyEvent]);
+ expect(withLiveEvent).toEqual([historyEvent, liveEvent]);
+ });
+
+ test("keeps the overall run open while one runtime is still running", () => {
+ const summary = deriveRunSummary([
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "sandbox",
+ kind: "runtime_completed",
+ timestamp: "2026-06-04T00:01:34.000Z",
+ }),
+ ]);
+
+ expect(summary.status).toBe("running");
+ expect(summary.completedAt).toBeNull();
+ expect(summary.elapsedMs).toBeNull();
+ expect(summary.runtimes.sandbox.status).toBe("completed");
+ expect(summary.runtimes.workspace.status).toBe("running");
+ });
+
+ test("derives per-runtime and overall terminal status", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "both",
+ kind: "run_started",
+ title: "Comparison run started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "runtime_started",
+ title: "Workspace started",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ title: "Sandbox started",
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "runtime_completed",
+ title: "Workspace completed",
+ timestamp: "2026-06-04T00:00:06.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "sandbox",
+ kind: "runtime_failed",
+ title: "Sandbox failed",
+ detail: "3040: Capacity temporarily exceeded, please try again.",
+ timestamp: "2026-06-04T00:00:08.500Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "both",
+ kind: "run_completed",
+ title: "Comparison run complete",
+ timestamp: "2026-06-04T00:00:08.500Z",
+ }),
+ ];
+
+ expect(deriveRunSummary(events)).toEqual({
+ status: "failed",
+ startedAt: "2026-06-04T00:00:00.000Z",
+ completedAt: "2026-06-04T00:00:08.500Z",
+ elapsedMs: 8500,
+ runtimes: {
+ workspace: {
+ status: "completed",
+ startedAt: "2026-06-04T00:00:01.000Z",
+ completedAt: "2026-06-04T00:00:06.000Z",
+ elapsedMs: 5000,
+ error: null,
+ },
+ sandbox: {
+ status: "failed",
+ startedAt: "2026-06-04T00:00:02.000Z",
+ completedAt: "2026-06-04T00:00:08.500Z",
+ elapsedMs: 6500,
+ error: "3040: Capacity temporarily exceeded, please try again.",
+ },
+ },
+ });
+ });
+});
+
+function event(overrides: Partial & { sequence: number }): RunEvent {
+ return {
+ id: `run-1:${overrides.sequence}`,
+ runId: "run-1",
+ sequence: overrides.sequence,
+ runtime: (overrides.runtime ?? "both") as EventRuntime,
+ kind: (overrides.kind ?? "run_started") as RunEventKind,
+ title: overrides.title ?? "Event",
+ detail: overrides.detail ?? "Detail",
+ timestamp: overrides.timestamp ?? "1970-01-01T00:00:00.000Z",
+ };
+}
diff --git a/examples/think-compare-runtimes/src/run-state.ts b/examples/think-compare-runtimes/src/run-state.ts
new file mode 100644
index 00000000..9e2e5859
--- /dev/null
+++ b/examples/think-compare-runtimes/src/run-state.ts
@@ -0,0 +1,125 @@
+import type { RunEvent, RuntimeId } from "../shared/events";
+
+export type RunMessage =
+ | {
+ type: "history";
+ events: RunEvent[];
+ }
+ | {
+ type: "event";
+ event: RunEvent;
+ };
+
+export type RuntimeRunStatus = "idle" | "running" | "completed" | "failed";
+export type OverallRunStatus = RuntimeRunStatus;
+
+export interface RuntimeRunSummary {
+ status: RuntimeRunStatus;
+ startedAt: string | null;
+ completedAt: string | null;
+ elapsedMs: number | null;
+ error: string | null;
+}
+
+export interface RunSummary {
+ status: OverallRunStatus;
+ startedAt: string | null;
+ completedAt: string | null;
+ elapsedMs: number | null;
+ runtimes: Record;
+}
+
+const runtimeIds: RuntimeId[] = ["workspace", "sandbox"];
+
+export function applyRunMessage(events: RunEvent[], message: RunMessage): RunEvent[] {
+ if (message.type === "history") {
+ return [...message.events].sort(bySequence);
+ }
+
+ return [...events, message.event].sort(bySequence);
+}
+
+export function deriveRunSummary(events: RunEvent[]): RunSummary {
+ const sorted = [...events].sort(bySequence);
+ const runStarted = sorted.find((event) => event.kind === "run_started") ?? null;
+ const runCompleted = findLast(sorted, (event) => event.kind === "run_completed");
+ const runtimes = Object.fromEntries(
+ runtimeIds.map((runtime) => [runtime, deriveRuntimeSummary(sorted, runtime)]),
+ ) as Record;
+ const hasRuntimeFailure = runtimeIds.some((runtime) => runtimes[runtime].status === "failed");
+
+ const status: OverallRunStatus = runStarted
+ ? hasRuntimeFailure
+ ? "failed"
+ : runCompleted
+ ? "completed"
+ : "running"
+ : "idle";
+ const startedAt = runStarted?.timestamp ?? null;
+ const completedAt =
+ runCompleted?.timestamp ??
+ (allRuntimesTerminal(runtimes) ? terminalCompletionTime(runtimes) : null);
+
+ return {
+ status,
+ startedAt,
+ completedAt,
+ elapsedMs: elapsedMs(startedAt, completedAt),
+ runtimes,
+ };
+}
+
+function deriveRuntimeSummary(events: RunEvent[], runtime: RuntimeId): RuntimeRunSummary {
+ const runtimeEvents = events.filter((event) => event.runtime === runtime);
+ const started = runtimeEvents.find((event) => event.kind === "runtime_started") ?? null;
+ const completed = findLast(runtimeEvents, (event) => event.kind === "runtime_completed");
+ const failed = findLast(
+ runtimeEvents,
+ (event) =>
+ event.kind === "runtime_failed" ||
+ (event.kind === "agent_tool_error" && event.title === "Think agent failed"),
+ );
+ const startedAt = started?.timestamp ?? runtimeEvents[0]?.timestamp ?? null;
+ const completedAt = failed?.timestamp ?? completed?.timestamp ?? null;
+
+ return {
+ status: failed ? "failed" : completed ? "completed" : startedAt ? "running" : "idle",
+ startedAt,
+ completedAt,
+ elapsedMs: elapsedMs(startedAt, completedAt),
+ error: failed?.detail ?? null,
+ };
+}
+
+function allRuntimesTerminal(runtimes: Record): boolean {
+ return runtimeIds.every((runtime) => {
+ const status = runtimes[runtime].status;
+ return status === "completed" || status === "failed";
+ });
+}
+
+function terminalCompletionTime(runtimes: Record): string | null {
+ const completedTimes = runtimeIds
+ .map((runtime) => runtimes[runtime].completedAt)
+ .filter((timestamp): timestamp is string => timestamp !== null)
+ .sort();
+ return completedTimes.at(-1) ?? null;
+}
+
+function elapsedMs(startedAt: string | null, completedAt: string | null): number | null {
+ if (!startedAt || !completedAt) return null;
+ const elapsed = Date.parse(completedAt) - Date.parse(startedAt);
+ return Number.isNaN(elapsed) ? null : elapsed;
+}
+
+function findLast(items: T[], predicate: (item: T) => boolean): T | undefined {
+ for (let index = items.length - 1; index >= 0; index -= 1) {
+ const item = items[index];
+ if (item !== undefined && predicate(item)) return item;
+ }
+ return undefined;
+}
+
+function bySequence(left: RunEvent, right: RunEvent): number {
+ return left.sequence - right.sequence;
+}
diff --git a/examples/think-compare-runtimes/src/runtime-panel-model.test.ts b/examples/think-compare-runtimes/src/runtime-panel-model.test.ts
new file mode 100644
index 00000000..56450934
--- /dev/null
+++ b/examples/think-compare-runtimes/src/runtime-panel-model.test.ts
@@ -0,0 +1,609 @@
+import { describe, expect, test } from "vitest";
+import type { RunEvent } from "../shared/events";
+import { buildDashboardModel } from "./dashboard-model";
+import { buildRuntimePanelModel } from "./runtime-panel-model";
+
+describe("buildRuntimePanelModel", () => {
+ test("builds Workspace substrate lanes from canonical event facts", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: JSON.stringify({
+ path: "/workspace/repo/feature-briefs/smart-request-policies.md",
+ }),
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({ command: "grep -R Smart docs", cwd: "/workspace/repo" }),
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ cwd: "/workspace/repo",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ stdout: "docs/workers/configuration.md:Smart Request Policies",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "workspace-container",
+ exitCode: 0,
+ stdout: "docs check passed",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:10.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "workspace",
+ kind: "agent_message_delta",
+ title: "Think response stream",
+ detail: "I am checking the nav entry.",
+ timestamp: "2026-06-04T00:00:11.000Z",
+ }),
+ event({
+ sequence: 6,
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn complete",
+ detail: "Updated the docs page, navigation, and Worker example.",
+ timestamp: "2026-06-04T00:00:12.000Z",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:12.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.summary).toEqual([
+ { label: "File ops", value: "1" },
+ { label: "Dynamic worker", value: "1" },
+ { label: "Container commands", value: "1" },
+ ]);
+ expect(model.lanes.map((lane) => lane.label)).toEqual(["VFS", "Dynamic worker", "Container"]);
+ expect(model.lanes[0]?.markers.map((marker) => marker.label)).toEqual([
+ "read feature-briefs/smart-request-policies.md",
+ ]);
+ expect(model.lanes[1]?.segments.map((segment) => segment.label)).toEqual([
+ "grep -R Smart docs",
+ ]);
+ expect(model.lanes[2]?.segments.map((segment) => segment.label)).toEqual(["npm run check"]);
+ expect(model.lanes[2]?.segments[0]?.status).toBe("passed");
+ expect(model.workItems).toMatchObject([
+ {
+ kind: "read",
+ label: "Read files",
+ text: "1 file · feature-briefs/smart-request-policies.md",
+ presentation: "compact",
+ },
+ {
+ kind: "exec",
+ label: "Ran command",
+ command: "grep -R Smart docs",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ presentation: "terminal",
+ },
+ {
+ kind: "exec",
+ label: "Ran command",
+ command: "npm run check",
+ executionTarget: "workspace-container",
+ exitCode: 0,
+ presentation: "terminal",
+ },
+ {
+ kind: "message",
+ label: "Response",
+ text: "I am checking the nav entry.\n\nUpdated the docs page, navigation, and Worker example.",
+ presentation: "markdown",
+ },
+ ]);
+ expect(model.transcript).toEqual([
+ {
+ id: "run-1:6",
+ text: "Updated the docs page, navigation, and Worker example.",
+ tone: "success",
+ },
+ ]);
+ });
+
+ test("groups interleaved thinking, reads, edits, and shell commands by intent", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "I need to locate the relevant docs.\n",
+ timestamp: "2026-06-04T00:00:01.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({ command: "grep -R Smart docs", cwd: "/workspace/repo" }),
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ cwd: "/workspace/repo",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ stdout: "docs/workers/security.md:Smart Request Policies",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:04.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "The grep result points at the security page.\n",
+ timestamp: "2026-06-04T00:00:05.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: JSON.stringify({ path: "/workspace/repo/README.md" }),
+ timestamp: "2026-06-04T00:00:06.000Z",
+ }),
+ event({
+ sequence: 6,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think read result",
+ detail: JSON.stringify({ path: "/workspace/repo/README.md" }),
+ timestamp: "2026-06-04T00:00:07.000Z",
+ }),
+ event({
+ sequence: 7,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/security.md" }),
+ timestamp: "2026-06-04T00:00:08.000Z",
+ }),
+ event({
+ sequence: 8,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think read result",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/security.md" }),
+ timestamp: "2026-06-04T00:00:09.000Z",
+ }),
+ event({
+ sequence: 9,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "I have enough context to edit.\n",
+ timestamp: "2026-06-04T00:00:10.000Z",
+ }),
+ event({
+ sequence: 10,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested edit",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/security.md" }),
+ timestamp: "2026-06-04T00:00:11.000Z",
+ }),
+ event({
+ sequence: 11,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think edit result",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/security.md" }),
+ timestamp: "2026-06-04T00:00:12.000Z",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.workItems).toMatchObject([
+ {
+ kind: "thinking",
+ label: "Thinking",
+ text: "I need to locate the relevant docs.\nThe grep result points at the security page.\nI have enough context to edit.\n",
+ presentation: "markdown",
+ },
+ {
+ kind: "exec",
+ label: "Ran command",
+ command: "grep -R Smart docs",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ stdout: "docs/workers/security.md:Smart Request Policies",
+ presentation: "terminal",
+ },
+ {
+ kind: "read",
+ label: "Read files",
+ count: 2,
+ text: "2 files · README.md · docs/workers/security.md",
+ presentation: "compact",
+ },
+ {
+ kind: "edit",
+ label: "Edited file",
+ text: "docs/workers/security.md · applied",
+ presentation: "compact",
+ },
+ ]);
+ });
+
+ test("starts a new thinking block after Think step boundaries", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "First step reasoning.\n",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({ command: "grep -R Smart docs", cwd: "/workspace/repo" }),
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "grep -R Smart docs",
+ cwd: "/workspace/repo",
+ executionTarget: "worker-shell",
+ exitCode: 0,
+ }),
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "Still the first step after the tool result.\n",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "agent_step",
+ title: "Think step finished",
+ detail: "finishReason: tool-calls",
+ }),
+ event({
+ sequence: 5,
+ runtime: "workspace",
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "Second step reasoning.\n",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.workItems).toMatchObject([
+ {
+ kind: "thinking",
+ text: "First step reasoning.\nStill the first step after the tool result.\n",
+ },
+ { kind: "exec", command: "grep -R Smart docs" },
+ { kind: "thinking", text: "Second step reasoning.\n" },
+ ]);
+ });
+
+ test("ignores fixture instrumentation and incomplete unpaired tool calls in agent work", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "write /workspace/repo/README.md",
+ detail: "Writing fixture bytes through workspace runtime.",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "write complete",
+ detail: "Wrote /workspace/repo/README.md.",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({}),
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested write",
+ detail: JSON.stringify({}),
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.workItems).toEqual([]);
+ });
+
+ test("does not duplicate streamed assistant text when final text repeats it", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "agent_message_delta",
+ title: "Think response stream",
+ detail: "I updated the docs and ran `npm run check`.",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn complete",
+ detail: "I updated the docs and ran `npm run check`.",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.workItems).toMatchObject([
+ {
+ kind: "message",
+ text: "I updated the docs and ran `npm run check`.",
+ presentation: "markdown",
+ },
+ ]);
+ });
+
+ test("shows container assignment separately from command execution", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "workspace",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "container_acquired" as RunEvent["kind"],
+ detail: JSON.stringify({
+ executionTarget: "workspace-container",
+ containerId: "workspace-container-1",
+ }),
+ timestamp: "2026-06-04T00:00:02.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested exec",
+ detail: JSON.stringify({ command: "npm run check", cwd: "/workspace/repo" }),
+ timestamp: "2026-06-04T00:00:05.000Z",
+ }),
+ event({
+ sequence: 3,
+ runtime: "workspace",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "workspace-container",
+ exitCode: 0,
+ stdout: "docs check passed",
+ stderr: "",
+ }),
+ timestamp: "2026-06-04T00:00:09.000Z",
+ }),
+ event({
+ sequence: 4,
+ runtime: "workspace",
+ kind: "container_released" as RunEvent["kind"],
+ detail: JSON.stringify({
+ executionTarget: "workspace-container",
+ containerId: "workspace-container-1",
+ }),
+ timestamp: "2026-06-04T00:00:12.000Z",
+ }),
+ event({
+ sequence: 5,
+ runtime: "workspace",
+ kind: "container_release_scheduled" as RunEvent["kind"],
+ detail: JSON.stringify({
+ executionTarget: "workspace-container",
+ containerId: "workspace-container-1",
+ sleepAfterMs: 120_000,
+ }),
+ timestamp: "2026-06-04T00:00:12.000Z",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+ const containerLane = model.lanes.find((lane) => lane.id === "workspace-container");
+
+ expect(containerLane?.segments.map((segment) => [segment.label, segment.status])).toEqual([
+ ["Container assigned", "lease"],
+ ["npm run check", "passed"],
+ ["Sleep-after", "residual"],
+ ]);
+ expect(containerLane?.segments[0]).toMatchObject({
+ startMs: Date.parse("2026-06-04T00:00:02.000Z"),
+ endMs: Date.parse("2026-06-04T00:00:12.000Z"),
+ });
+ expect(containerLane?.segments[2]).toMatchObject({
+ startMs: Date.parse("2026-06-04T00:00:12.000Z"),
+ endMs: Date.parse("2026-06-04T00:02:12.000Z"),
+ });
+ });
+
+ test("extends an active container assignment to the latest runtime clock", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "sandbox",
+ kind: "container_acquired" as RunEvent["kind"],
+ detail: JSON.stringify({ executionTarget: "sandbox-container" }),
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "sandbox",
+ kind: "agent_thinking_delta",
+ detail: "Still working.",
+ timestamp: "2026-06-04T00:00:08.000Z",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:00:13.000Z").runtimes.sandbox;
+
+ const model = buildRuntimePanelModel(events, "sandbox", telemetry);
+ const containerLane = model.lanes.find((lane) => lane.id === "container");
+
+ expect(containerLane?.segments[0]).toMatchObject({
+ label: "Container assigned",
+ status: "lease",
+ startMs: Date.parse("2026-06-04T00:00:03.000Z"),
+ endMs: Date.parse("2026-06-04T00:00:08.000Z"),
+ });
+ });
+
+ test("does not treat Think startup messages as final transcript", () => {
+ const events = [
+ event({
+ sequence: 1,
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Model-backed Think agent is running against the Workspace runtime.",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:01:00.000Z").runtimes.workspace;
+
+ const model = buildRuntimePanelModel(events, "workspace", telemetry);
+
+ expect(model.transcript).toEqual([]);
+ });
+
+ test("builds Sandbox as a container substrate with validation failure", () => {
+ const events = [
+ event({
+ sequence: 0,
+ runtime: "sandbox",
+ kind: "runtime_started",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ }),
+ event({
+ sequence: 1,
+ runtime: "sandbox",
+ kind: "agent_tool_call",
+ title: "Think requested write",
+ detail: JSON.stringify({ path: "/workspace/repo/docs/workers/smart-request-policies.md" }),
+ timestamp: "2026-06-04T00:00:03.000Z",
+ }),
+ event({
+ sequence: 2,
+ runtime: "sandbox",
+ kind: "agent_tool_result",
+ title: "Think exec result",
+ detail: JSON.stringify({
+ command: "npm run check",
+ cwd: "/workspace/repo",
+ executionTarget: "sandbox-container",
+ exitCode: 1,
+ stdout: "",
+ stderr: "Missing nav entry",
+ }),
+ timestamp: "2026-06-04T00:00:09.000Z",
+ }),
+ ];
+ const telemetry = buildDashboardModel(events, "2026-06-04T00:01:00.000Z").runtimes.sandbox;
+
+ const model = buildRuntimePanelModel(events, "sandbox", telemetry);
+
+ expect(model.summary).toEqual([
+ { label: "File ops", value: "1" },
+ { label: "Container commands", value: "1" },
+ ]);
+ expect(model.lanes.map((lane) => lane.label)).toEqual(["VFS", "Dynamic worker", "Container"]);
+ expect(model.lanes[0]?.markers).toEqual([]);
+ expect(model.lanes[1]?.markers).toEqual([]);
+ expect(model.lanes[2]?.markers.map((marker) => marker.label)).toEqual([
+ "write docs/workers/smart-request-policies.md",
+ ]);
+ expect(model.lanes[2]?.segments.map((segment) => [segment.label, segment.status])).toEqual([
+ ["Session setup", "neutral"],
+ ["npm run check", "failed"],
+ ]);
+ });
+});
+
+function event(overrides: Partial & { sequence: number }): RunEvent {
+ return {
+ id: `run-1:${overrides.sequence}`,
+ runId: "run-1",
+ sequence: overrides.sequence,
+ runtime: overrides.runtime ?? "workspace",
+ kind: overrides.kind ?? "runtime_note",
+ title: overrides.title ?? "Event",
+ detail: overrides.detail ?? "Detail",
+ timestamp: overrides.timestamp ?? "1970-01-01T00:00:00.000Z",
+ } as RunEvent;
+}
diff --git a/examples/think-compare-runtimes/src/runtime-panel-model.ts b/examples/think-compare-runtimes/src/runtime-panel-model.ts
new file mode 100644
index 00000000..b2dadfa1
--- /dev/null
+++ b/examples/think-compare-runtimes/src/runtime-panel-model.ts
@@ -0,0 +1,638 @@
+import type { ExecutionTarget, RunEvent, RuntimeId } from "../shared/events";
+import type { RuntimeDashboardModel } from "./dashboard-model";
+import { factForEvent, factsForRuntime, trimWorkspaceRoot } from "./run-event-facts";
+
+export type TimelineTone = "vfs" | "dynamic-worker" | "container" | "agent" | "error";
+export type SegmentStatus = "running" | "passed" | "failed" | "neutral" | "lease" | "residual";
+
+export interface RuntimePanelModel {
+ statusLine: string;
+ summary: RuntimeSummaryItem[];
+ lanes: TimelineLane[];
+ transcript: TranscriptItem[];
+ workItems: AgentWorkItem[];
+ clock: RuntimeClock;
+}
+
+export interface RuntimeSummaryItem {
+ label: string;
+ value: string;
+}
+
+export interface RuntimeClock {
+ startMs: number | null;
+ endMs: number | null;
+ durationLabel: string;
+}
+
+export interface TimelineLane {
+ id: string;
+ label: string;
+ tone: TimelineTone;
+ segments: TimelineSegment[];
+ markers: TimelineMarker[];
+}
+
+export interface TimelineSegment {
+ id: string;
+ startMs: number;
+ endMs: number;
+ label: string;
+ status: SegmentStatus;
+}
+
+export interface TimelineMarker {
+ id: string;
+ atMs: number;
+ label: string;
+ status: SegmentStatus;
+}
+
+export interface TranscriptItem {
+ id: string;
+ text: string;
+ tone: "neutral" | "success" | "error";
+}
+
+export interface AgentWorkItem {
+ id: string;
+ kind: "thinking" | "message" | "read" | "write" | "edit" | "exec" | "error" | "step";
+ label: string;
+ text: string;
+ tone: "neutral" | "success" | "error" | "stream";
+ presentation: "markdown" | "compact" | "terminal";
+ count?: number;
+ command?: string;
+ cwd?: string;
+ executionTarget?: ExecutionTarget;
+ exitCode?: number;
+ stdout?: string;
+ stderr?: string;
+}
+
+export function buildRuntimePanelModel(
+ events: RunEvent[],
+ runtime: RuntimeId,
+ telemetry: RuntimeDashboardModel,
+): RuntimePanelModel {
+ const runtimeEvents = factsForRuntime(events, runtime, "runtimeOnly").map((fact) => fact.event);
+ const clock = runtimeClock(runtimeEvents, telemetry);
+ const lanes =
+ runtime === "workspace" ? workspaceLanes(runtimeEvents) : sandboxLanes(runtimeEvents);
+
+ return {
+ statusLine: statusLine(telemetry),
+ summary: summaryItems(runtime, telemetry),
+ lanes,
+ transcript: transcriptItems(runtimeEvents),
+ workItems: workItems(runtimeEvents),
+ clock,
+ };
+}
+
+function workspaceLanes(events: RunEvent[]): TimelineLane[] {
+ return [
+ {
+ id: "vfs",
+ label: "VFS",
+ tone: "vfs",
+ segments: [],
+ markers: fileMarkers(events, "vfs"),
+ },
+ {
+ id: "dynamic-worker",
+ label: "Dynamic worker",
+ tone: "dynamic-worker",
+ segments: execSegments(events, "worker-shell"),
+ markers: [],
+ },
+ {
+ id: "workspace-container",
+ label: "Container",
+ tone: "container",
+ segments: containerSegments(events, "workspace-container"),
+ markers: [],
+ },
+ ];
+}
+
+function sandboxLanes(events: RunEvent[]): TimelineLane[] {
+ const firstWork = firstWorkTimestamp(events);
+ const started = events.find((event) => event.kind === "runtime_started");
+ const bootSegment =
+ started && firstWork && firstWork > Date.parse(started.timestamp)
+ ? [
+ {
+ id: "sandbox:boot",
+ startMs: Date.parse(started.timestamp),
+ endMs: firstWork,
+ label: "Session setup",
+ status: "neutral" as const,
+ },
+ ]
+ : [];
+
+ return [
+ {
+ id: "vfs",
+ label: "VFS",
+ tone: "vfs",
+ segments: [],
+ markers: [],
+ },
+ {
+ id: "dynamic-worker",
+ label: "Dynamic worker",
+ tone: "dynamic-worker",
+ segments: [],
+ markers: [],
+ },
+ {
+ id: "container",
+ label: "Container",
+ tone: "container",
+ segments: [...bootSegment, ...containerSegments(events, "sandbox-container")],
+ markers: fileMarkers(events, "container"),
+ },
+ ];
+}
+
+function fileMarkers(events: RunEvent[], _tone: TimelineTone): TimelineMarker[] {
+ return events.flatMap((event) => {
+ const fact = factForEvent(event);
+ if (
+ fact.phase !== "call" ||
+ (fact.tool !== "read" && fact.tool !== "write" && fact.tool !== "edit")
+ ) {
+ return [];
+ }
+ return [
+ {
+ id: event.id,
+ atMs: Date.parse(event.timestamp),
+ label: `${fact.tool} ${fact.path ? trimWorkspaceRoot(fact.path) : "file"}`,
+ status: "neutral" as const,
+ },
+ ];
+ });
+}
+
+function containerSegments(events: RunEvent[], target: ExecutionTarget): TimelineSegment[] {
+ return [...containerLeaseSegments(events, target), ...execSegments(events, target)].sort(
+ (left, right) =>
+ left.startMs - right.startMs || segmentOrder(left.status) - segmentOrder(right.status),
+ );
+}
+
+function containerLeaseSegments(events: RunEvent[], target: ExecutionTarget): TimelineSegment[] {
+ const segments: TimelineSegment[] = [];
+ const acquisitions: RunEvent[] = [];
+ const fallbackEndMs = latestEventTimestamp(events);
+
+ for (const event of events) {
+ const detail = containerLifecycleDetail(event);
+ if (!detail || detail.executionTarget !== target) continue;
+
+ if (event.kind === "container_acquired") {
+ acquisitions.push(event);
+ continue;
+ }
+
+ if (event.kind === "container_released") {
+ const acquired = acquisitions.shift();
+ if (!acquired) continue;
+ segments.push({
+ id: `${acquired.id}:lease`,
+ startMs: Date.parse(acquired.timestamp),
+ endMs: Date.parse(event.timestamp),
+ label: "Container assigned",
+ status: "lease",
+ });
+ continue;
+ }
+
+ if (event.kind === "container_release_scheduled" && detail.sleepAfterMs > 0) {
+ const startMs = Date.parse(event.timestamp);
+ segments.push({
+ id: `${event.id}:residual`,
+ startMs,
+ endMs: startMs + detail.sleepAfterMs,
+ label: "Sleep-after",
+ status: "residual",
+ });
+ }
+ }
+
+ for (const acquired of acquisitions) {
+ const startMs = Date.parse(acquired.timestamp);
+ segments.push({
+ id: `${acquired.id}:lease`,
+ startMs,
+ endMs: Math.max(startMs, fallbackEndMs ?? startMs),
+ label: "Container assigned",
+ status: "lease",
+ });
+ }
+
+ return segments;
+}
+
+function execSegments(events: RunEvent[], target: ExecutionTarget): TimelineSegment[] {
+ const calls = new Map();
+ const segments: TimelineSegment[] = [];
+
+ for (const event of events) {
+ const fact = factForEvent(event);
+ if (fact.tool !== "exec" || !fact.command) continue;
+ if (fact.phase === "call") {
+ const existing = calls.get(fact.command) ?? [];
+ existing.push(event);
+ calls.set(fact.command, existing);
+ continue;
+ }
+ if ((fact.phase !== "result" && fact.phase !== "error") || fact.executionTarget !== target)
+ continue;
+
+ const call = calls.get(fact.command)?.shift();
+ const endMs = Date.parse(event.timestamp);
+ const startMs = call ? Date.parse(call.timestamp) : endMs;
+ segments.push({
+ id: event.id,
+ startMs,
+ endMs: Math.max(startMs, endMs),
+ label: fact.validationCommand ? "npm run check" : fact.command,
+ status: fact.failed ? "failed" : fact.validationCommand ? "passed" : "neutral",
+ });
+ }
+
+ return segments;
+}
+
+function segmentOrder(status: SegmentStatus): number {
+ if (status === "lease") return 0;
+ if (status === "residual") return 1;
+ return 2;
+}
+
+function containerLifecycleDetail(
+ event: RunEvent,
+): { executionTarget: ExecutionTarget; sleepAfterMs: number } | null {
+ if (
+ event.kind !== "container_acquired" &&
+ event.kind !== "container_released" &&
+ event.kind !== "container_release_scheduled"
+ ) {
+ return null;
+ }
+ const fact = factForEvent(event);
+ if (!fact.executionTarget) return null;
+ return {
+ executionTarget: fact.executionTarget,
+ sleepAfterMs: numberDetail(fact, "sleepAfterMs") ?? 0,
+ };
+}
+
+function numberDetail(fact: ReturnType, key: string): number | null {
+ const value = fact.detail?.[key];
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
+}
+
+function latestEventTimestamp(events: RunEvent[]): number | null {
+ for (let index = events.length - 1; index >= 0; index -= 1) {
+ const parsed = Date.parse(events[index]?.timestamp ?? "");
+ if (!Number.isNaN(parsed)) return parsed;
+ }
+ return null;
+}
+
+function firstWorkTimestamp(events: RunEvent[]): number | null {
+ const first = events.find((event) => {
+ const fact = factForEvent(event);
+ return fact.phase === "call" || fact.phase === "result" || fact.phase === "error";
+ });
+ if (!first) return null;
+ const parsed = Date.parse(first.timestamp);
+ return Number.isNaN(parsed) ? null : parsed;
+}
+
+function workItems(events: RunEvent[]): AgentWorkItem[] {
+ const items: AgentWorkItem[] = [];
+ const streamItems = new Map<"thinking" | "message", AgentWorkItem>();
+ const pendingToolCalls = new Map[]>();
+ let readAggregate: { event: RunEvent; count: number; paths: string[] } | null = null;
+
+ const flushReads = () => {
+ if (!readAggregate) return;
+ const preview = readAggregate.paths.slice(0, 3).join(" · ");
+ const suffix =
+ readAggregate.paths.length > 3 ? ` · +${readAggregate.paths.length - 3} more` : "";
+ items.push({
+ id: readAggregate.event.id,
+ kind: "read",
+ label: "Read files",
+ text: `${readAggregate.count} file${readAggregate.count === 1 ? "" : "s"}${preview ? ` · ${preview}${suffix}` : ""}`,
+ tone: "neutral",
+ presentation: "compact",
+ count: readAggregate.count,
+ });
+ readAggregate = null;
+ };
+
+ const appendStream = (event: RunEvent, kind: "thinking" | "message", label: string) => {
+ const existing = streamItems.get(kind);
+ if (existing) {
+ existing.text += event.detail;
+ return;
+ }
+ flushReads();
+ const created: AgentWorkItem = {
+ id: event.id,
+ kind,
+ label,
+ text: event.detail,
+ tone: "stream",
+ presentation: "markdown",
+ };
+ items.push(created);
+ streamItems.set(kind, created);
+ };
+
+ const appendFinalMessage = (event: RunEvent) => {
+ const existing = streamItems.get("message");
+ if (existing) {
+ const streamed = existing.text.trim();
+ const final = event.detail.trim();
+ existing.text =
+ streamed === final || streamed.includes(final)
+ ? streamed
+ : [streamed, final].filter(Boolean).join("\n\n");
+ existing.tone = "success";
+ return;
+ }
+ flushReads();
+ items.push({
+ id: event.id,
+ kind: "message",
+ label: "Final response",
+ text: event.detail,
+ tone: "success",
+ presentation: "markdown",
+ });
+ };
+
+ for (const event of events) {
+ const fact = factForEvent(event);
+
+ if (event.kind === "agent_thinking_delta") {
+ appendStream(event, "thinking", "Thinking");
+ continue;
+ }
+ if (event.kind === "agent_message_delta") {
+ appendStream(event, "message", "Response");
+ continue;
+ }
+ if (event.kind === "agent_step") {
+ streamItems.delete("thinking");
+ continue;
+ }
+ if (event.kind === "agent_message" && event.title === "Think turn started") {
+ continue;
+ }
+ if (event.kind === "agent_message" && event.title === "Think turn complete") {
+ appendFinalMessage(event);
+ continue;
+ }
+ if (event.kind === "runtime_failed") {
+ flushReads();
+ items.push({
+ id: event.id,
+ kind: "error",
+ label: "Needs attention",
+ text: event.detail,
+ tone: "error",
+ presentation: "markdown",
+ });
+ continue;
+ }
+ if (!isAgentToolLifecycleEvent(event)) {
+ continue;
+ }
+
+ if (fact.tool === "read" && fact.phase === "error") {
+ flushReads();
+ items.push({
+ id: event.id,
+ kind: "error",
+ label: "Read failed",
+ text: event.detail,
+ tone: "error",
+ presentation: "markdown",
+ });
+ continue;
+ }
+ if (fact.tool === "read" && fact.phase === "call") {
+ readAggregate ??= { event, count: 0, paths: [] };
+ readAggregate.count += 1;
+ if (fact.path) readAggregate.paths.push(trimWorkspaceRoot(fact.path));
+ continue;
+ }
+ if (fact.tool === "read") {
+ continue;
+ }
+
+ if (fact.phase === "call") {
+ queuePendingToolCall(pendingToolCalls, fact);
+ continue;
+ }
+
+ flushReads();
+ if (fact.tool === "exec") {
+ items.push(execItem(fact, takePendingToolCall(pendingToolCalls, fact)));
+ continue;
+ }
+ if (fact.tool === "edit" || fact.tool === "write") {
+ items.push(fileMutationItem(fact, takePendingToolCall(pendingToolCalls, fact)));
+ }
+ }
+
+ flushReads();
+ appendConcreteUnpairedToolCalls(items, pendingToolCalls);
+ return items;
+}
+
+function isAgentToolLifecycleEvent(event: RunEvent): boolean {
+ return (
+ event.kind === "agent_tool_call" ||
+ event.kind === "agent_tool_result" ||
+ event.kind === "agent_tool_error"
+ );
+}
+
+function queuePendingToolCall(
+ pending: Map[]>,
+ fact: ReturnType,
+) {
+ const key = toolPairKey(fact);
+ const calls = pending.get(key) ?? [];
+ calls.push(fact);
+ pending.set(key, calls);
+}
+
+function takePendingToolCall(
+ pending: Map[]>,
+ fact: ReturnType,
+): ReturnType | null {
+ return pending.get(toolPairKey(fact))?.shift() ?? null;
+}
+
+function toolPairKey(fact: ReturnType): string {
+ return `${fact.tool ?? "tool"}:${fact.command ?? fact.path ?? fact.event.sequence}`;
+}
+
+function execItem(
+ fact: ReturnType,
+ call: ReturnType | null,
+): AgentWorkItem {
+ const command = fact.command ?? call?.command ?? "command";
+ const target = fact.executionTarget ?? call?.executionTarget ?? undefined;
+ const stdout = stringDetail(fact, "stdout");
+ const stderr = stringDetail(fact, "stderr");
+ const exit = typeof fact.exitCode === "number" ? ` · exit ${fact.exitCode}` : "";
+ const targetLabel = target ? ` · ${executionTargetLabel(target)}` : "";
+ return {
+ id: fact.event.id,
+ kind: "exec",
+ label: fact.failed ? "Command failed" : "Ran command",
+ text: `${command}${targetLabel}${exit}`,
+ tone: fact.failed ? "error" : "success",
+ presentation: "terminal",
+ command,
+ cwd: fact.cwd ?? call?.cwd ?? undefined,
+ executionTarget: target,
+ exitCode: fact.exitCode ?? undefined,
+ stdout: stdout ?? undefined,
+ stderr: stderr ?? undefined,
+ };
+}
+
+function fileMutationItem(
+ fact: ReturnType,
+ call: ReturnType | null,
+): AgentWorkItem {
+ const kind = fact.tool === "write" ? "write" : "edit";
+ const path = fact.path ?? call?.path;
+ const displayPath = path ? trimWorkspaceRoot(path) : "file";
+ const label = kind === "write" ? "Wrote file" : "Edited file";
+ const status = fact.failed ? "failed" : "applied";
+ return {
+ id: fact.event.id,
+ kind,
+ label: fact.failed ? `${label} failed` : label,
+ text: `${displayPath} · ${status}`,
+ tone: fact.failed ? "error" : "success",
+ presentation: "compact",
+ };
+}
+
+function appendConcreteUnpairedToolCalls(
+ items: AgentWorkItem[],
+ pending: Map[]>,
+) {
+ for (const calls of pending.values()) {
+ for (const fact of calls) {
+ if (fact.tool === "exec" && fact.command) {
+ items.push({
+ id: fact.event.id,
+ kind: "exec",
+ label: "Command requested",
+ text: fact.command,
+ tone: "neutral",
+ presentation: "terminal",
+ command: fact.command,
+ cwd: fact.cwd ?? undefined,
+ executionTarget: fact.executionTarget ?? undefined,
+ });
+ } else if ((fact.tool === "edit" || fact.tool === "write") && fact.path) {
+ items.push({
+ id: fact.event.id,
+ kind: fact.tool,
+ label: fact.tool === "write" ? "Write requested" : "Edit requested",
+ text: trimWorkspaceRoot(fact.path),
+ tone: "neutral",
+ presentation: "compact",
+ });
+ }
+ }
+ }
+}
+
+function stringDetail(fact: ReturnType, key: string): string | null {
+ const value = fact.detail?.[key];
+ return typeof value === "string" && value.length > 0 ? value : null;
+}
+
+function executionTargetLabel(target: ExecutionTarget): string {
+ if (target === "worker-shell") return "worker shell";
+ if (target === "workspace-container") return "workspace container";
+ return "sandbox";
+}
+
+function transcriptItems(events: RunEvent[]): TranscriptItem[] {
+ const final = [...events]
+ .reverse()
+ .find((event) => event.kind === "agent_message" && event.title === "Think turn complete");
+ const failure = [...events]
+ .reverse()
+ .find((event) => event.kind === "runtime_failed" || event.kind === "agent_tool_error");
+
+ if (failure) {
+ return [{ id: failure.id, text: failure.detail, tone: "error" }];
+ }
+ if (final) {
+ return [{ id: final.id, text: final.detail, tone: "success" }];
+ }
+ return [];
+}
+
+function runtimeClock(events: RunEvent[], telemetry: RuntimeDashboardModel): RuntimeClock {
+ const start = events.find((event) => event.kind === "runtime_started") ?? events[0] ?? null;
+ const terminal = [...events]
+ .reverse()
+ .find((event) => event.kind === "runtime_completed" || event.kind === "runtime_failed");
+ const last = events.at(-1) ?? null;
+ const startMs = parseTimestamp(start?.timestamp ?? null);
+ const endMs = parseTimestamp(terminal?.timestamp ?? last?.timestamp ?? null);
+ return {
+ startMs,
+ endMs,
+ durationLabel: telemetry.elapsedLabel,
+ };
+}
+
+function summaryItems(runtime: RuntimeId, telemetry: RuntimeDashboardModel): RuntimeSummaryItem[] {
+ if (runtime === "workspace") {
+ return [
+ { label: "File ops", value: String(telemetry.fileOps) },
+ { label: "Dynamic worker", value: String(telemetry.workerShellExecs) },
+ { label: "Container commands", value: String(telemetry.containerExecs) },
+ ];
+ }
+
+ return [
+ { label: "File ops", value: String(telemetry.fileOps) },
+ { label: "Container commands", value: String(telemetry.containerExecs) },
+ ];
+}
+
+function statusLine(telemetry: RuntimeDashboardModel): string {
+ if (telemetry.status === "idle") return "Ready";
+ if (telemetry.status === "running") return `Running · ${telemetry.elapsedLabel}`;
+ if (telemetry.status === "completed") return `Completed · ${telemetry.elapsedLabel}`;
+ return `Failed · ${telemetry.elapsedLabel}`;
+}
+
+function parseTimestamp(timestamp: string | null): number | null {
+ if (!timestamp) return null;
+ const parsed = Date.parse(timestamp);
+ return Number.isNaN(parsed) ? null : parsed;
+}
diff --git a/examples/think-compare-runtimes/src/runtime-wing.tsx b/examples/think-compare-runtimes/src/runtime-wing.tsx
new file mode 100644
index 00000000..948bf297
--- /dev/null
+++ b/examples/think-compare-runtimes/src/runtime-wing.tsx
@@ -0,0 +1,417 @@
+import type { RunEvent, RuntimeId } from "../shared/events";
+import { AutoScrollList } from "./auto-scroll-list";
+import type { RuntimeDashboardModel } from "./dashboard-model";
+import { MarkdownText } from "./markdown-text";
+import {
+ type AgentWorkItem,
+ buildRuntimePanelModel,
+ type RuntimePanelModel,
+ type SegmentStatus,
+ type TimelineLane,
+ type TimelineMarker,
+ type TimelineSegment,
+ type TimelineTone,
+} from "./runtime-panel-model";
+
+const runtimeCopy: Record<
+ RuntimeId,
+ {
+ label: "Workspace" | "Sandbox";
+ packageName: string;
+ }
+> = {
+ workspace: {
+ label: "Workspace",
+ packageName: "@cloudflare/workspace",
+ },
+ sandbox: {
+ label: "Sandbox",
+ packageName: "@cloudflare/sandbox",
+ },
+};
+
+const statusTone = {
+ idle: "text-[#8F8A81]",
+ running: "text-[#1D4ED8]",
+ completed: "text-[#166534]",
+ failed: "text-[#B42318]",
+};
+
+export function RuntimeWing({
+ events,
+ runtime,
+ telemetry,
+}: {
+ events: RunEvent[];
+ runtime: RuntimeId;
+ telemetry: RuntimeDashboardModel;
+}) {
+ const copy = runtimeCopy[runtime];
+ const panel = buildRuntimePanelModel(events, runtime, telemetry);
+
+ return (
+
+
+
+ {capacityHint(telemetry.error) ? (
+
+ {capacityHint(telemetry.error)}
+
+ ) : null}
+
+
+
+ );
+}
+
+function SummaryStrip({ items }: { items: RuntimePanelModel["summary"] }) {
+ return (
+
+ {items.map((item) => (
+
+
-
+ {item.label}
+
+ - {item.value}
+
+ ))}
+
+ );
+}
+
+function SubstrateTimeline({ model, runtime }: { model: RuntimePanelModel; runtime: RuntimeId }) {
+ const scale = timelineScale(model);
+ return (
+
+
+ Timeline
+ {model.clock.durationLabel}
+
+
+
+ {model.lanes.map((lane) => (
+
+ ))}
+
+
+ 0:00
+ {scale.durationMs > 0 ? formatMs(scale.durationMs) : "waiting"}
+
+
+ );
+}
+
+function TimelineLaneView({ lane, scale }: { lane: TimelineLane; scale: TimelineScale }) {
+ return (
+
+
+
+ {lane.label}
+
+
+ {lane.segments.map((segment) => (
+
+ ))}
+ {lane.markers.map((marker) => (
+
+ ))}
+ {lane.segments.length === 0 && lane.markers.length === 0 ? (
+
+ no activity
+
+ ) : null}
+
+
+ );
+}
+
+function Segment({
+ scale,
+ segment,
+ tone,
+}: {
+ scale: TimelineScale;
+ segment: TimelineSegment;
+ tone: TimelineTone;
+}) {
+ const left = positionPct(segment.startMs, scale);
+ const width = Math.max(positionPct(segment.endMs, scale) - left, 2.5);
+ return (
+
+ {segment.label}
+
+ );
+}
+
+function Marker({
+ marker,
+ scale,
+ tone,
+}: {
+ marker: TimelineMarker;
+ scale: TimelineScale;
+ tone: TimelineTone;
+}) {
+ return (
+
+ );
+}
+
+function TimelineLegend() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function LegendSwatch({ className, label }: { className: string; label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+function AgentWorkStream({ model, runtime }: { model: RuntimePanelModel; runtime: RuntimeId }) {
+ const items = model.workItems;
+ return (
+
+
+
+ Agent work
+
+
+ {items.length} activities
+
+
+ {items.length === 0 ? (
+ Waiting for the agent to start.
+ ) : (
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+function AgentWorkRow({ item }: { item: AgentWorkItem }) {
+ if (item.presentation === "terminal") {
+ return ;
+ }
+
+ if (item.presentation === "compact") {
+ return ;
+ }
+
+ return (
+
+
+ {item.label}
+
+
+
+
+
+ );
+}
+
+function CompactWorkRow({ item }: { item: AgentWorkItem }) {
+ return (
+
+
+ {item.label}
+
+ {item.text}
+
+ );
+}
+
+function TerminalWorkRow({ item }: { item: AgentWorkItem }) {
+ return (
+
+
+ {item.label}
+ {item.executionTarget ? (
+ {executionTargetCopy(item.executionTarget)}
+ ) : null}
+ {typeof item.exitCode === "number" ? (
+
+ exit {item.exitCode}
+
+ ) : null}
+
+
+ $ {item.command ?? item.text}
+
+ {item.stdout || item.stderr ? : null}
+
+ );
+}
+
+function CommandOutput({ item }: { item: AgentWorkItem }) {
+ const output = [
+ item.stdout ? `stdout\n${item.stdout}` : null,
+ item.stderr ? `stderr\n${item.stderr}` : null,
+ ]
+ .filter(Boolean)
+ .join("\n\n");
+ return (
+
+
+ output
+
+
+ {output.length > 1200 ? `${output.slice(0, 1200)}\n…` : output}
+
+
+ );
+}
+
+function workStreamWatchKey(items: AgentWorkItem[]): string {
+ const textLength = items.reduce(
+ (total, item) =>
+ total + item.text.length + (item.stdout?.length ?? 0) + (item.stderr?.length ?? 0),
+ 0,
+ );
+ return `${items.length}:${textLength}`;
+}
+
+function workToneClass(item: AgentWorkItem): string {
+ if (item.tone === "error") return "border-[#B42318] text-[#B42318]";
+ if (item.kind === "thinking") return "border-[#DED8CD] text-[#8F8A81]";
+ if (item.tone === "success") return "border-[#166534] text-[#166534]";
+ return "border-[#1D4ED8] text-[#1D4ED8]";
+}
+
+function markdownToneClass(item: AgentWorkItem): string {
+ if (item.tone === "error") return "text-[#B42318]";
+ if (item.kind === "thinking") return "text-[#6F6A62] opacity-75";
+ return "text-[#24211D]";
+}
+
+function compactToneClass(item: AgentWorkItem): string {
+ if (item.tone === "error") return "text-[#B42318]";
+ if (item.tone === "success") return "text-[#166534]";
+ if (item.kind === "exec") return "text-[#9A5B00]";
+ return "text-[#8F8A81]";
+}
+
+function executionTargetCopy(target: NonNullable): string {
+ if (target === "worker-shell") return "dynamic worker";
+ if (target === "workspace-container") return "workspace container";
+ return "sandbox container";
+}
+
+interface TimelineScale {
+ startMs: number;
+ endMs: number;
+ durationMs: number;
+}
+
+function timelineScale(model: RuntimePanelModel): TimelineScale {
+ const startMs = model.clock.startMs ?? model.clock.endMs ?? 0;
+ const segmentEndMs = model.lanes.flatMap((lane) => lane.segments.map((segment) => segment.endMs));
+ const markerMs = model.lanes.flatMap((lane) => lane.markers.map((marker) => marker.atMs));
+ const endMs = Math.max(model.clock.endMs ?? startMs, startMs, ...segmentEndMs, ...markerMs);
+ return { startMs, endMs, durationMs: endMs - startMs };
+}
+
+function positionPct(timestamp: number, scale: TimelineScale): number {
+ if (scale.durationMs <= 0) return 0;
+ return Math.max(0, Math.min(100, ((timestamp - scale.startMs) / scale.durationMs) * 100));
+}
+
+function formatMs(ms: number): string {
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
+}
+
+function toneClass(tone: TimelineTone): string {
+ if (tone === "container") return "bg-[#D97706]";
+ if (tone === "dynamic-worker") return "bg-[#7C3AED]";
+ if (tone === "error") return "bg-[#B42318]";
+ if (tone === "agent") return "bg-[#111111]";
+ return "bg-[#1D4ED8]";
+}
+
+function segmentClass(status: SegmentStatus, tone: TimelineTone): string {
+ if (status === "failed") return "bg-[#B42318]";
+ if (status === "passed") return "bg-[#166534]";
+ if (status === "lease") return "bg-[#F4C98F] opacity-55";
+ if (status === "residual") {
+ return "bg-[repeating-linear-gradient(45deg,#F4C98F_0,#F4C98F_3px,transparent_3px,transparent_7px)] opacity-55";
+ }
+ return toneClass(tone);
+}
+
+function capacityHint(error: string | null): string | null {
+ if (!error) return null;
+ return error.includes("Capacity temporarily exceeded")
+ ? "Upstream model capacity; retry later."
+ : null;
+}
diff --git a/examples/think-compare-runtimes/src/styles.css b/examples/think-compare-runtimes/src/styles.css
new file mode 100644
index 00000000..9d8f3acc
--- /dev/null
+++ b/examples/think-compare-runtimes/src/styles.css
@@ -0,0 +1,31 @@
+@source "../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}";
+@import "@cloudflare/kumo/styles/tailwind";
+@import "tailwindcss";
+
+@layer base {
+ :root {
+ color-scheme: light;
+ font-family: "Space Grotesk", "DM Sans", ui-sans-serif, system-ui, sans-serif;
+ background: #fbfaf6;
+ }
+
+ body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ background: #fbfaf6;
+ }
+
+ button {
+ font: inherit;
+ }
+
+ code,
+ pre,
+ kbd,
+ samp,
+ .font-mono {
+ font-family: "Space Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
+ Consolas, "Liberation Mono", "Courier New", monospace;
+ }
+}
diff --git a/examples/think-compare-runtimes/src/top-bar.tsx b/examples/think-compare-runtimes/src/top-bar.tsx
new file mode 100644
index 00000000..c8b13076
--- /dev/null
+++ b/examples/think-compare-runtimes/src/top-bar.tsx
@@ -0,0 +1,35 @@
+import { Button } from "@cloudflare/kumo/components/button";
+
+export interface TopBarProps {
+ actionLabel: string;
+ disabled: boolean;
+ error: string | null;
+ onStart: () => void;
+ runId: string | null;
+ runLabel: string;
+}
+
+export function TopBar({ actionLabel, disabled, error, onStart }: TopBarProps) {
+ return (
+
+ );
+}
diff --git a/examples/think-compare-runtimes/tsconfig.app.json b/examples/think-compare-runtimes/tsconfig.app.json
new file mode 100644
index 00000000..d1a8aa14
--- /dev/null
+++ b/examples/think-compare-runtimes/tsconfig.app.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowImportingTsExtensions": true,
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true
+ },
+ "include": ["src", "shared"]
+}
diff --git a/examples/think-compare-runtimes/tsconfig.json b/examples/think-compare-runtimes/tsconfig.json
new file mode 100644
index 00000000..e4903021
--- /dev/null
+++ b/examples/think-compare-runtimes/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.worker.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/examples/think-compare-runtimes/tsconfig.node.json b/examples/think-compare-runtimes/tsconfig.node.json
new file mode 100644
index 00000000..96b89592
--- /dev/null
+++ b/examples/think-compare-runtimes/tsconfig.node.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true
+ },
+ "include": ["vite.config.ts", "vitest.config.ts"]
+}
diff --git a/examples/think-compare-runtimes/tsconfig.worker.json b/examples/think-compare-runtimes/tsconfig.worker.json
new file mode 100644
index 00000000..e074965b
--- /dev/null
+++ b/examples/think-compare-runtimes/tsconfig.worker.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "allowImportingTsExtensions": true,
+ "module": "ESNext",
+ "types": ["@cloudflare/workers-types/2023-07-01"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true
+ },
+ "include": ["worker", "shared"]
+}
diff --git a/examples/think-compare-runtimes/vite.config.ts b/examples/think-compare-runtimes/vite.config.ts
new file mode 100644
index 00000000..2359f32d
--- /dev/null
+++ b/examples/think-compare-runtimes/vite.config.ts
@@ -0,0 +1,8 @@
+import { cloudflare } from "@cloudflare/vite-plugin";
+import tailwindcss from "@tailwindcss/vite";
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [react(), tailwindcss(), cloudflare()],
+});
diff --git a/examples/think-compare-runtimes/vitest.config.ts b/examples/think-compare-runtimes/vitest.config.ts
new file mode 100644
index 00000000..b0c8cf5e
--- /dev/null
+++ b/examples/think-compare-runtimes/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["shared/**/*.test.ts", "src/**/*.test.{ts,tsx}", "worker/**/*.test.ts"],
+ },
+});
diff --git a/examples/think-compare-runtimes/worker/comparison-agents.test.ts b/examples/think-compare-runtimes/worker/comparison-agents.test.ts
new file mode 100644
index 00000000..4227b058
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/comparison-agents.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../shared/fixture";
+import { runComparisonAgents } from "./comparison-agents";
+import type { RunEventInput } from "./run-events";
+
+describe("runComparisonAgents", () => {
+ test("records runtime and run terminal events", async () => {
+ const events: RunEventInput[] = [];
+
+ await runComparisonAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison() {},
+ },
+ sandboxAgent: {
+ async runComparison() {
+ throw new Error("capacity exceeded");
+ },
+ },
+ appendEvent(input) {
+ events.push(input);
+ },
+ });
+
+ expect(events).toEqual([
+ {
+ runtime: "workspace",
+ kind: "runtime_started",
+ title: "Workspace runtime started",
+ detail: "Workspace Think agent is running.",
+ },
+ {
+ runtime: "sandbox",
+ kind: "runtime_started",
+ title: "Sandbox runtime started",
+ detail: "Sandbox Think agent is running.",
+ },
+ {
+ runtime: "workspace",
+ kind: "runtime_completed",
+ title: "Workspace runtime completed",
+ detail: "Workspace Think agent completed.",
+ },
+ {
+ runtime: "sandbox",
+ kind: "runtime_failed",
+ title: "Sandbox runtime failed",
+ detail: "capacity exceeded",
+ },
+ {
+ runtime: "both",
+ kind: "run_completed",
+ title: "Comparison run complete",
+ detail: "Workspace completed; Sandbox failed.",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/comparison-agents.ts b/examples/think-compare-runtimes/worker/comparison-agents.ts
new file mode 100644
index 00000000..afeb1359
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/comparison-agents.ts
@@ -0,0 +1,76 @@
+import type { RuntimeId } from "../shared/events";
+import type { ComparisonFixture } from "../shared/fixture";
+import type { RunEventInput } from "./run-events";
+import { type RuntimeThinkAgentHandleInput, startRuntimeThinkAgents } from "./think/agent-starter";
+
+export interface RunComparisonAgentsOptions {
+ runId: string;
+ fixture: ComparisonFixture;
+ workspaceAgent: RuntimeThinkAgentHandleInput;
+ sandboxAgent: RuntimeThinkAgentHandleInput;
+ appendEvent(input: RunEventInput): void | Promise;
+}
+
+type RuntimeTerminalStatus = "completed" | "failed";
+
+export async function runComparisonAgents({
+ runId,
+ fixture,
+ workspaceAgent,
+ sandboxAgent,
+ appendEvent,
+}: RunComparisonAgentsOptions): Promise {
+ const terminalStatuses = new Map();
+
+ await startRuntimeThinkAgents({
+ runId,
+ fixture,
+ workspaceAgent,
+ sandboxAgent,
+ onAgentStart(runtime) {
+ return appendEvent({
+ runtime,
+ kind: "runtime_started",
+ title: `${runtimeLabel(runtime)} runtime started`,
+ detail: `${runtimeLabel(runtime)} Think agent is running.`,
+ });
+ },
+ async onAgentComplete(runtime) {
+ terminalStatuses.set(runtime, "completed");
+ await appendEvent({
+ runtime,
+ kind: "runtime_completed",
+ title: `${runtimeLabel(runtime)} runtime completed`,
+ detail: `${runtimeLabel(runtime)} Think agent completed.`,
+ });
+ },
+ async onAgentError(runtime, error) {
+ terminalStatuses.set(runtime, "failed");
+ await appendEvent({
+ runtime,
+ kind: "runtime_failed",
+ title: `${runtimeLabel(runtime)} runtime failed`,
+ detail: error instanceof Error ? error.message : String(error),
+ });
+ },
+ });
+
+ await appendEvent({
+ runtime: "both",
+ kind: "run_completed",
+ title: "Comparison run complete",
+ detail: runCompletionDetail(terminalStatuses),
+ });
+}
+
+function runCompletionDetail(statuses: Map): string {
+ const runtimes: RuntimeId[] = ["workspace", "sandbox"];
+ return runtimes
+ .map((runtime) => `${runtimeLabel(runtime)} ${statuses.get(runtime) ?? "unknown"}`)
+ .join("; ")
+ .concat(".");
+}
+
+function runtimeLabel(runtime: RuntimeId): "Workspace" | "Sandbox" {
+ return runtime === "workspace" ? "Workspace" : "Sandbox";
+}
diff --git a/examples/think-compare-runtimes/worker/container-config.ts b/examples/think-compare-runtimes/worker/container-config.ts
new file mode 100644
index 00000000..fb46b7cc
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/container-config.ts
@@ -0,0 +1,53 @@
+const DEFAULT_CONTAINER_SLEEP_AFTER = "2m";
+const DEFAULT_WARM_POOL_REFRESH_INTERVAL_MS = 10_000;
+const DEFAULT_WARM_POOL_TARGET = 0;
+
+export interface ContainerPoolConfigEnv {
+ CONTAINER_SLEEP_AFTER?: string;
+ WARM_POOL_REFRESH_INTERVAL?: string;
+ WARM_POOL_RESET_KEY?: string;
+ WARM_POOL_TARGET?: string;
+}
+
+export function containerSleepAfter(env: { CONTAINER_SLEEP_AFTER?: string }): string {
+ return env.CONTAINER_SLEEP_AFTER ?? DEFAULT_CONTAINER_SLEEP_AFTER;
+}
+
+export function containerSleepAfterMs(env: { CONTAINER_SLEEP_AFTER?: string }): number {
+ return parseDurationMs(containerSleepAfter(env));
+}
+
+export function warmPoolRefreshIntervalMs(env: ContainerPoolConfigEnv): number {
+ return parsePositiveInteger(
+ env.WARM_POOL_REFRESH_INTERVAL,
+ DEFAULT_WARM_POOL_REFRESH_INTERVAL_MS,
+ );
+}
+
+export function warmPoolTarget(env: ContainerPoolConfigEnv): number {
+ return parsePositiveInteger(env.WARM_POOL_TARGET, DEFAULT_WARM_POOL_TARGET);
+}
+
+function parsePositiveInteger(value: string | undefined, fallback: number): number {
+ if (!value) return fallback;
+ const parsed = Number.parseInt(value, 10);
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
+}
+
+function parseDurationMs(value: string): number {
+ const match = /^(\d+)(ms|s|m|h)?$/.exec(value.trim());
+ if (!match) return 120_000;
+ const amount = Number.parseInt(match[1] ?? "0", 10);
+ switch (match[2] ?? "s") {
+ case "ms":
+ return amount;
+ case "s":
+ return amount * 1_000;
+ case "m":
+ return amount * 60_000;
+ case "h":
+ return amount * 3_600_000;
+ default:
+ return 120_000;
+ }
+}
diff --git a/examples/think-compare-runtimes/worker/container-pool-manager.ts b/examples/think-compare-runtimes/worker/container-pool-manager.ts
new file mode 100644
index 00000000..f373adc7
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/container-pool-manager.ts
@@ -0,0 +1,218 @@
+const STATE_KEY = "container-warm-pool";
+
+export interface WarmPoolRuntime {
+ startContainer(containerId: string): Promise;
+ destroyContainer(containerId: string): Promise;
+ isContainerRunning(containerId: string): Promise;
+ keepContainerAlive(containerId: string): Promise;
+}
+
+interface WarmPoolStorage {
+ get(key: string): Promise;
+ put(key: string, value: T): Promise;
+}
+
+interface WarmPoolState {
+ warm: string[];
+ assignments: Record;
+ releasing: string[];
+}
+
+export interface ContainerWarmPoolManagerOptions {
+ storage: WarmPoolStorage;
+ runtime: WarmPoolRuntime;
+ target: number;
+ createContainerId?: () => string;
+}
+
+export class ContainerWarmPoolManager {
+ readonly #storage: WarmPoolStorage;
+ readonly #runtime: WarmPoolRuntime;
+ readonly #target: number;
+ readonly #createContainerId: () => string;
+ #operationQueue: Promise = Promise.resolve();
+
+ constructor({
+ storage,
+ runtime,
+ target,
+ createContainerId = () => crypto.randomUUID(),
+ }: ContainerWarmPoolManagerOptions) {
+ this.#storage = storage;
+ this.#runtime = runtime;
+ this.#target = Math.max(0, target);
+ this.#createContainerId = createContainerId;
+ }
+
+ getContainer(logicalId: string): Promise {
+ return this.#serialize(async () => {
+ const state = await this.#load();
+ const existing = state.assignments[logicalId];
+ if (existing && (await this.#runtime.isContainerRunning(existing))) {
+ return existing;
+ }
+ if (existing) {
+ delete state.assignments[logicalId];
+ markReleasing(state, existing);
+ }
+
+ const warm = state.warm.shift();
+ let containerId: string;
+ try {
+ containerId = warm ?? (await this.#startNewContainer(state));
+ } catch (error) {
+ await this.#save(state);
+ throw error;
+ }
+ state.assignments[logicalId] = containerId;
+ await this.#save(state);
+ return containerId;
+ });
+ }
+
+ releaseContainer(logicalId: string): Promise {
+ return this.#serialize(async () => {
+ const state = await this.#load();
+ const containerId = state.assignments[logicalId];
+ if (!containerId) return;
+
+ delete state.assignments[logicalId];
+ state.warm = state.warm.filter((id) => id !== containerId);
+ markReleasing(state, containerId);
+ await this.#drainReleasing(state);
+ await this.#save(state);
+ });
+ }
+
+ refresh(): Promise {
+ return this.#serialize(async () => {
+ const state = await this.#load();
+ await this.#drainReleasing(state);
+ await this.#reconcileAssignments(state);
+ await this.#reconcileWarmContainers(state);
+ await this.#scaleWarmContainersDown(state);
+
+ if (state.releasing.length === 0) {
+ while (state.warm.length < this.#target) {
+ try {
+ state.warm.push(await this.#startNewContainer(state));
+ } catch {
+ break;
+ }
+ }
+ }
+
+ await this.#save(state);
+ });
+ }
+
+ reset(): Promise {
+ return this.#serialize(async () => {
+ const state = await this.#load();
+ for (const containerId of [
+ ...state.releasing,
+ ...Object.values(state.assignments),
+ ...state.warm,
+ ]) {
+ markReleasing(state, containerId);
+ }
+ state.assignments = {};
+ state.warm = [];
+ await this.#drainReleasing(state);
+ await this.#save(state);
+ });
+ }
+
+ snapshot(): Promise {
+ return this.#serialize(() => this.#load());
+ }
+
+ #serialize(operation: () => Promise): Promise {
+ const result = this.#operationQueue.then(operation, operation);
+ this.#operationQueue = result.catch(() => {});
+ return result;
+ }
+
+ async #reconcileAssignments(state: WarmPoolState): Promise {
+ for (const [logicalId, containerId] of Object.entries(state.assignments)) {
+ if (await this.#runtime.isContainerRunning(containerId)) continue;
+ delete state.assignments[logicalId];
+ markReleasing(state, containerId);
+ }
+ await this.#drainReleasing(state);
+ }
+
+ async #reconcileWarmContainers(state: WarmPoolState): Promise {
+ const warm: string[] = [];
+ for (const containerId of state.warm) {
+ if (await this.#runtime.isContainerRunning(containerId)) {
+ await this.#runtime.keepContainerAlive(containerId);
+ warm.push(containerId);
+ } else {
+ markReleasing(state, containerId);
+ }
+ }
+ state.warm = warm;
+ await this.#drainReleasing(state);
+ }
+
+ async #scaleWarmContainersDown(state: WarmPoolState): Promise {
+ const excess = state.warm.splice(this.#target);
+ for (const containerId of excess) {
+ markReleasing(state, containerId);
+ }
+ await this.#drainReleasing(state);
+ }
+
+ async #drainReleasing(state: WarmPoolState): Promise {
+ const stillReleasing: string[] = [];
+ for (const containerId of state.releasing) {
+ try {
+ await this.#runtime.destroyContainer(containerId);
+ } catch {
+ stillReleasing.push(containerId);
+ }
+ }
+ state.releasing = unique(stillReleasing);
+ }
+
+ async #startNewContainer(state: WarmPoolState): Promise {
+ const containerId = this.#createContainerId();
+ try {
+ await this.#runtime.startContainer(containerId);
+ return containerId;
+ } catch (error) {
+ markReleasing(state, containerId);
+ await this.#drainReleasing(state);
+ throw error;
+ }
+ }
+
+ async #load(): Promise {
+ const stored = await this.#storage.get>(STATE_KEY);
+ return {
+ warm: stored?.warm ?? [],
+ assignments: stored?.assignments ?? {},
+ releasing: stored?.releasing ?? [],
+ };
+ }
+
+ async #save(state: WarmPoolState): Promise {
+ await this.#storage.put(STATE_KEY, {
+ warm: unique(state.warm),
+ assignments: state.assignments,
+ releasing: unique(state.releasing),
+ });
+ }
+}
+
+function markReleasing(state: WarmPoolState, containerId: string): void {
+ state.warm = state.warm.filter((id) => id !== containerId);
+ if (!state.releasing.includes(containerId)) {
+ state.releasing.push(containerId);
+ }
+}
+
+function unique(values: string[]): string[] {
+ return [...new Set(values)];
+}
diff --git a/examples/think-compare-runtimes/worker/container-pools.test.ts b/examples/think-compare-runtimes/worker/container-pools.test.ts
new file mode 100644
index 00000000..b13558ff
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/container-pools.test.ts
@@ -0,0 +1,305 @@
+import { describe, expect, test } from "vitest";
+import { ContainerWarmPoolManager, type WarmPoolRuntime } from "./container-pool-manager";
+
+class MemoryStorage {
+ readonly values = new Map();
+
+ async get(key: string): Promise {
+ return this.values.get(key) as T | undefined;
+ }
+
+ async put(key: string, value: T): Promise {
+ this.values.set(key, value);
+ }
+}
+
+describe("ContainerWarmPoolManager", () => {
+ test("assigns warmed containers to logical run ids", async () => {
+ const runtime = createRuntime();
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 2,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+ await expect(pool.getContainer("run-1")).resolves.toBe("warm-a");
+ await expect(pool.getContainer("run-1")).resolves.toBe("warm-a");
+
+ expect(runtime.started).toEqual(["warm-a", "warm-b"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: { "run-1": "warm-a" },
+ warm: ["warm-b"],
+ });
+ });
+
+ test("destroys released assignments and replenishes the warm target", async () => {
+ const runtime = createRuntime();
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 1,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+ await pool.getContainer("run-1");
+ await pool.releaseContainer("run-1");
+ await pool.refresh();
+
+ expect(runtime.destroyed).toEqual(["warm-a"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ warm: ["warm-b"],
+ });
+ });
+
+ test("does not assign one warm container to concurrent logical runs", async () => {
+ const runtime = createRuntime();
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 1,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+ const assigned = await Promise.all([pool.getContainer("run-1"), pool.getContainer("run-2")]);
+
+ expect(new Set(assigned).size).toBe(2);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: { "run-1": assigned[0], "run-2": assigned[1] },
+ warm: [],
+ });
+ });
+
+ test("retries failed release destruction from refresh", async () => {
+ const runtime = createRuntime();
+ let failDestroy = true;
+ runtime.destroyContainer = async (containerId) => {
+ if (failDestroy) throw new Error("destroy failed");
+ runtime.destroyed.push(containerId);
+ };
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 1,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+ await pool.getContainer("run-1");
+ await pool.releaseContainer("run-1");
+
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ releasing: ["warm-a"],
+ warm: [],
+ });
+
+ failDestroy = false;
+ await pool.refresh();
+
+ expect(runtime.destroyed).toEqual(["warm-a"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ releasing: [],
+ warm: ["warm-b"],
+ });
+ });
+
+ test("keeps warm ownership when stale container destruction fails", async () => {
+ const runtime = createRuntime();
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 1,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+ runtime.running.delete("warm-a");
+ runtime.destroyContainer = async () => {
+ throw new Error("destroy failed");
+ };
+ await pool.refresh();
+
+ expect(await pool.snapshot()).toMatchObject({
+ releasing: ["warm-a"],
+ warm: [],
+ });
+ });
+
+ test("resets tracked containers before replenishing", async () => {
+ const runtime = createRuntime();
+ const storage = new MemoryStorage();
+ await storage.put("container-warm-pool", {
+ assignments: { "run-1": "assigned-a" },
+ releasing: ["releasing-a"],
+ warm: ["warm-a", "warm-b"],
+ });
+ for (const containerId of ["assigned-a", "releasing-a", "warm-a", "warm-b"]) {
+ runtime.running.add(containerId);
+ }
+ const pool = new ContainerWarmPoolManager({
+ storage,
+ runtime,
+ target: 2,
+ createContainerId: nextId(["warm-c", "warm-d"]),
+ });
+
+ await pool.reset();
+ await pool.refresh();
+
+ expect(runtime.destroyed).toEqual(["releasing-a", "assigned-a", "warm-a", "warm-b"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ releasing: [],
+ warm: ["warm-c", "warm-d"],
+ });
+ });
+
+ test("does not replenish while cleanup is pending", async () => {
+ const runtime = createRuntime();
+ const storage = new MemoryStorage();
+ await storage.put("container-warm-pool", {
+ assignments: {},
+ releasing: ["stuck-a"],
+ warm: [],
+ });
+ runtime.destroyContainer = async () => {
+ throw new Error("destroy failed");
+ };
+ const pool = new ContainerWarmPoolManager({
+ storage,
+ runtime,
+ target: 2,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+
+ expect(runtime.started).toEqual([]);
+ expect(await pool.snapshot()).toMatchObject({
+ releasing: ["stuck-a"],
+ warm: [],
+ });
+ });
+
+ test("scales warm containers down to the target", async () => {
+ const runtime = createRuntime();
+ const storage = new MemoryStorage();
+ await storage.put("container-warm-pool", {
+ assignments: {},
+ releasing: [],
+ warm: ["warm-a", "warm-b", "warm-c", "warm-d"],
+ });
+ for (const containerId of ["warm-a", "warm-b", "warm-c", "warm-d"]) {
+ runtime.running.add(containerId);
+ }
+ const pool = new ContainerWarmPoolManager({
+ storage,
+ runtime,
+ target: 2,
+ createContainerId: nextId([]),
+ });
+
+ await pool.refresh();
+
+ expect(runtime.destroyed).toEqual(["warm-c", "warm-d"]);
+ expect(await pool.snapshot()).toMatchObject({
+ releasing: [],
+ warm: ["warm-a", "warm-b"],
+ });
+ });
+
+ test("cleans up failed warm starts without throwing from refresh", async () => {
+ const runtime = createRuntime();
+ runtime.startContainer = async (containerId) => {
+ runtime.started.push(containerId);
+ runtime.running.add(containerId);
+ throw new Error("start failed");
+ };
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 2,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await expect(pool.refresh()).resolves.toBeUndefined();
+
+ expect(runtime.started).toEqual(["warm-a"]);
+ expect(runtime.destroyed).toEqual(["warm-a"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ releasing: [],
+ warm: [],
+ });
+ });
+
+ test("tracks failed warm starts when cleanup also fails", async () => {
+ const runtime = createRuntime();
+ runtime.startContainer = async (containerId) => {
+ runtime.started.push(containerId);
+ runtime.running.add(containerId);
+ throw new Error("start failed");
+ };
+ runtime.destroyContainer = async () => {
+ throw new Error("destroy failed");
+ };
+ const pool = new ContainerWarmPoolManager({
+ storage: new MemoryStorage(),
+ runtime,
+ target: 2,
+ createContainerId: nextId(["warm-a", "warm-b"]),
+ });
+
+ await pool.refresh();
+
+ expect(runtime.started).toEqual(["warm-a"]);
+ expect(await pool.snapshot()).toMatchObject({
+ assignments: {},
+ releasing: ["warm-a"],
+ warm: [],
+ });
+ });
+});
+
+function createRuntime(): WarmPoolRuntime & {
+ destroyed: string[];
+ running: Set;
+ started: string[];
+} {
+ const running = new Set();
+ const started: string[] = [];
+ const destroyed: string[] = [];
+ return {
+ destroyed,
+ running,
+ started,
+ async startContainer(containerId) {
+ started.push(containerId);
+ running.add(containerId);
+ },
+ async destroyContainer(containerId) {
+ destroyed.push(containerId);
+ running.delete(containerId);
+ },
+ async isContainerRunning(containerId) {
+ return running.has(containerId);
+ },
+ async keepContainerAlive() {},
+ };
+}
+
+function nextId(ids: string[]): () => string {
+ let index = 0;
+ return () => {
+ const id = ids[index];
+ if (!id) throw new Error("out of ids");
+ index += 1;
+ return id;
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/container-pools.ts b/examples/think-compare-runtimes/worker/container-pools.ts
new file mode 100644
index 00000000..e2f85e4b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/container-pools.ts
@@ -0,0 +1,13 @@
+export { containerSleepAfter, containerSleepAfterMs } from "./container-config";
+export {
+ type ContainerWarmPoolHandle,
+ type ContainerWarmPoolNamespace,
+ getWarmPoolHandle,
+} from "./container-warm-pool";
+export { type SandboxPoolEnv, SandboxWarmPool } from "./sandbox-container-pool";
+export {
+ WorkspaceContainerHost,
+ type WorkspaceContainerHostHandle,
+ type WorkspacePoolEnv,
+ WorkspaceWarmPool,
+} from "./workspace-container-pool";
diff --git a/examples/think-compare-runtimes/worker/container-warm-pool.ts b/examples/think-compare-runtimes/worker/container-warm-pool.ts
new file mode 100644
index 00000000..4b7af566
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/container-warm-pool.ts
@@ -0,0 +1,84 @@
+import { DurableObject } from "cloudflare:workers";
+import {
+ type ContainerPoolConfigEnv,
+ warmPoolRefreshIntervalMs,
+ warmPoolTarget,
+} from "./container-config";
+import { ContainerWarmPoolManager, type WarmPoolRuntime } from "./container-pool-manager";
+
+const POOL_NAME = "default";
+
+export interface ContainerWarmPoolHandle {
+ getContainer(logicalId: string): Promise;
+ releaseContainer(logicalId: string): Promise;
+ refresh(): Promise;
+ reset(): Promise;
+}
+
+export type ContainerWarmPoolNamespace = DurableObjectNamespace;
+
+export abstract class ContainerWarmPool
+ extends DurableObject
+ implements ContainerWarmPoolHandle
+{
+ readonly #manager: ContainerWarmPoolManager;
+ readonly #refreshIntervalMs: number;
+
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env);
+ this.#manager = new ContainerWarmPoolManager({
+ storage: ctx.storage,
+ runtime: this.createRuntime(env),
+ target: warmPoolTarget(env),
+ });
+ this.#refreshIntervalMs = warmPoolRefreshIntervalMs(env);
+ ctx.blockConcurrencyWhile(async () => {
+ await this.#applyConfiguredReset();
+ await this.#scheduleRefresh();
+ });
+ }
+
+ protected abstract createRuntime(env: Env): WarmPoolRuntime;
+
+ async getContainer(logicalId: string): Promise {
+ return this.#manager.getContainer(logicalId);
+ }
+
+ async releaseContainer(logicalId: string): Promise {
+ await this.#manager.releaseContainer(logicalId);
+ this.ctx.waitUntil(this.#manager.refresh());
+ }
+
+ async refresh(): Promise {
+ await this.#applyConfiguredReset();
+ await this.#manager.refresh();
+ await this.#scheduleRefresh();
+ }
+
+ async reset(): Promise {
+ await this.#manager.reset();
+ }
+
+ async alarm(): Promise {
+ await this.refresh();
+ }
+
+ async #applyConfiguredReset(): Promise {
+ const resetKey = this.env.WARM_POOL_RESET_KEY;
+ if (!resetKey) return;
+
+ const appliedKey = await this.ctx.storage.get("container-warm-pool-reset-key");
+ if (appliedKey === resetKey) return;
+
+ await this.#manager.reset();
+ await this.ctx.storage.put("container-warm-pool-reset-key", resetKey);
+ }
+
+ async #scheduleRefresh(): Promise {
+ await this.ctx.storage.setAlarm(Date.now() + this.#refreshIntervalMs);
+ }
+}
+
+export function getWarmPoolHandle(namespace: ContainerWarmPoolNamespace): ContainerWarmPoolHandle {
+ return namespace.get(namespace.idFromName(POOL_NAME)) as unknown as ContainerWarmPoolHandle;
+}
diff --git a/examples/think-compare-runtimes/worker/http.test.ts b/examples/think-compare-runtimes/worker/http.test.ts
new file mode 100644
index 00000000..f5a5b27b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/http.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, test } from "vitest";
+import { handleApiRequest } from "./http";
+
+describe("handleApiRequest", () => {
+ test("starts a run from POST /api/runs", async () => {
+ const calls: string[] = [];
+ const response = await handleApiRequest(
+ new Request("https://example.com/api/runs", { method: "POST" }),
+ {
+ async startRun() {
+ calls.push("start");
+ return {
+ runId: "run-abc",
+ socketPath: "/parties/compare-run/run-abc",
+ events: [],
+ };
+ },
+ async stopRun() {
+ throw new Error("start route must not stop runs");
+ },
+ },
+ );
+
+ expect(calls).toEqual(["start"]);
+ expect(response).not.toBeNull();
+ expect(response?.status).toBe(201);
+ await expect(response?.json()).resolves.toMatchObject({
+ runId: "run-abc",
+ socketPath: "/parties/compare-run/run-abc",
+ });
+ });
+
+ test("stops a run from POST /api/runs/:runId/stop", async () => {
+ const calls: string[] = [];
+ const response = await handleApiRequest(
+ new Request("https://example.com/api/runs/run-abc/stop", { method: "POST" }),
+ {
+ async startRun() {
+ throw new Error("stop route must not start runs");
+ },
+ async stopRun(runId) {
+ calls.push(runId);
+ },
+ },
+ );
+
+ expect(calls).toEqual(["run-abc"]);
+ expect(response).not.toBeNull();
+ expect(response?.status).toBe(204);
+ });
+
+ test("returns null for non-API routes", async () => {
+ const response = await handleApiRequest(
+ new Request("https://example.com/parties/compare-run/run-abc"),
+ {
+ async startRun() {
+ throw new Error("non-API routes must not start runs");
+ },
+ async stopRun() {
+ throw new Error("non-API routes must not stop runs");
+ },
+ },
+ );
+
+ expect(response).toBeNull();
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/http.ts b/examples/think-compare-runtimes/worker/http.ts
new file mode 100644
index 00000000..2473d6bb
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/http.ts
@@ -0,0 +1,39 @@
+import type { RunSession } from "./runs";
+
+export interface ApiHandlers {
+ startRun(): Promise;
+ stopRun(runId: string): Promise;
+}
+
+export async function handleApiRequest(
+ request: Request,
+ handlers: ApiHandlers,
+): Promise {
+ const url = new URL(request.url);
+
+ if (url.pathname === "/api/runs") {
+ if (request.method !== "POST") {
+ return new Response("Method Not Allowed", {
+ status: 405,
+ headers: { Allow: "POST" },
+ });
+ }
+
+ return Response.json(await handlers.startRun(), { status: 201 });
+ }
+
+ const stopMatch = /^\/api\/runs\/([^/]+)\/stop$/.exec(url.pathname);
+ if (stopMatch) {
+ if (request.method !== "POST") {
+ return new Response("Method Not Allowed", {
+ status: 405,
+ headers: { Allow: "POST" },
+ });
+ }
+
+ await handlers.stopRun(decodeURIComponent(stopMatch[1] ?? ""));
+ return new Response(null, { status: 204 });
+ }
+
+ return null;
+}
diff --git a/examples/think-compare-runtimes/worker/index.ts b/examples/think-compare-runtimes/worker/index.ts
new file mode 100644
index 00000000..336a161c
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/index.ts
@@ -0,0 +1,193 @@
+import type { Sandbox as SandboxDO } from "@cloudflare/sandbox";
+import type { WorkerBackendOptions } from "@cloudflare/workspace/backends/worker";
+import { getServerByName, routePartykitRequest, Server } from "partyserver";
+import type { RunEvent } from "../shared/events";
+import { comparisonFixture } from "../shared/fixture";
+import { runComparisonAgents } from "./comparison-agents";
+import {
+ type ContainerWarmPoolNamespace,
+ getWarmPoolHandle,
+ SandboxWarmPool,
+ WorkspaceContainerHost,
+ WorkspaceWarmPool,
+} from "./container-pools";
+import { handleApiRequest } from "./http";
+import type { RunEventInput } from "./run-events";
+import { getRuntimeAgentHandles } from "./runtime-agent-handles";
+import { startComparisonRun } from "./start-run";
+import {
+ SandboxThinkAgent,
+ WorkspaceProxy,
+ WorkspaceServiceProxy,
+ WorkspaceThinkAgent,
+} from "./think/agents";
+
+export { Sandbox } from "@cloudflare/sandbox";
+export {
+ SandboxThinkAgent,
+ SandboxWarmPool,
+ WorkspaceContainerHost,
+ WorkspaceProxy,
+ WorkspaceServiceProxy,
+ WorkspaceThinkAgent,
+ WorkspaceWarmPool,
+};
+
+export interface Env {
+ AI: Ai;
+ CompareRun: DurableObjectNamespace;
+ LOADER: WorkerBackendOptions["loader"];
+ SANDBOX_TRANSPORT: "rpc";
+ CONTAINER_SLEEP_AFTER?: string;
+ WARM_POOL_REFRESH_INTERVAL?: string;
+ WARM_POOL_RESET_KEY?: string;
+ WARM_POOL_TARGET?: string;
+ FUSE_MOUNT?: string;
+ Sandbox: DurableObjectNamespace;
+ SandboxWarmPool: ContainerWarmPoolNamespace;
+ WorkspaceContainerHost: DurableObjectNamespace;
+ WorkspaceWarmPool: ContainerWarmPoolNamespace;
+ WorkspaceThinkAgent: DurableObjectNamespace;
+ SandboxThinkAgent: DurableObjectNamespace;
+}
+
+const EVENTS_KEY = "events";
+
+export class CompareRun extends Server {
+ static override options = { hibernate: true };
+
+ #events: RunEvent[] = [];
+ #appendQueue: Promise = Promise.resolve();
+ readonly #started: Promise;
+
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env);
+ this.#started = this.#loadEvents();
+ ctx.blockConcurrencyWhile(() => this.#started);
+ }
+
+ override async onStart(): Promise {
+ await this.#started;
+ }
+
+ override async fetch(request: Request): Promise {
+ const url = new URL(request.url);
+
+ if (url.pathname === "/health") {
+ return new Response("ok\n", {
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ });
+ }
+
+ return super.fetch(request);
+ }
+
+ override onConnect(connection: WebSocket): void {
+ connection.send(JSON.stringify({ type: "history", events: this.#events }));
+ }
+
+ async appendEvent(input: RunEventInput): Promise {
+ await this.#started;
+ const appended = this.#appendQueue.then(() => this.#appendEventNow(input));
+ this.#appendQueue = appended.catch(() => {});
+ return appended;
+ }
+
+ async startComparison(): Promise {
+ await this.#started;
+ const runId = this.name;
+ this.#events = [];
+ await this.ctx.storage.put(EVENTS_KEY, this.#events);
+ await this.appendEvent({
+ runtime: "both",
+ kind: "run_started",
+ title: "Comparison run started",
+ detail: "Workspace and Sandbox Think agents are running from the same fixture.",
+ });
+
+ this.ctx.waitUntil(this.#startAgents(runId));
+ return this.#events;
+ }
+
+ async stopComparison(): Promise {
+ await this.#started;
+ const { workspaceAgent, sandboxAgent } = await getRuntimeAgentHandles({
+ runId: this.name,
+ workspaceNamespace: this.env.WorkspaceThinkAgent,
+ sandboxNamespace: this.env.SandboxThinkAgent,
+ });
+ const agents = await Promise.all([workspaceAgent, sandboxAgent]);
+ await Promise.all(agents.map((agent) => agent.cancelComparison?.()));
+ }
+
+ async #startAgents(runId: string): Promise {
+ try {
+ const { workspaceAgent, sandboxAgent } = await getRuntimeAgentHandles({
+ runId,
+ workspaceNamespace: this.env.WorkspaceThinkAgent,
+ sandboxNamespace: this.env.SandboxThinkAgent,
+ });
+ await runComparisonAgents({
+ runId,
+ fixture: comparisonFixture,
+ workspaceAgent,
+ sandboxAgent,
+ appendEvent: async (input) => {
+ await this.appendEvent(input);
+ },
+ });
+ } catch (error) {
+ await this.appendEvent({
+ runtime: "both",
+ kind: "agent_tool_error",
+ title: "Comparison run failed",
+ detail: error instanceof Error ? error.message : String(error),
+ });
+ }
+ }
+
+ async #loadEvents(): Promise {
+ this.#events = (await this.ctx.storage.get(EVENTS_KEY)) ?? [];
+ }
+
+ async #appendEventNow(input: RunEventInput): Promise {
+ const sequence = this.#events.length;
+ const event: RunEvent = {
+ ...input,
+ id: `${this.name}:${sequence}`,
+ runId: this.name,
+ sequence,
+ timestamp: new Date().toISOString(),
+ };
+ this.#events = [...this.#events, event];
+ await this.ctx.storage.put(EVENTS_KEY, this.#events);
+ this.broadcast(JSON.stringify({ type: "event", event }));
+ return event;
+ }
+}
+
+export default {
+ async fetch(request, env) {
+ const apiResponse = await handleApiRequest(request, {
+ startRun: () =>
+ startComparisonRun({
+ getRun: (runId) => getServerByName(env.CompareRun, runId),
+ }),
+ async stopRun(runId) {
+ const run = (await getServerByName(env.CompareRun, runId)) as unknown as CompareRun;
+ await run.stopComparison();
+ },
+ });
+
+ if (apiResponse) {
+ return apiResponse;
+ }
+
+ return (await routePartykitRequest(request, env)) ?? new Response(null, { status: 404 });
+ },
+
+ async scheduled(_controller, env, ctx) {
+ ctx.waitUntil(getWarmPoolHandle(env.WorkspaceWarmPool).refresh());
+ ctx.waitUntil(getWarmPoolHandle(env.SandboxWarmPool).refresh());
+ },
+} satisfies ExportedHandler;
diff --git a/examples/think-compare-runtimes/worker/run-events.test.ts b/examples/think-compare-runtimes/worker/run-events.test.ts
new file mode 100644
index 00000000..27398ccc
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/run-events.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "./run-events";
+
+describe("RunEventRecorder", () => {
+ test("records ordered run events with generated IDs and timestamps", () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+
+ const first = recorder.record({
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "write /workspace/repo/package.json",
+ detail: "Writing fixture file",
+ });
+ const second = recorder.record({
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "write complete",
+ detail: "Wrote fixture file",
+ });
+
+ expect(first.sequence).toBe(0);
+ expect(second.sequence).toBe(1);
+ expect(recorder.events()).toEqual([
+ {
+ id: "run-abc:0",
+ runId: "run-abc",
+ sequence: 0,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "write /workspace/repo/package.json",
+ detail: "Writing fixture file",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ },
+ {
+ id: "run-abc:1",
+ runId: "run-abc",
+ sequence: 1,
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "write complete",
+ detail: "Wrote fixture file",
+ timestamp: "2026-06-04T00:00:00.000Z",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/run-events.ts b/examples/think-compare-runtimes/worker/run-events.ts
new file mode 100644
index 00000000..89a06e2f
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/run-events.ts
@@ -0,0 +1,54 @@
+import type { EventRuntime, RunEvent, RunEventKind } from "../shared/events";
+
+export interface RunEventInput {
+ runtime: EventRuntime;
+ kind: RunEventKind;
+ title: string;
+ detail: string;
+}
+
+export interface RunEventRecorderLike {
+ record(input: RunEventInput): RunEvent | Promise;
+}
+
+export interface RunEventRecorderOptions {
+ runId: string;
+ now?: () => string;
+ startSequence?: number;
+}
+
+export class RunEventRecorder {
+ readonly #runId: string;
+ readonly #now: () => string;
+ #nextSequence: number;
+ readonly #events: RunEvent[] = [];
+
+ constructor({
+ runId,
+ now = () => new Date().toISOString(),
+ startSequence = 0,
+ }: RunEventRecorderOptions) {
+ this.#runId = runId;
+ this.#now = now;
+ this.#nextSequence = startSequence;
+ }
+
+ record(input: RunEventInput): RunEvent {
+ const sequence = this.#nextSequence;
+ this.#nextSequence += 1;
+
+ const event: RunEvent = {
+ ...input,
+ id: `${this.#runId}:${sequence}`,
+ runId: this.#runId,
+ sequence,
+ timestamp: this.#now(),
+ };
+ this.#events.push(event);
+ return event;
+ }
+
+ events(): RunEvent[] {
+ return [...this.#events];
+ }
+}
diff --git a/examples/think-compare-runtimes/worker/runs.test.ts b/examples/think-compare-runtimes/worker/runs.test.ts
new file mode 100644
index 00000000..92a79ff6
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runs.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, test } from "vitest";
+import { createRunSession } from "./runs";
+
+describe("createRunSession", () => {
+ test("creates a run payload with a PartyServer socket path", () => {
+ const run = createRunSession(() => "abc123");
+
+ expect(run).toMatchObject({
+ runId: "abc123",
+ socketPath: "/parties/compare-run/abc123",
+ });
+ expect(run.events).toEqual([]);
+ });
+
+ test("creates a run ID from the runtime crypto object", () => {
+ const run = createRunSession();
+
+ expect(run.runId).toMatch(/^[0-9a-f-]{36}$/);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runs.ts b/examples/think-compare-runtimes/worker/runs.ts
new file mode 100644
index 00000000..6f67203e
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runs.ts
@@ -0,0 +1,17 @@
+import type { RunEvent } from "../shared/events";
+
+export interface RunSession {
+ runId: string;
+ socketPath: string;
+ events: RunEvent[];
+}
+
+export function createRunSession(createId = () => crypto.randomUUID()): RunSession {
+ const runId = createId();
+
+ return {
+ runId,
+ socketPath: `/parties/compare-run/${runId}`,
+ events: [],
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/runtime-agent-handles.test.ts b/examples/think-compare-runtimes/worker/runtime-agent-handles.test.ts
new file mode 100644
index 00000000..55d2903b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime-agent-handles.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, test, vi } from "vitest";
+
+vi.mock("agents", () => ({
+ getAgentByName: vi.fn(),
+}));
+
+import { getRuntimeAgentHandles } from "./runtime-agent-handles";
+
+describe("getRuntimeAgentHandles", () => {
+ test("requests Workspace and Sandbox agent handles concurrently", async () => {
+ const calls: string[] = [];
+ const workspace = deferred();
+ const sandbox = deferred();
+
+ const handles = getRuntimeAgentHandles({
+ runId: "run-abc",
+ workspaceNamespace: {} as DurableObjectNamespace,
+ sandboxNamespace: {} as DurableObjectNamespace,
+ getAgent(_namespace, name) {
+ calls.push(`start ${name}`);
+ if (name.endsWith("-workspace")) return workspace.promise;
+ if (name.endsWith("-sandbox")) return sandbox.promise;
+ throw new Error(`unexpected agent ${name}`);
+ },
+ });
+
+ await flushPromises();
+ expect(calls).toEqual(["start run-abc-workspace", "start run-abc-sandbox"]);
+
+ workspace.resolve({ runComparison: async () => {} });
+ sandbox.resolve({ runComparison: async () => {} });
+ await expect(handles.workspaceAgent).resolves.toEqual({ runComparison: expect.any(Function) });
+ await expect(handles.sandboxAgent).resolves.toEqual({ runComparison: expect.any(Function) });
+ });
+});
+
+async function flushPromises(): Promise {
+ await Promise.resolve();
+ await Promise.resolve();
+}
+
+function deferred() {
+ let resolve!: (value: T | PromiseLike) => void;
+ let reject!: (reason?: unknown) => void;
+ const promise = new Promise((promiseResolve, promiseReject) => {
+ resolve = promiseResolve;
+ reject = promiseReject;
+ });
+ return { promise, resolve, reject };
+}
diff --git a/examples/think-compare-runtimes/worker/runtime-agent-handles.ts b/examples/think-compare-runtimes/worker/runtime-agent-handles.ts
new file mode 100644
index 00000000..34c5b6d3
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime-agent-handles.ts
@@ -0,0 +1,43 @@
+import { getAgentByName } from "agents";
+import type { RuntimeThinkAgentHandle, RuntimeThinkAgentHandleInput } from "./think/agent-starter";
+
+export interface RuntimeAgentHandles {
+ workspaceAgent: RuntimeThinkAgentHandleInput;
+ sandboxAgent: RuntimeThinkAgentHandleInput;
+}
+
+export interface GetRuntimeAgentHandlesOptions {
+ runId: string;
+ workspaceNamespace: DurableObjectNamespace;
+ sandboxNamespace: DurableObjectNamespace;
+ getAgent?: (namespace: DurableObjectNamespace, name: string) => Promise;
+}
+
+export function getRuntimeAgentHandles({
+ runId,
+ workspaceNamespace,
+ sandboxNamespace,
+ getAgent = getRuntimeAgentByName,
+}: GetRuntimeAgentHandlesOptions): RuntimeAgentHandles {
+ return {
+ workspaceAgent: getAgent(
+ workspaceNamespace,
+ `${runId}-workspace`,
+ ) as Promise,
+ sandboxAgent: getAgent(
+ sandboxNamespace,
+ `${runId}-sandbox`,
+ ) as Promise,
+ };
+}
+
+async function getRuntimeAgentByName(
+ namespace: DurableObjectNamespace,
+ name: string,
+): Promise {
+ const getAgent = getAgentByName as unknown as (
+ namespace: DurableObjectNamespace,
+ name: string,
+ ) => Promise;
+ return getAgent(namespace, name);
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/adapter.test.ts b/examples/think-compare-runtimes/worker/runtime/adapter.test.ts
new file mode 100644
index 00000000..0d1aeb29
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/adapter.test.ts
@@ -0,0 +1,101 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import { createSandboxRuntimeAdapter, createWorkspaceRuntimeAdapter } from "./adapter";
+
+describe("runtime adapters", () => {
+ test("createWorkspaceRuntimeAdapter exposes runtime-neutral file tools", async () => {
+ const files = new Map([["/workspace/repo/src/index.ts", "workspace file"]]);
+ const recorder = new RunEventRecorder({ runId: "run-abc" });
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ workspace: {
+ async ready() {},
+ fs: {
+ async readFile(path: string, encoding: "utf8") {
+ expect(encoding).toBe("utf8");
+ return files.get(path) ?? "";
+ },
+ async writeFile(path: string, contents: string) {
+ files.set(path, contents);
+ },
+ },
+ shell: {
+ async exec(command: string) {
+ return {
+ async result() {
+ return { exitCode: 0, stdout: `${command}\n`, stderr: "", pushed: 0, pulled: 0 };
+ },
+ };
+ },
+ },
+ },
+ });
+
+ expect(adapter.runtime).toBe("workspace");
+ await expect(adapter.files.read("/workspace/repo/src/index.ts")).resolves.toBe(
+ "workspace file",
+ );
+ await adapter.files.write("/workspace/repo/src/created.ts", "created");
+ await expect(adapter.exec("node --version")).resolves.toEqual({
+ exitCode: 0,
+ stdout: "node --version\n",
+ stderr: "",
+ executionTarget: "workspace-container",
+ });
+ expect(files.get("/workspace/repo/src/created.ts")).toBe("created");
+ expect(recorder.events().map((event) => event.runtime)).toEqual([
+ "workspace",
+ "workspace",
+ "workspace",
+ "workspace",
+ "workspace",
+ "workspace",
+ ]);
+ });
+
+ test("createSandboxRuntimeAdapter exposes runtime-neutral file tools", async () => {
+ const files = new Map([["/workspace/repo/src/index.ts", "sandbox file"]]);
+ const recorder = new RunEventRecorder({ runId: "run-abc" });
+ const adapter = createSandboxRuntimeAdapter({
+ recorder,
+ sandbox: {
+ async readFile(path: string) {
+ return { content: files.get(path) ?? "" };
+ },
+ async writeFile(path: string, contents: string) {
+ files.set(path, contents);
+ },
+ async exec(command: string) {
+ return {
+ success: true,
+ exitCode: 0,
+ stdout: `${command}\n`,
+ stderr: "",
+ command,
+ duration: 1,
+ timestamp: "2026-06-04T00:00:00.000Z",
+ };
+ },
+ },
+ });
+
+ expect(adapter.runtime).toBe("sandbox");
+ await expect(adapter.files.read("/workspace/repo/src/index.ts")).resolves.toBe("sandbox file");
+ await adapter.files.write("/workspace/repo/src/created.ts", "created");
+ await expect(adapter.exec("node --version")).resolves.toEqual({
+ exitCode: 0,
+ stdout: "node --version\n",
+ stderr: "",
+ executionTarget: "sandbox-container",
+ });
+ expect(files.get("/workspace/repo/src/created.ts")).toBe("created");
+ expect(recorder.events().map((event) => event.runtime)).toEqual([
+ "sandbox",
+ "sandbox",
+ "sandbox",
+ "sandbox",
+ "sandbox",
+ "sandbox",
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runtime/adapter.ts b/examples/think-compare-runtimes/worker/runtime/adapter.ts
new file mode 100644
index 00000000..d13f59cc
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/adapter.ts
@@ -0,0 +1,86 @@
+import type { RuntimeId } from "../../shared/events";
+import type { RunEventRecorderLike } from "../run-events";
+import {
+ createRuntimeExecTool,
+ type RuntimeCommandRunner,
+ type RuntimeExecTool,
+} from "./exec-tools";
+import { createRuntimeFileTools, type RuntimeFileStore, type RuntimeFileTools } from "./file-tools";
+import { createSandboxCommandRunner, createSandboxFileStore } from "./sandbox";
+import { createWorkspaceCommandRunner, createWorkspaceFileStore } from "./workspace";
+
+export interface RuntimeAdapter {
+ runtime: RuntimeId;
+ files: RuntimeFileTools;
+ exec: RuntimeExecTool;
+}
+
+type WorkspaceRuntimeAdapterOptions = {
+ recorder: RunEventRecorderLike;
+} & (
+ | {
+ workspace: Parameters[0] &
+ Parameters[0];
+ store?: never;
+ runner?: never;
+ }
+ | { store: RuntimeFileStore; runner: RuntimeCommandRunner; workspace?: never }
+);
+
+type SandboxRuntimeAdapterOptions = {
+ recorder: RunEventRecorderLike;
+} & (
+ | {
+ sandbox: Parameters[0] &
+ Parameters[0];
+ store?: never;
+ runner?: never;
+ }
+ | { store: RuntimeFileStore; runner: RuntimeCommandRunner; sandbox?: never }
+);
+
+export function createWorkspaceRuntimeAdapter(
+ options: WorkspaceRuntimeAdapterOptions,
+): RuntimeAdapter {
+ const { recorder } = options;
+ const runtime = "workspace";
+
+ const store = options.store ?? createWorkspaceFileStore(options.workspace);
+ const runner = options.runner ?? createWorkspaceCommandRunner(options.workspace);
+
+ return {
+ runtime,
+ files: createRuntimeFileTools({
+ runtime,
+ recorder,
+ store,
+ }),
+ exec: createRuntimeExecTool({
+ runtime,
+ recorder,
+ runner,
+ }),
+ };
+}
+
+export function createSandboxRuntimeAdapter(options: SandboxRuntimeAdapterOptions): RuntimeAdapter {
+ const { recorder } = options;
+ const runtime = "sandbox";
+
+ const store = options.store ?? createSandboxFileStore(options.sandbox);
+ const runner = options.runner ?? createSandboxCommandRunner(options.sandbox);
+
+ return {
+ runtime,
+ files: createRuntimeFileTools({
+ runtime,
+ recorder,
+ store,
+ }),
+ exec: createRuntimeExecTool({
+ runtime,
+ recorder,
+ runner,
+ }),
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/comparison-run.test.ts b/examples/think-compare-runtimes/worker/runtime/comparison-run.test.ts
new file mode 100644
index 00000000..7979c765
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/comparison-run.test.ts
@@ -0,0 +1,129 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { runFixtureComparison } from "./comparison-run";
+
+describe("runFixtureComparison", () => {
+ test("records one ordered event stream for both runtime fixture setups", async () => {
+ const workspaceFiles = new Map();
+ const sandboxFiles = new Map();
+ const workspaceWrites: string[] = [];
+ const sandboxWrites: string[] = [];
+
+ const events = await runFixtureComparison({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ now: () => "2026-06-04T00:00:00.000Z",
+ workspaceRuntime: {
+ async mkdir() {},
+ async writeFile(path, contents) {
+ workspaceWrites.push(path);
+ workspaceFiles.set(path, contents);
+ },
+ },
+ sandboxRuntime: {
+ async mkdir() {},
+ async writeFile(path, contents) {
+ sandboxWrites.push(path);
+ sandboxFiles.set(path, contents);
+ },
+ },
+ workspaceAdapterStore: {
+ async readFile(path) {
+ return workspaceFiles.get(path) ?? "";
+ },
+ async writeFile(path, contents) {
+ workspaceFiles.set(path, contents);
+ },
+ },
+ sandboxAdapterStore: {
+ async readFile(path) {
+ return sandboxFiles.get(path) ?? "";
+ },
+ async writeFile(path, contents) {
+ sandboxFiles.set(path, contents);
+ },
+ },
+ workspaceCommandRunner: {
+ async exec(command, options) {
+ expect(options?.cwd).toBeUndefined();
+ return {
+ exitCode: 0,
+ stdout: `workspace ${command}\n`,
+ stderr: "",
+ executionTarget: "workspace-container",
+ };
+ },
+ },
+ sandboxCommandRunner: {
+ async exec(command, options) {
+ expect(options?.cwd).toBeUndefined();
+ return {
+ exitCode: 0,
+ stdout: `sandbox ${command}\n`,
+ stderr: "",
+ executionTarget: "sandbox-container",
+ };
+ },
+ },
+ });
+
+ expect(workspaceWrites).toEqual(expectedFixturePaths());
+ expect(sandboxWrites).toEqual(workspaceWrites);
+ expect(events.map((event) => event.sequence)).toEqual(
+ Array.from({ length: events.length }, (_, sequence) => sequence),
+ );
+ const fixtureSetupEventCount = 1 + 2 * perRuntimeFixtureSetupEventCount();
+ const scriptedTurnEventCount = 2 * (2 + 4 * 4);
+ expect(events).toHaveLength(fixtureSetupEventCount + scriptedTurnEventCount);
+ expect(events[0]).toMatchObject({
+ runtime: "both",
+ kind: "run_started",
+ title: "Comparison run started",
+ });
+ expect(events.map((event) => event.title)).toEqual(
+ expect.arrayContaining([
+ "Workspace fixture seeded",
+ "Sandbox fixture seeded",
+ "read /workspace/repo/feature-briefs/smart-request-policies.md",
+ "read complete",
+ "Scripted Think turn started",
+ "Think requested read",
+ "Think requested write",
+ "Think requested edit",
+ "Think requested exec",
+ "Scripted Think turn complete",
+ ]),
+ );
+ expect(events).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Scripted Think turn started",
+ }),
+ expect.objectContaining({
+ runtime: "sandbox",
+ kind: "agent_message",
+ title: "Scripted Think turn complete",
+ }),
+ ]),
+ );
+ });
+});
+
+function expectedFixturePaths(): string[] {
+ return comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+}
+
+function perRuntimeFixtureSetupEventCount(): number {
+ const rootMkdirEvents = 2;
+ const seededEvent = 1;
+ const files = comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+ const parentDirs = new Set(
+ files
+ .map((path) => path.slice(0, path.lastIndexOf("/")))
+ .filter((directory) => directory !== comparisonFixture.root),
+ );
+
+ return rootMkdirEvents + parentDirs.size * 2 + files.length * 2 + seededEvent;
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/comparison-run.ts b/examples/think-compare-runtimes/worker/runtime/comparison-run.ts
new file mode 100644
index 00000000..055e9b6f
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/comparison-run.ts
@@ -0,0 +1,90 @@
+import type { RunEvent } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { runScriptedThinkToolSmoke } from "../think/scripted-turn";
+import { createSandboxRuntimeAdapter, createWorkspaceRuntimeAdapter } from "./adapter";
+import type { RuntimeCommandRunner } from "./exec-tools";
+import type { RuntimeFileStore } from "./file-tools";
+import { runSandboxFixtureSetup } from "./sandbox-run";
+import type { FixtureRuntime } from "./seed";
+import { runWorkspaceFixtureSetup } from "./workspace-run";
+
+export interface FixtureComparisonOptions {
+ runId: string;
+ fixture: ComparisonFixture;
+ workspaceRuntime: FixtureRuntime;
+ sandboxRuntime: FixtureRuntime;
+ workspaceAdapterStore?: RuntimeFileStore;
+ sandboxAdapterStore?: RuntimeFileStore;
+ workspaceCommandRunner?: RuntimeCommandRunner;
+ sandboxCommandRunner?: RuntimeCommandRunner;
+ now?: () => string;
+}
+
+export async function runFixtureComparison({
+ runId,
+ fixture,
+ workspaceRuntime,
+ sandboxRuntime,
+ workspaceAdapterStore,
+ sandboxAdapterStore,
+ workspaceCommandRunner,
+ sandboxCommandRunner,
+ now = () => new Date().toISOString(),
+}: FixtureComparisonOptions): Promise {
+ const recorder = new RunEventRecorder({ runId, now });
+ recorder.record({
+ runtime: "both",
+ kind: "run_started",
+ title: "Comparison run started",
+ detail: "Workspace and Sandbox agents are queued from the same fixture.",
+ });
+
+ await Promise.all([
+ runWorkspaceFixtureSetup({
+ runId,
+ fixture,
+ runtime: workspaceRuntime,
+ recorder,
+ }),
+ runSandboxFixtureSetup({
+ runId,
+ fixture,
+ runtime: sandboxRuntime,
+ recorder,
+ }),
+ ]);
+
+ if (
+ workspaceAdapterStore &&
+ sandboxAdapterStore &&
+ workspaceCommandRunner &&
+ sandboxCommandRunner
+ ) {
+ const workspaceAdapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: workspaceAdapterStore,
+ runner: workspaceCommandRunner,
+ });
+ const sandboxAdapter = createSandboxRuntimeAdapter({
+ recorder,
+ store: sandboxAdapterStore,
+ runner: sandboxCommandRunner,
+ });
+
+ await Promise.all([
+ runScriptedThinkToolSmoke({
+ adapter: workspaceAdapter,
+ recorder,
+ root: fixture.root,
+ }),
+ runScriptedThinkToolSmoke({
+ adapter: sandboxAdapter,
+ recorder,
+ root: fixture.root,
+ }),
+ ]);
+ }
+
+ return recorder.events();
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/exec-tools.test.ts b/examples/think-compare-runtimes/worker/runtime/exec-tools.test.ts
new file mode 100644
index 00000000..4a31e94b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/exec-tools.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import { createRuntimeExecTool } from "./exec-tools";
+
+describe("createRuntimeExecTool", () => {
+ test("runs commands with runtime tool events", async () => {
+ const calls: Array<{ command: string; cwd?: string; timeoutMs?: number }> = [];
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const exec = createRuntimeExecTool({
+ runtime: "workspace",
+ recorder,
+ runner: {
+ async exec(command, options) {
+ calls.push({ command, cwd: options?.cwd, timeoutMs: options?.timeoutMs });
+ return {
+ exitCode: 0,
+ stdout: "ok\n",
+ stderr: "",
+ executionTarget: "workspace-container",
+ };
+ },
+ },
+ });
+
+ await expect(
+ exec("npm test -- --runInBand", { cwd: "/workspace/repo", timeoutMs: 30_000 }),
+ ).resolves.toEqual({
+ exitCode: 0,
+ stdout: "ok\n",
+ stderr: "",
+ executionTarget: "workspace-container",
+ });
+
+ expect(calls).toEqual([
+ { command: "npm test -- --runInBand", cwd: "/workspace/repo", timeoutMs: 30_000 },
+ ]);
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "exec npm test -- --runInBand",
+ detail: "Running command in /workspace/repo through workspace runtime.",
+ },
+ {
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "exec complete",
+ detail: "Exit 0; stdout 3 bytes; stderr 0 bytes.",
+ },
+ ]);
+ });
+
+ test("records tool errors when command startup fails", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const exec = createRuntimeExecTool({
+ runtime: "sandbox",
+ recorder,
+ runner: {
+ async exec() {
+ throw new Error("container unavailable");
+ },
+ },
+ });
+
+ await expect(exec("node --version")).rejects.toThrow("container unavailable");
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "sandbox",
+ kind: "tool_call",
+ title: "exec node --version",
+ detail: "Running command through sandbox runtime.",
+ },
+ {
+ runtime: "sandbox",
+ kind: "tool_error",
+ title: "exec failed",
+ detail: "container unavailable",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runtime/exec-tools.ts b/examples/think-compare-runtimes/worker/runtime/exec-tools.ts
new file mode 100644
index 00000000..e257be2b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/exec-tools.ts
@@ -0,0 +1,75 @@
+import type { ExecutionTarget, RuntimeId } from "../../shared/events";
+import type { RunEventRecorderLike } from "../run-events";
+
+export interface RuntimeExecOptions {
+ cwd?: string;
+ timeoutMs?: number;
+}
+
+export interface RuntimeExecResult {
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+}
+
+export interface RuntimeExecObservation extends RuntimeExecResult {
+ executionTarget: ExecutionTarget;
+}
+
+export interface RuntimeCommandRunner {
+ exec(command: string, options?: RuntimeExecOptions): Promise;
+}
+
+export type RuntimeExecTool = (
+ command: string,
+ options?: RuntimeExecOptions,
+) => Promise;
+
+export interface RuntimeExecToolOptions {
+ runtime: RuntimeId;
+ runner: RuntimeCommandRunner;
+ recorder: RunEventRecorderLike;
+}
+
+export function createRuntimeExecTool({
+ runtime,
+ runner,
+ recorder,
+}: RuntimeExecToolOptions): RuntimeExecTool {
+ return async (command, options) => {
+ await recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `exec ${command}`,
+ detail: execCallDetail(runtime, options),
+ });
+
+ try {
+ const result = await runner.exec(command, options);
+ await recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "exec complete",
+ detail: `Exit ${result.exitCode}; stdout ${byteLength(result.stdout)} bytes; stderr ${byteLength(result.stderr)} bytes.`,
+ });
+ return result;
+ } catch (error) {
+ await recorder.record({
+ runtime,
+ kind: "tool_error",
+ title: "exec failed",
+ detail: error instanceof Error ? error.message : String(error),
+ });
+ throw error;
+ }
+ };
+}
+
+function execCallDetail(runtime: RuntimeId, options: RuntimeExecOptions | undefined): string {
+ const location = options?.cwd ? ` in ${options.cwd}` : "";
+ return `Running command${location} through ${runtime} runtime.`;
+}
+
+function byteLength(contents: string): number {
+ return new TextEncoder().encode(contents).byteLength;
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/file-tools.test.ts b/examples/think-compare-runtimes/worker/runtime/file-tools.test.ts
new file mode 100644
index 00000000..b1ff58ca
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/file-tools.test.ts
@@ -0,0 +1,79 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import { createRuntimeFileTools } from "./file-tools";
+
+describe("createRuntimeFileTools", () => {
+ test("reads, writes, and edits files with runtime tool events", async () => {
+ const files = new Map([
+ ["/workspace/repo/src/index.ts", "export const value = 1;\n"],
+ ]);
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const tools = createRuntimeFileTools({
+ runtime: "workspace",
+ recorder,
+ store: {
+ async readFile(path) {
+ const contents = files.get(path);
+ if (contents === undefined) throw new Error(`missing ${path}`);
+ return contents;
+ },
+ async writeFile(path, contents) {
+ files.set(path, contents);
+ },
+ },
+ });
+
+ await expect(tools.read("/workspace/repo/src/index.ts")).resolves.toBe(
+ "export const value = 1;\n",
+ );
+ await tools.write("/workspace/repo/README.md", "hello\n");
+ await tools.edit("/workspace/repo/src/index.ts", [
+ { oldText: "value = 1", newText: "value = 2" },
+ ]);
+
+ expect(files.get("/workspace/repo/README.md")).toBe("hello\n");
+ expect(files.get("/workspace/repo/src/index.ts")).toBe("export const value = 2;\n");
+ expect(recorder.events().map(({ runtime, kind, title }) => ({ runtime, kind, title }))).toEqual(
+ [
+ { runtime: "workspace", kind: "tool_call", title: "read /workspace/repo/src/index.ts" },
+ { runtime: "workspace", kind: "tool_result", title: "read complete" },
+ { runtime: "workspace", kind: "tool_call", title: "write /workspace/repo/README.md" },
+ { runtime: "workspace", kind: "tool_result", title: "write complete" },
+ { runtime: "workspace", kind: "tool_call", title: "edit /workspace/repo/src/index.ts" },
+ { runtime: "workspace", kind: "tool_result", title: "edit complete" },
+ ],
+ );
+ });
+
+ test("records tool errors for non-unique edit replacements", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const tools = createRuntimeFileTools({
+ runtime: "sandbox",
+ recorder,
+ store: {
+ async readFile() {
+ return "repeat repeat";
+ },
+ async writeFile() {
+ throw new Error("write should not run");
+ },
+ },
+ });
+
+ await expect(
+ tools.edit("/workspace/repo/src/index.ts", [{ oldText: "repeat", newText: "once" }]),
+ ).rejects.toThrow("must match exactly once");
+ expect(recorder.events().map(({ runtime, kind, title }) => ({ runtime, kind, title }))).toEqual(
+ [
+ { runtime: "sandbox", kind: "tool_call", title: "edit /workspace/repo/src/index.ts" },
+ { runtime: "sandbox", kind: "tool_error", title: "edit failed" },
+ ],
+ );
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runtime/file-tools.ts b/examples/think-compare-runtimes/worker/runtime/file-tools.ts
new file mode 100644
index 00000000..1c993874
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/file-tools.ts
@@ -0,0 +1,133 @@
+import type { RuntimeId } from "../../shared/events";
+import type { RunEventRecorderLike } from "../run-events";
+
+export interface RuntimeFileStore {
+ readFile(path: string): Promise;
+ writeFile(path: string, contents: string): Promise;
+}
+
+export interface ExactEdit {
+ oldText: string;
+ newText: string;
+}
+
+export interface RuntimeFileTools {
+ read(path: string): Promise;
+ write(path: string, contents: string): Promise;
+ edit(path: string, edits: ExactEdit[]): Promise;
+}
+
+export interface RuntimeFileToolsOptions {
+ runtime: RuntimeId;
+ store: RuntimeFileStore;
+ recorder: RunEventRecorderLike;
+}
+
+export function createRuntimeFileTools({
+ runtime,
+ store,
+ recorder,
+}: RuntimeFileToolsOptions): RuntimeFileTools {
+ return {
+ async read(path) {
+ await recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `read ${path}`,
+ detail: `Reading file through ${runtime} runtime.`,
+ });
+ try {
+ const contents = await store.readFile(path);
+ await recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "read complete",
+ detail: `Read ${byteLength(contents)} bytes from ${path}.`,
+ });
+ return contents;
+ } catch (error) {
+ await recordToolError(recorder, runtime, "read failed", error);
+ throw error;
+ }
+ },
+
+ async write(path, contents) {
+ await recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `write ${path}`,
+ detail: `Writing ${byteLength(contents)} bytes through ${runtime} runtime.`,
+ });
+ try {
+ await store.writeFile(path, contents);
+ await recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "write complete",
+ detail: `Wrote ${path}.`,
+ });
+ } catch (error) {
+ await recordToolError(recorder, runtime, "write failed", error);
+ throw error;
+ }
+ },
+
+ async edit(path, edits) {
+ await recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `edit ${path}`,
+ detail: `Applying ${edits.length} exact replacement(s) through ${runtime} runtime.`,
+ });
+ try {
+ const contents = await store.readFile(path);
+ const updated = applyExactEdits(contents, edits);
+ await store.writeFile(path, updated);
+ await recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "edit complete",
+ detail: `Applied ${edits.length} replacement(s) to ${path}.`,
+ });
+ } catch (error) {
+ await recordToolError(recorder, runtime, "edit failed", error);
+ throw error;
+ }
+ },
+ };
+}
+
+function applyExactEdits(contents: string, edits: ExactEdit[]): string {
+ let updated = contents;
+
+ for (const edit of edits) {
+ const first = updated.indexOf(edit.oldText);
+ const last = updated.lastIndexOf(edit.oldText);
+
+ if (edit.oldText.length === 0 || first === -1 || first !== last) {
+ throw new Error(`oldText must match exactly once: ${JSON.stringify(edit.oldText)}`);
+ }
+
+ updated = `${updated.slice(0, first)}${edit.newText}${updated.slice(first + edit.oldText.length)}`;
+ }
+
+ return updated;
+}
+
+async function recordToolError(
+ recorder: RunEventRecorderLike,
+ runtime: RuntimeId,
+ title: string,
+ error: unknown,
+): Promise {
+ await recorder.record({
+ runtime,
+ kind: "tool_error",
+ title,
+ detail: error instanceof Error ? error.message : String(error),
+ });
+}
+
+function byteLength(contents: string): number {
+ return new TextEncoder().encode(contents).byteLength;
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/instrumented.test.ts b/examples/think-compare-runtimes/worker/runtime/instrumented.test.ts
new file mode 100644
index 00000000..0a9d9bc6
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/instrumented.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import { createInstrumentedFixtureRuntime } from "./instrumented";
+
+describe("createInstrumentedFixtureRuntime", () => {
+ test("records tool call and result events around runtime file operations", async () => {
+ const operations: string[] = [];
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const runtime = createInstrumentedFixtureRuntime({
+ runtime: "workspace",
+ inner: {
+ async mkdir(path) {
+ operations.push(`mkdir ${path}`);
+ },
+ async writeFile(path, contents) {
+ operations.push(`write ${path} ${contents}`);
+ },
+ },
+ recorder,
+ });
+
+ await runtime.mkdir("/workspace/repo");
+ await runtime.writeFile("/workspace/repo/package.json", "{}\n");
+
+ expect(operations).toEqual([
+ "mkdir /workspace/repo",
+ "write /workspace/repo/package.json {}\n",
+ ]);
+ expect(
+ recorder.events().map(({ sequence, runtime, kind, title, detail }) => ({
+ sequence,
+ runtime,
+ kind,
+ title,
+ detail,
+ })),
+ ).toEqual([
+ {
+ sequence: 0,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "mkdir /workspace/repo",
+ detail: "Creating directory through workspace runtime.",
+ },
+ {
+ sequence: 1,
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "mkdir complete",
+ detail: "Created /workspace/repo.",
+ },
+ {
+ sequence: 2,
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "write /workspace/repo/package.json",
+ detail: "Writing 3 bytes through workspace runtime.",
+ },
+ {
+ sequence: 3,
+ runtime: "workspace",
+ kind: "tool_result",
+ title: "write complete",
+ detail: "Wrote /workspace/repo/package.json.",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runtime/instrumented.ts b/examples/think-compare-runtimes/worker/runtime/instrumented.ts
new file mode 100644
index 00000000..78e56526
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/instrumented.ts
@@ -0,0 +1,48 @@
+import type { RuntimeId } from "../../shared/events";
+import type { RunEventRecorder } from "../run-events";
+import type { FixtureRuntime } from "./seed";
+
+export interface InstrumentedFixtureRuntimeOptions {
+ runtime: RuntimeId;
+ inner: FixtureRuntime;
+ recorder: RunEventRecorder;
+}
+
+export function createInstrumentedFixtureRuntime({
+ runtime,
+ inner,
+ recorder,
+}: InstrumentedFixtureRuntimeOptions): FixtureRuntime {
+ return {
+ async mkdir(path) {
+ recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `mkdir ${path}`,
+ detail: `Creating directory through ${runtime} runtime.`,
+ });
+ await inner.mkdir(path);
+ recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "mkdir complete",
+ detail: `Created ${path}.`,
+ });
+ },
+ async writeFile(path, contents) {
+ recorder.record({
+ runtime,
+ kind: "tool_call",
+ title: `write ${path}`,
+ detail: `Writing ${new TextEncoder().encode(contents).byteLength} bytes through ${runtime} runtime.`,
+ });
+ await inner.writeFile(path, contents);
+ recorder.record({
+ runtime,
+ kind: "tool_result",
+ title: "write complete",
+ detail: `Wrote ${path}.`,
+ });
+ },
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/sandbox-run.test.ts b/examples/think-compare-runtimes/worker/runtime/sandbox-run.test.ts
new file mode 100644
index 00000000..69421499
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/sandbox-run.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { runSandboxFixtureSetup } from "./sandbox-run";
+
+describe("runSandboxFixtureSetup", () => {
+ test("seeds the fixture and returns Sandbox timeline events", async () => {
+ const writes: string[] = [];
+
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+
+ const events = await runSandboxFixtureSetup({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ recorder,
+ runtime: {
+ async mkdir() {},
+ async writeFile(path) {
+ writes.push(path);
+ },
+ },
+ });
+
+ expect(writes).toEqual(expectedFixturePaths());
+ expect(
+ events.map(({ sequence, runtime, kind, title }) => ({
+ sequence,
+ runtime,
+ kind,
+ title,
+ })),
+ ).toEqual(expectedFixtureEventSummaries());
+ });
+});
+
+function expectedFixturePaths(): string[] {
+ return comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+}
+
+function expectedFixtureEventSummaries(): Array<{
+ sequence: number;
+ runtime: "sandbox";
+ kind: "tool_call" | "tool_result" | "runtime_note";
+ title: string;
+}> {
+ const files = comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+ const parentDirs = [
+ ...new Set(
+ files
+ .map((path) => path.slice(0, path.lastIndexOf("/")))
+ .filter((directory) => directory !== comparisonFixture.root),
+ ),
+ ];
+ const summaries = [
+ { runtime: "sandbox" as const, kind: "tool_call" as const, title: "mkdir /workspace/repo" },
+ { runtime: "sandbox" as const, kind: "tool_result" as const, title: "mkdir complete" },
+ ...parentDirs.map((directory) => ({
+ runtime: "sandbox" as const,
+ kind: "tool_call" as const,
+ title: `mkdir ${directory}`,
+ })),
+ ...parentDirs.map(() => ({
+ runtime: "sandbox" as const,
+ kind: "tool_result" as const,
+ title: "mkdir complete",
+ })),
+ ...files.map((path) => ({
+ runtime: "sandbox" as const,
+ kind: "tool_call" as const,
+ title: `write ${path}`,
+ })),
+ ...files.map(() => ({
+ runtime: "sandbox" as const,
+ kind: "tool_result" as const,
+ title: "write complete",
+ })),
+ {
+ runtime: "sandbox" as const,
+ kind: "runtime_note" as const,
+ title: "Sandbox fixture seeded",
+ },
+ ];
+ return summaries.map((summary, sequence) => ({ sequence, ...summary }));
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/sandbox-run.ts b/examples/think-compare-runtimes/worker/runtime/sandbox-run.ts
new file mode 100644
index 00000000..60aee4de
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/sandbox-run.ts
@@ -0,0 +1,41 @@
+import type { RunEvent } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { createInstrumentedFixtureRuntime } from "./instrumented";
+import { type FixtureRuntime, seedFixture } from "./seed";
+
+export interface SandboxFixtureSetupOptions {
+ runId: string;
+ fixture: ComparisonFixture;
+ runtime: FixtureRuntime;
+ recorder?: RunEventRecorder;
+ now?: () => string;
+}
+
+export async function runSandboxFixtureSetup({
+ runId,
+ fixture,
+ runtime,
+ recorder,
+ now = () => new Date().toISOString(),
+}: SandboxFixtureSetupOptions): Promise {
+ const eventRecorder = recorder ?? new RunEventRecorder({ runId, now });
+ const startIndex = eventRecorder.events().length;
+
+ await seedFixture(
+ createInstrumentedFixtureRuntime({
+ runtime: "sandbox",
+ inner: runtime,
+ recorder: eventRecorder,
+ }),
+ fixture,
+ );
+ eventRecorder.record({
+ runtime: "sandbox",
+ kind: "runtime_note",
+ title: "Sandbox fixture seeded",
+ detail: `Wrote ${fixture.files.length} files through Sandbox SDK file operations at ${fixture.root}.`,
+ });
+
+ return eventRecorder.events().slice(startIndex);
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/sandbox.test.ts b/examples/think-compare-runtimes/worker/runtime/sandbox.test.ts
new file mode 100644
index 00000000..7090e401
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/sandbox.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import {
+ createSandboxCommandRunner,
+ createSandboxFileStore,
+ createSandboxFixtureRuntime,
+} from "./sandbox";
+import { seedFixture } from "./seed";
+
+describe("createSandboxFixtureRuntime", () => {
+ test("seeds through Sandbox file operations", async () => {
+ const calls: Array<{ type: "mkdir" | "write"; path: string; contents?: string }> = [];
+ const sandbox = {
+ async mkdir(path: string, options?: { recursive?: boolean }) {
+ if (options?.recursive !== true) {
+ throw new Error("Sandbox fixture mkdir must be recursive");
+ }
+ calls.push({ type: "mkdir", path });
+ },
+ async writeFile(path: string, contents: string) {
+ calls.push({ type: "write", path, contents });
+ },
+ };
+
+ await seedFixture(createSandboxFixtureRuntime(sandbox), comparisonFixture);
+
+ expect(calls).toEqual(expectedSeedCalls());
+ });
+
+ test("adapts Sandbox SDK files to the text file store interface", async () => {
+ const calls: string[] = [];
+ const sandbox = {
+ async readFile(path: string) {
+ calls.push(`read ${path}`);
+ return { content: "contents" };
+ },
+ async writeFile(path: string, contents: string) {
+ calls.push(`write ${path} ${contents}`);
+ },
+ };
+ const store = createSandboxFileStore(sandbox);
+
+ await expect(store.readFile("/workspace/repo/src/index.ts")).resolves.toBe("contents");
+ await store.writeFile("/workspace/repo/src/index.ts", "updated");
+
+ expect(calls).toEqual([
+ "read /workspace/repo/src/index.ts",
+ "write /workspace/repo/src/index.ts updated",
+ ]);
+ });
+
+ test("exec adapts Sandbox SDK command results", async () => {
+ const calls: string[] = [];
+ const runner = createSandboxCommandRunner({
+ async exec(command: string, options?: { cwd?: string; timeout?: number }) {
+ calls.push(`${command} ${options?.cwd} ${options?.timeout}`);
+ return {
+ success: true,
+ exitCode: 0,
+ stdout: "sandbox\n",
+ stderr: "",
+ command,
+ duration: 12,
+ timestamp: "2026-06-04T00:00:00.000Z",
+ };
+ },
+ });
+
+ await expect(
+ runner.exec("npm test", { cwd: "/workspace/repo", timeoutMs: 30_000 }),
+ ).resolves.toEqual({
+ exitCode: 0,
+ stdout: "sandbox\n",
+ stderr: "",
+ executionTarget: "sandbox-container",
+ });
+ expect(calls).toEqual(["npm test /workspace/repo 30000"]);
+ });
+});
+
+function expectedSeedCalls(): Array<{ type: "mkdir" | "write"; path: string; contents?: string }> {
+ const files = comparisonFixture.files.map((file) => ({
+ ...file,
+ path: `${comparisonFixture.root}/${file.path}`,
+ }));
+ const parentDirs = [
+ ...new Set(
+ files
+ .map((file) => file.path.slice(0, file.path.lastIndexOf("/")))
+ .filter((directory) => directory !== comparisonFixture.root),
+ ),
+ ];
+
+ return [
+ { type: "mkdir", path: comparisonFixture.root },
+ ...parentDirs.map((directory) => ({ type: "mkdir" as const, path: directory })),
+ ...files.map((file) => ({ type: "write" as const, path: file.path, contents: file.contents })),
+ ];
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/sandbox.ts b/examples/think-compare-runtimes/worker/runtime/sandbox.ts
new file mode 100644
index 00000000..0cec9342
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/sandbox.ts
@@ -0,0 +1,68 @@
+import type { RuntimeCommandRunner, RuntimeExecOptions, RuntimeExecResult } from "./exec-tools";
+import type { RuntimeFileStore } from "./file-tools";
+import type { FixtureRuntime } from "./seed";
+
+interface SandboxFixtureTarget {
+ mkdir(path: string, options?: { recursive?: boolean }): Promise;
+ writeFile(path: string, contents: string): Promise;
+}
+
+interface SandboxReadFileResult {
+ content: string | Uint8Array;
+}
+
+interface SandboxFileStoreTarget {
+ readFile(path: string): Promise;
+ writeFile(path: string, contents: string): Promise;
+}
+
+interface SandboxCommandTarget {
+ exec(command: string, options?: { cwd?: string; timeout?: number }): Promise;
+}
+
+export function createSandboxFixtureRuntime(sandbox: SandboxFixtureTarget): FixtureRuntime {
+ return {
+ async mkdir(path) {
+ await sandbox.mkdir(path, { recursive: true });
+ },
+ async writeFile(path, contents) {
+ await sandbox.writeFile(path, contents);
+ },
+ };
+}
+
+export function createSandboxFileStore(sandbox: SandboxFileStoreTarget): RuntimeFileStore {
+ return {
+ async readFile(path) {
+ const file = await sandbox.readFile(path);
+ return typeof file.content === "string"
+ ? file.content
+ : new TextDecoder().decode(file.content);
+ },
+ async writeFile(path, contents) {
+ await sandbox.writeFile(path, contents);
+ },
+ };
+}
+
+export function createSandboxCommandRunner(sandbox: SandboxCommandTarget): RuntimeCommandRunner {
+ return {
+ async exec(command, options) {
+ const { exitCode, stdout, stderr } = await sandbox.exec(
+ command,
+ toSandboxExecOptions(options),
+ );
+ return { exitCode, stdout, stderr, executionTarget: "sandbox-container" };
+ },
+ };
+}
+
+function toSandboxExecOptions(options: RuntimeExecOptions | undefined): {
+ cwd?: string;
+ timeout?: number;
+} {
+ return {
+ cwd: options?.cwd,
+ timeout: options?.timeoutMs,
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/seed.test.ts b/examples/think-compare-runtimes/worker/runtime/seed.test.ts
new file mode 100644
index 00000000..613d3950
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/seed.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from "vitest";
+import type { ComparisonFixture } from "../../shared/fixture";
+import { seedFixture } from "./seed";
+
+describe("seedFixture", () => {
+ test("creates parent directories and writes fixture files under the root", async () => {
+ const calls: string[] = [];
+ const fixture: ComparisonFixture = {
+ root: "/workspace/repo",
+ task: "Test task",
+ files: [
+ { path: "package.json", contents: "{}\n" },
+ { path: "src/index.ts", contents: "export {};\n" },
+ ],
+ };
+
+ await seedFixture(
+ {
+ async mkdir(path) {
+ calls.push(`mkdir ${path}`);
+ },
+ async writeFile(path, contents) {
+ calls.push(`write ${path} ${contents.length}`);
+ },
+ },
+ fixture,
+ );
+
+ expect(calls[0]).toBe("mkdir /workspace/repo");
+ expect(calls.slice(1).sort()).toEqual([
+ "mkdir /workspace/repo/src",
+ "write /workspace/repo/package.json 3",
+ "write /workspace/repo/src/index.ts 11",
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/runtime/seed.ts b/examples/think-compare-runtimes/worker/runtime/seed.ts
new file mode 100644
index 00000000..409a3340
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/seed.ts
@@ -0,0 +1,39 @@
+import type { ComparisonFixture } from "../../shared/fixture";
+
+export interface FixtureRuntime {
+ mkdir(path: string): Promise;
+ writeFile(path: string, contents: string): Promise;
+}
+
+export async function seedFixture(
+ runtime: FixtureRuntime,
+ fixture: ComparisonFixture,
+): Promise {
+ await runtime.mkdir(fixture.root);
+
+ const files = fixture.files.map((file) => ({
+ ...file,
+ absolutePath: joinPath(fixture.root, file.path),
+ }));
+ const parentDirs = new Set(
+ files.map((file) => dirname(file.absolutePath)).filter((dir) => dir !== fixture.root),
+ );
+
+ await Promise.all([...parentDirs].map((dir) => runtime.mkdir(dir)));
+ await Promise.all(files.map((file) => runtime.writeFile(file.absolutePath, file.contents)));
+}
+
+function joinPath(root: string, path: string): string {
+ return `${root.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
+}
+
+function dirname(path: string): string {
+ const normalized = path.replace(/\/+$/, "");
+ const lastSlash = normalized.lastIndexOf("/");
+
+ if (lastSlash <= 0) {
+ return "/";
+ }
+
+ return normalized.slice(0, lastSlash);
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/workspace-run.test.ts b/examples/think-compare-runtimes/worker/runtime/workspace-run.test.ts
new file mode 100644
index 00000000..0a31ff65
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/workspace-run.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { runWorkspaceFixtureSetup } from "./workspace-run";
+
+describe("runWorkspaceFixtureSetup", () => {
+ test("seeds the fixture and returns Workspace timeline events", async () => {
+ const writes: string[] = [];
+
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+
+ const events = await runWorkspaceFixtureSetup({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ recorder,
+ runtime: {
+ async mkdir() {},
+ async writeFile(path) {
+ writes.push(path);
+ },
+ },
+ });
+
+ expect(writes).toEqual(expectedFixturePaths());
+ expect(
+ events.map(({ sequence, runtime, kind, title }) => ({
+ sequence,
+ runtime,
+ kind,
+ title,
+ })),
+ ).toEqual(expectedFixtureEventSummaries());
+ });
+});
+
+function expectedFixturePaths(): string[] {
+ return comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+}
+
+function expectedFixtureEventSummaries(): Array<{
+ sequence: number;
+ runtime: "workspace";
+ kind: "tool_call" | "tool_result" | "runtime_note";
+ title: string;
+}> {
+ const files = comparisonFixture.files.map((file) => `${comparisonFixture.root}/${file.path}`);
+ const parentDirs = [
+ ...new Set(
+ files
+ .map((path) => path.slice(0, path.lastIndexOf("/")))
+ .filter((directory) => directory !== comparisonFixture.root),
+ ),
+ ];
+ const summaries = [
+ { runtime: "workspace" as const, kind: "tool_call" as const, title: "mkdir /workspace/repo" },
+ { runtime: "workspace" as const, kind: "tool_result" as const, title: "mkdir complete" },
+ ...parentDirs.map((directory) => ({
+ runtime: "workspace" as const,
+ kind: "tool_call" as const,
+ title: `mkdir ${directory}`,
+ })),
+ ...parentDirs.map(() => ({
+ runtime: "workspace" as const,
+ kind: "tool_result" as const,
+ title: "mkdir complete",
+ })),
+ ...files.map((path) => ({
+ runtime: "workspace" as const,
+ kind: "tool_call" as const,
+ title: `write ${path}`,
+ })),
+ ...files.map(() => ({
+ runtime: "workspace" as const,
+ kind: "tool_result" as const,
+ title: "write complete",
+ })),
+ {
+ runtime: "workspace" as const,
+ kind: "runtime_note" as const,
+ title: "Workspace fixture seeded",
+ },
+ ];
+ return summaries.map((summary, sequence) => ({ sequence, ...summary }));
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/workspace-run.ts b/examples/think-compare-runtimes/worker/runtime/workspace-run.ts
new file mode 100644
index 00000000..20a7d2aa
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/workspace-run.ts
@@ -0,0 +1,41 @@
+import type { RunEvent } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { createInstrumentedFixtureRuntime } from "./instrumented";
+import { type FixtureRuntime, seedFixture } from "./seed";
+
+export interface WorkspaceFixtureSetupOptions {
+ runId: string;
+ fixture: ComparisonFixture;
+ runtime: FixtureRuntime;
+ recorder?: RunEventRecorder;
+ now?: () => string;
+}
+
+export async function runWorkspaceFixtureSetup({
+ runId,
+ fixture,
+ runtime,
+ recorder,
+ now = () => new Date().toISOString(),
+}: WorkspaceFixtureSetupOptions): Promise {
+ const eventRecorder = recorder ?? new RunEventRecorder({ runId, now });
+ const startIndex = eventRecorder.events().length;
+
+ await seedFixture(
+ createInstrumentedFixtureRuntime({
+ runtime: "workspace",
+ inner: runtime,
+ recorder: eventRecorder,
+ }),
+ fixture,
+ );
+ eventRecorder.record({
+ runtime: "workspace",
+ kind: "runtime_note",
+ title: "Workspace fixture seeded",
+ detail: `Wrote ${fixture.files.length} files through Workspace.fs at ${fixture.root} before starting a shell container.`,
+ });
+
+ return eventRecorder.events().slice(startIndex);
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/workspace.test.ts b/examples/think-compare-runtimes/worker/runtime/workspace.test.ts
new file mode 100644
index 00000000..d6a76213
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/workspace.test.ts
@@ -0,0 +1,191 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { seedFixture } from "./seed";
+import {
+ createWorkspaceCommandRunner,
+ createWorkspaceFileStore,
+ createWorkspaceFixtureRuntime,
+} from "./workspace";
+
+describe("createWorkspaceFixtureRuntime", () => {
+ test("seeds through Workspace.fs without connecting a shell backend", async () => {
+ const calls: Array<{ type: "mkdir" | "write"; path: string; contents?: string }> = [];
+ const workspace = {
+ fs: {
+ async mkdir(path: string, options?: { recursive?: boolean }) {
+ if (options?.recursive !== true) {
+ throw new Error("Workspace fixture mkdir must be recursive");
+ }
+ calls.push({ type: "mkdir", path });
+ },
+ async writeFile(path: string, contents: string) {
+ calls.push({ type: "write", path, contents });
+ },
+ },
+ async ready() {
+ throw new Error("ready() should not be needed for file seeding");
+ },
+ };
+
+ await seedFixture(createWorkspaceFixtureRuntime(workspace), comparisonFixture);
+
+ expect(calls).toEqual(expectedSeedCalls());
+ });
+
+ test("adapts Workspace.fs to the text file store interface", async () => {
+ const calls: string[] = [];
+ const workspace = {
+ fs: {
+ async readFile(path: string, encoding: "utf8") {
+ calls.push(`read ${path} ${encoding}`);
+ return "contents";
+ },
+ async writeFile(path: string, contents: string) {
+ calls.push(`write ${path} ${contents}`);
+ },
+ },
+ };
+ const store = createWorkspaceFileStore(workspace);
+
+ await expect(store.readFile("/workspace/repo/src/index.ts")).resolves.toBe("contents");
+ await store.writeFile("/workspace/repo/src/index.ts", "updated");
+
+ expect(calls).toEqual([
+ "read /workspace/repo/src/index.ts utf8",
+ "write /workspace/repo/src/index.ts updated",
+ ]);
+ });
+
+ test("exec routes package commands to the Workspace container backend", async () => {
+ const calls: string[] = [];
+ const runner = createWorkspaceCommandRunner({
+ async ready(backend?: string) {
+ calls.push(`ready ${backend ?? "default"}`);
+ },
+ shell: {
+ async exec(
+ command: string,
+ options?: { backend?: string; cwd?: string; encoding?: "utf8"; timeoutMs?: number },
+ ) {
+ calls.push(
+ `${command} ${options?.backend} ${options?.cwd} ${options?.encoding} ${options?.timeoutMs}`,
+ );
+ return {
+ async result() {
+ calls.push("result");
+ return { exitCode: 0, stdout: "workspace\n", stderr: "", pushed: 1, pulled: 1 };
+ },
+ };
+ },
+ },
+ });
+
+ await expect(
+ runner.exec("npm run check", { cwd: "/workspace/repo", timeoutMs: 30_000 }),
+ ).resolves.toEqual({
+ exitCode: 0,
+ stdout: "workspace\n",
+ stderr: "",
+ executionTarget: "workspace-container",
+ });
+ expect(calls).toEqual([
+ "ready container",
+ "npm run check container /workspace/repo utf8 30000",
+ "result",
+ ]);
+ });
+
+ test.each([
+ "grep -R node docs",
+ "cat package.json | grep npm",
+ "find . -name package.json",
+ "ls docs/workers",
+ "pwd",
+ ])("exec keeps generic Workspace inspection on the worker shell backend: %s", async (command) => {
+ const calls: string[] = [];
+ const runner = createWorkspaceCommandRunner({
+ async ready(backend?: string) {
+ calls.push(`ready ${backend ?? "default"}`);
+ },
+ shell: {
+ async exec(
+ actualCommand: string,
+ options?: { backend?: string; cwd?: string; encoding?: "utf8" },
+ ) {
+ calls.push(`${actualCommand} ${options?.backend} ${options?.cwd} ${options?.encoding}`);
+ return {
+ async result() {
+ return { exitCode: 0, stdout: "workspace\n", stderr: "", pushed: 0, pulled: 0 };
+ },
+ };
+ },
+ },
+ });
+
+ await expect(runner.exec(command, { cwd: "/workspace/repo" })).resolves.toEqual({
+ exitCode: 0,
+ stdout: "workspace\n",
+ stderr: "",
+ executionTarget: "worker-shell",
+ });
+
+ expect(calls).toEqual(["ready shell", `${command} shell /workspace/repo utf8`]);
+ });
+
+ test.each([
+ "npm run check",
+ "node scripts/check-docs.mjs",
+ "npx vitest",
+ "tsc --noEmit",
+ "./scripts/check-docs.mjs",
+ ])("exec routes runtime and package commands to the Workspace container backend: %s", async (command) => {
+ const calls: string[] = [];
+ const runner = createWorkspaceCommandRunner({
+ async ready(backend?: string) {
+ calls.push(`ready ${backend ?? "default"}`);
+ },
+ shell: {
+ async exec(
+ actualCommand: string,
+ options?: { backend?: string; cwd?: string; encoding?: "utf8" },
+ ) {
+ calls.push(`${actualCommand} ${options?.backend} ${options?.cwd} ${options?.encoding}`);
+ return {
+ async result() {
+ return { exitCode: 0, stdout: "workspace\n", stderr: "", pushed: 1, pulled: 1 };
+ },
+ };
+ },
+ },
+ });
+
+ await expect(runner.exec(command, { cwd: "/workspace/repo" })).resolves.toMatchObject({
+ exitCode: 0,
+ stdout: "workspace\n",
+ stderr: "",
+ executionTarget: "workspace-container",
+ });
+
+ expect(calls).toEqual(["ready container", `${command} container /workspace/repo utf8`]);
+ });
+});
+
+function expectedSeedCalls(): Array<{ type: "mkdir" | "write"; path: string; contents?: string }> {
+ const files = comparisonFixture.files.map((file) => ({
+ ...file,
+ path: `${comparisonFixture.root}/${file.path}`,
+ }));
+ const parentDirs = [
+ ...new Set(
+ files
+ .map((file) => file.path.slice(0, file.path.lastIndexOf("/")))
+ .filter((directory) => directory !== comparisonFixture.root),
+ ),
+ ];
+
+ return [
+ { type: "mkdir", path: comparisonFixture.root },
+ ...parentDirs.map((directory) => ({ type: "mkdir" as const, path: directory })),
+ ...files.map((file) => ({ type: "write" as const, path: file.path, contents: file.contents })),
+ ];
+}
diff --git a/examples/think-compare-runtimes/worker/runtime/workspace.ts b/examples/think-compare-runtimes/worker/runtime/workspace.ts
new file mode 100644
index 00000000..47527d37
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/runtime/workspace.ts
@@ -0,0 +1,215 @@
+import type { RuntimeCommandRunner, RuntimeExecOptions, RuntimeExecResult } from "./exec-tools";
+import type { RuntimeFileStore } from "./file-tools";
+import type { FixtureRuntime } from "./seed";
+
+interface WorkspaceFixtureTarget {
+ fs: {
+ mkdir(path: string, options?: { recursive?: boolean }): Promise;
+ writeFile(path: string, contents: string): Promise;
+ };
+}
+
+interface WorkspaceFileStoreTarget {
+ fs: {
+ readFile(path: string, encoding: "utf8"): Promise;
+ writeFile(path: string, contents: string): Promise;
+ };
+}
+
+interface WorkspaceCommandTarget {
+ ready(backend?: string): Promise;
+ shell: {
+ exec(
+ command: string,
+ options: { backend?: string; cwd?: string; encoding: "utf8"; timeoutMs?: number },
+ ): Promise<{
+ result(): Promise;
+ }>;
+ };
+}
+
+export function createWorkspaceFixtureRuntime(workspace: WorkspaceFixtureTarget): FixtureRuntime {
+ return {
+ mkdir(path) {
+ return workspace.fs.mkdir(path, { recursive: true });
+ },
+ writeFile(path, contents) {
+ return workspace.fs.writeFile(path, contents);
+ },
+ };
+}
+
+export function createWorkspaceFileStore(workspace: WorkspaceFileStoreTarget): RuntimeFileStore {
+ return {
+ readFile(path) {
+ return workspace.fs.readFile(path, "utf8");
+ },
+ writeFile(path, contents) {
+ return workspace.fs.writeFile(path, contents);
+ },
+ };
+}
+
+export function createWorkspaceCommandRunner(
+ workspace: WorkspaceCommandTarget,
+): RuntimeCommandRunner {
+ return {
+ async exec(command, options) {
+ const backend = workspaceBackendForCommand(command);
+ await workspace.ready(backend);
+ const handle = await workspace.shell.exec(command, toWorkspaceExecOptions(options, backend));
+ const { exitCode, stdout, stderr } = await handle.result();
+ return { exitCode, stdout, stderr, executionTarget: workspaceExecutionTarget(backend) };
+ },
+ };
+}
+
+function toWorkspaceExecOptions(
+ options: RuntimeExecOptions | undefined,
+ backend: string,
+): {
+ backend: string;
+ cwd?: string;
+ encoding: "utf8";
+ timeoutMs?: number;
+} {
+ return {
+ backend,
+ cwd: options?.cwd,
+ encoding: "utf8",
+ timeoutMs: options?.timeoutMs,
+ };
+}
+
+const workerShellCommands = new Set([
+ "cat",
+ "cd",
+ "echo",
+ "find",
+ "grep",
+ "head",
+ "ls",
+ "pwd",
+ "sed",
+ "tail",
+ "test",
+ "true",
+ "wc",
+]);
+
+const containerCommands = new Set(["node", "npm", "npx", "pnpm", "tsc", "vitest", "yarn"]);
+
+function workspaceBackendForCommand(command: string): "container" | "shell" {
+ const executables = shellExecutables(command);
+ if (executables.length === 0) return "shell";
+ return executables.every((executable) => workerShellCommands.has(executable))
+ ? "shell"
+ : "container";
+}
+
+function shellExecutables(command: string): string[] {
+ return shellSegments(command).flatMap((segment) => {
+ const executable = firstExecutable(segment);
+ return executable ? [executable] : [];
+ });
+}
+
+function shellSegments(command: string): string[] {
+ const segments: string[] = [];
+ let current = "";
+ let quote: "'" | '"' | "`" | null = null;
+ let escaped = false;
+
+ for (let index = 0; index < command.length; index++) {
+ const char = command[index];
+ if (escaped) {
+ current += char;
+ escaped = false;
+ continue;
+ }
+ if (char === "\\") {
+ current += char;
+ escaped = true;
+ continue;
+ }
+ if (quote) {
+ current += char;
+ if (char === quote) quote = null;
+ continue;
+ }
+ if (char === "'" || char === '"' || char === "`") {
+ current += char;
+ quote = char;
+ continue;
+ }
+ if (char === ";" || char === "|" || char === "&") {
+ const trimmed = current.trim();
+ if (trimmed) segments.push(trimmed);
+ current = "";
+ if ((char === "|" || char === "&") && command[index + 1] === char) index++;
+ continue;
+ }
+ current += char;
+ }
+
+ const trimmed = current.trim();
+ if (trimmed) segments.push(trimmed);
+ return segments;
+}
+
+function firstExecutable(segment: string): string | null {
+ for (const token of shellWords(segment)) {
+ if (isEnvironmentAssignment(token)) continue;
+ if (containerCommands.has(token)) return token;
+ if (token.startsWith("./") || token.startsWith("../") || token.startsWith("/")) return token;
+ return token;
+ }
+ return null;
+}
+
+function shellWords(segment: string): string[] {
+ const words: string[] = [];
+ let current = "";
+ let quote: "'" | '"' | "`" | null = null;
+ let escaped = false;
+
+ for (const char of segment) {
+ if (escaped) {
+ current += char;
+ escaped = false;
+ continue;
+ }
+ if (char === "\\") {
+ escaped = true;
+ continue;
+ }
+ if (quote) {
+ if (char === quote) quote = null;
+ else current += char;
+ continue;
+ }
+ if (char === "'" || char === '"' || char === "`") {
+ quote = char;
+ continue;
+ }
+ if (/\s/.test(char)) {
+ if (current) {
+ words.push(current);
+ current = "";
+ }
+ continue;
+ }
+ current += char;
+ }
+
+ if (current) words.push(current);
+ return words;
+}
+
+function isEnvironmentAssignment(token: string): boolean {
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
+}
+
+function workspaceExecutionTarget(backend: "container" | "shell") {
+ return backend === "shell" ? "worker-shell" : "workspace-container";
+}
diff --git a/examples/think-compare-runtimes/worker/sandbox-container-pool.ts b/examples/think-compare-runtimes/worker/sandbox-container-pool.ts
new file mode 100644
index 00000000..c4acadca
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/sandbox-container-pool.ts
@@ -0,0 +1,61 @@
+import { getSandbox, type Sandbox as SandboxDO } from "@cloudflare/sandbox";
+import { type ContainerPoolConfigEnv, containerSleepAfter } from "./container-config";
+import type { WarmPoolRuntime } from "./container-pool-manager";
+import { ContainerWarmPool } from "./container-warm-pool";
+
+export interface SandboxPoolEnv extends ContainerPoolConfigEnv {
+ Sandbox: DurableObjectNamespace;
+}
+
+export class SandboxWarmPool extends ContainerWarmPool {
+ protected createRuntime(env: SandboxPoolEnv): WarmPoolRuntime {
+ return createSandboxWarmPoolRuntime(env);
+ }
+}
+
+function createSandboxWarmPoolRuntime(env: SandboxPoolEnv): WarmPoolRuntime {
+ return {
+ async startContainer(containerId) {
+ const sandbox = getSandbox(env.Sandbox, containerId, {
+ sleepAfter: containerSleepAfter(env),
+ });
+ await sandbox.exec("true");
+ },
+ async destroyContainer(containerId) {
+ await getSandbox(env.Sandbox, containerId, {
+ sleepAfter: containerSleepAfter(env),
+ }).destroy();
+ },
+ async isContainerRunning(containerId) {
+ const stub = getDurableObjectByName(
+ env.Sandbox,
+ containerId,
+ ) as unknown as ContainerStateStub;
+ try {
+ const state = await stub.getState();
+ return state.status === "running" || state.status === "healthy";
+ } catch {
+ return false;
+ }
+ },
+ async keepContainerAlive(containerId) {
+ const stub = getDurableObjectByName(env.Sandbox, containerId) as unknown as {
+ renewActivityTimeout?: () => void;
+ };
+ stub.renewActivityTimeout?.();
+ },
+ };
+}
+
+function getDurableObjectByName(
+ namespace: DurableObjectNamespace,
+ name: string,
+): DurableObjectStub {
+ return namespace.get(namespace.idFromName(name));
+}
+
+interface ContainerStateStub {
+ getState(): Promise<{
+ status: "running" | "stopping" | "stopped" | "healthy" | "stopped_with_code";
+ }>;
+}
diff --git a/examples/think-compare-runtimes/worker/start-run.test.ts b/examples/think-compare-runtimes/worker/start-run.test.ts
new file mode 100644
index 00000000..9d6ba118
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/start-run.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, test } from "vitest";
+import type { RunEvent } from "../shared/events";
+import { startComparisonRun } from "./start-run";
+
+const event: RunEvent = {
+ id: "run-abc:0",
+ runId: "run-abc",
+ sequence: 0,
+ runtime: "workspace",
+ kind: "runtime_note",
+ title: "Workspace seeded",
+ detail: "Fixture files were written through Workspace.fs.",
+ timestamp: "1970-01-01T00:00:00.000Z",
+};
+
+describe("startComparisonRun", () => {
+ test("creates a run and starts its CompareRun durable object", async () => {
+ const started: string[] = [];
+
+ const session = await startComparisonRun({
+ createId: () => "run-abc",
+ getRun(runId) {
+ return {
+ async startComparison() {
+ started.push(runId);
+ return [event];
+ },
+ };
+ },
+ });
+
+ expect(started).toEqual(["run-abc"]);
+ expect(session).toEqual({
+ runId: "run-abc",
+ socketPath: "/parties/compare-run/run-abc",
+ events: [event],
+ });
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/start-run.ts b/examples/think-compare-runtimes/worker/start-run.ts
new file mode 100644
index 00000000..c9a6ad2b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/start-run.ts
@@ -0,0 +1,22 @@
+import type { RunEvent } from "../shared/events";
+import { createRunSession, type RunSession } from "./runs";
+
+export interface CompareRunStarter {
+ startComparison(): Promise;
+}
+
+export interface StartComparisonRunOptions {
+ createId?: () => string;
+ getRun(runId: string): CompareRunStarter | Promise;
+}
+
+export async function startComparisonRun({
+ createId,
+ getRun,
+}: StartComparisonRunOptions): Promise {
+ const session = createRunSession(createId);
+ const run = await getRun(session.runId);
+ const events = await run.startComparison();
+
+ return { ...session, events };
+}
diff --git a/examples/think-compare-runtimes/worker/think/agent-starter.test.ts b/examples/think-compare-runtimes/worker/think/agent-starter.test.ts
new file mode 100644
index 00000000..e107d3a5
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/agent-starter.test.ts
@@ -0,0 +1,217 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { startRuntimeThinkAgents } from "./agent-starter";
+
+describe("startRuntimeThinkAgents", () => {
+ test("starts Workspace without waiting for the Sandbox handle", async () => {
+ const lifecycle: string[] = [];
+ const sandboxHandle = deferred<{
+ runComparison(): Promise;
+ }>();
+
+ const run = startRuntimeThinkAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison() {
+ lifecycle.push("workspace run");
+ },
+ },
+ sandboxAgent: sandboxHandle.promise,
+ onAgentStart(runtime) {
+ lifecycle.push(`${runtime} started`);
+ },
+ onAgentComplete(runtime) {
+ lifecycle.push(`${runtime} completed`);
+ },
+ });
+
+ await flushPromises();
+ expect(lifecycle).toEqual(["workspace started", "workspace run", "workspace completed"]);
+
+ sandboxHandle.resolve({
+ async runComparison() {
+ lifecycle.push("sandbox run");
+ },
+ });
+ await run;
+
+ expect(lifecycle).toEqual([
+ "workspace started",
+ "workspace run",
+ "workspace completed",
+ "sandbox started",
+ "sandbox run",
+ "sandbox completed",
+ ]);
+ });
+
+ test("starts Workspace and Sandbox Think agents concurrently", async () => {
+ const calls: string[] = [];
+
+ await startRuntimeThinkAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison(input) {
+ calls.push(`workspace ${input.runId} ${input.fixture.root}`);
+ },
+ },
+ sandboxAgent: {
+ async runComparison(input) {
+ calls.push(`sandbox ${input.runId} ${input.fixture.root}`);
+ },
+ },
+ });
+
+ expect(calls.sort()).toEqual([
+ "sandbox run-abc /workspace/repo",
+ "workspace run-abc /workspace/repo",
+ ]);
+ });
+
+ test("records agent startup failures without cancelling the other agent", async () => {
+ const calls: string[] = [];
+ const failures: string[] = [];
+
+ await startRuntimeThinkAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison() {
+ calls.push("workspace start");
+ throw new Error("workspace failed");
+ },
+ },
+ sandboxAgent: {
+ async runComparison() {
+ calls.push("sandbox complete");
+ },
+ },
+ onAgentError(runtime, error) {
+ failures.push(`${runtime} ${error instanceof Error ? error.message : String(error)}`);
+ },
+ });
+
+ expect(calls.sort()).toEqual(["sandbox complete", "workspace start"]);
+ expect(failures).toEqual(["workspace workspace failed"]);
+ });
+
+ test("emits each runtime terminal callback as soon as that runtime settles", async () => {
+ const lifecycle: string[] = [];
+ const workspace = deferred();
+ const sandbox = deferred();
+
+ const run = startRuntimeThinkAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison() {
+ lifecycle.push("workspace run");
+ await workspace.promise;
+ },
+ },
+ sandboxAgent: {
+ async runComparison() {
+ lifecycle.push("sandbox run");
+ await sandbox.promise;
+ },
+ },
+ onAgentStart(runtime) {
+ lifecycle.push(`${runtime} started`);
+ },
+ onAgentComplete(runtime) {
+ lifecycle.push(`${runtime} completed`);
+ },
+ onAgentError(runtime, error) {
+ lifecycle.push(
+ `${runtime} failed ${error instanceof Error ? error.message : String(error)}`,
+ );
+ },
+ });
+
+ await flushPromises();
+ expect(lifecycle).toEqual([
+ "workspace started",
+ "sandbox started",
+ "workspace run",
+ "sandbox run",
+ ]);
+
+ workspace.resolve();
+ await flushPromises();
+ expect(lifecycle).toEqual([
+ "workspace started",
+ "sandbox started",
+ "workspace run",
+ "sandbox run",
+ "workspace completed",
+ ]);
+
+ sandbox.resolve();
+ await run;
+ expect(lifecycle).toEqual([
+ "workspace started",
+ "sandbox started",
+ "workspace run",
+ "sandbox run",
+ "workspace completed",
+ "sandbox completed",
+ ]);
+ });
+
+ test("emits lifecycle callbacks for runtime terminal status", async () => {
+ const lifecycle: string[] = [];
+
+ await startRuntimeThinkAgents({
+ runId: "run-abc",
+ fixture: comparisonFixture,
+ workspaceAgent: {
+ async runComparison() {
+ lifecycle.push("workspace run");
+ },
+ },
+ sandboxAgent: {
+ async runComparison() {
+ lifecycle.push("sandbox run");
+ throw new Error("capacity exceeded");
+ },
+ },
+ onAgentStart(runtime) {
+ lifecycle.push(`${runtime} started`);
+ },
+ onAgentComplete(runtime) {
+ lifecycle.push(`${runtime} completed`);
+ },
+ onAgentError(runtime, error) {
+ lifecycle.push(
+ `${runtime} failed ${error instanceof Error ? error.message : String(error)}`,
+ );
+ },
+ });
+
+ expect(lifecycle).toEqual([
+ "workspace started",
+ "sandbox started",
+ "workspace run",
+ "sandbox run",
+ "workspace completed",
+ "sandbox failed capacity exceeded",
+ ]);
+ });
+});
+
+async function flushPromises(): Promise {
+ await Promise.resolve();
+ await Promise.resolve();
+}
+
+function deferred() {
+ let resolve!: (value: T | PromiseLike) => void;
+ let reject!: (reason?: unknown) => void;
+ const promise = new Promise((promiseResolve, promiseReject) => {
+ resolve = promiseResolve;
+ reject = promiseReject;
+ });
+ return { promise, resolve, reject };
+}
diff --git a/examples/think-compare-runtimes/worker/think/agent-starter.ts b/examples/think-compare-runtimes/worker/think/agent-starter.ts
new file mode 100644
index 00000000..bb97fc1f
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/agent-starter.ts
@@ -0,0 +1,54 @@
+import type { RuntimeId } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+
+export interface RuntimeThinkAgentRunInput {
+ runId: string;
+ fixture: ComparisonFixture;
+}
+
+export interface RuntimeThinkAgentHandle {
+ runComparison(input: RuntimeThinkAgentRunInput): Promise;
+ cancelComparison?(): Promise | void;
+}
+
+export type RuntimeThinkAgentHandleInput =
+ | RuntimeThinkAgentHandle
+ | Promise;
+
+export interface StartRuntimeThinkAgentsOptions {
+ runId: string;
+ fixture: ComparisonFixture;
+ workspaceAgent: RuntimeThinkAgentHandleInput;
+ sandboxAgent: RuntimeThinkAgentHandleInput;
+ onAgentStart?: (runtime: RuntimeId) => void | Promise;
+ onAgentComplete?: (runtime: RuntimeId) => void | Promise;
+ onAgentError?: (runtime: RuntimeId, error: unknown) => void | Promise;
+}
+
+export async function startRuntimeThinkAgents({
+ runId,
+ fixture,
+ workspaceAgent,
+ sandboxAgent,
+ onAgentStart,
+ onAgentComplete,
+ onAgentError,
+}: StartRuntimeThinkAgentsOptions): Promise {
+ const agents: Array<{ runtime: RuntimeId; agent: RuntimeThinkAgentHandleInput }> = [
+ { runtime: "workspace", agent: workspaceAgent },
+ { runtime: "sandbox", agent: sandboxAgent },
+ ];
+
+ const runs = agents.map(async ({ runtime, agent }) => {
+ try {
+ const resolvedAgent = await agent;
+ await onAgentStart?.(runtime);
+ await resolvedAgent.runComparison({ runId, fixture });
+ await onAgentComplete?.(runtime);
+ } catch (error) {
+ await onAgentError?.(runtime, error);
+ }
+ });
+
+ await Promise.all(runs);
+}
diff --git a/examples/think-compare-runtimes/worker/think/agents.test.ts b/examples/think-compare-runtimes/worker/think/agents.test.ts
new file mode 100644
index 00000000..6d1cb784
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/agents.test.ts
@@ -0,0 +1,596 @@
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const {
+ containerBackendOptions,
+ getSandbox,
+ runRealThinkTurn,
+ runtimeOrder,
+ warmPoolReleases,
+ workerBackendOptions,
+ workspaceOptions,
+} = vi.hoisted(() => ({
+ containerBackendOptions: [] as Array>,
+ getSandbox: vi.fn(),
+ runRealThinkTurn: vi.fn(),
+ runtimeOrder: [] as string[],
+ warmPoolReleases: [] as string[],
+ workerBackendOptions: [] as Array>,
+ workspaceOptions: [] as Array<{ backends?: Array<{ id?: string }> }>,
+}));
+
+vi.mock("@cloudflare/sandbox", () => ({
+ Sandbox: class {},
+ getSandbox,
+}));
+
+vi.mock("@cloudflare/think", () => ({
+ Think: class {
+ constructor(
+ readonly ctx: DurableObjectState,
+ readonly env: unknown,
+ ) {}
+ },
+}));
+
+vi.mock("@cloudflare/workspace", () => ({
+ Workspace: class {
+ readonly fs = {
+ mkdir: async () => {},
+ readFile: async () => "",
+ writeFile: async () => {},
+ };
+ readonly shell = {
+ exec: async () => ({
+ result: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
+ }),
+ };
+
+ constructor(options: { backends?: Array<{ id?: string }> }) {
+ workspaceOptions.push(options);
+ }
+
+ async close() {}
+ async ready() {}
+ stub() {
+ return { kind: "workspace-stub" };
+ }
+ },
+ WorkspaceProxy: class {},
+ WorkspaceServiceProxy: class {},
+}));
+
+vi.mock("@cloudflare/workspace/backends/container", () => ({
+ CloudflareContainerBackend: class {
+ readonly id: string;
+
+ constructor(options: Record) {
+ this.id = typeof options.id === "string" ? options.id : "cloudflare-container";
+ containerBackendOptions.push(options);
+ }
+
+ async handleFetch() {
+ return new Response(null, { status: 404 });
+ }
+ },
+}));
+
+vi.mock("@cloudflare/workspace/backends/worker", () => ({
+ WorkerBackend: class {
+ readonly id: string;
+
+ constructor(options: Record) {
+ this.id = typeof options.id === "string" ? options.id : "worker";
+ workerBackendOptions.push(options);
+ }
+ },
+}));
+
+vi.mock("agents", () => ({
+ getAgentByName: vi.fn(),
+}));
+
+vi.mock("partyserver", () => ({
+ getServerByName: vi.fn(),
+}));
+
+vi.mock("./real-turn", () => ({
+ runRealThinkTurn,
+}));
+
+vi.mock("../container-pools", () => ({
+ containerSleepAfter: (env: { CONTAINER_SLEEP_AFTER?: string }) =>
+ env.CONTAINER_SLEEP_AFTER ?? "2m",
+ containerSleepAfterMs: () => 120_000,
+ getWarmPoolHandle: () => ({
+ async getContainer() {
+ return "sandbox-physical-1";
+ },
+ async releaseContainer(runId: string) {
+ warmPoolReleases.push(runId);
+ },
+ }),
+}));
+
+import {
+ type RuntimeThinkAgentEnv,
+ SandboxThinkAgent,
+ WorkspaceProxy,
+ WorkspaceServiceProxy,
+ WorkspaceThinkAgent,
+} from "./agents";
+
+describe("WorkspaceThinkAgent", () => {
+ test("exports both proxies used by Workspace backends", () => {
+ expect(WorkspaceProxy).toBeDefined();
+ expect(WorkspaceServiceProxy).toBeDefined();
+ });
+
+ beforeEach(() => {
+ containerBackendOptions.length = 0;
+ workerBackendOptions.length = 0;
+ workspaceOptions.length = 0;
+ runRealThinkTurn.mockReset();
+ });
+
+ test("streams Think text and reasoning chunks while a turn is running", async () => {
+ const recorded: Array<{ kind: string; title: string; detail: string }> = [];
+ const agent = new TestWorkspaceThinkAgent(
+ {
+ id: { toString: () => "workspace-agent-id" },
+ storage: {},
+ } as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ WorkspaceContainerHost: {
+ get: () => ({}),
+ idFromName: (name: string) => name,
+ } as unknown as DurableObjectNamespace,
+ WorkspaceWarmPool: {} as DurableObjectNamespace,
+ LOADER: {},
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+ runRealThinkTurn.mockImplementation(async () => {
+ await agent.onChunk({ chunk: { type: "reasoning-delta", text: "Checking files" } } as never);
+ await agent.onChunk({ chunk: { type: "text-delta", text: "I updated" } } as never);
+ await agent.onStepFinish({ finishReason: "tool-calls" } as never);
+ });
+
+ await agent.runWithRecorder(
+ { runId: "run-1", fixture: { root: "/workspace/repo", task: "", files: [] } },
+ {
+ record(input) {
+ recorded.push({ kind: input.kind, title: input.title, detail: input.detail });
+ return {} as never;
+ },
+ },
+ );
+
+ expect(recorded).toEqual(
+ expect.arrayContaining([
+ {
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail: "Checking files",
+ },
+ {
+ kind: "agent_message_delta",
+ title: "Think response stream",
+ detail: "I updated",
+ },
+ {
+ kind: "agent_step",
+ title: "Think step finished",
+ detail: "finishReason: tool-calls",
+ },
+ ]),
+ );
+ });
+
+ test("exposes the active Workspace stub for worker shell loopbacks", async () => {
+ const agent = new TestWorkspaceThinkAgent(
+ {
+ id: { toString: () => "workspace-agent-id" },
+ storage: {},
+ } as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ WorkspaceContainerHost: {
+ get: () => ({}),
+ idFromName: (name: string) => name,
+ } as unknown as DurableObjectNamespace,
+ WorkspaceWarmPool: {} as DurableObjectNamespace,
+ LOADER: {},
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+ runRealThinkTurn.mockImplementation(async () => {
+ await expect(agent.getWorkspace()).resolves.toEqual({ kind: "workspace-stub" });
+ });
+
+ await agent.run({ runId: "run-1", fixture: { root: "/workspace/repo", task: "", files: [] } });
+ await expect(agent.getWorkspace()).rejects.toThrow("no active Workspace session");
+ });
+
+ test("records Workspace container assignment lifecycle when the container backend is used", async () => {
+ const recorded: Array<{ kind: string; detail: string }> = [];
+ runRealThinkTurn.mockImplementation(async () => {
+ const container = containerBackendOptions[0]?.container as
+ | (() => Promise)
+ | undefined;
+ await container?.();
+ });
+ const agent = new TestWorkspaceThinkAgent(
+ {
+ id: { toString: () => "workspace-agent-id" },
+ storage: {},
+ } as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ WorkspaceContainerHost: {
+ get: (id: string) => ({ id }),
+ idFromName: (name: string) => name,
+ } as unknown as DurableObjectNamespace,
+ WorkspaceWarmPool: {} as DurableObjectNamespace,
+ LOADER: {},
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await agent.runWithRecorder(
+ { runId: "run-1", fixture: { root: "/workspace/repo", task: "", files: [] } },
+ {
+ record(input) {
+ recorded.push({ kind: input.kind, detail: input.detail });
+ return {} as never;
+ },
+ },
+ );
+
+ expect(recorded).toEqual(
+ expect.arrayContaining([
+ {
+ kind: "container_acquired",
+ detail: JSON.stringify({
+ executionTarget: "workspace-container",
+ containerId: "sandbox-physical-1",
+ }),
+ },
+ {
+ kind: "container_released",
+ detail: JSON.stringify({
+ executionTarget: "workspace-container",
+ containerId: "sandbox-physical-1",
+ }),
+ },
+ ]),
+ );
+ });
+
+ test("constructs a Workspace with worker shell and container backends", async () => {
+ runRealThinkTurn.mockImplementation(async () => {});
+ const agent = new TestWorkspaceThinkAgent(
+ {
+ id: { toString: () => "workspace-agent-id" },
+ storage: {},
+ } as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ WorkspaceContainerHost: {
+ get: () => ({}),
+ idFromName: (name: string) => name,
+ } as unknown as DurableObjectNamespace,
+ WorkspaceWarmPool: {} as DurableObjectNamespace,
+ LOADER: {},
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await agent.run({ runId: "run-1", fixture: { root: "/workspace/repo", task: "", files: [] } });
+
+ expect(workerBackendOptions).toHaveLength(1);
+ expect(workerBackendOptions[0]).toMatchObject({
+ id: "shell",
+ workspace: { binding: "WorkspaceThinkAgent", id: "workspace-agent-id" },
+ });
+ expect(containerBackendOptions).toHaveLength(1);
+ expect(containerBackendOptions[0]).toMatchObject({
+ id: "container",
+ workspace: { binding: "WorkspaceThinkAgent", id: "workspace-agent-id" },
+ });
+ expect(workspaceOptions[0]?.backends?.map((backend) => backend.id)).toEqual([
+ "shell",
+ "container",
+ ]);
+ });
+});
+
+describe("SandboxThinkAgent", () => {
+ test("does not cap model tool-loop steps", () => {
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ expect((agent as unknown as { maxSteps: number }).maxSteps).toBe(Number.POSITIVE_INFINITY);
+ });
+
+ beforeEach(() => {
+ getSandbox.mockReset();
+ runRealThinkTurn.mockReset();
+ runtimeOrder.length = 0;
+ });
+
+ test("uses one Sandbox session for seeding, files, and exec", async () => {
+ const calls: string[] = [];
+ runRealThinkTurn.mockImplementation(async ({ adapter }) => {
+ runtimeOrder.push("think");
+ await adapter.files.read("/workspace/repo/package.json");
+ await adapter.exec("pwd", { cwd: "/workspace/repo" });
+ });
+ const session = {
+ id: "run-1-sandbox-agent",
+ async mkdir(path: string) {
+ calls.push(`session mkdir ${path}`);
+ },
+ async writeFile(path: string) {
+ runtimeOrder.push(`seed:${path}`);
+ calls.push(`session write ${path}`);
+ },
+ async readFile(path: string) {
+ calls.push(`session read ${path}`);
+ return { content: "{}\n" };
+ },
+ async exec(command: string, options?: { cwd?: string }) {
+ calls.push(`session exec ${command} ${options?.cwd}`);
+ return { exitCode: 0, stdout: "/workspace/repo\n", stderr: "" };
+ },
+ };
+ getSandbox.mockReturnValue({
+ async createSession(options?: { id?: string; cwd?: string }) {
+ calls.push(`createSession ${options?.id} ${options?.cwd}`);
+ return session;
+ },
+ async deleteSession(sessionId: string) {
+ calls.push(`deleteSession ${sessionId}`);
+ },
+ async mkdir(path: string) {
+ calls.push(`sandbox mkdir ${path}`);
+ },
+ async writeFile(path: string) {
+ calls.push(`sandbox write ${path}`);
+ },
+ });
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ CONTAINER_SLEEP_AFTER: "2m",
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await agent.run({
+ runId: "run-1",
+ fixture: {
+ root: "/workspace/repo",
+ task: "test task",
+ files: [{ path: "package.json", contents: "{}\n" }],
+ },
+ });
+
+ expect(getSandbox).toHaveBeenCalledWith(
+ expect.anything(),
+ "sandbox-physical-1",
+ expect.objectContaining({ sleepAfter: "2m" }),
+ );
+ expect(calls).toEqual([
+ "createSession run-1-sandbox-agent /",
+ "session mkdir /workspace/repo",
+ "session write /workspace/repo/package.json",
+ "session exec test -d /workspace/repo && test -f /workspace/repo/package.json undefined",
+ "session read /workspace/repo/package.json",
+ "session exec pwd /workspace/repo",
+ "deleteSession run-1-sandbox-agent",
+ ]);
+ expect(runtimeOrder).toEqual(["seed:/workspace/repo/package.json", "think"]);
+ });
+
+ test("fails before Think starts when the Sandbox seed is not shell-visible", async () => {
+ runRealThinkTurn.mockImplementation(() => {
+ throw new Error("Think should not start");
+ });
+ getSandbox.mockReturnValue({
+ async createSession() {
+ return {
+ id: "run-1-sandbox-agent",
+ async mkdir() {},
+ async writeFile() {},
+ async readFile() {
+ return { content: "{}\n" };
+ },
+ async exec() {
+ return { exitCode: 1, stdout: "", stderr: "missing repo" };
+ },
+ };
+ },
+ async deleteSession() {},
+ });
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await expect(
+ agent.run({
+ runId: "run-1",
+ fixture: {
+ root: "/workspace/repo",
+ task: "test task",
+ files: [{ path: "package.json", contents: "{}\n" }],
+ },
+ }),
+ ).rejects.toThrow("Sandbox fixture seed is not visible to exec");
+ expect(runRealThinkTurn).not.toHaveBeenCalled();
+ });
+
+ test("records Sandbox container assignment lifecycle", async () => {
+ const recorded: Array<{ kind: string; detail: string }> = [];
+ getSandbox.mockReturnValue({
+ async createSession() {
+ return {
+ id: "run-1-sandbox-agent",
+ async mkdir() {},
+ async writeFile() {},
+ async readFile() {
+ return { content: "" };
+ },
+ async exec() {
+ return { exitCode: 0, stdout: "", stderr: "" };
+ },
+ };
+ },
+ async deleteSession() {},
+ });
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await agent.runWithRecorder(
+ { runId: "run-1", fixture: { root: "/workspace/repo", task: "", files: [] } },
+ {
+ record(input) {
+ recorded.push({ kind: input.kind, detail: input.detail });
+ return {} as never;
+ },
+ },
+ );
+
+ expect(recorded).toEqual(
+ expect.arrayContaining([
+ {
+ kind: "container_acquired",
+ detail: JSON.stringify({
+ executionTarget: "sandbox-container",
+ containerId: "sandbox-physical-1",
+ }),
+ },
+ {
+ kind: "container_released",
+ detail: JSON.stringify({
+ executionTarget: "sandbox-container",
+ containerId: "sandbox-physical-1",
+ }),
+ },
+ ]),
+ );
+ });
+
+ test("releases Sandbox warm-pool assignments during runtime cleanup", async () => {
+ warmPoolReleases.length = 0;
+ getSandbox.mockReturnValue({
+ async createSession() {
+ return {
+ id: "run-1-sandbox-agent",
+ async mkdir() {},
+ async writeFile() {},
+ async readFile() {
+ return { content: "" };
+ },
+ async exec() {
+ return { exitCode: 0, stdout: "", stderr: "" };
+ },
+ };
+ },
+ async deleteSession() {},
+ });
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+
+ await agent.run({
+ runId: "run-1",
+ fixture: { root: "/workspace/repo", task: "", files: [] },
+ });
+
+ expect(warmPoolReleases).toEqual(["run-1"]);
+ });
+ test("throws when a completed submission has no assistant text", async () => {
+ const agent = new TestSandboxThinkAgent(
+ {} as DurableObjectState,
+ {
+ AI: {} as Ai,
+ CompareRun: {} as DurableObjectNamespace,
+ Sandbox: {} as DurableObjectNamespace,
+ SandboxWarmPool: {} as DurableObjectNamespace,
+ } as unknown as RuntimeThinkAgentEnv,
+ );
+ Object.defineProperty(agent, "messages", {
+ configurable: true,
+ value: [{ id: "message-1", role: "assistant", parts: [{ type: "text", text: " " }] }],
+ });
+ agent.inspectSubmission = async () => ({
+ submissionId: "submission-1",
+ status: "completed",
+ createdAt: Date.now(),
+ });
+
+ await expect(agent.awaitAssistantText("submission-1")).rejects.toThrow(
+ "Think turn completed without assistant text",
+ );
+ });
+});
+
+class TestWorkspaceThinkAgent extends WorkspaceThinkAgent {
+ run(config: Parameters[0]) {
+ return this.runWithRuntime(config, {
+ record: () => ({}) as never,
+ });
+ }
+
+ runWithRecorder(
+ config: Parameters[0],
+ recorder: Parameters[1],
+ ) {
+ return this.runWithRuntime(config, recorder);
+ }
+}
+
+class TestSandboxThinkAgent extends SandboxThinkAgent {
+ run(config: Parameters[0]) {
+ return this.runWithRuntime(config, {
+ record: () => ({}) as never,
+ });
+ }
+
+ runWithRecorder(
+ config: Parameters[0],
+ recorder: Parameters[1],
+ ) {
+ return this.runWithRuntime(config, recorder);
+ }
+}
diff --git a/examples/think-compare-runtimes/worker/think/agents.ts b/examples/think-compare-runtimes/worker/think/agents.ts
new file mode 100644
index 00000000..e0a65f44
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/agents.ts
@@ -0,0 +1,531 @@
+import { getSandbox, type Sandbox as SandboxDO } from "@cloudflare/sandbox";
+import { type ChunkContext, type StepContext, Think } from "@cloudflare/think";
+import {
+ type DurableObjectStorageLike,
+ Workspace,
+ WorkspaceProxy,
+ WorkspaceServiceProxy,
+ type WorkspaceStub,
+} from "@cloudflare/workspace";
+import { CloudflareContainerBackend } from "@cloudflare/workspace/backends/container";
+import { WorkerBackend, type WorkerBackendOptions } from "@cloudflare/workspace/backends/worker";
+import type { ToolSet } from "ai";
+import { getServerByName } from "partyserver";
+import type { ExecutionTarget, RunEventKind, RuntimeId } from "../../shared/events";
+import type { ComparisonFixture, FixtureFile } from "../../shared/fixture";
+import {
+ type ContainerWarmPoolHandle,
+ type ContainerWarmPoolNamespace,
+ containerSleepAfter,
+ getWarmPoolHandle,
+ type WorkspaceContainerHost,
+} from "../container-pools";
+import type { CompareRun } from "../index";
+import {
+ createSandboxRuntimeAdapter,
+ createWorkspaceRuntimeAdapter,
+ type RuntimeAdapter,
+} from "../runtime/adapter";
+import {
+ createSandboxCommandRunner,
+ createSandboxFileStore,
+ createSandboxFixtureRuntime,
+} from "../runtime/sandbox";
+import { seedFixture } from "../runtime/seed";
+import {
+ createWorkspaceCommandRunner,
+ createWorkspaceFileStore,
+ createWorkspaceFixtureRuntime,
+} from "../runtime/workspace";
+import { createRuntimeThinkModel } from "./model";
+import { createRuntimeSystemPrompt } from "./prompts";
+import { runRealThinkTurn } from "./real-turn";
+import { type CompareRunEventSink, createRemoteRunEventRecorder } from "./remote-recorder";
+import { createRuntimeThinkTools, type RuntimeThinkToolRecorder } from "./runtime-tools";
+
+export { WorkspaceProxy, WorkspaceServiceProxy };
+
+export interface RuntimeThinkAgentEnv {
+ AI: Ai;
+ CompareRun: DurableObjectNamespace;
+ Sandbox: DurableObjectNamespace;
+ SandboxWarmPool: ContainerWarmPoolNamespace;
+ WorkspaceContainerHost: DurableObjectNamespace;
+ WorkspaceWarmPool: ContainerWarmPoolNamespace;
+ CONTAINER_SLEEP_AFTER?: string;
+ FUSE_MOUNT?: string;
+ LOADER: WorkerBackendOptions["loader"];
+ WARM_POOL_RESET_KEY?: string;
+}
+
+interface RunConfig {
+ runId: string;
+ fixture: ComparisonFixture;
+}
+
+abstract class RuntimeThinkAgent extends Think {
+ #activeRecorder: RuntimeThinkToolRecorder | null = null;
+ #messageDelta = "";
+ #preparedTools: ToolSet | null = null;
+ #thinkingDelta = "";
+
+ override chatRecovery = false;
+
+ abstract readonly runtime: RuntimeId;
+ abstract readonly runtimeLabel: "Workspace" | "Sandbox";
+
+ constructor(ctx: DurableObjectState, env: RuntimeThinkAgentEnv) {
+ super(ctx, env);
+ this.maxSteps = Number.POSITIVE_INFINITY;
+ }
+
+ protected abstract runWithRuntime(
+ config: RunConfig,
+ recorder: RuntimeThinkToolRecorder,
+ ): Promise;
+
+ override getModel() {
+ return createRuntimeThinkModel(this.env.AI);
+ }
+
+ override getSystemPrompt(): string {
+ return createRuntimeSystemPrompt(this.runtime);
+ }
+
+ async runComparison(config: RunConfig): Promise {
+ const compareRun = (await getServerByName(
+ this.env.CompareRun,
+ config.runId,
+ )) as unknown as CompareRunEventSink;
+ const recorder = createRemoteRunEventRecorder(compareRun);
+ await this.runWithRuntime(config, recorder);
+ }
+
+ async cancelComparison(): Promise {
+ this.cancelAllChats();
+ }
+
+ protected async runThinkTurn(
+ adapter: RuntimeAdapter,
+ recorder: RuntimeThinkToolRecorder,
+ fixture: ComparisonFixture,
+ ): Promise {
+ this.#activeRecorder = recorder;
+ this.#messageDelta = "";
+ this.#thinkingDelta = "";
+ this.#preparedTools = createRuntimeThinkTools({ adapter, recorder }) as unknown as ToolSet;
+
+ try {
+ await runRealThinkTurn({
+ adapter,
+ recorder,
+ fixture,
+ invoke: ({ prompt }) => this.invokeThink(prompt),
+ });
+ await this.#flushStreamingDeltas();
+ } finally {
+ this.#activeRecorder = null;
+ this.#messageDelta = "";
+ this.#thinkingDelta = "";
+ }
+ }
+
+ override getTools(): ToolSet {
+ return this.#preparedTools ?? ({} as ToolSet);
+ }
+
+ async invokeThink(prompt: string): Promise<{ text: string }> {
+ const submission = await this.submitMessages([
+ {
+ id: crypto.randomUUID(),
+ role: "user",
+ parts: [{ type: "text", text: prompt }],
+ },
+ ]);
+
+ return { text: await this.awaitAssistantText(submission.submissionId) };
+ }
+
+ override async onChunk(ctx: ChunkContext): Promise {
+ const chunk = ctx.chunk as { type?: string; text?: unknown; delta?: unknown };
+ const text =
+ typeof chunk.text === "string"
+ ? chunk.text
+ : typeof chunk.delta === "string"
+ ? chunk.delta
+ : "";
+ if (text.length === 0) return;
+
+ if (chunk.type === "reasoning-delta") {
+ this.#thinkingDelta += text;
+ await this.#flushThinkingDeltaIfReady(false);
+ return;
+ }
+
+ if (chunk.type === "text-delta") {
+ this.#messageDelta += text;
+ await this.#flushMessageDeltaIfReady(false);
+ }
+ }
+
+ override async onStepFinish(ctx: StepContext): Promise {
+ await this.#flushStreamingDeltas();
+ const finishReason = (ctx as { finishReason?: unknown }).finishReason;
+ await this.#recordStreamEvent({
+ kind: "agent_step",
+ title: "Think step finished",
+ detail: `finishReason: ${typeof finishReason === "string" ? finishReason : "unknown"}`,
+ });
+ }
+
+ async awaitAssistantText(submissionId: string): Promise {
+ for (;;) {
+ const inspection = await this.inspectSubmission(submissionId);
+ if (!inspection) throw new Error(`Submission ${submissionId} vanished`);
+ if (inspection.status === "completed") {
+ const text = collectAssistantText(this.messages);
+ if (text.length === 0) {
+ throw new Error("Think turn completed without assistant text.");
+ }
+ return text;
+ }
+ if (
+ inspection.status === "error" ||
+ inspection.status === "aborted" ||
+ inspection.status === "skipped"
+ ) {
+ throw new Error(
+ `Think turn ended in status=${inspection.status}${inspection.error ? `: ${inspection.error}` : ""}`,
+ );
+ }
+ await scheduler.wait(500);
+ }
+ }
+
+ async #flushStreamingDeltas(): Promise {
+ await this.#flushThinkingDeltaIfReady(true);
+ await this.#flushMessageDeltaIfReady(true);
+ }
+
+ async #flushThinkingDeltaIfReady(force: boolean): Promise {
+ if (this.#thinkingDelta.length === 0) return;
+ if (!force && this.#thinkingDelta.length < 80) return;
+ const detail = this.#thinkingDelta;
+ this.#thinkingDelta = "";
+ await this.#recordStreamEvent({
+ kind: "agent_thinking_delta",
+ title: "Think reasoning stream",
+ detail,
+ });
+ }
+
+ async #flushMessageDeltaIfReady(force: boolean): Promise {
+ if (this.#messageDelta.length === 0) return;
+ if (!force && this.#messageDelta.length < 80) return;
+ const detail = this.#messageDelta;
+ this.#messageDelta = "";
+ await this.#recordStreamEvent({
+ kind: "agent_message_delta",
+ title: "Think response stream",
+ detail,
+ });
+ }
+
+ async #recordStreamEvent(input: {
+ kind: "agent_message_delta" | "agent_thinking_delta" | "agent_step";
+ title: string;
+ detail: string;
+ }): Promise {
+ await this.#activeRecorder?.record({
+ runtime: this.runtime,
+ ...input,
+ });
+ }
+}
+
+export class WorkspaceThinkAgent extends RuntimeThinkAgent {
+ readonly runtime = "workspace";
+ readonly runtimeLabel = "Workspace";
+ readonly #ctx: DurableObjectState;
+ #activeBackend: CloudflareContainerBackend | null = null;
+ #activeWorkspace: Workspace | null = null;
+
+ constructor(ctx: DurableObjectState, env: RuntimeThinkAgentEnv) {
+ super(ctx, env);
+ this.#ctx = ctx;
+ }
+
+ override async fetch(request: Request): Promise {
+ const url = new URL(request.url);
+ if (url.pathname === "/ws" && this.#activeBackend) {
+ return this.#activeBackend.handleFetch(request);
+ }
+ return super.fetch(request);
+ }
+
+ async getWorkspace(): Promise {
+ if (!this.#activeWorkspace) {
+ throw new Error("WorkspaceThinkAgent has no active Workspace session.");
+ }
+ await this.#activeWorkspace.ready();
+ return this.#activeWorkspace.stub();
+ }
+
+ protected async runWithRuntime(
+ config: RunConfig,
+ recorder: RuntimeThinkToolRecorder,
+ ): Promise {
+ const session = this.createWorkspaceSession(config, recorder);
+ this.#activeBackend = session.backend;
+ this.#activeWorkspace = session.workspace;
+ try {
+ await seedFixture(createWorkspaceFixtureRuntime(session.workspace), config.fixture);
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: createWorkspaceFileStore(session.workspace),
+ runner: createWorkspaceCommandRunner(session.workspace),
+ });
+ await this.runThinkTurn(adapter, recorder, config.fixture);
+ } finally {
+ if (this.#activeBackend === session.backend) {
+ this.#activeBackend = null;
+ }
+ if (this.#activeWorkspace === session.workspace) {
+ this.#activeWorkspace = null;
+ }
+ await session.close();
+ }
+ }
+
+ private createWorkspaceSession(
+ config: RunConfig,
+ recorder: RuntimeThinkToolRecorder,
+ ): WorkspaceRunSession {
+ const workspaceRef = { binding: "WorkspaceThinkAgent", id: this.#ctx.id.toString() };
+ let assignedContainerId: string | null = null;
+ const backend = new CloudflareContainerBackend({
+ id: "container",
+ container: async () => {
+ const containerId = await getWarmPoolHandle(this.env.WorkspaceWarmPool).getContainer(
+ config.runId,
+ );
+ if (assignedContainerId !== containerId) {
+ assignedContainerId = containerId;
+ await recordContainerLifecycle(recorder, {
+ runtime: this.runtime,
+ kind: "container_acquired",
+ executionTarget: "workspace-container",
+ containerId,
+ });
+ }
+ return this.env.WorkspaceContainerHost.get(
+ this.env.WorkspaceContainerHost.idFromName(containerId),
+ );
+ },
+ workspace: workspaceRef,
+ containerEnv: this.env.FUSE_MOUNT ? { FUSE_MOUNT: this.env.FUSE_MOUNT } : undefined,
+ });
+ const workspace = new Workspace({
+ storage: this.#ctx.storage as unknown as DurableObjectStorageLike,
+ backends: [
+ new WorkerBackend({
+ id: "shell",
+ loader: this.env.LOADER,
+ workspace: workspaceRef,
+ ctx: this.#ctx,
+ }),
+ backend,
+ ],
+ });
+ return {
+ backend,
+ workspace,
+ close: () =>
+ this.closeWorkspaceSession(config.runId, workspace, recorder, () => assignedContainerId),
+ };
+ }
+
+ private async closeWorkspaceSession(
+ runId: string,
+ workspace: Workspace,
+ recorder: RuntimeThinkToolRecorder,
+ assignedContainerId: () => string | null,
+ ): Promise {
+ await bestEffortCleanup("Workspace session close", () => workspace.close());
+ await bestEffortCleanup("Workspace warm-pool release", async () => {
+ await getWarmPoolHandle(this.env.WorkspaceWarmPool).releaseContainer(runId);
+ const containerId = assignedContainerId();
+ if (containerId) {
+ await recordContainerLifecycle(recorder, {
+ runtime: this.runtime,
+ kind: "container_released",
+ executionTarget: "workspace-container",
+ containerId,
+ });
+ }
+ });
+ }
+}
+
+export class SandboxThinkAgent extends RuntimeThinkAgent {
+ readonly runtime = "sandbox";
+ readonly runtimeLabel = "Sandbox";
+
+ protected async runWithRuntime(
+ config: RunConfig,
+ recorder: RuntimeThinkToolRecorder,
+ ): Promise {
+ const { containerId, sandbox, session } = await this.createSandboxSession(config.runId);
+ await recordContainerLifecycle(recorder, {
+ runtime: this.runtime,
+ kind: "container_acquired",
+ executionTarget: "sandbox-container",
+ containerId,
+ });
+ try {
+ await seedFixture(createSandboxFixtureRuntime(session), config.fixture);
+ await assertSandboxFixtureVisible(session, config.fixture);
+ const adapter = createSandboxRuntimeAdapter({
+ recorder,
+ store: createSandboxFileStore(session),
+ runner: createSandboxCommandRunner(session),
+ });
+ await this.runThinkTurn(adapter, recorder, config.fixture);
+ } finally {
+ await bestEffortCleanup("Sandbox session delete", async () => {
+ await sandbox.deleteSession(session.id);
+ });
+ await bestEffortCleanup("Sandbox warm-pool release", async () => {
+ await this.getWarmPool().releaseContainer(config.runId);
+ await recordContainerLifecycle(recorder, {
+ runtime: this.runtime,
+ kind: "container_released",
+ executionTarget: "sandbox-container",
+ containerId,
+ });
+ });
+ }
+ }
+
+ private async createSandboxSession(runId: string): Promise {
+ const containerId = await this.getWarmPool().getContainer(runId);
+ const sandbox = getSandbox(this.env.Sandbox, containerId, {
+ sleepAfter: containerSleepAfter(this.env),
+ }) as unknown as SandboxSessionOwner;
+ const session = await sandbox.createSession({ id: sandboxSessionId(runId), cwd: "/" });
+ return { containerId, sandbox, session };
+ }
+
+ private getWarmPool(): ContainerWarmPoolHandle {
+ return getWarmPoolHandle(this.env.SandboxWarmPool);
+ }
+}
+
+interface WorkspaceRunSession {
+ backend: CloudflareContainerBackend;
+ workspace: Workspace;
+ close(): Promise;
+}
+
+interface SandboxRunSession {
+ containerId: string;
+ sandbox: SandboxSessionOwner;
+ session: SandboxRuntimeSession;
+}
+
+interface SandboxSessionOwner {
+ createSession(options: { id: string; cwd: string }): Promise;
+ deleteSession(sessionId: string): Promise;
+}
+
+interface SandboxRuntimeSession {
+ id: string;
+ mkdir(path: string, options?: { recursive?: boolean }): Promise;
+ writeFile(path: string, contents: string): Promise;
+ readFile(path: string): Promise<{ content: string | Uint8Array }>;
+ exec(
+ command: string,
+ options?: { cwd?: string; timeout?: number },
+ ): Promise<{
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+ }>;
+}
+
+async function assertSandboxFixtureVisible(
+ session: SandboxRuntimeSession,
+ fixture: ComparisonFixture,
+): Promise {
+ const command = [
+ `test -d ${shellQuote(fixture.root)}`,
+ ...fixture.files.map((file) => `test -f ${shellQuote(fixturePath(fixture.root, file))}`),
+ ].join(" && ");
+ const result = await session.exec(command);
+ if (result.exitCode !== 0) {
+ throw new Error(
+ `Sandbox fixture seed is not visible to exec: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`,
+ );
+ }
+}
+
+function fixturePath(root: string, file: FixtureFile): string {
+ return `${root.replace(/\/+$/, "")}/${file.path.replace(/^\/+/, "")}`;
+}
+
+function sandboxSessionId(runId: string): string {
+ return `${runId.replace(/[^a-zA-Z0-9_-]/g, "-")}-sandbox-agent`;
+}
+
+function shellQuote(value: string): string {
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
+}
+
+async function recordContainerLifecycle(
+ recorder: RuntimeThinkToolRecorder,
+ input: {
+ runtime: RuntimeId;
+ kind: Extract;
+ executionTarget: ExecutionTarget;
+ containerId: string;
+ },
+): Promise {
+ await recorder.record({
+ runtime: input.runtime,
+ kind: input.kind,
+ title: input.kind === "container_acquired" ? "Container assigned" : "Container released",
+ detail: JSON.stringify({
+ executionTarget: input.executionTarget,
+ containerId: input.containerId,
+ }),
+ });
+}
+
+async function bestEffortCleanup(label: string, cleanup: () => Promise): Promise {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.warn(`${label} failed`, { error });
+ }
+}
+
+function collectAssistantText(messages: Array<{ role?: string; parts?: Array }>): string {
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
+ const message = messages[index];
+ if (message?.role !== "assistant") continue;
+ const parts = message.parts ?? [];
+ const text = parts
+ .map((part) => {
+ if (!part || typeof part !== "object") return "";
+ const candidate = part as { type?: string; text?: unknown };
+ return candidate.type === "text" && typeof candidate.text === "string"
+ ? candidate.text
+ : "";
+ })
+ .join("")
+ .trim();
+ if (text.length > 0) return text;
+ }
+ return "";
+}
diff --git a/examples/think-compare-runtimes/worker/think/model.test.ts b/examples/think-compare-runtimes/worker/think/model.test.ts
new file mode 100644
index 00000000..b1122d16
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/model.test.ts
@@ -0,0 +1,28 @@
+import { beforeEach, describe, expect, test, vi } from "vitest";
+import { createRuntimeThinkModel } from "./model";
+
+const createWorkersAI = vi.hoisted(() => vi.fn());
+const modelFactory = vi.hoisted(() => vi.fn());
+
+vi.mock("workers-ai-provider", () => ({
+ createWorkersAI,
+}));
+
+describe("createRuntimeThinkModel", () => {
+ beforeEach(() => {
+ createWorkersAI.mockReset();
+ modelFactory.mockReset();
+ createWorkersAI.mockReturnValue(modelFactory);
+ });
+
+ test("uses low Kimi reasoning", () => {
+ const binding = {} as Ai;
+
+ createRuntimeThinkModel(binding);
+
+ expect(createWorkersAI).toHaveBeenCalledWith({ binding });
+ expect(modelFactory).toHaveBeenCalledWith("@cf/moonshotai/kimi-k2.6", {
+ reasoning_effort: "low",
+ });
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/model.ts b/examples/think-compare-runtimes/worker/think/model.ts
new file mode 100644
index 00000000..caba9439
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/model.ts
@@ -0,0 +1,7 @@
+import { createWorkersAI } from "workers-ai-provider";
+
+const MODEL_ID = "@cf/moonshotai/kimi-k2.6";
+
+export function createRuntimeThinkModel(binding: Ai) {
+ return createWorkersAI({ binding })(MODEL_ID, { reasoning_effort: "low" });
+}
diff --git a/examples/think-compare-runtimes/worker/think/prompts.test.ts b/examples/think-compare-runtimes/worker/think/prompts.test.ts
new file mode 100644
index 00000000..0c295931
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/prompts.test.ts
@@ -0,0 +1,119 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import {
+ createRuntimeSystemPrompt,
+ createRuntimeToolDescriptions,
+ createTaskPrompt,
+} from "./prompts";
+
+describe("runtime Think prompts", () => {
+ test("gives both runtimes the same coding workflow", () => {
+ const workspace = createRuntimeSystemPrompt("workspace");
+ const sandbox = createRuntimeSystemPrompt("sandbox");
+
+ for (const prompt of [workspace, sandbox]) {
+ expect(prompt).toContain("The project root is /workspace/repo.");
+ expect(prompt).toContain("Use whichever tool is fastest and most reliable for the job.");
+ expect(prompt).toContain(
+ "Use exec to search, list, and inspect before opening individual files",
+ );
+ expect(prompt).toContain("Use read when you need exact file contents before editing.");
+ expect(prompt).toContain("Use edit for targeted changes to existing files.");
+ expect(prompt).toContain(
+ "Combine multiple replacements for the same file in one edit call when practical.",
+ );
+ expect(prompt).toContain("Use write for new files or complete rewrites.");
+ expect(prompt).toContain(
+ "After validation passes, stop editing and summarize the completed work.",
+ );
+ expect(prompt).toContain("The fixture files are already seeded before you start.");
+ expect(prompt).toContain("Tool results are facts; reasoning is provisional.");
+ expect(prompt).toContain(
+ "Do not claim a directory is empty or missing unless a tool result shows that.",
+ );
+ expect(prompt).toContain(
+ "If expected fixture files appear missing, report a runtime visibility issue instead of bootstrapping replacement project files.",
+ );
+ }
+ });
+
+ test("adds Workspace-specific guidance for durable files, worker shell, and container validation", () => {
+ const prompt = createRuntimeSystemPrompt("workspace");
+
+ expect(prompt).toContain("Cloudflare Workspace");
+ expect(prompt).toContain("durable workspace storage");
+ expect(prompt).toContain("Start Workspace discovery with the worker shell");
+ expect(prompt).toContain("workspace container is for Node, npm, package scripts");
+ });
+
+ test("adds Sandbox-specific guidance for a normal container workflow", () => {
+ const prompt = createRuntimeSystemPrompt("sandbox");
+
+ expect(prompt).toContain("Cloudflare Sandbox");
+ expect(prompt).toContain("one container-backed project session");
+ expect(prompt).toContain("Use exec freely for search, listing, package scripts, tests");
+ expect(prompt).toContain("file tools and exec see the same filesystem");
+ });
+
+ test("builds an explicit docs task checklist for each runtime", () => {
+ const prompt = createTaskPrompt(comparisonFixture);
+
+ expect(prompt).toContain("You are working in a small docs project at /workspace/repo.");
+ expect(prompt).toContain(comparisonFixture.task);
+ expect(prompt).toContain("Useful source material:");
+ expect(prompt).toContain("- /workspace/repo/feature-briefs/smart-request-policies.md");
+ expect(prompt).toContain("- /workspace/repo/style-guide.md");
+ expect(prompt).toContain(
+ "Locate related Workers docs and examples before drafting the new page.",
+ );
+ expect(prompt).toContain("Seeded project files:");
+ for (const file of comparisonFixture.files) {
+ expect(prompt).toContain(`- /workspace/repo/${file.path}`);
+ }
+ expect(prompt).toContain(
+ "These files are already present at run start; do not recreate the baseline project.",
+ );
+ expect(prompt).toContain(
+ "If a listing appears inconsistent with this manifest, verify by reading known paths and report the inconsistency instead of creating substitute files.",
+ );
+ expect(prompt).toContain("Acceptance criteria:");
+ expect(prompt).toContain("Create /workspace/repo/docs/workers/smart-request-policies.md.");
+ expect(prompt).toContain("Include the exact header name `x-bypass-token`.");
+ expect(prompt).toContain("Include the exact phrase `Enterprise report exports`.");
+ expect(prompt).toContain(
+ "Add `/workers/smart-request-policies/` to the Workers section in docs-nav.json.",
+ );
+ expect(prompt).toContain("Update README.md with `smart-request-policies`");
+ expect(prompt).toContain("Run `npm run check` from /workspace/repo after writing changes.");
+ expect(prompt).toContain(
+ "If validation fails, use every reported failure as a repair checklist and rerun validation.",
+ );
+ });
+
+ test("tunes tool descriptions to the runtime boundary", () => {
+ const workspace = createRuntimeToolDescriptions("workspace");
+ const sandbox = createRuntimeToolDescriptions("sandbox");
+
+ expect(workspace.read).toContain("Workspace file tools");
+ expect(workspace.read).toContain("absolute path under /workspace/repo");
+ expect(workspace.exec).toContain(
+ "grep, find, ls, cat, pwd, head, tail, sed, and wc route to the worker shell",
+ );
+ expect(workspace.exec).toContain("npm, node, npx, pnpm, yarn, vitest, tsc");
+ expect(workspace.exec).toContain(
+ "After validation passes, summarize the work instead of making extra edits",
+ );
+ expect(workspace.exec).toContain(
+ "If discovery commands disagree with successful reads of seeded files, verify known paths and report a visibility issue",
+ );
+ expect(sandbox.read).toContain("Sandbox filesystem");
+ expect(sandbox.read).toContain("absolute path under /workspace/repo");
+ expect(sandbox.exec).toContain(
+ "Use this freely for search, listing, project inspection, package scripts, tests",
+ );
+ expect(sandbox.exec).toContain("If validation fails, repair the files and rerun the command");
+ expect(sandbox.exec).toContain(
+ "Do not bootstrap replacement project files if seeded fixture paths are already readable",
+ );
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/prompts.ts b/examples/think-compare-runtimes/worker/think/prompts.ts
new file mode 100644
index 00000000..3c1bd7ac
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/prompts.ts
@@ -0,0 +1,117 @@
+import type { RuntimeId } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+
+export type RuntimeToolDescriptions = {
+ read: string;
+ write: string;
+ edit: string;
+ exec: string;
+};
+
+export function createRuntimeSystemPrompt(runtime: RuntimeId): string {
+ return runtime === "workspace"
+ ? [sharedCodingPrompt(), workspaceRuntimePrompt()].join("\n\n")
+ : [sharedCodingPrompt(), sandboxRuntimePrompt()].join("\n\n");
+}
+
+export function createTaskPrompt(fixture: ComparisonFixture): string {
+ const root = fixture.root.replace(/\/+$/, "");
+ return [
+ `You are working in a small docs project at ${root}.`,
+ "",
+ "Goal:",
+ fixture.task,
+ "",
+ "Useful source material:",
+ `- ${root}/feature-briefs/smart-request-policies.md`,
+ `- ${root}/style-guide.md`,
+ `- ${root}/docs-nav.json`,
+ `- existing docs and examples under ${root}/docs/workers/`,
+ "",
+ "Locate related Workers docs and examples before drafting the new page.",
+ "",
+ "Seeded project files:",
+ ...fixture.files.map((file) => `- ${root}/${file.path}`),
+ "",
+ "These files are already present at run start; do not recreate the baseline project.",
+ "If a listing appears inconsistent with this manifest, verify by reading known paths and report the inconsistency instead of creating substitute files.",
+ "",
+ "Acceptance criteria:",
+ `- Create ${root}/docs/workers/smart-request-policies.md.`,
+ "- Start the new page with YAML frontmatter containing `title`, `description`, and `lastUpdated`.",
+ "- Describe Smart Request Policies clearly for Workers developers.",
+ "- Include the exact header name `x-bypass-token`.",
+ "- Include the exact phrase `Enterprise report exports`.",
+ "- Include a TypeScript Worker example in the new page.",
+ "- Mention that beta policies do not replace application authorization.",
+ "- Add `/workers/smart-request-policies/` to the Workers section in docs-nav.json.",
+ "- Update README.md with `smart-request-policies` so maintainers can find the new page.",
+ `- Run \`npm run check\` from ${root} after writing changes.`,
+ "- If validation fails, use every reported failure as a repair checklist and rerun validation.",
+ "",
+ "Before editing, inspect the source material and search or list related docs and examples as needed.",
+ "Finish by summarizing what changed and how you verified it.",
+ ].join("\n");
+}
+
+export function createRuntimeToolDescriptions(runtime: RuntimeId): RuntimeToolDescriptions {
+ if (runtime === "workspace") {
+ return {
+ read: "Read a UTF-8 text file with Workspace file tools. Use an absolute path under /workspace/repo when you need exact file contents. Workspace file tools read durable workspace storage and do not need a container.",
+ write:
+ "Create or overwrite a text file with Workspace file tools. Use an absolute path under /workspace/repo. This replaces the whole file, so use it for new files or full-file rewrites.",
+ edit: "Apply exact text replacements with Workspace file tools. Use an absolute path under /workspace/repo. Each oldText must match exactly one current region in the file; read the file first if you need exact text.",
+ exec: "Run a shell command through the Workspace environment. grep, find, ls, cat, pwd, head, tail, sed, and wc route to the worker shell for fast text inspection. npm, node, npx, pnpm, yarn, vitest, tsc, and executable project scripts route to the workspace container for package/runtime work. cwd defaults to /workspace/repo and must stay under /workspace/repo. If discovery commands disagree with successful reads of seeded files, verify known paths and report a visibility issue instead. If validation fails, repair the files and rerun the command. After validation passes, summarize the work instead of making extra edits.",
+ };
+ }
+
+ return {
+ read: "Read a UTF-8 text file from the Sandbox filesystem. Use an absolute path under /workspace/repo when you need exact file contents.",
+ write:
+ "Create or overwrite a text file in the Sandbox filesystem. Use an absolute path under /workspace/repo. This replaces the whole file, so use it for new files or full-file rewrites.",
+ edit: "Apply exact text replacements to a file in the Sandbox filesystem. Use an absolute path under /workspace/repo. Each oldText must match exactly one current region in the file; read the file first if you need exact text.",
+ exec: "Run a shell command inside the Sandbox container. Use this freely for search, listing, project inspection, package scripts, tests, and other shell-native workflows. cwd defaults to /workspace/repo and must stay under /workspace/repo. Do not bootstrap replacement project files if seeded fixture paths are already readable; verify known paths and report a visibility issue instead. If validation fails, repair the files and rerun the command.",
+ };
+}
+
+function sharedCodingPrompt(): string {
+ return [
+ "You are an expert coding agent working on a small docs project.",
+ "",
+ "Shared workflow:",
+ "- The project root is /workspace/repo.",
+ "- Use whichever tool is fastest and most reliable for the job.",
+ "- Use exec to search, list, and inspect before opening individual files when shell commands answer faster.",
+ "- Use read when you need exact file contents before editing.",
+ "- Use edit for targeted changes to existing files.",
+ "- Combine multiple replacements for the same file in one edit call when practical.",
+ "- Use write for new files or complete rewrites.",
+ "- Keep changes minimal and focused on the task.",
+ "- Treat validation failures as actionable repair checklists, then rerun validation when possible.",
+ "- After validation passes, stop editing and summarize the completed work.",
+ "- The fixture files are already seeded before you start.",
+ "- Tool results are facts; reasoning is provisional.",
+ "- Do not claim a directory is empty or missing unless a tool result shows that.",
+ "- If expected fixture files appear missing, report a runtime visibility issue instead of bootstrapping replacement project files.",
+ ].join("\n");
+}
+
+function workspaceRuntimePrompt(): string {
+ return [
+ "Runtime: Cloudflare Workspace.",
+ "- Workspace file tools read and write durable workspace storage directly.",
+ "- Start Workspace discovery with the worker shell for grep, find, ls, cat, pwd, head, tail, sed, and wc.",
+ "- The workspace container is for Node, npm, package scripts, tests, and executable project scripts.",
+ "- Prefer file tools for exact edits and the worker shell for fast discovery; use the container when the command needs a real runtime or package install context.",
+ ].join("\n");
+}
+
+function sandboxRuntimePrompt(): string {
+ return [
+ "Runtime: Cloudflare Sandbox.",
+ "- Files and commands run in one container-backed project session.",
+ "- Sandbox file tools and exec see the same filesystem.",
+ "- Use exec freely for search, listing, package scripts, tests, and other shell-native workflows.",
+ "- Use read and edit when exact file contents or precise replacements are clearer than shell output.",
+ ].join("\n");
+}
diff --git a/examples/think-compare-runtimes/worker/think/real-turn.test.ts b/examples/think-compare-runtimes/worker/think/real-turn.test.ts
new file mode 100644
index 00000000..2ce6f6e4
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/real-turn.test.ts
@@ -0,0 +1,162 @@
+import { describe, expect, test } from "vitest";
+import { comparisonFixture } from "../../shared/fixture";
+import { RunEventRecorder } from "../run-events";
+import { createWorkspaceRuntimeAdapter } from "../runtime/adapter";
+import { runRealThinkTurn } from "./real-turn";
+
+describe("runRealThinkTurn", () => {
+ test("records model-backed Think turn start and completion", async () => {
+ const prompts: string[] = [];
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: {
+ async readFile() {
+ return "";
+ },
+ async writeFile() {},
+ },
+ runner: {
+ async exec() {
+ return { exitCode: 0, stdout: "", stderr: "", executionTarget: "workspace-container" };
+ },
+ },
+ });
+
+ await expect(
+ runRealThinkTurn({
+ adapter,
+ recorder,
+ fixture: comparisonFixture,
+ invoke: async ({ prompt }) => {
+ prompts.push(prompt);
+ return { text: "I updated the runtime-neutral fixture." };
+ },
+ }),
+ ).resolves.toEqual({ text: "I updated the runtime-neutral fixture." });
+
+ expect(prompts[0]).toContain(comparisonFixture.task);
+ expect(prompts[0]).toContain("/workspace/repo");
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Model-backed Think agent is running against the Workspace runtime.",
+ },
+ {
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn complete",
+ detail: "I updated the runtime-neutral fixture.",
+ },
+ ]);
+ });
+
+ test("treats empty assistant output as a failed turn", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: {
+ async readFile() {
+ return "";
+ },
+ async writeFile() {},
+ },
+ runner: {
+ async exec() {
+ return { exitCode: 0, stdout: "", stderr: "", executionTarget: "workspace-container" };
+ },
+ },
+ });
+
+ await expect(
+ runRealThinkTurn({
+ adapter,
+ recorder,
+ fixture: comparisonFixture,
+ invoke: async () => ({ text: " " }),
+ }),
+ ).rejects.toThrow("Think turn completed without assistant text");
+
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Model-backed Think agent is running against the Workspace runtime.",
+ },
+ {
+ runtime: "workspace",
+ kind: "agent_tool_error",
+ title: "Think turn failed",
+ detail: "Think turn completed without assistant text.",
+ },
+ ]);
+ });
+
+ test("records model-backed Think turn failures", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: {
+ async readFile() {
+ return "";
+ },
+ async writeFile() {},
+ },
+ runner: {
+ async exec() {
+ return { exitCode: 0, stdout: "", stderr: "", executionTarget: "workspace-container" };
+ },
+ },
+ });
+
+ await expect(
+ runRealThinkTurn({
+ adapter,
+ recorder,
+ fixture: comparisonFixture,
+ invoke: async () => {
+ throw new Error("model unavailable");
+ },
+ }),
+ ).rejects.toThrow("model unavailable");
+
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "workspace",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Model-backed Think agent is running against the Workspace runtime.",
+ },
+ {
+ runtime: "workspace",
+ kind: "agent_tool_error",
+ title: "Think turn failed",
+ detail: "model unavailable",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/real-turn.ts b/examples/think-compare-runtimes/worker/think/real-turn.ts
new file mode 100644
index 00000000..edf1f636
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/real-turn.ts
@@ -0,0 +1,62 @@
+import type { RuntimeId } from "../../shared/events";
+import type { ComparisonFixture } from "../../shared/fixture";
+import type { RuntimeAdapter } from "../runtime/adapter";
+import { createTaskPrompt } from "./prompts";
+import type { RuntimeThinkToolRecorder } from "./runtime-tools";
+
+export interface ThinkTurnInvocation {
+ prompt: string;
+}
+
+export interface ThinkTurnResult {
+ text: string;
+}
+
+export interface RealThinkTurnOptions {
+ adapter: RuntimeAdapter;
+ recorder: RuntimeThinkToolRecorder;
+ fixture: ComparisonFixture;
+ invoke(input: ThinkTurnInvocation): Promise;
+}
+
+export async function runRealThinkTurn({
+ adapter,
+ recorder,
+ fixture,
+ invoke,
+}: RealThinkTurnOptions): Promise {
+ const runtime = adapter.runtime;
+ await recorder.record({
+ runtime,
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: `Model-backed Think agent is running against the ${runtimeLabel(runtime)} runtime.`,
+ });
+
+ try {
+ const result = await invoke({ prompt: createTaskPrompt(fixture) });
+ if (result.text.trim().length === 0) {
+ throw new Error("Think turn completed without assistant text.");
+ }
+ await recorder.record({
+ runtime,
+ kind: "agent_message",
+ title: "Think turn complete",
+ detail: result.text,
+ });
+ return result;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ await recorder.record({
+ runtime,
+ kind: "agent_tool_error",
+ title: "Think turn failed",
+ detail: message,
+ });
+ throw error;
+ }
+}
+
+function runtimeLabel(runtime: RuntimeId): string {
+ return runtime === "workspace" ? "Workspace" : "Sandbox";
+}
diff --git a/examples/think-compare-runtimes/worker/think/remote-recorder.test.ts b/examples/think-compare-runtimes/worker/think/remote-recorder.test.ts
new file mode 100644
index 00000000..5c9390ce
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/remote-recorder.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, test } from "vitest";
+import type { RunEventInput } from "../run-events";
+import { createRemoteRunEventRecorder } from "./remote-recorder";
+
+describe("createRemoteRunEventRecorder", () => {
+ test("forwards agent events to CompareRun", async () => {
+ const inputs: RunEventInput[] = [];
+ const recorder = createRemoteRunEventRecorder({
+ async appendEvent(input) {
+ inputs.push(input);
+ return {
+ ...input,
+ id: "run-abc:0",
+ runId: "run-abc",
+ sequence: 0,
+ timestamp: "2026-06-04T00:00:00.000Z",
+ };
+ },
+ });
+
+ await recorder.record({
+ runtime: "sandbox",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Running.",
+ });
+
+ expect(inputs).toEqual([
+ {
+ runtime: "sandbox",
+ kind: "agent_message",
+ title: "Think turn started",
+ detail: "Running.",
+ },
+ ]);
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/remote-recorder.ts b/examples/think-compare-runtimes/worker/think/remote-recorder.ts
new file mode 100644
index 00000000..58e2d70e
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/remote-recorder.ts
@@ -0,0 +1,16 @@
+import type { RunEvent } from "../../shared/events";
+import type { RunEventInput } from "../run-events";
+
+export interface CompareRunEventSink {
+ appendEvent(input: RunEventInput): Promise;
+}
+
+export function createRemoteRunEventRecorder(sink: CompareRunEventSink): {
+ record(input: RunEventInput): Promise;
+} {
+ return {
+ record(input) {
+ return sink.appendEvent(input);
+ },
+ };
+}
diff --git a/examples/think-compare-runtimes/worker/think/runtime-tools.test.ts b/examples/think-compare-runtimes/worker/think/runtime-tools.test.ts
new file mode 100644
index 00000000..661d2f89
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/runtime-tools.test.ts
@@ -0,0 +1,164 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import type { RuntimeAdapter } from "../runtime/adapter";
+import { createRuntimeThinkTools, executeRuntimeThinkTool } from "./runtime-tools";
+
+function createAdapter(): RuntimeAdapter {
+ const files = new Map([
+ ["/workspace/repo/src/index.ts", "export const value = 1;\n"],
+ ]);
+
+ return {
+ runtime: "workspace",
+ files: {
+ async read(path) {
+ const content = files.get(path);
+ if (content === undefined) throw new Error(`missing ${path}`);
+ return content;
+ },
+ async write(path, contents) {
+ files.set(path, contents);
+ },
+ async edit(path, edits) {
+ const content = files.get(path);
+ if (content === undefined) throw new Error(`missing ${path}`);
+ let updated = content;
+ for (const edit of edits) {
+ updated = updated.replace(edit.oldText, edit.newText);
+ }
+ files.set(path, updated);
+ },
+ },
+ async exec(command, options) {
+ return {
+ exitCode: 0,
+ stdout: `${command} ${options?.cwd ?? ""}`.trim(),
+ stderr: "",
+ executionTarget: "workspace-container",
+ };
+ },
+ };
+}
+
+describe("createRuntimeThinkTools", () => {
+ test("wraps runtime file and exec capabilities as Think tools", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const tools = createRuntimeThinkTools({ adapter: createAdapter(), recorder });
+
+ await expect(
+ executeRuntimeThinkTool(tools, "read", { path: "/workspace/repo/src/index.ts" }),
+ ).resolves.toEqual({
+ path: "/workspace/repo/src/index.ts",
+ content: "export const value = 1;\n",
+ });
+ await expect(
+ executeRuntimeThinkTool(tools, "write", {
+ path: "/workspace/repo/NOTES.md",
+ contents: "status: pending\n",
+ }),
+ ).resolves.toEqual({ path: "/workspace/repo/NOTES.md", bytesWritten: 16 });
+ await expect(
+ executeRuntimeThinkTool(tools, "edit", {
+ path: "/workspace/repo/NOTES.md",
+ edits: [{ oldText: "pending", newText: "done" }],
+ }),
+ ).resolves.toEqual({ path: "/workspace/repo/NOTES.md", editsApplied: 1 });
+ await expect(
+ executeRuntimeThinkTool(tools, "exec", { command: "node --version" }),
+ ).resolves.toEqual({
+ command: "node --version",
+ cwd: "/workspace/repo",
+ exitCode: 0,
+ stdout: "node --version /workspace/repo",
+ stderr: "",
+ executionTarget: "workspace-container",
+ });
+
+ expect(recorder.events().map(({ runtime, kind, title }) => ({ runtime, kind, title }))).toEqual(
+ [
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested read" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think read result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested write" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think write result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested edit" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think edit result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested exec" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think exec result" },
+ ],
+ );
+ });
+
+ test("exposes schemas usable by a real Think model loop", () => {
+ const recorder = new RunEventRecorder({ runId: "run-abc" });
+ const tools = createRuntimeThinkTools({ adapter: createAdapter(), recorder });
+
+ expect(tools.read.inputSchema.safeParse({ path: "/workspace/repo/src/index.ts" }).success).toBe(
+ true,
+ );
+ expect(tools.write.inputSchema.safeParse({ path: "/workspace/repo/a.txt" }).success).toBe(
+ false,
+ );
+ expect(
+ tools.edit.inputSchema.safeParse({
+ path: "/workspace/repo/a.txt",
+ edits: [{ oldText: "a", newText: "b" }],
+ }).success,
+ ).toBe(true);
+ expect(
+ tools.exec.inputSchema.safeParse({ command: "npm test", timeoutMs: 30_000 }).success,
+ ).toBe(true);
+ expect(tools.read.inputSchema.safeParse({ path: "/tmp/outside.txt" }).success).toBe(false);
+ expect(tools.exec.inputSchema.safeParse({ command: "pwd", cwd: "/tmp" }).success).toBe(false);
+ });
+
+ test("records Think tool errors", async () => {
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const tools = createRuntimeThinkTools({ adapter: createAdapter(), recorder });
+
+ await expect(
+ executeRuntimeThinkTool(tools, "read", { path: "/workspace/repo/missing.ts" }),
+ ).resolves.toEqual({ error: "missing /workspace/repo/missing.ts" });
+
+ expect(
+ recorder
+ .events()
+ .map(({ runtime, kind, title, detail }) => ({ runtime, kind, title, detail })),
+ ).toEqual([
+ {
+ runtime: "workspace",
+ kind: "agent_tool_call",
+ title: "Think requested read",
+ detail: '{"path":"/workspace/repo/missing.ts"}',
+ },
+ {
+ runtime: "workspace",
+ kind: "agent_tool_error",
+ title: "Think read error",
+ detail:
+ '{"path":"/workspace/repo/missing.ts","error":"missing /workspace/repo/missing.ts"}',
+ },
+ ]);
+ });
+
+ test("rejects runtime tool paths outside the seeded project", async () => {
+ const recorder = new RunEventRecorder({ runId: "run-abc" });
+ const tools = createRuntimeThinkTools({ adapter: createAdapter(), recorder });
+
+ await expect(
+ executeRuntimeThinkTool(tools, "read", { path: "/tmp/secret.txt" }),
+ ).resolves.toEqual({
+ error: "Path must be under /workspace/repo.",
+ });
+ await expect(
+ executeRuntimeThinkTool(tools, "exec", { command: "pwd", cwd: "/tmp" }),
+ ).resolves.toEqual({
+ error: "Path must be under /workspace/repo.",
+ });
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/runtime-tools.ts b/examples/think-compare-runtimes/worker/think/runtime-tools.ts
new file mode 100644
index 00000000..0d5c182b
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/runtime-tools.ts
@@ -0,0 +1,208 @@
+import { z } from "zod";
+import type { RunEvent, RuntimeId } from "../../shared/events";
+import type { RunEventInput } from "../run-events";
+import type { RuntimeAdapter } from "../runtime/adapter";
+import { createRuntimeToolDescriptions } from "./prompts";
+
+type RuntimeThinkToolName = "read" | "write" | "edit" | "exec";
+
+type RuntimeThinkTool = {
+ description: string;
+ inputSchema: z.ZodType;
+ execute(input: unknown): Promise;
+};
+
+type RuntimeThinkToolSet = Record;
+
+const projectRoot = "/workspace/repo";
+
+const projectPathSchema = z.string().refine(isProjectPath, `Path must be under ${projectRoot}.`);
+
+const readInputSchema = z.object({
+ path: projectPathSchema.describe("Absolute path under /workspace/repo to read."),
+});
+
+const writeInputSchema = z.object({
+ path: projectPathSchema.describe("Absolute path under /workspace/repo to create or overwrite."),
+ contents: z.string().describe("Complete file contents to write. This replaces the whole file."),
+});
+
+const editInputSchema = z.object({
+ path: projectPathSchema.describe("Absolute path under /workspace/repo to edit."),
+ edits: z
+ .array(
+ z.object({
+ oldText: z
+ .string()
+ .describe("Exact text that appears once in the current file, including whitespace."),
+ newText: z.string().describe("Replacement text."),
+ }),
+ )
+ .min(1)
+ .describe("Exact replacements to apply."),
+});
+
+const execInputSchema = z.object({
+ command: z.string().min(1).describe("Shell command to run."),
+ cwd: projectPathSchema
+ .optional()
+ .describe("Working directory for the command. Defaults to /workspace/repo."),
+ timeoutMs: z.number().int().positive().optional().describe("Command timeout in milliseconds."),
+});
+
+export interface RuntimeThinkToolRecorder {
+ record(input: RunEventInput): RunEvent | Promise;
+}
+
+export interface RuntimeThinkToolsOptions {
+ adapter: RuntimeAdapter;
+ recorder: RuntimeThinkToolRecorder;
+}
+
+export function createRuntimeThinkTools({
+ adapter,
+ recorder,
+}: RuntimeThinkToolsOptions): RuntimeThinkToolSet {
+ const runtime = adapter.runtime;
+ const descriptions = createRuntimeToolDescriptions(runtime);
+
+ return {
+ read: createRuntimeThinkTool({
+ runtime,
+ recorder,
+ name: "read",
+ description: descriptions.read,
+ inputSchema: readInputSchema,
+ execute: async (input) => {
+ const { path } = readInputSchema.parse(input);
+ return { path, content: await adapter.files.read(path) };
+ },
+ }),
+ write: createRuntimeThinkTool({
+ runtime,
+ recorder,
+ name: "write",
+ description: descriptions.write,
+ inputSchema: writeInputSchema,
+ execute: async (input) => {
+ const { path, contents } = writeInputSchema.parse(input);
+ await adapter.files.write(path, contents);
+ return { path, bytesWritten: byteLength(contents) };
+ },
+ }),
+ edit: createRuntimeThinkTool({
+ runtime,
+ recorder,
+ name: "edit",
+ description: descriptions.edit,
+ inputSchema: editInputSchema,
+ execute: async (input) => {
+ const { path, edits } = editInputSchema.parse(input);
+ await adapter.files.edit(path, edits);
+ return { path, editsApplied: edits.length };
+ },
+ }),
+ exec: createRuntimeThinkTool({
+ runtime,
+ recorder,
+ name: "exec",
+ description: descriptions.exec,
+ inputSchema: execInputSchema,
+ execute: async (input) => {
+ const { command, cwd: parsedCwd, timeoutMs } = execInputSchema.parse(input);
+ const cwd = parsedCwd ?? projectRoot;
+ const result = await adapter.exec(command, { cwd, timeoutMs });
+ return { command, cwd, ...result };
+ },
+ }),
+ };
+}
+
+export async function executeRuntimeThinkTool(
+ tools: RuntimeThinkToolSet,
+ name: RuntimeThinkToolName,
+ input: unknown,
+): Promise {
+ return tools[name].execute(input);
+}
+
+interface CreateRuntimeThinkToolOptions {
+ runtime: RuntimeId;
+ recorder: RuntimeThinkToolRecorder;
+ name: RuntimeThinkToolName;
+ description: string;
+ inputSchema: z.ZodType;
+ execute(input: unknown): Promise;
+}
+
+function createRuntimeThinkTool({
+ runtime,
+ recorder,
+ name,
+ description,
+ inputSchema,
+ execute,
+}: CreateRuntimeThinkToolOptions): RuntimeThinkTool {
+ return {
+ description,
+ inputSchema,
+ async execute(input) {
+ await recorder.record({
+ runtime,
+ kind: "agent_tool_call",
+ title: `Think requested ${name}`,
+ detail: stringifyForEvent(input),
+ });
+
+ try {
+ const result = await execute(input);
+ await recorder.record({
+ runtime,
+ kind: "agent_tool_result",
+ title: `Think ${name} result`,
+ detail: stringifyForEvent(result),
+ });
+ return result;
+ } catch (error) {
+ const message = formatToolError(error);
+ await recorder.record({
+ runtime,
+ kind: "agent_tool_error",
+ title: `Think ${name} error`,
+ detail: stringifyErrorForEvent(inputSchema, input, message),
+ });
+ return { error: message };
+ }
+ },
+ };
+}
+
+function isProjectPath(path: string): boolean {
+ return path === projectRoot || path.startsWith(`${projectRoot}/`);
+}
+
+function formatToolError(error: unknown): string {
+ if (error instanceof z.ZodError) {
+ return error.issues.map((issue) => issue.message).join("; ");
+ }
+ return error instanceof Error ? error.message : String(error);
+}
+
+function stringifyErrorForEvent(inputSchema: z.ZodType, input: unknown, error: string): string {
+ const parsed = inputSchema.safeParse(input);
+ if (!parsed.success || typeof parsed.data !== "object" || parsed.data === null) return error;
+ return stringifyForEvent({ ...(parsed.data as Record), error });
+}
+
+function stringifyForEvent(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value);
+ } catch {
+ return String(value);
+ }
+}
+
+function byteLength(contents: string): number {
+ return new TextEncoder().encode(contents).byteLength;
+}
diff --git a/examples/think-compare-runtimes/worker/think/scripted-turn.test.ts b/examples/think-compare-runtimes/worker/think/scripted-turn.test.ts
new file mode 100644
index 00000000..0bb54a12
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/scripted-turn.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, test } from "vitest";
+import { RunEventRecorder } from "../run-events";
+import { createWorkspaceRuntimeAdapter } from "../runtime/adapter";
+import { runScriptedThinkToolSmoke } from "./scripted-turn";
+
+describe("runScriptedThinkToolSmoke", () => {
+ test("drives the Think-facing tools through a deterministic transcript", async () => {
+ const files = new Map([
+ ["/workspace/repo/feature-briefs/smart-request-policies.md", "# Smart Request Policies\n"],
+ ]);
+ const recorder = new RunEventRecorder({
+ runId: "run-abc",
+ now: () => "2026-06-04T00:00:00.000Z",
+ });
+ const adapter = createWorkspaceRuntimeAdapter({
+ recorder,
+ store: {
+ async readFile(path) {
+ const contents = files.get(path);
+ if (contents === undefined) throw new Error(`missing ${path}`);
+ return contents;
+ },
+ async writeFile(path, contents) {
+ files.set(path, contents);
+ },
+ },
+ runner: {
+ async exec(command) {
+ return {
+ exitCode: 0,
+ stdout: `${command}\n`,
+ stderr: "",
+ executionTarget: "workspace-container",
+ };
+ },
+ },
+ });
+
+ await runScriptedThinkToolSmoke({ adapter, recorder, root: "/workspace/repo" });
+
+ expect(files.get("/workspace/repo/THINK_NOTES.md")).toBe("Think tool smoke: done\n");
+ expect(recorder.events().map(({ runtime, kind, title }) => ({ runtime, kind, title }))).toEqual(
+ [
+ { runtime: "workspace", kind: "agent_message", title: "Scripted Think turn started" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested read" },
+ {
+ runtime: "workspace",
+ kind: "tool_call",
+ title: "read /workspace/repo/feature-briefs/smart-request-policies.md",
+ },
+ { runtime: "workspace", kind: "tool_result", title: "read complete" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think read result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested write" },
+ { runtime: "workspace", kind: "tool_call", title: "write /workspace/repo/THINK_NOTES.md" },
+ { runtime: "workspace", kind: "tool_result", title: "write complete" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think write result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested edit" },
+ { runtime: "workspace", kind: "tool_call", title: "edit /workspace/repo/THINK_NOTES.md" },
+ { runtime: "workspace", kind: "tool_result", title: "edit complete" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think edit result" },
+ { runtime: "workspace", kind: "agent_tool_call", title: "Think requested exec" },
+ { runtime: "workspace", kind: "tool_call", title: "exec node --version" },
+ { runtime: "workspace", kind: "tool_result", title: "exec complete" },
+ { runtime: "workspace", kind: "agent_tool_result", title: "Think exec result" },
+ { runtime: "workspace", kind: "agent_message", title: "Scripted Think turn complete" },
+ ],
+ );
+ });
+});
diff --git a/examples/think-compare-runtimes/worker/think/scripted-turn.ts b/examples/think-compare-runtimes/worker/think/scripted-turn.ts
new file mode 100644
index 00000000..3b131432
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/think/scripted-turn.ts
@@ -0,0 +1,45 @@
+import type { RunEventRecorder } from "../run-events";
+import type { RuntimeAdapter } from "../runtime/adapter";
+import { createRuntimeThinkTools, executeRuntimeThinkTool } from "./runtime-tools";
+
+export interface ScriptedThinkToolSmokeOptions {
+ adapter: RuntimeAdapter;
+ recorder: RunEventRecorder;
+ root: string;
+}
+
+export async function runScriptedThinkToolSmoke({
+ adapter,
+ recorder,
+ root,
+}: ScriptedThinkToolSmokeOptions): Promise {
+ const runtime = adapter.runtime;
+ const tools = createRuntimeThinkTools({ adapter, recorder });
+
+ recorder.record({
+ runtime,
+ kind: "agent_message",
+ title: "Scripted Think turn started",
+ detail: "Deterministic harness is exercising the Think-facing tool surface.",
+ });
+
+ await executeRuntimeThinkTool(tools, "read", {
+ path: `${root}/feature-briefs/smart-request-policies.md`,
+ });
+ await executeRuntimeThinkTool(tools, "write", {
+ path: `${root}/THINK_NOTES.md`,
+ contents: "Think tool smoke: pending\n",
+ });
+ await executeRuntimeThinkTool(tools, "edit", {
+ path: `${root}/THINK_NOTES.md`,
+ edits: [{ oldText: "pending", newText: "done" }],
+ });
+ await executeRuntimeThinkTool(tools, "exec", { command: "node --version" });
+
+ recorder.record({
+ runtime,
+ kind: "agent_message",
+ title: "Scripted Think turn complete",
+ detail: "Think-facing tools completed through the runtime adapter.",
+ });
+}
diff --git a/examples/think-compare-runtimes/worker/workspace-container-pool.test.ts b/examples/think-compare-runtimes/worker/workspace-container-pool.test.ts
new file mode 100644
index 00000000..9912b644
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/workspace-container-pool.test.ts
@@ -0,0 +1,93 @@
+import { describe, expect, test, vi } from "vitest";
+import type { WorkspaceContainerHost } from "./workspace-container-pool";
+
+vi.mock("cloudflare:workers", () => ({
+ DurableObject: class {},
+ RpcTarget: class {},
+}));
+
+describe("createWorkspaceWarmPoolRuntime", () => {
+ test("keeps Workspace warm-start readiness inside the container host", async () => {
+ const { createWorkspaceWarmPoolRuntime } = await import("./workspace-container-pool");
+ const calls: string[] = [];
+ const host = {
+ async startWarmContainer(env: Record, inactivityTimeoutMs: number) {
+ calls.push(`start ${env.PORT} ${env.MOUNT_POINT} ${env.FUSE_MOUNT} ${inactivityTimeoutMs}`);
+ },
+ async destroyWarmContainer() {
+ calls.push("destroy");
+ },
+ async isWarmContainerHealthy() {
+ calls.push("healthy");
+ return true;
+ },
+ async getWorkspaceContainer() {
+ throw new Error("readiness should stay in the host DO");
+ },
+ };
+ const runtime = createWorkspaceWarmPoolRuntime({
+ CONTAINER_SLEEP_AFTER: "2m",
+ WorkspaceContainerHost: namespaceFor(host),
+ WARM_POOL_REFRESH_INTERVAL: "10000",
+ WARM_POOL_TARGET: "2",
+ FUSE_MOUNT: "shim",
+ });
+
+ await runtime.startContainer("warm-a");
+ await expect(runtime.isContainerRunning("warm-a")).resolves.toBe(true);
+
+ expect(calls).toEqual(["start 8080 /workspace shim 120000", "healthy"]);
+ });
+
+ test("retries Workspace container placement while waiting for health", async () => {
+ const { startWorkspaceContainerAndWait } = await import("./workspace-container-pool");
+ const calls: string[] = [];
+ let healthAttempts = 0;
+ const container = {
+ running: false,
+ async setInactivityTimeout(durationMs: number) {
+ calls.push(`timeout ${durationMs}`);
+ },
+ start() {
+ calls.push("start");
+ },
+ getTcpPort() {
+ return {
+ async fetch() {
+ healthAttempts += 1;
+ calls.push(`health ${healthAttempts}`);
+ if (healthAttempts < 3) {
+ throw new Error(
+ "There is no container instance that can be provided to this Durable Object, try again later",
+ );
+ }
+ container.running = true;
+ return new Response(null, { status: 200 });
+ },
+ } as unknown as Fetcher;
+ },
+ };
+
+ await startWorkspaceContainerAndWait(container, { PORT: "8080" }, 120_000, {
+ attempts: 3,
+ wait: async () => {},
+ });
+
+ expect(calls).toEqual([
+ "timeout 120000",
+ "start",
+ "health 1",
+ "start",
+ "health 2",
+ "start",
+ "health 3",
+ ]);
+ });
+});
+
+function namespaceFor(stub: T): DurableObjectNamespace {
+ return {
+ idFromName: (name: string) => name,
+ get: () => stub,
+ } as unknown as DurableObjectNamespace;
+}
diff --git a/examples/think-compare-runtimes/worker/workspace-container-pool.ts b/examples/think-compare-runtimes/worker/workspace-container-pool.ts
new file mode 100644
index 00000000..3461dabe
--- /dev/null
+++ b/examples/think-compare-runtimes/worker/workspace-container-pool.ts
@@ -0,0 +1,166 @@
+import { DurableObject } from "cloudflare:workers";
+import {
+ type IWorkspaceContainerAPI,
+ withWorkspaceContainer,
+} from "@cloudflare/workspace/backends/container";
+import { type ContainerPoolConfigEnv, containerSleepAfterMs } from "./container-config";
+import type { WarmPoolRuntime } from "./container-pool-manager";
+import { ContainerWarmPool } from "./container-warm-pool";
+
+const WORKSPACE_PORT = 8080;
+const WORKSPACE_HEALTH_INTERVAL_MS = 250;
+
+export interface WorkspacePoolEnv extends ContainerPoolConfigEnv {
+ FUSE_MOUNT?: string;
+ WorkspaceContainerHost: DurableObjectNamespace;
+}
+
+export interface WorkspaceContainerHostHandle {
+ getWorkspaceContainer(): IWorkspaceContainerAPI | Promise;
+ startWarmContainer(env: Record, inactivityTimeoutMs: number): Promise