From 71906dc51d3b06c47578a34cd95ab5ddf2af2662 Mon Sep 17 00:00:00 2001 From: Naresh Date: Thu, 11 Jun 2026 09:59:06 +0100 Subject: [PATCH 1/5] rpc, workspace: Build dependencies first Direct package builds depend on generated output from upstream workspace packages. Build dofs before workspace-rpc, and build workspace-rpc before workspace, so fresh checkouts can run package builds without relying on stale dist directories. --- packages/rpc/package.json | 2 ++ packages/workspace/package.json | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rpc/package.json b/packages/rpc/package.json index ff0f3cb3..de7d09fd 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -27,7 +27,9 @@ }, "types": "./dist/index.d.ts", "scripts": { + "prebuild": "npm run build --workspace @cloudflare/dofs", "build": "tsc -p tsconfig.build.json", + "pretypecheck": "npm run build --workspace @cloudflare/dofs", "typecheck": "tsc -p tsconfig.build.json --noEmit", "test": "vitest run" }, diff --git a/packages/workspace/package.json b/packages/workspace/package.json index c339a83b..13d0664c 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -39,10 +39,11 @@ "README.md" ], "scripts": { + "build:deps": "npm run build --workspace @cloudflare/workspace-rpc", "build:shell-bundle": "node ./src/backends/worker/script/build-bundle.mjs", - "prebuild": "npm run build:shell-bundle", + "prebuild": "npm run build:deps && npm run build:shell-bundle", "pretest": "npm run build:shell-bundle", - "pretypecheck": "npm run build:shell-bundle", + "pretypecheck": "npm run build:deps && npm run build:shell-bundle", "prepare": "npm run build:shell-bundle", "build": "rolldown -c", "typecheck": "tsc -p tsconfig.build.json --noEmit", From 72dc1254e4171e1012b0fc7c8379c8187691af61 Mon Sep 17 00:00:00 2001 From: Naresh Date: Thu, 11 Jun 2026 10:01:18 +0100 Subject: [PATCH 2/5] examples/think: Add runtime comparison Add a Think-vs-Think example that compares Workspace and Sandbox against the same docs task. The Workspace side uses durable file operations, a worker-shell backend for lightweight commands, and a container backend for real Linux tooling. The Sandbox side uses the Sandbox SDK session for files and exec. The example streams run events over PartyServer and renders a compact runtime dashboard so the difference between direct file work, shell commands, and container work is visible during a run. --- .../think-compare-runtimes/.dev.vars.example | 7 + examples/think-compare-runtimes/.gitignore | 31 + .../think-compare-runtimes/Dockerfile.sandbox | 6 + .../Dockerfile.workspace | 30 + examples/think-compare-runtimes/index.html | 12 + examples/think-compare-runtimes/package.json | 56 + .../think-compare-runtimes/shared/events.ts | 28 + .../shared/fixture.test.ts | 191 + .../think-compare-runtimes/shared/fixture.ts | 166 + .../think-compare-runtimes/src/App.test.tsx | 314 + examples/think-compare-runtimes/src/App.tsx | 584 ++ .../src/auto-scroll-list.test.tsx | 62 + .../src/auto-scroll-list.tsx | 34 + .../src/dashboard-model.test.ts | 194 + .../src/dashboard-model.ts | 174 + .../src/event-lanes.test.ts | 67 + .../think-compare-runtimes/src/event-lanes.ts | 78 + examples/think-compare-runtimes/src/main.tsx | 16 + .../src/markdown-text.tsx | 242 + .../src/run-state.test.ts | 112 + .../think-compare-runtimes/src/run-state.ts | 116 + .../think-compare-runtimes/src/styles.css | 31 + .../think-compare-runtimes/tsconfig.app.json | 21 + examples/think-compare-runtimes/tsconfig.json | 8 + .../think-compare-runtimes/tsconfig.node.json | 19 + .../tsconfig.worker.json | 19 + .../think-compare-runtimes/vite.config.ts | 8 + .../think-compare-runtimes/vitest.config.ts | 7 + .../worker/comparison-agents.test.ts | 59 + .../worker/comparison-agents.ts | 76 + .../worker/container-config.ts | 53 + .../worker/container-pool-manager.ts | 218 + .../worker/container-pools.test.ts | 305 + .../worker/container-pools.ts | 13 + .../worker/container-warm-pool.ts | 84 + .../worker/http.test.ts | 38 + .../think-compare-runtimes/worker/http.ts | 21 + .../think-compare-runtimes/worker/index.ts | 171 + .../worker/run-events.test.ts | 49 + .../worker/run-events.ts | 54 + .../worker/runs.test.ts | 20 + .../think-compare-runtimes/worker/runs.ts | 17 + .../worker/runtime-agent-handles.test.ts | 50 + .../worker/runtime-agent-handles.ts | 43 + .../worker/runtime/adapter.test.ts | 99 + .../worker/runtime/adapter.ts | 86 + .../worker/runtime/comparison-run.test.ts | 126 + .../worker/runtime/comparison-run.ts | 90 + .../worker/runtime/exec-tools.test.ts | 85 + .../worker/runtime/exec-tools.ts | 71 + .../worker/runtime/file-tools.test.ts | 79 + .../worker/runtime/file-tools.ts | 133 + .../worker/runtime/instrumented.test.ts | 71 + .../worker/runtime/instrumented.ts | 48 + .../worker/runtime/sandbox-run.test.ts | 75 + .../worker/runtime/sandbox-run.ts | 41 + .../worker/runtime/sandbox.test.ts | 88 + .../worker/runtime/sandbox.ts | 68 + .../worker/runtime/seed.test.ts | 36 + .../worker/runtime/seed.ts | 39 + .../worker/runtime/workspace-run.test.ts | 79 + .../worker/runtime/workspace-run.ts | 41 + .../worker/runtime/workspace.test.ts | 132 + .../worker/runtime/workspace.ts | 86 + .../worker/sandbox-container-pool.ts | 61 + .../worker/start-run.test.ts | 39 + .../worker/start-run.ts | 22 + .../worker/think/agent-starter.test.ts | 217 + .../worker/think/agent-starter.ts | 53 + .../worker/think/agents.test.ts | 374 ++ .../worker/think/agents.ts | 366 + .../worker/think/model.test.ts | 28 + .../worker/think/model.ts | 7 + .../worker/think/prompts.test.ts | 71 + .../worker/think/prompts.ts | 114 + .../worker/think/real-turn.test.ts | 162 + .../worker/think/real-turn.ts | 62 + .../worker/think/remote-recorder.test.ts | 37 + .../worker/think/remote-recorder.ts | 16 + .../worker/think/runtime-tools.test.ts | 143 + .../worker/think/runtime-tools.ts | 187 + .../worker/think/scripted-turn.test.ts | 64 + .../worker/think/scripted-turn.ts | 45 + .../worker/workspace-container-pool.test.ts | 92 + .../worker/workspace-container-pool.ts | 166 + .../worker/wrangler-config.test.ts | 81 + .../think-compare-runtimes/wrangler.jsonc | 109 + package-lock.json | 5971 +++++++++++++---- 88 files changed, 12605 insertions(+), 1359 deletions(-) create mode 100644 examples/think-compare-runtimes/.dev.vars.example create mode 100644 examples/think-compare-runtimes/.gitignore create mode 100644 examples/think-compare-runtimes/Dockerfile.sandbox create mode 100644 examples/think-compare-runtimes/Dockerfile.workspace create mode 100644 examples/think-compare-runtimes/index.html create mode 100644 examples/think-compare-runtimes/package.json create mode 100644 examples/think-compare-runtimes/shared/events.ts create mode 100644 examples/think-compare-runtimes/shared/fixture.test.ts create mode 100644 examples/think-compare-runtimes/shared/fixture.ts create mode 100644 examples/think-compare-runtimes/src/App.test.tsx create mode 100644 examples/think-compare-runtimes/src/App.tsx create mode 100644 examples/think-compare-runtimes/src/auto-scroll-list.test.tsx create mode 100644 examples/think-compare-runtimes/src/auto-scroll-list.tsx create mode 100644 examples/think-compare-runtimes/src/dashboard-model.test.ts create mode 100644 examples/think-compare-runtimes/src/dashboard-model.ts create mode 100644 examples/think-compare-runtimes/src/event-lanes.test.ts create mode 100644 examples/think-compare-runtimes/src/event-lanes.ts create mode 100644 examples/think-compare-runtimes/src/main.tsx create mode 100644 examples/think-compare-runtimes/src/markdown-text.tsx create mode 100644 examples/think-compare-runtimes/src/run-state.test.ts create mode 100644 examples/think-compare-runtimes/src/run-state.ts create mode 100644 examples/think-compare-runtimes/src/styles.css create mode 100644 examples/think-compare-runtimes/tsconfig.app.json create mode 100644 examples/think-compare-runtimes/tsconfig.json create mode 100644 examples/think-compare-runtimes/tsconfig.node.json create mode 100644 examples/think-compare-runtimes/tsconfig.worker.json create mode 100644 examples/think-compare-runtimes/vite.config.ts create mode 100644 examples/think-compare-runtimes/vitest.config.ts create mode 100644 examples/think-compare-runtimes/worker/comparison-agents.test.ts create mode 100644 examples/think-compare-runtimes/worker/comparison-agents.ts create mode 100644 examples/think-compare-runtimes/worker/container-config.ts create mode 100644 examples/think-compare-runtimes/worker/container-pool-manager.ts create mode 100644 examples/think-compare-runtimes/worker/container-pools.test.ts create mode 100644 examples/think-compare-runtimes/worker/container-pools.ts create mode 100644 examples/think-compare-runtimes/worker/container-warm-pool.ts create mode 100644 examples/think-compare-runtimes/worker/http.test.ts create mode 100644 examples/think-compare-runtimes/worker/http.ts create mode 100644 examples/think-compare-runtimes/worker/index.ts create mode 100644 examples/think-compare-runtimes/worker/run-events.test.ts create mode 100644 examples/think-compare-runtimes/worker/run-events.ts create mode 100644 examples/think-compare-runtimes/worker/runs.test.ts create mode 100644 examples/think-compare-runtimes/worker/runs.ts create mode 100644 examples/think-compare-runtimes/worker/runtime-agent-handles.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime-agent-handles.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/adapter.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/adapter.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/comparison-run.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/comparison-run.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/exec-tools.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/exec-tools.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/file-tools.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/file-tools.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/instrumented.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/instrumented.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/sandbox-run.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/sandbox-run.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/sandbox.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/sandbox.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/seed.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/seed.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/workspace-run.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/workspace-run.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/workspace.test.ts create mode 100644 examples/think-compare-runtimes/worker/runtime/workspace.ts create mode 100644 examples/think-compare-runtimes/worker/sandbox-container-pool.ts create mode 100644 examples/think-compare-runtimes/worker/start-run.test.ts create mode 100644 examples/think-compare-runtimes/worker/start-run.ts create mode 100644 examples/think-compare-runtimes/worker/think/agent-starter.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/agent-starter.ts create mode 100644 examples/think-compare-runtimes/worker/think/agents.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/agents.ts create mode 100644 examples/think-compare-runtimes/worker/think/model.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/model.ts create mode 100644 examples/think-compare-runtimes/worker/think/prompts.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/prompts.ts create mode 100644 examples/think-compare-runtimes/worker/think/real-turn.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/real-turn.ts create mode 100644 examples/think-compare-runtimes/worker/think/remote-recorder.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/remote-recorder.ts create mode 100644 examples/think-compare-runtimes/worker/think/runtime-tools.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/runtime-tools.ts create mode 100644 examples/think-compare-runtimes/worker/think/scripted-turn.test.ts create mode 100644 examples/think-compare-runtimes/worker/think/scripted-turn.ts create mode 100644 examples/think-compare-runtimes/worker/workspace-container-pool.test.ts create mode 100644 examples/think-compare-runtimes/worker/workspace-container-pool.ts create mode 100644 examples/think-compare-runtimes/worker/wrangler-config.test.ts create mode 100644 examples/think-compare-runtimes/wrangler.jsonc diff --git a/examples/think-compare-runtimes/.dev.vars.example b/examples/think-compare-runtimes/.dev.vars.example new file mode 100644 index 00000000..0e495e88 --- /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. +FUSE_SHIM=1 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..813c6765 --- /dev/null +++ b/examples/think-compare-runtimes/shared/events.ts @@ -0,0 +1,28 @@ +export type RuntimeId = "workspace" | "sandbox"; +export type EventRuntime = RuntimeId | "both"; + +export type RunEventKind = + | "run_started" + | "run_completed" + | "runtime_started" + | "runtime_completed" + | "runtime_failed" + | "runtime_note" + | "agent_message" + | "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..b9d6c8a5 --- /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 if useful 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..03f43467 --- /dev/null +++ b/examples/think-compare-runtimes/src/App.test.tsx @@ -0,0 +1,314 @@ +// @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 the idle instrument cluster with task metadata and two runtime wings", () => { + vi.stubGlobal("fetch", vi.fn()); + + render(); + + expect(screen.getByText("THINK · RUNTIME COMPARE")).toBeTruthy(); + expect(screen.getByText("TASK")).toBeTruthy(); + expect(screen.getByRole("button", { name: "START RUN" })).toBeTruthy(); + expect(screen.getByText(/Add documentation for Smart Request Policies/)).toBeTruthy(); + + const workspace = screen.getByLabelText("Workspace runtime wing"); + const sandbox = screen.getByLabelText("Sandbox runtime wing"); + + expect(within(workspace).getByText("L · WORKSPACE")).toBeTruthy(); + expect(within(workspace).getByText("@cloudflare/workspace")).toBeTruthy(); + expect(within(workspace).getByText("Durable files + routed exec")).toBeTruthy(); + expect( + within(workspace).getByText( + "Direct file tools use durable storage. Exec routes to the worker shell first, then container for real binaries.", + ), + ).toBeTruthy(); + expect(within(workspace).getByText("Shell")).toBeTruthy(); + expect(within(workspace).getByText("Container")).toBeTruthy(); + expect(within(workspace).getByText("asleep")).toBeTruthy(); + expect(within(workspace).getByText("◇ planned · seeds into DOFS on run start")).toBeTruthy(); + + expect(within(sandbox).getByText("R · SANDBOX")).toBeTruthy(); + expect(within(sandbox).getByText("@cloudflare/sandbox")).toBeTruthy(); + expect(within(sandbox).getByText("Container filesystem")).toBeTruthy(); + expect( + within(sandbox).getByText( + "Same fixture is seeded into the Sandbox filesystem. File tools and exec run there.", + ), + ).toBeTruthy(); + expect(within(sandbox).getByText("Container")).toBeTruthy(); + }); + + test("starts a comparison run from the top bar", 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: "2026-06-04T00:00:00.000Z", + }), + ], + }, + { 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(await screen.findByText(/^RUN · /)).toBeTruthy(); + expect(screen.getByText("run-123")).toBeTruthy(); + }); + + 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(); + }); + + expect(screen.getByText("RUN · 00:00")).toBeTruthy(); + + act(() => { + vi.setSystemTime(new Date("2026-06-04T00:00:01.000Z")); + vi.advanceTimersByTime(1000); + }); + + expect(screen.getByText("RUN · 00:02")).toBeTruthy(); + expect(within(screen.getByLabelText("Workspace runtime wing")).getByText("Files")).toBeTruthy(); + }); + + test("renders telemetry and grouped activity for live runtime events", 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", + title: "Workspace runtime started", + timestamp: "2026-06-04T00:00:01.000Z", + }), + event({ + sequence: 2, + runtime: "workspace", + kind: "tool_call", + title: "read called", + detail: JSON.stringify({ path: "/workspace/repo/src/policy.ts" }), + timestamp: "2026-06-04T00:00:02.000Z", + }), + event({ + sequence: 3, + runtime: "workspace", + kind: "agent_message", + title: "Workspace response", + detail: "I found the policy helper.", + timestamp: "2026-06-04T00:00:03.000Z", + }), + event({ + sequence: 4, + runtime: "sandbox", + kind: "runtime_started", + title: "Sandbox runtime started", + timestamp: "2026-06-04T00:00:04.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:05.000Z", + }), + ]), + ); + + render(); + fireEvent.click(screen.getByRole("button", { name: "START RUN" })); + + await screen.findByText("read called"); + const workspace = screen.getByLabelText("Workspace runtime wing"); + const sandbox = screen.getByLabelText("Sandbox runtime wing"); + + expect(within(workspace).getByText("Files")).toBeTruthy(); + expect(within(workspace).getByText("1")).toBeTruthy(); + expect(within(workspace).getByText("Shell")).toBeTruthy(); + expect(within(workspace).getByText("0")).toBeTruthy(); + expect(within(workspace).getByText("asleep")).toBeTruthy(); + expect(within(workspace).getByText("I found the policy helper.")).toBeTruthy(); + expect(within(workspace).getByText("read called")).toBeTruthy(); + + expect(within(sandbox).getByText("Container")).toBeTruthy(); + expect(within(sandbox).getAllByText("1").length).toBeGreaterThanOrEqual(1); + expect(within(sandbox).getByText("Think requested exec")).toBeTruthy(); + expect(within(sandbox).getByText("npm test")).toBeTruthy(); + }); + + test("renders assistant response details as Markdown", async () => { + vi.stubGlobal( + "fetch", + sessionWithEvents([ + event({ + sequence: 0, + runtime: "workspace", + kind: "agent_message", + title: "Think turn complete", + detail: + "## Summary of Changes\n\nI modified `src/index.ts`.\n\n1. **Empty array handling**: Added a guard.\n2. **Consistent decimal formatting**: Used `toFixed(1)`.\n\n### Runtime Observations\n\n- `npm test` passed.\n- No dependencies were needed.", + 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).getByRole("heading", { name: "Runtime Observations" })).toBeTruthy(); + expect(within(workspace).getByText("src/index.ts")).toBeTruthy(); + expect(within(workspace).getByText("Empty array handling")).toBeTruthy(); + expect(within(workspace).getByText("npm test")).toBeTruthy(); + }); + + test("renders completed run telemetry 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 screen.findByText("FAILED · 03:42")).toBeTruthy(); + expect(screen.getByRole("button", { name: "RUN AGAIN" })).toBeTruthy(); + + const workspace = screen.getByLabelText("Workspace runtime wing"); + const sandbox = screen.getByLabelText("Sandbox runtime wing"); + + expect(within(workspace).getByText(/done/)).toBeTruthy(); + expect(within(workspace).getByText("Files")).toBeTruthy(); + expect(within(workspace).getByText("Check")).toBeTruthy(); + expect(within(sandbox).getByText(/failed/)).toBeTruthy(); + expect(within(sandbox).getByText("Elapsed")).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..8cbce027 --- /dev/null +++ b/examples/think-compare-runtimes/src/App.tsx @@ -0,0 +1,584 @@ +import { Button } from "@cloudflare/kumo/components/button"; +import { usePartySocket } from "partysocket/react"; +import { useEffect, useMemo, useState } from "react"; +import type { RunEvent, RuntimeId } from "../shared/events"; +import { comparisonFixture } from "../shared/fixture"; +import { AutoScrollList } from "./auto-scroll-list"; +import { + buildDashboardModel, + type ContainerState, + type RuntimeDashboardModel, +} from "./dashboard-model"; +import { agentEventsForRuntime, formatEventDetail, runtimeEventsForRuntime } from "./event-lanes"; +import { MarkdownText } from "./markdown-text"; +import { applyRunMessage, type RunMessage } from "./run-state"; + +interface RunSessionResponse { + runId: string; + socketPath: string; + events: RunEvent[]; +} + +type StartState = "idle" | "starting" | "running" | "failed"; + +type WingMode = "idle" | "boot" | "activity"; + +const runtimeCopy: Record< + RuntimeId, + { + side: "L" | "R"; + label: "WORKSPACE" | "SANDBOX"; + packageName: string; + title: string; + subtitle: string; + accent: string; + dot: string; + } +> = { + workspace: { + side: "L", + label: "WORKSPACE", + packageName: "@cloudflare/workspace", + title: "Durable files + routed exec", + subtitle: + "Direct file tools use durable storage. Exec routes to the worker shell first, then container for real binaries.", + accent: "text-[#F2A93B]", + dot: "bg-[#F2A93B]", + }, + sandbox: { + side: "R", + label: "SANDBOX", + packageName: "@cloudflare/sandbox", + title: "Container filesystem", + subtitle: "Same fixture is seeded into the Sandbox filesystem. File tools and exec run there.", + accent: "text-[#5BC8A7]", + dot: "bg-[#5BC8A7]", + }, +}; + +const statusTone = { + idle: "border-[#22272E] bg-[#171A1F] text-[#8A9099]", + running: "border-[#F2A93B]/40 bg-[#F2A93B]/10 text-[#F2A93B]", + completed: "border-[#5BC8A7]/40 bg-[#5BC8A7]/10 text-[#5BC8A7]", + failed: "border-[#E15B5B]/45 bg-[#E15B5B]/10 text-[#E15B5B]", +}; + +const containerTone: Record = { + off: "text-[#8A9099]", + asleep: "text-[#8A9099]", + booting: "text-[#5BC8A7]", + awake: "text-[#E6E8EA]", +}; + +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()); + + usePartySocket({ + party: "compare-run", + room: runId ?? "idle", + enabled: runId !== null, + onMessage(message) { + const parsed = JSON.parse(String(message.data)) as RunMessage; + setEvents((current) => applyRunMessage(current, parsed)); + }, + }); + + const dashboard = useMemo(() => buildDashboardModel(events, nowIso), [events, nowIso]); + const lanesByRuntime = useMemo( + () => ({ + workspace: { + agent: agentEventsForRuntime(events, "workspace"), + runtime: runtimeEventsForRuntime(events, "workspace"), + }, + sandbox: { + agent: agentEventsForRuntime(events, "sandbox"), + runtime: runtimeEventsForRuntime(events, "sandbox"), + }, + }), + [events], + ); + const runLabel = runStatusLabel(startState, dashboard.run.status, dashboard.run.elapsedLabel); + const actionLabel = runId ? dashboard.run.actionLabel : "START RUN"; + + 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 startRun() { + 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; + setRunId(session.runId); + setEvents(session.events); + setStartState("running"); + } catch (cause) { + setStartState("failed"); + setError(cause instanceof Error ? cause.message : String(cause)); + } + } + + return ( +
+ + +
+ + +
+
+ ); +} + +function TopBar({ + actionLabel, + disabled, + error, + onStart, + runId, + runLabel, +}: { + actionLabel: string; + disabled: boolean; + error: string | null; + onStart: () => void; + runId: string | null; + runLabel: string; +}) { + return ( +
+
+
+ + + + + THINK · RUNTIME COMPARE + +
+ +
+

+ TASK {fixtureMeta()} +

+

{comparisonFixture.task}

+ {error ?

{error}

: null} +
+
+ +
+ + {runId ? ( + {runId} + ) : ( + ready + )} + +
+
+ ); +} + +function RuntimeWing({ + lanes, + runtime, + telemetry, +}: { + lanes: { agent: RunEvent[]; runtime: RunEvent[] }; + runtime: RuntimeId; + telemetry: RuntimeDashboardModel; +}) { + const copy = runtimeCopy[runtime]; + const mode = wingMode(runtime, telemetry); + const activityEvents = [...lanes.agent, ...lanes.runtime].sort( + (left, right) => left.sequence - right.sequence, + ); + + return ( +
+
+
+

+ {copy.side} · {copy.label}{" "} + {copy.packageName} +

+

+ {copy.title} +

+

{copy.subtitle}

+
+ +
+ + + + {capacityHint(telemetry.error) ? ( +
+ {capacityHint(telemetry.error)} +
+ ) : null} + + {mode === "idle" ? : null} + {mode === "boot" ? : null} + {mode === "activity" ? ( + + ) : null} +
+ ); +} + +function TelemetryStrip({ telemetry }: { telemetry: RuntimeDashboardModel }) { + const cells = + telemetry.id === "workspace" ? workspaceTelemetry(telemetry) : sandboxTelemetry(telemetry); + + return ( +
+ {cells.map(([label, value], index) => ( +
+
+ {label} +
+
+ {label === "Container" ? ( + + ) : null} + {value} +
+
+ ))} +
+ ); +} + +function workspaceTelemetry(telemetry: RuntimeDashboardModel): Array<[string, string]> { + const idle = telemetry.status === "idle"; + return [ + ["Files", idle ? "—" : String(telemetry.fileOps)], + ["Shell", idle ? "—" : String(telemetry.workerShellExecs)], + ["Container", telemetry.container], + ["Check", validationLabel(telemetry.validationStatus)], + ]; +} + +function sandboxTelemetry(telemetry: RuntimeDashboardModel): Array<[string, string]> { + const idle = telemetry.status === "idle"; + return [ + ["Files", idle ? "—" : String(telemetry.fileOps)], + ["Container", idle ? "—" : String(telemetry.containerExecs)], + ["Check", validationLabel(telemetry.validationStatus)], + ["Elapsed", telemetry.elapsedLabel === "--:--" ? "—" : telemetry.elapsedLabel], + ]; +} + +function validationLabel(status: RuntimeDashboardModel["validationStatus"]): string { + if (status === "not-run") return "—"; + return status; +} + +function IdlePanel({ runtime }: { runtime: RuntimeId }) { + if (runtime === "workspace") { + return ( +
+ + {fixtureTree()} +

+ On run start, this fixture is written to durable workspace storage before the agent's + first tool call. Reads, writes, and edits then run with no container in the loop. +

+
+ ); + } + + return ( +
+ + {bootPlan(false)} +

+ No filesystem exists yet. The container boots when the run starts, then receives the same + seed before the agent's first tool call. +

+
+ ); +} + +function BootPanel() { + return ( +
+ + {bootPlan(true)} +
+
+ container readiness + 82% +
+
+
+
+
+

+ Agent is blocked on container readiness. No tool calls can run until the sandbox finishes + seeding. +

+
+ ); +} + +function ActivityPanel({ + events, + runtime, + telemetry, +}: { + events: RunEvent[]; + runtime: RuntimeId; + telemetry: RuntimeDashboardModel; +}) { + const copy = runtimeCopy[runtime]; + const headerRight = + telemetry.status === "completed" + ? "finished cleanly" + : `turn ${Math.max(1, events.length)} of —`; + + return ( +
+ + + {events.length === 0 ? ( +
  • Awaiting agent activity.
  • + ) : ( + events.map((event, index) => ( + + )) + )} +
    +
    + ); +} + +function ActivityItem({ + accent, + event, + index, +}: { + accent: string; + event: RunEvent; + index: number; +}) { + const formatted = formatEventDetail(event.detail); + + return ( +
  • +
    + {String(index).padStart(2, "0")} + +
    +
    +
    + {eventLabel(event)} + {event.kind.replaceAll("_", " · ")} + → ok +
    +

    {event.title}

    + {formatted.fields.length > 0 ? ( +
    + {formatted.fields.map((field) => ( +
    +
    {field.label}
    +
    + {field.value} +
    +
    + ))} +
    + ) : ( + + )} +
    +
  • + ); +} + +function PanelHeader({ + left, + right, + tone = "text-[#8A9099]", +}: { + left: string; + right: string; + tone?: string; +}) { + return ( +
    + {left} + {right} +
    + ); +} + +function CodeBlock({ children }: { children: string }) { + return ( +
    +      {children}
    +    
    + ); +} + +function StatusPill({ status }: { status: RuntimeDashboardModel["status"] }) { + const label = status === "completed" ? "done" : status; + return ( + + ● {label} + + ); +} + +function StatusReadout({ label }: { label: string }) { + const done = label.startsWith("DONE"); + const run = label.startsWith("RUN"); + const tone = done ? "text-[#5BC8A7]" : run ? "text-[#F2A93B]" : "text-[#8A9099]"; + + return ( + + {label} + + ); +} + +function wingMode(runtime: RuntimeId, telemetry: RuntimeDashboardModel): WingMode { + if (telemetry.status === "idle") return "idle"; + if (runtime === "sandbox" && telemetry.container === "booting") return "boot"; + return "activity"; +} + +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 fixtureMeta(): string { + return `${comparisonFixture.files.length} files`; +} + +function fixtureTree(): string { + const srcFiles = comparisonFixture.files + .filter((file) => file.path.startsWith("src/")) + .map((file) => ` ${file.path.slice(4)}`); + const testFiles = comparisonFixture.files + .filter((file) => file.path.includes("test")) + .map((file) => ` ${file.path.replace(/^src\//, "")}`); + const rootFiles = comparisonFixture.files + .filter((file) => !file.path.startsWith("src/") && !file.path.includes("test")) + .map((file) => file.path); + + return [`▾ src/`, ...srcFiles, `▾ test/`, ...testFiles, ...rootFiles].join("\n"); +} + +function bootPlan(active: boolean): string { + if (active) { + return [ + "✓ pull image · cloudflare/sandbox:0.11.0 cached · 0.0s", + "✓ cold-start container 1.4s", + "▸ writing files into /workspace/repo 5 of 7", + "○ await tool calls from agent —", + ].join("\n"); + } + + return [ + "01 pull image · cloudflare/sandbox:0.11.0 ~ cached", + "02 cold-start container ~ 1.5s", + "03 write files into /workspace/repo ~ seed bytes", + "04 await tool calls from agent —", + ].join("\n"); +} + +function eventLabel(event: RunEvent): string { + if (event.kind === "agent_message") return "assistant"; + if (event.kind.includes("tool")) + return event.title.toLowerCase().includes("exec") ? "tool · exec" : "tool"; + if (event.kind.includes("runtime")) return "runtime"; + return "event"; +} + +function capacityHint(error: string | null): string | null { + if (!error) return null; + return error.includes("Capacity temporarily exceeded") + ? "Upstream model capacity; retry later." + : null; +} + +function lastEventSequence(events: RunEvent[]): number { + return events.at(-1)?.sequence ?? 0; +} + +function titleCase(value: RuntimeId): string { + return value === "workspace" ? "Workspace" : "Sandbox"; +} 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..7c623c8bb --- /dev/null +++ b/examples/think-compare-runtimes/src/dashboard-model.test.ts @@ -0,0 +1,194 @@ +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 backend", () => { + 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", + backend: "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", + backend: "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 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..40d00d5c --- /dev/null +++ b/examples/think-compare-runtimes/src/dashboard-model.ts @@ -0,0 +1,174 @@ +import type { RunEvent, RuntimeId } from "../shared/events"; +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); + const sortedEvents = [...events].sort((left, right) => left.sequence - right.sequence); + + 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 runtimeEvents = sortedEvents.filter((event) => event.runtime === runtime); + const toolCalls = runtimeEvents.filter(isToolCall).length; + const fileOps = runtimeEvents.filter(isFileCall).length; + const execEvents = runtimeEvents.filter(isExecCall); + const execCalls = execEvents.length; + const workerShellExecs = execEvents.filter( + (event) => execBackend(event) === "shell", + ).length; + const containerExecs = execEvents.filter( + (event) => execBackend(event) === "container", + ).length; + + return [ + runtime, + { + id: runtime, + status: runtimeSummary.status, + elapsedLabel: formatDuration( + runtimeSummary.elapsedMs ?? + runningElapsedMs(runtimeSummary.startedAt, runtimeSummary.completedAt, nowIso), + ), + toolCalls, + fileOps, + execCalls, + workerShellExecs, + containerExecs: runtime === "workspace" ? containerExecs : execCalls, + validationStatus: validationStatus(runtimeEvents), + container: containerState( + runtime, + runtimeSummary.status, + runtimeEvents, + runtime === "workspace" ? containerExecs : execCalls, + ), + error: runtimeSummary.error, + events: runtimeEvents, + }, + ]; + }), + ) 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 isToolCall(event: RunEvent): boolean { + return event.kind === "tool_call" || event.kind === "agent_tool_call"; +} + +function isFileCall(event: RunEvent): boolean { + if (!isToolCall(event)) return false; + const title = event.title.toLowerCase(); + return title.includes("read") || title.includes("write") || title.includes("edit"); +} + +function isExecCall(event: RunEvent): boolean { + if (!isToolCall(event)) return false; + if (event.title.toLowerCase().includes("exec")) return true; + return typeof parsedDetail(event)?.command === "string"; +} + +function execBackend(event: RunEvent): string | null { + const detail = parsedDetail(event); + return typeof detail?.backend === "string" ? detail.backend : null; +} + +function validationStatus(events: RunEvent[]): ValidationStatus { + const validationExecs = events.filter((event) => { + if (!isExecCall(event)) return false; + const command = parsedDetail(event)?.command; + return typeof command === "string" && /npm\s+run\s+check/.test(command); + }); + if (validationExecs.length === 0) return "not-run"; + + const failed = events.some((event) => { + if (event.kind === "tool_error" || event.kind === "agent_tool_error") return true; + const detail = parsedDetail(event); + return typeof detail?.exitCode === "number" && detail.exitCode !== 0; + }); + return failed ? "failed" : "passed"; +} + +function parsedDetail(event: RunEvent): Record | null { + try { + const detail = JSON.parse(event.detail) as unknown; + return detail && typeof detail === "object" ? (detail as Record) : null; + } catch { + return null; + } +} + +function containerState( + runtime: RuntimeId, + status: RuntimeRunStatus, + events: RunEvent[], + execCalls: number, +): ContainerState { + if (runtime === "workspace") { + return execCalls > 0 ? "awake" : "asleep"; + } + + if (status === "idle") return "off"; + if ( + events.some((event) => event.kind === "tool_call" || event.kind === "tool_result") || + execCalls > 0 + ) { + return "awake"; + } + return "booting"; +} diff --git a/examples/think-compare-runtimes/src/event-lanes.test.ts b/examples/think-compare-runtimes/src/event-lanes.test.ts new file mode 100644 index 00000000..64098bdc --- /dev/null +++ b/examples/think-compare-runtimes/src/event-lanes.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "vitest"; +import type { RunEvent } from "../shared/events"; +import { agentEventsForRuntime, formatEventDetail, runtimeEventsForRuntime } from "./event-lanes"; + +function event(overrides: Partial): RunEvent { + return { + id: `run-1:${overrides.sequence ?? 0}`, + runId: "run-1", + sequence: overrides.sequence ?? 0, + runtime: overrides.runtime ?? "workspace", + kind: overrides.kind ?? "runtime_note", + title: overrides.title ?? "Event", + detail: overrides.detail ?? "Detail", + timestamp: "1970-01-01T00:00:00.000Z", + }; +} + +describe("runtime event lanes", () => { + test("separates Think transcript events from runtime trace events", () => { + const events = [ + event({ sequence: 0, runtime: "both", kind: "run_started" }), + event({ sequence: 1, runtime: "workspace", kind: "agent_message" }), + event({ sequence: 2, runtime: "workspace", kind: "agent_tool_call" }), + event({ sequence: 3, runtime: "workspace", kind: "tool_call" }), + event({ sequence: 4, runtime: "sandbox", kind: "agent_message" }), + ]; + + expect(agentEventsForRuntime(events, "workspace").map((item) => item.sequence)).toEqual([1, 2]); + expect(runtimeEventsForRuntime(events, "workspace").map((item) => item.sequence)).toEqual([ + 0, 3, + ]); + expect(agentEventsForRuntime(events, "sandbox").map((item) => item.sequence)).toEqual([4]); + expect(runtimeEventsForRuntime(events, "sandbox").map((item) => item.sequence)).toEqual([0]); + }); + + test("formats JSON detail into prioritized structured fields", () => { + const detail = formatEventDetail( + JSON.stringify({ + stdout: "ok\n", + path: "/workspace/repo/src/index.ts", + exitCode: 0, + command: "npm test", + cwd: "/workspace/repo", + stderr: "", + }), + ); + + expect(detail).toEqual({ + text: null, + fields: [ + { label: "command", value: "npm test" }, + { label: "path", value: "/workspace/repo/src/index.ts" }, + { label: "cwd", value: "/workspace/repo" }, + { label: "exitCode", value: "0" }, + { label: "stdout", value: "ok\n" }, + { label: "stderr", value: "" }, + ], + }); + }); + + test("keeps plain text details readable", () => { + expect(formatEventDetail("Created /workspace/repo.")).toEqual({ + text: "Created /workspace/repo.", + fields: [], + }); + }); +}); diff --git a/examples/think-compare-runtimes/src/event-lanes.ts b/examples/think-compare-runtimes/src/event-lanes.ts new file mode 100644 index 00000000..0bc60fb1 --- /dev/null +++ b/examples/think-compare-runtimes/src/event-lanes.ts @@ -0,0 +1,78 @@ +import type { RunEvent, RunEventKind, RuntimeId } from "../shared/events"; + +export interface EventDetailField { + label: string; + value: string; +} + +export interface FormattedEventDetail { + text: string | null; + fields: EventDetailField[]; +} + +const agentEventKinds = new Set([ + "agent_message", + "agent_tool_call", + "agent_tool_result", + "agent_tool_error", +]); + +const preferredDetailFields = ["command", "path", "cwd", "exitCode", "stdout", "stderr", "error"]; + +export function agentEventsForRuntime(events: RunEvent[], runtime: RuntimeId): RunEvent[] { + return events.filter( + (event) => eventMatchesRuntime(event, runtime) && agentEventKinds.has(event.kind), + ); +} + +export function runtimeEventsForRuntime(events: RunEvent[], runtime: RuntimeId): RunEvent[] { + return events.filter( + (event) => eventMatchesRuntime(event, runtime) && !agentEventKinds.has(event.kind), + ); +} + +export function formatEventDetail(detail: string): FormattedEventDetail { + const parsed = parseJsonObject(detail); + if (parsed === null) { + return { text: detail, fields: [] }; + } + + return { + text: null, + fields: orderedEntries(parsed).map(([label, value]) => ({ + label, + value: stringifyFieldValue(value), + })), + }; +} + +function eventMatchesRuntime(event: RunEvent, runtime: RuntimeId): boolean { + return event.runtime === runtime || event.runtime === "both"; +} + +function parseJsonObject(detail: string): Record | null { + try { + const parsed = JSON.parse(detail) as unknown; + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch {} + + return null; +} + +function orderedEntries(value: Record): [string, unknown][] { + const entries = Object.entries(value); + const preferred = preferredDetailFields + .filter((field) => Object.hasOwn(value, field)) + .map((field): [string, unknown] => [field, value[field]]); + const rest = entries.filter(([field]) => !preferredDetailFields.includes(field)); + return [...preferred, ...rest]; +} + +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); +} 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..29439154 --- /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 ( +