From d9f618556e69cf98a41e3c71f636c4ec5f5775dc Mon Sep 17 00:00:00 2001 From: Context7 Bot Date: Tue, 7 Apr 2026 13:50:05 +0000 Subject: [PATCH] docs: add auto-generated code documentation --- docs/codedocs/api-reference/box-error.md | 43 ++ docs/codedocs/api-reference/box.md | 202 +++++++++ docs/codedocs/api-reference/ephemeral-box.md | 95 ++++ docs/codedocs/api-reference/helpers.md | 68 +++ docs/codedocs/api-reference/run.md | 73 +++ docs/codedocs/api-reference/stream-run.md | 65 +++ docs/codedocs/architecture.md | 37 ++ docs/codedocs/boxes-and-lifecycle.md | 80 ++++ docs/codedocs/files-and-cwd.md | 85 ++++ docs/codedocs/guides/ephemeral-processing.md | 58 +++ docs/codedocs/guides/git-automation.md | 69 +++ docs/codedocs/guides/scheduled-jobs.md | 63 +++ docs/codedocs/guides/structured-output.md | 71 +++ docs/codedocs/index.md | 98 ++++ docs/codedocs/runs-and-streaming.md | 95 ++++ docs/codedocs/schedules-and-webhooks.md | 83 ++++ docs/codedocs/types.md | 449 +++++++++++++++++++ 17 files changed, 1734 insertions(+) create mode 100644 docs/codedocs/api-reference/box-error.md create mode 100644 docs/codedocs/api-reference/box.md create mode 100644 docs/codedocs/api-reference/ephemeral-box.md create mode 100644 docs/codedocs/api-reference/helpers.md create mode 100644 docs/codedocs/api-reference/run.md create mode 100644 docs/codedocs/api-reference/stream-run.md create mode 100644 docs/codedocs/architecture.md create mode 100644 docs/codedocs/boxes-and-lifecycle.md create mode 100644 docs/codedocs/files-and-cwd.md create mode 100644 docs/codedocs/guides/ephemeral-processing.md create mode 100644 docs/codedocs/guides/git-automation.md create mode 100644 docs/codedocs/guides/scheduled-jobs.md create mode 100644 docs/codedocs/guides/structured-output.md create mode 100644 docs/codedocs/index.md create mode 100644 docs/codedocs/runs-and-streaming.md create mode 100644 docs/codedocs/schedules-and-webhooks.md create mode 100644 docs/codedocs/types.md diff --git a/docs/codedocs/api-reference/box-error.md b/docs/codedocs/api-reference/box-error.md new file mode 100644 index 0000000..0f8b111 --- /dev/null +++ b/docs/codedocs/api-reference/box-error.md @@ -0,0 +1,43 @@ +--- +title: "BoxError" +description: "SDK error type that includes optional HTTP status codes." +--- + +**Source**: `packages/sdk/src/client.ts` + +`BoxError` is thrown for SDK-level failures, HTTP errors, timeout errors, and parsing issues. It extends `Error` and includes an optional `statusCode` when the error originates from an HTTP response. + +## When it is thrown +The SDK throws `BoxError` in several situations: missing credentials, non-2xx API responses, timeouts enforced by `AbortController`, and structured-output parsing failures. For example, `Box.create()` throws immediately if no API key is provided, and `agent.run()` throws if `responseSchema` parsing fails. These errors all share the same class so you can handle them consistently. + +## Constructor +```ts +new BoxError(message: string, statusCode?: number) +``` + +## Properties +| Property | Type | Description | +|---------|------|-------------| +| name | `string` | Always `"BoxError"`. | +| message | `string` | Error message. | +| statusCode | `number \| undefined` | HTTP status if available. | + +## Example +```ts +import { Box } from "@upstash/box"; + +try { + await Box.create(); +} catch (err) { + if (err instanceof Error) { + console.error(err.name, err.message); + } +} +``` + +## Handling patterns +If you want to surface API failures to a user, check `statusCode` and map 401/403 errors to authentication issues, 429 to rate limits, and 5xx to transient failures. For timeouts, the SDK message is usually `"Request timeout"` or `"Run timed out"`. You can retry those selectively without retrying schema parsing errors, which typically indicate a prompt mismatch. + +In typed codebases, you can use `err instanceof BoxError` to branch on Box-specific failures. This keeps your error handling narrower and avoids catching unrelated errors thrown by your own code. + +When debugging, log both `message` and `statusCode` to differentiate authentication problems from runtime errors. If the status code is missing, the error likely occurred client-side (for example, a timeout or schema parsing failure) rather than from the HTTP response itself. diff --git a/docs/codedocs/api-reference/box.md b/docs/codedocs/api-reference/box.md new file mode 100644 index 0000000..8c768df --- /dev/null +++ b/docs/codedocs/api-reference/box.md @@ -0,0 +1,202 @@ +--- +title: "Box" +description: "Create and manage durable sandboxed boxes with agents, exec, files, git, schedules, and snapshots." +--- + +**Source**: `packages/sdk/src/client.ts` + +The `Box` class is the primary SDK surface. It represents a sandboxed workspace with a runtime, filesystem, and optional AI agent. You create a Box with `Box.create()` or reconnect to one with `Box.get()`. + +## Constructor (static) +```ts +static create(config?: BoxConfig): Promise> +``` + +**BoxConfig parameters** +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| apiKey | `string` | `process.env.UPSTASH_BOX_API_KEY` | API key for Box authentication. | +| baseUrl | `string` | `https://us-east-1.box.upstash.com` | Base URL for the Box API. | +| name | `string` | — | Human-readable name for the box. | +| runtime | `"node" \| "python" \| "golang" \| "ruby" \| "rust"` | — | Runtime environment for the box. | +| size | `"small" \| "medium" \| "large"` | `"small"` | Resource size preset. | +| agent | `AgentConfig` | — | Agent provider/model configuration. | +| git | `{ token?: string; userName?: string; userEmail?: string }` | — | GitHub token and optional git identity. | +| env | `Record` | — | Environment variables injected into the box. | +| attachHeaders | `Record>` | — | Secret headers injected into outbound HTTPS requests. | +| networkPolicy | `NetworkPolicy` | `{ mode: "allow-all" }` | Outbound network access policy. | +| skills | `string[]` | — | Skills to enable (owner/repo paths). | +| mcpServers | `McpServerConfig[]` | — | MCP servers to attach. | +| timeout | `number` | `600000` | Request timeout in milliseconds. | +| debug | `boolean` | `false` | Enable debug logging. | + +**Example** +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, + env: { NODE_ENV: "production" }, +}); +``` + +## Static methods + +### `Box.get()` +```ts +static get(boxId: string, options?: BoxGetOptions): Promise> +``` +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| boxId | `string` | — | Box ID to reconnect to. | +| options.apiKey | `string` | `process.env.UPSTASH_BOX_API_KEY` | API key. | +| options.baseUrl | `string` | `https://us-east-1.box.upstash.com` | API base URL. | +| options.gitToken | `string` | — | GitHub token used for git operations. | +| options.timeout | `number` | `600000` | Request timeout. | +| options.debug | `boolean` | `false` | Enable debug logging. | + +### `Box.getByName()` +```ts +static getByName(name: string, options?: BoxGetOptions): Promise> +``` +Alias for `Box.get()` in this SDK version (name is treated as ID by the backend). + +### `Box.list()` +```ts +static list(options?: ListOptions): Promise +``` + +### `Box.delete()` +```ts +static delete(options: BoxConnectionOptions & { boxIds: string | string[] }): Promise +``` + +### `Box.fromSnapshot()` +```ts +static fromSnapshot(snapshotId: string, config?: BoxConfig): Promise> +``` + +## Instance namespaces + +### `box.agent` +```ts +run(options: RunOptions): Promise> +stream(options: StreamOptions): Promise> +``` + +### `box.exec` +```ts +command(command: string): Promise> +code(options: CodeExecutionOptions): Promise> +stream(command: string): Promise> +streamCode(options: CodeExecutionOptions): Promise> +``` + +### `box.files` +```ts +read(path: string, options?: { encoding?: "base64" }): Promise +write(options: { path: string; content: string; encoding?: "base64" }): Promise +list(path?: string): Promise +upload(files: UploadFileEntry[]): Promise +download(options?: { folder?: string }): Promise +``` + +### `box.git` +```ts +clone(options: GitCloneOptions): Promise +diff(): Promise +status(): Promise +commit(options: GitCommitOptions): Promise +updateConfig(options: GitConfigUpdateOptions): Promise +push(options?: { branch?: string }): Promise +createPR(options: GitPROptions): Promise +exec(options: GitExecOptions): Promise +checkout(options: GitCheckoutOptions): Promise +``` + +### `box.schedule` +```ts +exec(options: ExecScheduleOptions): Promise +agent(options: AgentScheduleOptions): Promise +list(): Promise +get(id: string): Promise +pause(id: string): Promise +resume(id: string): Promise +delete(id: string): Promise +``` + +### `box.skills` +```ts +add(skillId: string): Promise +remove(skillId: string): Promise +list(): Promise +``` + +## Lifecycle and config + +### `box.cd()` +```ts +cd(path: string): Promise +``` +Changes the SDK-tracked working directory after verifying the path exists. + +### `box.getStatus()` +```ts +getStatus(): Promise<{ status: string }> +``` + +### `box.configureModel()` +```ts +configureModel(model: string): Promise +``` + +### `box.updateNetworkPolicy()` +```ts +updateNetworkPolicy(policy: NetworkPolicy): Promise +``` + +### `box.pause()` / `box.resume()` / `box.delete()` +```ts +pause(): Promise +resume(): Promise +delete(): Promise +``` + +## Snapshots +```ts +snapshot(options: { name: string }): Promise +listSnapshots(): Promise +deleteSnapshot(snapshotId: string): Promise +``` + +## Logs and runs +```ts +logs(options?: { offset?: number; limit?: number }): Promise +listRuns(): Promise +``` + +## Previews +```ts +getPreviewUrl(port: number, options?: { bearerToken?: boolean; basicAuth?: boolean }): Promise +listPreviews(): Promise<{ previews: Preview[] }> +deletePreview(port: number): Promise +``` + +## Example: Combine agent + git + files +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, + git: { token: process.env.GITHUB_TOKEN }, +}); + +await box.git.clone({ repo: "https://github.com/example/project" }); +await box.agent.run({ prompt: "Add a CONTRIBUTING.md file" }); +const diff = await box.git.diff(); +console.log(diff.slice(0, 200)); + +await box.delete(); +``` diff --git a/docs/codedocs/api-reference/ephemeral-box.md b/docs/codedocs/api-reference/ephemeral-box.md new file mode 100644 index 0000000..1960c21 --- /dev/null +++ b/docs/codedocs/api-reference/ephemeral-box.md @@ -0,0 +1,95 @@ +--- +title: "EphemeralBox" +description: "Create short-lived boxes optimized for exec and file operations." +--- + +**Source**: `packages/sdk/src/client.ts` + +`EphemeralBox` is a lightweight wrapper around `Box` for short-lived tasks. It exposes exec, files, schedules, and lifecycle methods, but excludes agent and git operations. + +## Constructor (static) +```ts +static create(config?: EphemeralBoxConfig): Promise +``` + +**EphemeralBoxConfig parameters** +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| apiKey | `string` | `process.env.UPSTASH_BOX_API_KEY` | API key for Box authentication. | +| baseUrl | `string` | `https://us-east-1.box.upstash.com` | API base URL. | +| name | `string` | — | Human-readable name. | +| runtime | `"node" \| "python" \| "golang" \| "ruby" \| "rust"` | — | Runtime environment. | +| size | `"small" \| "medium" \| "large"` | `"small"` | Resource size preset. | +| ttl | `number` | `259200` | Time-to-live in seconds (max 3 days). | +| env | `Record` | — | Environment variables for the box. | +| attachHeaders | `Record>` | — | Secret outbound headers. | +| networkPolicy | `NetworkPolicy` | `{ mode: "allow-all" }` | Outbound network policy. | +| timeout | `number` | `600000` | Request timeout. | +| debug | `boolean` | `false` | Enable debug logging. | + +## Static methods + +### `EphemeralBox.fromSnapshot()` +```ts +static fromSnapshot(snapshotId: string, config?: EphemeralBoxConfig): Promise +``` + +### `EphemeralBox.getByName()` +```ts +static getByName(name: string, options?: BoxGetOptions): Promise +``` + +### `EphemeralBox.delete()` +```ts +static delete(options: BoxConnectionOptions & { boxIds: string | string[] }): Promise +``` + +## Instance namespaces + +### `box.exec` +```ts +command(command: string): Promise> +code(options: CodeExecutionOptions): Promise> +stream(command: string): Promise> +streamCode(options: CodeExecutionOptions): Promise> +``` + +### `box.files` +```ts +read(path: string, options?: { encoding?: "base64" }): Promise +write(options: { path: string; content: string; encoding?: "base64" }): Promise +list(path?: string): Promise +upload(files: UploadFileEntry[]): Promise +download(options?: { folder?: string }): Promise +``` + +### `box.schedule` +```ts +exec(options: ExecScheduleOptions): Promise +agent(options: AgentScheduleOptions): Promise +list(): Promise +get(id: string): Promise +pause(id: string): Promise +resume(id: string): Promise +delete(id: string): Promise +``` + +## Lifecycle +```ts +cd(path: string): Promise +getStatus(): Promise<{ status: string }> +delete(): Promise +snapshot(options: { name: string }): Promise +listSnapshots(): Promise +deleteSnapshot(snapshotId: string): Promise +``` + +## Example +```ts +import { EphemeralBox } from "@upstash/box"; + +const box = await EphemeralBox.create({ runtime: "node", ttl: 3600 }); +const run = await box.exec.command("node -e 'console.log(1+1)'"); +console.log(run.result); +await box.delete(); +``` diff --git a/docs/codedocs/api-reference/helpers.md b/docs/codedocs/api-reference/helpers.md new file mode 100644 index 0000000..3413113 --- /dev/null +++ b/docs/codedocs/api-reference/helpers.md @@ -0,0 +1,68 @@ +--- +title: "Helpers" +description: "Utility helpers for inferring agent providers from model strings." +--- + +**Source**: `packages/sdk/src/client.ts` + +The SDK exports a small set of helper functions that make model configuration easier. These helpers are useful when you receive model strings dynamically and want a provider inferred automatically. + +## `inferDefaultProvider` +```ts +inferDefaultProvider(model: string): Agent +``` + +Infers the agent provider based on the model prefix: +- `openrouter/` → `Agent.ClaudeCode` +- `opencode/` → `Agent.OpenCode` +- `openai/` → `Agent.Codex` +- otherwise defaults to `Agent.ClaudeCode` + +**Example** +```ts +import { inferDefaultProvider, Agent } from "@upstash/box"; + +const provider = inferDefaultProvider("openai/gpt-5.3-codex"); +console.log(provider === Agent.Codex); // true +``` + +## When to use it +If you store models as strings in a database or configuration file, `inferDefaultProvider` helps you avoid duplicating a separate provider field. It is also useful when migrating older code that used the `runner` field in `AgentConfig`, because you can infer the provider and pass it explicitly. + +The inference is intentionally simple. It only checks string prefixes and falls back to `Agent.ClaudeCode` for unknown patterns. If you use a custom provider string, pass `provider` explicitly rather than relying on inference. + +If you are integrating with OpenRouter, the model names begin with `openrouter/`, which maps to `Agent.ClaudeCode` in this SDK. That is intentional because OpenRouter is routed through the Claude Code agent on the backend. When in doubt, set `provider` explicitly to remove ambiguity. + +This keeps configuration predictable. + +It also simplifies migrations. + +Use it sparingly. + +## `inferDefaultRunner` +```ts +inferDefaultRunner(model: string): Agent +``` + +Deprecated alias for `inferDefaultProvider`. Use the new function name when possible. + +**Example** +```ts +import { inferDefaultRunner } from "@upstash/box"; + +const provider = inferDefaultRunner("opencode/claude-sonnet-4-5"); +console.log(provider); +``` + +## Example: wiring inference into Box.create +```ts +import { Box, Agent, inferDefaultProvider } from "@upstash/box"; + +const model = "openrouter/anthropic/claude-opus-4-5"; +const provider = inferDefaultProvider(model); + +const box = await Box.create({ + runtime: "node", + agent: { provider, model }, +}); +``` diff --git a/docs/codedocs/api-reference/run.md b/docs/codedocs/api-reference/run.md new file mode 100644 index 0000000..48a689d --- /dev/null +++ b/docs/codedocs/api-reference/run.md @@ -0,0 +1,73 @@ +--- +title: "Run" +description: "Represents a single agent or exec execution with status, result, and cost metadata." +--- + +**Source**: `packages/sdk/src/client.ts` + +`Run` encapsulates a single execution. It is returned by `box.agent.run()`, `box.exec.command()`, and `box.exec.code()`. It tracks status, output, exit code, and cost metadata. + +## Constructor +`Run` is created internally by the SDK and is not instantiated directly. + +## When to use `Run` +Use `Run` when you want a simple request/response model: submit work, wait for completion, and consume the final output. It is a good fit for server-side tasks, cron-like jobs, or background workers where streaming output is not necessary. + +## Properties +| Property | Type | Description | +|---------|------|-------------| +| id | `string` | Run ID. Initially local, replaced by backend ID when available. | +| status | `"running" \| "completed" \| "failed" \| "cancelled" \| "detached"` | Final or current status. | +| result | `T` | Final output (typed if `responseSchema` was used). | +| exitCode | `number \| null` | Exit code for exec/code runs. | +| cost | `RunCost` | Token usage and compute time. | + +## Methods + +### `run.cancel()` +```ts +cancel(): Promise +``` +Cancels a running execution and updates status to `cancelled`. + +### `run.logs()` +```ts +logs(): Promise +``` +Fetches structured logs for the time window around this run. + +## Example +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const run = await box.exec.command("echo hello"); +console.log(run.result); // "hello" + +const logs = await run.logs(); +console.log(logs.length); + +await box.delete(); +``` + +## Example: handle failures and inspect cost +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const run = await box.exec.command("exit 1"); +if (run.status === "failed") { + console.log("Exit code:", run.exitCode); +} +console.log("Compute ms:", run.cost.computeMs); + +await box.delete(); +``` diff --git a/docs/codedocs/api-reference/stream-run.md b/docs/codedocs/api-reference/stream-run.md new file mode 100644 index 0000000..984132e --- /dev/null +++ b/docs/codedocs/api-reference/stream-run.md @@ -0,0 +1,65 @@ +--- +title: "StreamRun" +description: "Async-iterable Run for streaming output chunks in real time." +--- + +**Source**: `packages/sdk/src/client.ts` + +`StreamRun` extends `Run` and implements `AsyncIterable`, so you can `for await` over output chunks as they arrive. It is returned by `box.agent.stream()`, `box.exec.stream()`, and `box.exec.streamCode()`. + +## Constructor +`StreamRun` is created internally by the SDK and is not instantiated directly. + +## Properties +All properties of `Run`, plus async iteration support. While streaming, `status` is `"running"`. After iteration finishes, the SDK updates `status` to `"completed"` or `"failed"` depending on the final event and exit code. + +## Usage +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const stream = await box.agent.stream({ + prompt: "Refactor the auth flow and explain the changes.", +}); + +for await (const chunk of stream) { + if (chunk.type === "text-delta") process.stdout.write(chunk.text); + if (chunk.type === "finish") console.log(" +Tokens:", chunk.usage); +} + +await box.delete(); +``` + +## Notes +- If you break out of the loop early, the SDK marks the run as `detached`. +- Call `stream.cancel()` if you want to stop execution explicitly. + +## Chunk shapes +Agent streams emit `Chunk` objects with types like `start`, `text-delta`, `tool-call`, `finish`, and `stats`. Exec streams emit `ExecStreamChunk` objects with `output` and `exit` events. In both cases you can treat the stream as a real-time log and update UI or state incrementally. + +A common pattern is to buffer `text-delta` into a string, handle `tool-call` events for instrumentation, and read token usage from the final `finish` chunk. For exec streams, you can concatenate `output` chunks and check `exitCode` when the `exit` event arrives. + +## Example: capture output incrementally +```ts +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const stream = await box.agent.stream({ prompt: "Summarize this repo." }); +let output = ""; +for await (const chunk of stream) { + if (chunk.type === "text-delta") output += chunk.text; + if (chunk.type === "finish") console.log("Final tokens:", chunk.usage); +} + +console.log(output.slice(0, 120)); +await box.delete(); +``` diff --git a/docs/codedocs/architecture.md b/docs/codedocs/architecture.md new file mode 100644 index 0000000..fdae789 --- /dev/null +++ b/docs/codedocs/architecture.md @@ -0,0 +1,37 @@ +--- +title: "Architecture" +description: "How the Box SDK is layered, how requests flow, and why key design decisions were made." +--- + +This SDK is intentionally small and explicit. The public surface lives in `packages/sdk/src/index.ts`, while implementation details are concentrated in `packages/sdk/src/client.ts` and type definitions in `packages/sdk/src/types.ts`. The result is a single entry point that exports a focused set of classes and types, with one implementation file that owns all networking and streaming logic. + +```mermaid +graph TD + A[index.ts exports] --> B[client.ts] + A --> C[types.ts] + B --> D[Box] + B --> E[EphemeralBox] + B --> F[Run / StreamRun] + B --> G[_request + fetch] + B --> H[buildRunRequest] + H --> I[Multipart FormData] + H --> J[JSON body] + D --> K[agent.run / stream] + D --> L[exec / files / git / schedule] + C --> M[Config & Response Types] +``` + +**Key design decisions (and why)** +- **Single implementation module (`client.ts`)**: The SDK avoids a fragmented class hierarchy. `Box`, `EphemeralBox`, `Run`, and `StreamRun` share the same request and streaming logic, which keeps the surface predictable and makes it easy to reason about side effects. This is visible in `packages/sdk/src/client.ts`, where all class methods delegate to `_request`, `_run`, `_stream`, and `_parseExecStream`. +- **Typed API with lightweight runtime dependencies**: Types are exported from `packages/sdk/src/types.ts` and only one runtime dependency (`zod-to-json-schema`) is used, enabling structured outputs without pulling in a full schema validator at runtime. The SDK accepts a Zod schema and converts it to JSON Schema when needed. +- **Streaming built on SSE parsing**: Both `agent.stream()` and `exec.stream()` parse server-sent events. The agent stream parser understands `run_start`, `text`, `tool`, `done`, and `stats` events, while exec streams parse `event: exit`. This design reduces latency and gives you partial results quickly. +- **Separation of “Box” vs “EphemeralBox”**: Ephemeral boxes are created synchronously and expose only exec, files, and schedules. Internally, `EphemeralBox` wraps a normal `Box` instance and forwards operations, but avoids agent and git functionality. This keeps the API honest about what the backend supports. + +**How the pieces fit together** +1. **Entry point**: `index.ts` re-exports public classes and types. This is the only supported import path for application code. +2. **Box creation**: `Box.create()` builds a request body from `BoxConfig` and hits `POST /v2/box`. It then polls until `status !== "creating"`, which keeps the developer experience synchronous and reliable. +3. **Runs**: `box.agent.run()` uses `_executeRun()` to stream output over SSE, buffers text into `rawOutput`, and parses structured output if `responseSchema` is provided. `box.agent.stream()` returns a `StreamRun` that yields `Chunk` objects from the same SSE channel. +4. **Exec and files**: Exec uses `/exec` or `/exec-stream` endpoints, while file operations map to `/files/read`, `/files/write`, `/files/upload`, and `/files/download`. The SDK tracks `cwd` locally and injects `folder` parameters into these calls to keep path usage consistent. +5. **Schedules**: The `schedule` namespace maps to `/schedules` endpoints. These methods are lightweight wrappers that translate `ExecScheduleOptions` and `AgentScheduleOptions` into backend fields. + +The end result is a small but powerful client: everything you do is an HTTP request to the Box API, and every response is normalized into a consistent `Run` model with status, result, and cost metadata. diff --git a/docs/codedocs/boxes-and-lifecycle.md b/docs/codedocs/boxes-and-lifecycle.md new file mode 100644 index 0000000..f4e0a94 --- /dev/null +++ b/docs/codedocs/boxes-and-lifecycle.md @@ -0,0 +1,80 @@ +--- +title: "Boxes And Lifecycle" +description: "Understand Box and EphemeralBox lifecycles, state transitions, and how the SDK keeps them in sync." +--- + +A **Box** is a sandboxed workspace with a runtime, filesystem, and (optionally) an AI agent. An **EphemeralBox** is a lighter-weight variant intended for short-lived tasks, created instantly and auto-deleted after a TTL. Both are created through the Box API, but they differ in lifecycle guarantees and supported features. + +**Why this concept exists** +Most AI workflows need real file I/O, command execution, and a stable working directory. A simple prompt API does not provide these affordances. Box solves this by creating an isolated environment that persists between calls, while EphemeralBox offers a cost-effective alternative for quick, disposable tasks. + +**How it relates to other concepts** +- **Runs and streaming** happen inside a Box or EphemeralBox. +- **Files and cwd** are tracked per Box and affect every operation. +- **Schedules and webhooks** execute against a specific Box ID. + +```mermaid +stateDiagram-v2 + [*] --> Creating + Creating --> Idle: created + Creating --> Error: failed + Idle --> Running: agent/exec + Running --> Idle: completed + Idle --> Paused: pause() + Paused --> Idle: resume() + Idle --> Deleted: delete() +``` + +**How it works internally** +`Box.create()` builds a request body from `BoxConfig` and posts it to `POST /v2/box` in `packages/sdk/src/client.ts`. The SDK then polls the box status every 2 seconds until it is no longer `"creating"`, or a 5-minute timeout is hit. This is why `Box.create()` is async and why it can throw `BoxError("Box creation timed out")`. + +`EphemeralBox.create()` uses the same endpoint but sets `ephemeral: true` and skips polling. Internally, it wraps the resulting `Box` instance and forwards exec, files, schedule, and lifecycle methods. The wrapper is a guardrail: it keeps the API honest by not exposing agent or git operations that the backend does not support for ephemeral boxes. + +**Basic usage** +```ts filename="create-box.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const run = await box.agent.run({ prompt: "List the files in the workspace." }); +console.log(run.result); + +await box.pause(); +await box.resume(); +await box.delete(); +``` + +**Advanced / edge-case usage** +```ts filename="ephemeral-box.ts" +import { EphemeralBox } from "@upstash/box"; + +const box = await EphemeralBox.create({ + runtime: "python", + ttl: 1800, + env: { MODE: "fast" }, +}); + +const run = await box.exec.command("python -c 'print(2 + 2)'" ); +console.log(run.result); // "4" + +await box.delete(); // optional, but releases resources immediately +``` + + +Make sure the Box has an agent configured before calling `box.agent.run()` or `box.agent.stream()`. If you create a box without `agent` in `BoxConfig`, those methods throw a `BoxError` with guidance on how to configure the agent. Also ensure `UPSTASH_BOX_API_KEY` is set or passed explicitly, or `Box.create()` will fail immediately. + + + + +A durable Box preserves state until you delete it, which is ideal for multi-step coding flows, long-lived agents, and snapshot workflows. Ephemeral boxes are cheaper and faster to create, but they do not support agent or git operations and are auto-deleted after a TTL. If you need to run a single command or process a small set of files, EphemeralBox keeps cost and cleanup overhead low. If you need to iterate on a repository, run multiple prompts, or store snapshots, a full Box is the correct choice. + + +`Box.create()` polls the API until the box transitions out of `creating`. This makes the API simple to use but adds a startup delay. `EphemeralBox.create()` avoids polling to keep latency low, which is why it is recommended for short-lived tasks. If you are launching many boxes in parallel, polling can add a few seconds of overhead that you should plan for in your concurrency model. + + +Box size determines CPU and memory. Larger boxes can run heavier commands and handle bigger codebases, but they are more expensive and slower to provision. If you only need to run quick scripts or light prompts, `size: "small"` is often sufficient. If you are compiling, running tests, or generating large artifacts, consider `medium` or `large` and measure the run time improvements against cost. + + diff --git a/docs/codedocs/files-and-cwd.md b/docs/codedocs/files-and-cwd.md new file mode 100644 index 0000000..51e7849 --- /dev/null +++ b/docs/codedocs/files-and-cwd.md @@ -0,0 +1,85 @@ +--- +title: "Files And Cwd" +description: "Learn how file operations and working directories are resolved inside a Box." +--- + +The Box SDK exposes a file system API (`box.files`) and a tracked working directory (`box.cwd`). Together, they let you read and write files, upload assets, and execute commands relative to the same directory without manually rewriting paths. + +**Why this concept exists** +AI coding workflows frequently need to read, modify, and generate files. A stable working directory makes it easy to chain operations: clone a repo, `cd` into it, run commands, then stream the output without reconstructing paths every time. + +**How it relates to other concepts** +- **Runs** and **exec** inherit the current working directory. +- **Git operations** use the same `cwd` folder by default. +- **Schedules** can override the working directory with `folder`, but default to the Box `cwd`. + +```mermaid +flowchart TD + A[box.cwd] --> B[_getFolder] + B --> C{Workspace root?} + C -->|Yes| D[folder = ""] + C -->|No| E[folder = cwd relative] + E --> F[HTTP request with folder] + A --> G[_resolvePath] + G --> H[files.read/write/upload/download] +``` + +**How it works internally** +The SDK tracks `cwd` locally in `packages/sdk/src/client.ts`. `box.cd()` does not change the server's shell state; it updates `_cwd` in memory after verifying that the path exists by running `ls` inside the box. Every file, git, and exec method calls `_getFolder()` or `_resolvePath()` to translate relative paths into absolute box paths or `folder` query parameters. + +For uploads, the SDK reads local files and builds a multipart `FormData` request. For downloads, it fetches each file from `/files/download` and writes them to a local directory named after the remote folder, which means downloads affect your local filesystem, not the box. + +**Basic usage** +```ts filename="files-basic.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +await box.files.write({ path: "notes/todo.txt", content: "Ship the feature" }); +const contents = await box.files.read("notes/todo.txt"); +console.log(contents); + +const entries = await box.files.list("notes"); +console.log(entries.map((e) => e.name)); + +await box.delete(); +``` + +**Advanced / edge-case usage (cwd + downloads)** +```ts filename="files-advanced.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +await box.exec.command("mkdir -p /workspace/home/project/src"); +await box.cd("project"); + +await box.files.write({ path: "src/index.ts", content: "export const value = 42;" }); + +// Downloads write to local disk under ./project +await box.files.download({ folder: "src" }); + +await box.delete(); +``` + + +`box.cd()` only updates the SDK's internal path. If you run an exec command that changes directories (for example `cd src && ls`), that change does not persist between calls. Also note that `files.download()` writes to your local filesystem, so run it from a directory where you want the output folder to be created. + + + + +Relative paths are resolved against `box.cwd`, which keeps code concise but can be surprising if you forget to update `cwd`. Absolute paths bypass the cwd and are sent as-is. If you are automating across multiple repos inside the same box, use `cd()` and relative paths for each repo, then reset back to `/workspace/home` between steps. This pattern keeps file and git operations consistent without hard-coding long paths. + + +Uploading local files uses multipart form data, which is convenient for binary assets but requires reading from disk on the client side. If your input is already in memory or base64, you can attach files to prompts through `RunOptions.files` instead of uploading first. For large batches, consider compressing files or filtering to the minimal set, since each file counts toward API limits. Choose the path that keeps your latency and payload sizes predictable. + + +Downloads are intentionally simple: they fetch each file and write it to a local directory. This works well for small outputs but can be slow for large trees because each file is requested separately. If you need to transfer many files frequently, consider generating a tarball inside the box with `exec.command()` and downloading that single artifact. That approach reduces round trips and often simplifies cleanup. + + diff --git a/docs/codedocs/guides/ephemeral-processing.md b/docs/codedocs/guides/ephemeral-processing.md new file mode 100644 index 0000000..4590761 --- /dev/null +++ b/docs/codedocs/guides/ephemeral-processing.md @@ -0,0 +1,58 @@ +--- +title: "Ephemeral Processing" +description: "Use EphemeralBox for short-lived tasks like file processing and quick execs." +--- + +Ephemeral boxes are ideal when you need a fast, disposable environment. They are created immediately and auto-delete after a TTL, making them perfect for one-off scripts, transforms, or batch processing. + +**Problem** +You need a sandboxed runtime for a short task, but a full Box feels heavy and you do not need an AI agent or git. + +**Solution** +Use `EphemeralBox`. It provides the same exec and file APIs, but skips agent and git features to reduce overhead. + + + +### Create an ephemeral box +```ts filename="ephemeral-processing.ts" +import { EphemeralBox } from "@upstash/box"; + +const box = await EphemeralBox.create({ + runtime: "python", + ttl: 1800, + env: { MODE: "fast" }, +}); +``` + + +### Upload data and process it +```ts filename="ephemeral-processing.ts" +await box.files.upload([ + { path: "./data/input.csv", destination: "input.csv" }, +]); + +const run = await box.exec.command( + "python -c 'import pandas as pd; df=pd.read_csv(" + + ""input.csv"" + + "); print(df.head(2).to_string(index=False))'" +); + +console.log(run.result); +``` + + +### Download outputs and delete +```ts filename="ephemeral-processing.ts" +await box.files.download({ folder: "." }); +await box.delete(); +``` + + + +**Why this matters** +Ephemeral boxes provide the same sandboxing guarantees but avoid the startup cost of agent configuration and polling. If your application spins up many short-lived jobs, this is the most cost-efficient approach. + +**When to use EphemeralBox** +Choose EphemeralBox for ETL-style tasks, quick builds, or any workflow where you only need exec and files. Because ephemeral boxes skip agent setup and polling, they start faster and are simpler to clean up. If you later need an agent or git operations, switch to a full Box and keep the same code paths for exec and files. This makes it easy to prototype on EphemeralBox and scale up to a full-featured Box when the workflow grows. + +That migration path is smooth. diff --git a/docs/codedocs/guides/git-automation.md b/docs/codedocs/guides/git-automation.md new file mode 100644 index 0000000..2b5cecb --- /dev/null +++ b/docs/codedocs/guides/git-automation.md @@ -0,0 +1,69 @@ +--- +title: "Git Automation" +description: "Clone a repo, make changes, commit, and open a pull request inside a Box." +--- + +This guide shows how to automate a full git workflow in a sandboxed Box: clone a repository, modify files, commit, push, and create a pull request. It mirrors a real engineering workflow and is ideal for generating PRs programmatically. + +**Problem** +You want to use an AI agent to make changes to a repository, but you need deterministic git behavior and a safe environment to push the results. + +**Solution** +Use the Box git namespace to clone and manipulate the repo. The Box runtime includes git, and the SDK exposes common git operations plus PR creation. + + + +### Create a box with git access +```ts filename="git-automation.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, + git: { token: process.env.GITHUB_TOKEN }, +}); +``` + + +### Clone, modify, and review +```ts filename="git-automation.ts" +await box.git.clone({ repo: "https://github.com/example/project" }); + +await box.files.write({ + path: "docs/BOX_NOTES.md", + content: "This file was generated by Upstash Box.", +}); + +const diff = await box.git.diff(); +console.log(diff.slice(0, 400)); +``` + + +### Commit, push, and open a PR +```ts filename="git-automation.ts" +await box.git.commit({ message: "docs: add Box notes" }); +await box.git.push({ branch: "main" }); + +const pr = await box.git.createPR({ + title: "docs: add Box notes", + body: "Automated documentation update.", + base: "main", +}); + +console.log(pr.url); +``` + + +### Clean up +```ts filename="git-automation.ts" +await box.delete(); +``` + + + +**Production tips** +- Use a dedicated GitHub token with least-privilege access. +- Call `box.git.updateConfig()` if you need a specific git author identity. +- Consider writing a snapshot before and after large changes so you can restore quickly during development. + +If you are automating multiple PRs in parallel, create one Box per branch to avoid cross-contamination. Each Box tracks its own filesystem and git state, so you can safely run agents concurrently without race conditions. For large repos, start by cloning and immediately creating a snapshot so subsequent jobs can restore quickly without re-cloning. diff --git a/docs/codedocs/guides/scheduled-jobs.md b/docs/codedocs/guides/scheduled-jobs.md new file mode 100644 index 0000000..7777e96 --- /dev/null +++ b/docs/codedocs/guides/scheduled-jobs.md @@ -0,0 +1,63 @@ +--- +title: "Scheduled Jobs" +description: "Set up cron-based exec and agent jobs with optional webhooks." +--- + +This guide shows how to set up recurring jobs inside a Box. You can run shell commands, agent prompts, or a mix of both, and optionally receive results via a webhook. + +**Problem** +You want to run maintenance or analysis tasks on a schedule without managing your own cron infrastructure. + +**Solution** +Use the Box schedule API to create `exec` or `agent` schedules with cron expressions, then query or pause them as needed. + + + +### Create a box and schedule an exec job +```ts filename="scheduled-jobs.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const execSchedule = await box.schedule.exec({ + cron: "*/5 * * * *", + command: ["bash", "-c", "date >> /workspace/home/cron.log"], +}); + +console.log(execSchedule.id); +``` + + +### Add a daily agent summary +```ts filename="scheduled-jobs.ts" +const agentSchedule = await box.schedule.agent({ + cron: "0 9 * * *", + prompt: "Summarize /workspace/home/cron.log in bullet points.", + webhookUrl: "https://example.com/hooks/box", +}); + +console.log(agentSchedule.id); +``` + + +### Pause or delete schedules +```ts filename="scheduled-jobs.ts" +const schedules = await box.schedule.list(); +await box.schedule.pause(schedules[0].id); +await box.schedule.resume(schedules[0].id); +await box.schedule.delete(execSchedule.id); +``` + + + +**Operational tips** +- Cron expressions are interpreted in UTC, so convert local times carefully. +- If you change directories with `box.cd()`, schedules you created earlier will not update their folder path. +- For high-frequency tasks, consider batch work in a single run to avoid excessive scheduling overhead. + +If you need a verified delivery pipeline, pair schedules with a webhook endpoint that records the `run_id` and `status` for auditing. This makes it easy to build dashboards or alerting when runs fail. You can also use `box.schedule.list()` and `box.schedule.get()` to reconcile the desired schedule set with the actual one when deploying updates. + +Treat schedules as infrastructure. diff --git a/docs/codedocs/guides/structured-output.md b/docs/codedocs/guides/structured-output.md new file mode 100644 index 0000000..ddaa6fb --- /dev/null +++ b/docs/codedocs/guides/structured-output.md @@ -0,0 +1,71 @@ +--- +title: "Structured Output" +description: "Use Zod schemas to get typed, validated output from Box runs." +--- + +When you need predictable JSON from an agent, structured output is the safest option. You define a Zod schema, pass it to `responseSchema`, and the SDK validates the model output before returning it as a typed object. + +**Problem** +Free-form text is hard to parse and can break downstream automation. You want data that is guaranteed to match a schema. + +**Solution** +Use `responseSchema` with a Zod schema. The SDK converts it to JSON Schema, requests structured output, and parses the final result into strongly typed data. + + + +### Define the schema and create a box +```ts filename="structured-output.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; +import { z } from "zod"; + +const reviewSchema = z.object({ + title: z.string(), + risk: z.enum(["low", "medium", "high"]), + summary: z.string(), + files: z.array(z.string()), +}); + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); +``` + + +### Run the prompt with `responseSchema` +```ts filename="structured-output.ts" +const run = await box.agent.run({ + prompt: "Summarize this diff and list the files changed: ./", + responseSchema: reviewSchema, +}); + +console.log(run.result.title); +console.log(run.result.risk); +console.log(run.result.files); +``` + + +### Clean up +```ts filename="structured-output.ts" +await box.delete(); +``` + + + +Expected output (shape): +``` +{ + "title": "Refactor auth middleware", + "risk": "medium", + "summary": "...", + "files": ["src/auth.ts", "src/server.ts"] +} +``` + +**Notes and tips** +- The SDK throws a `BoxError` if the output cannot be parsed into the schema. +- Schema validation happens after the run completes, so you still want to craft prompts that describe the expected JSON clearly. +- If you need streaming *and* structured output, you can run a non-streaming request to produce validated results, and a separate streaming run for live progress. + +**Troubleshooting** +If the run fails with a schema parsing error, log the raw output and compare it with your schema field names and types. Models often return extra commentary when the prompt is vague. Make the prompt strict by telling the model to respond with JSON only, and include an example object that matches the schema. When the response is still inconsistent, narrow the schema to the minimum required fields and expand it gradually once the output is stable. diff --git a/docs/codedocs/index.md b/docs/codedocs/index.md new file mode 100644 index 0000000..0b60b79 --- /dev/null +++ b/docs/codedocs/index.md @@ -0,0 +1,98 @@ +--- +title: "Upstash Box" +description: "Create and control sandboxed AI coding environments with streaming runs, file I/O, git automation, and snapshots." +--- + +Upstash Box is a TypeScript SDK for creating sandboxed AI coding agents that can run prompts, execute commands, and manipulate files in isolated environments. + +**The Problem** +- You need a reproducible coding sandbox with real file systems and shells, not just chat completions. +- Running AI agents in parallel is hard when each task needs its own isolated workspace. +- Automating git workflows (clone, commit, PR) requires stable credentials and a predictable runtime. +- Long-running tasks need snapshots, schedules, and webhooks to avoid blocking your app. + +**The Solution** +Upstash Box provides an API-first sandbox that your code can drive. You create a box, run an agent or shell command, stream output, and then persist or discard the environment. The SDK wraps the Box API with typed methods, streaming utilities, and helper abstractions. + +```ts filename="quick-solution.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const run = await box.agent.run({ + prompt: "Create a hello-world Express server and explain how to run it.", +}); + +console.log(run.result); +await box.delete(); +``` + +**Installation** + + +```bash +npm install @upstash/box +``` + + +```bash +pnpm add @upstash/box +``` + + +```bash +yarn add @upstash/box +``` + + +```bash +bun add @upstash/box +``` + + + +**Quick start** +```ts filename="quick-start.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + apiKey: process.env.UPSTASH_BOX_API_KEY, + runtime: "node", + agent: { + provider: Agent.ClaudeCode, + model: ClaudeCode.Sonnet_4_5, + apiKey: process.env.CLAUDE_KEY, + }, +}); + +const run = await box.agent.run({ prompt: "Write a file hello.ts that prints Hello" }); +console.log(run.result); + +const content = await box.files.read("hello.ts"); +console.log(content.slice(0, 80)); + +await box.delete(); +``` + +Expected output (abridged): +``` +File created successfully... +console.log("Hello") +``` + +**Key features** +- Create long-lived or ephemeral sandboxes with configurable runtime and size. +- Run agents with streaming output and structured results via Zod schemas. +- Execute shell commands or inline code with unified `Run` objects. +- Upload, download, read, and write files with a tracked working directory. +- Automate git workflows: clone, diff, commit, push, and create PRs. +- Schedule recurring prompts or commands with cron and webhook callbacks. + + + How the SDK is structured internally + Learn the Box, Run, and file model + Explore the full SDK surface + diff --git a/docs/codedocs/runs-and-streaming.md b/docs/codedocs/runs-and-streaming.md new file mode 100644 index 0000000..96bf7d8 --- /dev/null +++ b/docs/codedocs/runs-and-streaming.md @@ -0,0 +1,95 @@ +--- +title: "Runs And Streaming" +description: "How Run and StreamRun work, how streaming is parsed, and how structured output is validated." +--- + +A **Run** represents a single execution inside a Box: an agent prompt, a shell command, or inline code. A **StreamRun** is a Run that yields incremental output chunks as the server sends them. These objects are returned by `box.agent.run()`, `box.agent.stream()`, `box.exec.command()`, and their streaming variants. + +**Why this concept exists** +AI tasks can be long-running, and you often need to show partial output, capture tool use, or stop execution early. Runs provide a consistent way to track status, cost, and output across agent and exec workflows, while StreamRun adds async iteration for low-latency UX. + +**How it relates to other concepts** +- Runs are created inside a **Box** or **EphemeralBox**. +- **Files and cwd** influence the execution context of each run. +- **Schedules and webhooks** create runs asynchronously and report results out-of-band. + +```mermaid +sequenceDiagram + participant Client + participant BoxAPI + Client->>BoxAPI: POST /v2/box/:id/run/stream + BoxAPI-->>Client: event: run_start (run_id) + BoxAPI-->>Client: event: text (chunks) + BoxAPI-->>Client: event: tool (tool calls) + BoxAPI-->>Client: event: done (usage + output) + Client->>Client: Update Run.status, Run.result +``` + +**How it works internally** +`Run` and `StreamRun` live in `packages/sdk/src/client.ts`. A `Run` holds internal fields for `status`, `result`, `exitCode`, and cost. The SDK updates those fields through the private `Run._update()` method to keep state consistent across different call paths. + +For `box.agent.run()`, the SDK actually uses the streaming endpoint (`/run/stream`) and consumes the SSE stream fully. It buffers `text` events into `rawOutput`, updates token usage when it sees `done`, and finally parses structured output if you passed a Zod schema. For `box.agent.stream()`, it exposes each parsed SSE event as a `Chunk` so you can render progress or react to tool calls in real time. + +Structured output is handled by `toJsonSchema()` which converts your Zod schema into JSON Schema for the API, and then parses the final output string back into a typed object. If parsing fails, the SDK throws a `BoxError` with the raw output preview, which helps debug prompt mismatches. + +**Basic usage (structured output)** +```ts filename="structured-run.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; +import { z } from "zod"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const schema = z.object({ title: z.string(), score: z.number() }); + +const run = await box.agent.run({ + prompt: "Score this pull request from 0-100 and provide a title.", + responseSchema: schema, +}); + +console.log(run.result.title, run.result.score); +await box.delete(); +``` + +**Advanced / edge-case usage (stream + tool hooks)** +```ts filename="streaming-run.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const stream = await box.agent.stream({ + prompt: "Refactor the auth flow and explain the changes.", + onToolUse: (tool) => { + console.log("Tool:", tool.name, JSON.stringify(tool.input)); + }, +}); + +for await (const chunk of stream) { + if (chunk.type === "text-delta") process.stdout.write(chunk.text); + if (chunk.type === "finish") console.log(" +Tokens:", chunk.usage); +} + +await box.delete(); +``` + + +If you break out of a `for await` loop early, the SDK marks the run as `detached` because the server may still be executing. If you need to stop execution, call `run.cancel()` explicitly. Also be careful with `responseSchema`: a mismatched schema will throw a `BoxError` even if the model output looks correct to a human. + + + + +`agent.run()` is simpler to consume, but you only get the final output when the run finishes. `agent.stream()` yields partial output and tool events as they happen, which is essential for interactive UX and progressive rendering. The trade-off is more control flow complexity: you must iterate the stream, handle early exit, and consider detachment. If you are building a CLI or UI, streaming is usually worth the additional logic. + + +Webhook runs (`webhook` in `RunOptions`) return immediately and deliver results to a URL later. This is ideal for serverless or background workflows where keeping a connection open is expensive. The downside is you lose real-time progress and must secure and validate incoming webhook payloads. Use webhooks when you can tolerate eventual results and prefer not to block your process. + + +`maxRetries` applies an exponential backoff around the underlying streaming request. This helps with transient network issues but can hide persistent failures if your prompt or credentials are invalid. Timeouts are enforced with `AbortController` and produce `BoxError("Run timed out")`, which is distinct from model errors. If you set both, design your retry logic to avoid hammering the API with the same failing prompt. + + diff --git a/docs/codedocs/schedules-and-webhooks.md b/docs/codedocs/schedules-and-webhooks.md new file mode 100644 index 0000000..3c6aff3 --- /dev/null +++ b/docs/codedocs/schedules-and-webhooks.md @@ -0,0 +1,83 @@ +--- +title: "Schedules And Webhooks" +description: "Run prompts or commands on cron schedules and deliver results via webhooks." +--- + +Schedules let you run a Box command or agent prompt on a cron cadence without keeping a process alive. Webhooks let you fire-and-forget a run and receive the result asynchronously. Together, they enable background automation for maintenance tasks, reporting, and recurring analysis. + +**Why this concept exists** +AI-assisted workflows are often periodic. You may want to re-run tests nightly, regenerate documentation weekly, or run a prompt against new data as it arrives. Schedules and webhooks let you do that without manually maintaining a worker or cron job in your own infrastructure. + +**How it relates to other concepts** +- Schedules create **runs** inside a Box. +- Schedule and webhook results can be consumed alongside **files** and **git** operations. +- Schedules and webhooks are often paired with **snapshots** to manage state. + +```mermaid +flowchart TD + A[Create schedule] --> B[Box API stores cron] + B --> C[Run fires on schedule] + C --> D[Exec or Agent run] + D --> E[Webhook POST (optional)] + D --> F[Run record available] +``` + +**How it works internally** +Schedules are thin wrappers in `packages/sdk/src/client.ts`. `box.schedule.exec()` posts a payload with `type: "exec"`, a `cron` string, and a command array. `box.schedule.agent()` posts `type: "prompt"` and includes prompt, optional model override, and agent options. The SDK resolves `folder` using the same `cwd` logic used by file and exec operations. + +Webhook runs are triggered by `RunOptions.webhook` and handled in `_executeWebhookRun()`. Instead of streaming output, the SDK sends the prompt plus webhook config to the backend. The API returns immediately with a run ID, and later posts a `WebhookPayload` to your URL when the run completes. + +**Basic usage (scheduled exec + scheduled prompt)** +```ts filename="schedule-basic.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const execSchedule = await box.schedule.exec({ + cron: "* * * * *", + command: ["bash", "-c", "date >> /workspace/home/cron.log"], +}); + +const agentSchedule = await box.schedule.agent({ + cron: "0 9 * * *", + prompt: "Summarize yesterday's logs in /workspace/home/cron.log", +}); + +console.log(execSchedule.id, agentSchedule.id); +``` + +**Advanced / edge-case usage (webhook run)** +```ts filename="webhook-run.ts" +import { Box, Agent, ClaudeCode } from "@upstash/box"; + +const box = await Box.create({ + runtime: "node", + agent: { provider: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 }, +}); + +const run = await box.agent.run({ + prompt: "Create a daily release notes summary for the repo", + webhook: { url: "https://example.com/hooks/box" }, +}); + +console.log("Webhook run accepted:", run.id); +``` + + +Cron schedules run in UTC and do not inherit your local timezone. If your webhook endpoint is not reachable from the public internet, the run will complete but delivery will fail silently on your side. Always validate incoming webhook payloads and be explicit about timeouts and auth. + + + + +Schedules are ideal when the job is tightly coupled to a Box environment or when you want to avoid managing infrastructure. External cron gives you more control over retries, concurrency, and alerting. If your workload requires strict guarantees or complex scheduling, you may prefer an external scheduler that triggers Box runs on demand. If the workflow is simple and self-contained, Box schedules reduce operational overhead. + + +Webhooks provide low-latency delivery without holding open a client connection. This works well for serverless systems but requires you to host and secure a public endpoint. Polling run status avoids webhook complexity but increases latency and API calls. Choose webhooks when you can receive HTTP callbacks reliably; choose polling when your environment cannot expose an endpoint. + + +Scheduled runs default to the Box `cwd` at the time you create the schedule. If you later call `cd()`, the schedule does not automatically follow. Use the `folder` option in schedule calls to lock the run to a known path. This prevents surprises when your code changes the cwd after scheduling jobs. + + diff --git a/docs/codedocs/types.md b/docs/codedocs/types.md new file mode 100644 index 0000000..64bb2cb --- /dev/null +++ b/docs/codedocs/types.md @@ -0,0 +1,449 @@ +--- +title: "Types" +description: "All exported TypeScript types, interfaces, and enums from the Box SDK." +--- + +Below are the full exported type definitions from `packages/sdk/src/types.ts`, grouped by theme. These are the public contract for the SDK and are safe to import in application code. + +## Enums and basic unions +```ts +export type Runtime = "node" | "python" | "golang" | "ruby" | "rust"; +export type BoxSize = "small" | "medium" | "large"; + +export enum Agent { + ClaudeCode = "claude-code", + Codex = "codex", + OpenCode = "opencode", +} + +export enum ClaudeCode { + Opus_4_5 = "claude/opus_4_5", + Opus_4_6 = "claude/opus_4_6", + Sonnet_4 = "claude/sonnet_4", + Sonnet_4_5 = "claude/sonnet_4_5", + Sonnet_4_6 = "claude/sonnet_4_6", + Haiku_4_5 = "claude/haiku_4_5", +} + +export enum OpenAICodex { + GPT_5_3_Codex = "openai/gpt-5.3-codex", + GPT_5_2_Codex = "openai/gpt-5.2-codex", + GPT_5_1_Codex_Max = "openai/gpt-5.1-codex-max", + GPT_5_1_Codex_Mini = "openai/gpt-5.1-codex-mini", +} + +export enum OpenRouterModel { + Claude_Sonnet_4 = "openrouter/anthropic/claude-sonnet-4", + Claude_Opus_4_5 = "openrouter/anthropic/claude-opus-4-5", + Claude_Haiku_4_5 = "openrouter/anthropic/claude-haiku-4-5", + DeepSeek_R1 = "openrouter/deepseek/deepseek-r1", + Gemini_2_5_Pro = "openrouter/google/gemini-2.5-pro", + Gemini_2_5_Flash = "openrouter/google/gemini-2.5-flash", + GPT_4_1 = "openrouter/openai/gpt-4.1", + O3 = "openrouter/openai/o3", + O4_Mini = "openrouter/openai/o4-mini", +} + +export enum OpenCodeModel { + Claude_Opus_4_5 = "claude/opus_4_5", + Claude_Opus_4_6 = "claude/opus_4_6", + Claude_Sonnet_4 = "claude/sonnet_4", + Claude_Sonnet_4_5 = "claude/sonnet_4_5", + Claude_Sonnet_4_6 = "claude/sonnet_4_6", + Claude_Haiku_4_5 = "claude/haiku_4_5", + GPT_5_3_Codex = "openai/gpt-5.3-codex", + GPT_5_2_Codex = "openai/gpt-5.2-codex", + GPT_5_1_Codex_Max = "openai/gpt-5.1-codex-max", + GPT_5_1_Codex_Mini = "openai/gpt-5.1-codex-mini", + GPT_4_1 = "openai/gpt-4.1", + O3 = "openai/o3", + O4_Mini = "openai/o4-mini", + Zen_GPT_5_Nano = "opencode/gpt-5-nano", + Zen_MiniMax_M2_5_Free = "opencode/minimax-m2.5-free", + Zen_Big_Pickle = "opencode/big-pickle", + Zen_Claude_Sonnet_4_6 = "opencode/claude-sonnet-4-6", + Zen_Claude_Sonnet_4_5 = "opencode/claude-sonnet-4-5", + Zen_Claude_Sonnet_4 = "opencode/claude-sonnet-4", + Zen_Claude_Haiku_4_5 = "opencode/claude-haiku-4-5", + Zen_Claude_Opus_4_6 = "opencode/claude-opus-4-6", + Zen_Claude_Opus_4_5 = "opencode/claude-opus-4-5", + Zen_Claude_Opus_4_1 = "opencode/claude-opus-4-1", + Zen_Gemini_3_1_Pro = "opencode/gemini-3.1-pro", + Zen_Gemini_3_Pro = "opencode/gemini-3-pro", + Zen_Gemini_3_Flash = "opencode/gemini-3-flash", +} + +export enum BoxApiKey { + UpstashKey = "UPSTASH_KEY", + StoredKey = "STORED_KEY", +} +``` + +## Agent configuration +```ts +export type AgentConfig = { + apiKey?: BoxApiKey | string; +} & ( + | { provider: Agent.ClaudeCode; model: ClaudeCode | OpenRouterModel; runner?: never } + | { provider: Agent.Codex; model: OpenAICodex | OpenRouterModel; runner?: never } + | { + provider: Agent.OpenCode; + model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel; + runner?: never; + } + | { provider: string; model: string; runner?: never } + | { runner: Agent; model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel; provider?: never } + | { runner: string; model: string; provider?: never } +); + +export interface ClaudeCodeAgentOptions { + maxTurns?: number; + maxBudgetUsd?: number; + effort?: "low" | "medium" | "high" | "max"; + thinking?: + | { type: "adaptive" } + | { type: "enabled"; budgetTokens: number } + | { type: "disabled" }; + disallowedTools?: string[]; + agents?: Record; + promptSuggestions?: boolean; + fallbackModel?: string; + systemPrompt?: string | Record; +} + +export interface CodexAgentOptions { + modelReasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; + modelReasoningSummary?: "auto" | "concise" | "detailed" | "none"; + personality?: "friendly" | "pragmatic" | "none"; + webSearch?: "live" | boolean; +} + +export interface OpenCodeAgentOptions { + reasoningEffort?: "low" | "medium" | "high"; + textVerbosity?: "low" | "medium" | "high"; + reasoningSummary?: "auto" | "concise" | "detailed" | "none"; + thinking?: { type: "enabled"; budgetTokens: number }; +} + +export type AgentOptions = TProvider extends Agent.ClaudeCode + ? ClaudeCodeAgentOptions + : TProvider extends Agent.Codex + ? CodexAgentOptions + : TProvider extends Agent.OpenCode + ? OpenCodeAgentOptions + : Record; +``` + +## Network policy and MCP +```ts +export type NetworkPolicy = + | { mode: "allow-all" | "deny-all" } + | { + mode: "custom"; + allowedDomains?: string[]; + allowedCidrs?: string[]; + deniedCidrs?: string[]; + }; + +export type McpServerConfig = { + name: string; +} & ( + | { package: string; args?: string[]; url?: never; headers?: never } + | { url: string; headers?: Record; package?: never; args?: never } +); +``` + +## Box configuration +```ts +export interface BoxConnectionOptions { + apiKey?: string; + baseUrl?: string; +} + +export interface ListOptions extends BoxConnectionOptions {} + +export interface BoxGetOptions extends BoxConnectionOptions { + gitToken?: string; + timeout?: number; + debug?: boolean; +} + +export interface BoxConfig extends BoxConnectionOptions { + name?: string; + runtime?: Runtime; + size?: BoxSize; + agent?: AgentConfig; + git?: { token?: string; userName?: string; userEmail?: string }; + env?: Record; + attachHeaders?: Record>; + networkPolicy?: NetworkPolicy; + skills?: string[]; + mcpServers?: McpServerConfig[]; + timeout?: number; + debug?: boolean; +} + +export interface EphemeralBoxConfig extends BoxConnectionOptions { + name?: string; + runtime?: Runtime; + size?: BoxSize; + ttl?: number; + env?: Record; + attachHeaders?: Record>; + networkPolicy?: NetworkPolicy; + timeout?: number; + debug?: boolean; +} + +export interface EphemeralBoxData extends BoxData { + ephemeral: boolean; + expires_at: number; +} +``` + +## Runs and streaming +```ts +export interface WebhookConfig { + url: string; + headers?: Record; +} + +export type Chunk = + | { type: "start"; runId: string } + | { type: "text-delta"; text: string } + | { type: "reasoning"; text: string } + | { type: "tool-call"; toolName: string; input: Record } + | { + type: "finish"; + output: string; + usage: { inputTokens: number; outputTokens: number }; + sessionId: string; + } + | { type: "stats"; cpuNs: number; memoryPeakBytes: number } + | { type: "unknown"; event: string; data: unknown }; + +export type PromptFiles = string[] | { data: string; mediaType: string; filename?: string }[]; + +export interface StreamOptions { + prompt: string; + files?: PromptFiles; + options?: AgentOptions; + timeout?: number; + onToolUse?: (tool: { name: string; input: Record }) => void; +} + +export interface RunOptions { + prompt: string; + responseSchema?: ZodType; + files?: PromptFiles; + options?: AgentOptions; + timeout?: number; + maxRetries?: number; + onToolUse?: (tool: { name: string; input: Record }) => void; + webhook?: WebhookConfig; +} + +export type RunStatus = "running" | "completed" | "failed" | "cancelled" | "detached"; + +export interface RunCost { + inputTokens: number; + outputTokens: number; + computeMs: number; + totalUsd: number; +} + +export interface RunLog { + timestamp: string; + level: "info" | "warn" | "error"; + message: string; +} + +export interface WebhookPayload { + box_id: string; + status: "completed" | "failed"; + run_id?: string; + output?: string; + metadata?: Record; + error?: string; +} +``` + +## Files and git +```ts +export interface UploadFileEntry { + path: string; + destination: string; +} + +export interface FileEntry { + name: string; + path: string; + size: number; + is_dir: boolean; + mod_time: string; +} + +export interface GitCloneOptions { repo: string; branch?: string; } +export interface GitExecOptions { args: string[]; } +export interface GitExecResult { output: string; } +export interface GitCheckoutOptions { branch: string; } +export interface GitPROptions { title: string; body?: string; base?: string; } +export interface GitCommitOptions { message: string; authorName?: string; authorEmail?: string; } +export interface GitConfigUpdateOptions { userName?: string; userEmail?: string; } +export interface GitConfig { git_user_name: string; git_user_email: string; } +export interface GitCommitResult { sha: string; message: string; } +export interface PullRequest { url: string; number: number; title: string; base: string; } +``` + +## Code execution +```ts +export type CodeLanguage = "js" | "ts" | "python"; + +export interface CodeExecutionOptions { + code: string; + lang: CodeLanguage; + timeout?: number; +} + +export interface CodeExecutionResult { + output: string; + exit_code: number; + error?: string; +} + +export type ExecStreamChunk = + | { type: "output"; data: string } + | { type: "exit"; exitCode: number; cpuNs: number }; +``` + +## Scheduling +```ts +export type ScheduleStatus = "active" | "paused" | "deleted"; + +export interface ExecScheduleOptions { + cron: string; + command: string[]; + folder?: string; + webhookUrl?: string; + webhookHeaders?: Record; +} + +export interface AgentScheduleOptions { + cron: string; + prompt: string; + folder?: string; + model?: string; + options?: AgentOptions; + timeout?: number; + webhookUrl?: string; + webhookHeaders?: Record; +} + +export interface Schedule { + id: string; + box_id: string; + customer_id?: string; + type: "exec" | "prompt"; + cron: string; + command?: string[]; + prompt?: string; + folder?: string; + model?: string; + agent_options?: Record; + timeout?: number; + status: ScheduleStatus; + qstash_schedule_id?: string; + webhook_url?: string; + webhook_headers?: Record; + last_run_at?: number; + last_run_status?: "completed" | "failed" | "skipped"; + last_run_id?: string; + total_runs: number; + total_failures: number; + created_at: number; + updated_at: number; +} +``` + +## API response records +```ts +export type BoxStatus = "creating" | "idle" | "running" | "paused" | "error" | "deleted"; + +export type BoxData = { + id: string; + customer_id?: string; + name?: string; + size?: BoxSize; + model?: string; + agent?: Agent; + enabled_skills?: string[]; + runtime?: string; + status: BoxStatus; + network_policy?: { + mode: "allow-all" | "deny-all" | "custom"; + allowed_domains?: string[]; + allowed_cidrs?: string[]; + denied_cidrs?: string[]; + }; + clone_repo?: string; + total_input_tokens?: number; + total_output_tokens?: number; + total_prompts?: number; + session_id?: string; + agent_id?: string; + total_cpu_ns?: number; + total_compute_cost_usd?: number; + total_token_cost_usd?: number; + use_managed_key?: boolean; + last_activity_at?: number; + created_at: number; + updated_at: number; +}; + +export interface RunResult { output: string; metadata?: RunMetadata; } +export interface RunMetadata { input_tokens?: number; output_tokens?: number; } +export interface ExecResult { exit_code: number; output: string; error?: string; } +export interface LogEntry { timestamp: number; level: "info" | "warn" | "error"; source: "system" | "agent" | "user"; message: string; } +export interface ErrorResponse { error: string; } + +export type BoxRunData = { + id: string; + box_id: string; + customer_id: string; + type: "agent" | "shell"; + prompt?: string; + model?: string; + output?: string; + input_tokens: number; + output_tokens: number; + cost_usd: number; + duration_ms: number; + cpu_ns?: number; + compute_cost_usd?: number; + memory_peak_bytes?: number; + error_message?: string; + session_id?: string; + created_at: number; + completed_at?: number; +} & ( + | { schedule_id?: never; status: "running" | "completed" | "failed" | "cancelled" } + | { schedule_id: string; status: "completed" | "failed" | "skipped" } +); + +export interface Snapshot { + id: string; + name: string; + box_id: string; + size_bytes: number; + status: "creating" | "ready" | "error" | "deleted"; + created_at: number; +} + +export interface Preview { + url: string; + port: number; + token?: string; + username?: string; + password?: string; +} +```