diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c66bc66..7796b42 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -77,6 +77,18 @@ jobs: with: fetch-depth: 0 + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Build inspector frontend + run: | + pnpm install + SANDBOX_AGENT_SKIP_INSPECTOR=1 pnpm --filter @sandbox-agent/inspector build + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index ffe035b..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,174 +0,0 @@ -# Architecture - -This document covers three key architectural areas of the sandbox-daemon system. - -## Agent Schema Pipeline - -The schema pipeline extracts type definitions from AI coding agents and converts them to a universal format. - -### Schema Extraction - -TypeScript extractors in `resources/agent-schemas/src/` pull schemas from each agent: - -| Agent | Source | Extractor | -|-------|--------|-----------| -| Claude | `claude --output-format json --json-schema` | `claude.ts` | -| Codex | `codex app-server generate-json-schema` | `codex.ts` | -| OpenCode | GitHub OpenAPI spec | `opencode.ts` | -| Amp | Scrapes ampcode.com docs | `amp.ts` | - -All extractors include fallback schemas for when CLIs or URLs are unavailable. - -**Output:** JSON schemas written to `resources/agent-schemas/artifacts/json-schema/` - -### Rust Type Generation - -The `server/packages/extracted-agent-schemas/` package generates Rust types at build time: - -- `build.rs` reads JSON schemas and uses the `typify` crate to generate Rust structs -- Generated code is written to `$OUT_DIR/{agent}.rs` -- Types are exposed via `include!()` macros in `src/lib.rs` - -``` -resources/agent-schemas/artifacts/json-schema/*.json - ↓ (build.rs + typify) -$OUT_DIR/{claude,codex,opencode,amp}.rs - ↓ (include!) -extracted_agent_schemas::{claude,codex,opencode,amp}::* -``` - -### Universal Schema - -The `server/packages/universal-agent-schema/` package defines agent-agnostic types: - -**Core types** (`src/lib.rs`): -- `UniversalEvent` - Wrapper with id, timestamp, session_id, agent, data -- `UniversalEventData` - Enum: Message, Started, Error, QuestionAsked, PermissionAsked, Unknown -- `UniversalMessage` - Parsed (role, parts, metadata) or Unparsed (raw JSON) -- `UniversalMessagePart` - Text, ToolCall, ToolResult, FunctionCall, FunctionResult, File, Image, Error, Unknown - -**Converters** (`src/agents/{claude,codex,opencode,amp}.rs`): -- Each agent has a converter module that transforms native events to universal format -- Conversions are best-effort; unparseable data preserved in `Unparsed` or `Unknown` variants - -## Session Management - -Sessions track agent conversations with in-memory state. - -### Storage - -Sessions are stored in an in-memory `HashMap` inside `SessionManager`: - -```rust -struct SessionManager { - sessions: Mutex>, - // ... -} -``` - -There is no disk persistence. Sessions are ephemeral and lost on server restart. - -### SessionState - -Each session tracks: - -| Field | Purpose | -|-------|---------| -| `session_id` | Client-provided identifier | -| `agent` | Agent type (Claude, Codex, OpenCode, Amp) | -| `agent_mode` | Operating mode (build, plan, custom) | -| `permission_mode` | Permission handling (default, plan, bypass) | -| `model` | Optional model override | -| `events: Vec` | Full event history | -| `pending_questions` | Question IDs awaiting reply | -| `pending_permissions` | Permission IDs awaiting reply | -| `broadcaster` | Tokio broadcast channel for SSE streaming | -| `ended` | Whether agent process has terminated | - -### Lifecycle - -``` -POST /v1/sessions/{sessionId} Create session, auto-install agent - ↓ -POST /v1/sessions/{id}/messages Spawn agent subprocess, stream output - ↓ -GET /v1/sessions/{id}/events Poll for new events (offset-based) -GET /v1/sessions/{id}/events/sse Subscribe to SSE stream - ↓ -POST .../questions/{id}/reply Answer agent question -POST .../permissions/{id}/reply Grant/deny permission request - ↓ -(agent process terminates) Session marked as ended -``` - -### Event Flow - -When a message is sent: - -1. `send_message()` spawns the agent CLI as a subprocess -2. `consume_spawn()` reads stdout/stderr line by line -3. Each JSON line is parsed and converted via `parse_agent_line()` -4. Events are recorded via `record_event()` which: - - Assigns incrementing event ID - - Appends to `events` vector - - Broadcasts to SSE subscribers - -## SDK Modes - -The TypeScript SDK supports two connection modes. - -### Embedded Mode - -Defined in `sdks/typescript/src/spawn.ts`: - -1. **Binary resolution**: Checks `SANDBOX_AGENT_BIN` env, then platform-specific npm package, then `PATH` -2. **Port selection**: Uses provided port or finds a free one via `net.createServer()` -3. **Token generation**: Uses provided token or generates random 24-byte hex string -4. **Spawn**: Launches `sandbox-agent --host --port --token ` -5. **Health wait**: Polls `GET /v1/health` until server is ready (up to 15s timeout) -6. **Cleanup**: On dispose, sends SIGTERM then SIGKILL if needed; also registers process exit handlers - -```typescript -const handle = await spawnSandboxDaemon({ log: "inherit" }); -// handle.baseUrl = "http://127.0.0.1:" -// handle.token = "" -// handle.dispose() to cleanup -``` - -### Server Mode - -Defined in `sdks/typescript/src/client.ts`: - -- Direct HTTP client to a remote `sandbox-agent` server -- Uses provided `baseUrl` and optional `token` -- No subprocess management - -```typescript -const client = new SandboxDaemonClient({ - baseUrl: "http://remote-server:8080", - token: "secret", -}); -``` - -### Auto-Detection - -`SandboxDaemonClient.connect()` chooses the mode automatically: - -```typescript -// If baseUrl provided → server mode -const client = await SandboxDaemonClient.connect({ - baseUrl: "http://remote:8080", -}); - -// If no baseUrl → embedded mode (spawns subprocess) -const client = await SandboxDaemonClient.connect({}); - -// Explicit control -const client = await SandboxDaemonClient.connect({ - spawn: { enabled: true, port: 9000 }, -}); -``` - -The `spawn` option can be: -- `true` / `false` - Enable/disable embedded mode -- `SandboxDaemonSpawnOptions` - Fine-grained control over host, port, token, binary path, timeout, logging diff --git a/Cargo.toml b/Cargo.toml index 23000a7..5b1e82f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ zip = { version = "0.6", default-features = false, features = ["deflate"] } # Misc url = "2.5" regress = "0.10" +include_dir = "0.7" # Code generation (build deps) typify = "0.4" diff --git a/README.md b/README.md index be7db10..0e072f7 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,16 @@ Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes. - **Any coding agent**: Universal API to interact with all agents with full feature coverage -- **Server, stdin/stdout, or SDK mode**: Run as an HTTP server, CLI using stdin/stdout, or with the SDK +- **Server or SDK mode**: Run as an HTTP server or with the TypeScript SDK - **Universal session schema**: Universal schema to store agent transcripts - **Supports your sandbox provider**: Daytona, E2B, Vercel Sandboxes, and more - **Lightweight, portable Rust binary**: Install anywhere with 1 curl command -- **OpenAPI spec**: Versioned API schema tracked in `sdks/openapi/openapi.json` +- **OpenAPI spec**: Versioned API schema tracked in `docs/openapi.json` -Coming soon: +Roadmap: -- **Vercel AI SDK Compatibility**: Works with existing AI SDK tooling, like `useChat` -- **Auto-configure MCP & Skills**: Auto-load MCP servers & skills for your agents -- **Process & logs manager**: Manage processes, logs, and ports for your agents to run background processes +[ ] Python SDK +[ ] Automatic MCP & skillfile configuration ## Agent Support @@ -85,5 +84,12 @@ The server is a single Rust binary that runs anywhere with a curl install. If yo **Can I use this with my personal API keys?** Yes. Use `sandbox-agent credentials extract-env` to extract API keys from your local agent configs (Claude Code, Codex, OpenCode, Amp) and pass them to the sandbox environment. -**Why rust?** +**Why Rust?** TODO + +**Why not use stdio/JSON-RPC?** + +- has benefit of not having to listen on a port +- more difficult to interact with, harder to analyze, doesn't support inspector for debugging +- may add at some point +- codex does this. claude sort of does this. diff --git a/ROADMAP.md b/ROADMAP.md index 759898e..a8d9d15 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,18 +1,30 @@ -## soon +## launch -- implement stdin/stdout -- switch sdk to use sdtin/stdout for embedded mdoe +- re-review agent schemas and compare it to ours +- auto-serve frontend from cli +- verify embedded sdk works +- fix bugs in ui + - double messages + - user-sent messages + - permissions +- consider migraing our standard to match the vercel ai standard - discuss actor arch in readme + give example - skillfile - specifically include the release checklist -- image/etc input + +## soon + +- **Vercel AI SDK Compatibility**: Works with existing AI SDK tooling, like `useChat` +- **Auto-configure MCP & Skills**: Auto-load MCP servers & skills for your agents +- **Process & logs manager**: Manage processes, logs, and ports for your agents to run background processes ## later +- review all flags available on coding agents clis +- set up agent to check diffs in versions to recommend updates - auto-updating for long running job - persistence - system information/cpu/etc -- git utils - api features - list agent modes available - list models available diff --git a/docs/agent-compatibility.mdx b/docs/agent-compatibility.mdx index 58b3045..5fb7795 100644 --- a/docs/agent-compatibility.mdx +++ b/docs/agent-compatibility.mdx @@ -19,8 +19,9 @@ description: "Supported agents, install methods, and streaming formats." ## Capability notes -- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event. -- **Streaming**: all agents stream events; OpenCode uses SSE, Codex uses JSON-RPC over stdio, others use JSONL. +- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event (tests do not currently exercise Claude question/permission flows). +- **Streaming**: all agents stream events; OpenCode uses SSE, Codex uses JSON-RPC over stdio, others use JSONL. Codex is currently normalized to thread/turn starts plus user/assistant completed items (deltas and tool/reasoning items are not emitted yet). +- **User messages**: Claude CLI output does not include explicit user-message events in our snapshots, so only assistant messages are surfaced for Claude today. - **Files and images**: normalized via `UniversalMessagePart` with `File` and `Image` parts. See [Universal API](/universal-api) for feature coverage details. diff --git a/docs/cli.mdx b/docs/cli.mdx index 0e81e7d..fa6cf20 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -3,12 +3,12 @@ title: "CLI" description: "CLI reference and server flags." --- -The `sandbox-agent` CLI mirrors the HTTP API so you can script everything without writing client code. +The `sandbox-daemon` CLI mirrors the HTTP API so you can script everything without writing client code. ## Server flags ```bash -sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 +sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 ``` - `--token`: global token for all requests. @@ -22,7 +22,7 @@ sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 agents list ```bash -sandbox-agent agents list --endpoint http://127.0.0.1:2468 +sandbox-daemon agents list --endpoint http://127.0.0.1:2468 ``` @@ -30,7 +30,7 @@ sandbox-agent agents list --endpoint http://127.0.0.1:2468 agents install ```bash -sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468 +sandbox-daemon agents install claude --reinstall --endpoint http://127.0.0.1:2468 ``` @@ -38,7 +38,7 @@ sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468 agents modes ```bash -sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468 +sandbox-daemon agents modes claude --endpoint http://127.0.0.1:2468 ``` @@ -48,7 +48,7 @@ sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468 sessions create ```bash -sandbox-agent sessions create my-session \ +sandbox-daemon sessions create my-session \ --agent claude \ --agent-mode build \ --permission-mode default \ @@ -60,7 +60,7 @@ sandbox-agent sessions create my-session \ sessions send-message ```bash -sandbox-agent sessions send-message my-session \ +sandbox-daemon sessions send-message my-session \ --message "Summarize the repository" \ --endpoint http://127.0.0.1:2468 ``` @@ -70,7 +70,7 @@ sandbox-agent sessions send-message my-session \ sessions events ```bash -sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468 +sandbox-daemon sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468 ``` @@ -78,7 +78,7 @@ sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http:/ sessions events-sse ```bash -sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468 +sandbox-daemon sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468 ``` @@ -86,7 +86,7 @@ sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0. sessions reply-question ```bash -sandbox-agent sessions reply-question my-session QUESTION_ID \ +sandbox-daemon sessions reply-question my-session QUESTION_ID \ --answers "yes" \ --endpoint http://127.0.0.1:2468 ``` @@ -96,7 +96,7 @@ sandbox-agent sessions reply-question my-session QUESTION_ID \ sessions reject-question ```bash -sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468 +sandbox-daemon sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468 ``` @@ -104,7 +104,7 @@ sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http:// sessions reply-permission ```bash -sandbox-agent sessions reply-permission my-session PERMISSION_ID \ +sandbox-daemon sessions reply-permission my-session PERMISSION_ID \ --reply once \ --endpoint http://127.0.0.1:2468 ``` diff --git a/docs/deployments/cloudflare-sandboxes.mdx b/docs/deployments/cloudflare-sandboxes.mdx index b2d910b..39de77c 100644 --- a/docs/deployments/cloudflare-sandboxes.mdx +++ b/docs/deployments/cloudflare-sandboxes.mdx @@ -12,7 +12,7 @@ description: "Deploy the daemon in Cloudflare Sandboxes." ```bash export SANDBOX_TOKEN="..." -cargo run -p sandbox-agent -- \ +cargo run -p sandbox-agent -- server \ --token "$SANDBOX_TOKEN" \ --host 0.0.0.0 \ --port 2468 diff --git a/docs/deployments/daytona.mdx b/docs/deployments/daytona.mdx index e86e380..2500cb4 100644 --- a/docs/deployments/daytona.mdx +++ b/docs/deployments/daytona.mdx @@ -12,7 +12,7 @@ description: "Run the daemon in a Daytona workspace." ```bash export SANDBOX_TOKEN="..." -cargo run -p sandbox-agent -- \ +cargo run -p sandbox-agent -- server \ --token "$SANDBOX_TOKEN" \ --host 0.0.0.0 \ --port 2468 diff --git a/docs/deployments/docker.mdx b/docs/deployments/docker.mdx index 2cd8ede..5cccd43 100644 --- a/docs/deployments/docker.mdx +++ b/docs/deployments/docker.mdx @@ -21,7 +21,7 @@ The binary will be written to `./artifacts/sandbox-agent-x86_64-unknown-linux-mu docker run --rm -p 2468:2468 \ -v "$PWD/artifacts:/artifacts" \ debian:bookworm-slim \ - /artifacts/sandbox-agent-x86_64-unknown-linux-musl --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468 + /artifacts/sandbox-agent-x86_64-unknown-linux-musl server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468 ``` You can now access the API at `http://localhost:2468`. diff --git a/docs/deployments/e2b.mdx b/docs/deployments/e2b.mdx index 09d78d6..682c6f8 100644 --- a/docs/deployments/e2b.mdx +++ b/docs/deployments/e2b.mdx @@ -16,7 +16,7 @@ export SANDBOX_TOKEN="..." # Install sandbox-agent binary (or build from source) # TODO: replace with release download once published -cargo run -p sandbox-agent -- \ +cargo run -p sandbox-agent -- server \ --token "$SANDBOX_TOKEN" \ --host 0.0.0.0 \ --port 2468 diff --git a/docs/deployments/vercel-sandboxes.mdx b/docs/deployments/vercel-sandboxes.mdx index 6e262d9..ea460ae 100644 --- a/docs/deployments/vercel-sandboxes.mdx +++ b/docs/deployments/vercel-sandboxes.mdx @@ -12,7 +12,7 @@ description: "Run the daemon inside Vercel Sandboxes." ```bash export SANDBOX_TOKEN="..." -cargo run -p sandbox-agent -- \ +cargo run -p sandbox-agent -- server \ --token "$SANDBOX_TOKEN" \ --host 0.0.0.0 \ --port 2468 diff --git a/docs/docs.json b/docs/docs.json index 4487d94..a13e72a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -45,6 +45,10 @@ "http-api", "typescript-sdk" ] + }, + { + "group": "API", + "openapi": "openapi.json" } ] }, diff --git a/docs/frontend.mdx b/docs/frontend.mdx index 5450dc1..0fe45f3 100644 --- a/docs/frontend.mdx +++ b/docs/frontend.mdx @@ -17,4 +17,6 @@ The UI expects: - Endpoint (e.g. `http://127.0.0.1:2468`) - Optional token -If you see CORS errors, enable CORS on the daemon with `--cors-allow-origin` and related flags. +When running the daemon, the inspector is also served automatically at `http://127.0.0.1:2468/ui`. + +If you see CORS errors, enable CORS on the daemon with `sandbox-daemon server --cors-allow-origin` and related flags. diff --git a/docs/index.mdx b/docs/index.mdx index 53ee39e..e88b6c8 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -18,7 +18,7 @@ Sandbox Agent SDK is a universal API and daemon for running coding agents inside Run the daemon locally: ```bash -sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 +sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 ``` Send a message: diff --git a/sdks/openapi/openapi.json b/docs/openapi.json similarity index 99% rename from sdks/openapi/openapi.json rename to docs/openapi.json index 10f921b..2c43393 100644 --- a/sdks/openapi/openapi.json +++ b/docs/openapi.json @@ -11,6 +11,11 @@ }, "version": "0.1.0" }, + "servers": [ + { + "url": "http://localhost:2468" + } + ], "paths": { "/v1/agents": { "get": { diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index a6a3715..010eff0 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -8,13 +8,19 @@ description: "Start the daemon and send your first message." Use the installed binary, or `cargo run` in development. ```bash -sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 +sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 ``` If you want to run without auth (local dev only): ```bash -sandbox-agent --no-token --host 127.0.0.1 --port 2468 +sandbox-daemon server --no-token --host 127.0.0.1 --port 2468 +``` + +If you're running from source instead of the installed CLI: + +```bash +cargo run -p sandbox-agent -- server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 ``` ### CORS (frontend usage) @@ -22,7 +28,7 @@ sandbox-agent --no-token --host 127.0.0.1 --port 2468 If you are calling the daemon from a browser, enable CORS explicitly: ```bash -sandbox-agent \ +sandbox-daemon server \ --token "$SANDBOX_TOKEN" \ --cors-allow-origin "http://localhost:5173" \ --cors-allow-method "GET" \ @@ -69,7 +75,7 @@ curl "http://127.0.0.1:2468/v1/sessions/my-session/events/sse?offset=0" \ The CLI mirrors the HTTP API: ```bash -sandbox-agent sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN" +sandbox-daemon sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN" -sandbox-agent sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN" +sandbox-daemon sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN" ``` diff --git a/docs/typescript-sdk.mdx b/docs/typescript-sdk.mdx index cab36a3..b1233bd 100644 --- a/docs/typescript-sdk.mdx +++ b/docs/typescript-sdk.mdx @@ -13,7 +13,7 @@ pnpm --filter sandbox-agent generate This runs: -- `cargo run -p sandbox-agent-openapi-gen` to emit OpenAPI JSON +- `cargo run -p sandbox-agent-openapi-gen -- --out docs/openapi.json` to emit OpenAPI JSON - `openapi-typescript` to generate types ## Usage diff --git a/frontend/packages/inspector/package.json b/frontend/packages/inspector/package.json index 9663d26..2c1c85b 100644 --- a/frontend/packages/inspector/package.json +++ b/frontend/packages/inspector/package.json @@ -7,7 +7,8 @@ "scripts": { "dev": "vite", "build": "pnpm --filter sandbox-agent build && vite build", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc --noEmit" }, "devDependencies": { "sandbox-agent": "workspace:*", diff --git a/frontend/packages/inspector/src/App.tsx b/frontend/packages/inspector/src/App.tsx index 68e9311..2209155 100644 --- a/frontend/packages/inspector/src/App.tsx +++ b/frontend/packages/inspector/src/App.tsx @@ -85,8 +85,17 @@ const formatTime = (value: string) => { return date.toLocaleTimeString(); }; +const getDefaultEndpoint = () => { + if (typeof window === "undefined") return "http://127.0.0.1:2468"; + const { origin, protocol } = window.location; + if (!origin || origin === "null" || protocol === "file:") { + return "http://127.0.0.1:2468"; + } + return origin; +}; + export default function App() { - const [endpoint, setEndpoint] = useState("http://localhost:2468"); + const [endpoint, setEndpoint] = useState(getDefaultEndpoint); const [token, setToken] = useState(""); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); @@ -195,18 +204,25 @@ export default function App() { return error instanceof Error ? error.message : fallback; }; - const connect = async () => { + const connectToDaemon = async (reportError: boolean) => { setConnecting(true); - setConnectError(null); + if (reportError) { + setConnectError(null); + } try { const client = createClient(); await client.getHealth(); setConnected(true); await refreshAgents(); await fetchSessions(); + if (reportError) { + setConnectError(null); + } } catch (error) { - const message = getErrorMessage(error, "Unable to connect"); - setConnectError(message); + if (reportError) { + const message = getErrorMessage(error, "Unable to connect"); + setConnectError(message); + } setConnected(false); clientRef.current = null; } finally { @@ -214,6 +230,8 @@ export default function App() { } }; + const connect = () => connectToDaemon(true); + const disconnect = () => { setConnected(false); clientRef.current = null; @@ -531,10 +549,10 @@ export default function App() { .filter((event): event is UniversalEvent & { data: { message: UniversalMessage } } => "message" in event.data) .map((event) => { const msg = event.data.message; - const parts = "parts" in msg ? msg.parts : []; + const parts = ("parts" in msg ? msg.parts : []) ?? []; const content = parts - .filter((part: UniversalMessagePart) => part.type === "text" && part.text) - .map((part: UniversalMessagePart) => part.text) + .filter((part: UniversalMessagePart): part is UniversalMessagePart & { type: "text"; text: string } => part.type === "text" && "text" in part && typeof part.text === "string") + .map((part) => part.text) .join("\n"); return { id: event.id, @@ -553,6 +571,20 @@ export default function App() { }; }, []); + useEffect(() => { + let active = true; + const attempt = async () => { + await connectToDaemon(false); + }; + attempt().catch(() => { + if (!active) return; + setConnecting(false); + }); + return () => { + active = false; + }; + }, []); + useEffect(() => { if (!connected) return; refreshAgents(); @@ -672,7 +704,7 @@ export default function App() {

Start the daemon with CORS enabled for browser access:
- sandbox-agent --cors-allow-origin http://localhost:5173 + sandbox-daemon server --cors-allow-origin http://localhost:5173

diff --git a/frontend/packages/inspector/vite.config.ts b/frontend/packages/inspector/vite.config.ts index 5072d0b..068e57f 100644 --- a/frontend/packages/inspector/vite.config.ts +++ b/frontend/packages/inspector/vite.config.ts @@ -1,9 +1,10 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -export default defineConfig({ +export default defineConfig(({ command }) => ({ + base: command === "build" ? "/ui/" : "/", plugins: [react()], server: { port: 5173 } -}); +})); diff --git a/package.json b/package.json index 3fbd926..e0d62c1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "turbo run build", "dev": "turbo run dev --parallel", - "generate": "turbo run generate" + "generate": "turbo run generate", + "typecheck": "turbo run typecheck" }, "devDependencies": { "turbo": "^2.4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 717e0f3..1d650a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ importers: specifier: ^5.7.0 version: 5.9.3 devDependencies: + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 '@types/node': specifier: ^22.0.0 version: 22.19.7 @@ -74,6 +77,34 @@ importers: specifier: ^4.19.0 version: 4.21.0 + resources/vercel-ai-sdk-schemas: + dependencies: + semver: + specifier: ^7.6.3 + version: 7.7.3 + tar: + specifier: ^7.0.0 + version: 7.5.6 + ts-json-schema-generator: + specifier: ^2.4.0 + version: 2.4.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + devDependencies: + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 + '@types/node': + specifier: ^22.0.0 + version: 22.19.7 + '@types/semver': + specifier: ^7.5.0 + version: 7.7.1 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + sdks/cli: {} sdks/cli/platforms/darwin-arm64: {} @@ -579,6 +610,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -772,6 +807,9 @@ packages: '@types/react@18.3.27': resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -827,6 +865,10 @@ packages: resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} engines: {node: '>=20.18.1'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1033,6 +1075,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1131,6 +1177,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1167,6 +1218,10 @@ packages: resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} engines: {node: '>=12'} + tar@7.5.6: + resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==} + engines: {node: '>=18'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1296,6 +1351,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -1648,6 +1707,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1798,6 +1861,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/semver@7.7.1': {} + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.7))': dependencies: '@babel/core': 7.28.6 @@ -1865,6 +1930,8 @@ snapshots: undici: 7.19.1 whatwg-mimetype: 4.0.0 + chownr@3.0.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2098,6 +2165,10 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + ms@2.1.3: {} nanoid@3.3.11: {} @@ -2214,6 +2285,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.3: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2246,6 +2319,14 @@ snapshots: supports-color@9.4.0: {} + tar@7.5.6: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -2346,4 +2427,6 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yargs-parser@21.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e678df2..d4d687a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - "sdks/cli" - "sdks/cli/platforms/*" - "resources/agent-schemas" + - "resources/vercel-ai-sdk-schemas" diff --git a/resources/agent-schemas/package.json b/resources/agent-schemas/package.json index 4942068..8d8f0c5 100644 --- a/resources/agent-schemas/package.json +++ b/resources/agent-schemas/package.json @@ -12,7 +12,8 @@ "extract:claude-events": "tsx src/claude-event-types.ts", "extract:claude-events:sdk": "tsx src/claude-event-types-sdk.ts", "extract:claude-events:cli": "tsx src/claude-event-types-cli.ts", - "extract:claude-events:docs": "tsx src/claude-event-types-docs.ts" + "extract:claude-events:docs": "tsx src/claude-event-types-docs.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "ts-json-schema-generator": "^2.4.0", @@ -23,6 +24,7 @@ }, "devDependencies": { "tsx": "^4.19.0", - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "@types/json-schema": "^7.0.15" } } diff --git a/resources/vercel-ai-sdk-schemas/.tmp/log.txt b/resources/vercel-ai-sdk-schemas/.tmp/log.txt new file mode 100644 index 0000000..78c90ba --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/.tmp/log.txt @@ -0,0 +1,50 @@ + +> vercel-ai-sdk-schemas@1.0.0 extract /home/nathan/sandbox-daemon/resources/vercel-ai-sdk-schemas +> tsx src/index.ts + +Vercel AI SDK UIMessage Schema Extractor +======================================== + + [cache hit] https://registry.npmjs.org/ai +Target version: ai@6.0.50 + [debug] temp dir: /tmp/vercel-ai-sdk-JnQ1yL + [cache hit] https://registry.npmjs.org/ai + [cache hit] https://registry.npmjs.org/@opentelemetry%2Fapi + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fgateway + [cache hit] https://registry.npmjs.org/@vercel%2Foidc + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider + [cache hit] https://registry.npmjs.org/json-schema + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider-utils + [cache hit] https://registry.npmjs.org/@standard-schema%2Fspec + [cache hit] https://registry.npmjs.org/eventsource-parser + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider + [cache hit] https://registry.npmjs.org/zod + [cache hit] https://registry.npmjs.org/zod + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider + [cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider-utils + [cache hit] https://registry.npmjs.org/zod + [shim] Wrote type-fest ValueOf shim + [debug] DataUIPart alias snippet: type DataUIPart = ValueOf<{ + [NAME in keyof DATA_TYPES & string]: { + type: `data-${NAME}`; + [patch] Simplified DataUIPart to avoid indexed access + [debug] ToolUIPart alias snippet: type ToolUIPart = ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: `tool-${NAME}`; + [patch] Simplified ToolUIPart to avoid indexed access + [warn] ValueOf alias declaration not found + [warn] ValueOf alias not found in ai types + [debug] ai types path: /tmp/vercel-ai-sdk-JnQ1yL/node_modules/ai/dist/index.d.ts + [debug] preview: ValueOf} from 'type-fest'; +import data = require('./data.json'); + +export function getData(name: string): ValueOf { + return data[name]; +} + +export function onlyBar(name: string): ValueOf + [debug] entry path: /tmp/vercel-ai-sdk-JnQ1yL/entry.ts + [debug] tsconfig path: /tmp/vercel-ai-sdk-JnQ1yL/tsconfig.json + [debug] entry size: 89 + + [wrote] /home/nathan/sandbox-daemon/resources/vercel-ai-sdk-schemas/artifacts/json-schema/ui-message.json diff --git a/resources/vercel-ai-sdk-schemas/README.md b/resources/vercel-ai-sdk-schemas/README.md new file mode 100644 index 0000000..e7f7e9e --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/README.md @@ -0,0 +1,23 @@ +# Vercel AI SDK Schemas + +This package extracts JSON Schema for `UIMessage` from the Vercel AI SDK v6 TypeScript types. + +## Usage + +- Install dependencies in this folder. +- Run the extractor: + +``` +pnpm install +pnpm extract +``` + +Optional flags: +- `--version=6.x.y` to pin an exact version +- `--major=6` to select the latest version for a major (default: 6) + +Output: +- `artifacts/json-schema/ui-message.json` + +The registry response is cached under `.cache/` for 24 hours. The extractor downloads the AI SDK package +and the minimal dependency tree needed for TypeScript type resolution into a temporary folder. diff --git a/resources/vercel-ai-sdk-schemas/artifacts/json-schema/ui-message.json b/resources/vercel-ai-sdk-schemas/artifacts/json-schema/ui-message.json new file mode 100644 index 0000000..12bedd7 --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/artifacts/json-schema/ui-message.json @@ -0,0 +1,1106 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/UIMessage", + "definitions": { + "UIMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the message." + }, + "role": { + "type": "string", + "enum": [ + "system", + "user", + "assistant" + ], + "description": "The role of the message." + }, + "metadata": { + "description": "The metadata of the message." + }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "text": { + "type": "string", + "description": "The text content." + }, + "state": { + "type": "string", + "enum": [ + "streaming", + "done" + ], + "description": "The state of the text part." + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "The provider metadata." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "A text part of a message." + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "reasoning" + }, + "text": { + "type": "string", + "description": "The reasoning text." + }, + "state": { + "type": "string", + "enum": [ + "streaming", + "done" + ], + "description": "The state of the reasoning part." + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "The provider metadata." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "A reasoning part of a message." + }, + { + "type": "object", + "additionalProperties": false + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "input-streaming" + }, + "input": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "input-available" + }, + "input": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "input", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "approval-requested" + }, + "input": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "approval": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "approval", + "input", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "approval-responded" + }, + "input": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "approval": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "approved": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "id", + "approved" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "approval", + "input", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "output-available" + }, + "input": {}, + "output": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "preliminary": { + "type": "boolean" + }, + "approval": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "approved": { + "type": "boolean", + "const": true + }, + "reason": { + "type": "string" + } + }, + "required": [ + "id", + "approved" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "input", + "output", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "output-error" + }, + "input": {}, + "errorText": { + "type": "string" + }, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "approval": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "approved": { + "type": "boolean", + "const": true + }, + "reason": { + "type": "string" + } + }, + "required": [ + "id", + "approved" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "errorText", + "input", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "output-denied" + }, + "input": {}, + "callProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + }, + "approval": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "approved": { + "type": "boolean", + "const": false + }, + "reason": { + "type": "string" + } + }, + "required": [ + "id", + "approved" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "const": "dynamic-tool" + }, + "toolName": { + "type": "string", + "description": "Name of the tool that is being called." + }, + "toolCallId": { + "type": "string", + "description": "ID of the tool call." + }, + "title": { + "type": "string" + }, + "providerExecuted": { + "type": "boolean", + "description": "Whether the tool call was executed by the provider." + } + }, + "required": [ + "approval", + "input", + "state", + "toolCallId", + "toolName", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "source-url" + }, + "sourceId": { + "type": "string" + }, + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + } + }, + "required": [ + "type", + "sourceId", + "url" + ], + "additionalProperties": false, + "description": "A source part of a message." + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "source-document" + }, + "sourceId": { + "type": "string" + }, + "mediaType": { + "type": "string" + }, + "title": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "Additional provider-specific metadata that is returned from the provider.\n\nThis is needed to enable provider-specific functionality that can be fully encapsulated in the provider." + } + }, + "required": [ + "type", + "sourceId", + "mediaType", + "title" + ], + "additionalProperties": false, + "description": "A document source part of a message." + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file" + }, + "mediaType": { + "type": "string", + "description": "IANA media type of the file." + }, + "filename": { + "type": "string", + "description": "Optional filename of the file." + }, + "url": { + "type": "string", + "description": "The URL of the file. It can either be a URL to a hosted file or a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)." + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "description": "The provider metadata." + } + }, + "required": [ + "type", + "mediaType", + "url" + ], + "additionalProperties": false, + "description": "A file part of a message." + }, + { + "type": "object", + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "step-start" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "A step boundary part of a message." + } + ] + }, + "description": "The parts of the message. Use this for rendering the message in the UI.\n\nSystem messages should be avoided (set the system prompt on the server instead). They can have text parts.\n\nUser messages can have text parts and file parts.\n\nAssistant messages can have text, reasoning, tool invocation, and file parts." + } + }, + "required": [ + "id", + "role", + "parts" + ], + "additionalProperties": false, + "description": "AI SDK UI Messages. They are used in the client and to communicate between the frontend and the API routes." + }, + "alias-_index.d.ts-405-470-_index.d.ts-0-119070": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + }, + { + "not": {} + } + ] + } + }, + "alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/alias-_index.d.ts-405-470-_index.d.ts-0-119070" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/alias-_index.d.ts-156-405-_index.d.ts-0-119070133205725" + } + } + ], + "description": "A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods." + } + }, + "$id": "https://sandbox-agent/schemas/vercel-ai-sdk/ui-message.json", + "title": "UIMessage", + "description": "Vercel AI SDK v6.0.50 UIMessage" +} \ No newline at end of file diff --git a/resources/vercel-ai-sdk-schemas/package.json b/resources/vercel-ai-sdk-schemas/package.json new file mode 100644 index 0000000..1ff03bf --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/package.json @@ -0,0 +1,22 @@ +{ + "name": "vercel-ai-sdk-schemas", + "version": "1.0.0", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "extract": "tsx src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "ts-json-schema-generator": "^2.4.0", + "typescript": "^5.7.0", + "tar": "^7.0.0", + "semver": "^7.6.3" + }, + "devDependencies": { + "tsx": "^4.19.0", + "@types/node": "^22.0.0", + "@types/semver": "^7.5.0", + "@types/json-schema": "^7.0.15" + } +} diff --git a/resources/vercel-ai-sdk-schemas/src/cache.ts b/resources/vercel-ai-sdk-schemas/src/cache.ts new file mode 100644 index 0000000..caf5b52 --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/src/cache.ts @@ -0,0 +1,93 @@ +import { createHash } from "crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; + +const CACHE_DIR = join(import.meta.dirname, "..", ".cache"); +const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours + +interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} + +function ensureCacheDir(): void { + if (!existsSync(CACHE_DIR)) { + mkdirSync(CACHE_DIR, { recursive: true }); + } +} + +function hashKey(key: string): string { + return createHash("sha256").update(key).digest("hex"); +} + +function getCachePath(key: string): string { + return join(CACHE_DIR, `${hashKey(key)}.json`); +} + +export function getCached(key: string): T | null { + const path = getCachePath(key); + + if (!existsSync(path)) { + return null; + } + + try { + const content = readFileSync(path, "utf-8"); + const entry: CacheEntry = JSON.parse(content); + + const now = Date.now(); + if (now - entry.timestamp > entry.ttl) { + return null; + } + + return entry.data; + } catch { + return null; + } +} + +export function setCache(key: string, data: T, ttl: number = DEFAULT_TTL_MS): void { + ensureCacheDir(); + + const entry: CacheEntry = { + data, + timestamp: Date.now(), + ttl, + }; + + const path = getCachePath(key); + writeFileSync(path, JSON.stringify(entry, null, 2)); +} + +export async function fetchWithCache(url: string, ttl?: number): Promise { + const cached = getCached(url); + if (cached !== null) { + console.log(` [cache hit] ${url}`); + return cached; + } + + console.log(` [fetching] ${url}`); + + let lastError: Error | null = null; + for (let attempt = 0; attempt < 3; attempt++) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const text = await response.text(); + setCache(url, text, ttl); + return text; + } catch (error) { + lastError = error as Error; + if (attempt < 2) { + const delay = Math.pow(2, attempt) * 1000; + console.log(` [retry ${attempt + 1}] waiting ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + throw lastError; +} diff --git a/resources/vercel-ai-sdk-schemas/src/index.ts b/resources/vercel-ai-sdk-schemas/src/index.ts new file mode 100644 index 0000000..839c3b6 --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/src/index.ts @@ -0,0 +1,398 @@ +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, + existsSync, + appendFileSync, + statSync, +} from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { createGenerator, type Config } from "ts-json-schema-generator"; +import { maxSatisfying, rsort, valid } from "semver"; +import { x as extractTar } from "tar"; +import type { JSONSchema7 } from "json-schema"; +import { fetchWithCache } from "./cache.js"; + +const REGISTRY_URL = "https://registry.npmjs.org/ai"; +const TARGET_TYPE = "UIMessage"; +const DEFAULT_MAJOR = 6; +const RESOURCE_DIR = join(import.meta.dirname, ".."); +const OUTPUT_DIR = join(RESOURCE_DIR, "artifacts", "json-schema"); +const OUTPUT_PATH = join(OUTPUT_DIR, "ui-message.json"); +const SCHEMA_ID = "https://sandbox-agent/schemas/vercel-ai-sdk/ui-message.json"; + +interface RegistryResponse { + versions?: Record< + string, + { + dist?: { tarball?: string }; + dependencies?: Record; + peerDependencies?: Record; + } + >; + "dist-tags"?: Record; +} + +interface Args { + version: string | null; + major: number; +} + +function parseArgs(): Args { + const args = process.argv.slice(2); + const versionArg = args.find((arg) => arg.startsWith("--version=")); + const majorArg = args.find((arg) => arg.startsWith("--major=")); + + const version = versionArg ? versionArg.split("=")[1] : null; + const major = majorArg ? Number(majorArg.split("=")[1]) : DEFAULT_MAJOR; + + return { + version, + major: Number.isFinite(major) && major > 0 ? major : DEFAULT_MAJOR, + }; +} + +function log(message: string): void { + console.log(message); +} + +function ensureOutputDir(): void { + if (!existsSync(OUTPUT_DIR)) { + mkdirSync(OUTPUT_DIR, { recursive: true }); + } +} + +async function fetchRegistry(url: string): Promise { + const registry = await fetchWithCache(url); + return JSON.parse(registry) as RegistryResponse; +} + +function resolveLatestVersion(registry: RegistryResponse, major: number): string { + const versions = Object.keys(registry.versions ?? {}); + const candidates = versions.filter((version) => valid(version) && version.startsWith(`${major}.`)); + const sorted = rsort(candidates); + if (sorted.length === 0) { + throw new Error(`No versions found for major ${major}`); + } + return sorted[0]; +} + +function resolveVersionFromRange(registry: RegistryResponse, range: string): string { + if (registry.versions?.[range]) { + return range; + } + + const versions = Object.keys(registry.versions ?? {}).filter((version) => valid(version)); + const resolved = maxSatisfying(versions, range); + if (!resolved) { + throw new Error(`No versions satisfy range ${range}`); + } + + return resolved; +} + +async function downloadTarball(url: string, destination: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`); + } + const buffer = Buffer.from(await response.arrayBuffer()); + writeFileSync(destination, buffer); +} + +async function extractPackage(tarballPath: string, targetDir: string): Promise { + mkdirSync(targetDir, { recursive: true }); + await extractTar({ + file: tarballPath, + cwd: targetDir, + strip: 1, + }); +} + +function packageDirFor(name: string, nodeModulesDir: string): string { + const parts = name.split("/"); + return join(nodeModulesDir, ...parts); +} + +async function installPackage( + name: string, + versionRange: string, + nodeModulesDir: string, + installed: Set +): Promise { + const encodedName = name.startsWith("@") + ? `@${encodeURIComponent(name.slice(1))}` + : encodeURIComponent(name); + const registryUrl = `https://registry.npmjs.org/${encodedName}`; + const registry = await fetchRegistry(registryUrl); + const version = resolveVersionFromRange(registry, versionRange); + const installKey = `${name}@${version}`; + + if (installed.has(installKey)) { + return; + } + + installed.add(installKey); + + const tarball = registry.versions?.[version]?.dist?.tarball; + if (!tarball) { + throw new Error(`No tarball found for ${installKey}`); + } + + const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-dep-")); + const tarballPath = join(tempDir, `${name.replace("/", "-")}-${version}.tgz`); + const packageDir = packageDirFor(name, nodeModulesDir); + + try { + await downloadTarball(tarball, tarballPath); + await extractPackage(tarballPath, packageDir); + + const packageJsonPath = join(packageDir, "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { + dependencies?: Record; + peerDependencies?: Record; + }; + + const dependencies = { + ...packageJson.dependencies, + ...packageJson.peerDependencies, + }; + + for (const [depName, depRange] of Object.entries(dependencies)) { + await installPackage(depName, depRange, nodeModulesDir, installed); + } + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +function writeTempTsconfig(tempDir: string): string { + const tsconfigPath = join(tempDir, "tsconfig.json"); + const tsconfig = { + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + strict: true, + skipLibCheck: true, + esModuleInterop: true, + }, + }; + writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2)); + return tsconfigPath; +} + +function writeEntryFile(tempDir: string): string { + const entryPath = join(tempDir, "entry.ts"); + const contents = `import type { ${TARGET_TYPE} as AI${TARGET_TYPE} } from "ai";\nexport type ${TARGET_TYPE} = AI${TARGET_TYPE};\n`; + writeFileSync(entryPath, contents); + return entryPath; +} + +function patchValueOfAlias(nodeModulesDir: string): void { + const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts"); + if (!existsSync(aiTypesPath)) { + log(" [warn] ai types not found for ValueOf patch"); + return; + } + + const contents = readFileSync(aiTypesPath, "utf-8"); + const valueOfMatch = contents.match(/type ValueOf[\\s\\S]*?;/); + if (valueOfMatch) { + const snippet = valueOfMatch[0].replace(/\\s+/g, " ").slice(0, 200); + log(` [debug] ValueOf alias snippet: ${snippet}`); + } else { + log(" [warn] ValueOf alias declaration not found"); + } + + let patched = contents.replace( + /ObjectType\\s*\\[\\s*ValueType\\s*\\]/, + "ObjectType[string]" + ); + + if (patched !== contents) { + writeFileSync(aiTypesPath, patched); + log(" [patch] Adjusted ValueOf alias for schema generation"); + return; + } + + const valueOfIndex = contents.indexOf("ValueOf"); + const preview = + valueOfIndex === -1 ? contents.slice(0, 200) : contents.slice(valueOfIndex, valueOfIndex + 200); + log(" [warn] ValueOf alias not found in ai types"); + log(` [debug] ai types path: ${aiTypesPath}`); + log(` [debug] preview: ${preview.replace(/\\s+/g, " ").slice(0, 200)}`); +} + +function ensureTypeFestShim(nodeModulesDir: string): void { + const typeFestDir = join(nodeModulesDir, "type-fest"); + if (!existsSync(typeFestDir)) { + mkdirSync(typeFestDir, { recursive: true }); + } + + const packageJsonPath = join(typeFestDir, "package.json"); + const typesPath = join(typeFestDir, "index.d.ts"); + + if (!existsSync(packageJsonPath)) { + const pkg = { + name: "type-fest", + version: "0.0.0", + types: "index.d.ts", + }; + writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2)); + } + + const shim = `export type ValueOf<\n ObjectType,\n ValueType extends keyof ObjectType = keyof ObjectType,\n> = ObjectType[string];\n\nexport type Simplify = { [KeyType in keyof T]: T[KeyType] } & {};\n`; + writeFileSync(typesPath, shim); + log(" [shim] Wrote type-fest ValueOf shim"); +} + +function generateSchema(entryPath: string, tsconfigPath: string): JSONSchema7 { + const config: Config = { + path: entryPath, + tsconfig: tsconfigPath, + type: TARGET_TYPE, + expose: "export", + skipTypeCheck: true, + }; + + const generator = createGenerator(config); + return generator.createSchema(TARGET_TYPE) as JSONSchema7; +} + +function addSchemaMetadata(schema: JSONSchema7, version: string): JSONSchema7 { + const withMeta: JSONSchema7 = { + ...schema, + $schema: schema.$schema ?? "http://json-schema.org/draft-07/schema#", + $id: SCHEMA_ID, + title: schema.title ?? TARGET_TYPE, + description: schema.description ?? `Vercel AI SDK v${version} ${TARGET_TYPE}`, + }; + + return withMeta; +} + +function loadFallback(): JSONSchema7 | null { + if (!existsSync(OUTPUT_PATH)) { + return null; + } + + try { + const content = readFileSync(OUTPUT_PATH, "utf-8"); + return JSON.parse(content) as JSONSchema7; + } catch { + return null; + } +} + +function patchUiMessageTypes(nodeModulesDir: string): void { + const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts"); + if (!existsSync(aiTypesPath)) { + log(" [warn] ai types not found for UIMessage patch"); + return; + } + + const contents = readFileSync(aiTypesPath, "utf-8"); + let patched = contents; + + const replaceAlias = (typeName: string, replacement: string): boolean => { + const start = patched.indexOf(`type ${typeName}`); + if (start === -1) { + log(` [warn] ${typeName} alias not found for patch`); + return false; + } + const end = patched.indexOf(";", start); + if (end === -1) { + log(` [warn] ${typeName} alias not terminated`); + return false; + } + const snippet = patched.slice(start, Math.min(end + 1, start + 400)).replace(/\\s+/g, " "); + log(` [debug] ${typeName} alias snippet: ${snippet}`); + + patched = patched.slice(0, start) + replacement + patched.slice(end + 1); + return true; + }; + + const dataReplaced = replaceAlias( + "DataUIPart", + "type DataUIPart = {\\n type: `data-${string}`;\\n id?: string;\\n data: unknown;\\n};" + ); + if (dataReplaced) { + log(" [patch] Simplified DataUIPart to avoid indexed access"); + } + + const toolReplaced = replaceAlias( + "ToolUIPart", + "type ToolUIPart = {\\n type: `tool-${string}`;\\n} & UIToolInvocation;" + ); + if (toolReplaced) { + log(" [patch] Simplified ToolUIPart to avoid indexed access"); + } + + if (patched !== contents) { + writeFileSync(aiTypesPath, patched); + } +} + +async function main(): Promise { + log("Vercel AI SDK UIMessage Schema Extractor"); + log("========================================\n"); + + const args = parseArgs(); + ensureOutputDir(); + + const registry = await fetchRegistry(REGISTRY_URL); + const version = args.version ?? resolveLatestVersion(registry, args.major); + + log(`Target version: ai@${version}`); + + const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-")); + const nodeModulesDir = join(tempDir, "node_modules"); + + try { + log(` [debug] temp dir: ${tempDir}`); + await installPackage("ai", version, nodeModulesDir, new Set()); + ensureTypeFestShim(nodeModulesDir); + patchUiMessageTypes(nodeModulesDir); + patchValueOfAlias(nodeModulesDir); + + const tsconfigPath = writeTempTsconfig(tempDir); + const entryPath = writeEntryFile(tempDir); + log(` [debug] entry path: ${entryPath}`); + log(` [debug] tsconfig path: ${tsconfigPath}`); + if (existsSync(entryPath)) { + const entryStat = statSync(entryPath); + log(` [debug] entry size: ${entryStat.size}`); + } + + const schema = generateSchema(entryPath, tsconfigPath); + const schemaWithMeta = addSchemaMetadata(schema, version); + + writeFileSync(OUTPUT_PATH, JSON.stringify(schemaWithMeta, null, 2)); + log(`\n [wrote] ${OUTPUT_PATH}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`\n [error] ${message}`); + if (error instanceof Error && error.stack) { + log(error.stack); + } + + const fallback = loadFallback(); + if (fallback) { + log(" [fallback] Keeping existing schema artifact"); + return; + } + + process.exitCode = 1; + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/resources/vercel-ai-sdk-schemas/tsconfig.json b/resources/vercel-ai-sdk-schemas/tsconfig.json new file mode 100644 index 0000000..559a1e0 --- /dev/null +++ b/resources/vercel-ai-sdk-schemas/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/scripts/release/main.ts b/scripts/release/main.ts index 9d5fa3d..a5c5eaa 100755 --- a/scripts/release/main.ts +++ b/scripts/release/main.ts @@ -313,10 +313,15 @@ function buildTypescript(rootDir: string) { } function generateArtifacts(rootDir: string) { + run("pnpm", ["install"], { cwd: rootDir }); + run("pnpm", ["--filter", "@sandbox-agent/inspector", "build"], { + cwd: rootDir, + env: { ...process.env, SANDBOX_AGENT_SKIP_INSPECTOR: "1" }, + }); const sdkDir = path.join(rootDir, "sdks", "typescript"); run("pnpm", ["run", "generate"], { cwd: sdkDir }); run("cargo", ["check", "-p", "sandbox-agent-universal-schema-gen"], { cwd: rootDir }); - run("cargo", ["run", "-p", "sandbox-agent-openapi-gen", "--", "--out", "sdks/openapi/openapi.json"], { + run("cargo", ["run", "-p", "sandbox-agent-openapi-gen", "--", "--out", "docs/openapi.json"], { cwd: rootDir, }); } @@ -367,14 +372,25 @@ function uploadBinaries(rootDir: string, version: string, latest: boolean) { } function runChecks(rootDir: string) { + console.log("==> Installing Node dependencies"); + run("pnpm", ["install"], { cwd: rootDir }); + + console.log("==> Building inspector frontend"); + run("pnpm", ["--filter", "@sandbox-agent/inspector", "build"], { + cwd: rootDir, + env: { ...process.env, SANDBOX_AGENT_SKIP_INSPECTOR: "1" }, + }); + console.log("==> Running Rust checks"); run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir }); run("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], { cwd: rootDir }); run("cargo", ["test", "--all-targets"], { cwd: rootDir }); console.log("==> Running TypeScript checks"); - run("pnpm", ["install"], { cwd: rootDir }); run("pnpm", ["run", "build"], { cwd: rootDir }); + + console.log("==> Validating OpenAPI spec for Mintlify"); + run("pnpm", ["dlx", "mint", "openapi-check", "docs/openapi.json"], { cwd: rootDir }); } function publishCrates(rootDir: string, version: string) { diff --git a/sdks/cli/package.json b/sdks/cli/package.json index 23d5455..05524a4 100644 --- a/sdks/cli/package.json +++ b/sdks/cli/package.json @@ -8,7 +8,8 @@ "url": "https://github.com/rivet-dev/sandbox-agent" }, "bin": { - "sandbox-agent": "bin/sandbox-agent" + "sandbox-agent": "bin/sandbox-agent", + "sandbox-daemon": "bin/sandbox-agent" }, "optionalDependencies": { "@sandbox-agent/cli-darwin-arm64": "0.1.0", diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 99ed3f2..1d0a3b6 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -20,10 +20,11 @@ "dist" ], "scripts": { - "generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../openapi/openapi.json", - "generate:types": "openapi-typescript ../openapi/openapi.json -o src/generated/openapi.ts", + "generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../../docs/openapi.json", + "generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts", "generate": "pnpm run generate:openapi && pnpm run generate:types", - "build": "pnpm run generate && tsc -p tsconfig.json" + "build": "pnpm run generate && tsc -p tsconfig.json", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/sdks/typescript/src/generated/openapi.ts b/sdks/typescript/src/generated/openapi.ts index 3fb0957..682c92d 100644 --- a/sdks/typescript/src/generated/openapi.ts +++ b/sdks/typescript/src/generated/openapi.ts @@ -22,6 +22,9 @@ export interface paths { "/v1/health": { get: operations["get_health"]; }; + "/v1/sessions": { + get: operations["list_sessions"]; + }; "/v1/sessions/{session_id}": { post: operations["create_session"]; }; @@ -179,6 +182,21 @@ export interface components { callId: string; messageId: string; }; + SessionInfo: { + agent: string; + agentMode: string; + agentSessionId?: string | null; + ended: boolean; + /** Format: int64 */ + eventCount: number; + model?: string | null; + permissionMode: string; + sessionId: string; + variant?: string | null; + }; + SessionListResponse: { + sessions: components["schemas"]["SessionInfo"][]; + }; Started: { details?: unknown; message?: string | null; @@ -358,6 +376,15 @@ export interface operations { }; }; }; + list_sessions: { + responses: { + 200: { + content: { + "application/json": components["schemas"]["SessionListResponse"]; + }; + }; + }; + }; create_session: { parameters: { path: { diff --git a/sdks/typescript/src/spawn.ts b/sdks/typescript/src/spawn.ts index 0cd19a4..4510da6 100644 --- a/sdks/typescript/src/spawn.ts +++ b/sdks/typescript/src/spawn.ts @@ -1,6 +1,5 @@ import type { ChildProcess } from "node:child_process"; import type { AddressInfo } from "node:net"; -import type { NodeRequire } from "node:module"; export type SandboxDaemonSpawnLogMode = "inherit" | "pipe" | "silent"; @@ -68,7 +67,7 @@ export async function spawnSandboxDaemon( } const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe"; - const args = ["--host", bindHost, "--port", String(port), "--token", token]; + const args = ["server", "--host", bindHost, "--port", String(port), "--token", token]; const child = spawn(binaryPath, args, { stdio, env: { @@ -112,7 +111,7 @@ function resolveBinaryFromEnv(fs: typeof import("node:fs"), path: typeof import( } function resolveBinaryFromCliPackage( - require: NodeRequire, + require: ReturnType, path: typeof import("node:path"), fs: typeof import("node:fs"), ): string | null { diff --git a/server/packages/sandbox-agent/Cargo.toml b/server/packages/sandbox-agent/Cargo.toml index c35e0a4..f25cc8c 100644 --- a/server/packages/sandbox-agent/Cargo.toml +++ b/server/packages/sandbox-agent/Cargo.toml @@ -31,6 +31,7 @@ schemars.workspace = true tracing.workspace = true tracing-logfmt.workspace = true tracing-subscriber.workspace = true +include_dir.workspace = true [dev-dependencies] http-body-util.workspace = true diff --git a/server/packages/sandbox-agent/build.rs b/server/packages/sandbox-agent/build.rs new file mode 100644 index 0000000..56e4ba0 --- /dev/null +++ b/server/packages/sandbox-agent/build.rs @@ -0,0 +1,63 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .expect("workspace root"); + let dist_dir = root_dir + .join("frontend") + .join("packages") + .join("inspector") + .join("dist"); + + println!("cargo:rerun-if-env-changed=SANDBOX_AGENT_SKIP_INSPECTOR"); + println!("cargo:rerun-if-changed={}", dist_dir.display()); + + let skip = env::var("SANDBOX_AGENT_SKIP_INSPECTOR").is_ok(); + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")); + let out_file = out_dir.join("inspector_assets.rs"); + + if skip { + write_disabled(&out_file); + return; + } + + if !dist_dir.exists() { + panic!( + "Inspector frontend missing at {}. Run `pnpm --filter @sandbox-agent/inspector build` (or `pnpm -C frontend/packages/inspector build`) or set SANDBOX_AGENT_SKIP_INSPECTOR=1 to skip embedding.", + dist_dir.display() + ); + } + + let dist_literal = quote_path(&dist_dir); + let contents = format!( + "pub const INSPECTOR_ENABLED: bool = true;\n\ + pub fn inspector_dir() -> Option<&'static include_dir::Dir<'static>> {{\n\ + Some(&INSPECTOR_DIR)\n\ + }}\n\ + static INSPECTOR_DIR: include_dir::Dir<'static> = include_dir::include_dir!(\"{}\");\n", + dist_literal + ); + + fs::write(&out_file, contents).expect("write inspector_assets.rs"); +} + +fn write_disabled(out_file: &Path) { + let contents = "pub const INSPECTOR_ENABLED: bool = false;\n\ + pub fn inspector_dir() -> Option<&'static include_dir::Dir<'static>> {\n\ + None\n\ + }\n"; + fs::write(out_file, contents).expect("write inspector_assets.rs"); +} + +fn quote_path(path: &Path) -> String { + path.to_str() + .expect("valid path") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} diff --git a/server/packages/sandbox-agent/src/lib.rs b/server/packages/sandbox-agent/src/lib.rs index 2fbc535..8b23835 100644 --- a/server/packages/sandbox-agent/src/lib.rs +++ b/server/packages/sandbox-agent/src/lib.rs @@ -2,3 +2,4 @@ pub mod credentials; pub mod router; +pub mod ui; diff --git a/server/packages/sandbox-agent/src/main.rs b/server/packages/sandbox-agent/src/main.rs index 392adc8..6f08183 100644 --- a/server/packages/sandbox-agent/src/main.rs +++ b/server/packages/sandbox-agent/src/main.rs @@ -16,6 +16,7 @@ use sandbox_agent_core::router::{ }; use sandbox_agent_core::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse}; use sandbox_agent_core::router::build_router; +use sandbox_agent_core::ui; use serde::Serialize; use serde_json::Value; use thiserror::Error; @@ -23,25 +24,42 @@ use tower_http::cors::{Any, CorsLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; const API_PREFIX: &str = "/v1"; +const DEFAULT_HOST: &str = "127.0.0.1"; +const DEFAULT_PORT: u16 = 2468; #[derive(Parser, Debug)] -#[command(name = "sandbox-agent")] -#[command(about = "Sandbox agent for managing coding agents", version)] +#[command(name = "sandbox-daemon", bin_name = "sandbox-agent")] +#[command(about = "Sandbox daemon for managing coding agents", version)] struct Cli { #[command(subcommand)] command: Option, - #[arg(long, short = 'H', default_value = "127.0.0.1")] - host: String, - - #[arg(long, short = 'p', default_value_t = 2468)] - port: u16, - - #[arg(long, short = 't')] + #[arg(long, short = 't', global = true)] token: Option, - #[arg(long, short = 'n')] + #[arg(long, short = 'n', global = true)] no_token: bool, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Run the sandbox daemon HTTP server. + Server(ServerArgs), + /// Manage installed agents and their modes. + Agents(AgentsArgs), + /// Create sessions and interact with session events. + Sessions(SessionsArgs), + /// Inspect locally discovered credentials. + Credentials(CredentialsArgs), +} + +#[derive(Args, Debug)] +struct ServerArgs { + #[arg(long, short = 'H', default_value = DEFAULT_HOST)] + host: String, + + #[arg(long, short = 'p', default_value_t = DEFAULT_PORT)] + port: u16, #[arg(long = "cors-allow-origin", short = 'O')] cors_allow_origin: Vec, @@ -56,16 +74,6 @@ struct Cli { cors_allow_credentials: bool, } -#[derive(Subcommand, Debug)] -enum Command { - /// Manage installed agents and their modes. - Agents(AgentsArgs), - /// Create sessions and interact with session events. - Sessions(SessionsArgs), - /// Inspect locally discovered credentials. - Credentials(CredentialsArgs), -} - #[derive(Args, Debug)] struct AgentsArgs { #[command(subcommand)] @@ -255,6 +263,8 @@ struct CredentialsExtractEnvArgs { #[derive(Debug, Error)] enum CliError { + #[error("missing command: run `sandbox-daemon server` to start the daemon")] + MissingCommand, #[error("missing --token or --no-token for server mode")] MissingToken, #[error("invalid cors origin: {0}")] @@ -280,8 +290,9 @@ fn main() { let cli = Cli::parse(); let result = match &cli.command { + Some(Command::Server(args)) => run_server(&cli, args), Some(command) => run_client(command, &cli), - None => run_server(&cli), + None => Err(CliError::MissingCommand), }; if let Err(err) = result { @@ -298,7 +309,7 @@ fn init_logging() { .init(); } -fn run_server(cli: &Cli) -> Result<(), CliError> { +fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> { let auth = if cli.no_token { AuthConfig::disabled() } else if let Some(token) = cli.token.clone() { @@ -312,11 +323,16 @@ fn run_server(cli: &Cli) -> Result<(), CliError> { let state = AppState::new(auth, agent_manager); let mut router = build_router(state); - if let Some(cors) = build_cors_layer(cli)? { + if let Some(cors) = build_cors_layer(server)? { router = router.layer(cors); } - let addr = format!("{}:{}", cli.host, cli.port); + let addr = format!("{}:{}", server.host, server.port); + let display_host = match server.host.as_str() { + "0.0.0.0" | "::" => "localhost", + other => other, + }; + let inspector_url = format!("http://{}:{}/ui", display_host, server.port); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() @@ -325,6 +341,11 @@ fn run_server(cli: &Cli) -> Result<(), CliError> { runtime.block_on(async move { let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!(addr = %addr, "server listening"); + if ui::is_enabled() { + tracing::info!(url = %inspector_url, "inspector ui available"); + } else { + tracing::info!("inspector ui not embedded; set SANDBOX_AGENT_SKIP_INSPECTOR=1 to skip embedding during builds"); + } axum::serve(listener, router) .await .map_err(|err| CliError::Server(err.to_string())) @@ -339,6 +360,9 @@ fn default_install_dir() -> PathBuf { fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> { match command { + Command::Server(_) => Err(CliError::Server( + "server subcommand must be invoked as `sandbox-daemon server`".to_string(), + )), Command::Agents(subcommand) => run_agents(&subcommand.command, cli), Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli), Command::Credentials(subcommand) => run_credentials(&subcommand.command), @@ -663,11 +687,11 @@ fn available_providers(credentials: &ExtractedCredentials) -> Vec { providers } -fn build_cors_layer(cli: &Cli) -> Result, CliError> { - let has_config = !cli.cors_allow_origin.is_empty() - || !cli.cors_allow_method.is_empty() - || !cli.cors_allow_header.is_empty() - || cli.cors_allow_credentials; +fn build_cors_layer(server: &ServerArgs) -> Result, CliError> { + let has_config = !server.cors_allow_origin.is_empty() + || !server.cors_allow_method.is_empty() + || !server.cors_allow_header.is_empty() + || server.cors_allow_credentials; if !has_config { return Ok(None); @@ -675,11 +699,11 @@ fn build_cors_layer(cli: &Cli) -> Result, CliError> { let mut cors = CorsLayer::new(); - if cli.cors_allow_origin.is_empty() { + if server.cors_allow_origin.is_empty() { cors = cors.allow_origin(Any); } else { let mut origins = Vec::new(); - for origin in &cli.cors_allow_origin { + for origin in &server.cors_allow_origin { let value = origin .parse() .map_err(|_| CliError::InvalidCorsOrigin(origin.clone()))?; @@ -688,11 +712,11 @@ fn build_cors_layer(cli: &Cli) -> Result, CliError> { cors = cors.allow_origin(origins); } - if cli.cors_allow_method.is_empty() { + if server.cors_allow_method.is_empty() { cors = cors.allow_methods(Any); } else { let mut methods = Vec::new(); - for method in &cli.cors_allow_method { + for method in &server.cors_allow_method { let parsed = method .parse() .map_err(|_| CliError::InvalidCorsMethod(method.clone()))?; @@ -701,11 +725,11 @@ fn build_cors_layer(cli: &Cli) -> Result, CliError> { cors = cors.allow_methods(methods); } - if cli.cors_allow_header.is_empty() { + if server.cors_allow_header.is_empty() { cors = cors.allow_headers(Any); } else { let mut headers = Vec::new(); - for header in &cli.cors_allow_header { + for header in &server.cors_allow_header { let parsed = header .parse() .map_err(|_| CliError::InvalidCorsHeader(header.clone()))?; @@ -714,7 +738,7 @@ fn build_cors_layer(cli: &Cli) -> Result, CliError> { cors = cors.allow_headers(headers); } - if cli.cors_allow_credentials { + if server.cors_allow_credentials { cors = cors.allow_credentials(true); } @@ -732,7 +756,7 @@ impl ClientContext { let endpoint = args .endpoint .clone() - .unwrap_or_else(|| format!("http://{}:{}", cli.host, cli.port)); + .unwrap_or_else(|| format!("http://{}:{}", DEFAULT_HOST, DEFAULT_PORT)); let token = if cli.no_token { None } else { cli.token.clone() }; let client = HttpClient::builder().build()?; Ok(Self { diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs index 88c5855..f7db020 100644 --- a/server/packages/sandbox-agent/src/router.rs +++ b/server/packages/sandbox-agent/src/router.rs @@ -46,7 +46,7 @@ use serde_json::{json, Value}; use tokio::sync::{broadcast, mpsc, Mutex}; use tokio_stream::wrappers::BroadcastStream; use tokio::time::sleep; -use utoipa::{OpenApi, ToSchema}; +use utoipa::{Modify, OpenApi, ToSchema}; use sandbox_agent_agent_management::agents::{ AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn, @@ -187,10 +187,21 @@ pub fn build_router(state: AppState) -> Router { (name = "meta", description = "Service metadata"), (name = "agents", description = "Agent management"), (name = "sessions", description = "Session management") - ) + ), + modifiers(&ServerAddon) )] pub struct ApiDoc; +struct ServerAddon; + +impl Modify for ServerAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + openapi.servers = Some(vec![utoipa::openapi::Server::new( + "http://localhost:2468", + )]); + } +} + #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error(transparent)] @@ -594,14 +605,14 @@ impl SessionManager { let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound { session_id: session_id.to_string(), })?; - if let Some(err) = session.ended_error() { - return Err(err); - } if !session.take_question(question_id) { return Err(SandboxError::InvalidRequest { message: format!("unknown question id: {question_id}"), }); } + if let Some(err) = session.ended_error() { + return Err(err); + } (session.agent, session.agent_session_id.clone()) }; @@ -628,14 +639,14 @@ impl SessionManager { let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound { session_id: session_id.to_string(), })?; - if let Some(err) = session.ended_error() { - return Err(err); - } if !session.take_question(question_id) { return Err(SandboxError::InvalidRequest { message: format!("unknown question id: {question_id}"), }); } + if let Some(err) = session.ended_error() { + return Err(err); + } (session.agent, session.agent_session_id.clone()) }; @@ -663,14 +674,14 @@ impl SessionManager { let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound { session_id: session_id.to_string(), })?; - if let Some(err) = session.ended_error() { - return Err(err); - } if !session.take_permission(permission_id) { return Err(SandboxError::InvalidRequest { message: format!("unknown permission id: {permission_id}"), }); } + if let Some(err) = session.ended_error() { + return Err(err); + } let codex_metadata = if session.agent == AgentId::Codex { session.events.iter().find_map(|event| { if let UniversalEventData::PermissionAsked { permission_asked } = &event.data { @@ -858,47 +869,45 @@ impl SessionManager { Ok(Ok(status)) if status.success() => {} Ok(Ok(status)) => { let message = format!("agent exited with status {:?}", status); - self.record_error( - &session_id, - message.clone(), - Some("process_exit".to_string()), - None, - ) + if !terminate_early { + self.record_error( + &session_id, + message.clone(), + Some("process_exit".to_string()), + None, + ) .await; + } self.mark_session_ended(&session_id, status.code(), &message) .await; } Ok(Err(err)) => { let message = format!("failed to wait for agent: {err}"); - self.record_error( - &session_id, - message.clone(), - Some("process_wait_failed".to_string()), - None, - ) - .await; - self.mark_session_ended( - &session_id, - None, - &message, - ) - .await; + if !terminate_early { + self.record_error( + &session_id, + message.clone(), + Some("process_wait_failed".to_string()), + None, + ) + .await; + } + self.mark_session_ended(&session_id, None, &message) + .await; } Err(err) => { let message = format!("failed to join agent task: {err}"); - self.record_error( - &session_id, - message.clone(), - Some("process_wait_failed".to_string()), - None, - ) - .await; - self.mark_session_ended( - &session_id, - None, - &message, - ) - .await; + if !terminate_early { + self.record_error( + &session_id, + message.clone(), + Some("process_wait_failed".to_string()), + None, + ) + .await; + } + self.mark_session_ended(&session_id, None, &message) + .await; } } } @@ -2179,15 +2188,22 @@ impl CodexAppServerState { serde_json::from_value::(value.clone()) { self.maybe_capture_thread_id(¬ification); - let conversion = convert_codex::notification_to_universal(¬ification); let should_terminate = matches!( notification, codex_schema::ServerNotification::TurnCompleted(_) | codex_schema::ServerNotification::Error(_) ); - CodexLineOutcome { - conversion: Some(conversion), - should_terminate, + if codex_should_emit_notification(¬ification) { + let conversion = convert_codex::notification_to_universal(¬ification); + CodexLineOutcome { + conversion: Some(conversion), + should_terminate, + } + } else { + CodexLineOutcome { + conversion: None, + should_terminate, + } } } else { CodexLineOutcome::default() @@ -2369,6 +2385,20 @@ fn codex_sandbox_policy(mode: Option<&str>) -> Option bool { + match notification { + codex_schema::ServerNotification::ThreadStarted(_) + | codex_schema::ServerNotification::TurnStarted(_) + | codex_schema::ServerNotification::Error(_) => true, + codex_schema::ServerNotification::ItemCompleted(params) => matches!( + params.item, + codex_schema::ThreadItem::UserMessage { .. } + | codex_schema::ThreadItem::AgentMessage { .. } + ), + _ => false, + } +} + fn codex_request_to_universal(request: &codex_schema::ServerRequest) -> EventConversion { match request { codex_schema::ServerRequest::ItemCommandExecutionRequestApproval { id, params } => { diff --git a/server/packages/sandbox-agent/src/ui.rs b/server/packages/sandbox-agent/src/ui.rs new file mode 100644 index 0000000..c1757d5 --- /dev/null +++ b/server/packages/sandbox-agent/src/ui.rs @@ -0,0 +1,81 @@ +use std::path::Path; + +use axum::body::Body; +use axum::extract::Path as AxumPath; +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; + +include!(concat!(env!("OUT_DIR"), "/inspector_assets.rs")); + +pub fn is_enabled() -> bool { + INSPECTOR_ENABLED +} + +pub fn router() -> Router { + if !INSPECTOR_ENABLED { + return Router::new(); + } + Router::new() + .route("/ui", get(handle_index)) + .route("/ui/", get(handle_index)) + .route("/ui/*path", get(handle_path)) +} + +async fn handle_index() -> Response { + serve_path("") +} + +async fn handle_path(AxumPath(path): AxumPath) -> Response { + serve_path(&path) +} + +fn serve_path(path: &str) -> Response { + let Some(dir) = inspector_dir() else { + return StatusCode::NOT_FOUND.into_response(); + }; + + let trimmed = path.trim_start_matches('/'); + let target = if trimmed.is_empty() { "index.html" } else { trimmed }; + + if let Some(file) = dir.get_file(target) { + return file_response(file); + } + + if !target.contains('.') { + if let Some(file) = dir.get_file("index.html") { + return file_response(file); + } + } + + StatusCode::NOT_FOUND.into_response() +} + +fn file_response(file: &include_dir::File) -> Response { + let mut response = Response::new(Body::from(file.contents().to_vec())); + *response.status_mut() = StatusCode::OK; + let content_type = content_type_for(file.path()); + let value = HeaderValue::from_static(content_type); + response.headers_mut().insert(header::CONTENT_TYPE, value); + response +} + +fn content_type_for(path: &Path) -> &'static str { + match path.extension().and_then(|ext| ext.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("js") => "text/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("svg") => "image/svg+xml", + Some("png") => "image/png", + Some("ico") => "image/x-icon", + Some("json") => "application/json", + Some("map") => "application/json", + Some("txt") => "text/plain; charset=utf-8", + Some("woff") => "font/woff", + Some("woff2") => "font/woff2", + Some("ttf") => "font/ttf", + Some("eot") => "application/vnd.ms-fontobject", + _ => "application/octet-stream", + } +} diff --git a/server/packages/sandbox-agent/tests/inspector_ui.rs b/server/packages/sandbox-agent/tests/inspector_ui.rs new file mode 100644 index 0000000..ea57d38 --- /dev/null +++ b/server/packages/sandbox-agent/tests/inspector_ui.rs @@ -0,0 +1,40 @@ +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use http_body_util::BodyExt; +use sandbox_agent_agent_management::agents::AgentManager; +use sandbox_agent_core::router::{build_router, AppState, AuthConfig}; +use sandbox_agent_core::ui; +use tempfile::TempDir; +use tower::util::ServiceExt; + +#[tokio::test] +async fn serves_inspector_ui() { + if !ui::is_enabled() { + return; + } + + let install_dir = TempDir::new().expect("create temp install dir"); + let manager = AgentManager::new(install_dir.path()).expect("create agent manager"); + let state = AppState::new(AuthConfig::disabled(), manager); + let app = build_router(state); + + let request = Request::builder() + .uri("/ui") + .body(Body::empty()) + .expect("build request"); + let response = app + .oneshot(request) + .await + .expect("request handled"); + + assert_eq!(response.status(), StatusCode::OK); + + let bytes = response + .into_body() + .collect() + .await + .expect("read body") + .to_bytes(); + let body = String::from_utf8_lossy(&bytes); + assert!(body.contains("" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 -- agent: codex - kind: message - message: - unparsed: true - seq: 6 -- agent: codex - kind: message - message: - unparsed: true - seq: 7 -- agent: codex - kind: message - message: - unparsed: true - seq: 8 -- agent: codex - kind: message - message: - unparsed: true - seq: 9 -- agent: codex - kind: message - message: - unparsed: true - seq: 10 -- agent: codex - kind: message - message: - unparsed: true - seq: 11 -- agent: codex - kind: message - message: - unparsed: true - seq: 12 -- agent: codex - kind: message - message: - unparsed: true - seq: 13 -- agent: codex - kind: message - message: - unparsed: true - seq: 14 -- agent: codex - kind: message - message: - unparsed: true - seq: 15 -- agent: codex - kind: message - message: - unparsed: true - seq: 16 -- agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@permission_reply_missing_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@permission_reply_missing_codex.snap index bad76e2..fafb7c8 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@permission_reply_missing_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@permission_reply_missing_codex.snap @@ -1,15 +1,11 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1028 +assertion_line: 1017 expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })" --- payload: - agent: codex - detail: "agent process exited: codex" - details: - exitCode: 1 - stderr: agent exited with status ExitStatus(unix_wait_status(256)) - status: 500 - title: Agent Process Exited - type: "urn:sandbox-agent:error:agent_process_exited" -status: 500 + detail: "invalid request: unknown permission id: missing-permission" + status: 400 + title: Invalid Request + type: "urn:sandbox-agent:error:invalid_request" +status: 400 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_events_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_events_codex.snap index 3f11561..0e3a6a0 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_events_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_events_codex.snap @@ -1,6 +1,6 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1117 +assertion_line: 1106 expression: normalize_events(&reject_events) --- - agent: codex @@ -9,83 +9,28 @@ expression: normalize_events(&reject_events) started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 -- agent: codex - kind: message - message: - unparsed: true - seq: 6 -- agent: codex - kind: message - message: - unparsed: true - seq: 7 -- agent: codex - kind: message - message: - unparsed: true - seq: 8 -- agent: codex - kind: message - message: - unparsed: true - seq: 9 -- agent: codex - kind: message - message: - unparsed: true - seq: 10 -- agent: codex - kind: message - message: - unparsed: true - seq: 11 -- agent: codex - kind: message - message: - unparsed: true - seq: 12 -- agent: codex - kind: message - message: - unparsed: true - seq: 13 -- agent: codex - kind: message - message: - unparsed: true - seq: 14 -- agent: codex - kind: message - message: - unparsed: true - seq: 15 -- agent: codex - kind: message - message: - unparsed: true - seq: 16 -- agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_missing_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_missing_codex.snap index a9a068c..6c6dbae 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_missing_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reject_missing_codex.snap @@ -1,15 +1,11 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1150 +assertion_line: 1139 expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })" --- payload: - agent: codex - detail: "agent process exited: codex" - details: - exitCode: 1 - stderr: agent exited with status ExitStatus(unix_wait_status(256)) - status: 500 - title: Agent Process Exited - type: "urn:sandbox-agent:error:agent_process_exited" -status: 500 + detail: "invalid request: unknown question id: missing-question" + status: 400 + title: Invalid Request + type: "urn:sandbox-agent:error:invalid_request" +status: 400 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_events_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_events_codex.snap index e931420..7fbb0ec 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_events_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_events_codex.snap @@ -1,6 +1,6 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1056 +assertion_line: 1045 expression: normalize_events(&question_events) --- - agent: codex @@ -9,83 +9,28 @@ expression: normalize_events(&question_events) started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 -- agent: codex - kind: message - message: - unparsed: true - seq: 6 -- agent: codex - kind: message - message: - unparsed: true - seq: 7 -- agent: codex - kind: message - message: - unparsed: true - seq: 8 -- agent: codex - kind: message - message: - unparsed: true - seq: 9 -- agent: codex - kind: message - message: - unparsed: true - seq: 10 -- agent: codex - kind: message - message: - unparsed: true - seq: 11 -- agent: codex - kind: message - message: - unparsed: true - seq: 12 -- agent: codex - kind: message - message: - unparsed: true - seq: 13 -- agent: codex - kind: message - message: - unparsed: true - seq: 14 -- agent: codex - kind: message - message: - unparsed: true - seq: 15 -- agent: codex - kind: message - message: - unparsed: true - seq: 16 -- agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_missing_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_missing_codex.snap index 4d080bc..8585cd4 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_missing_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__approval_flow_snapshots@question_reply_missing_codex.snap @@ -1,15 +1,11 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1089 +assertion_line: 1078 expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })" --- payload: - agent: codex - detail: "agent process exited: codex" - details: - exitCode: 1 - stderr: agent exited with status ExitStatus(unix_wait_status(256)) - status: 500 - title: Agent Process Exited - type: "urn:sandbox-agent:error:agent_process_exited" -status: 500 + detail: "invalid request: unknown question id: missing-question" + status: 400 + title: Invalid Request + type: "urn:sandbox-agent:error:invalid_request" +status: 400 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_concurrency_snapshot@concurrency_events_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_concurrency_snapshot@concurrency_events_codex.snap index 2c4e936..a279f1f 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_concurrency_snapshot@concurrency_events_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_concurrency_snapshot@concurrency_events_codex.snap @@ -1,6 +1,6 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 1219 +assertion_line: 1214 expression: snapshot --- session_a: @@ -10,86 +10,31 @@ session_a: started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 - - agent: codex - kind: message - message: - unparsed: true - seq: 6 - - agent: codex - kind: message - message: - unparsed: true - seq: 7 - - agent: codex - kind: message - message: - unparsed: true - seq: 8 - - agent: codex - kind: message - message: - unparsed: true - seq: 9 - - agent: codex - kind: message - message: - unparsed: true - seq: 10 - - agent: codex - kind: message - message: - unparsed: true - seq: 11 - - agent: codex - kind: message - message: - unparsed: true - seq: 12 - - agent: codex - kind: message - message: - unparsed: true - seq: 13 - - agent: codex - kind: message - message: - unparsed: true - seq: 14 - - agent: codex - kind: message - message: - unparsed: true - seq: 15 - - agent: codex - kind: message - message: - unparsed: true - seq: 16 - - agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 session_b: - agent: codex kind: started @@ -97,83 +42,28 @@ session_b: started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 - - agent: codex - kind: message - message: - unparsed: true - seq: 6 - - agent: codex - kind: message - message: - unparsed: true - seq: 7 - - agent: codex - kind: message - message: - unparsed: true - seq: 8 - - agent: codex - kind: message - message: - unparsed: true - seq: 9 - - agent: codex - kind: message - message: - unparsed: true - seq: 10 - - agent: codex - kind: message - message: - unparsed: true - seq: 11 - - agent: codex - kind: message - message: - unparsed: true - seq: 12 - - agent: codex - kind: message - message: - unparsed: true - seq: 13 - - agent: codex - kind: message - message: - unparsed: true - seq: 14 - - agent: codex - kind: message - message: - unparsed: true - seq: 15 - - agent: codex - kind: message - message: - unparsed: true - seq: 16 - - agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_http_events_snapshot@http_events_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_http_events_snapshot@http_events_codex.snap index 9915b2c..4e7c929 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_http_events_snapshot@http_events_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_http_events_snapshot@http_events_codex.snap @@ -1,6 +1,6 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 714 +assertion_line: 697 expression: normalized --- - agent: codex @@ -9,83 +9,28 @@ expression: normalized started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 -- agent: codex - kind: message - message: - unparsed: true - seq: 6 -- agent: codex - kind: message - message: - unparsed: true - seq: 7 -- agent: codex - kind: message - message: - unparsed: true - seq: 8 -- agent: codex - kind: message - message: - unparsed: true - seq: 9 -- agent: codex - kind: message - message: - unparsed: true - seq: 10 -- agent: codex - kind: message - message: - unparsed: true - seq: 11 -- agent: codex - kind: message - message: - unparsed: true - seq: 12 -- agent: codex - kind: message - message: - unparsed: true - seq: 13 -- agent: codex - kind: message - message: - unparsed: true - seq: 14 -- agent: codex - kind: message - message: - unparsed: true - seq: 15 -- agent: codex - kind: message - message: - unparsed: true - seq: 16 -- agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_sse_events_snapshot@sse_events_codex.snap b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_sse_events_snapshot@sse_events_codex.snap index 76ce09f..d00b732 100644 --- a/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_sse_events_snapshot@sse_events_codex.snap +++ b/server/packages/sandbox-agent/tests/snapshots/http_sse_snapshots__run_sse_events_snapshot@sse_events_codex.snap @@ -1,6 +1,6 @@ --- source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs -assertion_line: 751 +assertion_line: 734 expression: normalized --- - agent: codex @@ -9,83 +9,28 @@ expression: normalized started: message: session.created - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 2 + started: + message: thread/started - agent: codex - kind: message - message: - unparsed: true + kind: started seq: 3 + started: + message: turn/started - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: user seq: 4 - agent: codex kind: message message: - unparsed: true + parts: + - text: "" + type: text + role: assistant seq: 5 -- agent: codex - kind: message - message: - unparsed: true - seq: 6 -- agent: codex - kind: message - message: - unparsed: true - seq: 7 -- agent: codex - kind: message - message: - unparsed: true - seq: 8 -- agent: codex - kind: message - message: - unparsed: true - seq: 9 -- agent: codex - kind: message - message: - unparsed: true - seq: 10 -- agent: codex - kind: message - message: - unparsed: true - seq: 11 -- agent: codex - kind: message - message: - unparsed: true - seq: 12 -- agent: codex - kind: message - message: - unparsed: true - seq: 13 -- agent: codex - kind: message - message: - unparsed: true - seq: 14 -- agent: codex - kind: message - message: - unparsed: true - seq: 15 -- agent: codex - kind: message - message: - unparsed: true - seq: 16 -- agent: codex - error: - kind: process_exit - message: agent exited with status ExitStatus(unix_wait_status(256)) - kind: error - seq: 17 diff --git a/todo.md b/todo.md index 0e5f8a6..b01f8d6 100644 --- a/todo.md +++ b/todo.md @@ -104,6 +104,7 @@ - [ ] Add universal API feature checklist (questions, approve plan, etc.) - [ ] Document CLI, HTTP API, frontend app, and TypeScript SDK usage - [ ] Use collapsible sections for endpoints and SDK methods +- [x] Integrate OpenAPI spec with Mintlify (docs/openapi.json + validation) --- @@ -111,3 +112,4 @@ - implement e2b example - implement typescript "start locally" by pulling form server using version - [x] Move agent schema sources to src/agents +- [x] Add Vercel AI SDK UIMessage schema extractor diff --git a/turbo.json b/turbo.json index b1ef15f..5e4c877 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,9 @@ "dependsOn": ["^generate"], "outputs": ["src/generated/**"] }, + "typecheck": { + "dependsOn": ["^build", "build"] + }, "dev": { "cache": false, "persistent": true