# Agent Actors
An Agent Actor is the trusted caller identity for one invocation. It can drive access, rate limits, state partitioning, and inspection without turning a Channel or chat user object into an authorization decision.
The current public fields retain the name `invoker`: configure Actors with `defineAgent({ invoker })`, pass one through `context.invoker`, and read the normalized Actor as `actor` or `invoker` in callbacks.
## Pass a trusted Actor
Authenticate at the application boundary, then pass only validated identity facts.
```ts [server/api/support.post.ts]
import { runAgent } from 'vite-hub/agent'
import support from '../agents/support'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const user = await requireAuthenticatedUser(event)
const { prompt } = await readBody<{ prompt: string }>(event)
return runAgent(support, getRuntimeContext(event), {
prompt,
context: {
invoker: {
id: user.id,
kind: 'customer',
label: user.email,
meta: { customer: user.customerId },
},
},
})
})
```
ViteHub trusts this server-owned value. Never copy unverified request fields into `context.invoker`.
## Actor fields
| Field | Required | Purpose |
| ------- | -------- | ---------------------------------------------------------------------------------- |
| `id` | Yes | Stable identity for access, limits, state, and inspection. Empty ids are rejected. |
| `kind` | No | Identity family such as `customer`, `chat`, or `anonymous`. |
| `label` | No | Human-readable value for logs and CLI inspection. |
| `email` | No | Normalized `{ address, domain }`; invalid values are omitted. |
| `meta` | No | Application-owned trusted facts. Validate them before invocation. |
## Configure profiles
Profiles provide known Actors for local development, schedules, CLI use, and trusted routes.
```ts [server/agents/support.ts]
import { defineAgent, defineAgentInvoker } from 'vite-hub/agent'
export default defineAgent({
invoker: defineAgentInvoker({
profiles: [
{
id: 'portal-acme',
kind: 'customer',
label: 'Acme Portal',
meta: { customer: 'acme' },
},
{
id: 'support-admin',
kind: 'support',
label: 'Support Admin',
meta: { scope: 'all' },
},
],
}),
driver: { model: 'openai/gpt-5.1-mini' },
})
```
Select a profile with `context.invokerProfileId` for direct invocation or top-level `invokerProfileId` for `chat.message`. Unknown ids fail instead of falling back silently.
Use `invoker.resolve` to normalize or reject the trusted input before Capabilities run:
```ts [server/agents/support-actor.ts]
import { defineAgentInvoker } from 'vite-hub/agent'
export const supportActor = defineAgentInvoker({
resolve({ context, defaultInvoker, selectedProfile }) {
const customer = typeof defaultInvoker.meta?.customer === 'string'
? defaultInvoker.meta.customer.trim()
: undefined
context.set('support.customer', { customer }, { overwrite: true })
return selectedProfile ?? defaultInvoker
},
})
```
Resolution happens before Capabilities and the Driver run.
## Use Actors for access
Actor metadata can select a Workspace Scope, but authorization must remain deterministic.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { access } from 'vite-hub/agent/capabilities'
export default defineAgent({
capabilities: [
access({
workspace: {
resolve({ invoker }) {
return invoker.meta?.customer === 'acme' ? 'acme' : 'public'
},
scopes: {
public: { paths: ['public'] },
acme: { paths: ['customers/acme'] },
},
},
}),
],
driver: { model: 'openai/gpt-5.1-mini' },
workspace: 'product-docs',
})
```
Do not ask the model to decide its own Actor or access scope. Authenticate first, normalize once, and let Capabilities consume the trusted result.
## Current API names
| Task | API |
| ----------------------- | ----------------------------------------------------------- |
| Configure resolution | `defineAgent({ invoker })`, `defineAgentInvoker()` |
| Direct invocation input | `input.context.invoker` |
| `chat.message` input | top-level `invoker` |
| Read in callbacks | `actor` or `invoker` |
| Read from context store | `context.get('actor')` or `context.get('invoker')` |
| Public type | `AgentActor`; invoker-named APIs also expose `AgentInvoker` |
# Agent Definitions
An Agent Definition is the single configuration object for one Agent. It selects an [Agent Driver](https://vitehub.dev/docs/agents/agent-drivers), attaches Capabilities and Workspace context, and defines any Channels, Actor resolution, hooks, or hosted runtime behavior.
ViteHub discovers definitions in `server/agents`. Both `server/agents/support.ts` and `server/agents/support/agent.ts` create an Agent named `support`.
## Define an Agent
Start with the execution path. This Agent uses ViteHub's built-in Codex Driver:
```ts [server/agents/review.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
description: 'Reviews the current repository change.',
driver: 'codex',
})
```
Use a tagged built-in value when the Driver needs options:
```ts [server/agents/review.ts]
export default defineAgent({
driver: {
kind: 'codex',
model: 'gpt-5.5',
permissions: 'ask',
},
})
```
For application-supplied execution, use exactly one structural Driver variant: `{ model }` or `{ run }`.
## Add abilities and context
Capabilities decide which runtime abilities the selected Driver receives. Workspace context decides which files and Sources those abilities can reach.
```ts [server/agents/support/agent.ts]
import { defineAgent } from 'vite-hub/agent'
import { workspaceShell } from 'vite-hub/agent/capabilities'
import { glob } from 'vite-hub/workspace'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: 'Answer from the docs Workspace. Say when the answer is absent.',
},
capabilities: [workspaceShell({ mode: 'read' })],
workspace: {
sourceRootDir: process.cwd(),
sources: {
docs: glob({ cwd: '.', include: ['docs/content/**/*.md'] }),
},
},
})
```
Declaring a Workspace does not automatically grant model-backed or custom Drivers file access. Provider Drivers receive the selected Workspace as their working directory; Capabilities still control additional tools and invocation behavior.
## Return structured output
Set `driver.output` when downstream code needs validated data instead of free-form text.
```ts [server/agents/triage.ts]
import * as v from 'valibot'
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: 'Classify the request and explain the next action.',
output: {
schema: v.object({
priority: v.picklist(['low', 'normal', 'urgent']),
nextAction: v.string(),
}),
},
},
})
```
Inline `runAgent()` execution returns the validated structured result. A schema failure fails the invocation instead of returning unchecked model output. Workflow-backed calls return an `AgentWorkflowRun` after the Workflow starts. Poll `getWorkflowRun(workflowName, run.id)` until its status is `completed`, then read `result` for the validated Agent value. Treat `failed`, `cancelled`, and `unknown` as terminal states instead of waiting indefinitely.
When a model returns invalid native structured output, the Agent Driver makes one output-only correction call before failing validation. The correction keeps the invocation's prepared model and provider route, but it does not replay conversation messages or expose tools, so completed tool effects cannot run again. Tool results remain available as bounded evidence for the corrected output. Usage records include both model calls, with per-call attribution, aggregate token totals, and aggregate cost when provider metadata or configured pricing supplies it.
## Choose hosted execution
Discovered Agents use the active host's Workflow integration by default. Set `runtime: false` when a hosted Agent must complete inline, or select a named Workflow identity with `runtime: workflow('support')`.
With the implicit discovery-default Workflow binding, direct `runAgent()` calls without a discovered host identity remain inline. An explicit `runtime: workflow('support')` binding starts that named Workflow even for a direct call.
## Definition options
| Option | Purpose |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `driver` | Required. Selects one built-in provider, model-backed, or custom-run execution path. |
| `capabilities` | Attaches a static list or invocation-time Capability resolver. |
| `workspace` | Declares or reuses scoped files, Sources, bindings, and access policy. |
| `driver.instructions` | Configures instructions on the selected Driver; see [Instructions](https://vitehub.dev/docs/agents/instructions). |
| `driver.output` | Validates structured Agent output. |
| `channels` | Declares named Agent Channels and generated routes. |
| `messages` | Applies shared delivery, streaming, concurrency, session, and transcript settings to adapter Channels. |
| `invoker` | Configures Agent Actor profiles and resolution using the current API name. |
| `runtime` | Selects inline or Workflow-backed hosted execution. |
| `hooks` | Observes input, completion, failure, Capability lifecycle, or hook execution. |
| `runEvents` | Publishes application-owned progress for an invocation with a stable run id. |
| `name`, `description`, `version` | Adds explicit discovery and inspection metadata. |
| `cli.capabilities` | Enables or disables Capability-contributed CLI commands. |
Use the dedicated pages for option details rather than growing the Definition itself: [Drivers](https://vitehub.dev/docs/agents/agent-drivers), [Channels](https://vitehub.dev/docs/agents/channels), [Actors](https://vitehub.dev/docs/agents/actors), and [Invocations](https://vitehub.dev/docs/agents/invocations).
# Agent Drivers
An Agent Driver decides how one invocation runs. Choose the execution method that matches the work.
| Choose | Use it when |
| --------------- | ------------------------------------------------------------------------------- |
| Model-backed | ViteHub runs a model and its Capability-contributed tool loop. |
| Provider-backed | Codex or Claude Code runs the coding-agent loop, tools, approvals, and session. |
| Custom run | Application code runs the entire operation. |
Built-in `"codex"` and `"claude-code"` values are provider-backed. Application-supplied Drivers use exactly one of `{ model }` or `{ run }`.
## Use a model-backed Driver
Model-backed execution fits support answers, classification, extraction, structured output, and bounded tool use.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: 'Answer support requests from inspected evidence.',
execution: {
callSettings: { temperature: 0.2 },
stepLimit: 8,
},
},
})
```
Model strings run through AI Gateway. ViteHub discovers `AI_GATEWAY_API_KEY` from the process or Cloudflare Server Env. Supply an explicit descriptor when the Definition owns the credential:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
const apiKey = process.env.SUPPORT_AI_GATEWAY_API_KEY
if (!apiKey) throw new Error('SUPPORT_AI_GATEWAY_API_KEY is required')
export default defineAgent({
driver: {
model: { id: 'zai/glm-5v-turbo', apiKey },
},
})
```
The `model` value may also be a compatible AI SDK model or an invocation-time callback. Keep authorization in Access or Capability policy; use model callbacks and instrumentation for routing and call settings.
### Model options
| Option | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `model` | Required model id, `{ id, apiKey }`, AI SDK model, or callback. |
| `instructions` | String, string array, or callback parts. Defaults to colocated instructions when available. |
| `maxRetries` | Common model retry count. Do not also set `execution.callSettings.maxRetries`. |
| `execution.callSettings` | Provider and AI SDK call settings. |
| `execution.stepLimit` | Maximum model tool-loop steps; defaults to `20`. |
| `execution.instrumentation` | Invocation-scoped model wrapping or call-setting overrides. |
| `execution.workspaceFallback` | Controls synthesis from Workspace evidence when a run produced tool results but no text. |
## Use a provider-backed Driver
The built-in Drivers reuse T3 Code's normalized Codex and Claude Code runtime while ViteHub owns Agent Definitions, Capabilities, Workspaces, Invocations, and public lifecycle events.
```ts [server/agents/review/agent.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
kind: 'codex',
instructions: 'Review the exact pull request head before changing code.',
model: 'gpt-5.5',
permissions: 'ask',
},
workspace: { mode: 'write' },
})
```
Provider Drivers require a local Node.js host with the matching CLI and credentials available to the process. Provider Workspaces also require a POSIX host. Each invocation receives a temporary working directory, optional Workspace files, `AGENTS.md` or `CLAUDE.md`, and Capability tools through a private loopback MCP server. Successful write-mode runs commit through Workspace rules; failed and cancelled runs do not write back.
Provider runtime cursors resume a thread while the Agent Definition process remains active. Chat-backed cursors are also partitioned by origin, invoker, and resolved Chat Session, so a new session cannot inherit provider context from an earlier one. Cursors are process-local and do not survive restarts or resume on another worker; use the Agent Invocation message history as the durable conversation boundary.
Threads resume with the provider's opaque cursor. ViteHub normalizes assistant text, reasoning, native and Capability tool activity, approvals, provider questions, usage, warnings, errors, and terminal state into Agent Invocation events.
| Option | Purpose |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | Required tagged provider name: `"codex"` or `"claude-code"`. |
| `model` | Optional provider model id. |
| `env` | Explicit environment values passed to the local provider process. ViteHub otherwise inherits only standard host paths, locale, and user-directory variables, not arbitrary application secrets. |
| `execution.attachments.maxBytes` | Optional positive per-invocation image attachment budget; defaults to 25 MiB. Inline and application-resolved lazy images share the budget. |
| `instructions` | Invocation-scoped instructions composed with colocated instructions. |
| `permissions` | `"ask"`, `"allow-edits"`, or `"allow-all"`; defaults to `"allow-all"`. |
| `output` | Optional structured Agent output contract. |
| `capacity` | Optional process-local concurrency and queue limits. |
Provider Drivers do not accept Agent Boxes, model-specific Provider Tool contributions, Cloudflare Agents, or Deno. Provider Workspaces are also unsupported on Windows. These boundaries fail explicitly. Workspace-scoped Skills and ordinary Capability tools are supported.
## Use a custom run Driver
Use `driver.run` when application code owns the result and no model loop is needed.
```ts [server/agents/router.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
run({ input, invoker }) {
return { text: `Accepted ${invoker.id}: ${String(input.prompt ?? '')}` }
},
},
})
```
The callback receives prepared input, messages, tools, Workspace access, invocation context, and the resolved Actor as both `actor` and `invoker`. A custom run callback may call a model internally, but ViteHub treats that execution and usage as application-owned behavior.
Read [Instructions](https://vitehub.dev/docs/agents/instructions) for model-facing behavior and [Workspace context](https://vitehub.dev/docs/agents/workspace-context) for files and writeback.
# Boxes
Boxes prepare isolated or trusted process environments with explicit Home, checkout, environment, requirements, and state. Use `@vite-hub/box` directly when application code owns that process lifecycle.
Boxes do not attach to Agent Definitions. The built-in Codex and Claude Code Agent Drivers run through the local provider runtime with a temporary Workspace, so `defineAgent({ box })` is intentionally unsupported.
Use [`sandbox()`](https://vitehub.dev/docs/capabilities/sandbox) to give a model-backed Agent an allowlisted executable tool, or a custom `driver.run` when application code must compose Box execution with an Agent Invocation.
# Channels
A Channel describes where an Agent Invocation came from and how replies return there. It carries transport, event, thread, message, and delivery facts. It does not prove who the caller is.
Use [Agent Actors](https://vitehub.dev/docs/agents/actors) for trusted identity and [Input Commands](https://vitehub.dev/docs/capabilities/input-commands) for explicit command handling.
## Add a Channel
Import Channel helpers from `@vite-hub/agent/channels`.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { github, webChat } from 'vite-hub/agent/channels'
export default defineAgent({
channels: {
portal: webChat(),
github: github({ pullRequest: true }),
},
driver: { model: 'openai/gpt-5.1-mini' },
})
```
Built-in helpers include `discord()`, `github()`, `http()`, `slack()`, `teams()`, `telegram()`, and `webChat()`. Use `defineChannel()` for an application-owned Channel Kind.
`webChat()` enables a generated AI SDK chat route by default. `http()` is a generic HTTP Channel and keeps its route disabled unless you pass `http({ route: true })`.
## Connect a web chat
`webChat()` exposes the Agent through `/api/_vitehub/agents/[agent]/chat`. Set `route: false` to keep that Agent unreachable through the shared dispatcher.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({ preset: 'node', agent: true }),
],
})
```
Use the Vue client from the application:
```vue [app/components/SupportChat.vue]
```
Add `route.admission.authenticate` when the generated route needs authentication. ViteHub reads the raw body once, verifies the shared UI-message contract, and copies only fields named in `route.input.trust` after authentication.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { webChat } from 'vite-hub/agent/channels'
export default defineAgent({
channels: {
portal: webChat({
route: {
admission: {
authenticate({ rawBody, request }) {
verifyPortalSignature(rawBody, request.headers.get('x-portal-signature'))
return { customer: request.headers.get('x-customer') }
},
},
input: { trust: ['meta', 'user', 'session'] },
},
}),
},
driver: { run: () => 'ok' },
})
```
Use an application-owned route and [`streamAgentTrigger()`](https://vitehub.dev/docs/agents/triggers#consume-a-capability-trigger) when the shared dispatcher is not the right authentication or request boundary.
### Resume a web chat in one process
Set `resume: true` in `useChat()` and opt the generated route into process-scoped replay when a browser should follow an active response after reconnecting.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { webChat } from 'vite-hub/agent/channels'
export default defineAgent({
channels: {
portal: webChat({
route: {
admission: {
authenticate: ({ request }) => requireSession(request),
},
resumable: {
owner: ({ auth }) => auth.user.id,
scope: 'process',
ttlMs: 10 * 60 * 1000,
},
},
}),
},
driver: { model: 'openai/gpt-5.1-mini' },
})
```
The route de-duplicates one owner's repeated submission, replays buffered UI-message stream bytes, follows the live response, and retains a completed response for `ttlMs`. `scope: 'process'` is literal: active streams do not survive process replacement and cannot be discovered by another instance. Use this only where deployment keeps a chat on one process, or put durable execution, stream storage, and coordination behind an application-owned route.
## Connect an adapter platform
Adapter-backed Channels deliver the completed response by default. Set Agent-level `messages.stream: true` to publish draft and edit updates everywhere, or set `messages.stream` on one Channel.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { discord } from 'vite-hub/agent/channels'
export default defineAgent({
channels: {
discord: discord({
adapter: {
botToken: process.env.DISCORD_BOT_TOKEN,
publicKey: process.env.DISCORD_PUBLIC_KEY,
},
messages: { lockScope: 'thread' },
}),
},
driver: { run: () => 'Hello from ViteHub.' },
})
```
Install the matching `@chat-adapter/*` package when a built-in Channel uses provider adapter options. Keep provider credentials in Server Env.
For Telegram, ViteHub can own the verified webhook route and synchronize it after deployment:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
channels: {
telegram: { allowedUserIds: ['123'] },
},
driver: { run: () => 'Hello from ViteHub.' },
})
```
```bash [Terminal]
pnpm vitehub channels sync \
--stage staging \
--url https://staging.example.com \
--agent support \
--channel telegram \
--json
```
The command is a dry run by default. Apply a reviewed plan with `--apply` and the exact `--confirm-origin`; see [CLI channel synchronization](https://vitehub.dev/docs/development/cli#synchronize-channel-webhooks) for deletion and secret safeguards.
## Control admission and delivery
Set `messages.filter` to ignore unsupported adapter messages before an invocation starts:
```ts
teams({
adapter,
messages: {
filter: ({ deliveryKind }) =>
deliveryKind === 'direct' || deliveryKind === 'mention',
},
})
```
`deliveryKind` is `direct`, `mention`, or `subscribed`. Returning `false` posts no fallback error because the Agent never started.
Set `messages.commentary: 'message'` only when the Driver emits explicit commentary phases for public progress. Commentary is hidden by default; ViteHub never publishes reasoning as progress.
Use `messages.delivery: 'manual'` when finish hooks own replies. A generated Workflow may carry manual delivery across a durable boundary when the Channel and host support it. An explicit `messages.timeout` bounds inline execution and the durable handoff's typing indicator, but it does not cap the durable Agent Workflow. `steer` queues overlapping messages and preserves that Workflow handoff. Other overlap policies such as `serial`, `drop`, `queue`, and `reject` remain inline and cannot be combined with required durable delivery.
## Inspect delivery custody
Every built-in and custom Agent Channel records a delivery timeline before the Agent starts. The record keeps the provider event id separate from ViteHub's delivery id, then appends admission, invocation, retry, outbound, completion, and failure events. Discord Gateway and Telegram polling listeners also emit structured lifecycle events, so a listener gap can be distinguished from an event that reached ViteHub.
The evidence boundary stays explicit: no ViteHub record can prove a provider event existed when it never reached the process. Provider audit logs and Gateway session history remain the source for that side of an incident.
The journal uses the Channel's existing State Adapter and retains the timelines referenced by the 10,000 most recent admissions. Inspection de-duplicates concurrent or retried admissions of the same timeline. Each delivery and its newest 256 events expire 30 days after their last update. Records contain identifiers, timestamps, attempts, provider reply ids, and bounded error messages; ViteHub does not copy message text, attachment data, webhook bodies, or connector options into the journal. Production durability therefore follows the configured Agent state provider, while the default in-memory development state remains process-local.
Invocation hooks and Drivers receive the active record as `context.channelDelivery`. Trace Events repeat `channel.delivery.id`, `channel.delivery.provider`, and `channel.delivery.source.id`, while JSON logs use the `vitehub.channel.delivery` and `vitehub.channel.listener` scopes. The webhook route handler exposes `handler.deliveries(request, webhookId, options)` so host integrations inspect records through the same scoped State Adapter used by the Channel.
## Scope abilities to one Channel
Channel Capabilities apply only when that Channel is active. Agent-level Capabilities remain available to every invocation.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { openapi } from 'vite-hub/agent/capabilities'
import { teams, webChat } from 'vite-hub/agent/channels'
const portalApi = openapi({
cli: { name: 'portal-api' },
operations: ['purchaseOrders'],
spec: 'https://portal.example.com/_openapi.json',
})
export default defineAgent({
channels: {
portal: webChat({ capabilities: [portalApi] }),
teams: teams(),
},
driver: { run: () => 'ok' },
})
```
Channel-scoped Capabilities select abilities, not identity. Authenticate and resolve the Actor at the route, trigger, or `access()` boundary.
## Handle attachments
Adapter Channels preserve incoming images, audio, and files as typed message parts. Normalization can fetch a URL-only text attachment on the server to produce text bytes. It does not call lazy provider callbacks, write local files, or persist blobs.
Model-backed Drivers can consume inline data, adapter-owned `fetchData`, and HTTPS references within one invocation-wide byte budget. The default is 25 MiB; set `driver.execution.attachments.maxBytes` to lower it. Image, audio, and file HTTPS references are forwarded, but URL-only text attachments use the runtime's server-side `fetch()` without built-in scheme or host restrictions. Treat adapter-supplied text URLs as an SSRF boundary: reject untrusted URLs or validate them against an application-owned allowlist or fetch proxy before they reach normalization.
Channel history export archives inline data, size-declared adapter-owned `fetchData`, and Blob data within a 25 MiB total attachment budget and a 35 MiB total response budget. Lazy adapter reads need a trustworthy non-negative `size` within the remaining budget. URL-only attachments remain unavailable references because the Agent server cannot infer an application-owned host trust policy safely. Persist their bytes through the adapter when they must be recoverable. The export stops waiting for each provider history read or adapter-owned read after 30 seconds or when its request is aborted. The Chat SDK history and `fetchData` contracts have no cancellation channel, so their underlying private I/O remains adapter-owned and may settle after the export stops waiting. Attachments that exceed the remaining budget, contain malformed retained data, fail rehydration, omit the size required for a lazy read, or otherwise cannot be read remain in `history.json` as unavailable references. The export fails instead of building an archive above its total response limit.
Provider-backed Drivers materialize inline data and application-owned `fetchData` results. URL-only attachments require the application to validate and resolve the URL through `fetchData` before crossing the provider boundary; the Driver does not fetch arbitrary URLs from the ViteHub host.
## Separate responsibilities
| Concern | Owner |
| ----------------------------------------------------------- | ------------------------- |
| Origin, event, thread, message, custody, and reply delivery | Channel |
| Trusted caller identity | Agent Actor |
| User-authored command parsing | Input Commands Capability |
| Prior conversational messages | Chat History and sessions |
| Product event to Agent input | Trigger |
# Chat History and sessions
Chat History is the ordered set of prior messages eligible for one chat invocation. A Chat Session selects the host-visible conversation boundary. Neither is durable Agent Memory.
| Need | Use |
| --------------------------------------------------------- | ----------------------------------------------------------------- |
| Continue the visible thread | Thread-backed Chat History |
| Continue a conversation across changing transport threads | A Chat Session |
| Preserve knowledge or preferences across conversations | [Memory Capability](https://vitehub.dev/docs/capabilities/memory) |
## Enable thread history
Configure history on the Chat Capability:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { chat } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: 'Answer support chat messages.',
},
capabilities: [
chat({
concurrency: 'queue',
lockScope: 'thread',
triggerHistory: {
maxMessages: 20,
source: 'thread',
},
}),
],
})
```
The window limits messages supplied to the next invocation; it does not delete preserved history. For application-owned routes that call `runAgentTrigger()` or `streamAgentTrigger()`, supply the ordered messages for the current thread, including the new message. `triggerHistory` bounds that caller-supplied array; it does not load history from `threadId` or a session id. Adapter-backed Channels can perform their own history backfill. Thread scope is the normal choice for Discord threads, Slack threads, Teams conversations, GitHub comment threads, and application-owned support chats.
## Add a session
Use a session when the product has a stable conversation id that is independent of the current provider thread.
```ts [server/agents/support.ts]
import { chat } from 'vite-hub/agent/capabilities'
export const supportChat = chat({
sessions: {
idleTimeoutMs: 30 * 60 * 1000,
metadataKey: 'sessionId',
strategy: 'hybrid',
},
triggerHistory: {
maxMessages: 20,
source: 'thread',
},
})
```
Use `strategy: 'manual'` when a trusted host passes explicit session IDs, `idle-timeout` when inactivity starts a conversation, or `hybrid` for both. The `chat.message` input selects a manual session with `session: { action: 'switch', id }`.
The authenticated route or Channel supplies that ID. Do not accept an arbitrary session ID from an untrusted request, because that can expose another conversation's history.
## Partition transcripts
Keep transcript keys aligned with the product boundary. Use thread keys when each platform thread is independent; include Channel or tenant identity when ids can collide across providers.
History selection and persistence are separate decisions. The Chat Capability can select a bounded window, but the configured store owns durability, ordering, retention, and deletion. Cloudflare output defaults Agent State to its generated Durable Object binding, including Channel handlers invoked from generated Workflows. An explicit state provider still wins; a state configuration with only `url` keeps automatic libSQL selection instead of being replaced by the Cloudflare default. Generated non-Cloudflare production output requires a durable `VITEHUB_AGENT_STATE_URL` or explicit Agent State provider URL before stateful traffic. Cloudflare, Vercel, and Netlify production output reject `file:` URLs because their compute filesystems are ephemeral.
## Inspect the result
Run two messages through the same thread or session, then inspect the second invocation in the [CLI](https://vitehub.dev/docs/development/cli). The prepared input contains the bounded prior messages plus the current message. A different thread or session starts without that history.
# Child invocations
Use `startAgentInvocation()` when trusted host or parent code must control child Agent work after starting it. Use the [Subagents Capability](https://vitehub.dev/docs/capabilities/subagents) when the active model chooses and awaits delegated work itself.
## Start and inspect a child
```ts
import { startAgentInvocation } from 'vite-hub/agent'
import researcher from '../agents/researcher'
const child = await startAgentInvocation(researcher, runtimeContext, {
prompt: 'Compare the two deployment options.',
})
const current = await child.inspect()
if (current.outcome === 'available') {
console.log(current.invocation.id, current.invocation.status)
}
```
Every start gets a fresh stable id. `inspect()` returns an available snapshot or an explicit unavailable outcome. Available lifecycle states are `pending`, `running`, `completed`, `failed`, and `cancelled`.
Inline and serverless runtimes may become unavailable after their process ends. Workflow-backed children delegate inspection to their Workflow Run while the returned controller remains available. ViteHub does not add a separate invocation registry or public lookup by id.
## Cancel active work
```ts
const cancellation = await child.cancel('The parent no longer needs this work.')
if (cancellation.outcome === 'accepted') {
const latest = await child.inspect()
}
```
`accepted` means the runtime accepted the request; inspect again for the observed terminal state. A provider may return `unsupported`, and terminal invocations return `invalid-state`.
## Respond to provider requests
Check the controller's current support before sending input, then handle the operation result because support can change with lifecycle state.
```ts
if (child.support.respond) {
const result = await child.sendInput(
{ messages: [responseMessage] },
{ mode: 'respond' },
)
}
```
Inline provider runtimes accept approval decisions and `data-agent-input` answers while the matching provider request is pending. Text steering, follow-up turns, and Workflow-backed input remain unsupported until their runtime adapters provide equivalent ordering and lifecycle semantics.
The `subagents()` Capability uses the same start seam but returns a serializable tool result and waits for the child. The model cannot choose or reuse the trusted child id.
# Evals
Agent Evals run the real Agent Definition against repeatable inputs. They preserve its Driver, Capabilities, and Workspace while running inline, so a passing eval covers more than a standalone model prompt test. Verify Workflow scheduling, durability, and provider lifecycle separately on the configured host.
## Add one behavior check
Install the explicit runner dependencies:
```bash [Terminal]
pnpm add -D @vite-hub/agent evalite vitest
```
Create the eval beside the Agent it protects:
```ts [server/agents/support.eval.ts]
import { defineEval } from '@vite-hub/agent/eval'
import support from './support'
export default defineEval({
agent: support,
async test(t) {
await t.send('How do I configure billing retries?')
t.completed()
t.textContains('billing')
},
})
```
Run it from the workspace:
```bash [Terminal]
pnpm vitehub agent eval server/agents/support.eval.ts
```
A completed invocation containing `billing` passes and exits successfully. A failed invocation or missing text assertion fails the eval and exits non-zero.
Sibling `support.eval.ts` files can infer `support.ts`; a folder-level `eval.ts` can infer `agent.ts`. Keep the explicit `agent` import when it makes the relationship easier to see.
## Test several scenarios
Use declarative scenarios when independent inputs share scorers.
```ts [server/agents/support.eval.ts]
import {
callsTool,
defineEval,
doesNotCallTool,
textContains,
} from '@vite-hub/agent/eval'
import support from './support'
export default defineEval({
agent: support,
scenarios: [
{
name: 'inspects workspace before answering',
input: { prompt: 'Where is the billing retry policy documented?' },
scorers: [
callsTool('shell'),
doesNotCallTool('refund'),
textContains('billing'),
],
},
],
})
```
Scenarios accept normal Agent Invocation input, including `prompt`, `messages`, `context`, call options, timeout, and abort signal. Split unrelated behavior into separate scenarios so a failure identifies the boundary that changed.
Use imperative `test(t)` for a conversation. Repeated `t.send()` calls preserve that test's Chat History, and helpers inspect the latest observation:
| Helper | Check |
| -------------------------------------------- | ---------------------------------------------------------------- |
| `completed()` | The latest invocation completed. |
| `textContains(value)` | Response text contains a string or matches a regular expression. |
| `calledTool(name)` / `doesNotCallTool(name)` | Normalized tool steps include or exclude a tool. |
| `hasCapabilityExtension(id, key?)` | A Capability finish extension exists. |
| `expect(scorer)` | A custom scorer passes. |
| `observation` / `reply` | Access the latest normalized observation or response text. |
## Compare model variants
Variants run the same cases with model or instruction changes:
```ts [server/agents/support.eval.ts]
export default defineEval({
agent: support,
scenarios,
variants: [
{ name: 'baseline' },
{
name: 'strict',
instructions: 'Answer only from inspected evidence.',
},
],
})
```
Instruction-only variants require a model-backed Driver. A `model` variant may replace a model-backed or provider-backed Driver for the eval run. Use a separate Agent Definition when the change affects Capabilities, Workspace context, custom `driver.run` behavior, or host configuration.
## Configure the runner
Executable `*.eval.ts`, `*.eval.mts`, `*.eval.tsx`, and folder `eval.*` files enable the generated Evalite configuration. Configure defaults through `hubAgent({ eval })`:
```ts [vite.config.ts]
import { hubAgent } from '@vite-hub/agent/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubAgent({
eval: {
cache: true,
maxConcurrency: 2,
scoreThreshold: 85,
testTimeout: 60_000,
},
}),
],
})
```
Useful one-run flags are `--watch`, `--threshold `, `--output `, `--hide-table`, and `--no-cache`. CLI flags override integration defaults.
## Score product behavior
Prefer assertions about the behavior that matters: grounded answers, expected tool use, refusal when evidence is missing, Capability finish effects, and regressions in usage or latency. Read normalized usage from `observation.usage` and the finalized trace from `observation.trace`.
Keep provider credentials, model selection, permissions, and runtime selection on the Agent Definition. An eval owns scenarios and scores; duplicating runtime setup produces a different system than the application runs.
# Agents
An Agent is a named server-side program. Its definition records how it runs,
which files and tools it can use, and how callers reach it.
Start with an offline Agent if you have not built one yet. The tutorial creates
the definition, calls it from an H3 route, and shows the response without a model
key or hosted service.
::u-page-grid{.not-prose.mt-8.sm:grid-cols-2}
:::u-page-card
---
description: Define and call an Agent with a complete local example.
icon: i-lucide-rocket
title: Build your first Agent
to: https://vitehub.dev/docs/getting-started/first-agent
---
:::
:::u-page-card
---
description: Choose a Driver, Capabilities, Workspace, and Channels.
icon: i-lucide-file-user
title: Define an Agent
to: https://vitehub.dev/docs/agents/agent-definitions
---
:::
:::u-page-card
---
description: Run a model, Codex, Claude Code, or application code.
icon: i-lucide-cpu
title: Choose a Driver
to: https://vitehub.dev/docs/agents/agent-drivers
---
:::
:::u-page-card
---
description: Give the active Driver selected tools and runtime behavior.
icon: i-lucide-blocks
title: Add Capabilities
to: https://vitehub.dev/docs/capabilities
---
:::
::
## How an Agent fits together
| Part | What you choose |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
| [Agent Definition](https://vitehub.dev/docs/agents/agent-definitions) | One Agent's Driver, Capabilities, Workspace, Channels, and hooks. |
| [Agent Driver](https://vitehub.dev/docs/agents/agent-drivers) | A model, a coding provider, or your own `run` function. |
| [Agent Invocation](https://vitehub.dev/docs/agents/invocations) | The input for one run and whether the result returns or streams. |
| [Capabilities](https://vitehub.dev/docs/capabilities) | The tools and behavior available during an invocation. |
| [Workspace context](https://vitehub.dev/docs/agents/workspace-context) | The files, Sources, and bindings available to the Agent. |
| [Instructions](https://vitehub.dev/docs/agents/instructions) | Durable guidance for a model or coding provider. |
Capabilities grant access deliberately. Adding KV, Blob, a Workspace, or another
server feature to the application does not give a model access to it. Attach the
matching Capability only when the Agent needs that action.
## Connect an Agent
Call an Agent directly from trusted server code, or connect it to a product entry
point:
- [Channels](https://vitehub.dev/docs/agents/channels) connect web chat, Discord, Telegram, GitHub,
and other message transports.
- [Triggers](https://vitehub.dev/docs/agents/triggers) turn application events into Agent input.
- [Agent Actors](https://vitehub.dev/docs/agents/actors) carry trusted caller identity.
- [Chat History and sessions](https://vitehub.dev/docs/agents/chat-history-sessions) select the
earlier messages supplied to a chat invocation.
## Verify behavior
Use the [CLI development loop](https://vitehub.dev/docs/development/cli) to inspect and run the
Agent locally. Add an [Eval](https://vitehub.dev/docs/agents/evals) for behavior that must keep
working. Deployment-specific behavior still needs a build and runtime check on
the selected host.
## Advanced execution
- [Controlled child invocations](https://vitehub.dev/docs/agents/controlled-child-invocations)
start, inspect, cancel, or respond to child work from trusted code.
- [Boxes](https://vitehub.dev/docs/agents/boxes) prepare a process environment when application
code owns that lifecycle.
# Instructions
Instructions tell a model or coding provider how to behave. Keep tool schemas with Capabilities; use instructions for durable behavior, source policy, trust boundaries, escalation, and uncertainty handling.
## Start with a colocated document
Put longer guidance beside the Agent as `instructions.md`.
```md [server/agents/support/instructions.md]
# Support
Answer from inspected Workspace evidence before using outside knowledge.
When the docs do not answer the question, say that directly.
```
```ts [server/agents/support/agent.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
},
workspace: {
sources: {},
},
})
```
ViteHub parses instruction Markdown through Comark. A colocated document becomes the default when `driver.instructions` is absent. Provider Drivers receive the rendered document as `AGENTS.md` for Codex or `CLAUDE.md` for Claude Code.
Use `driver.instructions` for short or invocation-specific text:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: [
'You are a support engineer.',
'Answer from inspected evidence. State when evidence is missing.',
],
},
})
```
## Split reusable guidance
Use static `@./path.md` imports when one document becomes difficult to scan.
```md [server/agents/support/instructions.md]
# Support
@./shared-style.md
@./escalation-policy.md
```
Imports are relative, recursive up to four levels, and processed like the parent document. Remote URLs, absolute paths, and globs fail instead of widening instruction reachability.
## Insert trusted invocation values
Read explicit `context.*` values with double braces for scalars and triple braces for trusted Markdown.
```md [server/agents/support/instructions.md]
Answer for {{ context.customerName }}.
{{{ context.supportPolicy }}}
```
The caller or a Capability must set these values before composition. Missing bindings fail instead of rendering empty text; templates cannot read arbitrary request fields, environment variables, or JavaScript expressions.
Use conditions for small policy branches:
```md [server/agents/support/instructions.md]
::if{condition="context.audience === 'technical'"}
Include implementation details and cite file paths.
::else
Prefer customer-facing language and next actions.
::
```
Conditions support `context.*` paths, scalar literals, equality, `&&`, `||`, `!`, and parentheses.
## Insert Workspace bindings
Declare values or Markdown files under `workspace.bindings`, then reference only those named bindings.
```ts [server/agents/support/agent.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: [
'Use {{ workspace.tone }} tone.',
'@workspace.policy',
],
},
workspace: {
bindings: {
tone: 'short',
policy: { path: 'policies/support.md' },
},
},
})
```
`@workspace.policy` inserts the declared Markdown and composes it again. ViteHub does not scan or auto-load every Markdown file in the Workspace.
## Cover configured primitives
Name how to use each configured Source, Capability, or Skill. ViteHub records this coverage for inspection and warns when a configured primitive has no explicit policy.
```md [server/agents/support/instructions.md]
::source{key="docs"}
Use the docs Source for published product behavior. Say when it does not answer.
::
::capability{key="workspaceShell"}
Inspect the Workspace before answering implementation questions.
::
::skill{path="skills/review-browser-evidence"}
Use this Skill only when the task needs browser evidence.
::
```
ViteHub strips the wrapper directives before model execution and keeps their prose. A file that merely exists in the Workspace does not count as instruction coverage.
## Use the right instruction lifetime
| Instruction source | Use it for |
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
| Colocated `instructions.md` | Durable guidance shared by model and provider-backed execution. |
| Model `driver.instructions` | Model-facing behavior, including invocation-time callbacks and bindings. |
| Provider `driver.instructions` | Invocation-scoped policy written into the provider working directory. |
| Custom `driver.run` | Application code reads prepared context directly; ViteHub does not build a model prompt for it. |
Read [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) for execution-specific behavior and [Workspace context](https://vitehub.dev/docs/agents/workspace-context) for file visibility.
# Invocations
An Agent Invocation is one request to an Agent. ViteHub prepares its input, Actor, Capabilities, Workspace, and Driver, then returns or streams the result.
## Run an Agent
Use `runAgent()` when the caller needs the final result.
```ts [server/api/support.post.ts]
import { runAgent } from 'vite-hub/agent'
import support from '../agents/support'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const { prompt } = await readBody<{ prompt: string }>(event)
const user = await requireAuthenticatedUser(event)
return runAgent(support, getRuntimeContext(event), {
prompt,
context: {
invoker: {
id: user.id,
kind: 'customer',
label: user.email,
},
},
})
})
```
Authenticate the request before passing trusted identity or access facts. `context.invoker` is the current input field for an [Agent Actor](https://vitehub.dev/docs/agents/actors).
The second argument is [Runtime Context](https://vitehub.dev/docs/concepts/runtime-context); the third is invocation input. The application-owned `getRuntimeContext()` helper supplies the host's required `runtime`, `memo`, and `waitUntil` values. Keeping them separate prevents host resources from becoming user-controlled task data.
## Stream an Agent
Use `streamAgent()` when a chat UI or internal consumer needs incremental output.
```ts [server/api/support-stream.post.ts]
import { streamAgent } from 'vite-hub/agent'
import support from '../agents/support'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const { prompt } = await readBody<{ prompt: string }>(event)
return streamAgent(
support,
getRuntimeContext(event),
{ prompt },
{ output: 'ui-message-stream' },
)
})
```
Use `output: 'ui-message-stream'` for an AI SDK-compatible chat response. Use `output: 'events'` when server code needs ViteHub stream events.
The stream becomes terminal when the caller consumes it, cancels it, or receives an error. A caller that abandons the stream also abandons completion observation.
## Invoke a Trigger
Use `runAgentTrigger()` or `streamAgentTrigger()` when a Capability owns the event shape. This example invokes the Chat Capability's `chat.message` trigger:
```ts [server/api/support-chat.post.ts]
import { streamAgentTrigger } from 'vite-hub/agent'
import support from '../agents/support'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const { text } = await readBody<{ text: string }>(event)
const runId = crypto.randomUUID()
return streamAgentTrigger(
support,
getRuntimeContext(event),
'chat.message',
{
messages: [{
id: runId,
role: 'user',
parts: [{ type: 'text', text }],
}],
run: {
channelId: 'support-web',
messageId: runId,
origin: 'portal',
runId,
},
},
{ output: 'ui-message-stream' },
)
})
```
The consumer supplies the product event. The Capability prepares the Agent input and policy before the Driver starts. Read [Triggers](https://vitehub.dev/docs/agents/triggers) for when to use this path instead of direct invocation.
## Validate input
Use an `agent:input` hook for trusted invocation requirements that must be present before the Driver runs.
```ts [server/agents/review.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: { run: () => 'ok' },
hooks: {
'agent:input'({ input }) {
if (!input.context?.pullRequest) {
throw new Error('Missing context.pullRequest')
}
},
},
})
```
Validate untrusted request data at the route boundary. The hook protects the Agent contract when multiple trusted callers invoke the same Definition.
## Observe the outcome
Finish hooks receive normalized duration, result kind, and usage. Error hooks receive failed invocations.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: { model: 'openai/gpt-5.1-mini' },
hooks: {
'agent:finish'(event) {
const { durationMs, resultKind, usage } = event.invocation
event.runtime.waitUntil(recordInvocation({ durationMs, resultKind, usage }))
},
'agent:error'(event) {
event.runtime.waitUntil(recordFailure(event.publicError))
},
},
})
```
Error hooks receive the raw `event.error` for protected server diagnostics and a
sanitized `event.publicError` for logs, HTTP responses, or Channel replies. See
[Agent public errors](https://vitehub.dev/docs/reference/errors-diagnostics#agent-public-errors) for
the stable codes and redaction rules.
Every invocation also has an in-memory metadata trace through `runtime.trace` and `runtime.traceLog`. The default log is process-local and is not persisted across a Workflow boundary.
Attach the `otlp()` Capability to send completed invocation traces to any OTLP/HTTP JSON receiver:
```ts [server/agents/support.ts]
import { defineAgent } from '@vite-hub/agent'
import { otlp } from '@vite-hub/agent/capabilities'
export default defineAgent({
name: 'support',
capabilities: [
otlp({
endpoint: 'https://telemetry.example/otlp',
headers: { authorization: `Bearer ${process.env.OTLP_TOKEN!}` },
live: true,
resource: { 'service.namespace': 'quiver' },
}),
],
driver: { model: 'openai/gpt-5.1-mini' },
})
```
Pass the OTLP base endpoint; ViteHub appends `/v1/logs` and `/v1/traces`. With `live: true`, new Trace Events are batched as correlated OTLP LogRecords while the invocation runs, then ViteHub exports one completed trace. Without `live`, it exports only the completed trace and retains Trace Events as span events. Invocation content is metadata-only by default. Use `content.inputs`, `content.outputs`, and `content.instructions` to opt a trusted receiver into each content class independently.
Export runs through `runtime.waitUntil()`, so delivery failures do not replace the Agent result. See [`otlp()`](https://vitehub.dev/docs/capabilities/otlp) for batching, deduplication, privacy, and Capability-contribution details.
To persist a queryable invocation journal, attach Agent Invocations to the Agent Definition. Storage durability and recovery guarantees still depend on the selected store and host lifecycle. The SQLite adapter accepts a local SQLite or remote libSQL URL:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { createLibsqlAgentInvocationStore } from 'vite-hub/agent/invocations/sqlite'
import { defineAgentInvocations } from 'vite-hub/agent/server'
const invocations = defineAgentInvocations({
store: createLibsqlAgentInvocationStore({ url: 'file:./.data/invocations.db' }),
})
export default defineAgent({
driver: { model: 'openai/gpt-5.1-mini' },
invocations,
})
```
Invocation journals are metadata-only by default. Set `content: 'content'` only when the application must persist prompts, messages, reasoning, tool inputs and outputs, and result text. That opt-in stores sensitive model content in the configured durable store; apply the same access controls, retention policy, and encryption requirements as the source data.
The journal records pending, running, completed, failed, and cancelled states plus bounded invocation metadata and trace observations. Failed records retain bounded `cause` and `AggregateError.errors` trees, common status and code fields, and public ViteHub error details. Use `invocations.list()` for cursor-based summaries, `invocations.get(id)` for a stored record ID, and `invocations.getByRunId(runId, agentName?)` when starting from the source run ID. Always pass the Agent Definition name for a named Definition; the name is part of its durable invocation identity. Journal failures never change the Agent Invocation result.
When an application exposes the standard invocation journal route, inspect it without a dashboard:
```sh
vitehub agent invocations list --status running
vitehub agent invocations show INVOCATION_ID
vitehub agent invocations tail INVOCATION_ID
```
The CLI defaults to `http://localhost:5173/api/invocations`. Use `--url` or `VITEHUB_AGENT_INVOCATIONS_URL` for another local endpoint, and `--json` for automation-safe output.
Cloudflare and OpenWorkflow create the journal after durable recovery dispatch and reconcile failures after the generated Agent module loads but before the Agent handler starts. If that module cannot be evaluated, use Workflow inspection because the Agent-owned invocation store is unavailable.
Vercel Agent Definitions currently run through the inline Workflow adapter because arbitrary Agent handlers cannot be embedded in Vercel's deterministic native Workflow bundle. An accepted run starts its journal in that Agent worker, and ViteHub keeps bounded journal recovery work inside the active execution. Vercel does not expose a lifecycle hook that can guarantee arbitrary Agent recovery after that execution settles, so treat its journal as best-effort and use Workflow inspection as the authority for accepted runs. A synchronous Vercel start rejection is still recorded as a failed Agent Invocation. The run inspection metadata reports `mode: "inline"` for this path.
## Inspect invocations in the console
Enable the [ViteHub Console](https://vitehub.dev/docs/development/console) to browse retained sessions and inspect invocation events at `/_vitehub`. The Console is opt-in. Its page, API, plugin, and assets do not exist when `console` is omitted or set to `false`.
The Console guide covers Vite and Nuxt setup, fallback storage, production limits, usage records, and route authorization. An explicit `defineAgent({ invocations })` store remains authoritative when the Console is enabled.
## Control child work
Use [`startAgentInvocation()`](https://vitehub.dev/docs/agents/controlled-child-invocations) when trusted parent code must inspect or cancel a child after starting it. Use the [Subagents Capability](https://vitehub.dev/docs/capabilities/subagents) when the active model delegates work itself.
# Triggers
A Trigger turns a product event into Agent Invocation input. Use it when a Capability owns the event's shape or policy. The Agent Driver still owns execution.
## Call an Agent directly
An application route can call `runAgent()` when no Capability needs to prepare the event.
```ts [server/api/support.post.ts]
import { runAgent } from 'vite-hub/agent'
import support from '../agents/support'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const { prompt } = await readBody<{ prompt: string }>(event)
return runAgent(support, getRuntimeContext(event), { prompt })
})
```
This is a direct consumer, not a registered Trigger. Prefer it for ordinary authenticated server routes and scheduled application code.
## Use a Capability Trigger
Use a Trigger when a Capability owns event preparation. The Chat Capability registers `chat.message` and can apply history, session, concurrency, and delivery behavior before the Driver runs.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { chat } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: 'Answer support messages.',
},
capabilities: [
chat({ triggerHistory: { maxMessages: 20, source: 'thread' } }),
],
})
```
### Consume a Capability Trigger
Call the trigger from a server-owned route:
```ts [server/api/support-chat.post.ts]
import { streamAgentTrigger } from 'vite-hub/agent'
import support from '../agents/support'
import { loadAuthorizedSupportThreadMessages } from '../support-history'
import { getRuntimeContext } from '../runtime-context'
export default defineEventHandler(async (event) => {
const { text, threadId } = await readBody<{
text: string
threadId?: string
}>(event)
const user = await requireAuthenticatedUser(event)
const runId = crypto.randomUUID()
const messages = await loadAuthorizedSupportThreadMessages({
actorId: user.id,
threadId,
})
messages.push({
id: runId,
role: 'user',
parts: [{ type: 'text', text }],
})
return streamAgentTrigger(
support,
getRuntimeContext(event),
'chat.message',
{
messages,
run: {
channelId: 'portal',
messageId: runId,
origin: 'portal',
runId,
threadId,
},
},
{ output: 'ui-message-stream' },
)
})
```
`run` contains origin and trace metadata; it is not chat context. Authenticate before passing Actor identity, session selection, or trusted metadata into the Trigger input.
Direct Trigger consumers must authenticate first, reject threads the caller does not own, then load and supply the current thread's ordered messages, including the new message. `triggerHistory` limits that input; it does not backfill messages from `threadId` or a session id.
## Add an application-owned Trigger
Use `defineChannel()` when an application-owned Channel Kind prepares its own event.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { defineChannel } from 'vite-hub/agent/channels'
const ticketing = defineChannel('ticketing', {
messages: false,
triggers: {
'ticket.opened': {
invoke(context, event: { ticketId: string, summary: string }) {
return {
input: {
prompt: `Triage ticket ${event.ticketId}: ${event.summary}`,
},
run: {
channelId: context.trigger.channelId,
origin: 'ticketing',
runId: event.ticketId,
},
}
},
},
},
})
export default defineAgent({
channels: { ticketing },
driver: { model: 'openai/gpt-5.1-mini' },
})
```
The Trigger translates the event and attaches trusted context. Keep model selection, tools, and execution behavior in the Agent Definition.
## Choose how to call the Agent
| Situation | Use |
| ------------------------------------------------------- | ----------------------------------------------------------------------- |
| A server route already owns validation and input | `runAgent()` or `streamAgent()` |
| A Capability owns history, policy, or event preparation | `runAgentTrigger()` or `streamAgentTrigger()` |
| A messaging provider delivers an event | A [Channel](https://vitehub.dev/docs/agents/channels) and its Trigger |
| A model delegates to another Agent | [Subagents Capability](https://vitehub.dev/docs/capabilities/subagents) |
Webhook adapters may retain ownership until delivery finishes. Configure Channel timeout, concurrency, and durable delivery there rather than adding webhook policy to the Driver.
# Workspace context
Workspace context gives an Agent a named file tree and optional Sources. The Workspace decides what exists; Capabilities and the selected Driver decide how the Agent can access it.
Use a Workspace for project files, documentation, generated state, Source-backed paths, and controlled writeback. Do not use it as hidden prompt storage; model-facing policy belongs in [Instructions](https://vitehub.dev/docs/agents/instructions).
## Add a read-only Workspace
Enable both integrations in the ViteHub preset.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({ preset: 'node', agent: true, workspace: true }),
],
})
```
Declare a Source and grant a model-backed Driver read-only shell access:
```ts [server/agents/docs/agent.ts]
import { defineAgent } from 'vite-hub/agent'
import { workspaceShell } from 'vite-hub/agent/capabilities'
import { glob } from 'vite-hub/workspace'
export default defineAgent({
driver: {
model: 'openai/gpt-5.1-mini',
instructions: [
'Answer from the docs Source.',
'Use Workspace inspection before answering. Say when evidence is missing.',
],
},
capabilities: [workspaceShell({ mode: 'read' })],
workspace: {
sourceRootDir: process.cwd(),
sources: {
docs: glob({ cwd: '.', include: ['docs/content/**/*.md'] }),
},
},
})
```
The Source makes files available under the Workspace. `workspaceShell({ mode: 'read' })` exposes read operations to the model. Without that Capability, declaring a Source alone does not grant model-facing file access.
## Reuse a Workspace
Use `defineWorkspace()` when several Agents share the same file tree or Source configuration.
```ts [server/workspaces/product-docs.ts]
import { defineWorkspace, glob } from 'vite-hub/workspace'
export default defineWorkspace({
sourceRootDir: process.cwd(),
sources: {
docs: glob({ cwd: '.', include: ['docs/content/**/*.md'] }),
},
})
```
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'openai/gpt-5.1-mini' },
capabilities: [workspaceShell({ mode: 'read' })],
workspace: 'product-docs',
})
```
## Scope access by Actor
Use `access()` when trusted caller identity narrows the files visible to one invocation. Place it before `workspaceShell()` so the shell receives the scoped Workspace.
```ts [server/agents/editor.ts]
import { defineAgent } from 'vite-hub/agent'
import { access, workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
capabilities: [
access({
workspace: {
defaultScope: 'support',
scopes: {
support: { paths: ['support'] },
},
},
}),
workspaceShell({ mode: 'read' }),
],
driver: { model: 'openai/gpt-5.1-mini' },
workspace: 'product-docs',
})
```
Authenticate the request and pass an [Agent Actor](https://vitehub.dev/docs/agents/actors) before deriving Actor-specific access. Workspace policy enforces authorization, so base it only on trusted identity and application-owned facts. Actor-scoped Workspace access from `access()` is read-only for model-backed and custom Drivers; provider Drivers receive a writable session limited to the selected paths. Without Actor-scoped Access, write authority depends on the Workspace mode, its rules, and the tools exposed to the Driver.
## Use Workspace context with a provider
Provider Drivers receive the rendered instruction document and selected Workspace files in a temporary local working directory. Successful write-mode invocations commit through Workspace rules.
```ts [server/agents/review/agent.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: { kind: 'codex', model: 'gpt-5.5' },
workspace: { mode: 'write' },
})
```
## Keep context explicit
| Need | Use |
| -------------------------- | ------------------------------------- |
| Files or generated state | Workspace files and Sources |
| Model-facing rules | Colocated or Driver Instructions |
| Read or write tools | Capabilities such as `workspaceShell` |
| Caller-specific file scope | Access plus a trusted Agent Actor |
| Provider working directory | A write-mode Workspace |
Inspect the resolved Workspace, Sources, and access policy through the [CLI](https://vitehub.dev/docs/development/cli) before relying on them in production.
# ViteHub coding-agent skill
The public ViteHub skill helps coding agents build complete applications from current documentation and the application's installed package contract.
It keeps one compact proof loop in `SKILL.md`, then loads only the project-shape, feature, authority, host, or recovery references that match the task.
## Install the skill
Run the skills CLI from your project:
```bash [Terminal]
npx skills add https://vitehub.dev
```
The CLI discovers the published `vitehub` skill and installs it for the supported coding agents you select.
Run `npx skills list` to inspect installed project skills.
## Follow one proof loop
The skill orients in the current project, routes the smallest matching reference set, validates every planned import and option against installed exports and types, builds a coherent file set, and proves every requested behavior through its real runtime path.
The bundled references teach reusable composition rather than copying the API reference. They cover project shapes, preview contracts, Server Primitives, framework composition, Agent Definitions and Drivers, Workspaces and Sources, Channels and Triggers, Capabilities, orchestration, Boxes and hosts, proof and recovery, and public project patterns.
Links inside a reference are selection menus. The agent opens the smallest live raw page for the current behavior instead of loading the full reference library or documentation set.
## Ask for an outcome
The skill activates from normal ViteHub requests.
State the result you want and include any host or runtime constraint.
| Task | Example prompt |
| ---------------- | -------------------------------------------------------------------------------------- |
| Server primitive | `Add ViteHub KV to this route and prove that a value survives a restart.` |
| Agent | `Create a provider-backed review Agent with repository context and invoke it locally.` |
| Host boundary | `Build this ViteHub application for Cloudflare and inspect its Provider Output.` |
The skill selects one primary product lane, reads only matching references and the smallest live docs pages, checks installed exports and types, and reports the proof for each requested behavior.
When documentation and an installed version differ, the installed contract controls the implementation and the agent reports the mismatch.
## Keep instruction sources distinct
ViteHub uses several instruction sources for different actors.
Keeping them separate prevents repository guidance from leaking into runtime Agent behavior.
| Source | Audience | Purpose |
| ------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Public ViteHub skill | Coding agents building a ViteHub application | Routes project patterns through live docs, installed contracts, explicit authority, and runtime proof. |
| Repository `AGENTS.md` | Coding agents contributing to a repository | Defines local development rules and project boundaries. |
| [Agent Driver Instructions](https://vitehub.dev/docs/agents/instructions) | Agents that run inside an application | Defines model-facing runtime behavior. |
| Agent-local `skills/` | Provider-backed Agent Invocations | Automatically installs Skills owned by a folder Agent Definition. |
| [`skills()` Capability](https://vitehub.dev/docs/capabilities/skills) | ViteHub Agent Invocations | Makes Workspace-backed or external Source Skills available to the Agent. |
Agent-local Skills require a folder Definition. Place them beside `server/agents//agent.ts` under `server/agents//skills//SKILL.md`. A flat Definition such as `server/agents/review.ts` cannot own a sibling Skill tree; move it to `server/agents/review/agent.ts` when it needs colocated Skills.
## Use the docs fallback
When a coding environment cannot install skills, start from [`llms.txt`](https://vitehub.dev/llms.txt){rel=""nofollow""} and load one [raw Markdown page](https://vitehub.dev/docs/ai-resources/markdown-pages).
Keep the page URL with the supplied context so the agent can report which contract it followed.
# AI-readable documentation
ViteHub publishes a coding-agent skill, a compact documentation index, and raw Markdown pages.
Use the smallest resource that gives your agent enough context to complete the task.
## Public resources
| ViteHub resource | Use |
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [ViteHub coding-agent skill](https://vitehub.dev/docs/ai-resources/agent-instructions-skills) | Give Cursor, Claude Code, Codex, and other coding agents a repeatable ViteHub process. |
| [`/llms.txt`](https://vitehub.dev/llms.txt){rel=""nofollow""} | Discover the current documentation map. |
| [Raw Markdown pages](https://vitehub.dev/docs/ai-resources/markdown-pages) | Load one canonical page without the rendered site shell. |
| [`/llms-full.txt`](https://vitehub.dev/llms-full.txt){rel=""nofollow""} | Load the complete documentation set when a broad audit genuinely needs it. |
| [ViteHub OpenAPI document](https://vitehub.dev/openapi.json){rel=""nofollow""} | Discover the machine-readable resources served by the documentation host. |
| [ViteHub MCP server](https://vitehub.dev/mcp){rel=""nofollow""} | Search the documentation from an MCP client over Streamable HTTP. |
| [ViteHub CLI on npm](https://www.npmjs.com/package/vite-hub){rel=""nofollow""} | Install the official `vitehub` command with the framework distribution. |
The OpenAPI document describes `vitehub.dev`, not a shared hosted runtime API.
ViteHub runs inside your application, so its application endpoints depend on the Agent Definitions, Channels, and server routes that application declares.
## Install the skill
The public skill is the recommended entry point for a coding agent that edits a ViteHub application.
Install it through the skills CLI:
```bash [Terminal]
npx skills add https://vitehub.dev
```
Ask the agent for the application outcome rather than a package recipe.
The skill chooses the Server Primitives or Agents lane, checks the installed package contract, and proves the result.
```txt [Prompt]
Add durable rate limiting to this server route with ViteHub and prove it locally.
```
## Work without the skill
An AI tool that cannot install skills can use the same public sources directly.
Keep the context narrow so current task details remain prominent.
```txt [Agent flow]
1. Read https://vitehub.dev/llms.txt.
2. Choose the single smallest raw Markdown page for the task.
3. Inspect the application's installed ViteHub exports and types.
4. Keep the source URL with any copied context.
```
::tip
Use
`llms-full.txt`
for broad analysis, not routine implementation. One raw page usually gives a coding agent better signal.
::
## Choose the rendered site
Use rendered pages when navigation, diagrams, or visual examples matter.
Use raw Markdown when an agent needs source context or when you copy documentation into another tool.
# Raw Markdown pages
Raw Markdown pages expose canonical ViteHub content without the rendered site shell.
Use them after `llms.txt` identifies the smallest page that answers the current task.
## Route patterns
| Rendered route | Raw Markdown route |
| ------------------------ | ------------------------------- |
| `/docs` | `/raw/docs.md` |
| `/docs/` | `/raw/docs/.md` |
| `/docs//` | `/raw/docs//.md` |
For example, the rendered page `/docs/ai-resources/markdown-pages` is available as `/raw/docs/ai-resources/markdown-pages.md`.
## Give an agent one page
Start with the compact index, select one raw page, and preserve its URL with the supplied context.
```txt [Agent flow]
1. Fetch https://vitehub.dev/llms.txt.
2. Select one raw Markdown URL for the task.
3. Read that page and inspect the installed package contract.
4. Keep the URL in the final implementation report.
```
Add a second page only when the first page links to a required concept or reference.
This keeps task context focused and makes documentation drift easier to identify.
## Copy context into another tool
Copy the relevant section with its source URL.
The receiving tool can then preserve provenance and fetch current context when needed.
```txt [Prompt context]
Source: https://vitehub.dev/raw/docs/agents/instructions.md
```
## Inspect local source
Agents contributing to this repository can inspect `docs/content/docs/` directly.
Use the public raw URL when building an external application so the context remains portable.
::important
Raw pages describe the current published documentation. If their examples disagree with an application's installed exports or types, use the installed contract and report the mismatch.
::
## Choose another format
Use [AI-readable documentation](https://vitehub.dev/docs/ai-resources) to choose between the public skill, `llms.txt`, `llms-full.txt`, raw Markdown, and rendered pages.
# Access
`access()` adds invocation-time access resolution for chat admission and Workspace Scope.
Attach it first to restrict the Workspace seen by later Capabilities or to admit trusted chat webhooks.
`access()` can resolve chat access and apply read-only Workspace Scope before other Capabilities run.
Workspace scopes can grant paths or Sources and set a role.
Model-facing scope guidance belongs in Agent Driver Instructions or deterministic instruction imports.
## Configure access
Place `access()` before Workspace and storage Capabilities.
The selected scope narrows the Workspace facade before `workspaceShell()` exposes tools.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { access, workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
access({
workspace: {
defaultScope: 'support',
scopes: {
support: { paths: ['support'] },
},
},
}),
workspaceShell({ mode: 'read' }),
],
})
```
## How access works
The Capability records the selected Workspace Scope in invocation context and replaces the active Workspace facade with a scoped facade.
Put model-facing guidance for each Access scope in Agent Driver Instructions or an imported instruction file, and cover the Access Capability with an explicit `::capability{key="access"}` block when that guidance depends on Access.
Workspace Sources do not own authorization. Grant a Source by key from each Access scope that may use it, or grant its concrete Workspace path. Invocation-aware Source Resolution can then narrow the Source's repository, root, or Mount; Access recalculates a Source grant against that resolved shape before exposing the scoped Workspace.
## Requirements
`access({ workspace })` requires an explicit Workspace. Model-backed and custom-run-backed Agents receive a read-only scoped Workspace; writable Workspace access is supported only for Provider Agent Drivers and remains limited to the selected scope.
An admin role is required for an all-Workspace scope.
`access({ chat })` requires a resolver that returns an allow or reject decision for chat traffic.
Use trusted Agent Invoker or platform identity metadata; do not treat model text as access authority.
## Driver support
| Agent Driver | Support |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| Model-backed | Receives the scoped Workspace and any explicitly authored Agent instructions. |
| Provider-backed | Receives the scoped Workspace behavior; model-facing instructions require provider support. |
| Custom-run-backed | Receives the prepared context value and scoped Workspace; `driver.run` decides how to use them. |
## Verify access
Run an Agent Invocation that includes `access()` and inspect its traces or run events for the `access` Capability.
Verify that `access.workspaceScope` appears in invocation context and that later Workspace tools cannot read outside the selected paths.
Trigger a scope failure during development.
Confirm that a missing scope, root-mounted Source grant, missing Workspace, or invalid path escape fails before model execution.
## Options
| Option | Type | Default | Description |
| -------------------------------- | ------------------------------- | ---------- | ----------------------------------------------------------------------------------- |
| `chat.resolve` | `(context) => boolean | void` | none | Allow or reject trusted Chat Platform traffic before the Agent Invocation runs. |
| `workspace.defaultScope` | `string` | none | Fallback Workspace Scope name when `resolve` does not choose one. |
| `workspace.resolve` | `string | selection | function` | none | Select a Workspace Scope from trusted invocation context. |
| `workspace.scopes` | `Record` | none | Optional named Workspace Scope definitions for explicit grants or full access. |
| `selection.role` | `AccessRoleName` | `"viewer"` | Role applied to the selected scope. Full-Workspace access requires `"admin"`. |
| `scope.all` | `boolean` | `false` | Grant the full Workspace for that scope when the selection uses the `"admin"` role. |
| `scope.path` / `scope.paths` | `string | string[]` | none | Grant Workspace paths. |
| `scope.source` / `scope.sources` | `string | string[]` | none | Grant Workspace Sources. |
| `scope.grants` | `AccessWorkspaceScopeGrant[]` | none | Combine path and Source grants. |
## Related pages
- [Workspace context](https://vitehub.dev/docs/agents/workspace-context)
- [workspaceShell()](https://vitehub.dev/docs/capabilities/workspace-shell)
# Blob
`blob()` adds model-facing tools for a configured ViteHub Blob primitive.
It exposes object read, metadata, and list operations by default, then adds edits only in write mode.
The Capability contributes `blob_read` for get, head, and list operations.
When configured with write mode, it also contributes `blob_edit` for putting or deleting objects.
`blob_edit` can upload inline content, a current input attachment through `attachmentId`, or a Workspace file through `workspacePath`.
For Provider Agents, `assetPaths` also turns final-answer Markdown references into published delivery artifacts.
## Configure Blob access
Attach Blob in read mode until the Agent needs to write objects.
The Blob primitive must already be configured by the app.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { blob } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
blob({ mode: 'read' }),
],
})
```
## How Blob access works
ViteHub selects the configured Blob store and exposes the Blob tools.
Read mode supports one object read, metadata read, or prefix list operation per tool call.
Write mode adds put/delete operations and allows them by default.
Put operations accept exactly one of `attachmentId`, `body`, or `workspacePath`.
Delete operations return `{ pathname, deleted: true }`.
## Requirements
`blob()` uses the configured `blob` primitive when present, or the default export from an installed `@vite-hub/blob` package.
Named store selection requires the Blob primitive to expose store selection.
Writes require explicit write mode.
Set `policy: 'require-approval'` or `policy: 'deny'` when the product needs an additional gate.
## Driver support
| Agent Driver | Support |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives `blob_read` and, in write mode, `blob_edit`. |
| Provider-backed | Receives the Capability tools. In write mode, `assetPaths` also publishes current-run files referenced by the final Markdown. |
| Custom-run-backed | The configured primitive is available through runtime context; `driver.run` decides how to use it. |
## Verify Blob access
Run `vitehub agent info --agent --json` and inspect the resolved tool list.
Confirm that read mode shows only `blob_read`. Write mode also lists `blob_edit` with the configured policy.
Run one invocation against a missing Blob primitive during development.
Confirm that the Capability fails before it exposes tools.
## Options
| Option | Type | Default | Description |
| ------------ | ------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | `"read" | "write"` | `"read"` | Adds `blob_edit` when set to `"write"`. |
| `assetPaths` | `boolean | string | string[]` | `false` | Materializes Provider asset paths and publishes current-run files explicitly referenced by final Markdown. `true` uses `screenshots`. |
| `store` | `string` | default store | Selects a named Blob store when the Blob primitive supports `store()`. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for `blob_edit`. |
## Provider artifacts
Declare the directories where a Provider Agent may write public artifacts. The Agent can use its normal filesystem workflow, then reference a generated file in its final answer.
```ts [server/agents/review.ts]
import { defineAgent } from 'vite-hub/agent'
import { blob } from 'vite-hub/agent/capabilities'
import { github } from 'vite-hub/agent/channels'
export default defineAgent({
capabilities: [
blob({ assetPaths: ['artifacts'], mode: 'write', policy: 'deny' }),
],
channels: {
github: github({ pullRequest: true }),
},
driver: 'codex',
workspace: { commit: true, mode: 'write' },
})
```
If Codex adds `` to its final answer, ViteHub publishes the file through Blob, records it in `AgentRunResult.artifacts`, and rewrites that exact Markdown destination during Channel delivery.
Publication is deliberately bounded. ViteHub accepts only Markdown links or images under `assetPaths`, intersects them with files added or modified by the current Provider Workspace write-back, and ignores bare paths, stale files, removed files, and paths outside the declared roots. `policy` still controls the model-facing `blob_edit` tool; host-owned artifact publication does not require that tool to be enabled.
Configure Blob serving or a Blob driver that returns public URLs. When `blob.serve` returns a route-relative URL, Agent delivery resolves it against the invocation request URL.
## Workspace uploads
Use `workspacePath` to upload a Workspace artifact written by another Capability to Blob storage.
The path is Workspace-relative.
```ts [Agent tool call]
await blob_edit({
operation: 'put',
pathname: 'review/screenshots/home.png',
workspacePath: 'screenshots/home.png',
options: { contentType: 'image/png' },
})
```
## Related pages
- [Blob primitive](https://vitehub.dev/docs/server-primitives/blob)
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
# Browser
`browser()` mounts an inspectable browser Skill at `skills/browser/SKILL.md` for a Provider Agent. The matching CLI must already be available to the provider process.
```ts [server/agents/review.ts]
import { defineAgent } from 'vite-hub/agent'
import { browser } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: 'codex',
workspace: { mode: 'write' },
capabilities: [browser()],
})
```
The Skill tells the provider to use `agent-browser` through its native command tools and save screenshots inside the Workspace. ViteHub does not install the CLI or add a parallel shell tool. Model-backed and custom Drivers fail explicitly because they do not own the required provider command loop.
| Option | Type | Default | Description |
| -------------- | -------- | --------------------------- | ---------------------------------------- |
| `command` | `string` | `"agent-browser"` | Executable name named in the Skill. |
| `skillContent` | `string` | built-in browser Skill | Markdown content for the mounted Skill. |
| `skillPath` | `string` | `"skills/browser/SKILL.md"` | Workspace path for the Skill. |
| `sourceKey` | `string` | `"skill.browser"` | Workspace Source key for the Skill file. |
Use the [Browser primitive](https://vitehub.dev/docs/server-primitives/browser) when trusted server code owns the Browser Session lifecycle.
# Chat
`chat()` adds chat-oriented runtime behavior to an Agent Definition.
It contributes a `chat.message` Agent Trigger, Chat History state requirements, and a chat finish extension.
The Chat Capability turns message-shaped input into Agent Invocations and exposes the trigger to the CLI Dev Loop.
Message-shaped Channels own route admission and delivery into that trigger.
## Configure chat
Attach `chat()` to call the Agent from a chat interface through the Agent Trigger API.
Use [Channels](https://vitehub.dev/docs/agents/channels) to deliver messages from Slack, Telegram, Teams, web chat, or another adapter.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { chat } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
chat(),
],
})
```
## How chat works
`chat()` registers the `chat.message` trigger with `ui-message[]` input and `ui-message-stream` output.
It prepares Chat History state when available, records chat context, and provides chat finish data after the Agent Invocation completes.
## Requirements
Chat History state is optional in local development but needs an Agent State Provider when the deployed stack requires durable sessions or concurrency coordination.
External Chat Platform Adapters remain explicit application dependencies configured through Channels.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives message-shaped input and can stream chat output through the model-backed path. |
| Provider-backed | Receives the prepared invocation input and chat context; provider chat behavior follows the provider adapter. |
| Custom-run-backed | Receives the chat trigger input and context; `driver.run` owns the response shape. |
## Verify chat
Run `vitehub agent dev --agent --prompt "hello"` and confirm the Agent responds through the configured Chat Capability.
Send one message through `vitehub agent dev` and verify the invocation origin, Chat Session behavior, and finish extension through traces or run events.
For adapter-backed delivery, inspect the Channel-generated webhook registrations for the expected route metadata.
## Options
| Option | Type | Default | Description |
| ---------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hooks` | `AgentChatEventHooks` | none | Chat event hooks such as `onDirectMessage`. |
| `lifecycleHooks` | `Record` | none | Additional lifecycle-hook settings for integrations that consume Chat Capability configuration. |
| `event` | `"directMessage"` | none | Chat event binding hint. |
| `triggerHistory` | `"none" | { source: "thread"; maxMessages?: number }` | last 20 messages, or `threadHistory.maxMessages` when derived | Chat History Window sent into the `chat.message` Agent Trigger. |
| `threadHistory` | `{ maxMessages?: number; ttlMs?: number }` | inherited | Adapter thread backfill/cache; stores messages but does not by itself define model input. |
| `messageHistory` | Chat SDK message-history configuration | inherited | Adapter message-history behavior passed to the Chat SDK. |
| `logger` | Chat SDK logger | inherited | Logger passed to adapter-backed Chat SDK delivery. |
| `sessions` | `boolean | AgentChatSessionOptions` | inherited | Chat Session behavior, including `strategy`, `idleTimeoutMs`, and `metadataKey`. |
| `state` | `AgentChatStateResolver` | runtime state | Chat State adapter override. |
| `transcripts` | Chat SDK `TranscriptsConfig` | none | Transcript persistence configuration for adapter-backed Channels. |
| `identity` | `IdentityResolver` | channel-qualified user id when transcripts are enabled | Resolve the identity used to partition transcripts. |
| `stream` | `boolean` | inherited | Streams chat trigger output when enabled. |
| `streamingUpdateIntervalMs` | `number` | inherited | Minimum interval between streamed Channel message updates. |
| `concurrency` | `"drop" | "parallel" | "queue" | "reject" | "serial" | "steer" | string` | inherited | Overlapping message behavior. `serial` runs each retained message as a separate awaited Agent Invocation in queue order; `queue` coalesces retained messages into one invocation. `steer` is accepted for API compatibility and currently uses the same coalescing behavior as `queue`. Queue retention and failure guarantees come from the configured Chat State runtime. |
| `lockScope` | `"agent" | "channel" | "thread" | string` | inherited | Scope used for message locks. |
| `dedupeTtlMs` | `number` | inherited | Time-to-live for Chat SDK duplicate-message keys. |
| `userName` | `string` | `"vitehub"` | Agent username used by adapter-backed Chat SDK delivery. |
| `fallbackStreamingPlaceholderText` | `string | string[] | null | function` | inherited | Placeholder text while streaming starts. Arrays pick one entry per Agent Invocation; empty arrays skip the placeholder. |
| `errorFallbackText` | `string | null | function` | inherited | Fallback message when chat handling fails. |
## Related pages
- [Agent triggers](https://vitehub.dev/docs/agents/triggers)
- [Chat History and sessions](https://vitehub.dev/docs/agents/chat-history-sessions)
- [title()](https://vitehub.dev/docs/capabilities/title)
- [chatSummary()](https://vitehub.dev/docs/capabilities/chat-summary)
# Chat summary
`chatSummary()` adds a conversation summary command.
It looks for an explicit command in the latest user input, generates a summary, replaces the command with summary text, and exposes the generated summary as output metadata.
The Capability contributes input behavior similar to `inputCommands()`.
By default it recognizes a summary command, summarizes the current conversation, and writes the summary into Agent Run Input context.
## Configure summaries
Attach `chatSummary()` with `chat()` to let users request a summary from a chat interface.
The default command uses the standard input command trigger.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { chat, chatSummary } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
chat(),
chatSummary(),
],
})
```
## How summaries work
`chatSummary()` runs during the input phase.
When the configured command is present, it removes the command text, summarizes the source messages with a model, custom executor, or heuristic fallback, then replaces the command with `Conversation summary`.
The generated value is available as `chatSummary` in input context and as a finish extension for the invocation that generated it.
## Requirements
The command name must be a valid Input Command name.
Model-based summaries require an explicit model option; otherwise the Capability uses its heuristic fallback.
Disable the command only when application code invokes the summary behavior directly.
## Driver support
| Agent Driver | Support |
| ----------------- | ---------------------------------------------------------------------- |
| Model-backed | Receives the transformed input containing the generated summary. |
| Provider-backed | Receives the transformed input before provider execution. |
| Custom-run-backed | Receives the transformed input and context values before `driver.run`. |
## Verify summaries
Run a chat invocation with the summary command.
Inspect the final Agent Run Input and confirm the command was replaced with `Conversation summary` text.
Inspect the finish extension and verify it appears only on the invocation that generated the summary.
## Options
| Option | Type | Default | Description |
| --------------------- | ------------------------------------------ | ----------------------------------- | ---------------------------------------------------- |
| `command` | `false | ChatSummaryCommandOptions` | `{ name: "summary", trigger: "/" }` | Enables or configures the summary Input Command. |
| `command.name` | `string` | `"summary"` | Command name. |
| `command.trigger` | `string` | `"/"` | Command prefix. |
| `command.description` | `string` | generated | Command description. |
| `execute` | `(input) => string | { summary?: string }` | none | Custom summary generator. |
| `fallback` | `string` | `"No conversation to summarize."` | Summary used when generation returns no usable text. |
| `id` | `string` | `"chat-summary"` | Capability id and summary context key prefix. |
| `instructions` | `string` | generated | System instructions for model-backed summaries. |
| `maxLength` | `number` | `1200` | Maximum summary length. |
| `model` | AI SDK model | heuristic fallback | Model used for summaries. |
## Related pages
- [chat()](https://vitehub.dev/docs/capabilities/chat)
- [inputCommands()](https://vitehub.dev/docs/capabilities/input-commands)
# Cost
Add `cost()` when an application needs exact USD for arithmetic and a ready-to-render value for display. The Capability enriches the Agent Usage Record before Agent Finish Hooks run and before streamed usage is emitted to clients; raw usage capture works without it.
## Add cost
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { cost } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'zai/glm-5v-turbo' },
capabilities: [
cost(),
],
})
```
By default, `cost()` prices regular input, cache-read, cache-write, and output tokens from Vercel AI Gateway's public model catalog. ViteHub uses exact decimal arithmetic, caches successful catalog responses for five minutes, and bounds each request to ten seconds.
Pricing is best-effort. A missing model match, unavailable catalog, timeout, invalid price, or pricing callback error leaves the usage record and successful Agent Invocation unchanged. A cost already reported by the provider remains authoritative.
## Read the enriched record
Finish Hooks read the canonical record from `event.invocation.usage`. Use `cost.usd` for arithmetic or persistence and `cost.display` for UI; consumers do not need to format the value themselves. The Capability's typed `cost` finish extension returns the same record.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { cost } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'zai/glm-5v-turbo' },
capabilities: [cost()],
hooks: {
'agent:finish'(event) {
const usageCost = event.invocation.usage?.cost
if (!usageCost) return
console.log(usageCost.usd)
console.log(usageCost.display)
},
},
})
```
```txt [Output]
0.00125
~$0.00125
```
The canonical record keeps the full model identifier and Gateway transport separate, preserves exact USD for arithmetic, and includes a ready-to-render display value.
```ts
{
model: 'zai/glm-5v-turbo',
transport: 'gateway',
usage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
cost: {
usd: '0.00125',
display: '~$0.00125',
estimated: true,
source: 'vercel-ai-gateway',
},
}
```
For streams, ViteHub waits to resolve pricing until the usage record becomes available during consumption, then enriches it before client emission and Finish Hooks.
## Provide application pricing
Pass `pricing` when the application owns its catalog or provider mapping. Return exact USD as a decimal string; ViteHub derives the display value.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { cost, type AgentUsagePricing } from 'vite-hub/agent/capabilities'
const pricing: AgentUsagePricing = ({ model }) => {
if (model !== 'internal/support-model') return
return {
usd: '0.00125',
estimated: true,
source: 'custom',
}
}
export default defineAgent({
driver: { model: 'internal/support-model' },
capabilities: [cost({ pricing })],
})
```
Return `undefined` when pricing is unavailable. Custom pricing receives the model, response metadata, Agent Run metadata, and token usage, and can return a provider quote or a calculation from an application-owned decimal library. Keep it deterministic for those inputs because ViteHub may call it while a stream is consumed.
Import `vercelAiGatewayPricing()` when application-owned work adds usage after the Capability runs and must reprice the record with the same catalog behavior.
```ts
import { vercelAiGatewayPricing } from 'vite-hub/agent/capabilities'
const pricing = vercelAiGatewayPricing()
```
## Options
| Option | Type | Default | Description |
| --------- | ------------------- | --------------------------------- | --------------------------------------------------------------------------------------- |
| `pricing` | `AgentUsagePricing` | Vercel AI Gateway catalog pricing | Resolves a cost from the model, response metadata, Agent Run metadata, and token usage. |
## Verify it
Invoke the Agent with a model that reports token usage. Confirm that `event.invocation.usage` is present without the Capability, then install `cost()` and confirm a matched model adds `cost`. Test missing and failing pricing callbacks too: both must preserve the successful Agent Invocation and raw usage.
## Related
- [Agent Invocations](https://vitehub.dev/docs/concepts/agent-invocations)
- [Runtime events](https://vitehub.dev/docs/reference/runtime-events)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
# Custom capabilities
Create a custom Capability when the official catalog does not describe the Agent ability your product needs.
Start from the product ability, not from the raw tool or primitive call.
A custom Capability names what the Agent can do and declares the requirements it needs. Inspection output lists the tools, triggers, context values, metadata, and policy decisions contributed by that Capability.
## Minimum shape
Define the Capability near the application code that owns the behavior.
Use `defineCapability()` so ViteHub validates the id and composes the Capability through the normal lifecycle.
```ts [server/agents/capabilities/tickets.ts]
import { defineCapability } from 'vite-hub/agent'
import { z } from 'zod'
const searchTicketsInput = z.object({
query: z.string(),
})
export function tickets() {
return defineCapability({
id: 'tickets',
tools: {
searchTickets: {
name: 'searchTickets',
description: 'Search support tickets by query.',
inputSchema: searchTicketsInput,
execute: async (input: z.output) => searchTickets(input.query),
},
},
})
}
```
Agent tools accept raw JSON Schema or a validator that implements both Standard Schema and Standard JSON Schema. Zod 4 implements both directly. ViteHub does not bundle a validator; use the one your app owns.
Attach the custom Capability like any official Capability.
Keep instructions explicit in the Agent Driver so Capability config does not become a hidden prompt bag.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { tickets } from './capabilities/tickets'
export default defineAgent({
driver: {
model,
instructions: [
'Triage support requests.',
'Use ticket tools only for support ticket lookup and triage.',
].join('\n\n'),
},
capabilities: [
tickets(),
],
})
```
## Add requirements
Requirements fail before the Capability exposes behavior.
Use them when the Capability needs a configured primitive, a Workspace, a writable Workspace, or a specific Workspace path.
Do not create missing storage, Workspace paths, or execution authority implicitly.
If the product needs provisioning, keep that in the primitive or framework integration layer.
## Add policy
Tool policy defaults to `allow` when omitted.
Use `require-approval` or `deny` when a model-facing action needs an additional runtime gate after the Capability has established its modes, scopes, allowlists, and input validation.
Keep custom policy narrow and visible. A reviewer needs to understand the operations it permits by reading the Capability Definition.
## Contribute Workspace inputs
Use `workspace` to add Workspace Sources or rules for an invocation.
The contribution is add-only and inspectable; it does not mutate the Agent's authored Workspace Definition.
```ts [server/agents/capabilities/tickets.ts]
import { defineCapability } from 'vite-hub/agent'
export function ticketContext() {
return defineCapability({
id: 'ticket-context',
workspace: ({ context }) => {
const ticketId = context.get<{ id?: string }>('ticket')?.id
if (!ticketId) return
return {
rules: {
'support/tickets/**': { read: true },
},
sources: {
ticket: {
mount: 'support/tickets',
async getKeys() {
return [`${ticketId}.md`]
},
async getItem(key) {
return {
content: await loadTicketMarkdown(key),
key,
mediaType: 'text/markdown',
}
},
},
},
}
},
})
}
```
For provider-backed Agents, declare required files through `requires.workspace.paths` or contribute them through Workspace Sources. The Provider Workspace session materializes the selected scope, so the application does not need a provider-specific path list.
## Add a Capability CLI
Use `cli` when the Capability owns commands that agents and developers can run instead of a generic shell command.
The public API accepts a static command tree or an invocation resolver on the Capability Definition.
```ts [server/agents/capabilities/inventory-runtime.ts]
import { defineCapability } from 'vite-hub/agent'
import * as v from 'valibot'
const inventoryItemsInput = v.object({
limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
})
const inventoryItemsOutput = v.object({
items: v.array(v.object({ id: v.string() })),
})
export const inventoryRuntime = defineCapability({
id: 'inventory-runtime',
cli: {
name: 'inventory',
description: 'Inspect live inventory data.',
commands: {
items: {
description: 'Inventory item data.',
commands: {
list: {
description: 'List inventory items for the current application context.',
input: inventoryItemsInput,
output: { format: 'json', schema: inventoryItemsOutput },
effects: ['read', 'network:inventory'],
async run({ input }) {
return await listInventoryItems(input)
},
},
},
},
},
},
})
```
The `input` and `output.schema` values accept any Standard Schema-compatible validation library.
ViteHub exposes command metadata through the generated CLI-named tool. Keep `instructions.md` focused on policy. Use `::capability{key="inventoryRuntime"}` to mark guidance for that Capability.
Return `undefined` from a resolver to hide the CLI for the current invocation. The Capability remains attached and inspectable.
To omit the entire Capability, resolve the Agent Definition's `capabilities` list instead. This decides selection before the Capability contributes tools, CLI commands, requirements, hooks, or cleanup work.
```ts [server/agents/capabilities/inventory-runtime.ts]
export const inventoryRuntime = defineCapability({
id: 'inventory-runtime',
cli: ({ actor }) => actor.kind === 'support'
? {
name: 'inventory',
commands: {
list: {
run: () => listInventoryItems(),
},
},
}
: undefined,
})
```
First-party adapters can generate the same CLI shape from their own metadata. For example, `openapi({ cli: { name: 'billing' }, ... })` creates one subcommand per allowed OpenAPI operation and preserves each operation summary or description in the tool contract.
Use a resolver for invocation-specific availability, not to mutate command ownership after a run starts.
During development, run the Capability CLI through the Agent Dev Loop.
Agents expose attached Capability CLI Contributions to compatible Agent Drivers and the Agent Dev Loop by default. Use `defineAgent({ cli: { capabilities: false } })` to attach the Capability without exposing its CLI.
```bash [Terminal]
pnpm vitehub agent dev --url http://localhost:3000 --agent support --cli inventory -- items list --json
```
## Driver support
| Agent Driver | Custom Capability behavior |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives model-facing tools when the Capability contributes them. |
| Provider-backed | Receives Agent tools through the private MCP bridge plus supported runtime effects. Provider Tool contributions are unsupported. |
| Custom-run-backed | Receives prepared input and invocation context; `driver.run` decides which custom Capability outputs to consume. |
## Verify a custom Capability
Run one Agent Invocation through `vitehub agent dev` and inspect its streamed tool events.
Check that the custom Capability id appears once, its requirements pass, and its tools are exposed only when expected.
Add an Agent Eval when the Capability changes product behavior.
Use a focused fixture that proves the Capability exposes the intended ability and does not expose adjacent authority.
## Expose eval-visible metadata
Use a `finish` provider to publish invocation metadata for finish hooks, channel delivery code, or eval assertions.
The value is keyed by Capability id and is available through `observation.extensions.get(id)` in Agent Evals.
```ts [server/agents/capabilities/tickets.ts]
import { defineCapability } from 'vite-hub/agent'
export function tickets() {
return defineCapability({
id: 'tickets',
finish(event) {
return {
resultKind: typeof event.result,
status: event.error ? 'failed' : 'completed',
}
},
})
}
```
```ts [server/agents/support.eval.ts]
import { defineEval, hasCapabilityExtension } from 'vite-hub/agent/eval'
import support from './support'
export default defineEval({
agent: support,
scenarios: [{
name: 'uses ticket context',
input: { prompt: 'Find the open billing ticket' },
scorers: [
hasCapabilityExtension('tickets', 'status'),
],
}],
})
```
## Related APIs
- [Capabilities overview](https://vitehub.dev/docs/capabilities)
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
# Database
`db()` adds model-facing tools for a configured ViteHub Database primitive.
It exposes read-only query and schema inspection by default, then adds SQL mutation only when write modes allow it.
Cloudflare and Vercel hosted Agent routes receive the Database primitive automatically when the app configures `hubDb()`.
The Capability contributes `db_query` for one read-only SQL statement and `db_schema` for schema inspection.
When data or schema write modes allow it, it also contributes `db_exec` for one mutation statement with a rationale.
## Configure database access
Attach DB in read mode until the Agent needs guarded mutations.
The Database primitive must already be configured by the app.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { db } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
db({ mode: 'read' }),
],
})
```
## How database access works
ViteHub selects the configured database handle and enforces the single-statement SQL guardrail.
`db_query` accepts one read-only query.
`db_exec` rejects read-only SQL, requires a rationale, and separates data mutations from schema changes through `mode` and `schemaMode`.
## Requirements
`db()` requires a configured `db` primitive.
The primitive must expose raw string `query()` for reads and `exec()` for mutations.
Mutation tools require write mode.
DDL requires schema write mode.
Enabled mutations are allowed by default, while the single-statement, rationale, and SQL-kind checks still apply.
Set `policy: 'require-approval'` or `policy: 'deny'` when the product needs an additional gate.
## Driver support
| Agent Driver | Support |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| Model-backed | Receives `db_query`, `db_schema`, and write tools when enabled. |
| Provider-backed | Receives Database tools through the provider MCP bridge. |
| Custom-run-backed | The configured primitive is available through runtime context; `driver.run` decides how to use it. |
## Verify database access
Run `vitehub agent info --agent --json` and inspect the resolved tool list.
Confirm that read mode shows `db_query` and `db_schema`. A write-capable configuration also lists `db_exec` with the configured policy.
Run a multi-statement SQL input during development.
Confirm that the Capability rejects it before it reaches the Database primitive.
## Options
| Option | Type | Default | Description |
| ------------ | ------------------------------------ | ----------- | --------------------------------------------------------------------- |
| `database` | `string` | `"default"` | Selects a named database when the DB primitive supports `database()`. |
| `mode` | `"read" | "write"` | `"read"` | Allows data mutation through `db_exec` when set to `"write"`. |
| `schemaMode` | `"read" | "write"` | `"read"` | Allows DDL through `db_exec` when set to `"write"`. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for `db_exec`. |
## Related pages
- [Database primitive](https://vitehub.dev/docs/server-primitives/database)
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
# Diagnostics
`diagnostics()` is an opt-in operational Capability. It reports a terminal event for every Agent Invocation and can sample resources through a Runtime inspector. Reporters receive structured events, so an application can write JSON logs, metrics, or another operations sink without coupling the Agent Definition to one dashboard.
## Observe a Node service
Use ViteHub's Node adapter to observe process, host, and Linux cgroup resources:
```ts [server/agents/worker.ts]
import { defineAgent } from 'vite-hub/agent'
import { diagnostics } from 'vite-hub/agent/capabilities'
import { nodeRuntimeResources } from 'vite-hub/runtime/node'
export default defineAgent({
name: 'worker',
capabilities: [
diagnostics({
resources: nodeRuntimeResources(),
}),
],
driver: { model: 'openai/gpt-5.1-mini' },
})
```
The default reporter writes structured console objects. Pass `reporter` to own delivery:
```ts
diagnostics({
resources: nodeRuntimeResources(),
reporter: event => operations.write(event),
})
```
Reporter and inspector failures are contained. They produce a local diagnostic and do not replace a successful Agent result.
## Event contract
The Capability reports:
| Event | When |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `agent.invocation.terminal` | The invocation completes or fails. Includes outcome, duration, run ID when present, and a bounded structured error. |
| `agent.resource.snapshot` | Sampling starts, finishes, or reaches the heartbeat interval. |
| `agent.resource.peak` | A peak observation grows by at least `peakStepBytes`. |
| `agent.resource.inspect.failed` | The inspector fails or exceeds its timeout. |
Resource observations declare a `scope`, `source`, `unit`, and numeric `value`. The Node adapter uses `process` for Node memory and CPU, `host` for available host memory, and `service` for Linux cgroup v2 values. A service observation provides correlation with an invocation's run ID; it is not per-invocation attribution when multiple invocations share the service.
Unsupported sources are recorded in `support`. Unlimited cgroup values are omitted rather than reported as zero. This keeps small machines and non-Linux hosts honest without requiring application-specific `/proc` parsing.
## Sampling behavior
Sampling is bounded to one active inspection and one coalesced pending reason. Reporter delivery is ordered and bounded by `timeout`. A slow inspector or reporter cannot create an unbounded polling backlog. Finish supersedes a stale poll and waits for the final observation before the Capability closes.
`diagnostics()` is separate from `otlp()`: diagnostics records operator health and resource pressure, while OTLP exports the Agent Invocation trace. Keeping the lanes separate prevents a broken telemetry receiver from recursively hiding its own delivery failure.
## Options
| Option | Type | Default | Description |
| --------------- | --------------------------- | ------------------------- | --------------------------------------------------------------------------------- |
| `reporter` | `RuntimeDiagnosticReporter` | Structured console output | Receives operational events. |
| `resources` | `RuntimeResourceInspector` | None | Enables scoped resource sampling. |
| `interval` | `number` | `10000` | Resource polling interval in milliseconds. Must not exceed `heartbeat`. |
| `heartbeat` | `number` | `60000` | Maximum interval between snapshot events in milliseconds. |
| `peakStepBytes` | `number` | `67108864` | Minimum peak increase before a peak event. |
| `timeout` | `number` | `1000` | Maximum duration of one resource inspection or reporter delivery in milliseconds. |
## Related
- [OTLP](https://vitehub.dev/docs/capabilities/otlp)
- [Invocations](https://vitehub.dev/docs/agents/invocations)
- [Runtime context](https://vitehub.dev/docs/concepts/runtime-context)
# Email
`email()` grants an Agent one external side effect: `email_send` sends a plain-text message from an application-owned sender through the configured ViteHub Email primitive.
Attach it only when the Agent needs to contact external recipients. Restrict exact addresses with `recipients`, then add policy when delivery requires approval or contextual authorization.
::warning
An approved call can contact real people and incur provider charges.
Start with a short
`recipients`
allowlist,
`policy: 'require-approval'`
, a provider test account, and an approved test recipient.
::
## Configure the Email primitive first
Configure one Unemail provider in the ViteHub preset. Runtime Env resolves the credential on the server for every send.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
import { env } from 'vite-hub/env'
export default defineConfig({
plugins: [
vitehub({
preset: 'node',
email: {
driver: 'unemail/driver/resend',
options: {
apiKey: env({ secret: true, source: env.source('RESEND_API_KEY') }),
},
},
}),
],
})
```
Follow [Configure Resend](https://vitehub.dev/docs/server-primitives/email#configure-resend), or select another `unemail/driver/*` provider through the same `driver` option.
Keep credentials in Server Env or the deployment platform's secret store and reference them with an Env declaration without a default. Literal options and non-secret Env defaults are included in build output; ViteHub rejects defaults on declarations marked secret. The Capability never exposes runtime credentials to the model.
## Requirements
- The application must run on Node.js 24.15 or later.
- Email configuration requires Vite 8 or later and one configured provider.
- The configured provider must authorize the `from` address.
- Generated Agent routes receive the Email runtime handle only while the Email Vite integration is active.
## Grant the Agent permission to send
Import `email()` from the official Capability catalog.
Set `from` to a sender that the configured provider authorizes.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { email } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
email({
from: 'support@example.com',
recipients: [
'customer@example.net',
'owner@example.com',
],
policy: 'require-approval',
}),
],
})
```
This configuration exposes one tool:
| Tool | Side effect | Result |
| ------------ | -------------------------------------------------------- | --------------------------------------------------------- |
| `email_send` | Sends one plain-text message from the configured sender. | The Email primitive's `{ id, driver }` acceptance result. |
A successful result means the active provider accepted the message and returned an ID.
It does not prove inbox delivery, display, or reading.
## Send a message
`email_send` accepts exactly three fields.
| Field | Type | Required | Description |
| --------- | ---------------------------- | -------- | --------------------------------------------------------------------------- |
| `to` | `string | readonly string[]` | Yes | One recipient address or a non-empty list. Every address must be non-empty. |
| `subject` | `string` | Yes | A non-empty subject. |
| `text` | `string` | Yes | A non-empty plain-text body. Do not include credentials or other secrets. |
The Capability fixes `from` from application configuration.
The model cannot set HTML, headers, attachments, carbon-copy recipients, blind-carbon-copy recipients, or reply routing through this tool.
The Capability checks that recipient strings are non-empty; the Email driver and provider still own mailbox syntax, sender authorization, and delivery rules.
ViteHub does not add recipient-count, text-length, payload-size, or send-rate limits beyond non-whitespace validation.
Your delivery provider owns those limits and may charge for every accepted recipient or message.
## Authorize recipients
Use `recipients` as the allowlist of exact addresses the Agent may contact.
Every address in `email_send.to` must match the configured list; one address outside the list denies the entire call before the Email primitive runs, so ViteHub never partially sends a multi-recipient message.
```ts [server/agents/support.ts]
email({
from: 'support@example.com',
recipients: [
'customer@example.net',
'owner@example.com',
],
})
```
ViteHub includes this list in Capability metadata and the `email_send` tool description, so the Agent can select a valid address without guessing.
The list becomes part of the Agent's model context; include only addresses that the model is allowed to see.
Matching trims surrounding whitespace and ignores letter case, but the Capability forwards the original address values to the Email provider.
Aliases, display-name forms, and other address variations remain different strings unless they appear explicitly in `recipients`.
Set `recipients: []` to deny all sends, or omit `recipients` when static recipient restriction belongs elsewhere.
The optional `policy` is an additional gate after this allowlist.
It cannot widen `recipients`: a configured `policy: 'allow'` still denies an address outside the list, while `policy: 'require-approval'` prompts only for an address that passed the list.
Without `policy`, allowed recipients send immediately; a policy callback can apply contextual authorization by returning `allow`, `deny`, `require-approval`, or `retryable-failure`.
Read [Runtime policy, approvals, and traces](https://vitehub.dev/docs/concepts/runtime-policy-approvals-and-traces) before enabling unattended delivery.
## Handle failures without duplicate delivery
The Capability forwards the Email primitive result and error unchanged.
It does not retry.
Capability-owned validation and runtime failures happen before the Email driver runs, so these failures cannot have delivered a message:
| Failure | When it occurs |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Invalid or missing `from` | `email()` rejects the Agent Definition during construction. |
| Non-array `recipients`, or a blank, non-string, or sparse entry | `email()` rejects the Agent Definition during construction. |
| Missing Email primitive | Capability resolution rejects before the Agent Driver receives `email_send`. |
| Runtime handle without `send()` | Capability resolution rejects before the Agent Driver receives `email_send`. |
| Empty `to`, `subject`, or `text` | Tool execution rejects before calling the Email primitive. |
| Recipient outside `recipients` | Policy denies the entire tool call before calling the Email primitive. |
After the Capability calls the Email primitive, handle the `EMAIL_*` ViteHub error code according to the delivery state:
| Failure | What to do |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL_NOT_CONFIGURED` | Configure the Email integration with one provider. |
| `EMAIL_AUTHENTICATION` | Fix provider credentials or sender authorization before retrying. |
| `EMAIL_RATE_LIMITED` | Apply an application-owned backoff or queue policy. |
| `EMAIL_NETWORK` or `EMAIL_TIMEOUT` | Treat delivery as uncertain. Check provider delivery logs before retrying, because the provider may already have accepted the message. |
| `EMAIL_PROVIDER_FAILED` | Inspect protected server logs and provider delivery records. Never expose `cause` to the model. |
ViteHub-produced `ViteHubError.message` values are safe to return from the public runtime.
ViteHub-wrapped provider failures remain in `cause` for protected server-side diagnostics and may contain addresses, credentials, or response content; custom drivers must preserve the same rule.
## Keep Dynamic Markdown application-owned
`email_send` is plain-text-only by design.
It does not render model-authored Markdown into HTML because [`renderEmailMarkdown()`](https://vitehub.dev/docs/server-primitives/email#compose-dynamic-markdown) accepts trusted templates and does not sanitize authored HTML or trusted fragments.
When a product needs branded HTML, compose a trusted template in application code and call the Email primitive directly, or expose a [Custom Capability](https://vitehub.dev/docs/capabilities/custom-capabilities) with a narrow set of escaped template values.
Do not pass unrestricted model output into a trusted HTML fragment.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------- |
| Model-backed | Receives `email_send` after the Email primitive resolves. |
| Provider-backed | Runtime requirements apply; model-facing Email tools are not passed by default. |
| Custom-run-backed | Receives the resolved tool set; `driver.run` decides whether and when to call `email_send`. |
## Verify email delivery
Run `vitehub agent info --agent --json` and confirm its tool list contains only `email_send` for this Capability.
Confirm that the Capability reports write mode and an `email` primitive requirement.
For the first delivery, use an approved test recipient and a test or sandbox provider account.
Approve the call, confirm the tool returns a non-empty `id`, then verify the same message in provider delivery logs or the recipient mailbox.
## Options
| Option | Type | Default | Description |
| ------------ | ------------------------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `from` | `string` | Required | Non-empty application-owned sender passed to every message. The provider still validates and authorizes it. |
| `recipients` | `readonly string[]` | `undefined` (no static allowlist) | Exact recipient allowlist. Every `to` address must match; an empty list denies all sends. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Optional approval or authorization policy for `email_send`. |
`email()` has no `mode` option because Email exposes no read operation.
The Capability always reports `mode: 'write'` so inspection and policy tooling can identify the side effect.
## Related pages
- [Email primitive](https://vitehub.dev/docs/server-primitives/email)
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
- [Runtime policy, approvals, and traces](https://vitehub.dev/docs/concepts/runtime-policy-approvals-and-traces)
# Fetch
`fetch()` adds model-facing HTTP tools that the developer names and defines.
Use it for specific endpoints, not for unrestricted web browsing.
The Capability creates one tool per entry in `tools`.
Each tool can validate input, build a request, parse JSON or text, validate the response, and transform the output.
## Configure HTTP requests
Define at least one named fetch tool.
Keep the endpoint and method explicit.
```ts [server/agents/status.ts]
import { defineAgent } from 'vite-hub/agent'
import { fetch } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
fetch({
tools: {
serviceStatus: {
description: 'Fetch current service status.',
method: 'GET',
url: 'https://status.example.com/api/status',
},
},
}),
],
})
```
## How requests work
ViteHub validates the configured tool map and creates internal Agent tools.
At invocation time, each tool resolves the request, executes the HTTP call, parses the configured response type, and returns either the parsed data or a transformed output.
## Requirements
`fetch({ tools })` requires at least one tool definition.
Each tool must provide a URL directly or return one from its request resolver.
Use schemas for input and response validation when the endpoint accepts arguments or returns data that model behavior depends on.
## Driver support
| Agent Driver | Support |
| ----------------- | ---------------------------------------------------------------------------------------- |
| Model-backed | Receives the named fetch tools. |
| Provider-backed | Receives the named fetch tools through the provider MCP bridge. |
| Custom-run-backed | Receives prepared context; `driver.run` decides whether to call HTTP endpoints directly. |
## Verify HTTP requests
Inspect the Agent tool list and confirm only the named fetch tools appear.
Run one invocation with invalid input when a schema is configured and verify the request does not leave the process.
## Options
| Option | Type | Default | Description |
| ------------------------- | -------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------ |
| `tools` | `Record` | required | Named fetch tools exposed to the model. |
| `tools.*.description` | `string` | `Fetch .` | Tool description. |
| `tools.*.url` | `string | URL` | none | Static request URL. |
| `tools.*.request` | `object | function` | none | Static or input-derived request definition. |
| `tools.*.method` | `"GET" | "HEAD" | "POST"` | `"GET"` | HTTP method used when the request definition does not override it. |
| `tools.*.request.url` | `string | URL` | `tools.*.url` | Request URL. A request resolver can derive it from validated tool input. |
| `tools.*.request.method` | `"GET" | "HEAD" | "POST"` | `tools.*.method`, then `"GET"` | Per-request HTTP method override. |
| `tools.*.request.headers` | `Record` | none | Request headers. |
| `tools.*.request.query` | `Record` | none | Query parameters appended to the URL. |
| `tools.*.request.body` | `unknown` | none | Request body. |
| `tools.*.request.timeout` | `number` | none | Request timeout in milliseconds. |
| `tools.*.inputSchema` | Standard Schema | none | Validates model tool input before request construction. |
| `tools.*.schema` | Standard Schema | none | Validates parsed response data. |
| `tools.*.responseType` | `"json" | "text"` | `"json"` | Response parser. |
| `tools.*.transform` | `(data, input) => output` | none | Maps validated response data before returning it to the model. |
## Related pages
- [webSearch()](https://vitehub.dev/docs/capabilities/web-search)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
# Git
`git()` exposes bounded Git inspection and selected local Workspace Session state changes.
Use it for review, source-history inspection, and local branch selection, not for publishing repository history.
The Agent must use a git-capable Workspace Session.
The Capability adds one model-facing `shell` tool for controlled Git commands.
In write mode, the same tool can also run a narrow set of local Git operations.
## Configure Git access
```ts [server/agents/reviewer.ts]
import { defineAgent } from 'vite-hub/agent'
import { git } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
git({ mode: 'read' }),
],
})
```
## How Git access works
`shell` accepts one `git` command without shell composition.
Read mode allows source-history commands such as `status`, `diff`, `log`, `show`, `grep`, and ref inspection.
Write mode supports only `fetch`, `checkout`, and `switch` on a clean working tree.
It blocks commit, push, reset, rebase, tag, arbitrary remote URLs, shell composition, and path escapes outside the Workspace.
Supported write commands are allowed by default after the developer enables write mode. An explicit policy can require approval or deny them, but it cannot enable blocked commands.
## Requirements
`git()` requires a Workspace primitive that can start a Workspace Session.
The Workspace requirement is write-mode because ViteHub may need session-local Git state even when the exposed tool mode is read-only.
## Driver support
| Agent Driver | Support |
| ----------------- | --------------------------------------------------------------------------- |
| Model-backed | Receives one controlled Git `shell` tool. |
| Provider-backed | Receives the controlled Git `shell` tool through the provider MCP bridge. |
| Custom-run-backed | Can use the Workspace Session directly and may inspect Capability metadata. |
## Options
| Option | Type | Default | Description |
| ----------------- | ------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------- |
| `mode` | `"read" | "write"` | `"read"` | Allows local `fetch`, `checkout`, and `switch` commands through `shell` when set to `"write"`. |
| `maxOutputLength` | `number` | `Infinity` | Maximum stdout/stderr characters returned per command. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for write-mode `shell` commands. Read-only Git commands remain allowed. |
| `timeout` | `number` | `60000` | Execution timeout in milliseconds passed to Workspace Session Git commands. |
## Verify Git access
Run `vitehub agent info --agent --json` and inspect the resolved tool list.
Run `git status --short` through `shell`, then verify unsupported commands such as `git push` are rejected.
## Related pages
- [Workspace](https://vitehub.dev/docs/server-primitives/workspace)
- [Workspace shell](https://vitehub.dev/docs/capabilities/workspace-shell)
# Gmail
`gmail()` gives an Agent structured Gmail search and authorization tools. Draft mode adds draft creation, but the Capability never exposes a send tool or the underlying `gog` executable.
Use [`email()`](https://vitehub.dev/docs/capabilities/email) for application-owned transactional email through the Email primitive. Use `gmail()` for an operator-owned Gmail account and structured Gmail tools.
## Configure the Agent
Install [`gog`](https://github.com/openclaw/gogcli){rel=""nofollow""} on the Workspace Session host, configure its Google OAuth client, and keep its authentication state under the service account. The application owns this setup; the Capability never accepts OAuth client secrets or keyring passwords as tool input.
```ts [server/agents/inbox.ts]
import { defineAgent } from 'vite-hub/agent'
import { gmail } from 'vite-hub/agent/capabilities'
export default defineAgent({
capabilities: [
gmail({ mode: 'draft' }),
],
driver: 'codex',
workspace: {
mode: 'write',
},
})
```
Current [`gog` path conventions](https://github.com/openclaw/gogcli/blob/main/docs/paths.md){rel=""nofollow""} keep configuration in `.config/gogcli` and OAuth metadata plus file-keyring entries in `.local/share/gogcli` on Linux, so persist both directories for the service account. Supply `GOG_KEYRING_PASSWORD` through Server Env or the deployment secret store. Follow the [`gog` OAuth client setup](https://github.com/openclaw/gogcli/blob/main/docs/quickstart.md){rel=""nofollow""} before the first authorization attempt.
## Choose a mode
Read mode is the default and exposes two tools:
| Tool | Behavior |
| -------------- | -------------------------------------------------------------------------- |
| `gmail_auth` | Starts or completes remote authorization for one Gmail address. |
| `gmail_search` | Searches or lists Gmail threads. It does not retrieve full message bodies. |
Draft mode exposes the same tools plus `gmail_draft`, which creates an unsent draft with `to`, optional `cc` and `bcc`, a subject, and a plain-text body.
```ts
gmail()
gmail({ mode: 'draft' })
```
`gmail()` has no send mode. Search commands run with read-only and no-send controls. Draft creation runs with `--gmail-no-send`, and no Capability-owned tool can send the resulting draft.
This limits the tools exposed to the Agent, not the credential itself. If sending must be impossible, isolate the credential behind a runtime or provider policy that cannot send. `gmail()` does not provide that isolation.
## Complete authorization
Gmail tools return authorization as structured states instead of asking the user to run shell commands:
| Status | Next action |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `account_required` | Ask which Gmail address to use, then retry the original tool with `account`. |
| `authorization_required` | Send `authorizationUrl` to the user. Google may redirect to a localhost page that does not load; collect the full browser address-bar URL. |
| `connected` | Retry the original Gmail tool. |
| `configuration_required` | The operator must configure the OAuth client using `setupUrl`. Do not request secrets in chat. |
Complete a pending redirect through `gmail_auth`:
```ts [Agent tool call]
await gmail_auth({
action: 'complete',
account: 'owner@example.com',
redirectUrl: 'http://localhost:8080/?code=...&state=...',
})
```
The Capability accepts only an HTTP loopback URL with both `code` and `state`. It exchanges the URL on the Workspace Session host and does not return it in the result.
## Runtime requirements
`gmail()` requires all of the following:
- An explicit Workspace with `workspace.mode: 'write'`, because each structured Gmail call opens a writable Workspace Session.
- A Workspace Session host with command execution and `gog` available.
- Operator-owned OAuth client configuration and persistent service-account state.
Each underlying `gog` command opens its own Workspace Session and closes the Session on success or failure. Gmail search results remain untrusted external content and the contributed `skills/gmail/SKILL.md` tells the Agent to treat them as data, not instructions.
Draft authorization may grant the Gmail account scope that `gog` needs to create drafts. The no-send contract applies only to the Capability-owned tools and their command flags.
## Verify Gmail access
Run `vitehub agent info --agent --json` and inspect the resolved tools. Read mode lists only `gmail_auth` and `gmail_search`. Draft mode also lists `gmail_draft`.
Start with a test Gmail account. Search for `in:inbox`, create a draft in draft mode, and verify in Gmail that the message remains in Drafts and was not sent.
## Options
| Option | Type | Default | Description |
| ------ | ------------------ | -------- | ------------------------------------------------------------------------------------- |
| `mode` | `"read" | "draft"` | `"read"` | Exposes search and authorization tools, with draft creation added only in draft mode. |
## Related pages
- [Workspace shell](https://vitehub.dev/docs/capabilities/workspace-shell)
- [Email Capability](https://vitehub.dev/docs/capabilities/email)
# Capabilities
Capabilities give an Agent a named ability through `defineAgent({ capabilities })`.
They can add tools, triggers, input processing, output metadata, and checks that run before an invocation.
A Capability is not a server primitive.
Server primitives give trusted app code authority.
Capabilities decide which operations an Agent Invocation can use.
## Capability lifecycle
ViteHub applies Capabilities in the order listed or returned by the Agent Definition.
It validates duplicate ids, checks runtime requirements, applies Capability Trigger Contributions, and then runs configure, prepare, bind, input, resolve, and output phases for each invocation.
`access()` is the only official Capability with a fixed position rule.
Place it first so later Capabilities receive the restricted Workspace and tool access.
## Attach a Capability
Import official factories from `@vite-hub/agent/capabilities`.
Import the factory from the Capabilities entry point:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
workspaceShell({ mode: 'read' }),
],
})
```
Attaching a Capability opts the Agent into that ability. Model-facing tool policy defaults to `allow`. Set `policy: 'require-approval'` or `policy: 'deny'` when a tool needs another runtime check. Modes, scopes, allowlists, requirements, and input validation still restrict the operation before policy runs.
Use a callback when invocation context decides the Agent Definition's Capability list. ViteHub calls it once after resolving the Agent Invoker and before Capability setup; Capabilities contributed by the active Channel still compose normally.
```ts [server/agents/support.ts]
export default defineAgent({
driver: { model },
capabilities: ({ actor }) => [
workspaceShell({ mode: 'read' }),
...(actor.meta?.support === true ? [internalDiagnostics] : []),
],
})
```
Return only invocation-scoped behavior from the callback. Capabilities that contribute Agent Triggers, chat admission, or static Workspace Sources must stay in a static list because ViteHub registers those contributions before an invocation exists.
## Use an Eve extension
ViteHub detects compatible Eve extension packages in a static Capability list and compiles their tools into a Capability. Install the extension, then use its existing factory and options:
```ts [server/agents/reviewer.ts]
import github from '@github-tools/eve-extension'
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: { model },
capabilities: [
github({ preset: 'code-review' }),
],
})
```
The Vite plugin reads the package's Eve manifest and fails the build when a declared contract version is unsupported. The first bridge supports one mount per extension package, direct default-import factory calls, static and `session.started` tools, tool schemas and output conversion, and Eve's `always`, `never`, and `once` approval modes. ViteHub maps `session.started` to the start of each Agent Invocation and uses the invocation's `runId` as the Eve session ID, so every invocation resolves a fresh tool set without relying on process-local state. The built-in HTTP chat route persists pending approvals in its configured Chat state, reconstructs the authoritative tool call server-side, and consumes each response once under a session lock. Client-supplied chat history never creates approval authority. Unsupported dynamic events fail when ViteHub resolves the extension's tools for an Agent Invocation.
This bridge is not yet a complete Eve runtime. Tool and approval contexts do not support `getSandbox()`, `getSkill()`, `getToken()`, or `requireAuth()`; using one throws at runtime. Session authentication is unavailable and turn sequence metadata is not preserved. ViteHub Agent Invocations are not Eve durable sessions, so extensions that depend on one `session.started` resolution spanning several invocations are not supported yet.
## What Capabilities can contribute
| Contribution | What it changes |
| --------------- | --------------------------------------------------------------------------------------------------------- |
| Requirements | Primitive, Workspace mode, Workspace path, or policy checks that must pass before the Capability applies. |
| Tools | Model-facing operations exposed only to compatible Agent Drivers. |
| Provider tools | Provider-native tool requests, such as model web search mode. |
| Agent Triggers | Product events that start Agent Invocations through the Agent Package trigger API. |
| Input behavior | Pre-invocation input transforms, transcription, decisions, gates, and rate limits. |
| Output behavior | Stream renderers, finish extensions, usage records, titles, and summaries. |
| Metadata | Inspectable configuration for runtime diagnostics and CLI inspection. |
Capability metadata appears under the Capability id in Agent inspection output. ViteHub keeps JSON values, sorts object keys and Capability ids, drops unsupported or cyclic values, and redacts keys shaped like auth, API keys, credentials, passwords, secrets, or tokens. Metadata must describe configuration or an explicit check result; it must not include Env values, authentication material, or credentials.
Use `defineCapability({ finish })` for metadata read by evals, finish hooks, or channel delivery code after an invocation.
Agent Evals expose those values through `observation.extensions.get(capabilityId)` and the `hasCapabilityExtension(capabilityId)` scorer.
## Driver support
A model-backed Agent Driver can consume model-facing tools and Provider Tool contributions.
A provider-backed Agent Driver receives Agent tools through the private MCP bridge and scoped Workspace behavior. Provider-backed Drivers do not support model-specific Capability Provider Tool contributions such as `webSearch({ mode: 'model' })`.
A custom-run-backed Agent Driver receives prepared input and invocation context; the `driver.run` implementation decides which Capability outputs to read.
Free-form guidance about when and why to use a Capability belongs in Agent Driver Instructions or deterministic imported instruction Markdown. Tool descriptions and schemas remain part of the model-facing tool contract.
## Next steps
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
- [Agent definitions](https://vitehub.dev/docs/agents/agent-definitions)
# Input commands
`inputCommands()` adds command parsing for explicit user input before the main Agent Invocation runs.
Use it for commands that transform or enrich the user's prompt, not for host UI state or shell execution.
The Capability scans the latest prompt or user message for configured Input Commands.
Each command can replace text, update the Agent Run Input, or add invocation context before model execution.
Commands that produce model-facing text must return it. Accepting a command without handler text removes the matched text.
## Configure input commands
Define lowercase command names. Add a description to include the command's purpose in CLI and inspection output.
The default trigger is `/`.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { inputCommands } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
inputCommands({
commands: {
docs: {
description: 'Add documentation context to the request.',
call: ({ args }) => `Use documentation context for: ${args}`,
},
},
}),
],
})
```
## How input commands work
`inputCommands()` runs during the input phase.
It finds command invocations in the latest user text, calls the matching command handler, and updates the Agent Run Input before other model-facing behavior consumes it.
Commands without a handler are accepted and removed from model input; they do not implicitly pass arguments or command names through as prompts.
Command `agent:input` hooks run after the command updates the input and before the Agent Driver runs.
Command `agent:finish` hooks run for completed and failed Agent Invocations.
The Capability records command names and descriptions in metadata.
It stops command expansion when no configured command remains.
## Requirements
Command names must be lowercase stable identifiers.
The trigger must be a non-empty string without whitespace.
Input Commands are Capability concerns.
Host Commands that change chat, session, UI, or product state belong outside this Capability.
## Driver support
| Agent Driver | Support |
| ----------------- | ----------------------------------------------------------------------------------------- |
| Model-backed | Receives the transformed prompt, messages, or context before model execution. |
| Provider-backed | Receives the transformed Agent Run Input before provider execution. |
| Custom-run-backed | Receives the transformed Agent Run Input; `driver.run` decides how to use context values. |
## Verify input commands
Run an invocation with the configured command text.
Inspect the final Agent Run Input and confirm the command text was replaced or the expected context value was added before the Agent Driver ran.
Check Agent inspection metadata for the `inputCommands` Capability and its command descriptions.
## Options
| Option | Type | Default | Description |
| ------------------------ | ----------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
| `commands` | `Record` | required | Command map keyed by lowercase stable command names. |
| `id` | `string` | `"inputCommands"` | Capability id. |
| `trigger` | `string` | `"/"` | Non-whitespace command prefix. |
| `commands.*.description` | `string` | none | Optional command description for metadata and inspection. |
| `commands.*.call` | `(input) => AgentRunInput | Response | string | void` | remove command text | Handler that accepts, rejects, transforms, or enriches invocation input. |
| `commands.*.channels` | `string[]` | all channels | Optional configured Channel ID allowlist. |
| `commands.*.hooks` | `{ 'agent:input'?, 'agent:finish'? }` | none | Command-scoped lifecycle hooks with `ctx.message.reply/update/react` delivery primitives. |
## Related pages
- [chatSummary()](https://vitehub.dev/docs/capabilities/chat-summary)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
# KV
`kv()` adds model-facing tools for a configured ViteHub KV primitive.
It exposes read tools by default and edit tools only in write mode.
The Capability contributes `kv_read` for exact-key reads or prefix key listing.
When configured with write mode, it also contributes `kv_edit` for putting or deleting one key.
## Configure KV access
Attach KV in read mode until the product needs model-facing writes.
The KV primitive must already be configured by the app.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { kv } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
kv({ mode: 'read' }),
],
})
```
## How KV access works
ViteHub selects the configured KV store and exposes the KV tools.
Read mode supports one exact key or one prefix per tool call.
Write mode adds a put/delete tool and allows its normal operations by default.
## Requirements
`kv()` requires a configured `kv` primitive.
Named store selection requires the KV primitive to expose store selection.
Writes require explicit write mode.
Set `policy: 'require-approval'` or `policy: 'deny'` when the product needs an additional gate.
## Driver support
| Agent Driver | Support |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| Model-backed | Receives `kv_read` and, in write mode, `kv_edit`. |
| Provider-backed | Receives KV tools through the provider MCP bridge. |
| Custom-run-backed | The configured primitive is available through runtime context; `driver.run` decides how to use it. |
## Verify KV access
Run `vitehub agent info --agent --json` and inspect the resolved tool list.
Confirm that read mode shows only `kv_read`. Write mode also lists `kv_edit` with the configured policy.
Run one invocation against a missing KV primitive during development.
Confirm that the Capability fails before it exposes tools.
## Options
| Option | Type | Default | Description |
| -------- | ------------------------------------ | ------------- | ------------------------------------------------------------------ |
| `mode` | `"read" | "write"` | `"read"` | Adds `kv_edit` when set to `"write"`. |
| `store` | `string` | default store | Selects a named KV store when the KV primitive supports `store()`. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for `kv_edit`. |
## Related pages
- [KV primitive](https://vitehub.dev/docs/server-primitives/kv)
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
# LLM gate
`llmGate()` adds a pre-invocation model decision that classifies a request into allow or reject categories.
It can stop the Agent Invocation before the main Agent Driver runs.
The Capability asks a model to choose one configured allow or reject category.
It records the decision as an Agent Invocation Context Value, exposes it as a finish extension, and throws `ViteHubError` with code `LLM_GATE_REJECTED` when the selected category is rejected.
## Configure the gate
Define allow and reject categories with stable keys.
Set `message` to return a product-specific rejection message from the host.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { llmGate } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
llmGate({
allow: {
support: 'Support request the Agent can answer.',
},
reject: {
unrelated: 'Request unrelated to support.',
},
}),
],
})
```
## How the gate works
`llmGate()` runs during the input phase.
It resolves a model, renders a classifier prompt from the latest user text and configured categories, validates the structured output, and stores the decision in invocation context.
When the decision rejects the request, ViteHub throws `ViteHubError` with code `LLM_GATE_REJECTED`; the HTTP handler maps that code to `403`.
## Requirements
`llmGate()` requires at least one allow category and one reject category.
Category keys must be stable identifiers.
The Capability requires either an explicit model option or an Agent model resolver available to Capabilities.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------ |
| Model-backed | Runs the pre-invocation gate before model execution. |
| Provider-backed | Runs the pre-invocation gate before provider execution when a model resolver is available. |
| Custom-run-backed | Runs before `driver.run`; rejected requests do not reach custom code. |
## Verify the gate
Run one allowed and one rejected invocation.
Inspect the context value for `llm-gate` or your custom id, then verify rejected requests stop with code `LLM_GATE_REJECTED`.
Check that the gate does not attach, remove, or grant Capabilities.
It records a decision; later behavior must read that decision explicitly.
## Options
| Option | Type | Default | Description |
| --------- | ------------------------ | ------------------------- | ------------------------------------------------------------- |
| `allow` | `Record` | required | Allowed categories. |
| `reject` | `Record` | required | Rejected categories. |
| `history` | `boolean | number` | `false` | Include recent conversation history in the classifier prompt. |
| `id` | `string` | `"llm-gate"` | Capability id and invocation context key. |
| `message` | `string | function` | default rejection message | Error message when the gate rejects. |
| `model` | `AgentModelResolver` | Agent model | Model used for the pre-invocation decision. |
| `prompt` | `string` | generated | Additional classifier prompt text. |
## Related pages
- [llmRoute()](https://vitehub.dev/docs/capabilities/llm-route)
- [rateLimit()](https://vitehub.dev/docs/capabilities/rate-limit)
# LLM route
`llmRoute()` adds a pre-invocation model decision that chooses one developer-defined route.
It records the chosen route as an Agent Invocation Context Value and does not apply route effects by itself.
The Capability asks a model to select exactly one configured choice.
It can include recent conversation history, records the decision under a stable id, and exposes the decision as a finish extension.
## Configure routing
Define stable choice keys with short descriptions.
Later callbacks can read the recorded context value and decide how to use the route.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { llmRoute } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
llmRoute({
choices: {
billing: 'Billing and invoice requests.',
technical: 'Technical troubleshooting requests.',
},
}),
],
})
```
## How routing works
`llmRoute()` runs during the input phase before the main Agent Driver.
It resolves a model, renders a decision prompt from the latest user text and configured choices, validates the structured output, and stores the result in invocation context.
The default context id is `llm-route`.
Duplicate writers for the same invocation context value fail early.
## Requirements
`llmRoute()` requires at least one choice.
Choice keys must be stable identifiers.
The Capability requires either an explicit model option or an Agent model resolver available to Capabilities.
## Driver support
| Agent Driver | Support |
| ----------------- | ---------------------------------------------------------------------------------------------- |
| Model-backed | Runs the pre-invocation decision and records the route before model execution. |
| Provider-backed | Runs the pre-invocation decision before provider execution when a model resolver is available. |
| Custom-run-backed | Records the decision before `driver.run`; custom code decides how to use it. |
## Verify routing
Run one invocation and inspect the context value for `llm-route` or your custom id.
Confirm that the value includes `choice`. It may also include confidence or a reason.
Add a test case for an invalid model response if you provide a custom model wrapper.
Confirm that the Capability rejects choices outside the configured map.
## Options
| Option | Type | Default | Description |
| --------- | ------------------------------------------------------------------- | ------------- | ------------------------------------------------------------- |
| `choices` | `Record` | required | Developer-defined route choices. |
| `history` | `boolean | number` | `false` | Include recent conversation history in the classifier prompt. |
| `id` | `string` | `"llm-route"` | Capability id and invocation context key. |
| `model` | `AgentModelResolver` | Agent model | Model used for the pre-invocation decision. |
| `prompt` | `string` | generated | Additional classifier prompt text. |
## Related pages
- [llmGate()](https://vitehub.dev/docs/capabilities/llm-gate)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
# MCP
`mcp()` connects an Agent to external Model Context Protocol servers.
It resolves each configured MCP Server and exposes its tools as model-facing Agent tools.
The Capability normalizes MCP tool names with the server name, attaches sanitized MCP metadata, and closes MCP clients created from configs or resolvers after the invocation.
Static direct clients stay application-owned so they can be reused across invocations.
Tool names, descriptions, and schemas stay with the MCP tool contract. Put broader guidance about when to use an MCP server in Agent Driver Instructions.
An optional approved fingerprint map can block added or changed tool definitions before they reach an Agent Driver.
## Configure MCP servers
Pass a server map.
Each entry can be a static direct MCP client borrowed from the application, or a client config or resolver whose resolved client is owned by the Agent Invocation.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { mcp } from 'vite-hub/agent/capabilities'
import { docsMcpServer } from '../mcp/docs'
export default defineAgent({
driver: { model },
capabilities: [
mcp({
servers: {
docs: docsMcpServer,
},
}),
],
})
```
## How MCP connections work
During resolution, `mcp()` connects to each configured MCP Server and asks for its tool set.
ViteHub prefixes normalized tool names with `mcp__` and rejects duplicate normalized names.
Pass a resolver or client config for an invocation-owned connection, or a static direct client when the application owns its lifetime.
The Capability redacts secret-shaped metadata keys before exposing MCP metadata.
## Pin tool definitions
An MCP Server can return a different tool description, title, or input schema after its tools were reviewed.
Use `fingerprintTools()` during a trusted review step, persist the approved result in application code or configuration, then pass it to `integrity` under the matching server name.
```ts [scripts/review-docs-mcp.ts]
import { fingerprintTools } from 'ai'
const approved = await fingerprintTools(await client.tools())
console.log(JSON.stringify(approved, null, 2))
```
Review that output before saving it. Do not generate the baseline during normal application startup, because that would trust whichever definitions the server returns first.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { mcp } from 'vite-hub/agent/capabilities'
import { docsMcpServer } from '../mcp/docs'
import { docsToolFingerprints } from '../mcp/docs-tool-fingerprints'
export default defineAgent({
driver: { model },
capabilities: [
mcp({
integrity: {
docs: docsToolFingerprints,
},
servers: {
docs: docsMcpServer,
},
}),
],
})
```
ViteHub fingerprints each configured server independently before it normalizes or contributes tools.
Added and changed definitions fail Capability resolution; removal-only changes remain allowed because MCP tool lists can narrow by feature or authorization.
Drift errors include the server name and the added, changed, and removed original tool names.
Fingerprints cover tool names, string descriptions, titles, and resolved input schemas.
They do not prove that the first reviewed definition was safe or detect changed remote behavior behind an unchanged definition.
## Requirements
`mcp({ servers })` requires a server map. Each configured entry must resolve to an MCP client or MCP client configuration.
MCP client configuration uses the optional `@ai-sdk/mcp` runtime package when ViteHub creates the client from config.
Tool integrity requires `ai` 7.0.19 or newer only when `integrity` is configured.
The external MCP Server owns its own credentials, availability, and tool behavior.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Model-backed | Receives normalized MCP tools. |
| Provider-backed | Receives normalized MCP tools through the provider MCP bridge; runtime connection and cleanup still run around the invocation. |
| Custom-run-backed | Receives prepared context; `driver.run` decides whether to call MCP clients or tools through custom code. |
## Verify MCP connections
Successful invocations expose normalized MCP tools through `agent info` and stream tool steps through `agent dev`.
Confirm that MCP tools use normalized names such as `mcp_docs_search`.
Integrity checks run during invocation resolution. Static Agent inspection metadata does not connect to MCP Servers or claim that a configured baseline currently matches.
Run one invocation with a duplicate normalized tool name during development.
Confirm that the Capability fails before model execution.
## Options
| Option | Type | Default | Description |
| ----------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `integrity` | `Record` | none | Approved AI SDK tool fingerprints keyed by configured server name. Blocks added or changed definitions. |
| `servers` | `Record` | required | MCP clients, client configs, or resolvers keyed by server name. |
Cover MCP usage guidance in Agent Driver Instructions with explicit Capability coverage blocks. Keep MCP tool descriptions with the MCP Server because they are structured tool contracts.
## Related pages
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
- [AI SDK MCP tool-definition drift](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools#detecting-tool-definition-drift-rug-pull){rel=""nofollow""}
# Memory
`memory()` adds scoped durable records that an Agent can search, read, remember, or delete through configured Memory Stores.
Memory is explicit Agent behavior and is not the same as Chat History.
The Capability exposes `memory_search` and `memory_read`, plus remember and delete tools when a store opts into tool writes.
Each store owns its adapter, scope, allowed kinds, read behavior, and write policy.
## Configure memory
Configure at least one store with an explicit scope.
The workspace JSONL helper stores records inside the Agent Workspace.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { memory, workspaceJsonlMemoryStore } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
memory({
stores: {
agent: {
adapter: workspaceJsonlMemoryStore(),
scope: { agent: 'support' },
},
},
}),
],
})
```
## How memory works
During resolve, `memory()` creates read tools for stores that allow reading and write tools for stores that opt into tool writes.
Write tools add provenance from the current Agent Invocation and allow writes by default after a store opts into `write.mode: 'tool'`.
Set the store policy to `require-approval` or `deny` when writes need an additional gate.
## Requirements
`memory()` requires a store map.
Each store requires an adapter and an explicit non-empty scope.
`workspaceJsonlMemoryStore()` requires a Workspace.
It requires writable Workspace access when the Agent creates, supersedes, or deletes memory records.
## Driver support
| Agent Driver | Support |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives the configured memory tools. |
| Provider-backed | Receives the configured memory tools through the provider MCP bridge. |
| Custom-run-backed | Receives prepared context and can call store adapters from custom code when the application exposes them. |
## Verify memory
Inspect Agent inspection metadata for the configured memory stores.
Inspect the tool list and confirm write tools appear only for stores with `write.mode: 'tool'`.
For the workspace JSONL store, inspect the configured Workspace file and verify records include scope and provenance.
## Options
| Option | Type | Default | Description |
| ---------------------------- | ----------------------------------------- | --------- | -------------------------------------------------- |
| `stores` | `Record` | required | Named Memory Stores available to the Agent. |
| `stores.*.adapter` | `MemoryStoreAdapter | MemoryStoreFactory` | required | Store implementation. |
| `stores.*.scope` | `MemoryScope | function` | required | Scope attached to all operations for that store. |
| `stores.*.allowKinds` | `MemoryKind[]` | all kinds | Allowed memory kinds for the store. |
| `stores.*.read.tools.search` | `boolean` | `true` | Expose memory search. |
| `stores.*.read.tools.read` | `boolean` | `true` | Expose exact memory read. |
| `stores.*.write.mode` | `"off" | "tool"` | `"off"` | Expose remember/delete tools when set to `"tool"`. |
| `stores.*.write.policy` | `AgentToolPolicyDecision` | `"allow"` | Policy for write tools. |
### Workspace JSONL store
`workspaceJsonlMemoryStore()` persists records as append-only JSONL inside the Agent Workspace.
| Option | Type | Default | Description |
| ------ | -------- | ----------------------- | ----------------------------------- |
| `path` | `string` | `"memory/memory.jsonl"` | Workspace-relative JSONL file path. |
Cover Memory usage guidance in Agent Driver Instructions with explicit Capability coverage blocks. Keep memory tool descriptions with the tool definitions because they are structured tool contracts.
## Related pages
- [chat()](https://vitehub.dev/docs/capabilities/chat)
- [Workspace primitive](https://vitehub.dev/docs/server-primitives/workspace)
# Official capabilities
ViteHub exports its built-in Capabilities from `@vite-hub/agent/capabilities`.
Choose a Capability by what the Agent needs to do. Each linked page shows how to configure it, what the Agent receives, and how to verify it.
```ts [server/agents/support.ts]
import {
access,
blob,
browser,
chat,
chatSummary,
title,
db,
email,
fetch,
git,
gmail,
inputCommands,
kv,
llmGate,
llmRoute,
mcp,
memory,
openapi,
otlp,
papercuts,
progressSummary,
rateLimit,
repositoryHost,
repositoryHostContext,
sandbox,
schedule,
skills,
subagents,
transcribe,
usage,
cost,
diagnostics,
webSearch,
workspaceShell,
} from 'vite-hub/agent/capabilities'
```
## Catalog
### Invocation
| Ability | Capability | Use it when |
| ----------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Invocation access | [`access()`](https://vitehub.dev/docs/capabilities/access) | Narrow chat admission or Workspace access from trusted invocation identity. |
| Chat behavior | [`chat()`](https://vitehub.dev/docs/capabilities/chat) | Start Agent Invocations from chat messages and manage Chat History. |
| Input commands | [`inputCommands()`](https://vitehub.dev/docs/capabilities/input-commands) | Transform command-shaped user input before the Agent runs. |
| Subagents | [`subagents()`](https://vitehub.dev/docs/capabilities/subagents) | Delegate bounded work to named Agent Definitions through tools. |
### Workspace
| Ability | Capability | Use it when |
| ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Browser automation | [`browser()`](https://vitehub.dev/docs/capabilities/browser) | A Provider Agent needs headless browser guidance and the `agent-browser` CLI is installed. |
| Workspace files | [`workspaceShell()`](https://vitehub.dev/docs/capabilities/workspace-shell) | Inspect or edit Workspace files, or run configured Workspace commands. |
| Git source history | [`git()`](https://vitehub.dev/docs/capabilities/git) | The Agent needs bounded Git source-history inspection or local Workspace Session git state selection. |
| Skills file | [`skills()`](https://vitehub.dev/docs/capabilities/skills) | The Agent requires a Workspace skill file at invocation time. |
| Durable memory | [`memory()`](https://vitehub.dev/docs/capabilities/memory) | The Agent needs scoped durable records across invocations. |
### Runtime primitives
| Ability | Capability | Use it when |
| ----------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| KV storage | [`kv()`](https://vitehub.dev/docs/capabilities/kv) | The Agent needs scoped key-value read or edit tools. |
| Blob storage | [`blob()`](https://vitehub.dev/docs/capabilities/blob) | The Agent needs scoped object read or edit tools. |
| Database | [`db()`](https://vitehub.dev/docs/capabilities/db) | The Agent needs guarded SQL query, schema, or mutation tools. |
| Email | [`email()`](https://vitehub.dev/docs/capabilities/email) | Send authorized plain-text messages through the configured Email primitive. |
| Sandbox execution | [`sandbox()`](https://vitehub.dev/docs/capabilities/sandbox) | The Agent may run an allowlisted executable in an isolated runtime. |
| Schedules | [`schedule()`](https://vitehub.dev/docs/capabilities/schedule) | The Agent declares scheduled invocations or manages Runtime Schedules through tools. |
| OTLP telemetry | [`otlp()`](https://vitehub.dev/docs/capabilities/otlp) | Live Agent Invocation events and completed traces should be exported to an OpenTelemetry receiver. |
| Operational diagnostics | [`diagnostics()`](https://vitehub.dev/docs/capabilities/diagnostics) | Invocation outcomes and scoped runtime resource observations should go to an application-owned reporter. |
### External context
| Ability | Capability | Use it when |
| ----------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Repository host | [`repositoryHost()`](https://vitehub.dev/docs/capabilities/repository-host) | The Agent needs provider-hosted repository, Change Request, issue, comment, check, or status data through a configured Repository Host client. |
| Repository host context | [`repositoryHostContext()`](https://vitehub.dev/docs/capabilities/repository-host-context) | Read issue or Change Request data identified by a trigger or host. |
| MCP servers | [`mcp()`](https://vitehub.dev/docs/capabilities/mcp) | Add tools from external MCP servers to the Agent. |
| Web search | [`webSearch()`](https://vitehub.dev/docs/capabilities/web-search) | The Agent needs model web search or normalized web search/read tools. |
| Fetch tools | [`fetch()`](https://vitehub.dev/docs/capabilities/fetch) | The Agent needs named HTTP tools for developer-approved endpoints. |
| OpenAPI tools | [`openapi()`](https://vitehub.dev/docs/capabilities/openapi) | The Agent needs a selected OpenAPI operation catalog exposed as bounded HTTP tools or a generated Capability CLI. |
| Transcription | [`transcribe()`](https://vitehub.dev/docs/capabilities/transcribe) | Turn audio input into text before model execution. |
| Gmail | [`gmail()`](https://vitehub.dev/docs/capabilities/gmail) | Search Gmail or create unsent drafts through structured tools. |
### Decisions and output
| Ability | Capability | Use it when |
| ---------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| LLM routing | [`llmRoute()`](https://vitehub.dev/docs/capabilities/llm-route) | Choose one developer-defined route with a model before the invocation. |
| LLM gate | [`llmGate()`](https://vitehub.dev/docs/capabilities/llm-gate) | Allow or reject a request with a model before the invocation. |
| Rate limit | [`rateLimit()`](https://vitehub.dev/docs/capabilities/rate-limit) | Consume a trusted invocation budget before the Agent runs. |
| Title | [`title()`](https://vitehub.dev/docs/capabilities/title) | Generate a title for Agent output, finish extensions, or Channel threads. |
| Chat summary | [`chatSummary()`](https://vitehub.dev/docs/capabilities/chat-summary) | Replace a summary command with a conversation summary. |
| Progress summary | [`progressSummary()`](https://vitehub.dev/docs/capabilities/progress-summary) | Summarize current reasoning and tool activity while an Agent streams. |
| Papercut reports | [`papercuts()`](https://vitehub.dev/docs/capabilities/papercuts) | Report small runtime or developer-experience problems to application code. |
| Usage | [`usage()`](https://vitehub.dev/docs/capabilities/usage) | Request provider usage metadata and expose a normalized Agent Usage Record. |
| Cost | [`cost()`](https://vitehub.dev/docs/capabilities/cost) | Add exact and display-ready USD cost to Agent Usage Records. |
## Next steps
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
- [Capabilities API](https://vitehub.dev/docs/capabilities)
- [Agent definitions](https://vitehub.dev/docs/agents/agent-definitions)
# OpenAPI
`openapi()` turns selected OpenAPI `operationId`s into bounded HTTP operations.
Use it to generate Agent tools from a known API contract instead of writing one `fetch()` tool per endpoint.
Attaching the Capability is the opt-in.
Channel, customer, or tenant admission belongs in `access()`, Agent Trigger routing, or separate Agent Definitions, not in `openapi()`.
## Configure an OpenAPI Capability
Add `openapi()` to the Agent when every invocation needs the API, or to one [Channel's capabilities](https://vitehub.dev/docs/agents/channels#scope-abilities-to-one-channel) when only that Channel needs it.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { access, openapi } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
access({
chat: {
resolve({ invoker }) {
return invoker.kind === 'portal'
},
},
}),
openapi({
spec: 'https://portal.example.com/_openapi.json',
operations: ['portalProductSearch', 'portalPurchaseOrders'],
}),
],
})
```
By default, `openapi()` creates one model-facing tool per selected operation.
Each tool uses the OpenAPI request path, query parameters, request body schema, operation summary, and response parser.
When `cli` is set, ViteHub replaces those operation tools with one Capability CLI tool.
The generated CLI has one subcommand per selected operation and can also run through `vitehub agent dev --cli`.
## Select operations
Always pass the operation allowlist directly.
ViteHub does not expose every operation by default.
```ts [server/agents/support.ts]
openapi({
spec: 'https://billing.example.com/openapi.json',
operations: ['billingListCustomers', 'billingGetInvoice'],
})
```
Unsupported HTTP methods are ignored unless the operation is selected.
In v1, selected operations can use `GET`, `HEAD`, or `POST`.
## Configure requests
ViteHub derives the request server from the OpenAPI document by default:
1. `servers[0].url`
2. the OpenAPI spec URL origin
Use `hooks.request` for runtime auth, cookies, tenant values, body additions, query additions, and timeout changes.
The hook receives the selected operation, Agent Capability context, visible model input, and a mutable draft request.
When the hook owns OpenAPI fields such as tenant path params or runtime body tokens, declare them in `provides`; ViteHub removes those fields from the model and generated CLI schemas, strips them from caller input, then validates the final prepared request after the hook runs.
```ts [server/agents/support.ts]
openapi({
spec: 'https://portal.example.com/_openapi.json',
operations: ['portalProductSearch', 'portalPurchaseOrders'],
hooks: {
request: {
provides: {
body: ['cubeToken'],
path: ['tenantId'],
},
async handler({ context, operation, request }) {
const session = context.get<{
cubeToken: string
tenantId: string
token: string
}>('portalSession')
if (!session) throw new Error('Portal session missing.')
request.headers.set('authorization', `Bearer ${session.token}`)
request.path.tenantId = session.tenantId
if (operation.id === 'portalPurchaseOrders') {
request.body = {
...(request.body as Record | undefined),
cubeToken: session.cubeToken,
}
}
},
},
},
})
```
ViteHub still keeps caller-owned required fields in the model and generated CLI schemas. If the hook only provides `tenantId`, another required path param such as `orderId` remains required from the caller.
For a broken, missing, or environment-neutral `servers` entry, use `server` as an override escape hatch.
`server` can also be a callback when the override comes from the current Agent Invocation context.
```ts [server/agents/support.ts]
openapi({
spec: './openapi.json',
server: 'https://preview.example.com/api',
operations: ['listCustomers'],
})
```
## Generate a Capability CLI
Use `cli` to give agents and developers the same operation catalog as commands.
```ts [server/agents/support.ts]
openapi({
spec: 'https://billing.example.com/openapi.json',
operations: ['billingListCustomers', 'billingGetInvoice'],
cli: {
name: 'billing',
description: 'Inspect live billing API data.',
},
})
```
`cli` can also resolve from the current Agent Invocation. Return `false` or `undefined` to omit the generated CLI for that invocation without detaching the OpenAPI Capability or changing its lifecycle.
```ts [server/agents/support.ts]
openapi({
spec: 'https://portal.example.com/_openapi.json',
operations: ['portalProductSearch', 'portalPurchaseOrders'],
cli: ({ run }) => run?.channelId === 'portal'
? {
name: 'portal-api',
description: 'Inspect live Portal data.',
}
: false,
})
```
Treat Channel metadata as an availability hint, not authorization. Check trusted request, Agent Actor, or `access()` evidence before privileged API calls.
Run the generated CLI through the Agent Dev Loop.
Agents expose generated Capability CLI Contributions by default. Use `defineAgent({ cli: { capabilities: false } })` to attach the OpenAPI Capability without exposing its CLI.
```bash [Terminal]
pnpm vitehub agent dev --url http://localhost:3000 --agent support --cli billing -- list-customers --json
```
## Shape responses
Use `transformResponse` when the raw API response contains transport fields or verbose provider-specific rows.
```ts [server/agents/support.ts]
openapi({
spec: 'https://billing.example.com/openapi.json',
operations: ['billingGetInvoice'],
transformResponse(response, { operation, response: http }) {
return {
operationId: operation.id,
status: http.status,
data: response,
}
},
})
```
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives selected OpenAPI tools, or one generated Capability CLI tool when `cli` is set. |
| Provider-backed | Receives selected OpenAPI tools, or the generated Capability CLI tool, through the provider MCP bridge. |
| Custom-run-backed | Receives prepared context; `driver.run` decides whether to call API operations directly. |
## Options
| Option | Type | Default | Description |
| ------------------------ | ------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `spec` | `string | URL | object | function` | required | OpenAPI document URL, inline document, or invocation-scoped document resolver. |
| `operations` | `readonly string[]` | required | Selected OpenAPI `operationId`s exposed by this Capability. |
| `description` | `string` | none | Prefix for generated operation-tool descriptions and fallback description for the generated Capability CLI. |
| `hooks.request` | `(context) => patch | void` or `{ provides?, handler }` | none | Fetch-style request preparation hook for runtime headers, cookies, path, query, body, and timeout values. |
| `hooks.request.provides` | `{ body?, path?, query? }` | none | Runtime-owned OpenAPI input fields to remove from model and generated CLI schemas before caller validation. |
| `server` | `string | URL | function` | OpenAPI server | Override escape hatch for specs without a usable `servers[0].url` or spec URL origin. |
| `cli` | `false | { name, description? }` | `false` | Generates a Capability CLI instead of one model-facing tool per operation. |
| `responseType` | `"json" | "text"` | `"json"` | Response parser for operation results. |
| `transformResponse` | `(response, context) => output` | none | Maps parsed operation responses before returning them to the Agent. |
| `specHeaders` | `Record` | none | Headers used only when fetching the OpenAPI document. |
| `timeout` | `number` | none | Default request timeout in milliseconds. |
## Related pages
- [Fetch](https://vitehub.dev/docs/capabilities/fetch)
- [Access](https://vitehub.dev/docs/capabilities/access)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
- [CLI](https://vitehub.dev/docs/development/cli)
# OTLP
`otlp()` exports Agent Invocation telemetry using OTLP/HTTP JSON. It is a transport Capability, so the same Agent Definition works with any receiver that accepts ordinary OpenTelemetry logs and traces.
## Configuration
Import the Capability from `@vite-hub/agent/capabilities` and pass the receiver's OTLP base endpoint. ViteHub appends the conventional `/v1/logs` and `/v1/traces` signal paths.
```ts [server/agents/support.ts]
import { defineAgent } from '@vite-hub/agent'
import { otlp } from '@vite-hub/agent/capabilities'
export default defineAgent({
capabilities: [
otlp({
endpoint: process.env.OTLP_ENDPOINT!,
headers: {
authorization: `Bearer ${process.env.OTLP_TOKEN!}`,
},
resource: {
'service.namespace': 'support',
},
live: true,
}),
],
driver: { model: 'openai/gpt-5.1-mini' },
name: 'support',
})
```
## Trace contract
ViteHub always exports one completed trace. Its ordinary OTLP spans carry structural timing and `gen_ai.*` attributes. Without `live`, the root span also contains the invocation's Trace Events, including one `vitehub.agent.configured` event with sanitized Agent Definition metadata: Agent identity, Capability metadata, Driver and model identity when resolved, tool names, runtime, and Workspace name, mode, and Sources.
With `live: true`, each Trace Event is exported once as a correlated OTLP LogRecord while the invocation runs. LogRecords use the same trace and span IDs as the completed trace and carry `agent.invocation.id` plus `vitehub.event.sequence` for deduplication. ViteHub batches for up to five seconds or 512 new Trace Events, whichever comes first, and flushes immediately when the invocation ends. After all records are delivered, the completed trace omits span events because those events were already sent as logs. If a live batch fails, the completed trace retains its span events so the terminal export does not lose that evidence.
This is append-only export, not polling: ViteHub never resends an evolving in-progress span snapshot. OTLP's HTTP and gRPC exports are request/response protocols, so ViteHub does not add a receiver-specific SSE or WebSocket channel.
The configuration event is not a user message. User prompts and model or tool content remain governed by trace content policy and are metadata-only in this exporter.
Agent instructions are prompt content, so they are excluded by default. Opt in explicitly when the receiver is trusted:
```ts
otlp({
endpoint: process.env.OTLP_ENDPOINT!,
content: {
inputs: true,
instructions: true,
outputs: true,
},
})
```
Secret-shaped Capability metadata keys are replaced with `[redacted]`. Keep receiver credentials in `headers`; `otlp()` does not expose its endpoint or headers in Agent metadata.
## Custom Capability metadata
Every Capability contributes its public `metadata` automatically. A custom Capability can add invocation-resolved facts without knowing which exporter is installed:
```ts
import { defineCapability } from '@vite-hub/agent'
export const repository = defineCapability({
id: 'repository',
metadata: { provider: 'github' },
async resolve(context) {
const installation = await resolveInstallation(context)
context.telemetry.metadata({ installation: installation.slug })
},
})
```
This contribution belongs to the Capability's entry in the configuration event. It does not create another invocation stream or require a receiver-specific integration.
## Delivery behavior
The exporter retries transient HTTP failures and honors `Retry-After`. Export is best effort and runs through the host's `waitUntil()` boundary; an unavailable receiver does not change Agent output. Receiver failures produce a bounded structured local error with the Capability ID, invocation ID, run ID, and export phase. Receivers should deduplicate spans by trace and span ID, and live records by `agent.invocation.id` plus `vitehub.event.sequence`, because a retry can repeat a request.
Streaming command output remains trace activity, not a log drain. Provider command start, output deltas, and completion use the same tool-call ID so a session UI can group them without inspecting terminal escape sequences.
## Options
| Option | Type | Default | Description |
| ---------------------- | ------------------------------------ | -------------------------- | --------------------------------------------------------------------------------- |
| `endpoint` | `string` | Required | Absolute OTLP base endpoint. ViteHub appends `/v1/logs` and `/v1/traces`. |
| `headers` | `Record` or resolver | None | Request headers resolved for each export. |
| `resource` | OTLP resource attributes or resolver | Agent and runtime defaults | Additional resource attributes. |
| `content.inputs` | `boolean` | `false` | Includes user and tool input content in exported telemetry. |
| `content.instructions` | `boolean` | `false` | Includes resolved Agent instructions in the configuration telemetry. |
| `content.outputs` | `boolean` | `false` | Includes assistant, tool, and result output content in exported telemetry. |
| `live` | `boolean` | `false` | Sends append-only Trace Events as correlated OTLP logs while the invocation runs. |
# Papercuts
`papercuts()` adds the `report_papercut` tool to an Agent.
Use it to capture small, non-blocking friction while the details are still available to the current Agent Invocation.
The Capability owns the reporting contract and provenance.
Your application owns persistence, redaction, retention, deduplication, and triage.
Provide a `report` callback that accepts each papercut before the tool reports success.
The Capability always adds `report_papercut`.
The tool accepts one trimmed message between 1 and 1000 characters and asks the Agent to describe what it was doing and what got in the way.
Each report includes an id, creation time, source, and available Agent, run, and trace provenance.
The callback also receives the current Capability runtime context for application-specific routing.
## Configure papercut reports
Persist the normalized `papercut` record and use `context` only when the sink needs invocation-specific information.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { papercuts } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
papercuts({
async report({ papercut, context }) {
await savePapercut({
...papercut,
actorId: context.actor.id,
})
},
}),
],
})
```
Do not serialize the runtime `context` wholesale.
It is invocation-scoped and may contain request, actor, Workspace, or application values that do not belong in a papercut record; `workspace` and `fs` are absent on Agents without a Workspace.
## Add the Capability CLI
Set `cli: true` to let agents and developers submit the same reports from a command.
This adds the fixed `papercuts report` command without replacing `report_papercut`.
```ts [server/agents/support.ts]
papercuts({
cli: true,
report: ({ papercut }) => savePapercut(papercut),
})
```
Run the command through the Agent Dev Loop.
```bash [Terminal]
pnpm vitehub agent dev --agent support --cli papercuts -- report "The retry hid the original error."
```
Successful command output is `Papercut reported.`
## How reports work
ViteHub trims and validates the message, generates the record, and awaits `report`.
The tool returns `{ reported: true, id }` only after the sink accepts the report; callback errors fail the tool call instead of returning a false success.
The normalized record contains:
| Field | Description |
| ----------- | ------------------------------------------------------ |
| `id` | Generated `papercut_` identifier. |
| `createdAt` | ISO timestamp created when the report is submitted. |
| `message` | Trimmed report text. |
| `source` | `"tool"` or `"cli"`. |
| `agent` | Agent identity when the host provides one. |
| `run` | Agent Run metadata, including `runId`, when available. |
| `trace` | Runtime Trace Context when available. |
Attaching the Capability grants access to the developer-provided reporting sink, so `papercuts()` does not add an approval policy.
It does not provide a `when` option. Attach the Capability only to Agent Definitions that report papercuts.
## Requirements
`papercuts({ report })` requires a report callback.
Resolve the callback only after its destination accepts the record.
The tool description tells the Agent not to include secrets or customer data, but the sink still owns redaction and data handling appropriate to the application.
## Driver support
| Agent Driver | Support |
| ----------------- | -------------------------------------------------------------------------------------- |
| Model-backed | Receives `report_papercut` and the optional `papercuts` Capability CLI tool. |
| Provider-backed | Receives the same Capability tools through the Provider Agent tool bridge. |
| Custom-run-backed | Receives resolved tools in the run context; `driver.run` decides whether to call them. |
## Verify reports
Run one Agent Invocation that encounters a known problem and inspect the `report_papercut` call and normalized sink record.
When `cli` is enabled, run `papercuts report` through the Agent Dev Loop and confirm the same sink receives a report with `source: "cli"`.
## Options
| Option | Type | Default | Description |
| -------- | ------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------ |
| `report` | `(event: PapercutReportEvent) => void | Promise` | required | Application-owned sink for the normalized papercut and current Capability runtime context. |
| `cli` | `boolean` | `false` | Adds the fixed `papercuts report` Capability CLI while keeping `report_papercut`. |
## Related pages
- [Agent Evals](https://vitehub.dev/docs/agents/evals)
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
# Progress summary
`progressSummary()` observes reasoning and tool lifecycle events while an Agent works, then emits a replaceable one-sentence status as transient `data-progress-summary` stream data.
## Add progress summaries
Import the Capability from `@vite-hub/agent/capabilities` and give it the independent Agent Driver that writes progress:
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { progressSummary } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: primaryDriver,
capabilities: [
progressSummary({
driver: {
kind: 'codex',
model: 'gpt-5.6-luna',
},
}),
],
})
```
The Capability owns the summarizer instructions. The configured Driver selects the model and execution environment using the same contract as an Agent Definition or `title()`.
## Render the current summary
Listen for `data-progress-summary` parts and replace the currently displayed sentence when `revision` increases:
```ts
{
type: 'data-progress-summary',
data: {
type: 'progress-summary',
summary: 'Checking current SKU costs against the planning data.',
revision: 1,
},
}
```
The part is transient, so it does not become conversation history. Keep structured reasoning and tool logs separate when your interface exposes them.
With manual chat delivery, ViteHub edits the current placeholder as summaries arrive. When the Agent finishes, ViteHub deletes that placeholder and posts the final reply as a new message so chat platforms can deliver their normal notification.
## Understand the runtime behavior
With event-driven `intervalMs: 0`, the Capability starts its initial summary when the first non-terminal primary stream chunk arrives. A terminal-only or failed stream does not start unused auxiliary work.
Positive intervals begin when the first primary stream chunk arrives and use a fixed cadence from that point, so auxiliary generation never delays primary Driver startup. At most one generation runs at a time, and interval ticks are skipped while a generation is pending. Set `intervalMs: 0` to generate from reasoning and tool activity through the event-driven microtask behavior instead. Activity that arrives during an event-driven generation schedules one follow-up after it settles. Raw reasoning, tool input, and tool output are excluded from the generated prompt; reasoning is represented only as an `Active` presence signal.
The default prompt uses only reasoning presence, sanitized tool names, and the previous summary. It does not include user message text, code, commands, paths, traces, hidden instructions, credentials, raw tool details, or trusted `` payloads.
The Capability stops its cadence and aborts every in-flight generation when the parent invocation aborts or the primary stream finishes, cancels, or errors. A generation failure does not interrupt the primary response stream; ViteHub emits a sanitized warning and trace event without logging the Driver error message.
## Requirements
The primary Agent Driver must expose a compatible async stream or UI message stream. With a positive interval, the Capability attempts a summary on every tick and suppresses unchanged output. With `intervalMs: 0`, it remains silent after the first-chunk summary until reasoning or tool lifecycle events provide new activity.
Configure a `driver`, `model`, or `execute` option for summary generation. Without one of those options, the Capability uses the Agent model when available.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------- |
| Model-backed | Uses an explicit summary model or the Agent model. |
| Provider-backed | Uses an independent Agent Driver, including its model and environment settings. |
| Custom-run-backed | Uses an independent custom Driver or the `execute` callback. |
## Verify the result
Run an invocation and confirm that the primary stream continues immediately while an initial transient `data-progress-summary` part arrives. Keep the invocation open beyond `intervalMs`, then confirm that a later changed summary arrives with a higher `revision`.
Stop the invocation before the next interval and confirm that no later progress part appears.
## Options
| Option | Type | Default | Description |
| -------------- | ------------------------------------------ | ----------------------------- | ----------------------------------------------------------- |
| `driver` | `AgentDriver` | none | Independent Agent Driver used for progress generation. |
| `execute` | `(input) => string | { summary?: string }` | none | Custom progress generator. |
| `id` | `string` | `"progress-summary"` | Capability id. |
| `instructions` | `string` | Capability-owned instructions | System instructions for model-backed generation. |
| `intervalMs` | `number` | `10000` | Fixed generation cadence. Use `0` for event-driven updates. |
| `maxLength` | `number` | `180` | Maximum summary length. |
| `model` | `AgentModelResolver` | Agent model | Model used when no independent Driver is configured. |
| `template` | `string | function` | generated | Markdown prompt template. |
| `variables` | `Record` | none | Extra Markdown template variables. |
String templates use `@vite-hub/markdown-template` and receive `userText`, the `reasoning` presence signal, `activeTools`, `completedTools`, and `previous`. Referencing `userText` opts a custom template into handling user message text; keep sensitive content out of prompts you construct.
## Related pages
- [title()](https://vitehub.dev/docs/capabilities/title)
- [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers)
- [Markdown pages](https://vitehub.dev/docs/ai-resources/markdown-pages)
# Rate limit
`rateLimit()` consumes one budget unit before the main Agent Invocation starts. The Capability owns trusted Agent identity and rejection behavior, while the [Rate Limit primitive](https://vitehub.dev/docs/server-primitives/rate-limit) owns atomic enforcement.
## Configure a limiter
Create a direct limiter beside the Agent and pass it to the Capability. The Capability needs the portable decision to attach Agent identity and rejection context, so the handler-only `requireRateLimit()` guard is not its input.
The built-in memory driver is an owner-package API. Install that package when
using it.
```bash [Terminal]
pnpm add @vite-hub/rate-limit
```
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { rateLimit } from 'vite-hub/agent/capabilities'
import { createRateLimiter } from '@vite-hub/rate-limit'
import { memoryRateLimitDriver } from '@vite-hub/rate-limit/drivers/memory'
const invocations = createRateLimiter({
driver: memoryRateLimitDriver(),
limit: 20,
window: '1m',
})
export default defineAgent({
driver: { model },
capabilities: [
rateLimit({
limiter: invocations,
}),
],
})
```
## How rate limits work
The Capability runs during the input phase. It derives a stable key from the Capability id, scope, and trusted identity, then calls the `RateLimiter` exactly once.
A rejected decision throws `ViteHubError` with code `RATE_LIMIT_REJECTED`; the HTTP handler maps that code to `429` and emits `retry-after` headers only when the selected driver reports `retryAfter`. Cloudflare native enforcement does not return portable quota metadata.
The decision is stored under the Capability id in Agent Invocation Context and exposed as a finish extension. It contains the primitive decision plus `capabilityId`, `identity`, `identitySource`, `key`, and `scope`.
## Choose identity
The Agent Definition chooses the identity, not the Rate Limit Driver. The default `identity: 'auto'` prefers the Agent Invoker, then Agent Run metadata, then trusted IP headers, and finally an anonymous identity.
Use `identity: 'invoker'` when authentication provides a stable Agent Invoker. Use `identity: 'ip'` only after naming headers that the deployed host sets and sanitizes.
```ts [server/agents/public-support.ts]
rateLimit({
identity: 'ip',
limiter: invocations,
trustedIpHeaders: ['cf-connecting-ip'],
})
```
Do not trust a client-controlled forwarding header. The Capability reads only the headers listed in `trustedIpHeaders`, but the application remains responsible for ensuring the host overwrites them.
## Use a custom driver
Pass any `RateLimiter` when the application owns state or enforcement outside managed ViteHub Rate Limits.
```ts [server/rate-limiter.ts]
import { createRateLimiter } from 'vite-hub/rate-limit'
import type { RateLimitDriver } from 'vite-hub/rate-limit'
declare const driver: RateLimitDriver
export const invocationLimiter = createRateLimiter({
driver,
enforcement: 'strict',
limit: 20,
window: '1m',
})
```
```ts [server/agents/support.ts]
import { rateLimit } from 'vite-hub/agent/capabilities'
import { invocationLimiter } from '../rate-limiter'
rateLimit({ limiter: invocationLimiter })
```
The custom driver must implement atomic `consume()`. ViteHub does not provide a generic KV adapter because a portable `get()` followed by `set()` cannot guarantee an atomic decision under concurrency.
## Options
| Option | Type | Default | Description |
| ------------------ | ---------------------------------------------- | ------------------------- | --------------------------------------------------- |
| `limiter` | `RateLimiter | function` | required | Direct limiter or runtime resolver. |
| `id` | `string` | `"rate-limit"` | Capability id and Agent Invocation Context key. |
| `identity` | `"auto" | "invoker" | "ip" | "run" | function` | `"auto"` | Identity used to derive the private rate-limit key. |
| `scope` | `string | function` | Capability id | Additional key partition. |
| `trustedIpHeaders` | `string[]` | none | Host-controlled headers allowed for IP identity. |
| `message` | `string | function` | default rejection message | Error message for a rejected decision. |
| `onDecision` | `function` | none | Callback after every decision. |
| `onAllowed` | `function` | none | Callback after an allowed decision. |
| `onRejected` | `function` | none | Callback after a rejected decision. |
## Migrate from an inline store
The Capability no longer owns `limit`, `window`, `action`, or `store`. Move policy into a direct `RateLimiter`, then replace `store` with `limiter`.
```ts [Before]
rateLimit({
limit: 20,
store: 'memory',
window: '1m',
})
```
```ts [After]
const invocations = createRateLimiter({
driver: memoryRateLimitDriver(),
limit: 20,
window: '1m',
})
rateLimit({
limiter: invocations,
})
```
## Verify it
Run repeated Agent Invocations with the same identity. Confirm that the first `limit` invocations reach the Agent Driver and the next fails with code `RATE_LIMIT_REJECTED` before model, provider, or custom-run execution.
For local tests, use a dedicated memory driver instance. For Cloudflare, resolve the request binding in the limiter resolver and test the deployed binding because the native decision depends on request-scoped Worker environment.
## Related
- [Rate Limit primitive](https://vitehub.dev/docs/server-primitives/rate-limit)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
- [Auth Users and Agent Invokers](https://vitehub.dev/docs/concepts/auth-users-and-agent-invokers)
# Repository host
`repositoryHost()` gives an Agent a provider-neutral Repository Host Capability.
Use it when the Agent needs GitHub, GitLab, Bitbucket, or another repository host through a configured client.
Provide a client directly or configure a `repository-host` primitive.
The Capability adds `repository_host_read` in read mode.
In write mode it also adds `repository_host_write` for comments and reactions through the configured Repository Host client.
## Configure repository tools
```ts [server/agents/reviewer.ts]
import { defineAgent } from 'vite-hub/agent'
import { repositoryHost } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
repositoryHost({
client: githubRepositoryHostClient,
mode: 'read',
provider: 'github',
}),
],
})
```
## How repository tools work
`repository_host_read` accepts normalized operations for repositories, Change Requests, Change Request files, issues, comments, checks, and statuses.
Operations that target a single Change Request, issue, comment, check, or status require `target.id`.
`repository_host_write` requires write mode and a client with `write()`.
Comment writes require a `body`; all writes require a target id.
Supported writes are allowed by default after the developer enables write mode. Set an explicit policy when comments or reactions need approval or must be denied at runtime.
## Requirements
When `client` is omitted, ViteHub requires a configured `repository-host` primitive.
The client must expose `read()`, and write mode also requires `write()` before the write tool can succeed.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| Model-backed | Receives `repository_host_read`, plus `repository_host_write` in write mode. |
| Provider-backed | Receives Repository Host tools through the provider MCP bridge. |
| Custom-run-backed | Can use the configured client directly through runtime context if the runner owns that behavior. |
## Options
| Option | Type | Default | Description |
| ---------- | -------------------------------------------- | --------------- | ----------------------------------------------------- |
| `client` | `RepositoryHostClient | function` | primitive | Provider client with `read()` and optional `write()`. |
| `mode` | `"read" | "write"` | `"read"` | Adds `repository_host_write` when set to `"write"`. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for `repository_host_write`. |
| `provider` | `"github" | "gitlab" | "bitbucket" | string` | client provider | Provider metadata for inspection. |
## Verify repository tools
Run `vitehub agent info --agent --json` and inspect the resolved tool list.
Read one repository or Change Request through `repository_host_read`, then verify read mode exposes no write tool. When using an explicit approval policy, verify that posting comments or reactions requests approval.
## Related pages
- [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities)
- [Agent triggers](https://vitehub.dev/docs/agents/triggers)
# Repository host context
`repositoryHostContext()` records repository-host context for one Agent Invocation.
Use it when a trigger, webhook, or host identifies the current issue or Change Request and runtime code needs its provider data on demand.
Provide a Repository Host client directly, or configure a `repository-host` primitive for the invocation.
The Capability stores an async record in Agent Invocation Context under `repositoryHost` by default.
The record exposes `keys()`, `has(key)`, `get(key)`, `pick(keys)`, `entries(keys?)`, and `resolveAll()`.
Each key loads only when caller code requests it.
ViteHub caches in-flight and successful key loads for the current record, so repeated `get('comments')` calls reuse the same request.
The default keys are `issue`, `pullRequest`, `body`, `labels`, `comments`, and `files`.
Known keys that do not apply return `undefined`.
Unknown keys throw an error.
## Configure repository context
```ts [server/agents/reviewer.ts]
import { defineAgent } from 'vite-hub/agent'
import { repositoryHostContext } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
repositoryHostContext({
client: githubRepositoryHostClient,
materialize: './PULL_REQUEST.template.md',
target: {
repo: 'acme/app',
number: 42,
},
}),
],
})
```
## Read context
Read the async record from invocation context when hooks, custom runners, or host code need repository-host data.
The caller owns presentation and decides whether to render Markdown, JSON, or another format.
```ts [server/agents/reviewer.ts]
import { repositoryHostContext } from 'vite-hub/agent/capabilities'
const host = repositoryHostContext.read(ctx)
const keys = await host.keys()
const issue = await host.get('issue')
const pullRequest = await host.get('pullRequest')
const comments = await host.get('comments')
const labels = await host.get('labels')
```
Use `resolveAll()` when code needs a plain object with every available key.
The async record is not a JSON container, and `JSON.stringify(host)` throws instead of silently resolving async values.
## Target resolution
For GitHub, `repositoryHostContext()` reads the issue shape first.
When the issue includes pull request metadata, the record also exposes Change Request data through `pullRequest` and `files`.
```ts [server/agents/reviewer.ts]
repositoryHostContext({
client: githubRepositoryHostClient,
target: { repo: 'acme/app', number: 42 },
})
repositoryHostContext({
client: githubRepositoryHostClient,
target: { repo: 'acme/app', issue: 42 },
})
repositoryHostContext({
client: githubRepositoryHostClient,
target: { repo: 'acme/app', pullRequest: 42 },
})
```
V1 supports GitHub issues and pull requests.
Node ids, discussions, actions, and non-GitHub providers are not part of this context record yet.
## How context is loaded
`repositoryHostContext()` keeps context data-only unless `materialize` is configured.
With `materialize: './PULL_REQUEST.template.md'`, ViteHub bundles the colocated Markdown renderer and writes the resolved context to `PULL_REQUEST.md` in the Agent Workspace.
The generated path preserves directories and case while removing only the final `.template`.
Use `repositoryHost()` separately when a model-backed Agent needs repository-host tools such as `repository_host_read`.
Use `repositoryHostContext()` when trusted runtime code needs a typed invocation context value.
## Requirements
Static context does not require a Repository Host client.
Target-based context requires a client option or a configured `repository-host` primitive.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Model-backed | Does not receive rendered context automatically. Caller code must add selected values to instructions, input, or a tool. |
| Provider-backed | Receives the configured materialized Markdown file in its Agent Workspace. |
| Custom-run-backed | Can read the async record directly through `repositoryHostContext.read(ctx)`. |
## Options
| Option | Type | Default | Description |
| ------------- | ---------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------- |
| `client` | `RepositoryHostClient | function` | primitive | Provider client with `read()`. |
| `context` | `RepositoryHostContextInput | function` | invocation context | Static issue, Change Request, or selected key values. |
| `contextKey` | `string` | `"repositoryHost"` | Agent Invocation Context key used to store the async record. |
| `id` | `string` | `"repository-host-context"` | Capability id. |
| `materialize` | relative `*.template.md` path | none | Renders resolved context into the matching Workspace `.md` path. |
| `provider` | `"github" | string` | client provider | Provider guard. V1 accepts GitHub targets. |
| `target` | `RepositoryHostContextTarget | function` | none | Repository host target such as `{ repo, number }`, `{ repo, issue }`, or `{ repo, pullRequest }`. |
| `triggers` | `Record` | none | Trigger contributions tied to this context. |
## Verify repository context
Call `keys()` to inspect which values are available for the target.
Call `resolveAll()` in tests when you need to assert the full resolved shape.
## Related pages
- [Repository host](https://vitehub.dev/docs/capabilities/repository-host)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
# Sandbox
`sandbox()` owns sandbox execution for Agents.
Use `commands` only when the Agent needs a model-facing allowlist of executable names.
With `commands`, the Capability contributes `sandbox_exec`.
The tool accepts one configured executable name, optional args, cwd, environment, and timeout, then delegates execution to the Sandbox primitive.
## Choose sandbox commands
Pass executable names, not shell command strings.
The Capability rejects names that are not in the allowlist.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { sandbox } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
sandbox({ commands: ['node', 'pnpm'] }),
],
})
```
## How sandbox sessions work
ViteHub validates the command allowlist before the Capability attaches.
At invocation time, `sandbox_exec` checks the requested executable against the allowlist and calls the configured Sandbox primitive.
## Requirements
`sandbox({ commands })` requires an explicit Workspace and a configured `sandbox` primitive.
The `commands` option must contain at least one executable name when present.
Sandbox is not Workspace Shell.
Use `workspaceShell()` for Workspace inspection and structured Workspace mutation.
## Driver support
| Agent Driver | Support |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| Model-backed | Receives `sandbox_exec`. |
| Provider-backed | Receives `sandbox_exec` through the private MCP bridge when `commands` are configured. |
| Custom-run-backed | The Sandbox primitive is available through runtime context; `driver.run` decides whether to call it. |
## Verify the sandbox
Run `vitehub agent info --agent --json` and confirm `sandbox_exec` lists only the allowed executables.
Run a disallowed command during development and verify ViteHub rejects it before the Sandbox primitive executes.
## Options
| Option | Type | Default | Description |
| ---------- | ---------- | -------- | ------------------------------------------------------------------------------------------- |
| `commands` | `string[]` | required | Allowlisted executable names. Pass at least one executable name, not shell command strings. |
## Related pages
- [Sandbox primitive](https://vitehub.dev/docs/server-primitives/sandbox)
- [workspaceShell()](https://vitehub.dev/docs/capabilities/workspace-shell)
# Schedule
`schedule()` covers two schedule-related Agent abilities.
It can declare fixed Agent Schedules as Capability metadata, or it can expose one `cronjob` tool for Runtime Schedules when configured with a mode.
Static Agent Schedule mode records one or more five-field UTC cron expressions on the Capability.
Runtime Schedule mode contributes one `cronjob` tool. Read mode supports `targets`, `list`, and `get`. Write mode also supports `create`, `edit`, `pause`, `resume`, `run`, and `delete`.
## Configure schedules
Use static schedules to run the Agent on known cron entries.
ViteHub derives a stable id from the cron expression when you do not provide one.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { schedule } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
schedule({
schedules: ['0 9 * * 1'],
}),
],
})
```
To let an Agent create recurring turns for itself, enable self-targeting. ViteHub derives the target from the discovered Agent name and stores the prompt with a metadata-free copy of the resolved invoker identity. Every run passes through the Agent's normal Capability policies and `agent:input` hooks again. When `invoker.resolve` is configured, ViteHub also reruns it and continues only when the resolved `id` and `kind` still match. Without a resolver, ViteHub restores the durable identity; it does not perform external authentication automatically.
```ts [server/agents/mini.ts]
import { defineAgent } from 'vite-hub/agent'
import { schedule } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
schedule({
allowSelfTarget: true,
delivery: 'origin',
mode: 'write',
timeZone: 'Asia/Bangkok',
}),
],
})
```
The Agent can now create a cron job with a `prompt`, such as a daily report. Only an invocation with the same resolved invoker `id` and `kind` can inspect or manage that scheduled turn. `delivery: 'origin'` sends the result back to the channel thread where the schedule was created. Creating that schedule fails when the invocation has no deliverable channel thread.
## How schedules work
Static schedules add metadata that framework integrations and schedule-aware runtime behavior can inspect.
Runtime Schedule mode reads visible Runtime Schedules and can create, edit, pause, resume, run, or delete scoped schedules when write mode is enabled. The `cronjob` tool accepts an optional IANA `timeZone` on create and edit, while schedules without one continue to use UTC. A configured `timeZone` provides the default for new schedules; edits change it only when the tool supplies a new value.
## Requirements
Static schedules require at least one five-field UTC cron expression.
Runtime Schedule mode requires a configured `schedule` primitive.
Runtime Schedule edits require explicit write mode and are allowed by default.
Self-targeting requires explicit self-target permission.
Runtime Schedules still require a provider wake or a long-running Schedule runner to execute when due.
Set `policy: 'require-approval'` or `policy: 'deny'` when mutations need an additional gate; read operations remain allowed.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives the Runtime Schedule tool when mode is configured; static schedules are runtime metadata. |
| Provider-backed | Receives the Runtime Schedule tool through the provider MCP bridge when mode is configured; static schedules remain runtime metadata. |
| Custom-run-backed | Receives prepared metadata and context; `driver.run` decides how to use schedule context. |
## Verify schedules
Inspect Capability metadata for static schedule ids and cron expressions.
For Runtime Schedule mode, inspect the tool list and verify it contains only `cronjob` for scheduling. Its schema exposes only read operations in read mode.
Run a schedule with a six-field cron expression during development.
Confirm that the Capability rejects it before the Agent starts.
## Options
| Option | Type | Default | Description |
| ----------------- | ----------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `schedules` | `Array` | required for Agent Schedules | Declares fixed five-field UTC Agent Schedules. |
| `mode` | `"read" | "write"` | required for Runtime Schedule tools | Selects read or write Runtime Schedule tools. |
| `targets` | `string[]` | all visible targets | Allowlist of Runtime Schedule target names. |
| `allowSelfTarget` | `boolean` | `false` | Lets the Agent create scheduled turns for itself. ViteHub derives the target from the discovered Agent name. |
| `delivery` | `"origin"` | none | Delivers a scheduled Agent turn to the channel thread where it was created. Requires `allowSelfTarget: true`. |
| `timeZone` | `string` | none (UTC fallback) | Default IANA time zone for Runtime Schedules created by the tool. New schedules use it before falling back to UTC. |
| `policy` | `AgentToolPolicyDecision | function` | `"allow"` | Policy for mutating `cronjob` operations. Read operations remain allowed. |
## Related pages
- [Schedule primitive](https://vitehub.dev/docs/server-primitives/schedule)
- [Agent triggers](https://vitehub.dev/docs/agents/triggers)
# Skills
`skills()` makes a Workspace or external Source Skill available to an Agent Invocation.
For an Agent-owned Skill, use a folder Agent Definition whose entry file is named `agent.ts`, `agent.js`, `index.ts`, or `index.js`, including their `c` and `m` variants, then place `skills/` beside that entry file. ViteHub discovers and materializes those files automatically, so local Skills do not need a Capability declaration. Flat files such as `server/agents/support.ts` do not discover sibling Skills.
The Capability records the configured Skill path in metadata and requires the Workspace path to exist.
When `shellExecution` is set, model-backed Agents receive the normal Workspace Shell tools in the requested mode.
When `source` is set, ViteHub adds that source to the Agent Workspace at definition time and mounts it at the skill path.
Model-facing guidance for a Skill belongs in Agent Driver Instructions or deterministic imported instruction Markdown. Agent inspection metadata warns when `skills()` makes a Skill available but no explicit instruction coverage names it.
## Configure a Skill
The default path is `skills/SKILL.md`.
Pass a custom path when the Workspace stores the skill somewhere else.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { skills } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
workspace,
capabilities: [
skills(),
],
})
```
Mount a remote skill source when the skill file lives outside the local project:
```ts [server/agents/review/browser.ts]
import { defineAgent } from 'vite-hub/agent'
import { skills } from 'vite-hub/agent/capabilities'
import { github } from 'vite-hub/workspace'
export default defineAgent({
driver: { model },
workspace: { name: 'review', mode: 'write' },
capabilities: [
skills({
path: 'skills/agent-browser',
source: github({
repo: 'vercel/vercel-plugin',
root: 'skills/agent-browser',
include: ['SKILL.md', 'references/**', 'templates/**'],
materialize: 'build',
}),
shellExecution: 'write',
}),
],
})
```
## How Skills are loaded
ViteHub validates the Workspace read requirement before the Agent Driver runs.
The Capability metadata includes the directory path and the resolved `SKILL.md` path.
Model-facing Skill guidance belongs in Agent Driver Instructions or deterministic imported instruction Markdown with an explicit `::skill{path="..."}` coverage block.
For provider-backed drivers, `skills()` contributes the skill directory to the Provider Workspace session instead of adding model-facing instructions or tools.
With `shellExecution: 'write'`, model-backed Workspace Shell writes commit Workspace Session changes back into the Workspace.
With `source`, ViteHub still uses normal Workspace Source materialization, visibility, and Agent inspection metadata. `skills()` does not fetch source files at invocation time.
## Requirements
Workspace-scoped `skills()` requires an explicit Workspace with read access to the configured skill file.
The path can point to a directory or directly to a `SKILL.md` file.
When `source` is configured, `path` is the canonical mount. ViteHub mounts the source at the configured skill directory even when the source helper has its own default mount.
File Sources are single-file and root-confined. Use a directory-capable `github()` or `custom()` Source, or a root-confined `glob()` Source, when a Skill source is a directory.
## Driver support
| Agent Driver | Support |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| Model-backed | Validates the skill file requirement and can read the mounted Skill through Workspace tools when tools are available. |
| Provider-backed | Mounts Skills into the Provider Workspace session. |
| Custom-run-backed | Validates the skill file requirement before `driver.run`. |
## Verify the Skill
Run the Agent with the configured Workspace.
Confirm that a missing skill file fails before model execution with a Workspace path requirement error.
Inspect Capability metadata for the normalized `path` and `skillPath` values.
Agent inspection metadata warns when a configured Skill lacks explicit instruction coverage.
The warning clears when Agent Driver Instructions or a deterministic imported instruction file covers the Skill.
## Options
| Option | Type | Default | Description |
| ---------------- | ---------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `path` | `string` | `"skills"` | Directory or `SKILL.md` path required in the Workspace. |
| `shellExecution` | `"read" | "write"` | none | Optional Workspace Shell mode for model-backed Agents. Provider-backed Agents still receive the skill files, not Workspace Shell tools. |
| `source` | `WorkspaceSourceInput` | none | Workspace Source to mount at the skill directory. |
| `sourceKey` | `string` | derived from `path` | Workspace source key used when `source` is configured. |
Cover Skill usage guidance in Agent Driver Instructions with explicit Skill coverage blocks. Keep tool descriptions with Workspace Shell tools because they are structured tool contracts.
## Related pages
- [Workspace primitive](https://vitehub.dev/docs/server-primitives/workspace)
- [Agent instructions](https://vitehub.dev/docs/agents/instructions)
# Subagents
`subagents()` exposes one tool per configured child Agent Definition.
Use it to delegate bounded work without giving the model a generic agent runner.
Name each subagent with a lowercase identifier and give it a concrete description.
The Capability adds model-facing tools such as `run_researcher` or an explicit `toolName`.
Each tool starts the configured child Agent with a message, optional structured context, optional call options, and an inherited invoker.
## Add subagents
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { subagents } from 'vite-hub/agent/capabilities'
import researcher from './researcher'
export default defineAgent({
driver: { model },
capabilities: [
subagents({
agents: {
researcher: {
agent: researcher,
description: 'Research one narrow question and return sourced notes.',
},
},
}),
],
})
```
## How delegation works
`subagents()` validates the configured names and tool names before the Agent runs.
At invocation time, each subagent tool starts a fresh child Agent Invocation with the parent runtime context, then awaits the existing serializable Agent result. The model cannot select or reuse the child invocation id.
## Requirements
Each subagent entry requires an `agent` and a non-empty `description`.
Subagent keys must be lowercase stable identifiers, and generated tool names use underscores instead of dashes.
## Driver support
| Agent Driver | Support |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| Model-backed | Receives one model-facing tool per subagent. |
| Provider-backed | Receives one tool for each configured subagent through the provider MCP bridge. |
| Custom-run-backed | Can inspect the configured tools and invoke child Agents directly if the custom runner chooses to. |
## Options
| Option | Type | Default | Description |
| ---------------------- | ------------------------------------ | ------------- | --------------------------------------------------------------- |
| `agents` | `Record` | required | Named child Agent Definitions exposed as tools. |
| `id` | `string` | `"subagents"` | Capability id and instruction context key. |
| `agents.*.agent` | `AgentInput` | required | Child Agent Definition or Agent input accepted by `runAgent()`. |
| `agents.*.description` | `string` | required | Tool description shown to the model. |
| `agents.*.toolName` | `string` | `run_` | Explicit model-facing tool name. |
Cover delegation guidance in Agent Driver Instructions with explicit Capability coverage blocks. Keep each subagent's description with `agents.*.description` because it is the model-facing tool contract.
## Verify delegation
Run `vitehub agent info --agent --json` and confirm each subagent appears as a tool with the expected name and description.
Run one delegated task and verify the child invocation id and inherited invoker metadata.
Use [`startAgentInvocation()`](https://vitehub.dev/docs/agents/controlled-child-invocations) from trusted code when the caller needs to inspect or cancel the child after start.
## Related pages
- [Agent definitions](https://vitehub.dev/docs/agents/agent-definitions)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
# Title
`title()` generates a short title for an Agent Invocation.
It can use a model, a custom executor, or a local heuristic, then streams or returns the title as output metadata and optionally delivers it to a Channel thread.
The Capability reads the prepared first user message, generates a short title, provides it as a finish extension, and injects title data into compatible streams.
When that message has no semantic text, such as an attachment-only audio or image message, it waits for the successful Agent reply and uses that text instead.
Applications can use the finish extension to name a job, run, artifact, or other durable record without depending on a chat interface.
It can limit title generation to selected Agent Trigger ids.
## Configure titles
Attach `title()` to any Agent Definition that needs a generated title.
When no model is available to the Capability, ViteHub falls back to a short heuristic title.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { title } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
title(),
],
})
```
## How titles are generated
`title()` runs in the output phase.
It wraps compatible async streams or UI message streams so the title can arrive alongside the response, and it provides `{ title }` in the finish extension.
Input Capabilities run first, so `transcribe()` can replace audio with transcript text before `title()` reads it. Audio with authored text uses both in their prepared order. If the prepared input is still empty, `title()` waits for the normalized Agent reply; when that reply is also empty or the invocation fails, it leaves the title unset.
For framework-managed Chat SDK message Channels, ViteHub also delivers the title once per thread by default, even when each webhook contains only the current message or the handler is recreated. Follow-up Channel invocations skip title generation after successful delivery. Set `channelDelivery: "always"` to refresh the platform title on every invocation. Plain `runAgent()` and UI invocations without framework-managed Chat SDK delivery still receive stream data and finish extensions per invocation.
The Capability avoids wrapping the same result twice.
## Requirements
`title()` needs message-shaped input with at least one user message and semantic text from either the prepared input or a successful Agent reply.
A model, custom executor, or heuristic path must be available.
Use a custom template, variables, or executor when the title must include product-specific context.
## Driver support
| Agent Driver | Support |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Model-backed | Can use the Agent model or an explicit model to generate the title and decorate streams. |
| Provider-backed | Can decorate compatible output streams when the invocation produces them; model-based title generation still needs a model resolver. |
| Custom-run-backed | Can decorate compatible custom output; custom `driver.run` controls the response shape. |
## Verify titles
Run one Agent Invocation and inspect the stream for title data.
Confirm that the finish extension includes `{ title }` when title generation succeeds.
Test a vague first message and confirm the fallback title is used instead of an empty string.
## Options
| Option | Type | Default | Description |
| ----------------- | ---------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `channelDelivery` | `"once-per-thread" | "always"` | `"once-per-thread"` | Deliver framework-managed Chat SDK Channel titles once per thread, or on every invocation. |
| `driver` | `AgentDriver` | none | Agent Driver used only for title generation. |
| `execute` | `(input) => string | { title?: string }` | none | Custom title generator. `input.source` is `"input"` or `"response"`. |
| `fallback` | `string` | `"Untitled"` | Title used when generation returns no usable text. |
| `id` | `string` | `"title"` | Capability id. |
| `instructions` | `string` | none | System instructions for model-backed title generation. |
| `maxLength` | `number` | `80` | Maximum title length. |
| `model` | `AgentModelResolver` | Agent model, then heuristic fallback | Model used for title generation. |
| `template` | `string | function` | generated | Prompt template for model-backed generation. String templates can use `{{ message }}` and `{{ source }}`. |
| `trigger` | `string | string[]` | all triggers | Limit title generation to selected Agent Trigger ids. |
| `variables` | `Record` | none | Extra template variables. |
| `when` | `(input) => boolean` | none | Predicate that decides whether title generation runs. |
## Related pages
- [chat()](https://vitehub.dev/docs/capabilities/chat)
- [chatSummary()](https://vitehub.dev/docs/capabilities/chat-summary)
# Transcribe
`transcribe()` is an input-phase Official Capability for audio.
It turns audio message parts into transcript text before the Agent Driver receives the final input.
The Capability finds audio parts in input messages, transcribes them, appends transcript text to the message, and records transcription results in invocation context.
It can also persist transcript and source-audio artifacts into a writable Workspace.
## Configure transcription
Provide either an AI SDK transcription model configuration or an `execute()` function.
The example keeps artifacts off, so it does not require a writable Workspace.
```ts [server/agents/voice.ts]
import { defineAgent } from 'vite-hub/agent'
import { transcribe } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
transcribe({
model: transcriptionModel,
}),
],
})
```
### OpenRouter
Use `openRouterTranscriptionModel()` to keep OpenRouter authentication, audio encoding, request shape, and provider errors behind the AI SDK transcription model interface.
```ts [server/agents/voice.ts]
import { defineAgent } from 'vite-hub/agent'
import {
openRouterTranscriptionModel,
transcribe,
} from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
transcribe({
model: openRouterTranscriptionModel({
apiKey: () => env.OPENROUTER_API_KEY,
model: 'openai/gpt-4o-transcribe',
}),
}),
],
})
```
The adapter uses OpenRouter's base64 JSON request with `response_format: 'json'`. This avoids provider-specific multipart handling in Agent Definitions and does not request `verbose_json`, which OpenRouter only supports for some upstream providers.
AI SDK `providerOptions.openrouter` values for `language`, `temperature`, and `provider` routing are forwarded to OpenRouter. Unsupported OpenRouter transcription options fail explicitly instead of being silently ignored.
## How transcription works
`transcribe()` runs before model execution.
It enforces the configured maximum audio size, resolves audio data from direct data, `fetchData`, or URL, and replaces the consumed audio parts with transcript text in the user message.
When artifacts are enabled, it writes sanitized transcript and optional audio files to the Agent's writable Workspace and exposes results as a finish extension.
Use `artifacts.directory` to keep the transcript and source audio together.
The generated paths share a sanitized timestamp/message stem.
```ts
transcribe({
model: transcriptionModel,
artifacts: {
directory: 'inputs/voice-notes',
transcript: { format: 'markdown' },
},
})
```
## Streaming transcription
Use `streamTranscription()` for live raw audio. It wraps AI SDK streaming transcription and exposes `textStream`, an append-only text stream that can be passed directly to `event.reply()`.
```ts
import type { AgentFinishHookEvent } from 'vite-hub/agent'
import { streamTranscription } from 'vite-hub/agent/capabilities'
export async function liveTranscriptReply(
event: AgentFinishHookEvent,
audio: ReadableStream,
) {
const transcription = await streamTranscription({
model: 'openai/gpt-realtime-whisper',
audio,
inputAudioFormat: {
type: 'audio/pcm',
rate: 24_000,
},
})
return event.reply(transcription.textStream)
}
```
Return the reply intent without awaiting `transcription.text`; consuming the reply drives the provider stream and resolves the final text promise. Chat-backed Channels use native streaming when the adapter supports it and Chat SDK's post-and-edit fallback otherwise. Other message Channels use their native stream method when available and fall back to one final reply.
`streamTranscription()` emits provider `transcript-delta` events as reply chunks. Providers that only emit partial and final snapshots produce one final reply, which avoids duplicating corrected partial text.
## Asynchronous remote transcription
Use `createTranscription()` to submit a private remote object from a durable workflow and resume after the provider completes it.
The client returns the provider operation ID used for acknowledgement and idempotency, then normalizes an authenticated completion payload into a provider-neutral transcript or failure.
```ts
import {
createTranscription,
elevenLabsScribe,
} from 'vite-hub/agent/capabilities'
const transcription = createTranscription({
driver: elevenLabsScribe({
apiKey: () => env.ELEVENLABS_API_KEY,
diarize: true,
tagAudioEvents: true,
timestampsGranularity: 'word',
webhookId: env.ELEVENLABS_WEBHOOK_ID,
}),
})
const submission = await transcription.submit({
metadata: { attemptId, jobId },
source: { url: signedAudioUrl },
})
const completion = await transcription.receive(authenticatedProviderPayload)
if (completion.status === 'failed') {
console.error(completion.error.code, completion.error.message)
}
```
`submit()` never downloads the remote object into the application process.
Use a signed HTTPS URL for private Blob objects, with an expiry long enough for the provider to fetch it.
The caller still owns callback authentication before `receive()`, durable operation state, duplicate-delivery handling, timeouts, and workflow resumption.
Compose those concerns with the Workflow primitive; correlation metadata is untrusted until it matches the stored workflow attempt.
Provider callback payloads and SDK types do not cross the transcription client interface.
Failed completions contain a `ViteHubError` with a fixed `TRANSCRIPTION_*` code and message. Raw provider diagnostics stay behind the in-memory `cause` and are omitted when the completion is serialized.
## Requirements
Basic transcription requires a model or custom executor.
Artifact persistence requires an explicit writable Workspace.
Streaming transcription requires an AI SDK streaming transcription model, a `ReadableStream` of raw audio, and its input audio format.
Asynchronous remote transcription requires a `TranscriptionDriver`.
The built-in ElevenLabs Scribe driver requires an API key and an explicitly configured speech-to-text webhook ID.
Audio data must stay within `maxBytes`.
Artifact paths must stay inside the Workspace and cannot target reserved `.git` or `.vitehub` paths.
## Driver support
| Agent Driver | Support |
| ----------------- | --------------------------------------------------------------------------------------- |
| Model-backed | Receives text-enriched messages after transcription. |
| Provider-backed | Receives text-enriched Agent Run Input before provider execution. |
| Custom-run-backed | Receives text-enriched Agent Run Input and can read transcription results from context. |
Asynchronous transcription is independent of the Agent Driver because the caller composes its submitted operation and completion result into a durable Workflow.
## Verify transcription
Run an invocation with one audio part and inspect the final message text.
Confirm that the transcript appears before the Agent Driver runs.
When artifacts are enabled, inspect the Workspace for transcript files and the finish extension for transcription metadata.
## Options
| Option | Type | Default | Description |
| -------------------------------- | ---------------------------- | -------------------------------- | --------------------------------------------------------------- |
| `model` | AI SDK transcription model | required unless `execute` is set | Model used by AI SDK transcription. |
| `execute` | `(input) => string | result` | none | Custom transcription function; mutually exclusive with `model`. |
| `maxBytes` | `number` | `26214400` | Maximum accepted audio bytes. |
| `artifacts.directory` | `string | function` | generated | Directory for generated transcript and audio artifacts. |
| `artifacts.transcript` | `false | object` | disabled | Persist transcript artifacts to Workspace. |
| `artifacts.transcript.format` | `"text" | "markdown"` | `"text"` | Default transcript artifact body and generated extension. |
| `artifacts.transcript.path` | `string | function` | generated | Transcript artifact path. |
| `artifacts.transcript.mediaType` | `string | function` | inferred | Transcript artifact media type. |
| `artifacts.transcript.template` | `function` | default text | Custom transcript artifact body. |
| `artifacts.audio` | `boolean | object` | disabled | Persist source audio artifacts to Workspace. |
| `artifacts.audio.path` | `string | function` | generated | Audio artifact path. |
| `artifacts.audio.mediaType` | `string | function` | audio media type | Audio artifact media type. |
### Asynchronous client
| Interface | Result | Description |
| -------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
| `createTranscription({ driver })` | `TranscriptionClient` | Creates a provider-neutral asynchronous client. |
| `client.submit({ source, metadata, abortSignal })` | `submitted` operation | Submits a remote HTTP(S) source and returns the provider operation ID. |
| `client.receive(payload)` | `completed | failed` completion | Normalizes an already-authenticated provider completion payload. |
| `elevenLabsScribe(options)` | `TranscriptionDriver` | Maps remote Scribe v2 submission and callback payloads without exposing provider types. |
`openRouterTranscriptionModel({ apiKey, model })` returns an AI SDK transcription model for synchronous `transcribe({ model })` usage.
Without `artifacts.directory`, transcripts use `transcripts//.txt` and audio is placed beside the transcript. If transcripts are disabled, audio uses `audio//.`.
## Public helpers
Import these helpers from `@vite-hub/agent/capabilities` when custom hooks or executors need the same normalized data as the Capability.
| Helper | Return value | Behavior |
| ---------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audioBytes(audio, { maxBytes? })` | `Promise` | Resolves direct data, `fetchData`, or an audio URL and enforces a `26214400` byte default limit. |
| `getTranscriptionResults(context)` | `TranscriptionResult[]` | Reads the current invocation's results from an invocation context store or an object containing one. Returns an empty array when none exist. |
| `streamTranscription(options)` | `Promise` | Starts AI SDK streaming transcription and exposes `textStream` for streamed replies, `text` for the final transcript, and the underlying `result` metadata. |
Each `TranscriptionResult` contains `createdAt`, `date`, `messageId`, `stem`, and `transcript`, plus `audioPath` or `transcriptPath` when those artifacts were written.
## Related pages
- [AI Gateway streaming transcription](https://vercel.com/changelog/ai-gateway-now-supports-streaming-transcription){rel=""nofollow""}
- [Workspace primitive](https://vitehub.dev/docs/server-primitives/workspace)
- [Agent invocations](https://vitehub.dev/docs/agents/invocations)
# Usage
Add `usage()` to request the provider's complete usage metadata and expose ViteHub's normalized Agent Usage Record as a typed Finish Extension.
## Add usage
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { usage } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'anthropic/claude-sonnet-4.5' },
capabilities: [usage()],
})
```
For OpenRouter calls, the Capability sets `providerOptions.openrouter.usage.include` to `true`. Existing provider options and OpenRouter usage settings are preserved.
## Read the record
The Capability's typed `usage` Finish Extension returns the same normalized record available at `event.invocation.usage`.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { usage } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'anthropic/claude-sonnet-4.5' },
capabilities: [usage()],
hooks: {
'agent:finish'(event) {
console.log(event.invocation.usage)
console.log(event.extensions.get('usage'))
},
},
})
```
The record can contain normalized token usage, model and transport metadata, provider-reported cost, and other provider usage metadata. Fields remain optional when the provider does not report them.
## Verify it
Invoke the Agent through OpenRouter and inspect the provider call settings and Finish Event. Confirm that `usage.include` is enabled without replacing existing provider options, and that the `usage` Finish Extension matches `event.invocation.usage`.
## Related
- [Cost capability](https://vitehub.dev/docs/capabilities/cost)
- [Agent Invocations](https://vitehub.dev/docs/concepts/agent-invocations)
- [Runtime events](https://vitehub.dev/docs/reference/runtime-events)
# Web search
`webSearch()` gives an Agent access to web context through an explicit mode.
Use model mode for provider-native web search, or tool mode for normalized `web_search` and `web_read` tools.
Model mode contributes a provider tool request for `web_search`.
Tool mode contributes `web_search` and `web_read` model-facing tools backed by a single configured search provider.
## Configure web search
Use model mode when the selected model provider supports provider-native web search.
Tool mode requires a web search provider configuration.
```ts [server/agents/research.ts]
import { defineAgent } from 'vite-hub/agent'
import { webSearch } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model },
capabilities: [
webSearch({ mode: 'model' }),
],
})
```
## How web search works
Model mode adds a Provider Tool contribution and leaves search execution to the model provider.
Tool mode loads the configured provider, executes normalized search requests, and reads URLs as Markdown or text.
## Requirements
`webSearch()` requires `mode: 'model'` or `mode: 'tool'`.
Model mode requires an Agent Driver and model provider that support the provider tool.
Tool mode requires the application to install `askweb` and configure one web search provider with any required credentials.
## Driver support
| Agent Driver | Support |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Model-backed | Receives the provider tool in model mode or `web_search` and `web_read` in tool mode. |
| Provider-backed | Receives `web_search` and `web_read` through the provider MCP bridge in tool mode. Model mode is unsupported because Provider Agent Drivers do not accept Provider Tool contributions. |
| Custom-run-backed | Receives prepared context; `driver.run` decides how to perform web access. |
## Verify web search
Inspect provider tool contributions for model mode or the Agent tool list for tool mode.
Confirm that tool mode exposes `web_search` and `web_read`.
Run tool mode without `askweb` during development.
Confirm that the Capability reports the missing package and suggests model mode instead.
## Options
| Option | Type | Default | Description |
| ------------------ | ----------------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------- |
| `mode` | `"model" | "tool"` | required | Uses provider-native model web search or ViteHub-managed search/read tools. |
| `provider` | `WebSearchProviderInput` | required in tool mode | Provider name or provider options for tool mode. |
| `provider.name` | `"brave" | "exa" | "jina" | "searxng" | "serpapi" | "serpbase" | "tavily" | string` | required | Tool-mode web search provider. |
| `provider.apiKey` | `string | unsealer | function` | environment | Credential for providers that require one. |
| `provider.baseURL` | `string` | provider default | Override provider endpoint. |
## Credentials
Tool mode resolves credentials in this order: `provider.apiKey`, `VITEHUB__API_KEY`, then `_API_KEY`. ViteHub uppercases the provider name and replaces non-alphanumeric characters with underscores, so `my-search` uses `VITEHUB_MY_SEARCH_API_KEY` before `MY_SEARCH_API_KEY`.
## Tool inputs
Tool mode rejects properties outside these public input contracts.
### `web_search`
| Input | Type | Default | Description |
| ---------------- | ---------- | ---------------- | ----------------------------------- |
| `query` | `string` | required | Non-empty search query. |
| `includeDomains` | `string[]` | provider default | Restrict results to these domains. |
| `excludeDomains` | `string[]` | provider default | Exclude these domains from results. |
| `maxResults` | `number` | provider default | Maximum number of search results. |
### `web_read`
| Input | Type | Default | Description |
| ----------- | -------- | -------------- | -------------------------------- |
| `url` | `string` | required | Non-empty page URL to read. |
| `maxTokens` | `number` | reader default | Maximum normalized content size. |
## Related pages
- [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities)
- [fetch()](https://vitehub.dev/docs/capabilities/fetch)
# Workspace shell
`workspaceShell()` adds Workspace inspection tools in read mode, structured mutation tools in write mode, and an optional `workspace_exec` tool for explicitly configured executables.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
driver: { model: 'openai/gpt-5.1-mini' },
workspace: { mode: 'write' },
capabilities: [workspaceShell()],
})
```
Provider Drivers can also expose configured commands in write mode:
```ts [server/agents/coder.ts]
export default defineAgent({
driver: { kind: 'codex' },
workspace: { mode: 'write' },
capabilities: [workspaceShell({ commands: ['git'], mode: 'write', timeout: 30_000 })],
})
```
Command entries accept executable names or absolute paths, never shell command strings. Use `commands: 'all'` only on a trusted host because it permits any executable reachable by the Workspace Session. Successful command changes commit through Workspace rules.
ViteHub validates Workspace requirements before resolving tools. Configured commands require a writable Workspace because every command opens a Workspace Session. Workspace Sources, rules, and Actor Scope bound visible and committed paths, but they do not isolate host side effects outside the Workspace.
Provider Drivers already receive their materialized Workspace as the working directory, so ViteHub avoids duplicate file tools there. Configured `workspace_exec` commands still reach Provider Drivers through the private MCP bridge.
| Option | Type | Default | Description |
| ---------- | ------------------ | -------- | ------------------------------------------------------------------- |
| `mode` | `"read" | "write"` | `"read"` | Selects inspection tools or write-capable Workspace tools. |
| `commands` | `string[] | "all"` | - | Adds a provider-Driver executable allowlist in explicit write mode. |
| `timeout` | `number` | `60000` | Default command timeout in milliseconds. |
Use [`sandbox()`](https://vitehub.dev/docs/capabilities/sandbox) when a model-backed Agent needs an allowlisted executable. Provider Drivers use their native command tools inside the materialized working directory.
## Related pages
- [Workspace context](https://vitehub.dev/docs/agents/workspace-context)
- [Workspace primitive](https://vitehub.dev/docs/server-primitives/workspace)
- [sandbox()](https://vitehub.dev/docs/capabilities/sandbox)
# Agent Invocations
An Agent Invocation is one request to run an Agent. It includes the input, trusted caller, selected Capabilities, available context, execution method, and result.
An Agent Definition describes reusable behavior. An Invocation records one execution of that behavior.
## What happens during an Invocation
| Stage | What happens |
| ------------ | ------------------------------------------------------------------------------------- |
| Entry | A route, Channel, schedule, webhook, CLI command, or another caller provides input. |
| Identity | ViteHub identifies the trusted Agent Invoker and Actor. |
| Capabilities | The Definition and invocation context select the abilities available to this request. |
| Context | ViteHub prepares tools, policy, context values, and Workspace Scope. |
| Execution | The Agent Driver processes the prepared request. |
| Result | ViteHub returns or streams output and records events and usage. |
The Agent can use only the Capabilities selected for that Invocation. A Capability that isn't selected contributes nothing to the request.
## Don't confuse an Invocation with related records
| Term | Describes |
| ---------------- | --------------------------------------------------------------- |
| Agent Definition | Reusable Agent behavior. |
| Agent Invocation | One execution for one input. |
| Channel | Message origin and delivery facts around an invocation. |
| Workflow Run | Durable work that can continue across waits or server restarts. |
| Agent Memory | Persistent context stored outside the Invocation. |
A Channel can start many Invocations, and a Workflow Run can carry an Invocation. Neither one replaces the request record.
## Inspect an invocation
Inspect the input, invoker, Channel, Capabilities, Workspace Scope, Driver, events, usage, and output together. Run `vitehub agent info` to inspect the Agent definition and `vitehub agent dev` to stream a local Invocation.
Read [Invocations](https://vitehub.dev/docs/agents/invocations) for `runAgent()` and `streamAgent()`, or [Runtime policy, approvals, and traces](https://vitehub.dev/docs/concepts/runtime-policy-approvals-and-traces) for the records produced during execution.
# Auth Users and Agent Invokers
An Auth User is the person signed in to your application. An Agent Invoker is the trusted caller of one Agent Invocation. Agent and Capability code read that caller from `context.invoker`.
Auth answers "who is signed in?" An Agent Invoker answers "who or what started this invocation?"
## Choose the identity you need
| | Auth User | Agent Invoker |
| -------- | ---------------------------------------- | ------------------------------------------- |
| Scope | The application session | One Agent Invocation |
| Source | An auth provider | A trusted entry point or Auth bridge |
| Used by | Routes and application authorization | Agent and Capability code |
| Required | Only where the application requires auth | Every invocation, including anonymous calls |
## Auth can provide the invoker
An Agent can also start from a Channel, schedule, webhook, service account, CLI command, or local development. Those entry points don't need an Auth User, but they still provide an Agent Invoker.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { authenticated } from 'vite-hub/auth/agent'
export default defineAgent({
invoker: authenticated(),
driver: {
run: ({ invoker }) => ({ invoker }),
},
})
```
`authenticated()` maps the signed-in user to the Agent Invoker. Configuring Auth alone does not require a user session for every Agent.
## The invoker carries trusted caller data
| Field | Meaning |
| ------- | --------------------------------------------------------------- |
| `id` | Stable caller ID for the invocation. |
| `kind` | Caller type such as `authUser`, `chat`, or `anonymous`. |
| `label` | Optional label for inspection. |
| `meta` | Structured application data used by Capabilities and callbacks. |
Don't put secrets or raw session payloads in `meta`.
Read [Auth](https://vitehub.dev/docs/server-primitives/auth) for session setup and [Access](https://vitehub.dev/docs/capabilities/access) for decisions based on invoker identity.
# Bash
Bash is the tool an Agent uses to run commands contributed by its Capabilities. Each call selects a registered executable and passes its arguments through a Workspace Session.
Bash is not an unrestricted host shell. The Agent Definition and its selected Capabilities determine which commands exist for the invocation.
## Many commands share one tool
A browser Capability and a deployment Capability can each contribute a command. ViteHub exposes both through one `bash` tool, but each Capability still defines its own arguments and behavior.
Use Bash when an Agent needs to combine commands. Use a structured tool when an operation needs typed input and output or a separate policy for each action.
Read [Workspace Shell](https://vitehub.dev/docs/capabilities/workspace-shell) for the Capability API and [Shell](https://vitehub.dev/docs/server-primitives/shell) for the application-facing execution primitive.
# Capabilities
A Capability gives an Agent a selected ability. It can add tools, instructions, requirements, policy, triggers, metadata, or invocation context.
Installing a Server Primitive does not give an Agent access to it. Attach a Capability when the Agent needs that operation.
## Choose the API by its caller
| | Server Primitive | Capability |
| ------------ | -------------------------- | ------------------------------------------------ |
| Caller | Application server code | An Agent during an Invocation |
| Access | A documented server import | Selected tools, policy, requirements, or context |
| Selection | Application code calls it | The Agent Definition or invocation selects it |
| Model access | None | Only the operations the Capability contributes |
## Select the abilities an Agent can use
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
import { kv, workspaceShell } from 'vite-hub/agent/capabilities'
export default defineAgent({
workspace: { mode: 'read' },
capabilities: [
workspaceShell({ mode: 'read' }),
kv({ mode: 'read' }),
],
driver: {
run: ({ input }) => input,
},
})
```
The Agent receives only what the selected Capabilities contribute. Application code can call the same Server Primitives through their server APIs.
## Use a fixed or resolved list
Use an array when every invocation needs the same abilities. Use a resolver when trusted invocation data changes the list:
```ts
capabilities: ({ actor }) => [
customerRecords,
...(actor.meta?.support === true ? [internalDiagnostics] : []),
],
```
ViteHub resolves the Agent Invoker before it calls the resolver. The returned array is the complete Capability list for that request.
## Limit what the Agent can use
A Capability does not expose the full Runtime Context or unrestricted host access. Its tools, requirements, policy, and metadata define what the Agent can inspect and use.
Read the [Capabilities](https://vitehub.dev/docs/capabilities) section for implementation details and [First agent](https://vitehub.dev/docs/getting-started/first-agent) for a runnable Definition.
# Channels
A Channel records where a message came from and how to reply. It carries the message, sender, attachments, reply target, delivery facts, and host commands around an Agent Invocation.
A Channel handles message transport. An Agent Invocation runs the Agent for one input. One Agent Definition can run behind several Channel adapters because these records stay separate.
## Channel and Invocation have different jobs
| | Channel | Agent Invocation |
| --------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| Describes | Message origin and delivery | Identity, Capabilities, execution, and result |
| Lifetime | Can contain many messages and Invocations | One request |
| Can exist alone | Yes, a Channel can receive a message without starting an Agent | Yes, a route or schedule can start an Invocation without a Channel |
## Message and command facts stay separate
| Term | Carries |
| ---------------- | -------------------------------------------------------------------- |
| Message | Content, sender, attachments, reply target, and Channel metadata. |
| Command | A host action such as stop, retry, or inspect. |
| Agent Invocation | The resolved identity, Capabilities, context, execution, and result. |
| Chat Session | The message history selected for an invocation. |
A message can start an Invocation. A host command can affect a session or a running Invocation without becoming model input.
## Treat Channel metadata as input from the adapter
Use verified Channel metadata to identify the Agent Invoker, choose a Capability, or select a Workspace Scope. Inspect the Channel and Invocation together when a message reaches the wrong Agent, carries the wrong identity, or loses delivery data.
Read [Channels](https://vitehub.dev/docs/agents/channels), [Chat history and sessions](https://vitehub.dev/docs/agents/chat-history-sessions), and [Agent Invocations](https://vitehub.dev/docs/concepts/agent-invocations) for the operational APIs.
# Definition discovery
A Definition is a file that declares named behavior or state, such as an Agent, Queue, or Workspace. Discovery finds the file and assigns the name that application code uses.
## The file location supplies the name
ViteHub derives a Definition's name from its file path. Each package defines which paths it scans. For example:
```txt
server/agents/support.ts -> support
server/agents/docs/agent.ts -> docs
src/triager.agent.ts -> triager
```
Each package documents its paths and file suffixes. Put the Definition in one of those paths instead of adding another name in application code.
## The integration finds the file
The package integration scans the project during development and build. It prepares the routes, imports, bindings, or metadata that the package needs.
```ts [server/agents/support.ts]
import { defineAgent } from 'vite-hub/agent'
export default defineAgent({
driver: {
run: () => 'ok',
},
})
```
The Definition stays in application code. Generated files show what the integration prepared, but application code doesn't import them unless the package documents that import.
## Check the discovered result
If a Definition is missing or has the wrong name, check its location, the package integration, and the generated metadata.
Read [Agent Definitions](https://vitehub.dev/docs/agents/agent-definitions) for Agent-specific files and [Vite Integrations and Provider Output](https://vitehub.dev/docs/concepts/vite-integrations-and-provider-output) for the build side.
# Concepts
ViteHub adds server features to Vite applications and lets Agents use selected features through Capabilities. These pages explain the terms that connect those two paths.
For setup and API options, go to [Server primitives](https://vitehub.dev/docs/server-primitives), [Agents](https://vitehub.dev/docs/agents), [Capabilities](https://vitehub.dev/docs/capabilities), or [Reference](https://vitehub.dev/docs/reference).
## Start here
| Page | Read it when |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| [Server primitives](https://vitehub.dev/docs/concepts/server-primitives-for-any-host) | You need storage, background work, auth, isolated execution, or another server feature. |
| [Agents](https://vitehub.dev/docs/agents) | You need a named actor that runs with a model, coding provider, or application code. |
## Core vocabulary
| Page | Defines |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [Definition discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery) | How ViteHub finds and names a definition file. |
| [Agent Invocations](https://vitehub.dev/docs/concepts/agent-invocations) | What ViteHub resolves and records for one Agent request. |
| [Capabilities](https://vitehub.dev/docs/concepts/capabilities-api) | How an Agent receives a selected ability. |
| [Workspace and Sources](https://vitehub.dev/docs/concepts/workspace-and-sources) | How a writable file tree differs from the read-only content mounted into it. |
| [Auth Users and Agent Invokers](https://vitehub.dev/docs/concepts/auth-users-and-agent-invokers) | How application identity becomes trusted invocation identity. |
| [Channels](https://vitehub.dev/docs/concepts/channels-api) | How messages, delivery facts, and host commands reach an Agent. |
| [Bash](https://vitehub.dev/docs/concepts/bash) | How Capability-provided commands appear as one Agent tool. |
## Runtime execution
| Page | Defines |
| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| [Runtime Context](https://vitehub.dev/docs/concepts/runtime-context) | The host resources available to a server operation or Agent request. |
| [Runtime Helpers and stable imports](https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports) | The imports application code uses to call ViteHub. |
| [Runtime policy, approvals, and traces](https://vitehub.dev/docs/concepts/runtime-policy-approvals-and-traces) | The records that explain whether work ran, waited, or failed. |
## Host and build model
| Page | Defines |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [Vite Integrations and Provider Output](https://vitehub.dev/docs/concepts/vite-integrations-and-provider-output) | How ViteHub prepares a package for development and deployment. |
## Next steps
Open [Installation](https://vitehub.dev/docs/getting-started/installation) for a runnable project. If you already know what you need, go to [Server primitives](https://vitehub.dev/docs/server-primitives) or [Agents](https://vitehub.dev/docs/agents).
# Runtime Context
Runtime Context contains the trusted host resources available to one ViteHub operation. It can include platform resources, request data, provider bindings, background work, memoized values, and trace information.
Invocation input describes the task. Runtime Context provides the host resources needed to run it.
## Runtime Context comes from the host
Framework and provider integrations create Runtime Context in the generated route or handler. A custom server call passes the same values itself because `runAgent()` does not read framework globals.
| Value | Used for |
| ------------------------ | ------------------------------------------------------- |
| `runtime` and `platform` | Identifying the active runtime and platform. |
| `memo` | Resolve one value once during the current execution. |
| `waitUntil` | Continue background work after the handler returns. |
| Provider context | Reading trusted host resources such as bindings. |
| `trace` and `traceLog` | Keeping trace identity and recording structured events. |
Keep reusable behavior in Definitions and task data in invocation input. Runtime Context passes host resources to the operation.
## Adapt an H3 event
Application-owned routes need one host adapter for the Runtime Context used by
the Agent examples:
```ts [server/runtime-context.ts]
import type { AgentRuntimeName, AgentWaitUntil } from 'vite-hub/agent'
import type { H3Event } from 'h3'
function waitUntilFrom(value: unknown): AgentWaitUntil | undefined {
const owner = value as { waitUntil?: AgentWaitUntil } | undefined
return typeof owner?.waitUntil === 'function'
? owner.waitUntil.bind(value)
: undefined
}
function waitUntilFor(event: H3Event): AgentWaitUntil {
const context = event.context as {
cloudflare?: { context?: unknown }
_platform?: { cloudflare?: { context?: unknown } }
}
const node = event.node as {
req?: { runtime?: { cloudflare?: { context?: unknown } } }
}
const req = event.req as {
runtime?: { cloudflare?: { context?: unknown } }
}
return waitUntilFrom(event)
?? waitUntilFrom(context)
?? waitUntilFrom(context.cloudflare?.context)
?? waitUntilFrom(context._platform?.cloudflare?.context)
?? waitUntilFrom(req?.runtime?.cloudflare?.context)
?? waitUntilFrom(node?.req?.runtime?.cloudflare?.context)
?? (task => { void Promise.resolve(task).catch(error => console.error(error)) })
}
function cloudflareFor(event: H3Event) {
const runtimeEvent = event as H3Event & {
env?: Record
context: H3Event['context'] & {
cloudflare?: { context?: unknown, env?: Record }
_platform?: { cloudflare?: { context?: unknown, env?: Record } }
}
req?: { runtime?: { cloudflare?: { context?: unknown, env?: Record } } }
node?: { req?: { runtime?: { cloudflare?: { context?: unknown, env?: Record } } } }
}
const env = runtimeEvent.env
?? runtimeEvent.context.cloudflare?.env
?? runtimeEvent.context._platform?.cloudflare?.env
?? runtimeEvent.req?.runtime?.cloudflare?.env
?? runtimeEvent.node?.req?.runtime?.cloudflare?.env
const context = runtimeEvent.context.cloudflare?.context
?? runtimeEvent.context._platform?.cloudflare?.context
?? runtimeEvent.req?.runtime?.cloudflare?.context
?? runtimeEvent.node?.req?.runtime?.cloudflare?.context
return env ? { env, ...(context ? { context } : {}) } : undefined
}
function runtimeFor(event: H3Event): AgentRuntimeName {
const env = typeof process === 'object' && process ? process.env : undefined
if (cloudflareFor(event)) return 'cloudflare-agents'
if ('Deno' in globalThis) return 'deno'
if (env?.VERCEL) return 'vercel'
return env?.NODE_ENV === 'development' ? 'vite' : 'unknown'
}
export function getRuntimeContext(event: H3Event) {
const cloudflare = cloudflareFor(event)
const values = new Map()
return {
...(cloudflare ? { cloudflare } : {}),
memo(key: string, create: () => T): T {
if (!values.has(key)) values.set(key, create())
return values.get(key) as T
},
runtime: runtimeFor(event),
waitUntil: waitUntilFor(event),
}
}
```
Keep this adapter host-owned. Add provider resources here and delegate
`waitUntil` to the real host lifetime when one exists. The fallback observes
failures for long-lived local Node processes; serverless deployments must
expose their provider lifetime adapter through the event context.
## Runtime Context is not a Capability
A Runtime Capability handle passes an implementation between packages. An Agent Capability gives an Agent a selected ability. It can use Runtime Context without exposing that context to the model.
## Inspect the handoff
Inspect the generated route or custom server call that starts the operation. It shows which host supplies the runtime, background work, provider resources, and trace information.
Read [Runtime events](https://vitehub.dev/docs/reference/runtime-events) for the records carried through Runtime Context and [Agent Invocations](https://vitehub.dev/docs/concepts/agent-invocations) for the request record.
# Runtime Helpers and stable imports
A Runtime Helper is the documented server API that application code uses to call or inspect a ViteHub feature. ViteHub keeps generated registries, provider bindings, and framework adapters behind that import.
The package page documents what the API returns and which host resources it needs.
## Call the helper from server code
```ts [server/api/health.ts]
import { useServerEnv } from '#vitehub/env/server'
export function health() {
const env = useServerEnv()
return { environment: env.APP_ENV }
}
```
Use the package import or generated `#vitehub/...` path documented for the feature. Don't import `.vitehub` registry files directly. The integration can change their structure.
## Keep configuration and runtime code separate
| Part | What it contains |
| --------------- | ------------------------------------------------------------------------ |
| Runtime Helper | The application call or inspection API. |
| Definition | Named configuration and behavior in application code. |
| Runtime Context | Host resources needed during execution. |
| Provider Output | Generated routes, bindings, functions, workers, or other host artifacts. |
The Runtime Helper can use Runtime Context without making application code construct provider-specific objects.
## Check an import failure
Check the package's documented import, the registered Vite integration, and the generated type files. Read [Import paths](https://vitehub.dev/docs/reference/import-paths) for exact path rules and [Provider Output](https://vitehub.dev/docs/reference/provider-output) for generated files.
# Runtime policy, approvals, and traces
Runtime policy decides whether an operation can run. An Approval Request pauses it until a trusted actor responds. A Trace Event records what happened before, during, and after the operation.
Use these records to explain a runtime decision. They don't replace application logs, Agent Memory, or the final Agent output.
## Each record answers a different question
| Record | Question |
| ------------------- | ------------------------------------------------------------------------------- |
| Policy Decision | Was the operation allowed, denied, or sent for approval? |
| Approval Request | Which trusted response is needed before work continues? |
| Trace Event | Which runtime action or transition occurred? |
| Lease or wait state | Which work remains active while execution waits or continues in the background? |
Runtime Context passes these records between packages and the host.
## Put approval beside the action
A Capability can request approval for a tool or Server Primitive operation. The Invocation stream and runtime events include the request and result, so the host does not need to parse model text.
If the host cannot satisfy an approval requirement, the operation stays pending or fails according to the package contract.
## Inspect the decision
Inspect the invocation id, policy decision, approval request, trace event, and final result together. This shows whether work was rejected, waited, executed, or failed after execution began.
Read [Runtime events](https://vitehub.dev/docs/reference/runtime-events) for event fields and [Capabilities](https://vitehub.dev/docs/concepts/capabilities-api) for the model-facing contribution that can trigger policy.
# Server primitives
Server Primitives are APIs for environment values, databases, queues, workflows, files, sandboxes, and other server features.
Application code calls the ViteHub API. The Vite integration connects that call to the implementation supported by the current development or deployment host.
## Choose a primitive for the job
| You need | Start with |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Configure the app | [Env](https://vitehub.dev/docs/server-primitives/env), [Auth](https://vitehub.dev/docs/server-primitives/auth), or [Rate Limit](https://vitehub.dev/docs/server-primitives/rate-limit) |
| Store data and files | [Database](https://vitehub.dev/docs/server-primitives/database), [KV](https://vitehub.dev/docs/server-primitives/kv), [Blob](https://vitehub.dev/docs/server-primitives/blob), [Workspace](https://vitehub.dev/docs/server-primitives/workspace), or [Source](https://vitehub.dev/docs/server-primitives/source) |
| Send or receive messages | [Email](https://vitehub.dev/docs/server-primitives/email) or [Channels](https://vitehub.dev/docs/agents/channels) |
| Run work later | [Queue](https://vitehub.dev/docs/server-primitives/queue), [Schedule](https://vitehub.dev/docs/server-primitives/schedule), or [Workflow](https://vitehub.dev/docs/server-primitives/workflows) |
| Run isolated automation | [Browser](https://vitehub.dev/docs/server-primitives/browser), [Shell](https://vitehub.dev/docs/server-primitives/shell), or [Sandbox](https://vitehub.dev/docs/server-primitives/sandbox) |
## How a primitive works in your app
Most ViteHub primitives follow the same pattern:
::steps{level="3"}
### Configure ViteHub
Add ViteHub to your configuration. ViteHub uses the [Vite Environment API](https://vite.dev/guide/api-environment){rel=""nofollow""}, which requires Vite 8+, Nitro 3+, or Nuxt 5+.
:::tabs{.framework-tabs}
::::tabs-item{icon="i-simple-icons-vite" label="Vite"}
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({ preset: 'node', database: true }),
],
})
```
::::
::::tabs-item{icon="i-simple-icons-nuxtdotjs" label="Nuxt 5"}
```ts [nuxt.config.ts]
import viteHubNuxt from 'vite-hub/nuxt'
export default defineNuxtConfig({
modules: [
[viteHubNuxt, { preset: 'node', database: true }],
],
})
```
::::
::::tabs-item{icon="i-unjs-nitro" label="Nitro 3"}
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { nitro } from 'nitro/vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({ preset: 'node', database: true }),
nitro(),
],
})
```
::::
:::
### Define the database
Create a Database Definition file that ViteHub discovers automatically. Its file name becomes the database name, and the file defines the schema and options.
:::tabs{.framework-tabs}
::::tabs-item{icon="i-simple-icons-vite" label="Vite"}
```ts [src/notes.database.ts]
import { defineDatabase } from 'vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export default defineDatabase({
name: 'notes',
schema: {
notes: sqliteTable('notes', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
}),
},
})
```
::::
::::tabs-item{icon="i-simple-icons-nuxtdotjs" label="Nuxt 5"}
```ts [server/databases/notes/config.ts]
import { defineDatabase } from 'vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export default defineDatabase({
name: 'notes',
schema: {
notes: sqliteTable('notes', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
}),
},
})
```
::::
::::tabs-item{icon="i-unjs-nitro" label="Nitro 3"}
```ts [server/databases/notes/config.ts]
import { defineDatabase } from 'vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export default defineDatabase({
name: 'notes',
schema: {
notes: sqliteTable('notes', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
}),
},
})
```
::::
:::
### Use it from server code
Import the database API in a server route and query the named database. ViteHub connects that call to the provider configured for the current environment.
:::tabs{.framework-tabs}
::::tabs-item{icon="i-simple-icons-vite" label="Vite"}
```ts [src/server.ts]
import { useDatabase } from 'vite-hub/database/drizzle'
export default {
async fetch() {
const { db, schema } = useDatabase('notes')
return Response.json(await db.select().from(schema.notes))
},
}
```
::::
::::tabs-item{icon="i-simple-icons-nuxtdotjs" label="Nuxt 5"}
```ts [server/api/notes.get.ts]
import { useDatabase } from 'vite-hub/database/drizzle'
export default defineEventHandler(() => {
const { db, schema } = useDatabase('notes')
return db.select().from(schema.notes)
})
```
::::
::::tabs-item{icon="i-unjs-nitro" label="Nitro 3"}
```ts [server/api/notes.get.ts]
import { useDatabase } from 'vite-hub/database/drizzle'
export default defineEventHandler(() => {
const { db, schema } = useDatabase('notes')
return db.select().from(schema.notes)
})
```
::::
:::
::
The same path applies to other Server Primitives. Configure ViteHub, add a definition when the feature needs a name or schema, and call its server API. Check the [host support matrix](https://vitehub.dev/docs/frameworks-hosts/support-matrix) before choosing a deployment target.
# Vite integrations and Provider Output
A Vite integration connects a package to Vite development and builds. Provider Output is what that integration generates for a host, such as routes, functions, bindings, workers, crons, or runtime files.
Configure the integration in Vite. Inspect Provider Output when you need to check host wiring. Application code uses the package's documented server API instead of generated internals.
## Configure the integration
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({ preset: 'node', agent: true, kv: true }),
],
})
```
Put options in `vite.config.ts` when they affect discovery, generated files, provider bindings, or deployment. Put options in a Definition when they apply to that named item.
## Use Provider Output as build evidence
Inspect generated output to verify routes, bindings, or host wiring. Import it from application code only when the package documents a path such as `#vitehub/env/server`.
| Need | Surface |
| ---------------------------- | --------------------------------------------- |
| Development and build wiring | The package Vite Integration. |
| One named declaration | A Definition file. |
| Host deployment material | Provider Output generated by the integration. |
| Server code access | A documented package or generated import. |
Read [Definition discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery), [Runtime Helpers and stable imports](https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports), and [Provider Output](https://vitehub.dev/docs/reference/provider-output) for more detail.
# Workspace and Sources
A Workspace is a named file tree that can persist changes. A Source provides read-only files, items, or controlled requests. Mounting a Source makes its content available inside a Workspace.
Use a Workspace for files, rules, sessions, snapshots, and writes. Use a Source to read content from a local file, remote service, API, or another provider.
## Workspace and Source have different roles
| | Workspace | Source |
| ------------ | ---------------------------------------------------- | ------------------------------------------------ |
| Provides | A file tree and file operations | Read-only content from an origin |
| Writes | Can allow writes through Workspace rules | Does not accept Workspace writes |
| Placement | Defines the tree and mount points | Appears at a mount inside a Workspace |
| Agent access | Requires Workspace context and a selected Capability | Visible only within the selected Workspace Scope |
## Define the tree and its origins
```ts [server/workspaces/docs.ts]
import { defineWorkspace, file, glob } from 'vite-hub/workspace'
export default defineWorkspace({
sources: {
readme: file({ path: 'README.md' }),
docs: glob({ cwd: '.', include: ['docs/**/*.md'] }),
},
rules: {
'/**': { write: false },
'/drafts/**': { write: true, mediaType: 'text/markdown' },
},
})
```
The Workspace defines the rules and tree. The Sources provide the content mounted into that tree.
## Agent access is another choice
Workspace context does not give an Agent file tools. Attach a Capability such as `workspaceShell()` when the Agent needs to read or change Workspace files.
Workspace Scope narrows the visible tree for one invocation. The trusted host or invocation context selects that scope. The model cannot widen it.
## Inspect the result
Use `useWorkspace()` from server code to inspect the file tree. Generated Workspace and Source metadata lists discovered names and request descriptors without exposing provider credentials.
Read [Workspace](https://vitehub.dev/docs/server-primitives/workspace), [Source](https://vitehub.dev/docs/server-primitives/source), and [Workspace context](https://vitehub.dev/docs/agents/workspace-context) for the APIs.
# CLI
The ViteHub CLI loads the local Vite config and collects package-contributed command namespaces.
Commands stay owned by the package that understands the workflow, while `vitehub` gives agents and developers one predictable entry point.
The official [`vite-hub` package on npm](https://www.npmjs.com/package/vite-hub){rel=""nofollow""} publishes both `vitehub` and `vite-hub` binaries.
## Install and open help
The `vite-hub` framework distribution includes the CLI. The command reads
active Vite plugins, so a missing package integration also means a missing
package-owned command.
```bash [Terminal]
pnpm add vite-hub
pnpm vitehub --help
```
Libraries and advanced integrations that do not use the framework distribution
can install `@vite-hub/cli` directly.
Expected help lists available namespaces.
The Agent Package contributes `agent` and `channels` when `hubAgent()` is active, Database contributes `db` when `hubDb()` is active, Workspace contributes `workspace` when `hubWorkspace()` is active, and the CLI includes the built-in `provision` namespace.
```txt [Output]
Usage: vitehub [args...]
Available namespaces:
agent Agent development workflows.
channels External Channel registration workflows.
db Database development workflows.
workspace Workspace development workflows.
provision Idempotently create missing provider resources.
```
## Commands
| Command | Status | Owner | Use it for |
| -------------------------- | -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| `vitehub agent eval` | Opt-in tooling | Agent Package | Run discovered Agent Evals through ViteHub defaults. |
| `vitehub agent info` | Available | Agent Package | Inspect resolved Agent metadata through a running Vite Development Server. |
| `vitehub agent dev` | Available | Agent Package | Talk to a discovered Agent through a running Vite Development Server. |
| `vitehub channels history` | Available | Agent Package | Download one deployed conversation and its attachments. |
| `vitehub channels sync` | Available | Agent Package | Inspect or apply provider-owned webhook registrations for a deployed stage. |
| `vitehub db generate` | Available | Database Package | Refresh generated Database artifacts and generate Drizzle migrations. |
| `vitehub db migrate` | Available | Database Package | Refresh generated Database artifacts and apply Drizzle migrations. |
| `vitehub workspace dev` | Available | Workspace Package | Run commands through a Workspace Session exposed by a Compatible Vite Development Server. |
| `vitehub provision run` | Available | ViteHub CLI plus package Provision Steps | Create missing provider resources idempotently. |
## Synchronize Channel webhooks
Deploy the application stage before registering its Channel webhooks. `channels sync` loads the discovered Agent Definitions with the selected Vite stage, checks that every desired webhook route is live at the exact public HTTPS origin, and then compares the provider state. Telegram is the first supported provider.
The command is read-only by default. Start with sanitized JSON when an agent or another command needs to review the complete plan.
```bash [Terminal]
pnpm vitehub channels sync \
--stage staging \
--url https://staging.example.com \
--json
```
`--stage staging` loads Vite's stage-specific environment files, such as `.env.staging`; existing process environment values take precedence. Keep the Telegram bot token and webhook secret in Server Env. The command does not accept credentials as flags and does not include them in human or JSON output.
Apply the reviewed plan by repeating the exact origin in `--confirm-origin`. The confirmation is non-interactive, so the same contract works for developers, CI, and coding agents without silently selecting a deployment.
```bash [Terminal]
pnpm vitehub channels sync \
--stage staging \
--url https://staging.example.com \
--apply \
--confirm-origin https://staging.example.com
```
Use `--agent ` or `--channel ` to narrow a multi-Agent application. Switching a Telegram Channel to polling, or setting `webhooks: false`, plans removal of an existing Telegram webhook; applying that plan also requires `--allow-delete`, and the current provider URL must belong to the confirmed origin. ViteHub preserves pending updates during registration and removal.
Telegram exposes the registered URL and delivery errors through `getWebhookInfo`, but it does not return the configured secret token or allowed update list. The plan marks those fields as unverifiable. Use `--force` to reapply them when the URL already matches and credential or subscription configuration changed. Telegram accepts public webhook ports 443, 80, 88, and 8443; the CLI rejects other explicit ports before applying.
`channels sync` owns only the provider's mechanical registration. The first Telegram synchronizer subscribes to message updates because that is the built-in Channel's supported inbound event. Admission rules, allowed users, secrets, and additional update types remain in the application. An app that needs a custom adapter, certificate, fixed IP, or connection policy must keep the provider lifecycle application-owned; an app-owned `adapter` is not a synchronization target.
## Download Channel history
`channels history` loads the same stage-specific Agent and Channel configuration, then authenticates to the deployed webhook route with its configured webhook secret. It writes portable message metadata to `history.json` and downloads attachment data into `media/`; Agent traces and tool events are not included.
```bash [Terminal]
pnpm vitehub channels history \
--stage production \
--url https://app.example.com \
--agent calories \
--channel telegram \
--output ./channel-history
```
A Telegram direct-message Channel infers its thread when the adapter allows exactly one user. Pass `--thread ` for group conversations and adapters where one Channel serves multiple conversations, issues, or tickets. When a Channel declares multiple webhook registrations, select the deployed route and its authentication with `--webhook `.
The export can only contain history available through the Chat SDK adapter or its configured State Adapter. Telegram's Bot API cannot backfill arbitrary old messages, so its durable fallback uses the configured `threadHistory` window, which defaults to 100 messages retained for seven days. Export before that window expires when the archive is intended for recovery.
## Manage Database migrations
The Database commands refresh the discovered Database Definitions before running Drizzle Kit. When every Database is named, ViteHub runs the command once for each generated Drizzle config and stops at the first failure.
```bash [Terminal]
pnpm vitehub db generate
pnpm vitehub db generate --name add-audit-log
pnpm vitehub db generate --custom --name backfill-state
pnpm vitehub db migrate
```
`db generate` forwards Drizzle Kit arguments, supports `--name ` for a migration name, and uses `--custom` to create an empty custom migration. `db migrate` accepts forwarded Drizzle Kit migration arguments.
## Run Agent Evals
Use `vitehub agent eval` when the proof is Agent behavior, not just TypeScript.
The Agent namespace includes the command, while Eval authoring and execution keep
their test-only dependencies explicit. Install them before creating an Eval; the
optional path then narrows the Agent Eval Target.
```bash [Terminal]
pnpm add -D @vite-hub/agent evalite vitest
```
```bash [Terminal]
pnpm vitehub agent eval
pnpm vitehub agent eval server/agents/support.eval.ts --threshold 90
pnpm vitehub agent eval --output .vitehub/evals/support.json --hide-table
pnpm vitehub agent eval --watch
pnpm vitehub agent eval --no-cache
```
Use `--watch` to rerun affected evals after file changes and `--no-cache` to bypass cached model output. Set `agent.eval.testTimeout` in `vite.config.ts` for long-running model or provider evals.
## Inspect an Agent Definition
Start the app's Vite Development Server, then inspect the resolved metadata for one Agent Definition.
The command does not invoke the Agent Driver.
```bash [Terminal]
pnpm vitehub agent info --agent support
pnpm vitehub agent info --agent support --json
```
The default output summarizes the selected Driver, its execution authority, tools, visible Workspace files and Sources, instructions, Agent Invoker Profiles, warnings, and metadata status.
Execution authority is a resolution-time snapshot of filesystem, network, environment, credential, process, and isolation authority. An `unknown` value means the runtime or provider cannot prove that dimension during inspection; it does not mean restricted or denied. The snapshot describes runtime truth for the inspected context, not an enforcement decision or proof of safety.
Use `--json` for the structured inspection contract at `config.driver.executionAuthority`, and `--url` when Vite is not listening on `http://localhost:5173`.
When multiple Agents are discovered, `--agent` is required.
`agent info` reads resolved runtime metadata from the guarded Agent Dev Loop endpoint exposed by `hubAgent()`.
## Talk to an Agent during development
Start the app's Vite dev server in one terminal.
Then attach the Agent Dev Loop from another terminal.
```bash [Terminal]
pnpm vitehub agent dev --agent support --url http://localhost:5173
```
Pass a message or `--prompt` for a one-shot invocation, or omit both to enter an interactive session.
```bash [Terminal]
pnpm vitehub agent dev "/summary" --agent support
pnpm vitehub agent dev --agent support --prompt "/summary"
pnpm vitehub agent dev --agent support -p "/summary"
```
Use `--payload` when the invocation needs event input that would normally come from a Channel, route, webhook, or app transport.
The file can live anywhere in the app, but colocating it as `server/agents//dev.payload.json` keeps local fixtures next to the Agent Definition without making them part of production behavior.
It must contain one JSON object shaped for the selected Agent Trigger.
For non-chat triggers, pass `--trigger` and shape the file for that trigger instead of Agent Invocation Context Values.
```json [server/agents/support/dev.payload.json]
{
"user": {
"id": "user_123",
"name": "Local Developer"
},
"session": {
"id": "local-support"
},
"meta": {
"audience": "technical"
}
}
```
```bash [Terminal]
pnpm vitehub agent dev --agent support --payload server/agents/support/dev.payload.json
pnpm vitehub agent dev --agent support --payload server/agents/support/dev.payload.json -p "/summary"
pnpm vitehub agent dev --agent support --timeout 180000 -p "/summary"
```
Use `--cli` when a Capability attached to the Agent declares a Capability CLI.
Everything after `--` is parsed as the nested Capability CLI command.
Attached Capability CLI Contributions are available to the Agent Dev Loop by default, including for provider-backed Agents; set `defineAgent({ cli: { capabilities: false } })` to hide them from this surface.
During Agent runs, ViteHub renders operation tool calls as command lines, such as `api listCustomers --query '{"status":"active"}'`, instead of dumping the raw input object.
```bash [Terminal]
pnpm vitehub agent dev --url http://localhost:3000 --agent support --cli inventory -- items list --json
```
Expected output includes the resolved payload file path before the Agent Invocation starts.
In interactive mode, type a message or command such as `/summary` at the prompt.
```txt [Output]
Loaded payload: /Users/acme/app/server/agents/support/dev.payload.json
Connected to support at http://localhost:5173
> /summary
```
Prefix input with `!` when you need to run a direct Workspace command through the selected Agent Dev Loop Target.
The selected Agent must declare a writable Workspace.
ViteHub runs the command through that Workspace Session and commits successful changes back to the Workspace Store.
```bash [Terminal]
pnpm vitehub agent dev --agent support "!pnpm test"
pnpm vitehub agent dev --url http://localhost:5173 --timeout 180000 support !pnpm test --filter api
```
Put Agent Dev Loop options before the `!` command; flags after `!` are passed to the Workspace command.
In interactive mode, `!` input bypasses the Agent Driver for that turn.
Use normal messages for Agent reasoning, `!` commands for direct Workspace shell work, and `--cli` for Capability CLI Contributions.
```txt [Output]
Connected to support at http://localhost:5173
> !pnpm test
```
## Run Workspace commands during development
Use `vitehub workspace dev` when you want a direct command against a Workspace without routing through an Agent.
Start the app's Vite dev server first, then run the command from another terminal.
```bash [Terminal]
pnpm vitehub workspace dev --url http://localhost:5173 docs exec pnpm test --filter api
pnpm vitehub workspace dev --timeout 180000 docs exec "npm run lint"
pnpm vitehub workspace dev --path guides --path examples docs exec pnpm test
```
The command runs through the Workspace dev endpoint exposed by `hubWorkspace()` on the Compatible Vite Development Server.
ViteHub materializes a Workspace Session, executes the command, prints stdout and stderr, and commits the session when the command exits successfully.
Put Workspace Dev options before the Workspace target; use `exec` before one-shot command args.
Repeat `--path ` to materialize only those Workspace paths for the command session.
If you omit the command in an interactive terminal, the CLI opens a prompt for repeated Workspace commands.
```txt [Output]
Connected to docs at http://localhost:5173
> pnpm test
```
## Preview provisioning
Use `--dry-run` before writing Provider resources.
Provision never deletes or mutates existing resources, and non-secret ids are written only when a real run applies actions.
```bash [Terminal]
CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... pnpm vitehub provision run --provider cloudflare --dry-run
VERCEL_TOKEN=... VERCEL_PROJECT_ID=... pnpm vitehub provision run --provider vercel --dry-run
```
## Troubleshooting
| Symptom | Likely cause | Fix |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `Unknown ViteHub CLI namespace` | The package Vite Integration is not installed or is disabled. | Add the package's `hubX()` plugin to `vite.config.ts`. |
| `Provision requires --provider cloudflare|vercel` | The provider flag is missing or misspelled. | Pass a supported provider explicitly. |
| Provision fails before applying actions | Required provider credentials are missing. | Set `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, or set `VERCEL_TOKEN`. |
| Provision dry-run reports no actions | A package plan skipped provider lookup because its read credentials are missing. | Supply the provider credentials to inspect existing resources; `--dry-run` still prevents `apply()`. |
| Vercel Provision reports no resources for Blob | `VERCEL_PROJECT_ID` is missing, or the active Blob store is not `vercel-blob`. | Set the project id and select the Vercel Blob driver before rerunning the plan. |
| Agent eval CLI is disabled | `agent.eval` or `agent.cli` disables the Agent Eval Runner. | Re-enable the Agent integration option for local development. |
| Agent eval times out | The eval case, model call, or provider run exceeds `agent.eval.testTimeout`. | Increase `agent.eval.testTimeout` in `vite.config.ts` or narrow the eval case. |
| Vite config fails while loading a ViteHub plugin import | A fresh npm project is loading `vite.config.ts` as CommonJS, but ViteHub packages are ESM-only. | Set `"type": "module"` in `package.json` or rename the config to `vite.config.mts`. |
| `No Compatible Vite Development Server found` | The app dev server is not running or `--url` points at the wrong port. | Start Vite separately, then pass the dev server URL. |
| `Unknown Workspace Dev target` | The named Workspace is not discovered by the running Vite dev server. | Check the Workspace Definition name and make sure `hubWorkspace()` is active. |
| `Agent Dev Loop command requires workspace.mode: "write"` | A `!` command targeted an Agent without writable Workspace access. | Configure the selected Agent with `workspace: { mode: 'write' }`, or send a normal Agent message instead. |
| Agent Dev Loop request times out | A streamed invocation emitted no events before the inactivity timeout, or a Capability CLI/Workspace command exceeded its wall-clock deadline. | Pass `--timeout ` for the dev-loop operation or inspect the stalled work. |
| `Agent Dev Loop payload file must contain a JSON object` | The `--payload` file is not a JSON object. | Replace the file contents with one object shaped for the selected Agent Trigger. |
## Next steps
- Use [Agent Evals](https://vitehub.dev/docs/agents/evals) for behaviour checks.
- Use [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for Workspace Sessions and write access.
- Use [Provisioning](https://vitehub.dev/docs/development/provisioning) for provider resource ids.
- Use [Config options](https://vitehub.dev/docs/reference/config-options) for package integration switches.
# Console
The ViteHub Console is a read-only app for inspecting discovered Agents and retained Agent Invocations. It is off by default. Enable it, start the app, then open `/_vitehub` to browse sessions, search retained text, and inspect invocation events.
Console data can contain user prompts, model output, tool activity, and provider metadata. Protect the Console before making it reachable on a production URL.
## Enable the Console
Set `console: true` in the root ViteHub integration. The Node preset supports the Console in development and production.
```ts [vite.config.ts]
import { vitehub } from 'vite-hub'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vitehub({
agent: true,
console: true,
preset: 'node',
})],
})
```
A standalone Nitro 3 app adds Nitro after ViteHub. The Console app and its UI dependencies ship inside `vite-hub`, so an existing Nitro app does not need another Console or UI package.
```ts [vite.config.ts]
import { nitro } from 'nitro/vite'
import { vitehub } from 'vite-hub'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
vitehub({
agent: true,
console: true,
preset: 'node',
}),
nitro(),
],
})
```
Nuxt uses the same option. Install Nuxt UI because the Console uses the ViteHub UI module.
```bash [Terminal]
pnpm add @nuxt/ui
```
```ts [nuxt.config.ts]
import viteHubNuxt from 'vite-hub/nuxt'
export default defineNuxtConfig({
modules: [
[viteHubNuxt, {
agent: true,
console: true,
preset: 'node',
}],
],
})
```
Restart the development server after changing the option. Open `http://localhost:3000/_vitehub`, using your app's actual origin and port.
If `console` is omitted or set to `false`, ViteHub does not register a Console page, API handler, Nitro plugin, or public asset path. A disabled Console returns the host's normal not-found response.
## Protect both route groups
`console: true` registers the page under `/_vitehub/**` and its read API under `/api/_vitehub/console/**`. It does not add authentication or an admin role.
If the app uses ViteHub Auth, guard both route groups in the Primary Auth Definition. The host decides what makes a user an administrator.
```ts [server/auth.ts]
import { defineAuth, type AuthAccessAuthorize } from 'vite-hub/auth'
const authorizeConsole: AuthAccessAuthorize = ({ user }) =>
user.role === 'admin'
export default defineAuth({
access: {
routes: [
{ route: '/_vitehub/**', authorize: authorizeConsole },
{ route: '/api/_vitehub/console/**', authorize: authorizeConsole },
],
},
})
```
ViteHub checks for an Auth Session before it calls `authorizeConsole`. A missing session returns `401`. Returning `false` from the callback returns `403`. The callback can return a `Response` when the app needs another rejection or redirect.
The `role` field above is an application example, not a ViteHub field. Replace it with the role, permission, or allowlist already used by the host. Apps that use another authentication library should protect the same two route groups in host middleware.
Read [Auth](https://vitehub.dev/docs/server-primitives/auth#authorize-access-routes) for sign-in redirects and the complete callback contract.
## Know what the Console stores
The Console installs a fallback Agent Invocation journal at `.vitehub/data/console.sqlite`. It retains invocation records and selected searchable text, including prompts, messages, final text, and progress updates.
The journal has no automatic TTL or deletion. In production, the operator must define how long to retain the file and how to remove records that may contain sensitive data.
The fallback applies only when an Agent Definition does not configure `invocations`. An explicit `defineAgent({ invocations })` store remains authoritative, and its sessions are not copied into `console.sqlite` or read by the built-in Console.
The automatic fallback also requires `defineAgent` from `vite-hub/agent`. Definitions imported directly from `@vite-hub/agent` must configure their own `invocations` store. Use the [Invocation UI](https://vitehub.dev/docs/ui/invocation) with that store when the app needs a custom inspection page.
Production Console builds currently require `preset: 'node'` because the fallback journal uses local SQLite. The Node preset supports the build, but it does not make `.vitehub/data/console.sqlite` persistent: the host must provide durable storage that survives process and deployment replacement. The file is also local to one replica and is not shared across replicas. Other presets can run the Console during development. Their production builds fail while `console: true` is set, so ViteHub does not write the journal to storage that may disappear between requests or deployments.
The Console API accepts `GET` requests only. Responses set `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
## Inspect usage
Session details show recorded token totals when the invocation trace contains provider usage. Add the [Usage Capability](https://vitehub.dev/docs/capabilities/usage) when the provider needs an explicit usage request or the Agent must expose the normalized Agent Usage Record at finish.
The Console does not calculate missing provider data. Token counts, model metadata, and provider-reported cost remain absent when the provider does not report them.
## Fix common failures
| Symptom | Check |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `/_vitehub` returns `404` | Confirm `console: true`, then restart the development server. Omitted and false configurations register no route. |
| The Console opens but has no sessions | Invoke a discovered Agent. Confirm it uses the framework fallback instead of a separate `invocations` store. |
| A production build rejects `console: true` | Use the Node preset or disable the Console for that production build. |
| The page returns `401` | Sign in through the Auth provider configured by the host. |
| The page returns `403` | Check the host's `authorize` callback and the current user's role or permission. |
Use [Agent Invocations](https://vitehub.dev/docs/agents/invocations) for custom stores and invocation lifecycle behavior. Use [Invocation UI](https://vitehub.dev/docs/ui/invocation) when building an application-owned inspection page instead of mounting the complete Console.
# Generated files
Generated files prove how ViteHub resolved Definitions, Runtime Registries, stable imports, and Provider Output.
Application code uses documented ViteHub imports instead of generated files.
## Common generated paths
| Path | Owner | Purpose |
| ------------------------------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------- |
| `.vitehub/env/public.mjs` | Env Package | Generated Public Env runtime module. |
| `.vitehub/env/server.mjs` | Env Package | Generated Server Env runtime module. |
| `.vitehub/types/env.d.ts` | Env Package | Generated Public Env and Server Env types. |
| `.vitehub/types/browser.d.ts` | Browser Package | Generated Browser Definition names and input/result module types. |
| `.vitehub/types/email.d.ts` | Email Package | Exact module declarations for discovered `#vitehub/emails/` imports. |
| `.vitehub/types/markdown-template.d.ts` | Markdown Template Package | Generated module type for direct `*.template.md` imports. |
| `.vitehub/types/workspace.d.ts` | Workspace Package | Generated Workspace name types. |
| `.vitehub/email/templates/*.mjs` | Email Package | Bundled Markdown email templates used by provider builds. |
| `.vitehub/rate-limit/manifest.json` | Rate Limit Package | Sorted Rate Limit IDs, resolved providers, and inspectable driver capabilities. |
| `.vitehub/nitro/realtime/registry.mjs` | Realtime Package | Generated registry for discovered Realtime Definitions. |
| `.vitehub/nitro/realtime/handler.ts` | Realtime Package | Generated WebSocket and checkpoint route handler. |
| `.vitehub/agent/chat-webhook-route.ts` | Agent Package | Generated Chat Webhook Route handler for discovered chat-capable Agents. |
| `.vitehub/agent/discord-gateway-plugin.ts` | Agent Package | Generated Nitro process lifecycle for Discord Gateway listeners on the Node preset. |
| `.vitehub/agent/discord-gateway-route.ts` | Agent Package | Generated Nitro route handler that wakes the Discord Gateway listener for discovered Discord Agents. |
| `.vitehub/agent/deno-server.ts` | Agent Package | Generated Deno server output for Agent chat and webhook routes. |
| `.vitehub/schedule/deno-cron.mjs` | Schedule Package | Deno `Deno.cron` wake output for Static Schedule Definitions. |
| `.vitehub/nitro/schedule/*` | Schedule Package | Narrow Nitro bridge for Schedule Provider Wake. |
| `.vitehub/provision.json` | ViteHub CLI | Non-secret Provision State read by Vite Integrations. |
| `.vercel/output/**` | Provider Output | Vercel Build Output generated during provider builds. |
| `.netlify/v1/functions/**` | Provider Output | Netlify Agent and static Schedule functions generated by their package integrations. |
| `dist//wrangler.json` | Provider Output | Cloudflare worker config generated during provider builds. |
The `.vitehub/agent/**` paths above are the default for Vite integrations. The Nuxt integration keeps generated Agent artifacts inside Nuxt's build directory instead, under `/vitehub/agent/**` (normally `.nuxt/vitehub/agent/**`). Agent Definition discovery still uses the project and server directories.
## Inspect generation
List generated files after Vite startup or build.
The exact set depends on the package integrations and Provider Selection.
```bash [Terminal]
find .vitehub -maxdepth 4 -type f | sort
```
For Nuxt, inspect the framework-owned generated directory instead.
```bash [Terminal]
find .nuxt/vitehub -maxdepth 4 -type f | sort
```
Inspect resolved Rate Limit guarantees before deployment. The manifest uses `schemaVersion: 2` and contains sorted `rateLimits` entries with `name`, `provider`, and `capabilities`; it is generated state for agents and tooling, not an application import.
```bash [Terminal]
cat .vitehub/rate-limit/manifest.json
```
Inspect env aliases through the stable import paths.
The generated files are implementation details behind those imports.
```ts [server/config.ts]
import { useServerEnv } from '#vitehub/env/server'
const env = useServerEnv()
```
## Regenerate after edits
Vite Integrations refresh generated files during dev startup, hot updates, or build hooks depending on the package.
Restart the Vite dev server when a generated file does not match the current Definition set.
```bash [Terminal]
pnpm dev
```
## Troubleshooting
| Symptom | Likely cause | Fix |
| ------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Stable import does not resolve | Generated type path is missing from TypeScript includes, or the Vite Integration did not run. | Add `.vitehub/types/**/*.d.ts` to `tsconfig.json` and restart Vite. |
| Generated registry is empty | Discovery path does not match the package file convention. | Check [File conventions](https://vitehub.dev/docs/reference/file-conventions). |
| Provider output is stale | Build skipped provider output during dev or e2e mode. | Run a production-shaped build. |
| Provision ids are missing | Provision State was never written, or env vars override it. | Run `vitehub provision run --dry-run`, then apply with credentials if needed. |
## Next steps
- Use [Import paths](https://vitehub.dev/docs/reference/import-paths) for public import boundaries.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for host artifacts.
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for qualified host coverage.
- Use [File conventions](https://vitehub.dev/docs/reference/file-conventions) for discovery rules.
# Local development
Run the application locally before testing a hosted build. ViteHub uses the same
Vite config to discover definitions, prepare generated files, and resolve Agent
metadata.
## Local proof map
| Proof path | Use it for | Output to inspect |
| ----------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Vite dev server | Definition discovery, server imports, Agent streams, and local providers | Terminal output, CLI behavior, and resolved Agent metadata |
| ViteHub Console | Discovered Agents, retained sessions, search, invocation events, and provider usage | `/_vitehub` and the configured Agent Invocation store |
| ViteHub CLI | Package-owned command workflows such as Agent Evals and Provision | CLI exit code, concise output, optional JSON files |
| Generated files | Registries, generated env access, deployment output, and provision state | `.vitehub/**`, `.vercel/output/**`, `dist/**`, or provider config files |
| Application tests | Server API behavior and application regressions | Test output and application fixtures |
## Run the local app
Start with the app's normal development command.
ViteHub Vite Integrations run during Vite startup, so discovery and generated local files use the same root as the app.
```bash [Terminal]
pnpm dev
```
Run the application's normal test and build commands after the development server proves discovery.
```bash [Terminal]
pnpm test
pnpm build
```
## Inspect generated state
Generated files help you debug discovery and host output. Do not import them from
application code unless a reference page documents the path.
```bash [Terminal]
find .vitehub -maxdepth 3 -type f | sort
cat .vitehub/provision.json
```
Common generated paths include env modules, Workspace types, Agent webhook route
handlers, schedule Nitro bridge files, and deployment output. The installed
packages and selected host determine which files appear.
## Inspect interactive Agent behaviour
Use [the ViteHub CLI](https://vitehub.dev/docs/development/cli) to inspect Agent metadata or run an
Agent locally. Enable the [ViteHub Console](https://vitehub.dev/docs/development/console) when you
need retained session search and invocation event inspection in a browser. The
[Agents overview](https://vitehub.dev/docs/agents) links to registration, invocation, and deployment details.
## Verify before deploy
Run the narrowest check that proves the behavior you changed.
Use a package test for public contract changes, `vitehub agent eval` for Agent behavior, `vitehub provision run --dry-run` for provider resources, and a provider build when generated Provider Output changed.
```bash [Terminal]
pnpm vitehub agent eval
pnpm vitehub provision run --provider cloudflare --dry-run
pnpm build
```
## Next steps
- Open [CLI](https://vitehub.dev/docs/development/cli) for command-owned proof paths.
- Open [Console](https://vitehub.dev/docs/development/console) to inspect retained Agent Invocations in a browser.
- Open [Agent Evals](https://vitehub.dev/docs/agents/evals) for repeatable Agent behavior checks.
- Open [Generated files](https://vitehub.dev/docs/development/generated-files) when a Runtime Registry or Provider Output looks wrong.
- Open [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) for failure families.
# Provisioning
Provision is the ViteHub CLI workflow that creates missing provider resources required by app Definitions.
Provision Steps are package-contributed, idempotent, and create-only; they never delete or mutate existing resources.
## Preview the plan
Run a dry run first.
The CLI loads the Vite config, collects Provision Steps from active package integrations, and prints the actions for one provider.
```bash [Terminal]
CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... pnpm vitehub provision run --provider cloudflare --dry-run
VERCEL_TOKEN=... VERCEL_PROJECT_ID=... pnpm vitehub provision run --provider vercel --dry-run
```
Provider steps use read credentials during planning to distinguish existing resources from resources to create. A dry run does not call `apply()` or write Provision State, but a useful plan still needs the provider credentials required to inspect current state.
```txt [Output]
create d1-database app-content
exists r2-bucket uploads
```
## Apply the plan
The apply command uses the same provider credentials as the plan. Cloudflare and Vercel use different credential sets.
```bash [Terminal]
CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... pnpm vitehub provision run --provider cloudflare
VERCEL_TOKEN=... VERCEL_PROJECT_ID=... VERCEL_TEAM_ID=... pnpm vitehub provision run --provider vercel
```
Vercel Blob provisioning requires `VERCEL_PROJECT_ID` so the provision step can attach `BLOB_READ_WRITE_TOKEN` to the target project. `VERCEL_TEAM_ID` or `VERCEL_ORG_ID` supplies an optional team scope.
After a successful apply, the CLI writes non-secret ids to `.vitehub/provision.json`.
Vite Integrations may read that file as a binding-id source during dev or build.
```json [.vitehub/provision.json]
{
"cloudflare": {
"d1": {
"default": "database-id"
}
}
}
```
## Resource ownership
| Owner | Responsibility |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| ViteHub CLI | Loads Vite config, collects Provision Steps, validates provider credentials, and writes Provision State. |
| Primitive package | Plans and applies resources for the primitive it owns. |
| Provider | Owns cloud resources, credentials, and existing-resource lookup behavior. |
| Vite Integration | Reads Provision State when generated Provider Output needs resource ids. |
## Production boundary
Provision is not a build step.
Builds may read Provision State, but they must not create provider resources.
::warning
Do not commit
`.vitehub/provision.json`
unless a project deliberately decides that non-secret provider ids belong in source control. The root repository ignores
`.vitehub/**`
by default.
::
## Next steps
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) to understand generated host artifacts.
- Use [Cloudflare](https://vitehub.dev/docs/frameworks-hosts/cloudflare) or [Vercel](https://vitehub.dev/docs/frameworks-hosts/vercel) for host boundaries.
- Use [Troubleshooting](https://vitehub.dev/docs/development/troubleshooting) for credential and output failures.
# Troubleshooting
Troubleshooting starts from the failed proof path.
Identify whether the failure comes from discovery, generated files, provider resources, Runtime Helpers, the CLI Dev Loop, Agent behavior, or host output before changing code.
## Quick checks
| Symptom | First check | Proof path |
| ---------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Definition is missing | File path and default export shape | [File conventions](https://vitehub.dev/docs/reference/file-conventions) and `.vitehub/**` |
| Stable import fails | Vite Integration and generated TypeScript includes | [Generated files](https://vitehub.dev/docs/development/generated-files) |
| Provider build fails | Provider Selection and required resource ids | [Provider output](https://vitehub.dev/docs/reference/provider-output) |
| Agent CLI cannot inspect or invoke | Running Vite server and `hubAgent()` registration | [CLI](https://vitehub.dev/docs/development/cli) |
| Agent changed behaviour | Agent Eval result and Agent Usage Record | [Agent Evals](https://vitehub.dev/docs/agents/evals) |
| Agent proof times out | Dev-loop `--timeout`, `agent.eval.testTimeout`, or stalled provider/session setup | [CLI](https://vitehub.dev/docs/development/cli) and [Agent Evals](https://vitehub.dev/docs/agents/evals) |
| Runtime error lacks context | Package error family and diagnostics output | [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) |
## Discovery failures
Discovery Identity comes from the file location.
Do not add inline ids to force a name; move the file to the expected convention instead.
```txt [File tree]
server/
agents/
support.ts
queues/
welcome-email.ts
workspaces/
docs.ts
```
If a package requires a direct default export of a Definition Boundary Helper, avoid named aggregate exports and local indirection.
The direct export keeps Build-Extracted Definition Options inspectable.
## Provider failures
Provider failures usually belong to one of three layers: missing provider credentials, missing Provision State, or invalid Provider Output.
Dry-run provisioning first, then inspect generated host output.
```bash [Terminal]
pnpm vitehub provision run --provider cloudflare --dry-run
pnpm build
find dist -maxdepth 4 -type f | sort
```
## Agent failures
Separate Agent runtime failures from model behavior.
Use `vitehub agent dev` to inspect one interactive Agent Invocation, then use Agent Evals when the failure is repeatable behavior.
If the proof is timing out before it reaches the interesting failure, increase the dev-loop inactivity `--timeout` or the Agent Eval Runner `agent.eval.testTimeout` in `vite.config.ts`.
```bash [Terminal]
pnpm vitehub agent eval server/agents/support.eval.ts --output .vitehub/evals/support.json
```
When the Agent Dev Loop reports `Agent Invocation Stream timed out after of inactivity`, first decide whether the streamed invocation is expected to stay silent longer than the default timeout. If so, rerun with `vitehub agent dev --timeout `. If the timeout is surprising, inspect the Agent Driver boundary and any Workspace or provider session setup before changing prompts. For Capability CLI and `!` Workspace commands, the same option is a wall-clock command deadline because those operations do not emit Agent Invocation Stream events.
## When to escalate
Escalate to the owning ViteHub package when the same failure reproduces outside application code. Include the smallest reproduction, the generated artifact that failed, and the narrow command that demonstrates the problem.
## Next steps
- Use [Verification](https://vitehub.dev/docs/development/verification) to choose the right check.
- Use [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) to classify the failure.
- Use [Local development](https://vitehub.dev/docs/development) to restart from the full proof map.
# Verification
Verification proves that ViteHub primitives keep working across generated output, local provider execution, and live providers.
Use the narrowest tier that covers the risk introduced by the change.
## Verification tiers
| Tier | Runs where | Proves |
| ------------------------ | ----------------------------- | ------------------------------------------------------------------------ |
| Unit or package test | Package test suite | Pure runtime behavior, config normalization, and error branches. |
| Provider Output Contract | Pull request check | Generated Provider Output shape without cloud execution. |
| Local Provider Run | Pull request check | Built Provider Output can execute the application proof fixture locally. |
| Live Smoke | Scheduled provider deployment | Thin real-provider coverage for the same application behaviour. |
| Agent Eval | Local or CI behavior check | Agent Definition behavior and scored Agent Invocations. |
## Run application checks
Run the application's tests before inspecting generated host output. A successful test suite proves application behaviour, while a production-shaped build proves that the selected integrations can generate their current artifacts.
```bash [Terminal]
pnpm test
pnpm build
```
## Verify Provider Output
Provider Output Contracts inspect generated files rather than cloud state.
Use them when the change affects bindings, worker bundles, Vercel Build Output, generated functions, cron entries, or runtime imports.
Inspect the selected host directory after the build. The [Provider output reference](https://vitehub.dev/docs/reference/provider-output) lists the expected artifact families.
## Keep Live Smoke thin
A deployment smoke exercises the same application behavior as the local checks. Keep the deployed check narrow, but verify every provider binding or hosted service that local adapters cannot reproduce.
## Next steps
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated artifact families.
- Use [Generated files](https://vitehub.dev/docs/development/generated-files) to inspect local output.
- Use [Troubleshooting](https://vitehub.dev/docs/development/troubleshooting) when a proof fails.
# Cloudflare
Cloudflare is a Provider Selection for packages that can generate Workers, bindings, queues, workflows, schedules, storage, or sandbox output.
Keep the Definition and Runtime Helper host-neutral. Put Cloudflare details in Integration Options and Provider Output.
## Cloudflare boundaries
| Concern | ViteHub boundary |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Workers and generated bundles | Provider Output written during production-shaped builds. |
| D1, R2, KV, Queues, Browser Run, Rate Limiting bindings, Workflows, and Sandbox resources | Primitive package configuration plus Provision Steps where available. |
| Credentials | Provider env vars for provisioning or host runtime secrets through Server Env. |
| Runtime context | Runtime Host Context passed by the host integration, not app-owned global state. |
| Agent state | Agent Package state provider configuration when Cloudflare-backed state is selected. |
| Cloudflare Computer Boxes | App-owned Computer Durable Object, backend, bindings, migrations, and compatibility flags; ViteHub adapts the configured namespace at runtime. |
## Provider-owned configuration
The primitive package options select Cloudflare. Each provider field stays with the package that owns the primitive.
```ts [vite.config.ts]
import { hubDb } from '@vite-hub/database/vite'
import { hubQueue } from '@vite-hub/queue/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubDb(),
hubQueue(),
],
queue: {
provider: 'cloudflare',
binding: 'JOBS',
},
})
```
Database D1 metadata belongs to each Database Definition because a Vite app can have multiple Named Databases.
```ts [src/database.ts]
import { defineDatabase } from '@vite-hub/database'
import { notes } from './schema'
export default defineDatabase({
cloudflare: {
binding: 'DB',
databaseName: 'app',
},
schema: { notes },
})
```
The Nuxt-only `database.driver: 'd1'` option configures one Nuxt Content and Nitro host resource; it is not a Vite Database Definition shortcut.
## Provision boundary
Provision exposes a dry-run plan before it applies changes. A successful apply writes non-secret ids into `.vitehub/provision.json`; secrets remain in environment variables or provider env stores.
```bash [Terminal]
CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... pnpm vitehub provision run --provider cloudflare --dry-run
CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... pnpm vitehub provision run --provider cloudflare
```
## Generated output
Cloudflare output can include worker bundles, `wrangler.json`, D1 bindings, queue consumers, Rate Limiting bindings, cron triggers, and package-specific runtime imports. A production-shaped build materialises the selected output under `dist`.
```bash [Terminal]
pnpm build
find dist -maxdepth 4 -type f | sort
```
Generate Agent routes through Provider Output. Raw Cloudflare Worker fetch handlers are not a public Agent API.
Required secret Server Env declarations with one exact Env Source are written to `secrets.required` in the generated Wrangler configuration, including each configured named Wrangler environment because secrets are not inherited. Wrangler reuses an existing Worker secret and stops deployment when that binding is absent; ViteHub records only the binding name and never resolves or writes its value during build. Optional secrets, non-secret values, defaults, and alternative source lists remain runtime-only because Wrangler's required list cannot express fallback names.
### Workers Builds
Use Nitro's generated deployment command for production and non-production Workers Builds:
```bash [Deploy command]
pnpm exec nitro deploy --prebuilt
```
When Sandbox is enabled, ViteHub writes an explicit gradual Container rollout into `.output/nitro.json`. This deploys the Worker and its Container application through the same generated contract. A direct Wrangler command with `--containers-rollout=none` bypasses ViteHub's deployment command and can leave a newly scoped Sandbox binding without a Container application to run.
### Rate Limiting bindings
Register the Rate Limit integration. A Cloudflare Nitro preset infers the provider, and each handler-local `requireRateLimit()` policy contributes one `ratelimits` entry to Nitro's Wrangler config. Do not repeat those bindings in `nitro.cloudflare.wrangler`; plain Vite builds continue to write them to generated `wrangler.json`.
```ts [vite.config.ts]
import { hubRateLimit } from '@vite-hub/rate-limit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubRateLimit({ namespace: 'acme-image-service-production' })],
})
```
Cloudflare native enforcement is best-effort and supports 10-second or 60-second fixed windows. The integration rejects `enforcement: 'strict'` and unsupported periods during the build instead of silently weakening the policy. Its inspectable capabilities report location-scoped counters, unknown rejected-attempt behavior, and `availability: 'never'` for every quota metadata field; the binding returns only an allow-or-reject decision.
The generated binding is request-scoped. If it is unavailable at runtime, the handle's `failure` policy decides whether the request is denied or allowed. Give every separately deployed Worker and environment a unique namespace because Cloudflare shares counters with the same namespace ID across Workers in one account. Rate Limiting bindings do not use Cloudflare KV and require no separate resource provisioning.
Workspace adds an Artifacts binding when its Store explicitly selects Cloudflare Artifacts:
```ts [vite.config.ts]
import { hubWorkspace } from '@vite-hub/workspace/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkspace()],
workspace: {
store: {
provider: 'cloudflare-artifacts',
binding: 'WORKSPACE_ARTIFACTS',
namespace: 'vitehub',
},
},
})
```
Inspect the generated `artifacts` entry in `wrangler.json` before deployment. Workspace preserves unrelated app-owned Artifacts bindings. Cloudflare hosting still defaults to the ephemeral `memory` Store because Artifacts requires explicit beta access.
### Browser Run bindings
Register the Browser integration when trusted server code needs Cloudflare Browser Run sessions.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({
preset: 'cloudflare',
browser: true,
}),
],
})
```
ViteHub writes the generated `browser` binding, a compatible default `compatibility_date`, and the `nodejs_compat` flag. Browser Definitions import runtime helpers from `vite-hub/browser`; provider modules and the generated `wrangler.json` are not application import surfaces.
`wrangler dev` can run Browser Run against a local browser. Set `browser: { remote: true }` to keep Worker code local while connecting its Browser binding to Cloudflare, which is useful when a proof must exercise the hosted Browser Run service.
### Cloudflare Computer Boxes
Cloudflare Computer is an optional preview runtime for [Boxes](https://vitehub.dev/docs/agents/boxes). The application configures its `withWorkspace()` Durable Object, execution backends, Worker Loader or Container bindings, migrations, compatibility flags, and deployment lifecycle. Pass the configured Durable Object namespace to ViteHub with `{ kind: 'cloudflare-computer' }`. ViteHub does not create or modify Computer infrastructure.
Keep the Computer filesystem and backend configuration in the Cloudflare integration. Box callers supply inputs and select a registered backend ID without importing Computer filesystem or execution APIs.
## Production notes
Cloudflare local development and deployed Workers do not always expose the same runtime behavior.
Use Provider Output Contracts and Local Provider Runs for pull request checks, then keep Live Smoke thin against real Cloudflare deployments.
::warning
Cloudflare Provider Output can require real Worker bindings such as D1, R2, KV, Rate Limiting, Queues, Durable Objects, Cloudflare Artifacts, or Agent state. Verify generated bindings before deploy, then smoke test the deployed Worker when runtime bindings matter.
::
Agent Definitions run on Cloudflare through generated host output where the Agent integration owns the route. Keep model keys, Durable Object state bindings, and other Runtime Env in Worker bindings.
## Next steps
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for exact package and proof coverage.
- Use [Provisioning](https://vitehub.dev/docs/development/provisioning) for resource creation.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated artifact families.
- Use [Verification](https://vitehub.dev/docs/development/verification) for Cloudflare proof tiers.
# Deno
Deno is an Agent Package runtime target and a host boundary for Deno-shaped Provider Output.
ViteHub keeps Agent Definitions, Schedule Definitions, KV Stores, and Runtime Helpers portable; Deno-specific code stays in generated output and driver configuration.
## Deno boundaries
| Concern | ViteHub boundary |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent chat and webhook routes | Agent Package writes `.vitehub/agent/deno-server.ts` when `runtime: 'deno'` and hosted Agent Definitions exist. It mounts the conventional chat dispatcher and webhook route; route-enabled Channels select which Agents answer chat requests. |
| Static cron schedules | Schedule Package writes `.vitehub/schedule/deno-cron.mjs` for Deno `Deno.cron` wake output. |
| Lightweight state | KV Package can use `driver: 'deno-kv'` and native `Deno.openKv()`. |
| Deployment | Deno Deploy owns app entrypoint configuration, environment variables, permissions, logs, and production rollout. |
## Deno output boundary
The Agent integration selects Deno when generated Agent routes run through `Deno.serve`. Other primitives retain their package-owned runtime options.
```ts [vite.config.ts]
import { hubAgent } from '@vite-hub/agent/vite'
import { hubKv } from '@vite-hub/kv/vite'
import { hubSchedule } from '@vite-hub/schedule/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubAgent({ runtime: 'deno' }),
hubKv(),
hubSchedule(),
],
kv: {
driver: 'deno-kv',
},
})
```
The generated Agent server imports discovered Agent Definitions and mounts both the webhook route pattern and the conventional `/api/_vitehub/agents/[agent]/chat` dispatcher.
If Schedule output exists, the generated server loads `.vitehub/schedule/deno-cron.mjs` before serving requests.
## Generated output
A production-shaped build writes the Deno files under `.vitehub`.
In a Nuxt app, the Agent entrypoint is written under Nuxt's build directory at `.nuxt/vitehub/agent/deno-server.ts` by default; the Schedule output remains project-owned under `.vitehub/schedule`.
```bash [Terminal]
pnpm build
test -f .vitehub/agent/deno-server.ts
find .vitehub -maxdepth 4 -type f | sort
```
The generated server runs locally with Deno network permission for the selected port.
```bash [Terminal]
deno run --allow-net=127.0.0.1:8787 .vitehub/agent/deno-server.ts --host 127.0.0.1 --port 8787
```
A route using `driver: 'deno-kv'` also requires Deno KV support.
```bash [Terminal]
deno run --unstable-kv --allow-net=127.0.0.1:8787 .vitehub/agent/deno-server.ts --host 127.0.0.1 --port 8787
```
For a single discovered `support` Agent with the default chat route enabled, the generated route accepts the following request. The target Agent must attach a route-enabled `webChat()` Channel; Agents without one remain unreachable through the dispatcher.
```bash [Terminal]
curl -X POST http://127.0.0.1:8787/api/_vitehub/agents/support/chat \
-H 'content-type: application/json' \
-d '{"id":"local","messages":[{"id":"user-1","role":"user","parts":[{"type":"text","text":"ping"}]}]}'
```
## Production notes
Deno Deploy uses `.vitehub/agent/deno-server.ts` as the generated server entrypoint.
For Nuxt, configure `.nuxt/vitehub/agent/deno-server.ts` instead, or the equivalent path under a custom `buildDir`.
Do not import generated files from application code to work around deployment configuration; keep application code on Agent Definitions, Runtime Helpers, and stable ViteHub imports.
Use Deno environment variables for model keys and other Runtime Env.
If you use Deno KV, verify the deployed runtime can call `Deno.openKv()` and choose an explicit KV Store when local development must not share production state.
## Next steps
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for the qualified host boundary.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated artifact boundaries.
- Use [Generated files](https://vitehub.dev/docs/development/generated-files) to inspect `.vitehub/**`.
- Use [Config options](https://vitehub.dev/docs/reference/config-options) for Agent `runtime` and KV `driver` placement.
# Frameworks and hosts
ViteHub discovers definitions during the Vite build, then prepares the files and
bindings required by the selected host. Application code keeps using ViteHub
imports instead of generated paths or provider SDKs.
Host support remains package-specific. A host can support one primitive without supporting every ViteHub package, and a Runtime Helper can work without a ViteHub-generated deployment bundle.
## Choose a host
| Need | Open |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Compare current host coverage and proof maturity | [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) |
| Generate Cloudflare Worker output and bindings | [Cloudflare](https://vitehub.dev/docs/frameworks-hosts/cloudflare) |
| Generate Vercel Build Output | [Vercel](https://vitehub.dev/docs/frameworks-hosts/vercel) |
| Use package-specific Netlify functions and Blob runtime | [Netlify](https://vitehub.dev/docs/frameworks-hosts/netlify) |
| Run Agent routes, schedules, or KV on Deno | [Deno](https://vitehub.dev/docs/frameworks-hosts/deno) |
| Understand package-owned Nitro bridges | [Nitro and UnJS](https://vitehub.dev/docs/frameworks-hosts/nitro-unjs) |
| Mount supported helpers in a Node-shaped server | [Node and self-hosted](https://vitehub.dev/docs/frameworks-hosts/node-self-hosted) |
## What the Vite integration does
| Step | Result |
| -------------------- | ------------------------------------------------------------------------------------ |
| Discover definitions | Finds named Agents, Workspaces, queues, workflows, and other configured resources. |
| Generate registries | Lets server code load discovered definitions by name. |
| Resolve options | Applies the host and provider choices from `vite.config.ts`. |
| Write host output | Generates only the bindings, routes, functions, or config supported by that package. |
## Compose integrations
Use `vite-hub` in applications. Queue remains opt-in, and each feature keeps its
own public import.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [vitehub({ preset: "node" })],
})
```
Register an individual `hubX()` integration from its `@vite-hub/*/vite` package
when a library or focused integration needs direct control.
## Keep runtime imports stable
Import server APIs through documented ViteHub paths. Do not import framework
virtual modules or generated files unless a reference page marks the path public.
```ts [server/settings.ts]
import { kv } from 'vite-hub/kv'
export async function saveSettings(settings: Record) {
const [error] = await kv.set('settings', settings)
if (error) throw error
}
```
## Inspect the output
Vite development proves discovery and local generation. A production build
creates the output for the selected host. Netlify development can also create
functions for the Netlify CLI.
```bash [Terminal]
pnpm dev
find .vitehub -maxdepth 4 -type f | sort
pnpm build
```
## Next steps
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) before making a portability claim.
- Use [File conventions](https://vitehub.dev/docs/reference/file-conventions) for discovery paths.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated host artifacts.
- Use [Local development](https://vitehub.dev/docs/development) for proof paths.
# Netlify
Netlify support is package-specific. ViteHub currently provides Netlify-owned behaviour for Blob, generated Agent HTTP routes, and static Schedule wake functions; it does not expose a platform-wide Netlify provider for every primitive.
## Available boundaries
| Surface | Current contract |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Blob | `hubBlob()` selects the `netlify-blobs` driver when the build reports Netlify hosting. Application code continues to use `@vite-hub/blob`. |
| Agent routes | `hubAgent()` writes one `vitehub-agent` function when hosted Agent Definitions exist. It mounts the conventional chat dispatcher and webhook route; route-enabled Channels select which Agents answer chat requests. `routes.discordGateway` remains explicit. |
| Static schedules | `hubSchedule()` writes one scheduled Netlify function per discovered static Schedule Definition. |
| Local proof | The repository runs a real-project fixture through Netlify CLI in pull-request CI. |
Agent function output lives under `.netlify/v1/functions`, with its generated source wrapper under `.vitehub/agent/netlify-function.mjs`. The wrapper and deployed function are Provider Output, not public application imports.
In a Nuxt app, the source wrapper follows Nuxt's build directory and is normally `.nuxt/vitehub/agent/netlify-function.mjs`; the deployed function path is unchanged.
## Package output composition
Each active package integration contributes only its owned Netlify output.
```ts [vite.config.ts]
import { hubAgent } from '@vite-hub/agent/vite'
import { hubBlob } from '@vite-hub/blob/vite'
import { hubSchedule } from '@vite-hub/schedule/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubBlob(),
hubSchedule(),
hubAgent(),
],
})
```
Netlify environment detection selects the Blob driver and Agent function output. Static Schedule Definitions generate Netlify functions alongside the other supported Schedule output families.
## Generated functions
A Netlify-shaped build writes the generated functions and wrappers to their provider-owned directories.
```bash [Terminal]
pnpm build
find .netlify/v1/functions -maxdepth 2 -type f | sort
find .vitehub/agent -maxdepth 2 -type f | sort
```
For Nuxt, replace the wrapper inspection command with `find .nuxt/vitehub/agent -maxdepth 2 -type f | sort`, or use the equivalent path under a custom `buildDir`.
## Unsupported inference
ViteHub does not infer native Netlify providers for Queue, Workflow, or Sandbox. Disable an unused preset integration or select an explicit supported provider only when that external provider is valid from the Netlify runtime.
The ViteHub Provision CLI does not create Netlify resources. It currently accepts Cloudflare and Vercel plans only.
Netlify-specific KV Provider Output is also not provided. Configure a remote KV driver explicitly for deployed state; do not rely on the local `fs-lite` fallback in a serverless deployment.
Workspace has no Netlify-specific hosted store. Select a durable remote Workspace store explicitly; the local filesystem fallback does not persist safely across serverless instances.
## Production proof
Pull-request CI exercises the Netlify output through Netlify CLI. ViteHub does not currently publish a deployed Netlify Live Smoke, so verify the generated functions in the target Netlify site before treating an application-specific combination as production-proven.
## Related pages
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
- [Provider output](https://vitehub.dev/docs/reference/provider-output)
- [Import paths](https://vitehub.dev/docs/reference/import-paths)
# Nitro and UnJS
ViteHub is not a Nitro module system.
ViteHub uses Vite Integrations as the public integration layer, while package-owned Nitro wiring appears only where a host boundary needs generated runtime hooks.
## Boundary
| Layer | ViteHub expectation |
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| Vite | Public integration layer for discovery, generated files, the CLI Agent Dev Loop, and Provider Output. |
| Nitro | Host runtime bridge when a package must register generated handlers, middleware, or runtime hooks. |
| UnJS libraries | Useful implementation dependencies for server primitives, not public ViteHub framework identity. |
| Application server code | Calls Runtime Helpers and stable handlers without importing generated Nitro internals. |
## Accepted Nitro handoffs
| Bridge | Status | Owner | Purpose |
| ------------------------------- | ---------------------------------------- | ----------------- | ------------------------------------------------------------------------------------ |
| Schedule Provider Wake | Available | Schedule Package | Registers Cloudflare scheduled runtime hooks and cron output for Nitro-shaped hosts. |
| Workspace hosted runtime setup | Available where hosted stores require it | Workspace Package | Moves generated Workspace runtime setup into Nuxt's top-level Nitro config. |
| Database Nuxt D1 host wiring | Available for Nuxt D1 host resources | Database Package | Keeps one D1 Database Host Resource in sync with Nuxt Content and Cloudflare output. |
| General Nitro-first integration | Not the public direction | Not applicable | ViteHub keeps the public contract on Vite Integrations. |
## Generated route output
::warning
Auth and Agent integrations generate Nitro handlers for their owned routes. Treat those files as Provider Output, not as a general Nitro Framework Integration or a public
`@vite-hub/*/nitro`
authoring surface.
::
| Output | Current owner | Boundary |
| ----------------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
| Auth route handler | Auth Package | Exposes the configured Auth route through a generated Nitro handler. |
| Agent chat and webhook routes | Agent Package | Dispatches generated Agent route output without making Nitro discovery or route files the app API. |
## Package-owned handlers
The package Vite Integration reads the application Auth Definition and generates Nitro route output when `route` is enabled.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
database: true,
})
```
```ts [vite.config.ts]
import { hubAuth } from '@vite-hub/auth/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubAuth(),
],
})
```
## What not to do
Do not treat Nitro route files as the primary ViteHub API.
If a package generates Nitro output, inspect it as Provider Output and keep application code on the package's Runtime Helpers or stable server handler.
## Next steps
- Use [Frameworks and hosts](https://vitehub.dev/docs/frameworks-hosts) for the public integration model.
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for the complete qualified matrix.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated Nitro and host artifacts.
- Use [Import paths](https://vitehub.dev/docs/reference/import-paths) for public imports.
# Node and self-hosted
Node and self-hosted runtimes can use ViteHub server primitives when the package exposes host-neutral Runtime Helpers or stable server handlers.
Unified self-hosted Provider Output is not the default contract for every package.
## What works today
| Surface | Status | Boundary |
| ------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------- |
| Runtime Helpers in server code | Available per package | Use the package root or runtime subpath imports. |
| Local filesystem and memory providers | Available where a primitive supports them | Useful for development and simple self-hosted setups. |
| Stable server handlers | Available per package | Mount the handler the package marks stable, such as Auth server behavior. |
| Unified self-hosted Provider Output | Not provided | ViteHub does not emit one general Node deployment bundle for all primitives. |
## Runtime Helper boundary
Server code calls the same Runtime Helpers used in hosted applications. The provider or store choice remains in package configuration.
```ts [server/settings.ts]
import { kv } from '@vite-hub/kv'
export async function saveSettings(settings: Record) {
const [error] = await kv.set('settings', settings)
if (error) throw error
}
```
```ts [vite.config.ts]
import { hubKv } from '@vite-hub/kv/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubKv(),
],
kv: {
driver: 'fs-lite',
base: '.vitehub/data/kv',
},
})
```
## Auth handler boundary
The Auth Package exposes a stable server handler that a Node framework can mount through its request API. The handler comes from the application Definition; generated host files remain implementation details unless the package reference marks them public.
```ts [server/manual-auth-handler.ts]
import { defineAuth } from '@vite-hub/auth'
import { createAuthHandler } from '@vite-hub/auth/server'
const definition = defineAuth({
appName: 'Acme',
route: false,
})
export const handleAuth = createAuthHandler(definition)
```
`handleAuth` accepts a Web `Request` and returns a `Promise`. Adapt that handler at the Node framework boundary instead of importing a generated Nitro route.
## Production notes
Self-hosted deployments must make durability explicit.
Memory providers and single-process local state are development providers, not production coordination systems.
Use Server Env for runtime secrets, configure durable stores for stateful primitives, and add verification that starts the deployed Node process rather than only typechecking package code.
## Next steps
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for the qualified self-hosted boundary.
- Use [Import paths](https://vitehub.dev/docs/reference/import-paths) for stable runtime imports.
- Use [Config options](https://vitehub.dev/docs/reference/config-options) for local and hosted providers.
- Use [Verification](https://vitehub.dev/docs/development/verification) to choose a self-hosted proof path.
# Runtime and host support
`Package-specific` means support belongs to the named package or generated output, not the host as a whole.
## Server primitives
| Primitive | Local Vite | Cloudflare | Vercel | Netlify | Deno | Nitro and UnJS | Node and self-hosted |
| ---------- | ---------------- | --------------------- | --------------------- | -------------------- | ---------------------- | -------------------- | --------------------- |
| Browser | Local provider | Browser Run | — | — | — | — | Local provider |
| Blob | `fs` | R2 | Vercel Blob | Netlify Blobs | S3-compatible | Host driver | `fs` or S3-compatible |
| Database | SQLite | D1 | libSQL or D1 HTTP | libSQL | libSQL | Nuxt D1 | SQLite or libSQL |
| Email | Unemail driver | Cloudflare Email | Unemail driver | Unemail driver | Unemail driver | Host driver | Unemail driver |
| KV | `fs-lite` | Workers KV | Upstash Redis | Upstash Redis | Deno KV | Host driver | `fs-lite` or Upstash |
| Queue | Discovery only | Cloudflare Queues | Vercel Queues | Cloudflare or Vercel | — | Cloudflare or Vercel | — |
| Rate Limit | `memory` | Rate Limiting binding | — | — | — | Cloudflare | `memory` |
| Realtime | `memory` | Durable Objects | — | — | — | Host authority | `memory` |
| Sandbox | Box provider | Cloudflare Sandbox | Vercel Sandbox | Vercel Sandbox | — | Cloudflare or Vercel | Box provider |
| Schedule | Local or process | Cron triggers | Vercel Cron Jobs | Scheduled functions | Standalone `Deno.cron` | Provider Wake | Process runtime |
| Workflow | OpenWorkflow | Cloudflare Workflows | Vercel Workflow | OpenWorkflow | OpenWorkflow | Host provider | OpenWorkflow |
| Workspace | Local or memory | Artifacts or GitHub | Vercel Blob or GitHub | GitHub | GitHub | Host store | Local or GitHub |
Names in this table are concrete built-in providers or adapters. Browser Definitions currently require the Cloudflare preset; local Wrangler can connect to Browser Run with remote mode. Trusted local and self-hosted Node processes can call `createBrowser({ provider: localBrowser({ executablePath }) })`, but Browser Definitions do not select that provider. Email's boolean default selects Cloudflare Email only on the Cloudflare preset; every other host requires an explicit compatible Unemail driver. Realtime production uses Cloudflare Durable Objects or explicitly selected memory on a single-process Node server; distributed Vercel, Netlify, and Deno presets reject memory. A remote provider shown under Netlify, Deno, Nitro, or Node is an explicit package choice, not host inference. Local filesystem and memory options remain single-process development providers.
Local Vite discovers Queue Definitions and generates provider output, but it does not deliver Queue Jobs. Netlify requires an explicit Cloudflare or Vercel Queue Provider because it cannot infer one. Netlify can use an explicit Vercel Sandbox provider when Vercel credentials are configured; the Cloudflare Sandbox provider requires a Cloudflare binding and cannot run on Netlify.
## Deployment and proof
| Contract | Local Vite | Cloudflare | Vercel | Netlify | Deno | Nitro and UnJS | Node and self-hosted |
| ------------------------- | ------------------- | -------------------- | -------------------- | ---------------------------- | ---------------------------- | ---------------------------- | ---------------------------- |
| Runtime helpers | **Available** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** |
| Local providers | **Available** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** | **Not provided** | **Local-only** |
| Generated Provider Output | **Not provided** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** | **Package-specific** | **Not provided** |
| Provision support | **Not provided** | **Package-specific** | **Package-specific** | **Not provided** | **Not provided** | **Not provided** | **Not provided** |
| Contract tests | **Contract-tested** | **Contract-tested** | **Contract-tested** | **Contract-tested** | **Contract-tested** | **Contract-tested** | **Contract-tested** |
| Local Provider Run | — | ✓ | ✓ | ✓ | — | — | — |
| Live Smoke | — | ✓ | ✓ | **Live proof not published** | **Live proof not published** | **Live proof not published** | **Live proof not published** |
Cloudflare's nightly run covers nine primitives, including Rate Limit. Vercel covers eight because ViteHub has no native Vercel Rate Limit driver. Browser and Agent routes have contract tests but are outside those deployed runs.
## Qualifications
- **Local Vite:** Active integrations expose their package imports and generated registries. Blob `fs`, KV `fs-lite`, Rate Limit `memory`, and Workspace `local` or `memory` provide local state. A local build can still generate output for an explicit or inferred hosted provider.
- **Cloudflare:** Blob, Database, KV, Queue, Rate Limit, Sandbox, Schedule, Workflow, and Workspace run in the live playground. Browser and Agent have package-owned output outside the nightly run. Enabled integrations compose the Worker, `wrangler.json`, bindings, callbacks, and runtime modules. ViteHub can provision R2 buckets, D1 databases, and Cloudflare Queues.
- **Vercel:** Blob, Database, KV, Queue, Sandbox, Schedule, Workflow, and Workspace run in the live playground. Agent routes have separate package output outside the nightly run. Enabled integrations write Vercel Build Output, functions, routes, cron entries, and runtime modules. ViteHub can create a Blob store and configure the project environment.
- **Netlify:** Blob uses `netlify-blobs`. Agent HTTP routes and static Schedules write functions under `.netlify/v1/functions`. CI runs the real-project fixture through Netlify CLI. ViteHub does not provide Netlify provisioning or published live proof.
- **Deno:** Agent chat and webhook routes and KV with `deno-kv` are supported with their documented permissions. The standalone Schedule integration writes a `Deno.cron` entrypoint, but `vitehub({ preset: "deno", schedule: true })` rejects Schedule because that output is outside the deployed Nitro entrypoint. ViteHub does not generate a general Deno bundle or publish live proof.
- **Nitro and UnJS:** Auth and Agent handlers, the Schedule Nitro bridge, Workspace runtime setup, and Database Nuxt D1 wiring are package-owned integrations. Nitro is integration glue rather than a storage or execution provider. ViteHub does not provide Nitro provisioning or one unified live matrix.
- **Node and self-hosted:** Server APIs and handlers run when their selected driver supports Node. Blob `fs`, KV `fs-lite`, Rate Limit `memory`, and Workspace `local` or `memory` are single-process providers. ViteHub does not emit one Node deployment bundle, provision a self-hosted plan, or publish one live suite.
Local memory and filesystem providers stay single-process after deployment. Generated files remain package-owned and must not be imported by application code.
# Vercel
Vercel is a Provider Selection for packages that can emit Vercel Build Output, functions, queues, workflows, blob-backed storage, or sandbox integration.
ViteHub keeps Definitions portable and moves Vercel-specific behavior into package Integration Options and Provider Output.
## Vercel boundaries
| Concern | ViteHub boundary |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Build Output | Generated `.vercel/output/**` files written by package integrations. |
| Vercel Blob | Blob Store or Workspace Store configuration, depending on which primitive owns the behavior. |
| Vercel Queues | Queue provider configuration and generated callback output. |
| Vercel Workflow | Workflow provider configuration and generated runtime output. |
| Vercel Sandbox | Sandbox Provider configuration, with Sandbox Identity passed only when the run needs reuse. |
| External Database | A Database Definition backed by Cloudflare D1 over authenticated HTTP, or Database integration `connection` backed by hosted libSQL. |
| Credentials | `VERCEL_TOKEN` and `VERCEL_PROJECT_ID` for Blob Provision, with an optional team id; Server Env for app runtime secrets. |
## Provider-owned configuration
The package that owns a primitive also owns its Vercel selection. Provider fields remain in Integration Options when they affect generated output rather than one runtime invocation.
```ts [vite.config.ts]
import { hubBlob } from '@vite-hub/blob/vite'
import { hubQueue } from '@vite-hub/queue/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubBlob(),
hubQueue(),
],
blob: {
driver: 'vercel-blob',
},
queue: {
provider: 'vercel',
region: 'iad1',
},
})
```
::warning
Vercel-hosted state needs hosted stores. Use
`driver: 'vercel-blob'`
with
`BLOB_READ_WRITE_TOKEN`
for Blob, and use Upstash-backed KV with
`KV_REST_API_URL`
and
`KV_REST_API_TOKEN`
when KV runs on Vercel. Local filesystem stores are development-only in Vercel deployments.
::
Database Definitions own tables and identity, while the Database Integration selects the hosted connection used by Vercel output. Use Runtime Env declarations for Marketplace-provisioned credentials so generated output reads them at runtime.
A Definition uses Cloudflare D1 from Vercel only when it declares `cloudflare.http`. Set it to `true` for Cloudflare's D1 raw API, then configure `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` as Vercel Server Env. Cloudflare deployments still prefer the D1 binding.
```ts [server/databases/config.ts]
import { defineDatabase } from '@vite-hub/database'
import { notes } from './schema'
export default defineDatabase({
cloudflare: {
databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID,
databaseName: process.env.CLOUDFLARE_D1_DATABASE_NAME,
http: true,
},
schema: { notes },
})
```
For sustained application traffic, Cloudflare recommends a proxy Worker because its built-in D1 REST API is intended primarily for administrative use and shares the global Cloudflare API rate limit. Set `cloudflare.http` to `{ url, authToken }` for an authenticated raw-compatible HTTP(S) proxy. Omitting `cloudflare.http` preserves the hosted libSQL selection even when the Definition includes a D1 database id.
```ts [vite.config.ts]
import { hubDb } from '@vite-hub/database/vite'
import { env, hubEnv } from '@vite-hub/env/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubEnv(),
hubDb({
connection: {
url: env({ source: env.source('TURSO_DATABASE_URL') }),
authToken: env({ secret: true, source: env.source('TURSO_AUTH_TOKEN') }),
},
}),
],
})
```
## Provision boundary
Provision exposes a dry-run plan before applying actions. `VERCEL_TEAM_ID` or `VERCEL_ORG_ID` supplies team scope when the token requires it.
```bash [Terminal]
VERCEL_TOKEN=... VERCEL_PROJECT_ID=... pnpm vitehub provision run --provider vercel --dry-run
VERCEL_TOKEN=... VERCEL_PROJECT_ID=... VERCEL_TEAM_ID=... pnpm vitehub provision run --provider vercel
```
## Generated output
Vercel output appears under `.vercel/output`. Server functions include their own `.vc-config.json`, while the root output config describes routing and build metadata.
```bash [Terminal]
pnpm build
find .vercel/output -maxdepth 4 -type f | sort
```
## Production notes
Vercel provider output makes provider-specific runtime packages reachable only when selected.
If a build bundles an unselected provider dependency, treat that as a Provider Output Contract issue in the owning package.
Agent Definitions run on Vercel through generated host output where the Agent integration owns the route. Keep model keys, state credentials, and other Runtime Env in Vercel environment variables.
## Next steps
- Use [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) for exact package and proof coverage.
- Use [Provisioning](https://vitehub.dev/docs/development/provisioning) for provider resource ids.
- Use [Config options](https://vitehub.dev/docs/reference/config-options) for Provider Selection placement.
- Use [Provider output](https://vitehub.dev/docs/reference/provider-output) for generated Vercel output.
# First Agent
An Agent is a server file that tells ViteHub what to run. Every Agent needs a
Driver, which can be a function, model, or coding provider such as Codex or Claude
Code, among others. You can add Capabilities, Channels, Workspace access, and
other options later.
This tutorial starts with a function that returns a fixed greeting. It runs
offline and needs no credentials.
::note
You need Node.js 24.15 or newer and
`pnpm`
. This project runs completely offline.
::
## Install ViteHub and the server packages
Create an empty project, then install ViteHub with Vite and H3.
```bash [Terminal]
mkdir vitehub-agent-start
cd vitehub-agent-start
pnpm init
pnpm pkg set type=module
pnpm add vite-hub h3 vite
```
## Configure the server build
Add `vitehub()` to the Vite config. Vite builds `src/server.ts` for Node.js, and
ViteHub discovers Agent Definitions under `server/agents`.
```ts [vite.config.ts]
import { resolve } from "node:path"
import { defineConfig } from "vite"
import { vitehub } from "vite-hub"
export default defineConfig({
root: import.meta.dirname,
appType: "custom",
build: {
outDir: "dist",
rollupOptions: {
input: resolve(import.meta.dirname, "src/server.ts"),
output: { entryFileNames: "server.js" },
},
ssr: true,
},
plugins: [vitehub({
preset: "node",
agent: true,
env: false,
})],
ssr: {
external: ["vite-hub/agent"],
},
})
```
## Define the greeting Agent
Create `server/agents/greeting.ts`. Its required `driver.run` function reads the
prompt and returns the greeting without calling a provider.
```ts [server/agents/greeting.ts]
import { defineAgent } from "vite-hub/agent"
export default defineAgent({
driver: {
run({ prompt }) {
const name = typeof prompt === "string" ? prompt : "friend"
return {
text: `Hello, ${name}. This result came from an Agent Invocation.`,
}
},
},
})
```
## Call the Agent from H3
`runAgent()` takes the Definition, runtime values for the current request, and
the invocation input. The route creates a new memo cache for each request,
identifies Vite as the runtime, and reports errors from background tasks.
```ts [src/server.ts]
import { createServer } from "node:http"
import { H3, readBody } from "h3"
import { toNodeHandler } from "h3/node"
import { runAgent } from "vite-hub/agent"
import greeting from "../server/agents/greeting"
function createMemo() {
const values = new Map()
return (key: string, create: () => T): T => {
if (!values.has(key)) values.set(key, create())
return values.get(key) as T
}
}
const app = new H3().post("/greet", async (event) => {
const body = await readBody<{ name?: string }>(event) || {}
return await runAgent(greeting, {
memo: createMemo(),
runtime: "vite",
waitUntil: task => { void task.catch(error => console.error(error)) },
}, {
prompt: body.name?.trim() || "friend",
})
})
const port = Number(process.env.PORT || 5173)
createServer(toNodeHandler(app)).listen(port, () => {
console.log(`ViteHub Agents tutorial listening on http://localhost:${port}`)
})
```
The route imports the Definition directly, so the greeting returns in the same
request.
## Run the Agent and see the response
Build the project and start the generated Node.js server.
```bash [Terminal]
pnpm vite build
node dist/server.js
```
From another terminal, send a name to the H3 route.
```bash [Terminal]
curl -X POST http://localhost:5173/greet \
-H 'content-type: application/json' \
-d '{"name":"Ada"}'
```
The Agent returns the greeting:
```json [Response]
{"text":"Hello, Ada. This result came from an Agent Invocation."}
```
From here, add only what your Agent needs:
- Read [Agent Definitions](https://vitehub.dev/docs/agents/agent-definitions) to choose another Driver or add Channels, Workspace context, trusted caller settings, or hooks.
- Read [Capabilities](https://vitehub.dev/docs/capabilities) before you give a model tools, triggers, policy, metadata, or context values.
- Read [Invocations](https://vitehub.dev/docs/agents/invocations) when the route needs streaming or failure handling.
# First Server Primitive
This quickstart adds a local KV store to a small H3 server. One request writes a
value, reads it back, and returns the result.
::note
You need Node.js 24.15 or newer and
`pnpm`
. The first result runs locally without
an account or credential.
::
## Install KV
Create an empty project and install ViteHub with Vite and H3.
```bash [Terminal]
mkdir vitehub-kv-start
cd vitehub-kv-start
pnpm init
pnpm pkg set type=module
pnpm add vite-hub h3 vite
```
## Configure the Vite Integration
Register `vitehub()` and select the file-backed local KV driver. The explicit
configuration stores values under `.vitehub/data/kv`.
```ts [vite.config.ts]
import { resolve } from "node:path"
import { defineConfig } from "vite"
import { vitehub } from "vite-hub"
export default defineConfig({
root: import.meta.dirname,
appType: "custom",
build: {
outDir: "dist",
rollupOptions: {
input: resolve(import.meta.dirname, "src/server.ts"),
output: { entryFileNames: "server.js" },
},
ssr: true,
},
plugins: [
vitehub({
preset: "node",
blob: false,
env: false,
kv: { driver: "fs-lite", base: ".vitehub/data/kv" },
}),
],
})
```
## Write and read one value
Create one H3 route and use `kv` to write and read the setting.
```ts [src/server.ts]
import { createServer } from "node:http"
import { H3, readBody } from "h3"
import { toNodeHandler } from "h3/node"
import { kv } from "vite-hub/kv"
const app = new H3().post("/settings", async (event) => {
const settings = await readBody<{ theme: string }>(event)
const [writeError] = await kv.set("settings", settings)
if (writeError) throw writeError
const [readError, storedSettings] = await kv.get("settings")
if (readError) throw readError
return { settings: storedSettings }
})
const port = Number(process.env.PORT || 5173)
createServer(toNodeHandler(app)).listen(port, () => {
console.log(`ViteHub KV tutorial listening on http://localhost:${port}`)
})
```
## Run the server
Build and start the generated Node.js entry.
```bash [Terminal]
pnpm vite build
node dist/server.js
```
Send a value from another terminal.
```bash [Terminal]
curl -X POST http://localhost:5173/settings \
-H 'content-type: application/json' \
-d '{"theme":"system"}'
```
The response proves that the route wrote and read through ViteHub:
```json [Response]
{"settings":{"theme":"system"}}
```
To move to a hosted store, change the provider in `vite.config.ts`. The server
route keeps importing `kv` from `vite-hub/kv`.
## Next steps
- Follow the longer [Server Primitives tutorial](https://vitehub.dev/blog/server-primitives) for a complete walkthrough.
- Read [KV](https://vitehub.dev/docs/server-primitives/kv) for named stores and hosted drivers.
- Read [Runtime Helpers and stable imports](https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports) to see how provider changes stay out of server code.
# Introduction
ViteHub adds a server layer to Vite. Server Primitives give application code
APIs for storage, background work, auth, and other server features. Agents
combine those APIs with models, coding providers, or application code.
Agents can use Server Primitives. Server Primitives also work on their own.
Start with the path that matches the result your product needs today.
::u-page-grid{.not-prose.mt-8.sm:grid-cols-2}
:::u-page-card
---
description: Store a value in local KV and read it from a server route.
icon: i-lucide-server-cog
title: Use a Server Primitive
to: https://vitehub.dev/docs/getting-started/first-server-primitive
---
:::
:::u-page-card
---
description: Define an Agent and inspect the result of one invocation.
icon: i-lucide-bot
title: Run an Agent
to: https://vitehub.dev/docs/getting-started/first-agent
---
:::
::
## Choose a first path
Start with Server Primitives when application code needs auth, environment
values, storage, files, background work, or isolated execution. Your server
code calls the ViteHub API, and the Vite integration connects it to the
selected provider.
Start with Agents when the product needs a named server-side actor. An Agent
Definition puts its instructions, execution method, Capabilities, and
Workspace in one file.
| You want to build | Start here |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Settings, feature flags, caches, cursors, or small records | [First Server Primitive](https://vitehub.dev/docs/getting-started/first-server-primitive) |
| A support, coding, research, or workspace-aware actor | [First Agent](https://vitehub.dev/docs/getting-started/first-agent) |
| Relational data, uploads, background work, workflows, schedules, or sandboxes | [Server Primitives](https://vitehub.dev/docs/server-primitives) |
| Model-facing tools, guarded product abilities, or chat entry points | [Capabilities](https://vitehub.dev/docs/capabilities) |
## What ViteHub does
Most features use the same path:
| Part | What it does |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| Vite integration | Finds definitions and prepares the selected provider during development and build. |
| Definition | Declares named work or state, such as an Agent, Workspace, Queue, Workflow, or Schedule. |
| Server API | Lets application code call a feature through an import such as `kv`, `useWorkspace()`, or `runAgent()`. |
| Capability | Lets an Agent use a selected operation such as `workspaceShell()` or `kv()`. |
Application code uses ViteHub imports. The integration handles the
provider-specific routes, bindings, and files.
## Verify your setup
Check the ViteHub plugin in `vite.config.ts`, the definition file when the
feature needs one, and the ViteHub call in server code. Each first guide ends
with a response you can inspect before adding another feature.
## Next steps
- Read [Installation](https://vitehub.dev/docs/getting-started/installation) to start with the framework distribution or choose a direct owner package for advanced composition.
- Follow the longer [Server Primitives tutorial](https://vitehub.dev/blog/server-primitives).
- Follow the longer [Agents tutorial](https://vitehub.dev/blog/agents).
- Open [Concepts](https://vitehub.dev/docs/concepts) when you need the full runtime model.
# Installation
Install `vite-hub` when you are building an application. It provides the Vite
integration and public feature imports through one dependency.
## Prerequisites
- Node.js 24.15 or newer.
- Vite 8 or newer.
- An ESM package with `"type": "module"` or a `vite.config.mts` file.
- A package manager such as `pnpm`, `npm`, `yarn`, or `bun`.
Model providers and hosted primitives may require credentials. Each feature
guide lists its own environment, network, and billing prerequisites before the
first call.
## Install the framework distribution
Add `vite-hub` to an existing Vite application.
```bash [Terminal]
pnpm add vite-hub
```
Register the framework integration in Vite.
```ts [vite.config.ts]
import { vitehub } from "vite-hub"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [
vitehub({ preset: "node" }),
],
})
```
In Nuxt, register the framework module. It installs the same Vite integrations
and carries their Nitro configuration through Nuxt's lifecycle.
```ts [nuxt.config.ts]
import viteHubNuxt from "vite-hub/nuxt"
export default defineNuxtConfig({
modules: [
[viteHubNuxt, { preset: "node" }],
],
})
```
Import application APIs from explicit feature subpaths.
```ts [server/agents/support.ts]
import { defineAgent } from "vite-hub/agent"
import { access } from "vite-hub/agent/capabilities"
import { requireRateLimit } from "vite-hub/rate-limit"
import { defineWorkspace } from "vite-hub/workspace"
```
Install third-party model providers and chat adapters separately. Built-in coding
providers use the provider runtime pinned by ViteHub. The distribution includes
the Workflow DevKit runtime and builders for Vercel Workflow; install other
provider SDKs only when you use them.
Until T3 publishes the provider runtime on npm, pnpm consumers using a built-in coding provider must set `blockExoticSubdeps: false` in `pnpm-workspace.yaml`; ViteHub pins an exact pkg.pr.new tarball rather than a moving branch.
## Install an owner package directly
Every `@vite-hub/*` package can also be installed on its own. Use a package
directly when you are building a library or need to configure one integration
without the framework distribution.
| Path | Direct install | Integration |
| ----------------- | ------------------------------------ | ------------------------------------------------- |
| Server Primitives | `pnpm add @vite-hub/kv vite` | `hubKv()` from `@vite-hub/kv/vite` |
| Rate Limit | `pnpm add @vite-hub/rate-limit vite` | `hubRateLimit()` from `@vite-hub/rate-limit/vite` |
| Agents | `pnpm add @vite-hub/agent vite` | `hubAgent()` from `@vite-hub/agent/vite` |
::tip
Start new applications with
`vite-hub`
. Direct owner packages are the
escape hatch when package-level control is the goal.
::
## Add generated types
Some packages write types under `.vitehub/types`. Include that directory when
your application uses generated names or stable `#vitehub/...` imports.
```json [tsconfig.json]
{
"include": [
"server/**/*.ts",
"src/**/*.ts",
".vitehub/types/**/*.d.ts"
]
}
```
## Verify the integration
Run the application with its Vite-based development command. If a server API
is missing its Vite integration, ViteHub reports the configuration error instead
of selecting another provider.
The two first-success guides include complete build and runtime commands:
- [First Server Primitive](https://vitehub.dev/docs/getting-started/first-server-primitive) stores and reads a KV value without credentials.
- [First Agent](https://vitehub.dev/docs/getting-started/first-agent) runs a deterministic Agent Invocation without a model key.
## Next steps
- Read [Vite Integrations and Provider Output](https://vitehub.dev/docs/concepts/vite-integrations-and-provider-output) to understand integration ownership.
- Open [Server Primitives](https://vitehub.dev/docs/server-primitives) to choose infrastructure.
- Open [Agents](https://vitehub.dev/docs/agents) to choose an Agent Driver and Capabilities.
# ViteHub docs
ViteHub adds a server layer to Vite. Use **Server Primitives** directly from application code, or combine them with models and tools in an **Agent**.
Server Primitives work without an Agent. When you build an Agent, Capabilities control which operations it can use.
::u-page-grid{.not-prose.mt-8}
:::u-page-card
---
description: Add auth, storage, queues, schedules, sandboxes, and other server APIs.
icon: i-lucide-server-cog
title: Server primitives
to: https://vitehub.dev/docs/server-primitives
---
:::
:::u-page-card
---
description: Define an Agent, select its Capabilities and Workspace, then run
and inspect it.
icon: i-lucide-bot
title: Agents
to: https://vitehub.dev/docs/agents
---
:::
:::u-page-card
---
description: Install ViteHub and run one Server Primitive or Agent.
icon: i-lucide-rocket
title: Get started
to: https://vitehub.dev/docs/getting-started
---
:::
:::u-page-card
---
description: Learn how definitions, integrations, Workspaces, Sources, and
Capabilities fit together.
icon: i-lucide-map
title: Concepts
to: https://vitehub.dev/docs/concepts
---
:::
::
## Find what you need
| You are building | Start with |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| A new installation | [Get started](https://vitehub.dev/docs/getting-started) |
| Auth, storage, background work, schedules, sandboxes, or environment values | [Server primitives](https://vitehub.dev/docs/server-primitives) |
| Model-backed actors, Agent Invocations, triggers, chat history, evals, or CLI inspection | [Agents](https://vitehub.dev/docs/agents) |
| Tools and product operations that an Agent can use | [Capabilities](https://vitehub.dev/docs/capabilities) |
| The difference between definitions, server APIs, Workspaces, Sources, and Capabilities | [Concepts](https://vitehub.dev/docs/concepts) |
| Host support, generated output, or deployment support | [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix) |
## How ViteHub connects your code to a host
Most features use this path:
1. Start with [Installation](https://vitehub.dev/docs/getting-started/installation).
2. Register `vitehub()` in the Vite configuration.
3. Add a definition when the feature needs a name, schema, or reusable configuration.
4. Call the documented server API from application code.
5. Attach a Capability if an Agent needs to call that operation.
The Vite integration discovers definitions and prepares the provider-specific files for the selected host. Application code keeps using ViteHub imports.
## Check host support
Not every Server Primitive runs on every host. Check the [runtime and host support matrix](https://vitehub.dev/docs/frameworks-hosts/support-matrix) before you choose a deployment target.
Open [Installation](https://vitehub.dev/docs/getting-started/installation) for a runnable path, or read [Concepts](https://vitehub.dev/docs/concepts) when you need the runtime model.
# Channels
`vite-hub/channels` gives server code one named destination for outbound messages. You define the connectors that a Channel can use, then call `useChannel(name).send(text, options)` from an H3 or Nitro handler.
## Enable Channel discovery
Add the Channels integration to your Vite config. ViteHub then discovers files below `server/channels` and files that end in `.channel.ts`.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
import { env } from 'vite-hub/env'
export default defineConfig({
plugins: [vitehub({ preset: 'node', channels: true })],
env: {
server: {
telegram: {
botToken: env({
secret: true,
source: env.source('TELEGRAM_BOT_TOKEN'),
}),
},
},
},
})
```
## Define a named Channel
Create `server/channels/alerts.ts`. Read typed Server Env inside the connector's `send()` method so the value is resolved when the message is delivered. Unseal a secret only when the provider call needs the raw value.
```ts [server/channels/alerts.ts]
import { defineChannel } from 'vite-hub/channels'
import { useServerEnv } from '#vitehub/env/server'
type TelegramOptions = {
chatId: string
}
export default defineChannel({
connectors: {
telegram: {
async send(text: string, { chatId }: TelegramOptions) {
const { telegram } = useServerEnv()
const response = await fetch(`https://api.telegram.org/bot${telegram.botToken.unseal()}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, text }),
})
if (!response.ok) throw new Error(`Telegram returned ${response.status}.`)
const result = await response.json() as { result?: { message_id?: number } }
return { id: result.result?.message_id?.toString() }
},
},
},
})
```
This example calls Telegram directly to keep the connector contract visible; use a provider client when your application already has one. Channels does not bundle provider adapters. For a connector that does not need credentials, omit the `useServerEnv()` call.
The file name becomes the Channel name. For a Vite suffix definition, use `src/alerts.channel.ts` instead; both forms discover the same `alerts` Channel.
## Send from an H3 or Nitro handler
`useChannel()` returns immediately. `send()` performs the connector call and returns a normalized result with the Channel name, connector name, ViteHub delivery id, and optional provider message id.
```ts [server/api/build-finished.post.ts]
import { defineEventHandler } from 'h3'
import { useChannel } from 'vite-hub/channels/server'
export default defineEventHandler(async () => {
return await useChannel('alerts').send('Build finished.', {
connector: 'telegram',
chatId: 'build-room',
})
})
```
The handler returns a result like this:
```json
{
"channel": "alerts",
"connector": "telegram",
"deliveryId": "e4875238-2922-4787-9f7f-b13e2e7839be",
"id": "1730000000000"
}
```
Each send emits metadata-only JSON events with the `vitehub.channel.send` scope for `started`, `completed`, and `failed`. The events include `deliveryId`, Channel, connector, provider message id, and error message, but never include message text or connector options. This outbound-only package has no State Adapter, so durability comes from the application's configured log drain; use Agent Channels when inbound custody and recovery are required.
## Add another connector
Add another entry to `connectors` when the same logical destination can deliver through more than one provider. Each entry defines its own options, so Telegram can require `chatId` while Slack requires `channelId` and optionally accepts `threadTs`.
```ts
await useChannel('alerts').send('Build finished.', {
connector: 'slack',
channelId: 'builds',
threadTs: '1730000000.000100',
})
```
Keep `connector` explicit when a Channel has more than one delivery path. This makes the delivery choice visible at each call site.
## Know what this primitive includes
Channels provide discovery, connector selection, and a normalized outbound send contract. The current package does not ship Telegram or Slack adapters and does not generate inbound webhook routes; implement those connectors on top of the contract or add them as a later provider package.
`vite-hub/channels` is separate from `vite-hub/agent/channels`. Ordinary Channels send application messages. Agent Channels describe Agent conversation origins, inbound events, and Agent delivery policy; the Agent API stays unchanged.
See [Agent Channels](https://vitehub.dev/docs/agents/channels) when the destination starts or drives an Agent Invocation.
# Config options
Integration Options configure ViteHub package integrations.
Provider Selection belongs in Integration Options when it changes generated output, bindings, imports, or deployment behavior.
## Built-in deployment preset
`vitehub()` requires exactly one built-in `preset`: `cloudflare`, `netlify`, `vercel`, `deno`, or `node`. The selection is the single source for host identity, runtime, Nitro output, packaging, and built-in Blob, Queue, Rate Limit, and Sandbox adapters. Conflicting Nitro or hosting environment selections fail configuration.
`name` is ViteHub's logical deployment identity. Cloudflare Workers Builds supplies its connected Worker through `WRANGLER_CI_OVERRIDE_NAME`, which the Cloudflare preset resolves below explicit `name` and above the nearest `package.json` name and Vite root directory name. A differing explicit ViteHub identity fails because the connected Worker remains the deployment target. Cloudflare uses the resolved identity for default Worker, Blob bucket, Queue prefix, Rate Limit namespace, Sandbox, and Container names. Explicit Wrangler Worker names remain authoritative outside Workers Builds, while explicit Blob bucket, driver, or store options still win. This fallback derives deterministic names but does not provision the corresponding R2 bucket or Queue. The generated deployment manifest records the resolved identity and its source.
| Import | Public type | Placement | Defaults |
| ---------- | ---------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vite-hub` | `ViteHubOptions` | `vitehub({ preset })` in Vite `plugins` | Composes Env. Agent, Auth, Blob, Browser, Channels, Database, KV, Queue, Rate Limit, Sandbox, Schedule, Workflow, and Workspace are enabled with `true` or explicit options. On Cloudflare, Email also supports `true`; other presets reject the Cloudflare-only default. |
Unsupported requested capabilities fail before a production build can silently select a weaker provider. The `node` preset intentionally exposes its filesystem Blob store as single-host and its memory Rate Limiter as single-process. The `deno` preset rejects Schedule and `agent.runtime: "deno"` because those generated servers are not part of its deployed Nitro entrypoint. Deno output includes runtime package staging, a validated deployment manifest, and a non-interactive create-or-update runner.
Direct `hubX()` integration functions remain available from their independent
`@vite-hub/*/vite` owner-package paths.
The root `vitehub()` facade enables Agent, Blob, Browser, Channels, Database, KV, Queue, Rate Limit, Sandbox, Schedule, Workflow, and Workspace with `true`. Email accepts `true` with the Cloudflare preset, where it selects the Cloudflare Email driver; other presets reject that boolean default and require explicit provider options. Direct owner-package integrations retain their detailed option types. Auth follows the same opt-in shape but currently has no plugin option bag.
## Vite Integration options
| Package | Public type | Placement | Confirmed options and defaults |
| ---------- | -------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent | `AgentModuleOptions` | `agent` config key or `hubAgent(options)` | Omission or `false` disables Agent in `vitehub()`; `true` enables inferred defaults, and an options object enables and configures it. `runtime`: `auto`, `cloudflare-agents`, `deno`, `unknown`, `vercel`, `vite`; default `auto`. `execution`: `inline`, `sandbox`, `workflow`; default `inline`. `imports` defaults to `true`. `integrations.sandbox` and `integrations.workflow` default to `auto`. Provider groups `sandbox`, `scheduler`, and `state` default to provider `auto`. Automatic state uses Cloudflare state on Cloudflare and local SQLite at `file:.vitehub/data/agent-state.sqlite` during Vite development; production output requires a durable `VITEHUB_AGENT_STATE_URL` or explicit provider options. Hosted Agent Definitions mount `/api/_vitehub/agents/[agent]/chat`; each Agent's route-enabled Channel controls whether it answers. Webhook routes remain Channel-owned and available for adapter delivery. Set `routes.discordGateway` to generate the Discord Gateway listener route; `true` selects the package default route. `routes.inspection` is disabled by default; `true` mounts `/api/_vitehub/agents/[agent]/inspection`, while a string selects a custom route. Inspection includes operational metadata and does not add authorization. The host must authorize the route before ViteHub resolves the Agent. |
| Auth | `AuthModuleOptions` | `auth` config key or `hubAuth(options)` | `false` disables the integration. The enabled integration has no plugin option bag yet. `defineAuth()` owns `basePath` default `/api/auth`, `route: false`, `access`, `database`, `secondaryStorage`, and `runtime`. |
| Blob | `BlobModuleOptions` | `blob` config key or `hubBlob(options)` | Omission or `false` disables Blob in `vitehub()`; `true` enables the selected preset's store, and an options object enables and configures it. Driver literals include `fs`, `cloudflare-r2`, `netlify-blobs`, `vercel-blob`, `minio`, `s3`, `gcs`, `azure`, and other exported Blob drivers. Defaults: Cloudflare hosting selects `cloudflare-r2` binding `BLOB`; Netlify hosting selects `netlify-blobs`; `BLOB_READ_WRITE_TOKEN` or Vercel hosting selects `vercel-blob` with `access: "public"`; otherwise the integration selects `fs` at `.vitehub/data/blob`. MinIO defaults to bucket `vitehub-blob`, endpoint `http://localhost:9000`, region `us-east-1`, and `forcePathStyle: true`. |
| Browser | `BrowserModuleOptions` | `browser` config key or `hubBrowser(options)` | Omission or `false` disables Browser in `vitehub()`; `true` enables Cloudflare Browser Run actions with binding `BROWSER`, `{ binding }` changes the binding name, and `remote: true` connects local Wrangler development to the hosted service. Browser Definitions currently require the Cloudflare preset. The root integration and direct standalone `hubBrowser()` output generate the binding and required compatibility fields while preserving unrelated Wrangler fields. |
| Channels | `ChannelsVitePluginOptions` | `channels` config key or `hubChannels(options)` | Omission or `false` disables Channel discovery in `vitehub()`; `true` discovers `server/channels/.ts` and `.channel.ts`. `projectRoot` changes where ViteHub looks for those files. Connectors and provider credentials belong in the Channel Definition. |
| Console | `boolean` | `console` config key in `vitehub()` | Omission or `false` registers no Console page, API handler, plugin, or assets. `true` mounts the complete read-only [Console](https://vitehub.dev/docs/development/console) at `/_vitehub`, its API at `/api/_vitehub/console/**`, and a fallback SQLite invocation journal at `.vitehub/data/console.sqlite`. Production builds currently require the Node preset. The host must protect both route groups. |
| Database | `DBModulePublicOptions` | `database` config key or `hubDb(options)` | Omission or `false` disables Database in `vitehub()`; `true` enables inferred defaults, and an options object enables and configures it. `projectRoot` sets the Database discovery, generated-artifact, and provisioning root; relative paths resolve from the Vite root in Vite and the Nuxt `rootDir` in Nuxt. Integration options are `cli.generate` and `cli.migrate`, each disableable with `false`. `connection` supplies a hosted libSQL default for Vercel and other hosted output. Cloudflare D1 runtime fields are `driver: "d1"`, `binding`, `databaseId`, `previewDatabaseId`, `databaseName`, `migrationsTable`, and `local.filename`. Database Definitions own tables and may override integration connection values. |
| Email | `EmailVitePluginOptions` | `email` config key or `hubEmail(options)` | `driver` is required for explicit options and selects an exact `unemail/driver/*` subpath; `options` accepts serializable literals and runtime Env declarations. Omission disables Email in `vitehub()`. The root package also accepts `email: true` on Cloudflare and rejects it on other presets. Markdown under `server/emails/**/*.md` is discovered recursively and exposed through typed `#vitehub/emails/` renderer imports. |
| Env | `EnvIntegrationOptions` and `EnvViteConfigOptions` | `hubEnv(options)` plus Vite `env` config | `diagnostics`: `off`, `summary`, `trace`; default `summary`. `prefix` changes inferred environment variable names. `projectRoot` changes generated file placement. Vite `env.public`, `env.define`, and `env.server` own Public Env, build define values, and Server Env declarations. |
| KV | `KVModuleOptions` | `kv` config key or `hubKv(options)` | Accepts `false`, one store config, or `{ stores }` with `stores.default`. Driver literals are `fs-lite`, `cloudflare-kv-binding`, `deno-kv`, and `upstash`. Defaults: Deno hosting selects `deno-kv`; Upstash env selects `upstash`; Vercel hosting selects `upstash`; Cloudflare hosting selects `cloudflare-kv-binding` binding `KV`; otherwise `fs-lite` at `.vitehub/data/kv`. |
| Queue | `QueueModuleOptions` | `queue` config key or `hubQueue(options)` | `false` disables the integration. When active, `provider` is `cloudflare` or `vercel`; Cloudflare hosting selects `cloudflare`, and other supported hosts select `vercel`. Netlify does not infer a provider. Shared `cache` belongs here. Cloudflare uses `binding`; Vercel uses `region`. Queue concurrency and retry behaviour belong to Queue Definition or enqueue options. |
| Rate Limit | `RateLimitVitePluginOptions` | `rateLimit` config key or `hubRateLimit(options)` | `provider`: `auto`, `cloudflare`, or `memory`; default `auto`. Auto selects memory for Vite serve and Cloudflare for a known Cloudflare production host. Cloudflare requires a deployment-unique `namespace`. `projectRoot` and `scanDirs` are source-collection escape hatches. Handler-local `requireRateLimit()` calls own static limits, windows, enforcement guarantees, and failure behavior. |
| Realtime | `RealtimeModuleOptions` | `realtime` config key or `hubRealtime(options)` | `authority`: `auto`, `cloudflare`, or `memory`; default `auto`. Auto uses a Durable Object when Realtime can resolve a Cloudflare Nitro preset or hosting environment. With only `vitehub({ preset: 'cloudflare' })` during Vite development, set `authority: 'cloudflare'` explicitly. Other development presets fall back to process memory. Other production builds require an explicit authority; `memory` is accepted only for a single-process server. Realtime Definitions keep the engine and document format separate from this deployment choice. |
| Sandbox | `SandboxPublicOptions` | `sandbox` config key or `hubSandbox(options)` | `false` disables the integration. Provider selection belongs here: `cloudflare`, `vercel`, or inferred provider options. Netlify requires an explicit provider when Sandbox is active. Cloudflare defaults are binding `SANDBOX`, class name `Sandbox`, and migration tag `v1`. Per-run sandbox identity belongs to Sandbox Run invocation options. |
| Schedule | `ScheduleVitePluginOptions` | `hubSchedule(options)` | `providerOutput`: `auto`, `standalone`, `nitro`, or `false`; default `auto`. `projectRoot` changes where generated schedule output is written. There is no public `schedule.provider` option. |
| Workflow | `WorkflowModuleOptions` | `workflow` config key or `hubWorkflow(options)` | Omission or `false` disables Workflow in `vitehub()`; `true` enables inferred defaults, and an options object enables and configures it. `provider`: `cloudflare`, `openworkflow`, or `vercel`. Cloudflare hosting selects `cloudflare`; Node or Docker with OpenWorkflow storage config selects `openworkflow`; other supported hosts select `vercel`. Netlify does not infer a provider. Shared fields are `binding` and `name`. OpenWorkflow fields are `database`, `postgres`, `sqlite`, and `worker.concurrency`. |
| Workspace | `WorkspaceModuleOptions` | `workspace` config key or `hubWorkspace(options)` | Omission or `false` disables Workspace in `vitehub()`; `true` enables inferred defaults, and an options object enables and configures it. `root` defaults to `.vitehub/workspaces`. `projectRoot` changes source root resolution. `assets` controls build-time asset generation. `store` provider literals are `local`, `memory`, `cloudflare-artifacts`, `vercel-blob`, and `github`. Explicit `cloudflare-artifacts` selection generates its Cloudflare binding. Defaults: local development uses `local`; Cloudflare hosting uses `memory`; `BLOB_READ_WRITE_TOKEN` selects `vercel-blob`; Vercel hosting without Blob env uses `memory`; otherwise `local`. |
## Option placement
| Option kind | Belongs in | Example |
| ------------------- | --------------------------------------- | ---------------------------------------------------------------- |
| Integration Options | Vite config or package integration call | Provider Selection, generated output mode, project root. |
| Definition Options | Definition Boundary Helper file | Queue concurrency, Database tables, Workspace Sources and rules. |
| Invocation Options | Runtime Helper call | Sandbox Identity, Agent input options, schedule creation input. |
| Runtime Env | Env Package Server Env | Provider tokens, app secrets, request-time runtime values. |
Provider-specific driver fields are intentionally summarized here. Read the exported package types when configuring a deep provider adapter, and keep provider choices in Integration Options unless the owning package documents an invocation-time option.
## Agent eval options
Agent Eval Runner defaults live under the Agent Package integration.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({
preset: "node",
agent: {
eval: {
cache: true,
maxConcurrency: 2,
scoreThreshold: 85,
testTimeout: 60_000,
},
},
}),
],
})
```
## Env options
Env separates Public Env, compile-time define values, and Server Env.
Secret Env values belong in `env.server`, not `env.public` or `env.define`.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
import { env } from 'vite-hub/env'
export default defineConfig({
plugins: [vitehub({ preset: "node", env: { diagnostics: 'summary' } })],
env: {
public: {
appName: env({ default: 'Acme' }),
},
server: {
apiToken: env({ secret: true, source: env.source('API_TOKEN') }),
},
},
})
```
## Related
- [Vite](https://vitehub.dev/docs/frameworks-hosts)
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
- [Provisioning](https://vitehub.dev/docs/development/provisioning)
- [Provider output](https://vitehub.dev/docs/reference/provider-output)
# Errors and diagnostics
Errors and diagnostics belong to the package that owns the failing boundary. ViteHub exposes one operational error class, `ViteHubError` from `@vite-hub/runtime`; use its namespaced `code` to choose the next proof path.
`ViteHubError` snapshots its public `name`, `code`, `message`, `details`, and `requestId` fields at construction. The snapshot and its details are frozen, `cause` remains private, and later mutation cannot change `toJSON()`. Details must be bounded JSON data without accessors, cycles, `bigint`, non-finite numbers, or class instances; invalid public contracts fail with a fixed `TypeError` instead of serializing the rejected value.
## Code families
| Code prefix | Owner | Usually means |
| ----------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `CAPABILITY_*` | Runtime Package | Capability lookup or policy failed. |
| `ENV_*` | Env Package | Env Declaration or runtime resolution failed. |
| `BLOB_*` | Blob Package | Blob lookup or Provider-backed storage failed. |
| `KV_*` | KV Package | Provider-backed key-value storage failed. |
| `AUTH_*` and `AUTHENTICATION_*` | Auth Package | Authentication is required or a provider operation failed. HTTP adapters map `AUTHENTICATION_REQUIRED` to `401`. |
| `EMAIL_*` | Email Package | Message validation, configuration, credentials, throttling, network, timeout, or delivery failed. |
| `QUEUE_*`, `CLOUDFLARE_*`, and `VERCEL_*` | Queue Package | Queue dispatch, callback, or Provider handling failed. Queue Delivery owns retry and acknowledgement decisions. |
| `WORKSPACE_*` | Workspace Package | Workspace lookup, path, runtime, store, rule, or file-tree behavior failed. |
| `SOURCE_*` | Source Package | Source lookup, path validation, retrieval, or loader behavior failed. |
| `SCHEDULE_*` | Schedule Package | Static or runtime Schedule behavior failed. |
| `SANDBOX_*` | Sandbox Package | Sandbox Provider setup, execution, or output recovery failed. |
| `WORKFLOW_*` and `OPENWORKFLOW_*` | Workflow Package | Workflow run, step, or Provider behavior failed. |
| Rate Limit policy or driver error | Rate Limit Package | A policy is invalid, the selected driver cannot satisfy its guarantees, a Definition is unknown, or a provider binding is unavailable. |
| `RATE_LIMIT_REJECTED` | Agent Package | Rate Limit Capability rejected an Agent Invocation. |
| `LLM_GATE_REJECTED` | Agent Package | LLM Gate Capability rejected before the main Agent Invocation. |
| `Agent Invocation Stream timed out after .` | Agent Package | The dev-loop stream aborted a long or stalled Agent Invocation after its timeout. |
## Agent public errors
Agent routes and hooks expose a sanitized `AgentPublicError` beside the original
server error. It is safe to serialize to a caller or use in an application-owned
reply:
```ts
interface AgentPublicError {
code: AgentPublicErrorCode
error: string
details?: {
capability?: string
category?: string
retryAfter?: number
}
requestId?: string
}
```
`agent:error` hooks receive the raw failure as `error` and the sanitized value as
`publicError`. Chat error hooks receive the same pair. Keep the raw error in
protected diagnostics; provider payloads and causes can contain credentials or
private response data.
| Public code | Meaning |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `PROVIDER_AUTHENTICATION_FAILED` | The model provider rejected its credentials. |
| `PROVIDER_QUOTA_EXHAUSTED` | The account or project has no remaining provider quota. |
| `PROVIDER_RATE_LIMITED` | The provider returned a temporary rate limit. |
| `PROVIDER_UNAVAILABLE` | The provider returned a server or availability failure. |
| `APPROVAL_REQUIRED` | A Capability needs approval before it can continue. `requestId` identifies the approval request when available. |
| `AUTHENTICATION_REQUIRED`, `RATE_LIMIT_*`, `LLM_GATE_REJECTED`, `CAPABILITY_*`, `TRANSCRIPTION_*` | ViteHub recognized a public application or Capability failure. |
| `INTERNAL` | The failure has no approved public mapping. The message stays generic. |
The mapper includes only bounded identifiers, categories, retry delays, and
request IDs. It replaces unknown errors with a context-specific `INTERNAL`
message instead of copying `error.message`.
## Diagnostics sources
| Source | Use |
| ------------------------ | ------------------------------------------------------------------- |
| CLI exit code and stderr | Command parsing, provisioning, and Agent Eval failures. |
| Env diagnostics | Missing, defaulted, valid, and masked Env Declaration status. |
| Generated files | Discovery, Runtime Registry, and Provider Output inspection. |
| Agent Dev Loop responses | Local Agent inspection and invocation failures. |
| Trace Events | Runtime policy, approval, capability, lifecycle, and error records. |
| Package tests | Contract failures owned by the primitive package. |
## Local response
Start with `getViteHubErrorShape(error)?.code`, then inspect the owning package and failing proof path. For packages that generate Provider Output, inspect that output before changing runtime code. Authenticated Agent bridges distinguish `AUTHENTICATION_REQUIRED` from `AUTH_PROVIDER_OPERATION_FAILED`; use `details.operation` for safe diagnostics and `cause` only in protected server-side diagnostics. Email emits no Provider Output; inspect the `EMAIL_*` code and `details.driver`.
For Env, inspect the `ENV_*` code first. Its code set and public messages are fixed. `ENV_DECLARATION_INVALID` can include `details.path`, `ENV_REQUIRED_MISSING` can include a bounded source identifier and declaration path, and `ENV_RUNTIME_VALUE_INVALID` and `ENV_SOURCE_FAILED` can include a bounded source identifier such as `env`, `git:branch`, `package.json`, or `custom`. Raw labels and diagnostics remain in `cause`, which the serialized shape omits. Custom source resolvers keep application-owned errors unchanged.
```bash [Terminal]
pnpm vitehub provision run --provider cloudflare --dry-run
find .vitehub -maxdepth 4 -type f | sort
pnpm --filter @vite-hub/sandbox test
```
### Rate Limit diagnostics
| Symptom | Likely cause | Verify |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Conflicting Rate Limit policy | Multiple `requireRateLimit()` calls use the same stable ID with different static policies. | Follow both reported source locations and make the policies identical or rename one ID. |
| Driver provides best-effort enforcement | A policy requires `strict`, but the selected provider cannot guarantee it. | Keep strict enforcement and choose another driver, or change the policy only when best-effort protection is acceptable. |
| Driver does not support the window | The provider accepts fewer fixed-window periods than the portable policy type. | Use a supported period or select a driver that advertises the required window. |
| Production hosting requires an explicit provider | The build target is unknown or has no native inferred Rate Limit provider. | Set `provider: 'cloudflare'` with a project-unique `namespace` for Cloudflare, set `provider: 'memory'` only for a deliberate single-process deployment, or construct a custom Rate Limiter. |
| Cloudflare binding was not found | Generated `ratelimits` output is missing from the running Worker or request context. | Inspect `wrangler.json`, then exercise the deployed Worker rather than an unrelated Node process. |
| `reason: 'unavailable'` with `allowed: true` | A `failure: 'allow'` policy allowed work after a driver error. | Record the unavailable decision and inspect provider health before changing the budget. |
## Production response
Keep secrets out of production diagnostics.
Use Server Env and Secret Env for runtime secret values, and rely on package diagnostics to redact known secret values where supported.
## Related
- [Troubleshooting](https://vitehub.dev/docs/development/troubleshooting)
- [Runtime events](https://vitehub.dev/docs/reference/runtime-events)
- [Verification](https://vitehub.dev/docs/development/verification)
# File conventions
File conventions produce Discovered Definitions. Discovery Identity comes from the discovery location, not from arbitrary inline Definition Options.
## Definition files
| Definition | Directory convention | Suffix convention | Discovery Identity |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent | `server/agents/.ts`, `server/agents//agent.ts`, or `server/agents//index.ts` | `.agent.ts` outside `server/` | Relative file or directory path. A leading `src/` is removed from suffix identities. |
| Auth | `server/auth.ts` | `server.auth.ts` | `default`. Only one Auth Definition is allowed. |
| Browser | `server/browsers/.ts` | `.browser.ts` | Normalized relative path. A leading `src/` is removed from suffix identities. |
| Channel | `server/channels/.ts` | `.channel.ts` | Normalized relative path. A leading `src/` is removed from suffix identities. |
| Database | `server/databases/config.ts` for one default database, or `server/databases//config.ts` for named databases | `src/database.ts` for the default database, or `.database.ts` for a named database | `default` or the normalized relative path. Default and named modes cannot be mixed. |
| Queue | `server/queues/.ts` | `.queue.ts` | Normalized relative path. A leading `src/` is removed from suffix identities. |
| Realtime | `server/realtime/.ts` | None | Normalized relative path. |
| Workflow | `server/workflows/.ts` or a folder containing `index.ts` or numbered step files | `.workflow.ts` | Normalized relative file or folder path. Agent Definitions contribute their Agent identity by default, `workflow(...)` can override it, and `runtime: false` opts out. |
| Schedule | `server/schedules/.ts` | `.schedule.ts` | Normalized relative path. A leading `src/` is removed from suffix identities. |
| Sandbox | `server/sandboxes//{package.json,index.ts}` | `.sandbox.ts` outside `server/sandboxes/` | Normalized folder or suffix path, without a trailing `.sandbox` segment. |
| Workspace | `server/workspaces/.ts`, `server/workspaces//config.ts`, or `server/agents//agent.ts` when the Agent declares a Workspace | `.workspace.ts` | Normalized relative path or the colocated Agent name. |
The table uses `.ts` for brevity. Directory and suffix patterns accept JavaScript and TypeScript module variants where the owning package permits them. `src/database.ts` is the exact default Database suffix-mode file.
Rate Limit deliberately has no file convention. Call `requireRateLimit(event, id, options)` inside ordinary H3 handlers; its explicit ID is the provider identity.
## Export shape
Most discovered Definition files default-export the package-owned Definition Boundary Helper. This keeps Build-Extracted Definition Options limited to the direct discovered default export.
Canonical Sandbox package projects are the exception: `server/sandboxes//index.ts` default-exports an async `(payload, context) => result` function. The adjacent `package.json` must set `"type": "module"`. Local TypeScript uses explicit relative ESM imports, while bare dependencies must expose runtime-ready JavaScript; CommonJS source and package-local import aliases are not compiled. The folder supplies the Definition identity, and optional static wall-clock policy comes from `vitehub.sandbox.timeout`. Free-form `.sandbox.ts` files still default-export `defineSandbox(...)`.
```ts [server/queues/welcome-email.ts]
import { defineQueue } from '@vite-hub/queue'
export default defineQueue<{ email: string }>(async (job) => {
await sendWelcomeEmail(job.payload.email)
})
```
Avoid aggregate named exports for discovered Definitions. The generated Runtime Registry expects one discovered boundary per file convention.
## Colocated Workspace files
Agent folders can colocate Workspace content beside the Agent Definition. When a folder contains `workspace/`, that folder becomes the Workspace Source Root for the colocated Workspace Definition.
```txt [File tree]
server/
agents/
docs/
agent.ts
workspace/
README.md
guides/
setup.md
```
## Colocated Agent Skills
An Agent folder can own Skills in an adjacent `skills/` directory. ViteHub recursively embeds every file during discovery and materializes the directory into the Provider Workspace. Existing files remain in place, and files below a `scripts/` directory become executable.
```txt [File tree]
server/
agents/
review/
agent.ts
skills/
code-review/
SKILL.md
scripts/
review.sh
```
This convention needs no `skills()` Capability declaration. Use [`skills()`](https://vitehub.dev/docs/capabilities/skills) when the Skill comes from a Workspace or external Source instead of the Agent folder.
## Markdown templates
Place a `*.template.md` file beside the TypeScript or JavaScript module that renders it, then import the generated render function directly. For example, `server/agents/review/agent.ts` can import `./reply.template.md`.
When one caller owns several templates, you can group them in a local directory such as `server/agents/review/templates/`. The directory has no discovery behavior; keep the `.template.md` suffix and import each file explicitly. See [Markdown templates](https://vitehub.dev/docs/reference/markdown-templates) for rendering and generated-type examples.
## Email templates
Markdown files under `server/emails` become typed email renderers. ViteHub
removes the directory prefix and `.md` extension, so
`server/emails/welcome.md` becomes `#vitehub/emails/welcome` and
`server/emails/monthly/recap.md` becomes `#vitehub/emails/monthly/recap`.
Email template names must use non-empty path segments and cannot contain `.` or
`..` segments, query strings, fragments, backslashes, or a trailing `.md`.
ViteHub rejects duplicate names across configured server directories.
## Generated files
Generated files live under `.vitehub/**` and host output directories.
They prove discovery and Provider Output, but the source Definition files remain the authoring surface.
## Related
- [Generated files](https://vitehub.dev/docs/development/generated-files)
- [Definitions and discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery)
- [Import paths](https://vitehub.dev/docs/reference/import-paths)
# Import paths
Stable ViteHub Import Paths are ViteHub-owned app-facing import specifiers.
They may resolve to Runtime Registries, generated files, virtual modules, or
owner-package runtime code, but application code must not depend on that
implementation detail.
## Canonical application imports
Applications that install `vite-hub` use the root only for framework
composition and explicit feature subpaths for application APIs.
| Import path | Use |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vite-hub` | Register the framework Vite Integration with `vitehub()`. |
| `vite-hub/agent` | Agent Definition, invocation, trigger, and Agent Actor APIs. |
| `vite-hub/agent/capabilities` | Official Capability factories. |
| `vite-hub/agent/channels` | Official Channel Kind helpers. |
| `vite-hub/agent/eval` | Agent Eval authoring helpers; install Evalite and the test runner explicitly. |
| `vite-hub/agent/cloudflare` | Cloudflare Agent state configuration helpers. |
| `vite-hub/agent/vue` | Vue Agent client handle and AI SDK chat composable. |
| `vite-hub/agent/server` and `vite-hub/agent/state/sqlite` | Manual server integration and libSQL-compatible durable Agent state. |
| `vite-hub/agent/invocations/sqlite` | LibSQL-compatible durable Agent Invocation Journal. |
| `vite-hub/console` | Route metadata for the local read-only invocation console. |
| `vite-hub/console/server` | Local console invocation journal for server integrations. |
| `vite-hub/auth` and `vite-hub/auth/server` | Auth Definitions and server runtime helpers. |
| `vite-hub/auth/agent` | Better Auth session mapping into Agent Invokers. |
| `vite-hub/auth/vue` | Better Auth Vue client and normalized session composables. |
| `vite-hub/blob` | Blob Runtime Helpers and Blob Store access. |
| `vite-hub/blob/content-type` | Detect common image and PDF signatures from leading bytes before upload. |
| `vite-hub/browser` | Browser Definitions, invocation-scoped Playwright sessions, and named Browser runs. |
| `vite-hub/browser/actions` | ViteHub Browser actions backed by Cloudflare Browser Run. |
| `vite-hub/browser/controllers/cdp` and `vite-hub/browser/controllers/playwright` | Advanced raw CDP and Playwright Browser Session controllers. |
| `vite-hub/browser/providers/cloudflare` and `vite-hub/browser/providers/local` | Advanced explicit provider selection for low-level Browser Clients. |
| `vite-hub/channels` and `vite-hub/channels/server` | Channel Definitions and discovered named delivery. |
| `vite-hub/box` | Box Definitions and built-in runtime selection for trusted-host, Crabbox, ASCII, Cloudflare Sandbox, Cloudflare Computer, and Vercel Sandbox execution. |
| `vite-hub/database` and `vite-hub/database/drizzle` | Database Definitions and generated `useDatabase()` access. |
| `vite-hub/env` | Env Declaration helpers and authoring types. |
| `vite-hub/email`, `vite-hub/email/server`, and `vite-hub/email/markdown` | Email clients, configured runtime delivery, and Dynamic Markdown HTML with a composed Markdown text fallback. |
| `vite-hub/env/presets` and `vite-hub/env/schema` | Reusable Env presets and schema helpers. |
| `vite-hub/env/secret` and `vite-hub/env/server` | Secret declarations and server-only Env access. |
| `vite-hub/history` | Durable Workspace history checkpoint contract and types. |
| `vite-hub/kv` | KV Runtime Helper. |
| `vite-hub/markdown-template` | Deterministic Markdown rendering from explicit template strings. |
| `vite-hub/queue` | Queue Definitions and dispatch helpers. |
| `vite-hub/rate-limit` | Source-local managed Rate Limit handles and direct Rate Limiters. |
| `vite-hub/realtime`, `vite-hub/realtime/server`, and `vite-hub/realtime/vue` | Realtime Definitions, manual server integration, and Vue collaborative editing with canonical [Realtime checkpoints](https://vitehub.dev/docs/reference/realtime). |
| `vite-hub/runtime` | Runtime Host Context, policy, approval, trace, and capability APIs. |
| `vite-hub/runtime/node` | Node process, host, and Linux cgroup resource observations. |
| `vite-hub/sandbox` | Sandbox Definitions and Sandbox Run helpers. |
| `vite-hub/schedule` and `vite-hub/schedule/runtime` | Static and runtime Schedule APIs. |
| `vite-hub/schedule/runtime/driver` and `vite-hub/schedule/runtime/process` | Host wake registration and process-backed runtime Schedule controls. |
| `vite-hub/shell` | Shell runtime and command analysis APIs. |
| `vite-hub/shell/providers/cloudflare` and `vite-hub/shell/providers/just-bash` | Cloudflare and Just Bash Shell providers. |
| `vite-hub/shell/workspace` | Workspace-backed Shell execution helpers. |
| `vite-hub/source` | Runtime-neutral Source Definitions, custom loaders, and registry APIs. |
| `vite-hub/source/client` and `vite-hub/source/server` | Vue Collection pagination and the H3 Collection route adapter. |
| `vite-hub/source/content` and `vite-hub/source/content/client` | Comark Content definition, Source adaptation, generated runtime handler, and typed client. |
| `vite-hub/source/file`, `vite-hub/source/glob`, and `vite-hub/source/markdown` | Local file implementations, loaded only when selected. |
| `vite-hub/source/github` | GitHub Source implementation, loaded only when selected. |
| `vite-hub/source/mcp` | MCP Resources implementation with its private SDK closure. |
| `vite-hub/ui`, `vite-hub/ui/headless`, and `vite-hub/ui/styles.css` | AI interface components, headless message scrolling, and default styles. |
| `vite-hub/ui/nuxt` and `vite-hub/ui/vite` | Register the canonical UI package for Nuxt or Vue with Vite. |
| `vite-hub/tsconfig` | TypeScript config that includes ViteHub's generated declaration entry without taking ownership of application source includes. |
| `vite-hub/workflow` | Workflow Definitions and run helpers. |
| `vite-hub/workspace` and `vite-hub/workspace/runtime` | Workspace Definitions, Sources, runtime facades, and registry APIs. |
| `vite-hub/workspace/cloudflare` | Cloudflare Workspace runtime setup. |
| `vite-hub/workspace/collections` and `vite-hub/workspace/collections/client` | Bounded Workspace Collection queries and optional Vue client composables. |
| `vite-hub/workspace/loader`, `vite-hub/workspace/publish`, and `vite-hub/workspace/server` | Workspace loader, publisher, and manual server extension APIs. |
ViteHub-owned adapters use canonical `vite-hub/*` imports. Their optional
third-party providers and SDKs remain explicit dependencies. Provider Output,
build integrations, tests, and unlisted provider-specific modules stay on their
owner packages.
## Direct owner-package imports
Every owner package remains independently installable. These paths are stable
for libraries, focused integrations, and advanced composition.
| Import path | Owner | Use |
| ---------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `@vite-hub/agent` | Agent Package | Agent Definition helpers, invocation helpers, trigger helpers, and trusted caller types. |
| `@vite-hub/agent/capabilities` | Agent Package | Official Capability factories such as `access()`, `browser()`, `workspaceShell()`, `inputCommands()`, and `subagents()`. |
| `@vite-hub/agent/channels` | Agent Package | Official Channel Kind helpers such as `github()`, `teams()`, `telegram()`, `webChat()`, and `defineChannel()`. |
| `@vite-hub/agent/eval` | Agent Package | Agent Eval authoring helpers. |
| `@vite-hub/agent/test` | Agent Package | Agent test runner helpers for local and CI Agent Invocation checks. |
| `@vite-hub/agent/cloudflare` | Agent Package | Cloudflare Agent state helpers. |
| `@vite-hub/agent/vue` | Agent Package | Vue Agent client handle and AI SDK chat composable. |
| `@vite-hub/auth` | Auth Package | Auth Definition helpers. |
| `@vite-hub/auth/server` | Auth Package | Better Auth runtime creation, request handlers, and session access for manual host integration. |
| `@vite-hub/blob` | Blob Package | Blob Runtime Helpers and Blob Store access. |
| `@vite-hub/blob/content-type` | Blob Package | Detect common image and PDF signatures from leading bytes before upload. |
| `@vite-hub/browser` | Browser Package | Browser Definitions, invocation-scoped sessions, and low-level Browser Client lifecycle. |
| `@vite-hub/browser/actions` | Browser Package | ViteHub Browser actions backed by Cloudflare Browser Run. |
| `@vite-hub/browser/controllers/cdp` and `@vite-hub/browser/controllers/playwright` | Browser Package | Raw CDP and Playwright Browser Session controllers. |
| `@vite-hub/browser/providers/cloudflare` and `@vite-hub/browser/providers/local` | Browser Package | Cloudflare Browser Run and local Chromium providers. |
| `@vite-hub/box` | Box Package | Box Definitions, sessions, and built-in runtime selection. |
| `@vite-hub/channels` | Channels Package | Channel Definitions, explicit clients, portable types, and normalized delivery results. |
| `@vite-hub/channels/server` | Channels Runtime | Server-only discovered named delivery. |
| `@vite-hub/email` | Email Package | Explicit clients, portable types, and normalized errors. |
| `@vite-hub/email/server` | Email Runtime | Server-only configured `email` Runtime Helper. |
| `@vite-hub/email/markdown` | Email Package | Dynamic Markdown composition into HTML and a composed Markdown text fallback. |
| `@vite-hub/email/test` | Email Package | Isolated in-memory message capture for tests. |
| `#vitehub/emails/` | Email Package | Generated async renderer for a discovered `server/emails/**/*.md` template. |
| `@vite-hub/database/drizzle` | Database Package | Generated `useDatabase()` access to a Drizzle database and schema. |
| `@vite-hub/env` | Env Package | Env Declaration helpers. |
| `@vite-hub/history` | History Package | Durable history checkpoint contract and types. |
| `#vitehub/env/public` | Env Package | Generated Public Env access. |
| `#vitehub/env/server` | Env Package | Generated Server Env access. |
| `@vite-hub/kv` | KV Package | KV Runtime Helper. |
| `@vite-hub/markdown-template` | Markdown Template Package | Deterministic Markdown rendering from explicit template strings. |
| `@vite-hub/queue` | Queue Package | Queue Definition and enqueue Runtime Helper. |
| `@vite-hub/rate-limit` | Rate Limit Package | Source-local managed Rate Limit handles and direct Rate Limiters. |
| `@vite-hub/rate-limit/drivers/memory` | Rate Limit Package | Local, test, and single-process fixed-window enforcement. |
| `@vite-hub/rate-limit/drivers/cloudflare` | Rate Limit Package | Direct access to a Cloudflare Rate Limiting binding. |
| `@vite-hub/realtime` | Realtime Package | Realtime Definitions and portable collaboration types. |
| `@vite-hub/realtime/server` and `@vite-hub/realtime/vue` | Realtime Package | Manual server integration and Vue collaborative editing. |
| `@vite-hub/ui` and `@vite-hub/ui/headless` | UI Package | AI SDK-native Vue components and headless message scrolling. |
| `@vite-hub/sandbox` | Sandbox Package | Sandbox Definition and Sandbox Run helpers. |
| `@vite-hub/schedule/runtime` | Schedule Package | Runtime schedule helpers. |
| `@vite-hub/schedule/runtime/driver` | Schedule Package | Host integration boundary for reconciling stored Runtime Schedules with native wake registrations. |
| `#vitehub/schedule/registry` | Schedule Package | Generated static schedule registry for host bridges. |
| `@vite-hub/workflow` | Workflow Package | Workflow Definition and run helpers. |
| `@vite-hub/workflow/runtime/openworkflow-worker` | Workflow Package | OpenWorkflow-specific worker lifecycle helpers; install `openworkflow` explicitly. |
| `@vite-hub/workspace` | Workspace Package | Workspace Definition, Source helpers, Workspace facade access, and authoring types. |
| `@vite-hub/workspace/runtime` | Workspace Package | Workspace runtime registry, `useWorkspace()`, and source resolution/request helpers for integrations. |
## Integration imports
| Import path | Use |
| ------------------------------------------- | --------------------------------------------------------------------------------------- |
| `vite-hub` | Canonical application import for `vitehub()`. |
| `vite-hub/nuxt` | Register the framework Nuxt module and carry Vite integration configuration into Nitro. |
| `@vite-hub/agent/vite` | Register the Agent Vite Integration. |
| `@vite-hub/auth/vite` | Register the Auth Vite Integration. |
| `@vite-hub/blob/vite` | Register the Blob Vite Integration. |
| `@vite-hub/browser/vite` | Register Cloudflare Browser Run Provider Output. |
| `@vite-hub/channels/vite` | Register Channel Definition discovery and generated runtime bindings. |
| `@vite-hub/database/vite` | Register the Database Vite Integration. |
| `@vite-hub/email/vite` | Configure one Unemail provider and generate its runtime binding. |
| `@vite-hub/env/vite` | Register the Env Vite Integration and `env()` declaration helper. |
| `@vite-hub/ui/nuxt` and `@vite-hub/ui/vite` | Register the UI package for Nuxt or Vue with Vite. |
| `@vite-hub/kv/vite` | Register the KV Vite Integration. |
| `@vite-hub/markdown-template/vite` | Register generated types and direct `.template.md` imports. |
| `@vite-hub/queue/vite` | Register the Queue Vite Integration. |
| `@vite-hub/rate-limit/vite` | Register Rate Limit source collection and provider output. |
| `@vite-hub/realtime/vite` | Register Realtime Definition discovery and generated runtime wiring. |
| `@vite-hub/sandbox/vite` | Register the Sandbox Vite Integration. |
| `@vite-hub/schedule/vite` | Register the Schedule Vite Integration. |
| `@vite-hub/workflow/vite` | Register the Workflow Vite Integration. |
| `@vite-hub/workspace/vite` | Register the Workspace Vite Integration. |
## Generated and internal paths
| Path family | Status | Guidance |
| ------------------------------------------ | ------------------------- | ---------------------------------------------------------------------- |
| `.vitehub/**` | Generated | Inspect during development; do not author imports against these files. |
| `.vercel/output/**` | Generated Provider Output | Deploy or inspect as Vercel Build Output. |
| `.netlify/v1/**` | Generated Provider Output | Deploy or inspect as Netlify function output. |
| `dist/**/wrangler.json` | Generated Provider Output | Deploy or inspect as Cloudflare output. |
| Vite virtual module ids with `\0` prefixes | Internal | Never import directly. |
| `vite-hub/_internal/*` | Internal | Generated ViteHub code only; application imports are unsupported. |
| `@vite-hub/internal/*` | Internal | Package implementation only. |
The Agent Package does not expose an `@vite-hub/agent/netlify` application import. Netlify Agent output is generated Provider Output under `.netlify/v1` plus the `.vitehub/agent/netlify-function.mjs` source wrapper.
With Nuxt, that source wrapper is generated under `/vitehub/agent/netlify-function.mjs` (normally `.nuxt/vitehub/agent/netlify-function.mjs`).
The framework distribution does not introduce public `vite-hub/*/vite` or
provider-specific application aliases. Use root `vitehub()` for framework
composition and the owner-package paths above for advanced integration control.
## Related
- [Generated files](https://vitehub.dev/docs/development/generated-files)
- [File conventions](https://vitehub.dev/docs/reference/file-conventions)
- [Package reference](https://vitehub.dev/docs/reference)
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
# Package reference
Most applications need only `vite-hub`. Install an individual `@vite-hub/*`
package when you are building a library or configuring one integration directly.
## Framework distribution
| Package | Owns | Primary imports |
| ---------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `vite-hub` | Framework defaults, the `vitehub()` Vite Integration, the tested compatibility matrix, and intentional feature subpaths | `vite-hub`, `vite-hub/agent`, `vite-hub/env`, `vite-hub/workspace`, `vite-hub/workflow` |
The root export stays focused on framework composition. Runtime Helpers,
Definitions, Capabilities, and other application APIs use feature subpaths
instead of one root barrel.
## Independent owner packages
| Package | Owns | Primary imports |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `@vite-hub/agent` | Agent Definitions, Agent Invocations, Agent Driver boundary, Capability composition, Agent Evals, Agent Trigger API | `@vite-hub/agent`, `@vite-hub/agent/capabilities`, `@vite-hub/agent/channels`, `@vite-hub/agent/eval`, `@vite-hub/agent/vite` |
| `@vite-hub/auth` | Auth Definitions, Better Auth server wiring, generated Auth route behavior | `@vite-hub/auth`, `@vite-hub/auth/server`, `@vite-hub/auth/vite` |
| `@vite-hub/blob` | Blob Stores, Default Blob Store behavior, Blob Driver Modules, provider storage output | `@vite-hub/blob`, `@vite-hub/blob/vite`, `@vite-hub/blob/drivers/*` |
| `@vite-hub/browser` | Browser Definitions, invocation-scoped sessions, controllers and providers, live handoff, and Browser Run output | `@vite-hub/browser`, `@vite-hub/browser/controllers/*`, `@vite-hub/browser/providers/*`, `@vite-hub/browser/vite` |
| `@vite-hub/box` | Box Definitions and provider-neutral execution sessions | `@vite-hub/box` |
| `@vite-hub/channels` | Provider-neutral Channel definitions, server access, and Vite discovery | `@vite-hub/channels`, `@vite-hub/channels/server`, `@vite-hub/channels/vite` |
| `@vite-hub/database` | Database Definitions, Drizzle schema generation, D1 and hosted database wiring | `@vite-hub/database`, `@vite-hub/database/drizzle`, `@vite-hub/database/vite` |
| `@vite-hub/email` | Declarative Unemail provider integration, runtime delivery, Dynamic Markdown composition, and test capture | `@vite-hub/email`, `@vite-hub/email/markdown`, `@vite-hub/email/server`, `@vite-hub/email/test`, `@vite-hub/email/vite` |
| `@vite-hub/env` | Env Declarations, Public Env, Server Env, Secret Env, generated env access | `@vite-hub/env`, `@vite-hub/env/vite`, `@vite-hub/env/server`, `@vite-hub/env/secret` |
| `@vite-hub/history` | Shared history records, cursors, pages, and store contracts for stateful features | `@vite-hub/history` |
| `@vite-hub/kv` | KV Runtime Helper and configured KV Stores | `@vite-hub/kv`, `@vite-hub/kv/vite` |
| `@vite-hub/markdown-template` | Markdown templates with data bindings, conditions, fragments, and direct Vite imports | `@vite-hub/markdown-template`, `@vite-hub/markdown-template/vite` |
| `@vite-hub/queue` | Queue Definitions, queue dispatch Runtime Helpers, provider queue output | `@vite-hub/queue`, `@vite-hub/queue/vite` |
| `@vite-hub/rate-limit` | Rate Limit declarations, runtime decisions, drivers, and provider output | `@vite-hub/rate-limit`, `@vite-hub/rate-limit/runtime`, `@vite-hub/rate-limit/drivers/*`, `@vite-hub/rate-limit/vite` |
| `@vite-hub/realtime` | Realtime documents, server routes, history, and Vue bindings | `@vite-hub/realtime`, `@vite-hub/realtime/server`, `@vite-hub/realtime/vue`, `@vite-hub/realtime/vite` |
| `@vite-hub/runtime` | Runtime Host Context, Runtime Capability handles, Policy Decisions, approvals, Trace Events, leases | `@vite-hub/runtime` |
| `@vite-hub/sandbox` | Sandbox Definitions, Sandbox Runs, Sandbox Provider integration | `@vite-hub/sandbox`, `@vite-hub/sandbox/vite` |
| `@vite-hub/schedule` | Static schedules, runtime schedules, Schedule Targets, cron Provider Output | `@vite-hub/schedule`, `@vite-hub/schedule/runtime`, `@vite-hub/schedule/vite` |
| `@vite-hub/shell` | Shell-shaped runtime execution providers and Workspace shell integration helpers | `@vite-hub/shell`, `@vite-hub/shell/workspace` |
| `@vite-hub/source` | Source Definitions and Source Loaders for file, glob, markdown, GitHub, custom, and MCP resource retrieval | `@vite-hub/source`, `@vite-hub/source/*` |
| `@vite-hub/ui` | AI SDK-native Vue and Nuxt components, headless message scrolling, Agent inspection, and Pierre code views | `@vite-hub/ui`, `@vite-hub/ui/headless`, `@vite-hub/ui/nuxt`, `@vite-hub/ui/vite` |
| `@vite-hub/workflow` | Workflow Definitions, durable run state, step execution, provider workflow output | `@vite-hub/workflow`, `@vite-hub/workflow/vite` |
| `@vite-hub/workspace` | Workspace Definitions, Workspace Stores, Source Bindings, Workspace runtime facades, Workspace extensions | `@vite-hub/workspace`, `@vite-hub/workspace/vite`, `@vite-hub/workspace/runtime` |
## Internal and support packages
| Package | Status | Purpose |
| -------------------- | ------------------ | ---------------------------------------------------------------------------- |
| `@vite-hub/cli` | Public CLI package | Loads Vite config and runs package-owned CLI namespaces. |
| `@vite-hub/internal` | Internal package | Shared discovery, Provider Output, provisioning, runtime, and build helpers. |
## Package rules
Applications start with `vite-hub` and its documented feature imports. Libraries
and focused integrations can depend on one `@vite-hub/*` package directly.
Do not import `@vite-hub/internal`, generated files, or framework virtual modules
unless a reference page documents that path.
Provider-specific behavior belongs to the package that owns the primitive.
For example, Blob Provider SDK Adapters belong behind Blob Driver Modules, and Workspace Provider Adapters stay behind Workspace configuration and generated runtime wiring.
## Related
- [Import paths](https://vitehub.dev/docs/reference/import-paths)
- [Config options](https://vitehub.dev/docs/reference/config-options)
- [Provider output](https://vitehub.dev/docs/reference/provider-output)
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
# Markdown templates
`@vite-hub/markdown-template` composes Markdown without evaluating JavaScript or reading files implicitly. Use it when Agent Instructions, review prompts, or other generated documents need predictable data binding and conditional sections while preserving authored Markdown structure.
## Install
The package requires Node.js 24 or later.
```bash [Terminal]
pnpm add @vite-hub/markdown-template
```
## Import a template file
Place a `*.template.md` file beside the module that renders it. Importing the file returns an asynchronous render function.
```md [server/agents/reviewer/prompt.template.md]
# Review {{ pullRequest.number }}
Title: {{ pullRequest.title }}
```
```ts [server/agents/reviewer/agent.ts]
import renderPrompt from './prompt.template.md'
const prompt = await renderPrompt({
pullRequest: { number: 611, title: 'Refine navigation' },
})
```
ViteHub bundles the template and its relative Markdown imports before deployment. The deployed application does not read these source files at runtime. The generated module type accepts an optional `Record` and returns `Promise`.
When one caller owns several templates, you may group them in a local directory such as `./templates/`. The directory has no discovery behavior; import each `*.template.md` file directly. Multiple callers can also import the same template from an explicitly shared source path.
For a fixed runtime choice, define the allowed names with an ordinary TypeScript map:
```ts [server/agents/reviewer/replies.ts]
import renderFailure from './failure.template.md'
import renderSuccess from './success.template.md'
export const replies = {
failure: renderFailure,
success: renderSuccess,
} as const
```
The `vitehub()` preset installs the template module integration. Modular Vite configurations can add `hubMarkdownTemplate()` from `@vite-hub/markdown-template/vite`. Both forms generate the ambient module type under `.vitehub/types`, which the application `tsconfig.json` must include.
Imported fragments can remain ordinary `.md` files.
`repositoryHostContext({ materialize })` uses the same path convention. Pass a caller-relative `.template.md` path; ViteHub bundles its renderer and derives the generated `.md` path by removing only the final `.template`, preserving directories and case.
## Render a template string
Pass the template string and the complete data available to it. Scalar bindings are escaped as Markdown text, while triple bindings insert an intentional Markdown fragment.
```ts [src/review-template.ts]
import { renderMarkdownTemplate } from '@vite-hub/markdown-template'
const markdown = await renderMarkdownTemplate([
'# Review {{ pullRequest.number }}',
'',
'Title: {{ pullRequest.title }}',
'',
'::if{pullRequest.draft}',
'This pull request is a draft.',
'::else',
'{{{ sections.files }}}',
'::',
].join('\n'), {
data: {
pullRequest: {
draft: false,
number: 611,
title: 'Refine navigation',
},
sections: {
files: '## Files\n\n- `DocsAsideLeftBody.vue`',
},
},
})
```
The result keeps the fragment as document structure:
```md [Rendered Markdown]
# Review 611
Title: Refine navigation
## Files
- `DocsAsideLeftBody.vue`
```
## Template syntax
| Syntax | Purpose | Behavior |
| --------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `{{ path.to.value }}` | Scalar binding | Accepts a string, number, or boolean and escapes Markdown syntax in the value. A scalar may occupy a complete inline link destination, such as `[Open]({{ url }})`; unsafe destinations and values whose URL meaning cannot be preserved fail rendering. Missing paths and non-scalar values fail rendering. |
| `{{{ path.to.markdown }}}` | Markdown fragment | Inserts trusted Markdown without evaluating bindings, conditions, or imports inside the fragment again. Block Markdown is rejected when the binding appears in an inline position. |
| `::if{condition}` | Conditional section | Selects an `if`, `else-if`, or `else` branch. Conditions support data paths, literals, `!`, equality and inequality (`===`, `!==`, `==`, and `!=` use strict semantics), `&&`, `||`, and parentheses. |
| `@./relative.md` | Template import | Calls `resolveImport` for a relative file. Absolute paths, URLs, and globs are rejected. |
| `{{ value }}` in a quoted XML-style attribute | Attribute binding | Escapes HTML attribute characters before inserting the scalar value. |
Template syntax inside code spans, fenced code blocks, and indented code blocks remains literal. Authored XML-style tags remain in the rendered Markdown.
## Render options
`renderMarkdownTemplate(template, options)` returns a `Promise` and accepts every `RenderMarkdownTemplateOptions` field below.
| Option | Type | Default | Purpose |
| ---------------- | ------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `data` | `Record` | `{}` | Supplies values for scalar bindings, fragments, and conditions. Paths resolve own properties only. |
| `maxImportDepth` | `number` | `4` | Limits nested imports when `resolveImport` is present. Use a non-negative integer; `0` rejects every import. |
| `resolveImport` | `ResolveMarkdownTemplateImport` | none | Resolves one relative specifier against the current canonical source id. Without it, relative-looking text remains literal. |
| `sourceId` | `string` | `` | Identifies the root template for relative resolution and circular-import detection. |
The import resolver returns `{ id, template }`, where `id` is the canonical identity used for nested imports and cycle detection.
```ts [src/render-instructions.ts]
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { renderMarkdownTemplate } from '@vite-hub/markdown-template'
const sourceId = resolve('instructions/review.md')
const template = await readFile(sourceId, 'utf8')
const markdown = await renderMarkdownTemplate(template, {
data: { repository: { name: 'vite-hub/vitehub' } },
sourceId,
async resolveImport(specifier, importer) {
const id = resolve(dirname(importer), specifier)
return { id, template: await readFile(id, 'utf8') }
},
})
```
The resolver owns filesystem, URL, authorization, and caching policy. ViteHub resolves imports before evaluating conditional sections, rejects missing resolutions, and stops circular imports, so the resolver must authorize every requested import even when it appears inside an unselected branch.
## Security and limits
Scalar escaping prevents untrusted values from becoming Markdown syntax, but rendered Markdown is still data for the next consumer. Triple-bound fragments are trusted input and do not create an instruction or security boundary for a model.
The package deliberately has no loops, helpers, macros, compile phase, HTML renderer, implicit filesystem access, or public syntax-tree API. Prepare repeated sections in application code, pass the finished Markdown as a fragment, and keep import access inside `resolveImport`.
## Related pages
- [Agent Instructions](https://vitehub.dev/docs/agents/instructions)
- [Package reference](https://vitehub.dev/docs/reference)
- [Import paths](https://vitehub.dev/docs/reference/import-paths)
# Provider output
Provider Output is generated deployment or runtime artifacts required by a provider.
It belongs to the package that owns the primitive. Application code does not edit or import it.
## Output families
| Output | Provider | Owner | Purpose |
| --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `deployment.json` | All built-in presets | `vite-hub` | Records the resolved deployment identity and source with the host, runtime, output, and service contract. |
| Worker bundle | Cloudflare | Package integration using Cloudflare output | Runs server or primitive runtime code in Workers. |
| `wrangler.json` entries | Cloudflare | Blob, Database, Queue, Rate Limit, Schedule, Workflow, Sandbox, Agent state as applicable | Declares bindings, crons, durable objects, queues, Rate Limiting bindings, and other worker config. |
| Vercel Build Output | Vercel | Package integration using Vercel output | Writes functions, static files, routes, and function config under `.vercel/output`. |
| Netlify function output | Netlify | Agent and Schedule Packages | Writes generated functions and static config under `.netlify/v1`. |
| Deno Agent server output | Deno | Agent Package | Writes `.vitehub/agent/deno-server.ts` for `Deno.serve` chat and webhook routes. |
| Deno cron output | Deno | Schedule Package | Writes `.vitehub/schedule/deno-cron.mjs` for `Deno.cron` static schedule wake output. |
| Rate Limit manifest | Local and hosted | Rate Limit Package | Writes `.vitehub/rate-limit/manifest.json` with sorted Rate Limit IDs, resolved providers, and driver capabilities. |
| Generated Runtime Registry | Local and hosted | Package that discovers Definitions | Maps Discovery Identity to lazy-loaded Definitions. |
| Generated Nitro handler or plugin | Nitro-shaped hosts | Package that requires a host bridge | Registers package-owned routes or runtime hooks. |
| Provision State | Local development and build input | ViteHub CLI plus package Provision Steps | Stores non-secret provider ids under `.vitehub/provision.json`. |
## Generation timing
Provider Output is normally written during production-shaped builds. Vite dev proves discovery and local generated files; Netlify local development also materialises package functions for Netlify CLI.
```bash [Terminal]
pnpm build
find .vitehub -maxdepth 4 -type f | sort
find .vercel/output -maxdepth 4 -type f | sort
find dist -maxdepth 4 -type f | sort
```
### Rate Limit output
Rate Limit writes `.vitehub/rate-limit/manifest.json` during Vite config resolution and refreshes it during Provider Output. Its `schemaVersion: 2` document contains sorted `rateLimits` entries shaped as `{ name, provider, capabilities }`. Inspect it before deploy; do not import it from application code.
When Cloudflare is selected, each handler-local `requireRateLimit()` policy adds one `ratelimits` entry to generated `wrangler.json`. The entry contains a ViteHub-derived binding name, a stable namespace id, and the guard's static `limit` and 10-second or 60-second period.
The Rate Limit integration owns only binding names derived from declared stable IDs. It preserves unrelated app and package entries, and removes stale entries when a handle is renamed, deleted, or built with a non-Cloudflare provider while the integration remains active.
Application code calls `requireRateLimit()` with the H3 event. Do not persist the generated provider binding name as source authority.
## Provider Output Contracts
Provider Output Contracts assert generated artifact shape without deploying to a cloud account.
Use them for bindings, emitted functions, generated worker config, bundle purity, cron entries, and selected provider dependency reachability.
```bash [Terminal]
pnpm --filter @vite-hub/database test
pnpm --filter @vite-hub/workflow test
```
## Public boundary
Application code imports Runtime Helpers and documented handlers.
Generated Provider Output may import generated files, virtual modules, or provider runtime packages internally.
Netlify Agent output is Provider Output, not an app import: there is no stable `@vite-hub/agent/netlify` import. Inspect `.netlify/v1/functions/vitehub-agent.mjs` and `.vitehub/agent/netlify-function.mjs` during deployment debugging instead of importing them from application code.
Nuxt keeps generated Agent source artifacts under `/vitehub/agent` (normally `.nuxt/vitehub/agent`), so inspect the Netlify wrapper or Deno entrypoint there. Provider-owned `.netlify/v1` output and project-owned non-Agent `.vitehub` output do not move.
| Do | Avoid |
| ----------------------------------------------------- | ----------------------------------------------------- |
| Call `runQueue('welcome-email', payload)`. | Import a generated queue consumer from `.vitehub`. |
| Import `useServerEnv()` from `#vitehub/env/server`. | Import `.vitehub/env/server.mjs` directly. |
| Inspect `.vercel/output` during deployment debugging. | Treat `.vercel/output` files as source files to edit. |
## Related
- [Cloudflare](https://vitehub.dev/docs/frameworks-hosts/cloudflare)
- [Vercel](https://vitehub.dev/docs/frameworks-hosts/vercel)
- [Netlify](https://vitehub.dev/docs/frameworks-hosts/netlify)
- [Deno](https://vitehub.dev/docs/frameworks-hosts/deno)
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
- [Verification](https://vitehub.dev/docs/development/verification)
# Realtime collaboration
Realtime connects TipTap editors through Yjs while keeping canonical Markdown in
a [Workspace](https://vitehub.dev/docs/server-primitives/workspace). Use it when several clients
need to edit the same Workspace document and see presence, connection state, and
external file changes.
## Configure Realtime
Install the ViteHub distribution in a Vue or Nuxt application.
```bash [Terminal]
pnpm add vite-hub @tiptap/vue-3
```
Enable Workspace and Realtime. The memory authority is suitable for local
development and a single-process Node server.
```ts [vite.config.ts]
import { vitehub } from 'vite-hub'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
vitehub({
preset: 'node',
realtime: { authority: 'memory' },
workspace: true,
}),
],
})
```
Create a Realtime Definition under `server/realtime`. Its name comes from the
relative file path, so this file defines `docs`.
```ts [server/realtime/docs.ts]
import { defineRealtime } from 'vite-hub/realtime'
export default defineRealtime({
document: { workspace: 'docs' },
history: {
checkpoint: { message: 'Save collaborative document' },
},
})
```
Add the writable Workspace referenced by the Realtime Definition. This memory
store matches the local, single-process setup above.
```ts [server/workspaces/docs.ts]
import { defineWorkspace } from 'vite-hub/workspace'
export default defineWorkspace({
store: { provider: 'memory' },
rules: {
'/**': { write: true, mediaType: 'text/markdown' },
},
})
```
Set
`auth: true` on the Realtime Definition when every WebSocket and checkpoint
request must have a valid ViteHub Auth session. Connections are public when
`auth` is omitted.
## Connect a TipTap editor
Call `useRealtimeTiptap()` with the Realtime Definition name and a safe
Workspace path. Its editor state is exposed as Vue refs.
```ts [app/composables/useDocumentEditor.ts]
import { useEditor } from '@tiptap/vue-3'
import { useRealtimeTiptap } from 'vite-hub/realtime/vue'
export function useDocumentEditor() {
const realtime = useRealtimeTiptap('docs', 'guides/getting-started.md')
const editor = useEditor({
extensions: realtime.extensions.value,
})
realtime.people.value // Connected people
realtime.status.value // connected, connecting, or disconnected
realtime.synced.value // Whether the initial Yjs sync has completed
return { editor, realtime }
}
```
The composable connects to ViteHub's generated
`/api/_vitehub/realtime/**` WebSocket route. With `auth: true`, the server
verifies the ViteHub Auth session and binds that user to presence updates. In a
public Definition, presence identity is client-asserted—even if the client has a
session—and must not be used as an authorization or verified-identity boundary.
`realtime.workspace.change` reports file changes published by other Workspace
clients. Call `realtime.workspace.notify(change)` after an application changes
a Workspace path outside the collaborative editor.
## Create a durable checkpoint
A room update is collaborative state, not a Workspace write. Create a checkpoint
when the current document must become canonical Markdown in Workspace.
```ts
const checkpoint = await realtime.history.checkpoint()
checkpoint.content
checkpoint.snapshot
```
`history.pending` remains `true` until every overlapping checkpoint request
settles. Checkpoints require a Workspace Store with conditional writes. A
durable Realtime authority also requires a durable Workspace Store.
A checkpoint succeeds only when its snapshot contains the canonical document
digest. If Workspace changed during publication, Realtime rebases onto the
remote head, preserves unrelated staged paths, and reconciles the room. A path
changed both locally and remotely remains a Workspace conflict.
Disabling the composable destroys its document provider, disconnects Workspace
events, clears queued notifications, and makes checkpoint calls reject with
`Realtime is disabled.` Enabling it reconnects both providers for the current
document.
## Choose a room authority
| Authority | Use |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto` | Uses Cloudflare Durable Objects when Realtime can resolve a Cloudflare Nitro preset or hosting environment. With only `vitehub({ preset: 'cloudflare' })` during Vite development, set `authority: 'cloudflare'` explicitly. Other development presets use memory; other production builds fail until an authority is selected. |
| `cloudflare` | Generates a SQLite-backed Durable Object binding and migration. Use it for durable, distributed rooms on Cloudflare. |
| `memory` | Keeps rooms in one process. Use it for local development or an explicitly single-process Node deployment. Room state is lost when the process stops. |
ViteHub rejects the memory authority on distributed host presets. It also
rejects a Cloudflare authority paired with another deployment preset.
## Limits
| Boundary | Limit |
| --------------------------------------- | -------------------------------------------- |
| WebSocket message | 1 MiB |
| Document state per room | 8 MiB |
| Awareness state per room | 8 MiB |
| Awareness clients per peer | 1,024 |
| Active rooms under the memory authority | 128, with inactive clean rooms evicted first |
## Public imports and generated output
| Import | Use |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `defineRealtime` from `vite-hub/realtime` | Declare a discovered Realtime Definition. |
| `useRealtimeTiptap` from `vite-hub/realtime/vue` | Connect a Vue TipTap editor, presence, Workspace events, and checkpoints. |
| `createRealtimeHandler` from `vite-hub/realtime/server` | Build a handler for a manual server integration. The ViteHub integration generates this route for normal applications. |
The integration generates `.vitehub/nitro/realtime/registry.mjs` and
`.vitehub/nitro/realtime/handler.ts`. Treat both as inspectable build output,
not application imports.
## Related
- [Workspace](https://vitehub.dev/docs/server-primitives/workspace)
- [Auth](https://vitehub.dev/docs/server-primitives/auth)
- [File conventions](https://vitehub.dev/docs/reference/file-conventions)
- [Config options](https://vitehub.dev/docs/reference/config-options)
- [Generated files](https://vitehub.dev/docs/development/generated-files)
# Runtime events
Runtime events describe what happened across package boundaries.
The Runtime Package owns Trace Events, Policy Decisions, Approval Requests, runtime capability handles, and wait-until behavior carried through [Runtime Context](https://vitehub.dev/docs/concepts/runtime-context).
## Trace Event
`TraceEvent` is the shared observability event shape.
It can describe policy, approval, capability, error, lifecycle, or run activity.
```ts [@vite-hub/runtime]
interface TraceEvent {
attributes?: Record
name: string
timestamp?: Date | string
trace?: {
id: string
parentId?: string
sampled?: boolean
}
type: 'approval' | 'capability' | 'error' | 'lifecycle' | 'policy' | 'run'
}
```
## Runtime lifecycle hooks
Runtime lifecycle hooks are host-provided callbacks for observing shared runtime behavior.
They do not replace package-owned hooks such as Agent Finish Hooks.
| Hook | Payload |
| ---------- | ----------------------------------------- |
| `request` | Runtime Host Context before work starts. |
| `approval` | Approval Request and runtime context. |
| `trace` | Trace Event and runtime context. |
| `error` | Error and runtime context. |
| `finish` | Runtime Host Context after work finishes. |
## Policy and approvals
Policy Decisions are runtime outcomes, not generic booleans.
An Approval Request is created only when policy requires external approval.
An omitted policy resolves to `allow`.
| Value | Meaning |
| ------------------- | -------------------------------------------------- |
| `allow` | Continue the operation. |
| `deny` | Stop the operation. |
| `require-approval` | Stop until an Approval Decision permits execution. |
| `retryable-failure` | Treat the operation as failed but retryable. |
## Agent stream events
Agent stream output is package-owned Agent behavior.
The current event stream includes text deltas, data parts, tool input and result events, progress events, approval events, errors, finish events, and usage events.
| Event family | Use |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `text-delta` | Stream assistant text. |
| `tool-call` and `tool-result` | Represent model-facing tool execution. `tool-result.durationMs` can report elapsed tool time. |
| `progress` | Represent non-tool runtime progress such as `workspace.prepare`. |
| `approval-request` and `approval-decision` | Represent approval-shaped tool policy. |
| `data-agent-input` | Carries provider questions as `{ requestId, questions, status: "requested" }`; replies use `{ requestId, answers }`. |
| `error` | Represent recoverable or terminal stream errors. |
| `finish` | Mark completion. |
| `usage` | Carry an Agent Usage Record when an Agent Driver reports usage. |
## Agent Usage Record
Agent Usage Records normalize usage across model-backed, provider-backed, and custom-run-backed Agent Drivers when usage exists.
Token fields appear only when the provider reports them or ViteHub can derive them safely.
Streams can carry the full Agent Usage Record through `{ type: "usage", usageRecord }`.
At finish time, read the normalized Agent Usage Record from `event.invocation.usage` in an Agent Finish Hook or `context.invocation.usage` in a Channel Delivery finish effect.
Applications format that data when a product surface needs text, UI, notes, billing records, or comments.
## Related
- [Agent Evals](https://vitehub.dev/docs/agents/evals)
- [CLI inspection](https://vitehub.dev/docs/development/cli)
- [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics)
# Auth
Use Auth to add Better Auth sessions and server-side identity checks to a ViteHub app. ViteHub discovers one Auth Definition, mounts its route, and provides server helpers. Better Auth still provides the sign-in UI, client plugins, and provider-specific behavior.
The `database` and `secondaryStorage` fields record where Auth data belongs. They don't create Better Auth storage adapters. Supply those adapters in the runtime configuration when you need persistent storage.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/auth @vite-hub/runtime better-auth
```
### Configure
```ts [vite.config.ts]
import { hubAuth } from '@vite-hub/auth/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubAuth()],
})
```
### Start using it
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
emailAndPassword: { enabled: true },
})
```
::
## Public imports
| Import | Use |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `defineAuth` from `@vite-hub/auth` | Declare the Primary Auth Definition. |
| `auth`, `getAuth`, `getAuthForRequest` from `@vite-hub/auth/server` | Access the Better Auth instance from server code. |
| `handleAuth`, `handleAuthRequest`, `createAuthHandler` from `@vite-hub/auth/server` | Mount or call the Auth handler manually. |
| `requireAuth` from `@vite-hub/auth/server` | Guard server routes with an Auth Session. |
| `authenticated` from `@vite-hub/auth/agent` | Map a Better Auth session into an Agent Invoker. |
| `getViteHubErrorShape` from `@vite-hub/runtime` | Handle missing authentication and provider failures by stable Auth code. |
| `hubAuth` from `@vite-hub/auth/vite` | Register Auth discovery, route exposure, and generated server aliases. |
Create one Primary Auth Definition. ViteHub discovers `server/auth.ts` or `server.auth.ts`.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
emailAndPassword: {
enabled: true,
},
})
```
Better Auth-compatible options stay top-level. ViteHub reserves Auth fields such as `database`, `secondaryStorage`, `basePath`, `route`, `access`, and `runtime` for package-owned behavior.
## Auth Definition options
`defineAuth()` accepts Better Auth server options at the top level, plus ViteHub-owned Auth fields.
| Option | Type | Default | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Better Auth options | `AuthBetterAuthOptions` | Better Auth defaults | Passed through to `betterAuth()` after ViteHub-owned fields are removed. |
| `database` | `AuthDatabaseConfiguration` | Default Database metadata | Records intended Default or Named Database placement for inspection. Use `true` or `{ name, dedicated? }`. It does not create a Better Auth database adapter. |
| `secondaryStorage` | `AuthSecondaryStorageConfiguration` | disabled | Records intended Default or named KV Store placement for inspection. Use `true` or `{ store }`. It does not create a Better Auth secondary storage adapter. |
| `basePath` | `string` | `/api/auth` | Sets the Auth Base Path. |
| `route` | `false` | enabled | Disables automatic Auth Route Exposure when set to `false`. |
| `access.routes` | `AuthAccessRoute[]` | `[]` | Routes guarded by generated Auth access middleware. Route objects accept `method`, `route`, and an `authorize` callback. |
| `access.signIn` | `{ provider: string, callbackURL?: string, errorCallbackURL?: string, requestSignUp?: boolean, scopes?: string[] }` | none | Redirect behavior for HTML requests rejected by `requireAuth()`. |
| `runtime` | `AuthRuntimeConfiguration` | none | Supplies runtime-only Better Auth values such as `baseURL`, `secret`, and `secrets`. |
`baseURL`, `secret`, and `secrets` are runtime-only. Put them in the Definition callback or `runtime`, not as static top-level fields. Concrete Better Auth `database` and `secondaryStorage` adapters are also runtime values; return them from the callback or `runtime` when Auth needs persistent storage.
## Use it at runtime
The default Auth route is `/api/auth/**`. ViteHub mounts it automatically, so same-origin apps don't need a manual route file.
Vue apps can use the same-origin ViteHub Auth client and normalized session state directly.
```ts [lib/auth-client.ts]
import { useUserSession } from '@vite-hub/auth/vue'
export const userSession = useUserSession()
```
Import `createAuthClient` from the same entry when a custom base path or Better Auth client plugins are required.
Server code can read the discovered Auth instance or require a session for a request.
```ts [server/api/me.get.ts]
import { auth } from '@vite-hub/auth/server'
export default defineEventHandler(async (event) => {
const headers = new Headers(getRequestHeaders(event))
return auth.api.getSession({ headers })
})
```
## Runtime options
Use an Auth Definition callback or the `runtime` field when values depend on the current request, Server Env, provider credentials, or request origin.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth(({ env, requestOrigin }) => ({
appName: 'Acme',
baseURL: requestOrigin,
runtime: {
secret: env.auth.secret.unseal(),
},
socialProviders: {
github: {
clientId: env.auth.github.clientId,
clientSecret: env.auth.github.clientSecret.unseal(),
},
},
}))
```
When `@vite-hub/env` is installed before Auth, the callback receives typed Server Env. `requestOrigin` lets same-origin apps avoid a separate auth URL variable.
## Server helpers
| Helper | Description |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `auth` | Proxy to the discovered Better Auth instance. |
| `getAuth(runtimeOptions?)` | Returns the discovered Better Auth instance. |
| `getAuthForRequest(request, runtimeOptions?, event?)` | Returns a request-aware Better Auth instance. |
| `handleAuth(input, runtimeOptions?)` | Handles an Auth HTTP request. |
| `handleAuthRequest(definition, request, runtimeOptions?, event?)` | Handles an Auth request for an explicit Auth Definition. |
| `createAuthHandler(definition, runtimeOptions?)` | Creates a Better Auth handler from a Definition. |
| `requireAuth(input, definition?)` | Returns `undefined` when a session exists, otherwise returns an unauthorized or sign-in response. |
### Authorize access routes
Add `authorize` when a session alone is not enough. ViteHub calls it only after authentication and returns `403` when it returns `false`. The callback can return a `Response` for a custom rejection. Returning `true` allows the request.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
access: {
routes: [
{
route: '/_vitehub/**',
authorize: ({ user }) => user.isAdmin === true,
},
{
route: '/api/_vitehub/console/**',
authorize: ({ user }) => user.isAdmin === true,
},
],
},
})
```
The callback receives the authenticated `user`, `session`, and request. ViteHub does not define an admin role. The host maps its own role or permission model here.
Read [Console](https://vitehub.dev/docs/development/console#protect-both-route-groups) for its page and API routes, plus the behavior when Console is disabled.
## Storage placement metadata
The `database` and `secondaryStorage` fields describe intended ViteHub primitive placement. ViteHub removes these metadata values before it calls `betterAuth()`, so they do not connect Better Auth to `@vite-hub/database` or `@vite-hub/kv`.
Omitting `database`, or setting it to `true`, selects Default Database metadata. Use a named reference when inspection needs to record the target.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
database: true,
})
```
Set `dedicated: true` to record that the Named Database is dedicated to Auth.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
database: { name: 'auth', dedicated: true },
})
```
Secondary Storage metadata is opt-in. Use `true` for the Default KV Store metadata or `{ store }` for a named target.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
secondaryStorage: { store: 'auth' },
})
```
To persist Better Auth data today, supply a concrete Better Auth database or secondary storage adapter from the Auth Definition callback or its `runtime` field. The placement metadata above does not substitute for that adapter.
## Vite Integration options
`hubAuth()` accepts `false` to disable Auth integration for a build. The Vite config key is `auth`.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubAuth()],
auth: false,
})
```
## Provider output
Auth generates the definition module, route handler, access middleware, and ambient types needed by the host integration. Application code uses `@vite-hub/auth/server` or Better Auth clients, not generated files.
Set `route: false` only when a host integration or manual route mounts the Auth handler itself.
```ts [server/auth.ts]
import { defineAuth } from '@vite-hub/auth'
export default defineAuth({
appName: 'Acme',
route: false,
})
```
## Connect Auth to Agents
Auth identifies application users and sessions. Agents receive Agent Invokers. Map trusted Auth state into an Agent Invoker instead of adding Auth to the Agent Definition.
Read [Auth Users and Agent Invokers](https://vitehub.dev/docs/concepts/auth-users-and-agent-invokers) for the mental model and [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) for agent-facing access patterns.
### Handle required authentication
`authenticated()` throws `ViteHubError` with code `AUTHENTICATION_REQUIRED` when no required Auth Session exists. HTTP adapters map that code to `401`; the error serializes its public message without its cause or stack.
```ts
import { ViteHubError } from '@vite-hub/runtime'
const error = new ViteHubError('AUTHENTICATION_REQUIRED', 'Sign in to use this Agent.')
console.log(error.toJSON())
```
When a default Better Auth request or session operation fails, the boundary throws the same shared error with code `AUTH_PROVIDER_OPERATION_FAILED` and safe operation details; raw provider diagnostics remain available only through `cause`. Existing ViteHub errors and structural `AbortError` objects keep their identity. Missing APIs, malformed responses, invalid Auth Definitions, invalid `authenticated()` configuration, and invalid custom callback results are programmer or provider-contract errors, so they continue to throw `TypeError` rather than authentication failures.
## Next steps
- Configure typed secrets with [Env](https://vitehub.dev/docs/server-primitives/env).
- Protect the [ViteHub Console](https://vitehub.dev/docs/development/console) before enabling it in production.
- Use [Database](https://vitehub.dev/docs/server-primitives/database) and [KV](https://vitehub.dev/docs/server-primitives/kv) as application primitives; Auth placement metadata does not wire them into Better Auth.
- Learn shared identity boundaries in [Auth Users and Agent Invokers](https://vitehub.dev/docs/concepts/auth-users-and-agent-invokers).
# Blob
Use Blob for uploads, generated media, PDFs, exports, and other objects that don't need a file tree.
Use [Workspace](https://vitehub.dev/docs/server-primitives/workspace) when files need paths, snapshots, diffs, Source sync, or agent access. A Blob Store only keeps objects and their metadata.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/blob
```
### Configure
```ts [vite.config.ts]
import { hubBlob } from '@vite-hub/blob/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubBlob()],
})
```
### Start using it
```ts [server/api/files.post.ts]
import { blob } from '@vite-hub/blob'
export default defineEventHandler(async () => {
const [error, object] = await blob.put('hello.txt', 'Hello from ViteHub')
if (error) throw error
return object
})
```
::
## Public imports
| Import | Use |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `blob` from `@vite-hub/blob` | Read and write the Default Blob Store or named Blob Stores. |
| `detectContentType` from `vite-hub/blob/content-type` or `@vite-hub/blob/content-type` | Classify common image and PDF signatures before storage. |
| `ensureBlob` from `@vite-hub/blob` or `@vite-hub/blob/ensure` | Validate upload size and content type. |
| `hubBlob` from `@vite-hub/blob/vite` | Register Blob runtime configuration and Provider Output. |
| `resolveBlobViteConfig` from `@vite-hub/blob/vite` | Resolve Blob Vite runtime config manually. |
| `@vite-hub/blob/drivers/*` | Import provider-specific Blob Driver Modules. |
All Blob driver, object, list, put, store, and module types are exported from `@vite-hub/blob`.
Blob writes preserve the metadata you provide. `detectContentType()` checks common leading signatures, but it doesn't validate the complete file or prove that the file is safe.
## Store configuration
Configure one default Blob Store directly, or configure named stores with `blob.stores`.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubBlob()],
blob: {
stores: {
default: { driver: 'fs' },
reports: { driver: 'vercel-blob', access: 'private' },
},
},
})
```
| Shape | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `blob: false` | Disables Blob runtime configuration. |
| `blob: BlobStoreConfig` | Configures one Default Blob Store with a `driver` and its provider options. Without a driver, ViteHub infers Cloudflare R2, Netlify Blobs, Vercel Blob, or local filesystem storage from the host and runtime env. |
| `blob: { stores: Record }` | Defines named Blob Stores. `stores.default` is required. |
| `blob: { serve: false }` | Disables Blob route generation. This is the default. |
| `blob: { serve: true }` | Generates an opt-in Nitro route at `/api/_vitehub/blob/**` for serving the Default Blob Store. |
| `blob: { serve: { route?, store?, publicBaseUrl?, headers? } }` | Generates an opt-in Nitro route. `route` defaults to `/api/_vitehub/blob`, `store` defaults to `default`, `publicBaseUrl` changes generated public URLs, and `headers` adds static response headers. |
## Provider options
Every Blob Store config is a discriminated union selected by `driver`. Keep credentials in Server Env or provider-managed secrets; fields in these tables describe the exact public config shape, not a recommendation to commit secrets to Vite config.
### Local filesystem
| Option | Type | Default | Description |
| --------------------- | -------- | -------------------------------------- | ----------------------------------------------------- |
| `driver` | `'fs'` | Required | Selects local filesystem storage. |
| `base` | `string` | `BLOB_FS_BASE` or `.vitehub/data/blob` | Sets the storage directory. |
| `defaultUrlExpiresIn` | `number` | Files SDK default | Sets the default generated URL lifetime in seconds. |
| `urlBaseUrl` | `string` | None | Sets the base URL returned by the filesystem adapter. |
Filesystem storage is for local or single-process use. It does not become durable shared storage on a serverless host.
### Cloudflare R2
| Option | Type | Default | Description |
| --------------------- | ----------------- | ---------------------- | -------------------------------------------------------------------------------- |
| `driver` | `'cloudflare-r2'` | Required | Selects the Cloudflare R2 driver. |
| `binding` | `string` | `BLOB` | Names the runtime R2 binding. |
| `bucketName` | `string` | R2 or Blob bucket env | Names the bucket for Provider Output and HTTP fallback. |
| `accountId` | `string` | Cloudflare account env | Supplies the account id for HTTP fallback. |
| `accessKeyId` | `string` | R2 access-key env | Supplies the HTTP fallback access key. |
| `secretAccessKey` | `string` | R2 secret-key env | Supplies the HTTP fallback secret. |
| `defaultUrlExpiresIn` | `number` | Files SDK default | Sets the default signed URL lifetime in seconds. |
| `publicBaseUrl` | `string` | None | Uses a public or CDN base URL for objects instead of signed URLs when supported. |
The runtime binding takes precedence. HTTP credentials are used when no active binding exists.
### Vercel Blob
| Option | Type | Default | Description |
| ------------------- | ---------------------- | ----------------------- | ------------------------------------------------------------------------------------------- |
| `driver` | `'vercel-blob'` | Required | Selects Vercel Blob. |
| `access` | `'private' | 'public'` | `'public'` | Sets the store-level access policy. A `blob.put()` call can override it. |
| `allowOverwrite` | `boolean` | `true` | Allows writes to replace an existing pathname. |
| `downloadTimeoutMs` | `number` | Provider default | Sets the download timeout in milliseconds. |
| `token` | `string` | `BLOB_READ_WRITE_TOKEN` | Supplies the Vercel Blob token. ViteHub resolves masked build-time values again at runtime. |
### Netlify Blobs
| Option | Type | Default | Description |
| -------------- | ----------------------- | ------------------- | -------------------------------------------------------------- |
| `driver` | `'netlify-blobs'` | Required | Selects Netlify Blobs. |
| `name` | `string` | `vitehub-blob` | Names the Netlify Blob Store. |
| `consistency` | `'eventual' | 'strong'` | Provider default | Selects the Netlify read-consistency mode. |
| `deployScoped` | `boolean` | Provider default | Scopes the store to the active deploy when enabled. |
| `siteID` | `string` | Netlify runtime env | Supplies the Netlify site id outside an injected runtime. |
| `token` | `string` | Netlify runtime env | Supplies the Netlify access token outside an injected runtime. |
### S3 and S3-compatible providers
The `s3`, `akamai`, `digitalocean-spaces`, `hetzner`, `storj`, and `minio` drivers share object-storage routing options. Their required fields differ.
| Driver | Required fields | ViteHub defaults |
| --------------------- | ------------------ | ------------------------------------------------------------------------------------------------------- |
| `s3` | `bucket` | None |
| `akamai` | `bucket`, `region` | None |
| `digitalocean-spaces` | `bucket`, `region` | None |
| `hetzner` | `bucket`, `region` | None |
| `storj` | `bucket` | None |
| `minio` | None | Bucket `vitehub-blob`, endpoint `http://localhost:9000`, region `us-east-1`, and `forcePathStyle: true` |
| Option | Drivers | Type | Description |
| --------------------- | ------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `driver` | All | provider literal | Selects one driver from the table above. |
| `bucket` | All | `string` | Names the object-storage bucket. It is optional only for `minio`. |
| `endpoint` | All | `string` | Overrides the provider endpoint. |
| `region` | All | `string` | Selects the provider region. It is required for Akamai, DigitalOcean Spaces, and Hetzner. |
| `forcePathStyle` | All | `boolean` | Uses path-style bucket URLs instead of virtual-hosted URLs. |
| `publicBaseUrl` | All | `string` | Uses a public or CDN base URL for objects. |
| `defaultUrlExpiresIn` | All | `number` | Sets the default signed URL lifetime in seconds. |
| `credentials` | `s3` | `{ accessKeyId, secretAccessKey, sessionToken? }` | Supplies an explicit AWS-compatible credential object. |
| `accessKeyId` | Provider-specific drivers and `minio` | `string` | Supplies the access key directly. |
| `secretAccessKey` | Provider-specific drivers and `minio` | `string` | Supplies the secret key directly. |
MinIO resolves credentials from `MINIO_ACCESS_KEY_ID`, `MINIO_ACCESS_KEY`, `MINIO_ROOT_USER`, or `AWS_ACCESS_KEY_ID`, with matching secret-key env aliases. Other S3-compatible adapters also support their provider or SDK credential sources.
### Google Cloud Storage
| Option | Type | Required | Description |
| --------------------- | ------------------------------- | -------- | ------------------------------------------------ |
| `driver` | `'gcs'` | Yes | Selects Google Cloud Storage. |
| `bucket` | `string` | Yes | Names the bucket. |
| `credentials` | `{ client_email, private_key }` | No | Supplies service-account credentials inline. |
| `keyFilename` | `string` | No | Loads service-account credentials from a file. |
| `projectId` | `string` | No | Selects the Google Cloud project. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default signed URL lifetime in seconds. |
| `publicBaseUrl` | `string` | No | Uses a public or CDN base URL for objects. |
### Azure Blob Storage
| Option | Type | Required | Description |
| --------------------- | --------- | -------- | -------------------------------------------------------------------------- |
| `driver` | `'azure'` | Yes | Selects Azure Blob Storage. |
| `container` | `string` | Yes | Names the container. |
| `accountName` | `string` | No | Supplies the storage account name. |
| `accountKey` | `string` | No | Authenticates with the account key. |
| `connectionString` | `string` | No | Authenticates and configures the endpoint with an Azure connection string. |
| `sasToken` | `string` | No | Authenticates with a shared access signature. |
| `endpoint` | `string` | No | Overrides the Blob service endpoint. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default signed URL lifetime in seconds. |
| `publicBaseUrl` | `string` | No | Uses a public or CDN base URL for objects. |
### Supabase Storage
| Option | Type | Required | Description |
| --------------------- | ------------ | -------- | ------------------------------------------------ |
| `driver` | `'supabase'` | Yes | Selects Supabase Storage. |
| `bucket` | `string` | Yes | Names the bucket. |
| `url` | `string` | No | Supplies the Supabase project URL. |
| `key` | `string` | No | Supplies the Supabase API key. |
| `public` | `boolean` | No | Treats the bucket as public. |
| `publicBaseUrl` | `string` | No | Overrides the public object URL base. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default signed URL lifetime in seconds. |
### UploadThing
| Option | Type | Required | Description |
| --------------------- | --------------------------- | -------- | --------------------------------------------------- |
| `driver` | `'uploadthing'` | Yes | Selects UploadThing. |
| `token` | `string` | No | Supplies the UploadThing token. |
| `acl` | `'private' | 'public-read'` | No | Sets the uploaded object ACL. |
| `region` | `string` | No | Selects the upload region. |
| `slug` | `string` | No | Selects the UploadThing route or file slug. |
| `downloadTimeoutMs` | `number` | No | Sets the download timeout in milliseconds. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default generated URL lifetime in seconds. |
### Google Drive
| Option | Type | Required | Description |
| ----------------- | ------------------------------- | -------- | ---------------------------------------------- |
| `driver` | `'google-drive'` | Yes | Selects Google Drive. |
| `credentials` | `{ client_email, private_key }` | No | Supplies service-account credentials inline. |
| `keyFilename` | `string` | No | Loads service-account credentials from a file. |
| `subject` | `string` | No | Selects the delegated Workspace user. |
| `driveId` | `string` | No | Selects a shared drive. |
| `rootFolderId` | `string` | No | Restricts objects to a root folder. |
| `fileIdCacheSize` | `number` | No | Limits the path-to-file-id cache. |
| `publicByDefault` | `boolean` | No | Makes newly written files public by default. |
### OneDrive
| Option | Type | Required | Description |
| ------------------- | ----------------------------------------------------- | -------- | ---------------------------------------------------- |
| `driver` | `'onedrive'` | Yes | Selects OneDrive or SharePoint-backed storage. |
| `accessToken` | `string` or async callback | No | Supplies or resolves a Microsoft Graph access token. |
| `clientCredentials` | `{ tenantId, clientId, clientSecret }` | No | Uses the OAuth client-credentials flow. |
| `oauth` | `{ clientId, clientSecret, refreshToken, tenantId? }` | No | Uses a refresh-token OAuth flow. |
| `driveId` | `string` | No | Selects a drive directly. |
| `siteId` | `string` | No | Selects a SharePoint site. |
| `userId` | `string` | No | Selects a user's drive. |
| `rootFolderPath` | `string` | No | Restricts objects to a root folder path. |
| `copyTimeoutMs` | `number` | No | Sets the asynchronous copy timeout in milliseconds. |
| `publicByDefault` | `boolean` | No | Makes newly written files public by default. |
### Dropbox
| Option | Type | Required | Description |
| --------------------- | -------------------------- | -------- | --------------------------------------------------- |
| `driver` | `'dropbox'` | Yes | Selects Dropbox. |
| `accessToken` | `string` or async callback | No | Supplies or resolves an access token. |
| `appKey` | `string` | No | Supplies the OAuth app key. |
| `appSecret` | `string` | No | Supplies the OAuth app secret. |
| `refreshToken` | `string` | No | Refreshes OAuth access with the app credentials. |
| `rootFolderPath` | `string` | No | Restricts objects to a root folder path. |
| `publicByDefault` | `boolean` | No | Creates shared links by default. |
| `publicBaseUrl` | `string` | No | Uses an app-owned public URL base. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default generated URL lifetime in seconds. |
### Box
| Option | Type | Required | Description |
| --------------------- | ---------------------------------------------------- | -------- | --------------------------------------------------- |
| `driver` | `'box'` | Yes | Selects Box. |
| `developerToken` | `string` | No | Authenticates with a Box developer token. |
| `ccg` | `{ clientId, clientSecret, enterpriseId?, userId? }` | No | Uses Box Client Credentials Grant authentication. |
| `jwt` | `{ configJsonString } | { configFilePath }` | No | Uses a Box JWT application configuration. |
| `oauth` | `{ clientId, clientSecret, refreshToken }` | No | Uses an OAuth refresh-token flow. |
| `rootFolderId` | `string` | No | Restricts objects to a root folder. |
| `publicByDefault` | `boolean` | No | Creates shared links by default. |
| `publicBaseUrl` | `string` | No | Uses an app-owned public URL base. |
| `defaultUrlExpiresIn` | `number` | No | Sets the default generated URL lifetime in seconds. |
## Use it at runtime
Use the `blob` Runtime Helper from server code.
```ts [server/api/files.post.ts]
import { blob } from '@vite-hub/blob'
export default defineEventHandler(async (event) => {
const body = await readBody<{ path: string, text: string }>(event)
const [error] = await blob.put(body.path, body.text, {
contentType: 'text/plain',
customMetadata: { source: 'api' },
})
if (error) throw error
return { ok: true }
})
```
```ts [server/api/files/[...path\\].get.ts]
import { blob } from '@vite-hub/blob'
export default defineEventHandler(async (event) => {
const path = getRouterParam(event, 'path')!
const [error, object] = await blob.get(path)
if (error) throw error
if (!object) {
throw createError({ statusCode: 404 })
}
return object
})
```
Use named Blob Stores when configuration defines multiple stores.
```ts [server/reports.ts]
import { blob } from '@vite-hub/blob'
export const reports = blob.store('reports')
```
## Serve blob-backed assets
Blob serving is opt-in. Set `serve` in the Blob config, or pass `hubBlob({ serve: true })`, to generate a Nitro route that serves Blob-backed assets through `blob.serve()`.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubBlob()],
blob: {
driver: 'fs',
serve: true,
},
})
```
`serve: true` uses `/api/_vitehub/blob` as the route base. ViteHub chooses a namespaced API route by default so generated handlers avoid app routes, static assets, and framework asset directories. The default also mirrors server API route conventions.
Use an explicit `serve.route` for product-facing asset URLs.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubBlob()],
blob: {
driver: 's3',
bucket: 'app-assets',
serve: {
route: '/assets',
headers: {
'Cache-Control': 'public, max-age=300',
'X-Content-Type-Options': 'nosniff',
},
},
},
})
```
Use `serve.headers` for static cache and security policy. Blob metadata remains authoritative for content headers such as `Content-Type`, `Content-Length`, and `ETag`.
The generated Nitro route maps `${route}/**` to the selected Blob Store and delegates streaming to `blob.store(storeName).serve(event, pathname)`. The default route is a safe framework default. It is not a recommendation that every app expose public assets under `/api`.
Objects from the served store include a URL. With `serve.publicBaseUrl`, the URL is absolute. Without it, the URL is route-relative so request-aware consumers can resolve it against their own origin.
## Runtime helper
`blob` implements `BlobStorage`.
Every async method returns `[error, value]`. Expected provider and storage failures are `ViteHubError` values with `BLOB_*` codes, so application code can apply HTTP, retry, logging, or best-effort policy without `try/catch`. Invalid arguments, unknown stores, and unsupported signing capabilities still throw because they indicate API or configuration misuse. Generated serving routes unwrap `blob.serve()` and pass its error to H3.
| Method | Description |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| `blob.put(pathname, body, options?)` | Stores text, bytes, streams, ArrayBuffers, or `Blob` objects. |
| `blob.get(pathname)` | Reads a `Blob` or returns `null`. |
| `blob.head(pathname)` | Reads object metadata. |
| `blob.list(options?)` | Lists objects with optional `prefix`, `limit`, `cursor`, and folded folders. |
| `blob.del(pathnames)` | Deletes one or more objects. |
| `blob.sign(pathname, options)` | Signs a short-lived `GET` or `PUT` request for one object. |
| `blob.serve(event, pathname)` | Serves an object stream through an H3 event. |
| `blob.store(name)` | Selects a named Blob Store. |
## Write options
| Option | Type | Description |
| ----------------- | -------------------------- | ------------------------------------------------------------------------------ |
| `contentType` | `string` | Stored MIME type. |
| `contentLength` | `string` | Expected content length when the provider supports it. |
| `customMetadata` | `Record` | Provider custom metadata. |
| `access` | `BlobPutOptions['access']` | Object access policy when the driver supports it. Values: `private`, `public`. |
| `addRandomSuffix` | `boolean` | Adds a random suffix when supported by the driver. |
| `prefix` | `string` | Provider path prefix when supported by the driver. |
## Signed requests
Use `blob.sign()` when a client or provider needs short-lived direct access to one private object. The result contains the URL, HTTP method, and every header that must be sent with the request.
```ts [server/api/uploads/presign.post.ts]
import { blob } from '@vite-hub/blob'
const [sourceError, source] = await blob.sign('users/user/jobs/job/source.mp3', {
method: 'GET',
expiresIn: 6 * 60 * 60,
})
if (sourceError) throw sourceError
const [uploadError, upload] = await blob.sign('users/user/jobs/job/source.mp3', {
method: 'PUT',
expiresIn: 15 * 60,
contentType: 'audio/mpeg',
createOnly: true,
})
if (uploadError) throw uploadError
```
Send `upload.headers` unchanged with the `PUT` body. `contentType` binds the upload MIME type into the signed request. `createOnly` binds a provider condition that rejects the upload when the object already exists; drivers that cannot enforce it throw instead of silently allowing an overwrite.
Cloudflare R2 signs through its S3-compatible HTTP credentials, including when normal reads and writes use a Workers binding. A binding alone cannot mint a presigned URL, so configure `accountId`, `accessKeyId`, `secretAccessKey`, and `bucketName` through runtime environment values. [R2 presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/){rel=""nofollow""} accept expiries from 1 second through 7 days, and the [S3 compatibility contract](https://developers.cloudflare.com/r2/api/s3/api/){rel=""nofollow""} supports `If-None-Match` on `PutObject`.
## `ensureBlob(blob, options)`
Use `ensureBlob()` at upload boundaries.
| Option | Type | Description |
| --------- | ------------ | -------------------------------------------------------------------------------------------- |
| `maxSize` | `BlobSize` | Rejects blobs larger than the limit. Examples: `4MB`, `128KB`, `1GB`. |
| `types` | `BlobType[]` | Allows exact MIME types or broad types such as `image`, `video`, `audio`, `pdf`, and `text`. |
## Provider output
The Blob package selects the default or named store and loads its driver. Put provider bucket names, tokens, and bindings in integration configuration or deployment setup.
Application code keeps importing `blob` from `@vite-hub/blob` when you switch providers.
## Connect Blob to Agents
Direct Blob access is for server code. To let a model inspect or edit scoped object storage, attach the Blob Capability.
Give a Blob Capability the narrowest useful key prefix and configure write access deliberately. Use Workspace when the model needs a file tree, diffs, snapshots, or Source-backed context.
## Production checks
Store content types and metadata at write time. Avoid guessing object type later from path names.
Blob can store Workspace data, but it doesn't provide a file tree to an Agent. Workspace handles file operations, rules, snapshots, and diffs.
Blob stores binary objects and small object metadata. Keep catalogs, indexes, permissions, search records, domain records, and richer metadata queries in KV, Database, or another NoSQL/catalog store next to Blob.
## Cloudflare R2 bucket
Cloudflare R2 Blob Stores use the configured runtime binding when it exists. `binding` defaults to `BLOB`, and `bucketName` lets ViteHub emit the matching Cloudflare R2 bucket binding in Provider Output.
```ts [vite.config.ts]
export default defineConfig({
blob: {
driver: 'cloudflare-r2',
binding: 'BLOB',
bucketName: 'assets',
},
})
```
When no runtime binding exists, ViteHub falls back to R2 HTTP access through `files-sdk/r2`. Set `accessKeyId` and `secretAccessKey` with runtime env, not `vite.config.ts`; non-secret values such as `bucketName` can stay in config.
```env [.env]
R2_ACCOUNT_ID=account-id
R2_ACCESS_KEY_ID=access-key-id
R2_SECRET_ACCESS_KEY=secret-access-key
R2_BUCKET_NAME=assets
```
| Runtime value | Source |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accountId` | `R2_ACCOUNT_ID`, `CLOUDFLARE_R2_ACCOUNT_ID`, `CLOUDFLARE_ACCOUNT_ID` |
| `accessKeyId` | `R2_ACCESS_KEY_ID`, `CLOUDFLARE_R2_ACCESS_KEY_ID` |
| `secretAccessKey` | `R2_SECRET_ACCESS_KEY`, `CLOUDFLARE_R2_SECRET_ACCESS_KEY` |
| `bucketName` | `bucketName` config, or `BLOB_BUCKET_NAME`, `CLOUDFLARE_R2_BUCKET_NAME`, `R2_BUCKET_NAME` read at config/build time for generated Cloudflare `r2_buckets`. HTTP fallback can also read these names from active runtime env. |
Install the optional R2 HTTP dependencies only when you rely on fallback access.
```bash [Terminal]
pnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
```
## S3-compatible object storage
Use `driver: 's3'` for production S3-compatible object storage that is not one of ViteHub's provider-specific drivers.
```bash [Terminal]
pnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
```
```ts [vite.config.ts]
export default defineConfig({
blob: {
driver: 's3',
bucket: 'app-assets',
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
publicBaseUrl: 'https://assets.example.com',
},
})
```
Store S3 credentials in Server Env or the provider credential chain used by the S3 SDK. Put non-secret routing values such as `bucket`, `endpoint`, `region`, and `publicBaseUrl` in config.
Use Cloudflare R2 when the app runs with an R2 binding or R2 HTTP credentials. Use MinIO when local development or Docker Compose needs to exercise S3-compatible behavior.
## MinIO object storage
Use MinIO when you want Docker Compose or local staging to exercise object-storage semantics instead of a mounted filesystem.
```bash
pnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
```
```ts [vite.config.ts]
export default defineConfig({
blob: {
driver: 'minio',
},
})
```
```env [.env]
MINIO_ENDPOINT=http://minio:9000
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=password
BLOB_BUCKET_NAME=vitehub-blob
```
ViteHub reads MinIO credentials from runtime env and masks them in generated provider output. It accepts the Files SDK names `MINIO_ACCESS_KEY_ID` and `MINIO_SECRET_ACCESS_KEY`, plus Docker Compose aliases such as `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD`. `driver: 'minio'` defaults to path-style S3 requests, `us-east-1`, `http://localhost:9000`, and the `vitehub-blob` bucket. For production Docker deployments, use managed `s3` or a production S3-compatible store instead of a single-host Compose MinIO service.
## Next steps
- Use [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for file-tree state.
- Use [Source](https://vitehub.dev/docs/server-primitives/source) for read-only retrieval.
- Expose scoped model access through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities).
# Browser
Use a Browser Definition when trusted server code needs to inspect a page, render browser-only UI, take a screenshot, or create a PDF. Give each operation a name, then call it from a route, queue, or workflow.
Browser Definitions currently run through Cloudflare Browser Run and require the Cloudflare preset. ViteHub configures the provider, so application code doesn't import Cloudflare packages or pass browser credentials.
Server code calls Browser Definitions directly. To give an Agent browser access, attach the [`browser()` Capability](https://vitehub.dev/docs/capabilities/browser) or expose a narrower tool.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add vite-hub
```
### Configure
Enable Browser on the Cloudflare deployment preset.
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'
export default defineConfig({
plugins: [
vitehub({
preset: 'cloudflare',
browser: true,
}),
],
})
```
### Define a browser operation
Place Browser Definitions in `server/browsers/` or name them `*.browser.ts`.
```ts [server/browsers/page-html.ts]
import { defineBrowser } from 'vite-hub/browser'
export default defineBrowser(async (
input: { url: string },
{ browser },
) => {
return await browser.content(input.url)
})
```
### Run it by name
```ts [server/api/page-html.post.ts]
import { runBrowser } from 'vite-hub/browser'
export default defineEventHandler(async (event) => {
const input = await readBody<{ url: string }>(event)
const [error, html] = await runBrowser('page-html', input)
if (error) throw error
return html
})
```
::
The generated Browser registry infers each definition's input and result types. `runBrowser()` returns an error-first tuple.
## Runtime API
| API | Description |
| ------------------------------------------ | --------------------------------------------------------------------------------------- |
| `defineBrowser(handler)` | Defines one discovered browser operation. |
| `browser.content(input)` | Returns fully rendered HTML as text. |
| `browser.run(action, input)` | Runs a browser action and returns its standard Web `Response`. |
| `browser.open(options?)` | Opens an invocation-owned page session. The definition runtime closes it automatically. |
| `session.page.goto(url, options?)` | Navigates the session page and waits for the destination document to load. |
| `session.page.locator(selector, options?)` | Creates a locator with `click()`, `count()`, `fill()`, `inputValue()`, and `waitFor()`. |
| `session.page.press(key)` | Dispatches a keyboard key to the page. |
| `session.inspect()` | Returns the provider-neutral session identifier, state, features, and expiry. |
| `session.close()` | Releases the controller and provider session; concurrent calls share cleanup. |
| `runBrowser(name, input)` | Runs a discovered definition and returns `[error, result]` with inferred types. |
## Configuration
`browser: true` enables Cloudflare Browser Run actions. Use an object only to change the host binding or connect local development to the hosted service.
```ts [vite.config.ts]
export default defineConfig({
plugins: [
vitehub({
preset: 'cloudflare',
browser: {
binding: 'RENDER_BROWSER',
remote: true,
},
}),
],
})
```
| Shape | Description |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `browser: true` | Enables Browser Run with the `BROWSER` binding. |
| `browser: { binding?, engine?, remote? }` | Customizes the Cloudflare binding; `engine: 'chromium'` selects a persistent Chromium session, and `remote: true` connects local Wrangler development to Browser Run. |
| `browser: false` | Disables Browser Provider Output. |
The Cloudflare preset writes the Browser Run binding, a compatible default `compatibility_date`, and the `nodejs_compat` flag to Nitro's generated Provider Output.
Cloudflare's Worker `quickAction()` currently requires remote mode during local development. Set `remote: true` when local Wrangler development must call Browser Run. Both the root integration and direct `hubBrowser()` output write the Browser binding and required compatibility fields while preserving unrelated Wrangler fields.
Install `@cloudflare/playwright` and `playwright-core` when `browser.open()` uses `engine: 'chromium'`. Stateless Browser actions and the default Kitesurf session path do not require those optional peers.
## Browser actions
Use a Browser action directly when the operation does not need a persistent session:
```ts [server/render-og.ts]
import { runBrowserContent } from 'vite-hub/browser/actions'
const [error, html] = await runBrowserContent('https://example.com')
if (error) throw error
```
`runBrowserAction(action, input)` returns the raw `Response` for binary actions such as screenshots or PDFs. `runBrowserContent(input)` reads the `content` action response as text. Cloudflare's `quickAction()` name stays inside the provider adapter.
Browser Definitions can use the same path through the definition context:
```ts [server/browsers/page-html.ts]
import { defineBrowser } from 'vite-hub/browser'
export default defineBrowser(async (input: { url: string }, { browser }) => {
return await browser.content(input.url)
})
```
Use `browser.run(action, input)` for other actions. The current ViteHub action backend is Cloudflare Browser Run; the public Definition contract does not expose the provider method.
## Keep a page session open
Use `browser.open()` when one Browser Definition needs several interactions with the same page. ViteHub closes the session after the handler exits. Call `session.close()` when you can release it sooner.
```ts [server/browsers/page-title.ts]
import { defineBrowser } from 'vite-hub/browser'
export default defineBrowser(async (input: { url: string }, { browser }) => {
const session = await browser.open()
await session.page.goto(input.url)
await session.page.locator('main').waitFor()
return await session.page.locator('h1').count()
})
```
Page navigation and pointer clicks are serialized because either operation can replace the active document. Timeouts that leave page state ambiguous invalidate the page instead of allowing later operations to reuse uncertain state.
## Low-level sessions
`createBrowser()` remains available for libraries and standalone integrations that deliberately own provider selection, controller attachment, and cleanup.
Install the owner package before importing its low-level providers and
controllers:
```bash [Terminal]
pnpm add @vite-hub/browser
```
```ts [server/browser.ts]
import { createBrowser } from '@vite-hub/browser'
import { playwright } from '@vite-hub/browser/controllers/playwright'
import { cloudflareBrowser } from '@vite-hub/browser/providers/cloudflare'
const browser = createBrowser({
provider: cloudflareBrowser({ binding: 'BROWSER' }),
})
const session = await browser.open()
const control = await session.attach(playwright())
try {
await control.client.page.goto('https://example.com')
}
finally {
try {
await control.release()
}
finally {
await session.close()
}
}
```
Provider and controller subpaths are for low-level integrations. Use them when an application needs Playwright, mutable page state, downloads, or CDP instead of stateless actions. Install `@cloudflare/playwright` and `playwright-core` when using the Cloudflare Playwright controller.
`localBrowser({ executablePath })` from `@vite-hub/browser/providers/local` starts a local Chromium process for trusted-host development. It supports CDP control and live handoff, but ViteHub doesn't select it through `browser: true`. Pass it to `createBrowser()` when the application manages the browser process itself.
## Live handoff
Low-level sessions can transfer one provider session through an opaque reference tied to an audience. Cloudflare's Kitesurf default is sessionless and doesn't support live handoff. Select `engine: 'chromium'` when a handoff must preserve the session.
```ts [server/browser-handoff.ts]
import { cdp } from '@vite-hub/browser/controllers/cdp'
const session = await browser.open()
const control = await session.attach(cdp())
try {
await control.client.send('Target.createTarget', {
url: 'https://example.com',
})
}
finally {
await control.release()
}
const ref = await session.handoff({
audience: 'review-agent-run-42',
mode: 'live',
})
```
Refs are one-time, short-lived, and scoped to the Browser Client that created them. Use the CDP controller when live preservation matters; Playwright attachment is lifecycle-scoped and cannot be handed off after release.
## Production checks
Run browser automation only from trusted server code. Browser sessions can observe authenticated pages, cookies, screenshots, network responses, and rendered private UI.
Do not log provider session ids, CDP endpoints, cookies, authorization headers, or raw handoff refs. Treat screenshots and downloaded files as user data and route them through the same storage, retention, and approval policies as other artifacts.
## Next steps
- Store screenshots and downloaded files with [Blob](https://vitehub.dev/docs/server-primitives/blob).
- Expose model-facing browser access through [Browser capability](https://vitehub.dev/docs/capabilities/browser).
- Deploy Browser Run output on [Cloudflare](https://vitehub.dev/docs/frameworks-hosts/cloudflare).
# Database
Use Database when your app needs relational schemas, constraints, joins, migrations, or queryable state. Define the schema with Drizzle, then query it through generated ViteHub imports.
Use [KV](https://vitehub.dev/docs/server-primitives/kv) for small values addressed by key, [Blob](https://vitehub.dev/docs/server-primitives/blob) for object storage, and [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for file-tree state.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/database drizzle-orm
pnpm add -D @vite-hub/cli drizzle-kit
```
### Configure
```ts [vite.config.ts]
import { hubDb } from '@vite-hub/database/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubDb()],
})
```
### Start using it
```ts [src/database.ts]
import { defineDatabase } from '@vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export default defineDatabase({
schema: {
notes: sqliteTable('notes', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
}),
},
})
```
::
## Public imports
| Import | Use |
| ----------------------------------------------- | ------------------------------------------------------------------- |
| `defineDatabase` from `@vite-hub/database` | Declare a Database Definition. |
| `useDatabase` from `@vite-hub/database/drizzle` | Select a generated Drizzle database and its schema by name. |
| `hubDb` from `@vite-hub/database/vite` | Register database discovery, generated schema, and Provider Output. |
| `@vite-hub/database/config` | Resolve database config values and discovery config. |
| `@vite-hub/database/cli` | Use package-owned database CLI contribution. |
| `@vite-hub/database/nuxt` | Use the narrow Nuxt D1 host-resource bridge. |
All Database Definition, integration, connection, Cloudflare D1, Drizzle, and runtime config types are exported from `@vite-hub/database`.
## Configure the Vite Integration
```ts [vite.config.ts]
import { hubDb } from '@vite-hub/database/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubDb()],
})
```
The Vite config key is `database`.
| Option | Type | Default | Description |
| ---------------------------- | ------------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `database` | `boolean` or `DBModulePublicOptions` | disabled | Enables database discovery and generated runtime imports through `vitehub()` with `true` or an options object; `false` leaves it disabled. |
| `database.projectRoot` | `string` | effective application root | Sets the root used for Database Definition discovery, generated artifacts, and provisioning. Relative paths resolve from the Vite root in a Vite app and from the Nuxt `rootDir` in a Nuxt app. |
| `database.cli.generate` | `false` | enabled | Disables package-owned schema generation CLI contribution. |
| `database.cli.migrate` | `false` | enabled | Disables package-owned migration CLI contribution. |
| `database.connection` | `DatabaseConnectionConfig` | local SQLite | Supplies a hosted libSQL connection for Database Definitions that do not declare one. Definition connection values override matching integration values. |
| `database.driver` | `DatabaseRuntimeD1Options['driver']` | none | Selects Cloudflare D1 runtime output when configured at integration level. Value: `d1`. |
| `database.binding` | `string` | `DB` or `DB_` | Cloudflare D1 binding for integration-level runtime output. |
| `database.databaseId` | `DatabaseConfigValue` | Provision State | Cloudflare D1 database id. |
| `database.previewDatabaseId` | `DatabaseConfigValue` | none | Cloudflare D1 preview database id. |
| `database.databaseName` | `DatabaseConfigValue` | none | Cloudflare D1 database name. |
| `database.migrationsTable` | `string` | provider default | Cloudflare D1 migrations table. |
## Define a database
Database Definitions keep the Database Table Schema next to the server code that uses it.
```ts [src/database.ts]
import { defineDatabase } from '@vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export default defineDatabase({
schema: {
notes: sqliteTable('notes', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
body: text('body').notNull(),
}),
},
})
```
A project uses either one Default Database or a set of Named Databases. Do not mix both modes in one app.
## Database Definition options
`defineDatabase()` accepts one object.
| Option | Type | Required | Description |
| ------------------------------ | ------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | `string` | Named Databases only | Runtime identity. Must match the discovered file or directory name. |
| `schema` | `Record` | Yes | Database Table Schema source of truth. |
| `connection.url` | `DatabaseConfigValue` | No | SQLite/libSQL connection URL. Defaults to `.vitehub/data/database/sqlite.db` for a Default Database. |
| `connection.authToken` | `DatabaseConfigValue` | No | Hosted database auth token. |
| `cloudflare.binding` | `string` | No | D1 binding. Defaults to `DB` for Default Database and `DB_` for Named Databases. |
| `cloudflare.databaseId` | `DatabaseConfigValue` | No | D1 database id. |
| `cloudflare.http` | `true | { url, authToken }` | No | Explicitly selects authenticated D1 raw HTTP access for local and hosted runtimes. `true` uses Cloudflare's API; an object selects a compatible proxy. |
| `cloudflare.previewDatabaseId` | `DatabaseConfigValue` | No | D1 preview database id. |
| `cloudflare.databaseName` | `DatabaseConfigValue` | No | D1 database name. |
| `cloudflare.migrationsTable` | `string` | No | D1 migrations table. |
| `drizzle.casing` | `DrizzleCasing` | No | Drizzle casing option. Values: `snake_case`, `camelCase`. |
ViteHub currently exposes `sqlite` as the public `DatabaseDialect`.
## Generate and apply migrations
The Database integration adds migration commands to the ViteHub CLI. `vite-hub` includes the CLI; direct package installations need `@vite-hub/cli` as shown in the quick start. Run the commands from the project root:
```bash [Terminal]
pnpm vitehub db generate
pnpm vitehub db migrate
```
`db generate` refreshes the generated Drizzle config and creates migrations from your Database Definitions. Pass `--name ` to name a migration or `--custom` to create an empty migration. `db migrate` refreshes the config and applies pending migrations.
## Use it at runtime
Call `useDatabase()` from server code with the discovered database name. Use `default` for a Default Database.
```ts [server/api/notes.get.ts]
import { useDatabase } from '@vite-hub/database/drizzle'
export default defineEventHandler(() => {
const { db, schema } = useDatabase('default')
return db.select().from(schema.notes)
})
```
The `@vite-hub/database/drizzle` runtime import is resolved by the ViteHub Vite Integration for server code and provider output. Do not run files that import it directly with plain `node`; run them through your Vite-built server path or provider output.
Use Named Databases when the app needs more than one independent database.
```ts [src/analytics.database.ts]
import { defineDatabase } from '@vite-hub/database'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
const events = sqliteTable('events', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
})
export default defineDatabase({
name: 'analytics',
schema: { events },
})
```
Name a database for a real data or deployment split, not for a source-code folder.
```ts [server/api/events.get.ts]
import { useDatabase } from '@vite-hub/database/drizzle'
export default defineEventHandler(() => {
const { db, schema } = useDatabase('analytics')
return db.select().from(schema.events)
})
```
## Providers
| Provider/runtime | Configure with | Nuance |
| ------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Local SQLite | `connection.url` or no connection config | Default for local development and generated Drizzle artifacts. |
| Hosted SQLite/libSQL-style connection | `connection.url` and optional `connection.authToken` | Keep URLs and tokens in Server Env when they are secrets. |
| Cloudflare D1 | `cloudflare` Definition options or integration-level `database.driver: 'd1'` | Uses a D1 binding on Cloudflare. Local development and hosted Vercel output use D1 only when `cloudflare.http` is selected explicitly. |
### Use Cloudflare D1 over HTTP
A Database Definition can use the same D1 database during local development and from hosted providers. Cloudflare output prefers the configured binding. Set `cloudflare.http: true` to call Cloudflare's D1 raw API with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` from Server Env.
```ts [server/databases/config.ts]
import { defineDatabase } from '@vite-hub/database'
import { notes } from './schema'
export default defineDatabase({
cloudflare: {
databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID,
databaseName: process.env.CLOUDFLARE_D1_DATABASE_NAME,
http: true,
},
schema: { notes },
})
```
Use `cloudflare.http: { url, authToken }` to send the same raw query wire format to an authenticated HTTP(S) proxy instead. Both values are required at runtime, and proxy authentication never falls back to `CLOUDFLARE_API_TOKEN`.
```ts [server/databases/config.ts]
import { defineDatabase } from '@vite-hub/database'
import { notes } from './schema'
export default defineDatabase({
cloudflare: {
databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID,
http: {
authToken: process.env.D1_HTTP_TOKEN,
url: process.env.D1_HTTP_URL,
},
},
schema: { notes },
})
```
Selecting D1 HTTP also generates Drizzle Kit's `d1-http` credentials. Migration and inspection commands use Cloudflare's API with `CLOUDFLARE_ACCOUNT_ID`, the database id, and `CLOUDFLARE_API_TOKEN`; those credentials are never embedded in generated output.
::warning
Cloudflare describes its built-in D1 REST API as best suited to administrative use because the global Cloudflare API rate limit applies. For sustained application traffic, use a narrowly authenticated proxy Worker and validate which queries or tables it may access. See Cloudflare's
[D1 proxy Worker guide](https://developers.cloudflare.com/d1/tutorials/build-an-api-to-access-d1/){rel=""nofollow""}
.
::
Omitting `cloudflare.http` preserves local SQLite and the existing hosted libSQL selection even when `cloudflare.databaseId` is present.
### Select a hosted database for Vercel
Keep the Database Definition limited to tables, then select its hosted libSQL connection in the Vite Integration. Runtime Env declarations preserve the Vercel Marketplace environment variable lookup in generated output instead of embedding credentials at build time.
```ts [vite.config.ts]
import { hubDb } from '@vite-hub/database/vite'
import { env, hubEnv } from '@vite-hub/env/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubEnv(),
hubDb({
connection: {
url: env({ source: env.source('TURSO_DATABASE_URL') }),
authToken: env({ secret: true, source: env.source('TURSO_AUTH_TOKEN') }),
},
}),
],
})
```
This connection is a deployment default. A Database Definition can still declare a different `connection.url` or `connection.authToken`; those values take precedence for that database.
## Provider output
The Database Package discovers Database Definitions, generates the Drizzle Runtime Surface, produces generated schema artifacts, and wires provider-specific output. Provider bindings are integration details; the public database identity is the Default Database or Named Database.
Put Cloudflare D1 bindings, hosted libSQL URLs, and Nuxt host resources in Database configuration or host setup. Route code keeps using the generated Drizzle imports.
Cloudflare Nuxt output copies discovered D1 migration SQL beside the generated Wrangler config, so `.output/server` can apply migrations without the source checkout.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@vite-hub/database/nuxt'],
database: {
driver: 'd1',
databaseName: 'app-content',
},
})
```
Run `vitehub provision run --provider cloudflare` to resolve the D1 id into `.vitehub/provision.json`. Cloudflare production builds fail before deployment when provision state, `database.databaseId`, and an existing complete matching Nitro Wrangler binding all fail to supply the id.
::note
`@vite-hub/database/nuxt`
is a narrow Nuxt lifecycle bridge for one D1 Database Host Resource, mainly to keep Nuxt Content and Cloudflare
`wrangler.d1_databases`
in sync. Discovered Database Definitions still own the Drizzle Runtime Surface.
::
## Connect it to Agents
Direct Database access is for server code. To let a model inspect schema or run guarded statements, attach the Database Capability.
The Database Capability is not a raw Drizzle client proxy. It uses agent-facing guardrails such as schema mode, data mode, write approvals, and the single-statement SQL guardrail. Read [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) before exposing database access to an Agent.
## Production checks
Database Table Schema is the schema in code. The live schema can differ when migrations haven't run or when an Agent has schema write permission.
Keep provider credentials in Server Env. Direct D1 HTTP access sends `CLOUDFLARE_API_TOKEN` only as the Cloudflare Bearer credential; proxy access sends only its configured `cloudflare.http.authToken`. Keep migrations, backup behavior, and hosted database lifecycle in deployment workflows instead of hiding them in route code.
## Next steps
- Store small key values with [KV](https://vitehub.dev/docs/server-primitives/kv).
- Store file-shaped objects with [Blob](https://vitehub.dev/docs/server-primitives/blob).
- Learn shared discovery rules in [Definitions and discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery).
# Email
Use Email to send transactional messages from server code through any Unemail driver. ViteHub configures the driver, normalizes delivery errors, renders trusted Markdown templates, and provides an in-memory test client.
## Before you begin
The quick start takes about ten minutes and sends a real message through Resend. You need:
- Node.js 24.15 or later and an existing Vite 8 or later server application.
- pnpm and a POSIX-compatible shell for the commands below.
- A Resend API key and a sender address accepted by Resend.
- A real recipient address you can check.
## Send your first message
::steps{level="3"}
### Install the email dependencies
```bash [Terminal]
pnpm add vite-hub
```
`vite-hub` includes Email runtime support and composes providers from Unemail.
### Configure Resend
```ts [vite.config.ts]
import { vitehub } from 'vite-hub'
import { env } from 'vite-hub/env'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vitehub({
preset: 'node',
email: {
driver: 'unemail/driver/resend',
options: {
apiKey: env({ secret: true, source: env.source('RESEND_API_KEY') }),
},
},
})],
})
```
The driver subpath selects the upstream Unemail provider. The Env declaration is serialized, but its source value is resolved in the server runtime for every send, so the API key stays out of build output and request-scoped Cloudflare secrets stay current. Literal options and non-secret Env defaults are serialized into the build; never use literal options for credentials, and ViteHub rejects defaults on declarations marked secret.
### Provide the Resend secret
Set `RESEND_API_KEY` in the server process:
```bash [Terminal]
export RESEND_API_KEY='re_...'
```
Use your deployment platform's secret store in production. Do not use a `VITE_` prefix because Vite-prefixed values can be exposed to browser code.
### Send from server code
Replace both addresses with values accepted by Resend. The request performs a real delivery.
```ts [server/api/welcome.post.ts]
import { defineEventHandler } from 'h3'
import { email } from 'vite-hub/email/server'
export default defineEventHandler(async () => {
return await email.send({
from: 'verified-sender@example.com',
to: 'you@example.com',
subject: 'Welcome',
text: 'Welcome to ViteHub.',
})
})
```
### Verify the result
Start the application with its normal development command and send a `POST` request to `/api/welcome`. A successful response has this shape:
```json
{
"id": "",
"driver": "resend"
}
```
Resend supplies `id`. Confirm delivery in the recipient inbox or the provider's delivery log; an accepted message ID does not guarantee final inbox placement.
::
## Choose the client surface
| Surface | Use it when |
| ------------------------- | --------------------------------------------------------------------------------------------- |
| `email.send(message)` | A Vite app configures one Unemail provider through `vitehub({ email: { driver, options } })`. |
| `createEmail({ driver })` | Low-level integrations that do not use Vite create and own a client explicitly. |
| `createTestEmail()` | A test needs deterministic in-memory capture without delivery. |
## Public imports
Use the `vite-hub` paths for framework APIs. Select providers through an `unemail/driver/*` subpath string in Vite config.
| Import | Runtime values | Public types |
| ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vite-hub` | `vitehub` | Framework Vite Integration options. |
| `vite-hub/email` | `createEmail` | `EmailAddress`, `EmailAddressList`, `EmailAttachment`, `EmailMessage`, `EmailDriver`, `EmailDriverFactory`, `EmailDriverSource`, `EmailDefinition`, `EmailClient`, `EmailSendResult`, `EmailErrorCode` |
| `vite-hub/runtime` | `ViteHubError`, `getViteHubErrorShape` | Shared operational error contract. |
| `vite-hub/email/server` | `email` | None |
| `vite-hub/email/markdown` | `renderEmailMarkdown` | `RenderEmailMarkdownOptions`, `RenderedEmailMarkdown` |
| `unemail/driver/*` | Provider drivers | Provider options and capabilities are owned by Unemail. |
| `@vite-hub/email/test` | `createTestEmail`, `createMemoryEmailDriver` | `TestEmailClient`, `MemoryEmailDriver` |
| `@vite-hub/email/vite` | `hubEmail` | `EmailVitePluginOptions`, `EmailVitePlugin`, `EmailVitePluginAPI` |
The direct `@vite-hub/email`, `@vite-hub/email/server`, and `@vite-hub/email/markdown` paths remain stable for focused libraries and applications that install the owner package without the framework distribution.
## Message contract
`email.send()` and explicit clients accept Unemail's `EmailMessage`.
| Field | Type | Required | Behavior |
| ---------------------- | ---------------------------- | -------- | ------------------------------------------------------------------- |
| `from` | `EmailAddress` | Yes | One sender as a string or `{ email, name? }`. |
| `to` | `EmailAddressList` | Yes | One address or a non-empty address array. |
| `cc`, `bcc`, `replyTo` | `EmailAddressList` | No | Optional portable recipient fields. |
| `subject` | `string` | Yes | The message subject. |
| `html`, `text` | `string` | No | Body alternatives supported by the active driver. |
| `headers` | `Record` | No | Custom headers supported by the active driver. |
| `attachments` | `readonly EmailAttachment[]` | No | In-memory string or `Uint8Array` content with a non-empty filename. |
`EmailAttachment` also accepts optional `contentType`, `cid`, and `disposition: 'attachment' | 'inline'`. `EmailMessage` includes Unemail's scheduling, provider-template, tagging, tracking, unsubscribe, sandbox, metadata, and personalization fields.
The active Unemail driver owns field support, validation, address rules, message limits, and sender authorization. Check its `flags` and provider documentation before using optional fields.
Every successful send returns `Promise`:
| Field | Type | Meaning |
| -------- | -------- | ------------------------------------------------ |
| `id` | `string` | The non-empty message ID returned by the driver. |
| `driver` | `string` | The Unemail driver that accepted the message. |
## Compose dynamic Markdown
`renderEmailMarkdown()` first composes the template through `@vite-hub/markdown-template`, then parses the composed Markdown with Comark and renders HTML.
```ts [server/welcome.ts]
import { renderEmailMarkdown } from 'vite-hub/email/markdown'
import { email } from 'vite-hub/email/server'
export async function sendWelcome(name: string, to: string) {
const body = await renderEmailMarkdown([
'# Welcome {{ user.name }}',
'',
'Your **ViteHub** workspace is ready.',
'',
'::if{user.trial}',
'Your trial is active.',
'::',
].join('\n'), {
data: { user: { name, trial: true } },
})
return await email.send({
...body,
from: 'verified-sender@example.com',
to,
subject: 'Your workspace is ready',
})
}
```
`html` contains rendered HTML. `text` contains the fully composed Markdown, which is readable in text clients but can retain Markdown markers such as `**`. Supply your own `text` when the application requires marker-free plain text.
### Discover application email templates
Put reusable templates under `server/emails`. ViteHub discovers Markdown files
recursively and creates a typed `#vitehub/emails/` import from each path.
```md [server/emails/welcome.md]
# Welcome {{ user.name }}
Your workspace is ready.
```
```ts [server/welcome.ts]
import renderWelcome from '#vitehub/emails/welcome'
import { renderEmailMarkdown } from 'vite-hub/email/markdown'
import { email } from 'vite-hub/email/server'
export async function sendWelcome(name: string, to: string) {
const markdown = await renderWelcome({ user: { name } })
const body = await renderEmailMarkdown(markdown)
return await email.send({
...body,
from: 'verified-sender@example.com',
to,
subject: 'Your workspace is ready',
})
}
```
`server/emails/monthly/recap.md` becomes
`#vitehub/emails/monthly/recap`. ViteHub generates exact module declarations in
`.vitehub/types/email.d.ts` and bundles the templates for provider builds under
`.vitehub/email/templates`.
| Option | Type | Default | Use |
| ---------------- | --------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | `Record` | `{}` | Supplies scalar bindings, Markdown fragments, and conditional values. |
| `resolveImport` | `RenderEmailMarkdownOptions['resolveImport']` | None | Resolves relative imports synchronously or asynchronously. Return `{ id, template }`, or `undefined` when an import is unavailable. No files or URLs are read without it. |
| `sourceId` | `string` | `''` | Identifies the root template to the import resolver and cycle detector. |
| `maxImportDepth` | `number` | `4` | Limits nested imports. It must be a non-negative integer. |
::warning
`renderEmailMarkdown()`
does not sanitize authored HTML, trusted Markdown fragments, or imported templates, and it does not inline email CSS. Use scalar
`{{ value }}`
bindings for untrusted text. Sanitize any untrusted content before intentionally passing it through a
`{{{ fragment }}}`
binding or an imported template.
::
## Provider behavior for Resend
Use the quick-start `vitehub({ email: { driver: 'unemail/driver/resend', options } })` configuration. A successful result has `driver: 'resend'`. ViteHub maps Unemail errors to `EMAIL_*` codes and keeps the original error in `cause`.
## Configure another provider
Set `email.driver` to another `unemail/driver/*` subpath and declare its serializable options in the same Vite config. Keep credentials in Server Env or the deployment platform's secret store, and pass them as Env declarations without defaults. When Unemail does not support a provider, add the driver upstream with its `defineDriver()` contract so every consumer benefits instead of adding a ViteHub-only adapter.
## Test without delivery
The framework distribution does not re-export test utilities. Install the Email
owner package as a development dependency when tests use its in-memory client.
```bash [Terminal]
pnpm add -D @vite-hub/email
```
```ts [welcome.test.ts]
import { expect, it } from 'vitest'
import { createTestEmail } from '@vite-hub/email/test'
it('sends the welcome message', async () => {
const mail = createTestEmail()
await expect(mail.send({
from: 'hello@example.com',
to: 'you@example.com',
subject: 'Welcome',
text: 'Hello',
})).resolves.toEqual({ driver: 'memory', id: 'memory-1' })
expect(mail.messages[0]?.subject).toBe('Welcome')
})
```
Each test client owns an isolated mailbox. Captured messages are cloned before storage, delivery order is stable, and `clear()` empties the mailbox and resets the next ID to `memory-1`.
Use `createMemoryEmailDriver()` when another client or test harness needs to manage the in-memory driver directly.
## Handle delivery errors
Use the `EMAIL_*` code for control flow and `details.driver` to identify the failing adapter. ViteHub keeps the original Unemail error in `cause` while exposing a safe public message.
```ts [server/send.ts]
import { getViteHubErrorShape } from 'vite-hub/runtime'
import { type EmailMessage } from 'vite-hub/email'
import { email } from 'vite-hub/email/server'
export async function send(message: EmailMessage) {
try {
return await email.send(message)
}
catch (error) {
const shape = getViteHubErrorShape(error)
if (shape?.code.startsWith('EMAIL_')) {
console.error('Email delivery failed', {
code: shape.code,
driver: shape.details?.driver,
})
}
throw error
}
}
```
Inspect `cause` only in protected server-side diagnostics because provider errors can contain addresses, credentials, response text, or infrastructure details.
| Code | Produced by | Meaning | Retry guidance |
| ----------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
| `EMAIL_NOT_CONFIGURED` | Runtime configuration or Unemail `INVALID_OPTIONS` | The provider or required driver options are missing. | Fix configuration; do not retry unchanged. |
| `EMAIL_AUTHENTICATION` | Unemail `AUTH` | Delivery credentials were rejected. | Fix credentials before retrying. |
| `EMAIL_RATE_LIMITED` | Unemail `RATE_LIMIT` | The provider reported throttling. | Apply an application-owned retry policy. |
| `EMAIL_TIMEOUT` | Unemail `TIMEOUT` | Delivery did not complete before the transport timeout. | Treat the outcome as uncertain before retrying. |
| `EMAIL_NETWORK` | Unemail `NETWORK` | The driver could not reach its provider. | Treat the outcome as uncertain before retrying. |
| `EMAIL_PROVIDER_FAILED` | Unemail `PROVIDER`, `UNSUPPORTED`, or `CANCELLED`; invalid success results | Delivery failed outside a more specific category. | Inspect protected diagnostics and provider delivery logs. |
Provider-specific classification and retry metadata come from Unemail. ViteHub only maps its stable generic codes and validates that a successful result includes a non-empty message ID.
The package does not retry automatically. A timeout or disconnected response can occur after a provider accepted the message, so a blind retry can send a duplicate. Put retry and idempotency policy in Queue, Workflow, or the provider adapter that has enough information to make that decision.
## Configure Vite
The ViteHub preset keeps Email opt-in:
```ts [vite.config.ts]
import { vitehub } from 'vite-hub'
import { env } from 'vite-hub/env'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vitehub({
preset: 'node',
email: {
driver: 'unemail/driver/resend',
options: { apiKey: env({ secret: true, source: env.source('RESEND_API_KEY') }) },
},
})],
})
```
| Option | Type | Default | Behavior |
| --------- | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `driver` | `` `unemail/driver/${string}` `` | Required | Selects one upstream Unemail driver through its exact package subpath. |
| `options` | `EnvRuntimeConfigOptions` | `{}` | Supplies serializable non-secret literals and runtime Env declarations. Env source values resolve in the server runtime for every send; literals and non-secret defaults are included in build output, while defaults on secret declarations are rejected. |
The integration serializes the driver subpath, literal options, and Env declarations into a server-only generated module. Credential values remain in the runtime environment when they are supplied through an Env source without a default.
## Troubleshoot common failures
### `No Email provider is configured`
Configure `vitehub({ email: { driver, options } })`. Applications using the owner integration directly configure `hubEmail({ driver, options })`. Restart the development server, then call `email.send()` again.
### `Email delivery failed through .`
Read the `EMAIL_*` code first. For `EMAIL_AUTHENTICATION`, verify the provider credentials and sender authorization. For `EMAIL_NETWORK` or `EMAIL_TIMEOUT`, verify DNS and outbound connectivity from the deployed server. Inspect `cause` and provider logs only on the server.
## Requirements
- `vite-hub/email` requires Node.js 24.15 or later. The direct `@vite-hub/email` package requires Node.js 24 or later.
- Email provider configuration requires Vite 8 or later; explicit `createEmail()` clients do not require Vite.
- Provider runtime support comes from the selected Unemail driver.
ViteHub does not independently certify provider behavior. ViteHub owns provider composition, runtime delivery, normalized errors, dynamic Markdown composition, and test capture. Unemail owns provider drivers, message features, and transport behavior; Queue, Workflow, and Schedule remain the orchestration layer.
## Expose Email to an Agent
Use the official [`email()` Capability](https://vitehub.dev/docs/capabilities/email) when a model needs to send through the configured Email provider.
The Capability fixes the sender in application code and exposes one plain-text `email_send` tool with optional policy; provider configuration and credentials stay in this primitive.
Dynamic Markdown remains an application composition boundary.
The official Capability doesn't render model-authored Markdown or expose HTML, headers, and attachments. Use a trusted application template or a narrowly scoped Custom Capability for richer messages.
## Next steps
- Use [Queue](https://vitehub.dev/docs/server-primitives/queue) when a request must return before email delivery completes.
- Use [Schedule](https://vitehub.dev/docs/server-primitives/schedule) or [Workflows](https://vitehub.dev/docs/server-primitives/workflows) for recurring or durable delivery orchestration.
- Use [Env](https://vitehub.dev/docs/server-primitives/env) for typed server credentials.
- Check [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) for the shared error contract.
# Env
Use Env to declare browser-safe values, build replacements, server-only values, and secrets without mixing their access rules. ViteHub generates typed imports for browser and server code and redacts Secret Env values by default.
Your host still stores and supplies secrets. Server code calls `unseal()` only where it needs the raw value.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/env @vite-hub/runtime
```
### Configure
```ts [vite.config.ts]
import { env, hubEnv } from '@vite-hub/env/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubEnv()],
env: {
public: {
appName: env({ default: 'Acme' }),
},
},
})
```
### Start using it
```ts [src/app.ts]
import { usePublicEnv } from '#vitehub/env/public'
const publicEnv = usePublicEnv()
console.log(publicEnv.appName)
```
::
## Public imports
| Import | Use |
| ----------------------------------------------------------------- | --------------------------------------------------- |
| `env` from `@vite-hub/env` or `@vite-hub/env/vite` | Declare Env values and Env Sources. |
| `getViteHubErrorShape` from `@vite-hub/runtime` | Inspect operational Env failures by `ENV_*` code. |
| `hubEnv` from `@vite-hub/env/vite` | Register the Vite Integration. |
| `usePublicEnv` from `#vitehub/env/public` | Read generated Public Env from browser-safe code. |
| `useServerEnv` from `#vitehub/env/server` | Read generated Server Env from server code. |
| `SecretEnv` from `@vite-hub/env` or `@vite-hub/env/secret` | Represent Secret Env values that redact by default. |
| `resolveServerEnv` from `@vite-hub/env` or `@vite-hub/env/server` | Resolve a server env registry manually. |
| `openWorkflowEnv` from `@vite-hub/env` or `@vite-hub/env/presets` | Use the OpenWorkflow env preset. |
| `parseSchema` from `@vite-hub/env` or `@vite-hub/env/schema` | Parse Standard Schema-compatible values. |
## Configure Env
Add `hubEnv()` and declare values in the Vite config. `env.public` becomes browser-safe Public Env, `env.define` becomes Vite replacements, and `env.server` becomes Server Env for server runtime code.
```ts [vite.config.ts]
import { env, hubEnv } from '@vite-hub/env/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubEnv()],
env: {
public: {
appName: env({ default: 'Acme' }),
},
define: {
__BUILD_TARGET__: env({ default: 'preview' }),
},
server: {
github: {
token: env({ secret: true, source: env.source('GITHUB_TOKEN') }),
},
},
},
})
```
## Integration options
Pass Integration Options to `hubEnv()`.
| Option | Type | Default | Description |
| ----------------------- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `diagnostics` | `EnvDiagnostics` | Package default | Controls Env diagnostic output during Vite config/dev/build. Values: `off`, `summary`, `trace`. |
| `prefix` | `string` | None | Prefixes env variable lookup names. |
| `projectRoot` | `string` | ViteHub project root | Resolves generated files and package import updates from a custom project root. |
| `runtimeImports.secret` | `string` | `@vite-hub/env/secret` | Replaces the type import used for `SecretEnv` in generated Server Env modules. Framework integrations can point generated code at their runtime-owned entry point. |
| `runtimeImports.server` | `string` | `@vite-hub/env/server` | Replaces the `resolveServerEnv` import used by generated Server Env modules. Framework integrations can point generated code at their runtime-owned entry point. |
## Env config sections
| Section | Runtime | Public | Use |
| ------------ | --------------- | ------------------- | ------------------------------------------------------ |
| `env.public` | Build | Yes | Browser-safe Public Env through `#vitehub/env/public`. |
| `env.define` | Build transform | Yes in bundled code | Vite compile-time replacements. |
| `env.server` | Server runtime | No | Server Env through `#vitehub/env/server`. |
## Env Declaration options
`env()` and `env.variable()` accept the same options.
| Option | Type | Default | Description |
| ---------- | ---------------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
| `source` | `EnvSource` or `EnvSourceResolver` | Section key lookup | Selects where the value comes from. |
| `default` | `unknown` | None | Value used when the source is absent. |
| `required` | `boolean` | `true` unless `optional` is set | Throws when a runtime value is missing. |
| `optional` | `boolean` | `false` | Sets `required` to `false`. Cannot be combined with `required`. |
| `mode` | `EnvMode` | `runtime` | Marks the value as Build Env or Runtime Env. Values: `build`, `runtime`. |
| `schema` | Standard Schema-compatible parser | string parser | Validates and parses the value. |
| `secret` | `boolean` | `false` | Wraps runtime values in `SecretEnv`. |
| `type` | `string` | Inferred | Overrides the generated type label. |
## Env sources
| Source helper | Description |
| ------------------------------------- | --------------------------------------------------- |
| `env.source('NAME')` | Reads one host env variable. |
| `env.source(['PRIMARY', 'FALLBACK'])` | Reads the first available env variable from a list. |
| `env.custom(label, resolver)` | Resolves from a custom callback. |
| `env.gitBranch()` | Reads the current Git branch. |
| `env.gitCommit({ short })` | Reads the current Git commit. |
| `env.gitRef()` | Reads the current Git ref. |
| `env.gitSha({ short })` | Reads the current Git SHA. |
| `env.gitTag()` | Reads the current Git tag. |
| `env.buildTimestamp()` | Reads the build timestamp. |
| `env.packageJson(path)` | Reads a value from `package.json`. |
## Use it at runtime
Use Public Env from browser-safe code. The import path stays stable even though ViteHub generates the backing module.
```ts [src/config.ts]
import { usePublicEnv } from '#vitehub/env/public'
export const appName = usePublicEnv().appName
```
Use Server Env from server-only code. Secret Env values redact by default and require `unseal()` before a third-party SDK or request can receive the underlying string.
```ts [server/github.ts]
import { useServerEnv } from '#vitehub/env/server'
export async function listIssues() {
const { github } = useServerEnv()
return fetch('https://api.github.com/issues', {
headers: {
authorization: `Bearer ${github.token.unseal()}`,
},
})
}
```
## Structured errors
Env resolution failures use `ViteHubError` with closed, stable codes and JSON-safe context. The codes distinguish invalid declarations, missing required values, invalid runtime values, and failed built-in sources. Custom source resolvers keep application-owned errors unchanged.
```ts
import { getViteHubErrorShape } from '@vite-hub/runtime'
try {
await resolveEnv()
}
catch (error) {
const shape = getViteHubErrorShape(error)
if (shape?.code === 'ENV_SOURCE_FAILED') {
console.error('Env source failed', shape.details?.source)
}
throw error
}
```
Each code owns a fixed public message and bounded details. Source details use identifiers such as `git:branch`, `package.json`, `env`, or `custom`; raw variable names, package paths, labels, and provider diagnostics remain behind `cause`. `error.toJSON()` includes `code`, `message`, and `details`; it omits `cause`, which remains available only on the in-memory error. Invalid calls to declaration helpers remain `TypeError`, while `parseSchema()` continues to throw ordinary schema errors.
## Provider output
`hubEnv()` writes generated env modules under `.vitehub/env/` and ambient types under `.vitehub/types/`. Import `#vitehub/env/public` and `#vitehub/env/server` from application code, not generated file paths or integration virtual modules.
Add the generated type directory to `tsconfig.json` when the app wants field-level types for generated Env access.
```json [tsconfig.json]
{
"include": [
"src/**/*.ts",
"server/**/*.ts",
".vitehub/types/**/*.d.ts"
]
}
```
## Use Env with Agents
Read application secrets through Server Env inside Agent and Capability callbacks. Don't pass secrets through Agent Invocation metadata or model-facing instructions.
Env is usually not an agent-facing Capability. Other Capabilities consume Server Env when they need credentials, provider tokens, or app-owned configuration.
## Production checks
Public Env and Vite define values are visible to built client code. Put secrets only in Server Env with `secret: true`.
Secret Env provides type friction and default redaction, but it is not a complete leak-prevention system. Unseal secrets as late as possible and avoid returning them in responses, logs, traces, or Agent output.
## Next steps
- Learn the server primitive model in [Server primitives for any host](https://vitehub.dev/docs/concepts/server-primitives-for-any-host).
- Use Env with [Auth](https://vitehub.dev/docs/server-primitives/auth) when Auth runtime options need secrets.
- Expose agent abilities through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) without making secrets model-facing.
# Server primitives
## Server primitives for Vite apps and any host
ViteHub adds storage, queues, schedules, email, and other server APIs to Vite apps. Call them from routes, handlers, jobs, or workers. You don't need an Agent Definition.
Start with [Your first server primitive](https://vitehub.dev/docs/getting-started/first-server-primitive) for a runnable example. Return to Concepts when you need to understand generated imports or host configuration. Read the Agents section only when a model needs access to one of these APIs.
::u-page-grid{.not-prose.mt-8}
:::u-page-card
---
description: Add KV to an app, register the Vite Integration, and call the
Runtime Helper from server code.
icon: i-lucide-rocket
title: First primitive
to: https://vitehub.dev/docs/getting-started/first-server-primitive
---
:::
:::u-page-card
---
description: Learn how generated imports and host configuration keep application
code independent from providers.
icon: i-lucide-map
title: Server model
to: https://vitehub.dev/docs/concepts/server-primitives-for-any-host
---
:::
:::u-page-card
---
description: Call primitives through ViteHub-owned imports instead of generated
files or provider SDK wiring.
icon: i-lucide-code-2
title: Runtime imports
to: https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports
---
:::
:::u-page-card
---
description: Give an Agent selected access to server APIs through Capabilities.
icon: i-lucide-bot
title: Agents
to: https://vitehub.dev/docs/agents
---
:::
::
::note
Server code calls runtime helpers directly. Agents receive only the abilities added through Capabilities. Read
[Runtime helpers and stable imports](https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports)
and
[Capabilities API](https://vitehub.dev/docs/concepts/capabilities-api)
when you need those contracts.
::
## Pick the right primitive
| You need | Start with |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Public, server, build-time, runtime, or secret environment values | [Env](https://vitehub.dev/docs/server-primitives/env) |
| Application users, sessions, Better Auth routing, or guarded app routes | [Auth](https://vitehub.dev/docs/server-primitives/auth) |
| Request budgets that must be consumed before expensive server work starts | [Rate Limit](https://vitehub.dev/docs/server-primitives/rate-limit) |
| Outbound transactional messages with provider-neutral delivery | [Email](https://vitehub.dev/docs/server-primitives/email) |
| Small key-addressed values, settings, flags, cursors, or lightweight state | [KV](https://vitehub.dev/docs/server-primitives/kv) |
| Relational data, constraints, joins, migrations, or queryable history | [Database](https://vitehub.dev/docs/server-primitives/database) |
| Uploads, generated artifacts, binary files, or object metadata | [Blob](https://vitehub.dev/docs/server-primitives/blob) |
| Provider-backed browser sessions, screenshots, DOM inspection, or live handoff | [Browser](https://vitehub.dev/docs/server-primitives/browser) |
| Persistent file-tree state, snapshots, diffs, rules, or sessions | [Workspace](https://vitehub.dev/docs/server-primitives/workspace) |
| Collaborative Markdown editing, presence, and Workspace checkpoints | [Realtime](https://vitehub.dev/docs/reference/realtime) |
| Read-only retrieval from files, globs, GitHub, markdown, MCP, or custom loaders | [Source](https://vitehub.dev/docs/server-primitives/source) |
| Background delivery that returns before work finishes | [Queue](https://vitehub.dev/docs/server-primitives/queue) |
| Durable long-running work with provider-tracked run state | [Workflows](https://vitehub.dev/docs/server-primitives/workflows) |
| Static cron output or recurring runtime schedules | [Schedule](https://vitehub.dev/docs/server-primitives/schedule) |
| Isolated provider-managed execution | [Sandbox](https://vitehub.dev/docs/server-primitives/sandbox) |
| Controlled Unix-like command sessions | [Shell](https://vitehub.dev/docs/server-primitives/shell) |
## Use primitives from server code
Most primitives expose the same application import on every host. ViteHub connects that import to the selected provider during the build.
```ts [server/api/settings.put.ts]
import { kv } from 'vite-hub/kv'
export default defineEventHandler(async (event) => {
const [error] = await kv.set('settings', await readBody(event))
if (error) throw error
return { ok: true }
})
```
The route doesn't need to know whether KV uses local files, Cloudflare, Vercel, or another driver.
## Definitions and generated output
Some primitives work directly after configuration. Env, KV, Blob, Source, and Shell can often be called from server code without a discovered Definition.
Other primitives need a Definition so ViteHub can discover runtime behavior or named work. Email composes one declaratively configured provider, while Auth uses a singleton Definition bound at runtime. Database schemas, Workspace Definitions, Queue Definitions, Workflow Definitions, Static Schedule Definitions, Sandbox Definitions, and Agent Definitions can also generate Runtime Registries or host-specific Provider Output. Rate Limit uses source-local handles with explicit stable IDs instead of location-derived Definitions.
| Need | Read |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Understand portable Definitions and location-derived discovery | [Definitions and discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery) |
| Check where Definition files belong | [File conventions](https://vitehub.dev/docs/reference/file-conventions) |
| Inspect generated host artifacts | [Provider output](https://vitehub.dev/docs/reference/provider-output) |
| Configure package integrations and host settings | [Config options](https://vitehub.dev/docs/reference/config-options) |
| Use ViteHub's framework and host boundary | [Frameworks and hosts](https://vitehub.dev/docs/frameworks-hosts) |
| Emit Cloudflare bindings, routes, queues, workflows, crons, and workers | [Cloudflare](https://vitehub.dev/docs/frameworks-hosts/cloudflare) |
| Emit Vercel output for functions, queues, workflows, and runtime bindings | [Vercel](https://vitehub.dev/docs/frameworks-hosts/vercel) |
| Emit Deno Agent server output and Deno cron wake output | [Deno](https://vitehub.dev/docs/frameworks-hosts/deno) |
| Run the generated server output yourself | [Node/self-hosted](https://vitehub.dev/docs/frameworks-hosts/node-self-hosted) |
## Connect primitives to Agents
Capabilities expose controlled agent-facing access to primitives. A storage Capability can expose scoped read/edit tools, a Schedule Capability can manage allowed Runtime Schedules, and `workspaceShell()` can expose file inspection through Workspace and Shell boundaries.
Don't expose a server API to a model just because the app uses it. Add the relevant [Official Capability](https://vitehub.dev/docs/capabilities/official-capabilities) only when the Agent needs that ability. Configure its scope, write mode, and approvals for that task.
| Need | Read |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Build the Agent that will receive the ability | [Agents](https://vitehub.dev/docs/agents) |
| Understand the agent-facing contribution model | [Capabilities overview](https://vitehub.dev/docs/capabilities) |
| Pick from built-in Capability factories | [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) |
| Expose KV with scoped storage tools | [KV capability](https://vitehub.dev/docs/capabilities/kv) |
| Expose Blob storage with scoped file tools | [Blob capability](https://vitehub.dev/docs/capabilities/blob) |
| Expose relational data intentionally | [Database capability](https://vitehub.dev/docs/capabilities/db) |
| Let an Agent send authorized plain-text email | [Email capability](https://vitehub.dev/docs/capabilities/email) |
| Consume a trusted budget before an Agent Invocation | [Rate Limit capability](https://vitehub.dev/docs/capabilities/rate-limit) |
| Give an Agent headless browser evidence through an allowlisted command | [Browser capability](https://vitehub.dev/docs/capabilities/browser) |
| Let an Agent manage allowed Runtime Schedules | [Schedule capability](https://vitehub.dev/docs/capabilities/schedule) |
| Expose Workspace-backed inspection or mutation | [Workspace shell](https://vitehub.dev/docs/capabilities/workspace-shell) |
| Run isolated execution from an Agent boundary | [Sandbox capability](https://vitehub.dev/docs/capabilities/sandbox) |
## Next steps
- [Build the first primitive](https://vitehub.dev/docs/getting-started/first-server-primitive)
- [Build the first Agent](https://vitehub.dev/docs/getting-started/first-agent)
- [Read the shared primitive pattern](https://vitehub.dev/docs/concepts/server-primitives-for-any-host)
# KV
Use KV for settings, feature flags, cursors, cache records, and other small values addressed by key.
Use [Database](https://vitehub.dev/docs/server-primitives/database) when data needs relationships or constraints, [Blob](https://vitehub.dev/docs/server-primitives/blob) for large objects, and [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for file trees.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/kv
```
### Configure
```ts [vite.config.ts]
import { hubKv } from '@vite-hub/kv/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubKv()],
})
```
### Start using it
```ts [server/api/settings.put.ts]
import { kv } from '@vite-hub/kv'
export default defineEventHandler(async (event) => {
const [error] = await kv.set('settings', await readBody(event))
if (error) throw error
return { ok: true }
})
```
::
## Public imports
| Import | Use |
| ---------------------------------------------- | -------------------------------------------------------- |
| `kv` from `@vite-hub/kv` | Read and write the Default KV Store or a named KV Store. |
| `hubKv` from `@vite-hub/kv/vite` | Register KV runtime configuration. |
| `resolveKVViteConfig` from `@vite-hub/kv/vite` | Resolve KV Vite runtime config manually. |
All KV driver, store, module, and storage types are exported from `@vite-hub/kv`.
## Configuration options
Configure a default store directly, or configure named stores with `kv.stores`.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubKv()],
kv: {
stores: {
default: { driver: 'fs-lite' },
rateLimit: { driver: 'upstash' },
},
},
})
```
| Shape | Description |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `kv: false` | Disables KV runtime configuration. |
| `kv: { driver: 'fs-lite', base?: string }` | Uses local filesystem-backed KV. Default `base`: `.vitehub/data/kv`. |
| `kv: { driver: 'cloudflare-kv-binding', binding?: string, namespaceId?: string }` | Uses Cloudflare KV. Default `binding`: `KV`. `namespaceId` can come from `KV_NAMESPACE_ID`. |
| `kv: { driver: 'deno-kv', path?: string }` | Uses native Deno KV through `Deno.openKv()`. |
| `kv: { driver: 'upstash', url?: string, token?: string }` | Uses Upstash REST KV. Values can come from `KV_REST_API_URL` and `KV_REST_API_TOKEN`. |
| `kv: { stores: Record }` | Defines named KV Stores. `stores.default` is required. |
## Providers
| Provider | Driver | Default resolution |
| ---------------- | ----------------------- | -------------------------------------------------------------------------- |
| Local filesystem | `fs-lite` | Used for local/non-hosted development when no hosted env is detected. |
| Cloudflare KV | `cloudflare-kv-binding` | Used on Cloudflare hosting. |
| Deno KV | `deno-kv` | Used on Deno hosting. |
| Upstash | `upstash` | Used when Upstash env vars are present or when Vercel hosting is detected. |
## Use it at runtime
Use the `kv` Runtime Helper from server code.
```ts [server/api/settings.put.ts]
import { kv } from '@vite-hub/kv'
export default defineEventHandler(async (event) => {
const [error] = await kv.set('settings', await readBody(event))
if (error) throw error
return { ok: true }
})
```
```ts [server/api/settings.get.ts]
import { kv } from '@vite-hub/kv'
export default defineEventHandler(async () => {
const [error, settings] = await kv.get('settings')
if (error) throw error
return { settings }
})
```
Use named stores when configuration defines multiple KV Stores.
```ts [server/tenant-preferences.ts]
import { kv } from '@vite-hub/kv'
const preferences = kv.store('tenant-preferences')
export async function savePreferences(tenantId: string, value: unknown) {
const [error] = await preferences.set(tenantId, value)
if (error) throw error
}
```
KV does not provide the atomic consume contract required for request budgets. Use the [Rate Limit primitive](https://vitehub.dev/docs/server-primitives/rate-limit) instead of composing `get()` and `set()` under concurrency.
## Runtime helper
`kv` implements `KVStorage`.
| Method | Description |
| ----------------------- | ------------------------------------------- |
| `kv.get(key)` | Reads a value or returns `null`. |
| `kv.set(key, value)` | Writes a value. |
| `kv.has(key)` | Checks whether a key exists. |
| `kv.del(key)` | Deletes one key. |
| `kv.keys(base?)` | Lists keys under an optional base prefix. |
| `kv.clear(base?)` | Deletes keys under an optional base prefix. |
| `kv.store(name)` | Selects a named KV Store. |
Every async method returns `[error, value]`. Provider failures are `ViteHubError` values with code `KV_OPERATION_FAILED`, operation/store details, and the provider failure in `cause`. Application code can log, retry, ignore, or translate the error without `try/catch`. Invalid configuration and unknown named stores still throw before provider execution.
## Provider output
The KV package selects the default or named store and generates store-name types. Put provider namespaces, bindings, and credentials in integration configuration or deployment setup.
Application code keeps importing `kv` from `@vite-hub/kv` when you switch between local, Cloudflare, Deno, Vercel-compatible, or other drivers.
## Connect KV to Agents
Direct KV access is for app and server code. To let a model inspect or edit scoped key-value data, attach the KV Capability from the agent capability catalog.
```bash [Terminal]
pnpm add @vite-hub/agent
```
```ts [server/agents/support/agent.ts]
import { kv } from '@vite-hub/agent/capabilities'
```
Give model-facing tools the narrowest useful key prefix and configure write access deliberately. Read [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) for storage modes and write approvals.
## Production checks
KV prefixes are conventions, not relational models. Move data to Database when you need constraints, joins, migrations, history, or complex queries.
Do not build public coordination locks on top of basic `kv.get()` and `kv.set()`. ViteHub runtime coordination uses package-owned internal APIs when stronger guarantees are required.
## Next steps
- Use [Database](https://vitehub.dev/docs/server-primitives/database) for relational data.
- Use [Blob](https://vitehub.dev/docs/server-primitives/blob) for object storage.
- Expose scoped model access through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities).
# Queue
Use Queue when a request needs to hand off work and return before that work finishes. Enqueueing confirms that the provider accepted the job. It doesn't confirm that the handler ran successfully.
Use [Workflows](https://vitehub.dev/docs/server-primitives/workflows) when work needs a tracked run, durable steps, waits, or progress inspection.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/queue @vite-hub/runtime
```
For Vercel Queues, also install the provider package and ambient TypeScript types:
```bash [Terminal]
pnpm add @vercel/queue
pnpm add -D @types/node @types/ws
```
### Configure
```ts [vite.config.ts]
import { hubQueue } from '@vite-hub/queue/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubQueue()],
})
```
### Start using it
```ts [server/queues/welcome-email.ts]
import { defineQueue } from '@vite-hub/queue'
export default defineQueue<{ email: string }>(async ({ payload }) => {
await sendWelcomeEmail(payload.email)
})
```
```ts [server/api/welcome.post.ts]
import { runQueue } from '@vite-hub/queue'
export default defineEventHandler(async () => {
return runQueue('welcome-email', { email: 'ada@example.com' })
})
```
::
## Public imports
| Import | Use |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `defineQueue` from `@vite-hub/queue` | Declare a Queue Definition. |
| `runQueue`, `deferQueue`, `getQueue` from `@vite-hub/queue` | Enqueue jobs and access discovered QueueClients. |
| `createQueueClient` from `@vite-hub/queue` | Create a direct provider QueueClient. |
| `createQueueMessageId` from `@vite-hub/queue` | Generate a ViteHub message id with an optional prefix. |
| `ViteHubError` and `getViteHubErrorShape` from `@vite-hub/runtime` | Throw application failures or inspect Queue errors by namespaced code. |
| `createCloudflareQueueBatchHandler` from `@vite-hub/queue` | Build a Cloudflare batch handler outside generated Provider Output. |
| `getCloudflareQueueName`, `getCloudflareQueueBindingName`, `getCloudflareQueueDefinitionName`, `getVercelQueueTopicName` from `@vite-hub/queue` | Inspect provider-derived names. Don't persist these names as application identifiers. |
| `handleHostedVercelQueueCallback` from `@vite-hub/queue/runtime/hosted`, `createQueueCloudflareWorker` from `@vite-hub/queue` | Host adapter helpers used by generated Provider Output. Install `@vercel/functions` when importing the Vercel-specific runtime. |
| `hubQueue`, `createCloudflareQueueConfig` from `@vite-hub/queue/vite` | Register the Vite Integration and emit Cloudflare queue config. |
All Queue option, client, job, provider, and result types are exported from `@vite-hub/queue`.
## Configure the Vite Integration
Register the Queue Vite Integration and choose a Queue Provider with the `queue` Integration Options.
```ts [vite.config.ts]
import { hubQueue } from '@vite-hub/queue/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubQueue()],
queue: {
provider: 'cloudflare',
},
})
```
You can also pass the same options to `hubQueue()`. A `queue` key in `vite.config.ts` takes precedence.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubQueue({ provider: 'vercel', region: 'iad1' })],
})
```
### `provider` `'cloudflare' | 'vercel'`
Selects the Queue Provider. If you omit it, ViteHub resolves Cloudflare for Cloudflare hosting and Vercel for other supported production builds. Netlify cannot infer a Queue Provider, so set `provider` explicitly or disable Queue there.
### Integration-level `cache` `boolean`
Controls named QueueClient reuse for providers that can cache clients. Default: enabled. Cloudflare QueueClients still resolve the request-scoped binding for each request.
### `queue: false`
Disables runtime queue dispatch and skips generated Vercel queue consumer functions. Runtime calls throw `QUEUE_DISABLED`.
## Providers
| Provider | Configure with | Generated output | Nuance |
| ---------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Cloudflare | `queue: { provider: 'cloudflare' }` | Worker queue handler and `wrangler.json` `queues.producers` / `queues.consumers` entries. | Uses request-scoped queue bindings. Supports `contentType` and `delaySeconds`. |
| Vercel | `queue: { provider: 'vercel', region?: string }` | `.vercel/output` queue consumer functions with Vercel queue triggers. | Requires `@vercel/queue`. Supports idempotency, region, retention, and delayed send options. |
### Cloudflare options
`binding` `string`
Overrides the generated Cloudflare binding name. Without this option, ViteHub derives a binding from the Queue Definition name, such as `QUEUE_77656C636F6D65`.
Cloudflare queue names are generated as `queue--`. Application code must not depend on that name. Use `runQueue()` with the Queue Definition name.
### Vercel options
`region` `string`
Sets the default Vercel Queue region. If you omit it, ViteHub checks `QUEUE_REGION`, then `VERCEL_REGION`, then request headers in a Vercel request context.
Vercel topic names are generated as `topic--`. Application code must not depend on that topic. Use `runQueue()` with the Queue Definition name.
## Define a queue
Create a Queue Definition in `server/queues/.ts` or `src/.queue.ts`.
```ts [server/queues/welcome-email.ts]
import { defineQueue } from '@vite-hub/queue'
export default defineQueue<{ email: string }>(async (job) => {
await sendWelcomeEmail(job.payload.email)
})
```
The queue name comes from discovery. This file is addressed as `welcome-email` by Runtime Helpers.
## Queue job
The handler receives a normalized Queue Job.
| Field | Type | Description |
| ---------- | ---------- | ------------------------------------------------------------------------- |
| `payload` | `TPayload` | The payload passed by Queue Enqueue. |
| `id` | `string` | The provider message id when available, otherwise a generated message id. |
| `attempts` | `number` | Delivery attempt count. |
| `metadata` | `unknown` | Provider delivery metadata when the Queue Provider supplies it. |
Handler return values belong to Queue Delivery. `runQueue()` does not return the handler result.
Throw `ViteHubError` when the Queue Definition needs a stable application failure code. Queue retry policy belongs to Queue Delivery and provider callbacks, not the error object.
```ts [server/queues/image-expiry.ts]
import { getViteHubErrorShape, ViteHubError } from '@vite-hub/runtime'
import { defineQueue } from '@vite-hub/queue'
export default defineQueue<{ key?: string }>(async ({ payload }) => {
if (!payload.key) {
throw new ViteHubError('EXPIRY_INVALID_PAYLOAD', 'Image expiry payload requires a key.', {
details: { field: 'key' },
})
}
try {
await deleteImage(payload.key)
}
catch (cause) {
throw new ViteHubError('EXPIRY_FAILED', 'Image expiry failed.', {
cause,
details: { key: payload.key },
})
}
}, {
onError: error => getViteHubErrorShape(error)?.code === 'EXPIRY_INVALID_PAYLOAD' ? 'ack' : undefined,
callbackOptions: {
retry: error => getViteHubErrorShape(error)?.code === 'EXPIRY_INVALID_PAYLOAD'
? { acknowledge: true }
: undefined,
},
})
```
Application error codes and details are public. Keep credentials, provider responses, and private resource locations in `cause`. ViteHub reports each failed delivery before it chooses a provider action. Reports include the Queue Definition, safe message identifiers, attempt count, `code`, `details`, and retry policy. They don't serialize `cause` or unsafe identifiers.
## Queue Definition options
Pass Definition Options as the second argument to `defineQueue()`.
```ts [server/queues/report.ts]
import { defineQueue } from '@vite-hub/queue'
export default defineQueue<{ reportId: string }>(async (job) => {
await buildReport(job.payload.reportId)
}, {
concurrency: 5,
})
```
### Definition-level `cache` `boolean`
Overrides QueueClient caching for this Queue Definition.
### `concurrency` `number`
Controls Cloudflare batch delivery concurrency for this Queue Definition. Default: `1`. Values are floored to an integer and never lower than `1`.
### `onError` `(error, message, batch) => 'ack' | 'retry' | { retry: { delaySeconds?: number } } | void`
Handles Cloudflare message delivery errors. Return `'ack'` to acknowledge the failed message, `'retry'` to retry it, or `{ retry: { delaySeconds } }` to retry with a delay. Returning `void` applies the default Queue Delivery policy.
An explicit return value overrides the default Queue Delivery action. Returning `void` uses the built-in action for the error code.
### `callbackOptions` `{ retry?: VercelQueueRetryHandler, visibilityTimeoutSeconds?: number }`
Passes Vercel callback options to `@vercel/queue` for this Queue Definition.
When `retry` returns a directive, that directive overrides the default Queue Delivery action. Returning `void` preserves normal provider behavior.
### `onDispatchError` `(error, context) => unknown | Promise`
Handles dispatch errors from `deferQueue()`. This is not a Queue Delivery error hook.
## Enqueue work
Use `runQueue()` from server code.
```ts [server/api/signup.post.ts]
import { runQueue } from '@vite-hub/queue'
export default defineEventHandler(async (event) => {
const body = await readBody<{ email: string }>(event)
return runQueue('welcome-email', {
payload: { email: body.email },
idempotencyKey: `welcome:${body.email}`,
})
})
```
You can pass the payload directly when you do not need Queue Enqueue options.
```ts
await runQueue('welcome-email', { email: 'ava@example.com' })
```
## Queue Enqueue options
Queue Enqueue accepts either a raw payload or an envelope with `payload` and options.
```ts
await runQueue('welcome-email', {
payload: { email: 'ava@example.com' },
delaySeconds: 60,
})
```
| Option | Type | Cloudflare | Vercel | Description |
| ------------------ | ---------------------------- | ---------- | ------ | ------------------------------------------------------------------------------------- |
| `payload` | `TPayload` | Yes | Yes | The payload delivered to the Queue Definition. Required when using the envelope form. |
| `id` | `string` | Yes | Yes | ViteHub message id. If omitted, ViteHub generates one. |
| `contentType` | `CloudflareQueueContentType` | Yes | No | Cloudflare message content type. Values: `bytes`, `json`, `text`, `v8`. |
| `delaySeconds` | `number` | Yes | Yes | Provider-supported enqueue delay. |
| `idempotencyKey` | `string` | No | Yes | Vercel idempotency key. Defaults to the generated `id` when omitted. |
| `region` | `string` | No | Yes | Vercel send region for this Queue Enqueue. |
| `retentionSeconds` | `number` | No | Yes | Vercel message retention time. |
Unsupported provider options throw `ViteHubError` with a provider-specific code instead of being ignored.
## Develop locally
Use the Vite Integration to check that ViteHub discovers your Queue Definitions and generates the right provider output. A standalone Node process, such as a `tsx` script, does not run Vite discovery or load the generated Queue Runtime Registry, so `runQueue()` cannot find queue files from there.
```bash [Terminal]
pnpm vite build
```
After the build, inspect `.vitehub/queue/registry.mjs` to confirm that ViteHub found the queue. Then inspect the Queue Provider Output for the Queue Provider you configured.
| Provider | Output to inspect |
| ---------- | ---------------------------------------------------------------------------------------------- |
| Cloudflare | `dist/**/wrangler.json` queue producers and consumers, plus the generated worker bundle. |
| Vercel | `.vercel/output/functions/api/vitehub/queues/vercel/**` consumer functions and trigger config. |
Vercel projects that typecheck generated Queue Provider Output need `lib: ['DOM', 'ESNext']` and `types: ['node']` in `tsconfig.json`.
::note
Queue does not include an in-memory Queue Provider for local Queue Delivery. Test the code your handler calls when you need fast unit coverage, and use generated provider runtime or deployed provider output when you need to prove Queue Enqueue and Queue Delivery together.
::
## Runtime helpers
### `runQueue(name, input)`
Enqueues one Queue Job and returns the Queue Provider acceptance result.
```ts
const result = await runQueue('welcome-email', { email: 'ava@example.com' })
```
Returns:
```ts
type QueueSendResult = {
messageId?: string
status: 'queued'
}
```
### `deferQueue(name, input)`
Schedules Queue Enqueue through the current request's `waitUntil` support and returns `void`.
```ts
deferQueue('welcome-email', { email: 'ava@example.com' })
```
Use this when the current request must return without awaiting provider enqueue. ViteHub logs dispatch failures and passes them to `onDispatchError` when the Queue Definition provides one.
### `getQueue(name)`
Returns the provider-specific QueueClient for a discovered Queue Definition.
```ts
const queue = await getQueue('welcome-email')
await queue.send({ email: 'ava@example.com' })
```
### `createQueueClient(options)`
Creates a direct provider QueueClient. Most application code can use `runQueue()` or `getQueue()` and let ViteHub handle discovery and provider configuration.
Cloudflare direct clients require a concrete binding object.
```ts
await createQueueClient({
provider: 'cloudflare',
binding,
})
```
Vercel direct clients require a concrete topic.
```ts
await createQueueClient({
provider: 'vercel',
topic: 'topic--77656c636f6d65',
region: 'iad1',
})
```
## Errors
Queue APIs throw the shared `ViteHubError`. Built-in failures derive their public message and allowlisted details from a closed `QueueErrorCode` vocabulary. Application failures can use any stable code with a public message, JSON-safe `details`, an optional `requestId`, and a non-serialized `cause`.
Built-in failures use the closed `QueueErrorCode` union and allowlisted details. Queue Definitions can add an application code explicitly:
```ts
new ViteHubError('WELCOME_EMAIL_REJECTED', 'Welcome email was rejected.', {
cause,
details: { campaign: 'welcome' },
})
```
`JSON.stringify(error)` uses the shared safe shape and omits `cause`. Built-in provider errors use fixed messages and allowlisted `{ provider, operation }` details, while the raw SDK or binding failure remains available as `error.cause` in protected server-side diagnostics.
When migrating from package-specific Queue errors, import `ViteHubError` from `@vite-hub/runtime` for application failures and move acknowledgement or retry decisions into `onError` or `callbackOptions.retry`.
| Code | Meaning |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `QUEUE_DISABLED` | Queue runtime support is disabled. |
| `QUEUE_DEFINITION_NOT_FOUND` | No discovered Queue Definition matches the requested name. |
| `QUEUE_DEFINITION_LOAD_FAILED` | A discovered Queue Definition could not be loaded. |
| `QUEUE_PROVIDER_OPERATION_FAILED` | Queue client creation, send, or batch send failed. |
| `QUEUE_PROVIDER_RESPONSE_INVALID` | A successful Vercel send returned a missing or malformed `messageId`. |
| `CLOUDFLARE_BINDING_RESOLUTION_REQUIRED` | A direct Cloudflare client was created without a concrete binding. |
| `CLOUDFLARE_BINDING_INVALID` | The Cloudflare binding does not expose `send()` and `sendBatch()`. |
| `CLOUDFLARE_UNSUPPORTED_ENQUEUE_OPTIONS` | Cloudflare received unsupported enqueue options: `idempotencyKey`, `region`, or `retentionSeconds`. |
| `VERCEL_QUEUE_SDK_LOAD_FAILED` | `@vercel/queue` could not be loaded. |
| `VERCEL_QUEUE_SDK_INVALID` | `@vercel/queue` did not expose the expected client API. |
| `VERCEL_QUEUE_REGION_REQUIRED` | Vercel region could not be resolved for the installed SDK shape. |
| `VERCEL_PROVIDER_EXPECTED` | Hosted Vercel Queue Delivery resolved another provider. |
| `VERCEL_TOPIC_RESOLUTION_REQUIRED` | A direct Vercel client was created without a topic. |
| `VERCEL_UNSUPPORTED_ENQUEUE_OPTIONS` | Vercel received unsupported enqueue options such as `contentType`. |
## Provider output
The Queue Package discovers Queue Definitions, generates a Runtime Registry, and emits provider-specific Queue Delivery output.
| Provider | Output |
| ---------- | ---------------------------------------------------------------------------------------------------------------- |
| Cloudflare | Worker bundle plus `wrangler.json` queue producer and consumer entries. |
| Vercel | Queue consumer functions under `.vercel/output/functions/api/vitehub/queues/vercel/**` and queue trigger config. |
Generated files are Provider Output. Do not import them from application code.
## Connect Queue to Agents
Queue is a server primitive, not an Agent Capability by default. An Agent can enqueue work only when you expose that behavior through an app-owned Capability or server route.
Keep the Capability specific to the product task. Don't give a model arbitrary queue access because the app uses Queue internally.
## Next steps
- Use [Workflows](https://vitehub.dev/docs/server-primitives/workflows) for durable orchestration.
- Learn shared discovery rules in [Definitions and discovery](https://vitehub.dev/docs/concepts/definitions-and-discovery).
- Expose app-owned agent actions through [Custom capabilities](https://vitehub.dev/docs/capabilities/custom-capabilities).
# Rate Limit
Use Rate Limit before expensive server work to cap requests by client, user, account, or tenant. The selected driver consumes one unit atomically and reports whether the request can continue.
Don't build this with a KV `get()` followed by `set()`. Concurrent requests can read the same value. Rate Limit accepts drivers that implement atomic `consume()` for their backend.
## Quick start
Register the integration. It uses memory during local Vite development and infers Cloudflare from a production Nitro preset.
```ts [vite.config.ts]
import { vitehub } from 'vite-hub'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vitehub({ preset: "node", rateLimit: true })],
})
```
Require the Rate Limit directly in ordinary server code. The guard does not need a dedicated directory, file suffix, or module-scope declaration.
```ts [server/api/image-upload.post.ts]
import { requireRateLimit } from 'vite-hub/rate-limit'
export default defineEventHandler(async (event) => {
await requireRateLimit(event, 'image-upload', {
limit: 10,
window: '1m',
})
return { ok: true }
})
```
`requireRateLimit()` uses the event's client address and throws a standard H3 `HTTPError` when the request is limited. Pass `key: authenticatedUser.id` when a user, account, tenant, or API client is the correct budget boundary.
## Require a managed rate limit
`requireRateLimit(event, id, options)` resolves when the request is allowed. The integration finds calls inside handlers through the compiler AST and uses their stable IDs and provider policies for Provider Output.
```ts
await requireRateLimit(event, 'image-upload', {
enforcement: 'best-effort',
failure: 'deny',
key: authenticatedUser.id,
limit: 10,
window: '1m',
})
```
The ID, `limit`, `window`, `enforcement`, and `failure` must use static literals because ViteHub generates provider configuration before runtime. `event` and `key` remain runtime inputs, so an authenticated identity can be dynamic. Repeated IDs with the same normalized policy share one budget. Conflicting policies fail the build and report both source locations.
| Option | Type | Default | Description |
| ------------- | -------------------------- | ---------------------- | ------------------------------------------------------------------------------- |
| `limit` | positive integer | required | Allowed consumptions in each fixed window. |
| `window` | duration string | required | Fixed window such as `10s`, `1m`, `1h`, or `1d`. |
| `enforcement` | `"best-effort" | "strict"` | `"best-effort"` | Minimum enforcement guarantee the selected driver must provide. |
| `failure` | `"deny" | "allow"` | `"deny"` | Whether an unavailable driver returns a denied or allowed unavailable decision. |
| `key` | `string` | request client address | Runtime identity for a user, tenant, account, or API client. |
## Public imports
| Import | Use |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `requireRateLimit` from `vite-hub/rate-limit` or `@vite-hub/rate-limit` | Enforce a discovered managed Rate Limit inside an H3 handler. |
| `createRateLimiter` from `vite-hub/rate-limit` or `@vite-hub/rate-limit` | Build a direct limiter around a custom driver. |
| `memoryRateLimitDriver` from `@vite-hub/rate-limit/drivers/memory` | Enforce fixed windows in one process. |
| `cloudflareRateLimitDriver` from `@vite-hub/rate-limit/drivers/cloudflare` | Consume a Cloudflare Rate Limiting binding directly. |
| `hubRateLimit` from `@vite-hub/rate-limit/vite` | Register source collection, runtime setup, and Provider Output without the framework preset. |
`@vite-hub/rate-limit/runtime` is reserved for framework integration. Applications call `requireRateLimit()` or build a direct limiter.
## Understand the decision
Every driver returns `allowed`. Portable quota metadata is optional because native providers do not expose the same fields.
```ts
interface RateLimitDecision {
allowed: boolean
cause?: unknown
limit: number
reason?: 'limited' | 'unavailable'
remaining?: number
resetAt?: number
retryAfter?: number
used?: number
windowMs: number
}
```
Use `createRateLimiter()` when the application needs this decision for a custom response, explicit logging, or another transport. Provider unavailability follows the declared failure policy and carries its original `cause`; configuration and provider-contract defects still throw normal `TypeError` or `Error` instances. The managed guard maps rejection to H3 `HTTPError`: status `429` when limited and status `503` when fail-closed enforcement is unavailable. It adds `retry-after` only when the driver supplies `retryAfter`, so do not calculate billing or authorization from optional best-effort metadata.
## Inspect generated guarantees
The integration writes `.vitehub/rate-limit/manifest.json` during configuration and Provider Output. Agents and tooling can inspect it. Application code keeps using the guard.
The manifest records `enforcement`, counter `scope`, `rejectedAttempts`, and supported `windows` without duplicating optional response metadata contracts.
```json [.vitehub/rate-limit/manifest.json]
{
"schemaVersion": 2,
"rateLimits": [
{
"name": "image-upload",
"provider": "cloudflare",
"capabilities": {
"enforcement": "best-effort",
"rejectedAttempts": "unknown",
"scope": "location",
"windows": [10000, 60000]
}
}
]
}
```
## Use a direct driver
Use `createRateLimiter()` when the policy or driver is intentionally resolved outside managed Provider Output.
Install the owner package before importing a driver directly:
```bash [Terminal]
pnpm add @vite-hub/rate-limit
```
```ts
import { createRateLimiter } from '@vite-hub/rate-limit'
import { memoryRateLimitDriver } from '@vite-hub/rate-limit/drivers/memory'
const limiter = createRateLimiter({
driver: memoryRateLimitDriver(),
enforcement: 'strict',
limit: 2,
window: '1m',
})
const decision = await limiter.consume({ key: 'demo' })
```
Every direct limiter exposes its resolved `policy` and the provider capabilities that affect enforcement and deployment. The memory driver is process-local and intended for development, tests, and known single-process hosts.
Custom drivers return `[null, result]` after consuming the counter and `[error, undefined]` only for expected operational outages handled by the failure policy. Configuration, provider-contract, and implementation defects must throw normally.
## Deploy to Cloudflare
With a Cloudflare Nitro preset, `hubRateLimit()` infers the provider. Set a deployment-unique namespace so matching Rate Limit IDs cannot share counters across Workers or environments in the same Cloudflare account.
```ts [vite.config.ts]
export default defineConfig({
plugins: [hubRateLimit({ namespace: 'acme-image-service-production' })],
})
```
Cloudflare native enforcement is best-effort and exposes only 10-second and 60-second windows. It does not return portable quota metadata, so incompatible policies fail during the build. Use a different namespace for staging, production, and any separately deployed Worker because Cloudflare shares counters with the same namespace ID across Workers.
Inspect generated `wrangler.json` entries and exercise the deployed Worker. A request-scoped Cloudflare binding cannot be validated from an unrelated Node script.
## Limitations
- Memory enforcement is local and single-process. It is not a production default for horizontally scaled or request-scoped hosts.
- A production build with unknown hosting must select a provider explicitly or use a direct Rate Limiter.
- Request identity defaults to the H3 event's client address; explicit user or tenant identities remain application policy.
- Managed policies must be static so ViteHub can provision provider infrastructure.
- The package exposes atomic consumption, not a non-consuming check that providers cannot implement consistently.
## Related
- [Rate Limit Capability](https://vitehub.dev/docs/capabilities/rate-limit)
- [Cloudflare Provider Output](https://vitehub.dev/docs/frameworks-hosts/cloudflare)
- [Import paths](https://vitehub.dev/docs/reference/import-paths)
# Sandbox
Use a Sandbox Definition to run a named package project in a Box. The package supplies dependencies, Workspace supplies durable files, and the Box adapter runs the process.
## Quick start
Install and register the Vite integration:
```bash [Terminal]
pnpm add @vite-hub/sandbox
```
```ts [vite.config.ts]
import { hubSandbox } from '@vite-hub/sandbox/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubSandbox()],
})
```
Every discovered Definition belongs to a real package project. ViteHub never writes a manifest into your repository, so create the smallest valid one when the package has no dependencies:
```json [server/sandboxes/release-notes/package.json]
{
"private": true,
"type": "module",
"vitehub": {
"sandbox": {
"timeout": 30000
}
}
}
```
```ts [server/sandboxes/release-notes/index.ts]
interface SandboxPayload {
notes?: string
}
export default async function releaseNotes(payload: SandboxPayload = {}) {
return { text: payload.notes?.toUpperCase() || 'No notes' }
}
```
```ts [server/api/release-notes.post.ts]
import { runSandbox } from '@vite-hub/sandbox'
export default defineEventHandler(async () => {
const [error, result] = await runSandbox('release-notes', { notes: 'ship it' })
if (error) throw error
return result
})
```
## How Sandbox, Workspace, and Box fit together
- Sandbox discovers definitions, resolves package projects, serializes values, applies timeouts, and coordinates each run.
- Workspace stores durable files and handles Sources, snapshots, diffs, commits, and rollbacks.
- Box provides process isolation, runtime files, caches, ports, and provider-specific deployment output.
Sandbox and Agent use the same Box Interface. Workspace never selects Cloudflare, Vercel, Crabbox, or trusted-host execution.
## Package projects
Under `server/sandboxes`, use one folder per package project with an adjacent `package.json` and `index.ts`. The folder path supplies the Definition name. Other files in the package are ordinary helpers rather than independently discovered Sandboxes.
```text
server/sandboxes/
├── image/
│ ├── package.json
│ └── index.ts
└── metadata/
├── package.json
└── index.ts
```
For free-form Definitions outside `server/sandboxes`, use the `.sandbox.ts` suffix convention with `defineSandbox()`. Those Definitions use their nearest `package.json`, so several files can share one package project.
Package-manager selection uses the manifest's `packageManager` field, then a lockfile at that package root, then npm. A nested independent package never inherits an unrelated ancestor lockfile. Lockfiles enable frozen installation. ViteHub installs the project inside the Box before the entrypoint launches, and dependency trees never enter Workspace commits.
ViteHub also understands a standard pnpm Workspace without adding ViteHub-specific workspace configuration:
```text
server/sandboxes/
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── image/
├── package.json
└── index.ts
```
Installation runs at the pnpm Workspace root and the Definition runs from `server/sandboxes/image`. ViteHub carries every local package in the transitive `workspace:*` dependency closure, then pnpm remains responsible for installation and linking semantics. Other Workspace packages stay outside the runtime project.
## Package entry point
The package `index.ts` default-exports an ordinary async function. ViteHub calls it with the invocation payload and context, then returns its awaited result.
```ts
export default async function optimize(
payload: { image: Blob },
context: { requestId: string },
) {
return await optimizeImage(payload.image, context.requestId)
}
```
`runSandbox()` infers its payload and result from the default function. A zero-argument function accepts an `unknown` payload.
Nested `Blob` and `Uint8Array` values in payloads and results are staged through invocation-local Box files. Node.js `Buffer` values retain their `Buffer` type. Application code keeps the binary values and does not convert them to base64 JSON. Other values keep the existing JSON-serialization contract.
The entrypoint gets normal JavaScript, package imports, top-level await, `process.cwd()`, environment variables, and a filesystem, without a runtime framework import.
The first package metadata schema contains only `vitehub.sandbox.timeout`. It must be a positive integer no greater than `2_147_483_647`, and ViteHub enforces it while preparing and executing the package.
## Box adapters and images
Cloudflare, Vercel, Crabbox, and trusted host implement the Box Interface. The common contract covers binary files, directory operations, cwd/env/timeout command execution, abort, and lifecycle. Processes and ports are explicit optional capabilities.
Provider selection and full image overrides are application or host configuration. For Cloudflare, configure the application-owned container with a complete Dockerfile; for Vercel, configure the Box runtime image. Sandbox has no Dockerfile-fragment helper because partial image syntax cannot be portable across providers.
## Public imports
| Import | Use |
| ------------------------------------------ | ------------------------------------------------------------ |
| `defineSandbox` from `@vite-hub/sandbox` | Declare a free-form `.sandbox.ts` Definition. |
| `runSandbox` from `@vite-hub/sandbox` | Invoke a discovered Definition. |
| `hubSandbox` from `@vite-hub/sandbox/vite` | Register discovery, types, preparation, and provider output. |
Use [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for durable file state and Box configuration for execution environments.
# Schedule
Use a Static Schedule Definition for cron entries deployed with the app. Use Runtime Schedules when the app creates, updates, or removes recurring work while it runs.
A Schedule Target can start an Agent Invocation, but Schedule itself runs on the server. Give an Agent schedule access only through a Schedule Capability.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/schedule
```
### Configure
```ts [vite.config.ts]
import { hubSchedule } from '@vite-hub/schedule/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubSchedule()],
})
```
### Start using it
```ts [server/schedules/daily-report.ts]
import { defineSchedule } from '@vite-hub/schedule'
export default defineSchedule({
cron: '0 8 * * *',
async handler({ scheduledAt, waitUntil }) {
await sendDailyReport(scheduledAt)
waitUntil(recordDelivery())
},
})
```
::
## Public imports
| Import | Use |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `defineSchedule` from `@vite-hub/schedule` | Declare a Static Schedule Definition. |
| `defineScheduleTarget` from `@vite-hub/schedule` | Declare a cronless target for Runtime Schedules. |
| `schedules`, `validateRuntimeScheduleCron` from `@vite-hub/schedule` or `@vite-hub/schedule/runtime` | Manage Runtime Schedules and validate cron strings. |
| `executeSchedule`, `executeStaticSchedule`, `executeRuntimeSchedule`, `createScheduleRun` from `@vite-hub/schedule` | Execute schedules from provider hooks or custom runtime wiring. |
| `createMemoryRuntimeScheduleStore`, `createKVRuntimeScheduleStore` from `@vite-hub/schedule` | Configure Runtime Schedule storage. |
| `createMemoryScheduleRunStore`, `createKVScheduleRunStore` from `@vite-hub/schedule` | Configure Schedule Run storage. |
| `setRuntimeScheduleStore`, `setScheduleRunStore`, `setScheduleRuntimeRegistry` from `@vite-hub/schedule` | Wire custom runtime state. |
| `installScheduleRuntime` from `@vite-hub/schedule/runtime/driver` | Connect stored Runtime Schedules to a host-owned wake driver. |
| `createProcessScheduleWakeDriver` from `@vite-hub/schedule/runtime/process` | Scan and wake due Runtime Schedules inside a long-running process. |
| `hubSchedule`, `createScheduleNitroConfig` from `@vite-hub/schedule/vite` | Register discovery and generated provider output. |
Schedule Definition, Runtime Schedule, Schedule Run, and Schedule Store types are exported from `@vite-hub/schedule`.
## Configure the Vite Integration
```ts [vite.config.ts]
import { hubSchedule } from '@vite-hub/schedule/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
hubSchedule({
runtime: {
driver: 'process',
prefix: 'my-app:schedule',
},
}),
],
})
```
| Option | Type | Default | Description |
| ---------------- | --------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `providerOutput` | `ScheduleVitePluginOptions['providerOutput']` | `auto` | Controls generated provider cron output. Values: `auto`, `standalone`, `nitro`, `false`. |
| `projectRoot` | `string` | ViteHub project root | Resolves discovered schedule files and generated registry output from a custom project root. |
| `runtime` | `ScheduleProcessRuntimeOptions` | No runtime driver | Explicitly installs the generated Nitro Process Runtime. Accepts `driver: 'process'`, plus optional `prefix` (default `vitehub:schedule`), `intervalMs` (default `60_000`), and `concurrency` (default `1`). |
Use `createScheduleNitroConfig()` when a Nitro integration owns config merging and needs Schedule to return Nitro-ready provider output.
The Process Runtime imports the discovered registry and runs Static Schedule Definitions alongside persisted Runtime Schedules through one driver queue. It creates the Runtime Schedule and Schedule Run stores through the default KV store configured by `hubKv()`, applies the Schedule prefix to both, reports errors through Nitro, and closes the driver during Nitro shutdown. It scans once per minute with one concurrent wake unless configured otherwise, and `intervalMs` cannot exceed the one-minute cron resolution. This setting is orthogonal to `providerOutput`; selecting one does not infer the other.
::warning
The Process Runtime requires exactly one long-lived process or replica. The KV run store records occurrences but does not provide distributed leader election or locking. Do not use this driver on request-scoped or serverless hosts that may stop between requests. It scans inside the Node.js process and does not create cron, systemd, or another operating-system schedule.
::
## Provider output
| Mode | Output | Nuance |
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
| `auto` | Selects the appropriate generated output for the active build context. | Default mode for Vite projects. |
| `standalone` | Writes standalone provider output outside Nitro. | Use when ViteHub owns provider output directly. |
| `nitro` | Writes Nitro Cloudflare module and plugin output. | Use when Nitro owns Cloudflare cron wiring. |
| `false` | Disables generated provider output. | Runtime helpers still work when you wire execution yourself. |
| Host | Static Schedule output | Runtime Schedule nuance |
| ---------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Cloudflare | Cron trigger output and Cloudflare schedule runtime entry wiring. | Runtime Schedules still need Provider Wake output or a long-running runner. |
| Vercel | Vercel cron-compatible output for static schedules. | Runtime Schedules still need Provider Wake output or a long-running runner. |
| Deno | `Deno.cron` output loaded by generated Deno Agent server output. | Runtime Schedules still need Provider Wake output or a long-running runner. |
::warning
Provider Wake output requires a static five-field UTC cron string compatible with generated provider output. Runtime Schedules still need an existing Provider Wake or a long-running host to execute due schedules.
::
## Define a static schedule
Use a Static Schedule Definition when the host needs build-time Provider Output such as cron entries or provider wake configuration.
```ts [server/schedules/daily-report.ts]
import { defineSchedule } from '@vite-hub/schedule'
export default defineSchedule({
cron: '0 8 * * *',
async handler(context) {
await sendDailyReport(context.scheduledAt)
},
})
```
Cron expressions use the Schedule Time Base, currently UTC. The discovered file name provides the Static Schedule Definition identity.
## Schedule Definition options
| Option | Type | Required | Description |
| ----------------------- | ----------------- | -------- | ------------------------------------------------------------------ |
| `cron` | `string` | Yes | Five-field UTC cron expression for the Static Schedule Definition. |
| `handler` | `ScheduleHandler` | Yes | Function called with Schedule Run Context. |
| `allowRuntimeSchedules` | `boolean` | No | Allows Runtime Schedules to target this definition. |
`ScheduleRunContext` includes `id`, `scheduledAt`, `waitUntil`, optional `attemptId`, optional `runId`, optional Runtime Schedule id, optional Runtime Schedule target, and optional Runtime Schedule `input`.
Use `waitUntil(promise)` for consequential work that can outlive the handler body. Direct and local execution settles registered work before recording the Schedule Run result; a rejection fails the run with the same diagnostics as a handler rejection. An installed wake runtime instead retains registered work after the handler returns, reports rejection through its `onError` hook, and drains outstanding work when the runtime closes.
## Create recurring Runtime Schedules
Runtime Schedules are cron schedules stored by ViteHub. A Runtime Schedule can target only a Runtime Schedule Target that opted into runtime reuse. Set an IANA `timeZone` when the cron must follow local civil time and daylight-saving changes. Omit it to use UTC.
Use `defineScheduleTarget()` when the handler runs only through Runtime Schedules and doesn't need its own build-time cron. These targets don't emit static provider output.
```ts [server/schedules/report.ts]
import { defineScheduleTarget } from '@vite-hub/schedule'
export default defineScheduleTarget<{ prompt: string }>({
async handler({ input }) {
if (input) await generateReport(input.prompt)
},
})
```
`defineSchedule()` remains cron-required and can opt into runtime reuse with `allowRuntimeSchedules`:
```ts [server/schedules/daily-report.ts]
import { defineSchedule } from '@vite-hub/schedule'
export default defineSchedule({
allowRuntimeSchedules: true,
cron: '0 8 * * *',
async handler() {
await sendDailyReport()
},
})
```
Use the `schedules` Runtime Helper from server code.
```ts [server/api/schedules.post.ts]
import { schedules } from '@vite-hub/schedule/runtime'
export default defineEventHandler(async () => {
return schedules.create({
cron: '30 8 * * 1-5',
id: 'weekday-report',
input: { prompt: 'Summarize yesterday' },
target: 'report',
timeZone: 'Europe/Copenhagen',
})
})
```
## Runtime Schedule input
| Input | Type | Required | Description |
| ---------- | -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| `cron` | `string` | create only | Five-field cron expression evaluated in `timeZone`, or UTC when `timeZone` is omitted. |
| `target` | `ScheduleTargetName` | create only | A `defineScheduleTarget()` declaration or Static Schedule Definition that set `allowRuntimeSchedules: true`. |
| `id` | `string` | No | Stable Runtime Schedule id. ViteHub generates one when omitted. |
| `enabled` | `boolean` | No | Whether the Runtime Schedule executes. Defaults to `true` on create. |
| `input` | `unknown` | No | Opaque input passed to the target handler as `context.input`. |
| `timeZone` | `string` | No | Named IANA time zone used to evaluate the cron expression. Numeric offsets such as `+01:00` are rejected. Defaults to UTC. |
`RuntimeScheduleUpdateInput` accepts `cron`, `target`, `enabled`, `input`, and `timeZone`. Create stores an input snapshot. Providing `input` on update replaces the complete snapshot; omitting it preserves the existing value. Schedule does not merge or interpret input, and the configured store must support the value's serialization requirements. Omitting `timeZone` on update preserves the stored zone; set it explicitly to `UTC` to reset UTC evaluation.
Local cron matching follows conventional daylight-saving behavior: a local time missing during a DST gap is skipped, while both distinct instants in a repeated local time during a DST overlap run.
## Runtime helper methods
| Method | Description |
| ------------------------------- | ------------------------------------------ |
| `schedules.create(input)` | Creates a Runtime Schedule. |
| `schedules.list()` | Lists Runtime Schedules. |
| `schedules.get(id)` | Reads one Runtime Schedule. |
| `schedules.update(id, input)` | Updates a Runtime Schedule. |
| `schedules.delete(id)` | Deletes a Runtime Schedule. |
| `schedules.enable(id)` | Sets `enabled` to `true`. |
| `schedules.disable(id)` | Sets `enabled` to `false`. |
| `schedules.run(id, options?)` | Executes one Runtime Schedule immediately. |
| `schedules.listRuns()` | Lists Schedule Run records. |
| `schedules.getRun(id)` | Reads one Schedule Run record. |
| `schedules.listAttempts(runId)` | Lists attempts for one Schedule Run. |
One-time delayed execution is not part of the first-version Scheduling vocabulary; use a recurring cron schedule, Queue delay, or Workflow design when that matches the actual behavior.
## Connect a Runtime Schedule wake driver
Host integrations use a wake driver when the host can create and remove native schedule registrations at runtime.
```ts [server/runtime/schedule.ts]
import { installScheduleRuntime } from '@vite-hub/schedule/runtime/driver'
const controller = await installScheduleRuntime({
createDriver: context => hostScheduler.driver(context),
registry: scheduleRegistry,
runtimeScheduleStore,
scheduleRunStore,
staticRegistry: scheduleRegistry,
})
```
`createDriver(context)` returns a driver with `reconcile(schedules)`. Pass `staticRegistry` when the driver also schedules discovered Static Schedule Definitions. Each reconciliation then receives those definitions with the complete stored Runtime Schedule snapshot, including disabled records. Installation waits for the first reconciliation.
The installed runtime processes Runtime Schedule creates, updates, and deletes one at a time. It saves each change before reconciling the wake driver. If reconciliation fails, ViteHub restores the previous record and rejects the change. Manual `schedules.run()` calls execute immediately and don't reconcile the driver.
When the host fires a native wake, call `context.wake({ scheduleId, scheduledAt })` with the exact stored Runtime Schedule id and occurrence time. Call `controller.close()` during host shutdown to release process resources; closing does not delete definitions, schedules, or run history.
Use `createProcessScheduleWakeDriver()` from `@vite-hub/schedule/runtime/process` when a custom long-running host wants the same in-process wake behavior without generated Nitro wiring.
`startScheduleRunner()` has been removed. Existing self-hosted processes must install `createProcessScheduleWakeDriver()` through `installScheduleRuntime()` and await `controller.close()` during host shutdown.
Static provider output remains build-time configuration; selecting the Process Runtime also executes discovered Static Schedule Definitions without requiring provider output.
## Storage
| Store | Configure with | Nuance |
| ----------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Memory Runtime Schedule Store | `createMemoryRuntimeScheduleStore()` | Default in-process behavior; useful for tests and local runtime only. |
| KV Runtime Schedule Store | `createKVRuntimeScheduleStore(options?)` | Persists Runtime Schedule records through a KV-compatible storage object. |
| Memory Schedule Run Store | `createMemoryScheduleRunStore()` | Default in-process run history; useful for tests and local runtime only. |
| KV Schedule Run Store | `createKVScheduleRunStore(options?)` | Persists Schedule Runs and attempts through KV-compatible storage. |
| Custom Store | `setRuntimeScheduleStore(store)`, `setScheduleRunStore(store)` | Implement `RuntimeScheduleStore` or `ScheduleRunStore` directly. |
## Connect Schedule to Agents
The Schedule Capability can let an Agent read or manage allowed Runtime Schedules through Capability policy. Inline Agent Schedules start the owning Agent with Schedule Invocation Input, not a synthetic user message.
Attach a Schedule Capability only when a model needs to manage schedules. Read [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) for Capability modes and write policy.
## Production checks
Schedule Runs, Schedule Run Attempts, retry policy, overlap policy, and dedupe policy belong to Schedule. Naming a policy does not imply every policy is configurable in the first version.
Static Schedule Definitions and Provider Wake output remain UTC. Runtime Schedules use UTC by default and can persist an IANA `timeZone` when local clock time must follow daylight-saving changes.
## Next steps
- Use [Queue](https://vitehub.dev/docs/server-primitives/queue) when a provider-supported enqueue delay is enough.
- Use [Workflows](https://vitehub.dev/docs/server-primitives/workflows) for durable orchestration.
- Learn trigger language in [Channels API](https://vitehub.dev/docs/concepts/channels-api).
# Shell
Use Shell when server code needs to inspect or change files through Unix-like commands. You choose which commands, files, processes, network access, and timeouts each provider supports.
Shell runs commands through a provider. [Sandbox](https://vitehub.dev/docs/server-primitives/sandbox) can supply an isolated provider, but it doesn't replace Shell's sessions, command analysis, or execution policy.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/shell @vite-hub/workspace
```
### Configure
```ts [server/tasks/search-docs.ts]
import { createShellRuntime } from '@vite-hub/shell'
import { createJustBashProvider } from '@vite-hub/shell/providers/just-bash'
import { createReadonlyWorkspaceFs, workspaceMountPoint } from '@vite-hub/shell/workspace'
import { useWorkspace } from '@vite-hub/workspace'
const workspace = useWorkspace('docs')
const shell = createShellRuntime({
provider: createJustBashProvider({
commands: ['pwd', 'ls', 'cat', 'rg'],
cwd: workspaceMountPoint,
fs: createReadonlyWorkspaceFs(workspace.fs),
}),
})
```
### Start using it
```ts [server/tasks/search-docs.ts]
await shell.exec('rg auth docs', { cwd: workspaceMountPoint })
```
::
## Public imports
| Import | Use |
| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `createShellRuntime` from `@vite-hub/shell` | Create a Shell Runtime from an Execution Provider. |
| `analyzeShellCommand` from `@vite-hub/shell` | Parse a command and return static command facts. |
| `createJustBashProvider` from `@vite-hub/shell/providers/just-bash` | Run Bash-compatible commands in the `just-bash` browser runtime. |
| `createCloudflareShellProvider` from `@vite-hub/shell/providers/cloudflare` | Adapt a Cloudflare execution client to Shell. |
| `createReadonlyWorkspaceFs`, `createWritableWorkspaceFs`, `workspaceMountPoint` from `@vite-hub/shell/workspace` | Mount Workspace file access into Shell providers. |
| `runWorkspaceInspectionCommand` from `@vite-hub/shell/workspace` | Run a preflighted read-only Workspace inspection command. |
| `cleanWorkspaceShellPath`, `cleanWorkspaceMutationPath` from `@vite-hub/shell/workspace` | Normalize Workspace paths for shell-facing behavior. |
Shell Runtime, Session, Policy, Boundary, Observation, Provider, process, and Workspace filesystem types are exported from these entrypoints.
## Providers
Shell providers implement `ShellExecutionProvider`. Shell has provider adapters, not Vite Integration output.
| Provider | Configure with | Boundary nuance |
| ---------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Just Bash | `createJustBashProvider({ fs, commands?, cwd? })` | Runs `just-bash` against the filesystem adapter you provide. Network is disabled; background and interactive processes are unsupported. |
| Cloudflare | `createCloudflareShellProvider({ sandbox })` | Delegates command execution to a Cloudflare client that exposes `exec(command, args, options)`. CWD and env support come from `sandbox.supports`; network is reported as `unknown`. |
| Custom | A `ShellExecutionProvider` object | Implement `boundary`, `exec`, optional `analyze`, and optional process methods. |
For Cloudflare Workers agents that use Cloudflare's structured shell runtime, install the Cloudflare packages beside ViteHub Shell:
```bash [Terminal]
pnpm add @cloudflare/shell @cloudflare/codemode
```
`@cloudflare/shell` exposes structured `state.*` and Git tools through `@cloudflare/codemode`; it is not a Bash interpreter. Use `createCloudflareShellProvider()` when the Cloudflare runtime provides a command-execution client. Use a custom `ShellExecutionProvider` to translate ViteHub Shell calls into `@cloudflare/shell` state operations.
## Create a Shell runtime
Pass an execution provider to `createShellRuntime()`. The built-in Just Bash provider runs Bash-compatible commands against the filesystem adapter you supply.
```ts [server/tasks/search-docs.ts]
import { createShellRuntime } from '@vite-hub/shell'
import { createJustBashProvider } from '@vite-hub/shell/providers/just-bash'
import { createReadonlyWorkspaceFs, workspaceMountPoint } from '@vite-hub/shell/workspace'
import { useWorkspace } from '@vite-hub/workspace'
export async function searchDocs() {
const workspace = useWorkspace('docs')
const runtime = createShellRuntime({
provider: createJustBashProvider({
commands: ['pwd', 'ls', 'cat', 'rg'],
cwd: workspaceMountPoint,
fs: createReadonlyWorkspaceFs(workspace.fs),
}),
})
return runtime.exec('rg auth docs', {
cwd: workspaceMountPoint,
})
}
```
The provider controls available commands. The Workspace filesystem controls whether file writes can happen.
## Runtime options
| Option | Type | Description |
| ---------- | ------------------------ | ------------------------------------------------------------------ |
| `provider` | `ShellExecutionProvider` | Required execution provider. |
| `policy` | `ShellSessionPolicy` | Default policy applied to runtime `exec()` calls and new sessions. |
`runtime.exec(command, options?)` creates a short-lived session, runs one command, disposes the session, and returns a Shell Observation.
## Use Shell sessions
A Shell Session adds stateful policy around repeated commands, output size, timeouts, and process budget.
```ts [server/tasks/inspect-docs.ts]
import { createShellRuntime } from '@vite-hub/shell'
export async function inspect(runtime: ReturnType) {
const session = runtime.createSession({
policy: {
maxOutputLength: 10_000,
maxShellCalls: 4,
timeout: 30_000,
},
})
try {
return await session.exec('pwd')
}
finally {
await session.dispose()
}
}
```
## Session and exec options
| Option | Type | Applies to | Description |
| ----------------- | ------------------------ | ---------------- | ---------------------------------------------------------------------- |
| `env` | `Record` | `createSession` | Default environment for session commands. |
| `maxOutputLength` | `number` | `policy` | Truncates Shell Observation output. |
| `maxShellCalls` | `number` | `policy` | Limits calls to `exec()` in one session. |
| `maxProcesses` | `number` | `policy` | Limits tracked background processes when a provider supports them. |
| `timeout` | `number` | `policy`, `exec` | Command timeout in milliseconds. |
| `cwd` | `string` | `exec` | Working directory for the command when the provider supports CWD. |
| `stdin` | `string` | `exec` | Standard input sent to the command. |
| `onStdout` | `function` | `exec` | Receives stdout chunks when the provider supports streaming callbacks. |
| `onStderr` | `function` | `exec` | Receives stderr chunks when the provider supports streaming callbacks. |
## Analyze commands
Command Analysis reports facts about a command before execution. The caller makes the final policy decision.
```ts [server/tasks/analyze-command.ts]
import { analyzeShellCommand } from '@vite-hub/shell'
export async function analyze(command: string) {
return analyzeShellCommand(command)
}
```
`analyzeShellCommand(command, options?)` uses `sh-syntax` and returns `ok`, parser name, command names, and flags for pipelines, redirects, heredocs, and command substitution. `ShellAnalyzeOptions` accepts `maxInputBytes` and `timeoutMs`.
Don't treat analysis as sandbox enforcement. The execution provider and caller policy control what the command can do.
## Shell observation shape
| Field | Type | Description |
| -------------------- | ----------------------- | --------------------------------------------------------------------------------------- |
| `event` | `ShellObservationEvent` | `command_finished`, `command_timed_out`, `policy_denied`, or `session_disposed`. |
| `exitCode` | `number or null` | Provider exit code, or `null` when no process exit happened. |
| `stdout` | `string` | Captured stdout. |
| `stderr` | `string` | Captured stderr. |
| `command` | `string` | Command that ran, when available. |
| `cwd` | `string` | Working directory used by the provider, when available. |
| `durationMs` | `number` | Runtime duration, when available. |
| `outputTruncated` | `boolean` | Whether `maxOutputLength` truncated output. |
| `timedOut` | `boolean` | Whether timeout ended command execution. |
| `workspaceGuardrail` | `object` | Workspace inspection feedback such as broad search, missing path, no match, or timeout. |
## Connect Shell to Agents
Agents use Shell through Capabilities, usually `workspaceShell()`. That Capability exposes shell-shaped Workspace inspection and optional structured Workspace mutation tools through Workspace Scope, Workspace rules, and Shell policy.
The global Agent `bash` tool is separate from the Shell runtime. Capabilities register executables, and ViteHub sends each structured call through an executable Workspace Session. Read the [Bash concept](https://vitehub.dev/docs/concepts/bash) for the Agent contract.
Don't expose a raw Shell runtime to a model. Use [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities) so its policy, metadata, driver support, and tools stay attached to the Agent Definition.
## Production checks
Configure command, filesystem, network, process, streaming, and timeout access before running commands. A Shell Network Grant permits only the network access it names.
Use Sandbox when the app needs provider-managed isolation. Use Shell when the app needs controlled command semantics over a declared Shell Workspace.
## Next steps
- Understand the model-facing [Bash](https://vitehub.dev/docs/concepts/bash) tool.
- Use [Workspace](https://vitehub.dev/docs/server-primitives/workspace) for file-tree state.
- Use [Sandbox](https://vitehub.dev/docs/server-primitives/sandbox) for isolated execution providers.
- Expose command inspection to agents through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities).
# Source
Use Source when server code needs read-only content from files, globs, Markdown, GitHub, MCP resources, or a custom loader.
Source retrieves content but doesn't place it in a persistent file tree. Bind a Source to [Workspace](https://vitehub.dev/docs/server-primitives/workspace) when the content needs paths, sync, snapshots, rules, or agent access.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add vite-hub
```
### Configure
```ts [server/sources.ts]
import { defineSources, registerSources } from 'vite-hub/source'
import { file } from 'vite-hub/source/file'
export const sources = defineSources({
readme: file('README.md'),
})
registerSources(sources)
```
### Start using it
```ts [server/api/readme.get.ts]
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(() => {
return useSource('readme').read('README.md')
})
```
::
## Public imports
| Import | Use |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `defineSource`, `defineSources`, `createSource`, `combineSources`, `custom` from `vite-hub/source` | Define Sources, create context-dependent readers, and combine keyed readers. |
| `defineCollection`, `table` from `vite-hub/source`, `useCollection` from `vite-hub/source/client` | Turn a table or custom loader into a typed, paginated HTTP read model and consume it from Vue. |
| `useDatabase` from `vite-hub/database/drizzle` | Access a discovered database and its generated schema. |
| `registerSource`, `registerSources`, `clearSources`, `getRegisteredSource`, `useSource` from `vite-hub/source` | Manage and read the process-local Source registry. |
| `file`, `glob`, `github`, `markdown`, `mcpResources` from the matching `vite-hub/source/*` subpath | Select one built-in loader and its private implementation closure. |
| `defineContent`, `contentSource` from `vite-hub/source/content` | Define the Comark Content runtime from registered ViteHub Sources or adapt one reader explicitly. |
| `createContentClient` from `vite-hub/source/content/client` | Use Comark Content's typed runtime client. |
| `getViteHubErrorShape` from `vite-hub/runtime` | Inspect registry, path, and loader failures by `SOURCE_*` code. |
Source, Source Reader, Source Item, revision, cache, and error types are exported from `vite-hub/source`. Loader option types live beside their implementation subpath. Libraries that install the package directly can use the matching `@vite-hub/source` paths.
## Register Sources
Use `vite-hub/source` when you want a direct retrieval registry.
```ts [server/sources.ts]
import { defineSources, registerSources } from 'vite-hub/source'
import { file } from 'vite-hub/source/file'
import { github } from 'vite-hub/source/github'
export const sources = defineSources({
readme: file('README.md'),
docs: github({
repo: 'acme/docs',
ref: 'main',
root: 'docs',
include: ['**/*.md'],
}),
})
registerSources(sources)
```
Named Source Loader imports are the public authoring shape. Import the helpers you need directly.
Source has no discovery or Vite Integration by itself. Import the module that registers Sources before calling `useSource()` in a process.
## Source loader options
| Loader | Key options | Nuance |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `file(input)` | A path string, `{ path, workspacePath?, mediaType? }`, or inline `{ workspacePath, content, mediaType? }`. | Reads one file from the Source Context root. `workspacePath` controls the Source key. |
| `markdown(options)` | `{ path, workspacePath?, mediaType? }` or inline `{ workspacePath, content, mediaType? }`. | Uses the `file()` contract with `text/markdown` as the default media type. Unlike `file()`, it requires an options object. |
| `glob(options)` | `include`, `cwd`, `ignore`, `dot`, `followSymlinks`, `keyCache`, `prefix`. | Expands local files with `tinyglobby`; `keyCache: false` refreshes keys on each read path. |
| `github(options)` | `repo`, `ref`, `root`, `auth`, `include`, `exclude`, `cache`. | Retrieves repository archive content. `auth` can be a token string or a trusted callback. |
| `mcpResources(options)` | `server`, `include`, `exclude`, `path`, `request`, `cache`. | Reads MCP Resource content. `server` can be a client, client config, or resolver. |
| `custom(source)` | A `Source` object. | Use when the built-in loaders do not match the origin contract. |
### Cache options
`github()`, `mcpResources()`, and custom Sources can expose a cache policy; `false` disables it. GitHub applies the policy to its own ref, archive, and metadata caches. Workspace can also consume the same policy when it decides whether materialized Source content is fresh.
| Option | Type | Default | Description |
| -------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `maxAge` | `number` | Consumer default | Maximum cache age in seconds. Workspace uses this value when deciding whether materialized Source content is still fresh. |
## Source object contract
A custom `Source` implements the retrieval behavior directly.
| Field | Type | Description |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------- |
| `name` | `string` | Loader name used in errors and metadata. |
| `cache` | `false or SourceCacheOptions` | Optional cache policy. |
| `fingerprint` | `unknown` | Cache identity for origin state. |
| `resolveRevision(ctx)` | `function` | Optionally pins a mutable origin ref to one revision before any other operation. |
| `prepare(ctx)` | `function` | Optional prefetch or validation hook. |
| `getKeys(ctx)` | `function` | Returns all addressable Source keys. |
| `getItem(key, ctx)` | `function` | Returns a `SourceItem` for one key. |
| `getItems(ctx)` | `function` | Optional bulk item reader. |
| `getMeta(key, ctx)` | `function` | Optional metadata reader. |
| `getKeys()` and `getItem()` are required. `resolveRevision()` and `prepare()` each run at most once for every `useSource()` reader before its first operation. The resolved revision is added to the shared context, so preparation, keys, items, and metadata observe the same origin snapshot. `getItems()` lets a consumer load all items in one call; `getMeta()` can return origin metadata without loading content. | | |
### Source context
The caller supplies `SourceContext` to every custom Source method.
| Field | Type | Default | Description |
| --------------- | ---------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `rootDir` | `string` | `process.cwd()` for `useSource()` | Base project directory. |
| `sourceRootDir` | `string` | None | Optional Source-specific root. Built-in local file loaders fall back to `rootDir` when it is absent. |
| `source` | `string` | Registered Source name | Identifies the active Source. |
| `workspace` | `string` | None | Identifies the Workspace consuming the Source. |
| `abortSignal` | `AbortSignal` | None | Cancels in-flight work. Custom loaders must forward it to fetches and other abortable operations. |
| `revision` | `SourceRevision` | None | The revision pinned by `resolveRevision()` for every later operation in this reader or Workspace lifecycle. |
## Use it at runtime
Read a Source by name with `useSource()`.
```ts [server/api/readme.get.ts]
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(async () => {
const readme = useSource('readme')
return {
text: await readme.read('README.md'),
}
})
```
## Source reader API
| Method | Returns |
| ---------------------------- | --------------------------------------------------------------- |
| `source.revision()` | The pinned origin revision, when supported. |
| `source.keys()` | All Source keys. |
| `source.get(key)` | A `SourceItem` with content, data, media type, and metadata. |
| `source.read(key, options?)` | Text by default, or `Uint8Array` with `{ encoding: 'binary' }`. |
| `source.meta(key)` | Metadata for one key, when the loader supports it. |
| `source.exists(key)` | Whether a key exists. |
| `source.list(prefix?)` | Direct child files and directories below a prefix. |
```ts [server/api/docs.get.ts]
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(async () => {
const docs = useSource('docs')
return {
files: await docs.keys(),
root: await docs.list(''),
}
})
```
## Parse, search, and serve content at runtime
Install `comark-content` when Source output is documentation or application
content that should become a parsed runtime API:
```bash [Terminal]
pnpm add vite-hub comark-content
```
```ts [server/content.ts]
import sqlite from 'comark-content/database/sqlite-node'
import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search'
import { defineContent } from 'vite-hub/source/content'
export const content = defineContent({
plugins: [sqliteFullTextSearch({ database: sqlite() })],
sources: {
docs: 'docs',
},
})
await content.get('/guide')
await content.navigation(['docs'])
await content.search(['docs'], 'runtime')
```
ViteHub discovers `server/content.ts` and serves its exported `content` instance
at `/api/content/**` in Vite and Nuxt. `defineContent()` delegates the runtime
contract to Comark and preserves methods contributed by its server plugins. No
manual framework route or `fetch()` wrapper is required.
Registered Source names, explicit Source Readers, and native Comark Content
Sources can coexist in one definition. The ViteHub adapter gives each Comark
cache refresh a new Source Reader, so a runtime can discover a newer origin
revision without mixing revisions within one load.
Use `sqlite-wasm` where Node SQLite is unavailable. Comark Content owns parsed
document cache entries and exposes `refresh(source)`, `invalidate(key)`, and
`expire(key)`. ViteHub therefore does not duplicate content parsing or ranked
search inside Source.
```ts [app/utils/content.ts]
import { createContentClient } from 'vite-hub/source/content/client'
import searchClient from 'comark-content/plugins/sqlite-full-text-search/client'
export const content = createContentClient({
plugins: [searchClient()],
})
await content.search(['docs'], 'runtime')
```
Workspace keeps its filesystem search because it searches every visible file,
including generated and non-content files. Collections also remain distinct:
they are typed, paginated application read models over records, while Comark
Content exposes parsed document manifests and content APIs.
## Combine keyed Source readers
Use `combineSources()` when several readers can return the same key. A
combined reader identifies each item with a `[source, key]` tuple, so the source
alias remains part of the runtime value and its inferred type.
```ts [server/recaps.ts]
import { combineSources, createSource, defineSource } from 'vite-hub/source'
const github = defineSource(context => ({
async get(month: `${number}-${number}`) {
return { month, rootDir: context.rootDir }
},
async items() {
return [{ key: '2026-07' as const }]
},
}))
export const recaps = combineSources({
sources: {
github: createSource(github, { rootDir: process.cwd() }),
},
})
await recaps.get(['github', '2026-07'])
await recaps.items()
// [{ key: '2026-07', source: 'github', identity: ['github', '2026-07'] }]
```
Source aliases must be strings. `get()` infers the accepted key and result
for each alias. `items()` is available on every combined reader, but it rejects a
partially enumerable reader before starting any work. When every reader
implements `items()`, each returned item includes `source` and `identity`.
`defineSource(context => reader)` declares a context-dependent keyed reader.
`createSource()` creates that reader with a `SourceContext`. Combined readers do not
change the process-local registry: `defineSources()`, `registerSources()`, and
`useSource()` keep their existing behavior.
## Expose a typed Collection
A Source describes where data comes from. A Collection describes the paginated
object shape an application exposes to a client. For a discovered Drizzle
database, let the database adapter own the keyset query:
```ts [server/collections/articles.ts]
import { eq } from 'drizzle-orm'
import * as v from 'valibot'
import { useDatabase } from 'vite-hub/database/drizzle'
import { defineCollection, table } from 'vite-hub/source'
const { db, schema } = useDatabase('default')
export const articles = defineCollection({
source: table({
db,
table: schema.articles,
orderBy: {
column: schema.articles.createdAt,
direction: 'desc',
tieBreaker: schema.articles.id,
},
defaultLimit: 25,
maxLimit: 100,
querySchema: v.object({ author: v.optional(v.string()) }),
where: ({ query, table }) => query.author
? eq(table.author, query.author)
: undefined,
}),
transform: article => ({ id: article.id, title: article.title }),
})
```
`column` and `tieBreaker` must be non-null columns on the selected table, and the
tie-breaker must be unique. The table source applies `where` before its lexicographic
cursor predicate, orders both columns consistently, requests the extra row, and
keeps the cursor opaque to clients. Omit `querySchema` and `where` when the
Collection has no filters.
Use `defineCollection` directly when the origin is a Source reader, SDK, HTTP
API, joined query, or another loader whose pagination is not a single Drizzle
table. In that escape hatch, the loader owns its origin-specific cursor logic.
```ts [server/collections/articles.ts]
import { defineCollection } from 'vite-hub/source'
import * as v from 'valibot'
export const articles = defineCollection(async ({ cursor, limit, query }) => {
return db.listArticles({ after: cursor, author: query.author, limit })
}, {
cursor: article => [article.createdAt, article.id] as const,
cursorSchema: v.tuple([v.number(), v.string()]),
defaultLimit: 25,
maxLimit: 100,
querySchema: v.object({ author: v.optional(v.string()) }),
transform: article => ({ id: article.id, title: article.title }),
})
```
The generic Collection requests one extra row from the loader, enforces its configured
limits, and turns the last visible row into an opaque cursor. `transform()` is
the server-to-client boundary, so private columns and provider objects stay out
of the response while its return type becomes the client item type. Any Standard
Schema validator can provide `cursorSchema` and `querySchema`; their output types
flow into the loader without manual generic annotations.
```vue [app/pages/articles.vue]
```
ViteHub discovers modules in `server/collections` and generates their type
registry and read-only GET routes. Each module exports a Collection with the
same name as its filename, so `articles.ts` exports `articles` and maps to
`/api/articles`. The Nuxt module auto-imports `useCollection`; outside Nuxt,
import it from `vite-hub/source/client`. Everything in `server/collections` is
public through its transformed shape; keep private definitions elsewhere and do
not create a matching `server/api` handler. Restart Nuxt after adding, removing,
or renaming a Collection module so Nitro rebuilds its handler manifest. Use
`filter` for validated request input. It stays
fixed while `loadMore()` advances the opaque cursor. For a bounded Collection,
set `all: true` to fetch every page asynchronously. `cursor` and `limit` are
reserved route query parameters. Invalid limits, cursor encodings, and parsed
filters return HTTP 400.
## Use Sources with Workspace
Use Workspace Source Bindings when retrieved content needs to appear inside a persistent Workspace file tree.
```ts [server/workspaces/docs.ts]
import { defineWorkspace, file, github } from 'vite-hub/workspace'
export default defineWorkspace({
sources: {
readme: file('README.md'),
docs: github({
repo: 'acme/docs',
root: 'docs',
mount: 'docs',
materialize: 'lazy',
}),
},
})
```
The same loader names appear in both packages. Import them from `vite-hub/source/*` for direct retrieval through `useSource()`. Import them from `vite-hub/workspace` when retrieved items need Workspace paths, materialization, sync, validation, resolution, or access rules.
## Provider output
Source has no Vite integration. By itself, it doesn't generate host output, provider config, or discovered Definitions.
Workspace and other consuming packages can wrap Sources in discovered Definitions, runtime registries, generated metadata, or Provider Output when they need placement, persistence, or deployment wiring.
## Production checks
Sources are read-only. Read secrets for private origins from Server Env or trusted callbacks, not from model-authored input.
Use Workspace when content needs durable sync, path-scoped rules, diffs, snapshots, or scoped agent visibility. Use Source directly when server code only needs to retrieve and inspect items.
## Next steps
- Learn the shared model in [Workspace and Sources](https://vitehub.dev/docs/concepts/workspace-and-sources).
- Persist retrieved content through [Workspace](https://vitehub.dev/docs/server-primitives/workspace).
- Expose visible Workspace content to agents through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities).
# Workflows
Use Workflows for long-running work that needs a tracked run, retries, resumable state, or durable steps.
Use [Queue](https://vitehub.dev/docs/server-primitives/queue) when you only need to deliver a job. A Workflow starts and tracks a run.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/runtime @vite-hub/workflow
```
### Configure
```ts [vite.config.ts]
import { hubWorkflow } from '@vite-hub/workflow/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkflow()],
})
```
### Start using it
```ts [server/workflows/onboard-user.ts]
import { defineWorkflow } from '@vite-hub/workflow'
export default defineWorkflow<{ email: string }>(async ({ payload }) => {
return createUser(payload.email)
})
```
```ts [server/api/onboard.post.ts]
import { runWorkflow } from '@vite-hub/workflow'
export default defineEventHandler(async () => {
return runWorkflow('onboard-user', { email: 'ada@example.com' })
})
```
::
## Public imports
| Import | Use |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `defineWorkflow` from `@vite-hub/workflow` | Declare a Workflow Definition. |
| `runWorkflow`, `deferWorkflow`, `getWorkflowRun`, `cancelWorkflow`, `resumeWorkflowSignal` from `@vite-hub/workflow` | Start, defer, inspect, cancel, or resume Workflow Runs. |
| `createWorkflow` from `@vite-hub/workflow` | Create an inline Workflow Handle for app-owned code. |
| `normalizeWorkflowOptions` from `@vite-hub/workflow` | Resolve Integration Options to a concrete Workflow Provider. |
| `ViteHubError` from `@vite-hub/runtime` | Throw application-owned Workflow failures with stable codes. |
| `readRequestPayload`, `readValidatedPayload`, `validatePayload` from `@vite-hub/workflow` | Read provider request payloads in custom runtime entrypoints. |
| `hubWorkflow` from `@vite-hub/workflow/vite` | Register Workflow discovery and provider output generation. |
Workflow Provider, Definition, Run, Step, Start Options, and Integration Options types are exported from `@vite-hub/workflow`.
## Configure the Vite Integration
```ts [vite.config.ts]
import { hubWorkflow } from '@vite-hub/workflow/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkflow()],
})
```
The Vite config key is `workflow`.
| Option | Type | Default | Description |
| ------------------------ | ------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `workflow` | `boolean` or `WorkflowModuleOptions` | disabled | Enables Workflow discovery and provider output through `vitehub()` with `true` or an options object; `false` leaves it disabled. |
| `provider` | `WorkflowProvider` | inferred | Selects `cloudflare`, `vercel`, or `openworkflow`. |
| `binding` | `string` | provider default | Provider binding name for generated output. |
| `name` | `string` | discovered workflow name | Provider resource name override. |
| `database` | `string` | none | OpenWorkflow storage through a ViteHub Named Database. |
| `postgres.url` | `WorkflowRuntimeConfigValue` | none | OpenWorkflow Postgres URL. |
| `postgres.schema` | `string` | provider default | OpenWorkflow Postgres schema. |
| `postgres.namespaceId` | `string` | provider default | OpenWorkflow namespace id. |
| `postgres.runMigrations` | `boolean` | provider default | Runs OpenWorkflow storage migrations. |
| `sqlite.path` | `WorkflowRuntimeConfigValue` | none | OpenWorkflow SQLite path. |
| `sqlite.namespaceId` | `string` | provider default | OpenWorkflow SQLite namespace id. |
| `sqlite.runMigrations` | `boolean` | provider default | Runs OpenWorkflow SQLite migrations. |
| `worker.concurrency` | `number` | provider default | OpenWorkflow worker concurrency. |
When no provider is configured, ViteHub selects Cloudflare on Cloudflare hosting and Vercel on other supported hosts. Netlify cannot infer a Workflow Provider, so set `provider` explicitly or disable Workflow there. On Node or Docker hosting, OpenWorkflow is inferred when OpenWorkflow storage is configured through `database`, `postgres.url`, or `sqlite.path`.
## Providers
| Provider | Configure with | Provider output | Nuance |
| ------------ | ------------------------------------------------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Cloudflare | `workflow: { provider: 'cloudflare' }` | Cloudflare Workflow class, binding, and runtime entry output. | Runs through Cloudflare Workflow bindings. Use `binding` and `name` when the generated names must match existing infrastructure. |
| Vercel | `workflow: { provider: 'vercel' }` | Vercel workflow runtime output under the build output. | Persists run state through provider runtime support and Vercel-specific workflow names. |
| OpenWorkflow | `workflow: { provider: 'openworkflow', database/postgres/sqlite }` | OpenWorkflow worker/runtime output. | Requires explicit storage. `database`, `postgres.url`, and `sqlite.path` are mutually exclusive storage choices. |
## Define a workflow
Create a Workflow Definition for named long-running work.
```ts [server/workflows/onboard-user.ts]
import { defineWorkflow } from '@vite-hub/workflow'
export default defineWorkflow<{ email: string }>(async ({ payload }) => {
const user = await createUser(payload.email)
await sendWelcomeEmail(user.email)
return { userId: user.id }
})
```
Use Workflow Steps only when the selected provider and definition need independently retryable or inspectable units.
## Workflow Definition options
`defineWorkflow(handler, options?)` accepts these options. The discovered file name provides the Definition name.
| Option | Type | Description |
| ---------- | ----------------- | --------------------------------------------------------------------------- |
| `id` | `string` | Static provider id override for the Workflow Definition. |
| `native` | `WorkflowHandler` | Provider-native durable entry used by Vercel Workflow DevKit. |
| `rootStep` | `boolean` | Wraps the handler in a root Workflow Step when the provider supports steps. |
The handler receives a `WorkflowExecutionContext` with `name`, `payload`, `provider`, optional run `id`, and provider-backed `step` or typed `steps` helpers when available.
### Add a durable Vercel entry
Vercel runs the normal handler inline unless the definition provides `native`.
Inline work does not survive a function restart. Register a Workflow DevKit entry
when the run needs Vercel's durable execution:
```bash [Terminal]
pnpm add workflow @workflow/builders
```
```ts [server/workflows/onboard-user.ts]
import {
defineWorkflow,
type WorkflowExecutionContext,
} from '@vite-hub/workflow'
interface OnboardPayload {
email: string
}
async function createUserStep(email: string) {
'use step'
return await createUser(email)
}
async function sendWelcomeEmailStep(email: string) {
'use step'
await sendWelcomeEmail(email)
}
async function durableOnboard({ payload }: WorkflowExecutionContext) {
'use workflow'
const user = await createUserStep(payload.email)
await sendWelcomeEmailStep(user.email)
return { userId: user.id }
}
async function inlineOnboard({ payload }: WorkflowExecutionContext) {
const user = await createUser(payload.email)
await sendWelcomeEmail(user.email)
return { userId: user.id }
}
export default defineWorkflow(inlineOnboard, { native: durableOnboard })
```
ViteHub transforms the `native` entry when it generates Vercel output. Other
providers keep using the normal handler. Keep external side effects in `use step`
functions and make them idempotent because a step can be retried.
## Start a run
Use `runWorkflow()` from server code.
```ts [server/api/onboard.post.ts]
import { runWorkflow } from '@vite-hub/workflow'
export default defineEventHandler(async (event) => {
const body = await readBody<{ email: string }>(event)
return runWorkflow('onboard-user', body)
})
```
The run id belongs to Invocation Options. Use a stable id when the selected
provider supports caller-assigned ids and needs to deduplicate or resume the
same logical run. Native Vercel workflows reject an explicit `id`; let Workflow
DevKit assign it as shown above.
## Runtime helpers
| Helper | Description |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| `runWorkflow(name, payload?, options?)` | Starts a Workflow Run immediately. |
| `deferWorkflow(name, payload?, options?)` | Starts a run through the deferred provider path when available. |
| `getWorkflowRun(name, id)` | Reads the current run state. |
| `cancelWorkflow(name, id)` | Cancels a durable Vercel run. |
| `resumeWorkflowSignal(token, payload)` | Resumes a Vercel operation using a registered Workflow DevKit hook token. |
| `createWorkflow(name, options?)` | Returns a handle with `run`, `defer`, `getRun`, and `cancel`. |
`WorkflowStartOptions` currently accepts `id`.
Cancellation currently requires a native Vercel Workflow Definition.
Cloudflare, OpenWorkflow, and inline Vercel runs report
`WORKFLOW_OPERATION_UNSUPPORTED` instead of simulating cancellation.
Signal resumption requires the Vercel provider, the Workflow DevKit runtime,
and a registered hook token. The application can choose a deterministic opaque
token; it becomes resumable when a native workflow registers the hook and
suspends while waiting for it. Pass that token to `resumeWorkflowSignal()`.
It identifies the hook, not a Workflow Run. Cloudflare and OpenWorkflow report
signals as unsupported.
## Structured errors
Throw `ViteHubError` when app code needs a stable failure contract across Workflow Providers. ViteHub-owned failures use the package's fixed `WorkflowErrorCode` vocabulary.
```ts [server/workflows/transcribe.ts]
import { ViteHubError } from '@vite-hub/runtime'
import { defineWorkflow } from '@vite-hub/workflow'
export default defineWorkflow<{ recordingId: string }>(async ({ payload }) => {
try {
return await transcribeRecording(payload.recordingId)
}
catch (cause) {
throw new ViteHubError('TRANSCRIPTION_FAILED', 'Transcription failed.', {
cause,
details: { recordingId: payload.recordingId },
})
}
})
```
Every `ViteHubError` requires a stable `code` and public `message`. Calling `error.toJSON()` returns `name`, `code`, `message`, and JSON-safe `details`; it omits `cause`, which stays on the in-memory error for logging and debugging. ViteHub's built-in codes are typed as `WorkflowErrorCode` with code-derived messages and code-specific details. Configure retries on the Workflow Step; throwing an error does not override the Step's retry policy.
## Workflow Run shape
| Field | Type | Description |
| ---------- | ------------------- | --------------------------------------------------------- |
| `id` | `string` | Provider or ViteHub Workflow Run id. |
| `provider` | `WorkflowProvider` | Selected provider for the run. |
| `status` | `WorkflowRunStatus` | `queued`, `running`, `completed`, `failed`, or `unknown`. |
| `result` | `TResult` | Completed result when available. |
| `payload` | `TPayload` | Original payload when the provider returns it. |
| `metadata` | `unknown` | Provider metadata. |
## Inspect a run
Use `getWorkflowRun()` when server code needs current run state.
```ts [server/api/workflows/[id\\].get.ts]
import { getWorkflowRun } from '@vite-hub/workflow'
export default defineEventHandler((event) => {
return getWorkflowRun('onboard-user', getRouterParam(event, 'id')!)
})
```
## Connect Workflows to Agents
An Agent can start a workflow only when you expose that action through a Capability or server route. Workflows track durable work. Agents provide model-backed behavior.
Use a product-specific Capability when a model needs to start or inspect a particular Workflow Run.
## Production checks
Use Queue when background delivery is enough. Use Workflow when the app must inspect run state, resume work, or coordinate multiple steps over time.
Keep credentials and database URLs in Server Env. Hosted workflow providers may require explicit state storage or deployment setup.
## Next steps
- Use [Queue](https://vitehub.dev/docs/server-primitives/queue) for simple background delivery.
- Trigger recurring work with [Schedule](https://vitehub.dev/docs/server-primitives/schedule).
- Learn shared runtime events in [Runtime policy, approvals, and traces](https://vitehub.dev/docs/concepts/runtime-policy-approvals-and-traces).
# Workspace
Use Workspace when server code or an Agent needs a persistent file tree. A Workspace can read and write files, sync Sources, create snapshots and diffs, and open transactional sessions. You control which operations each caller receives.
[Blob](https://vitehub.dev/docs/server-primitives/blob) stores objects without file-tree behavior. [Source](https://vitehub.dev/docs/server-primitives/source) retrieves read-only content. Workspace can store its files in Blob and bind content from Sources.
## Quick start
::steps{level="3"}
### Install
```bash [Terminal]
pnpm add @vite-hub/workspace
```
### Configure
```ts [vite.config.ts]
import { hubWorkspace } from '@vite-hub/workspace/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkspace()],
})
```
### Start using it
```ts [server/workspaces/docs.ts]
import { defineWorkspace, glob } from '@vite-hub/workspace'
export default defineWorkspace({
sources: {
docs: glob({ include: ['docs/**/*.md'] }),
},
})
```
::
## Public imports
| Import | Use |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `defineWorkspace` from `@vite-hub/workspace` | Declare a Workspace Definition. |
| `useWorkspace` from `@vite-hub/workspace` or `@vite-hub/workspace/runtime` | Read, write, diff, snapshot, sync, or start sessions for a Workspace. |
| `file`, `glob`, `github`, `markdown`, `mcpResources`, `fetch`, `custom` from `@vite-hub/workspace` | Declare Workspace Source Bindings. |
| `createWorkspaceTools` from `@vite-hub/workspace` or `@vite-hub/workspace/ai` | Build AI SDK tools from Workspace access. |
| Source resolution and request helpers from `@vite-hub/workspace/runtime` | Integrate resolved Workspace Sources into runtime facades. |
| `defineWorkspaceFileHandler`, `readWorkspaceFileResponse` from `@vite-hub/workspace/server` | Serve Workspace files from H3 routes. |
| `hubWorkspace` from `@vite-hub/workspace/vite` | Register Workspace discovery, generated types, assets, and runtime wiring. |
| `@vite-hub/workspace/loader`, `@vite-hub/workspace/publish`, `@vite-hub/workspace/test` | Add loaders and publishers, or create test Workspaces. |
Workspace definition, Source Binding, rule, hook, store, sync, facade, and session types are exported from `@vite-hub/workspace`. Source resolution runtime types are exported from `@vite-hub/workspace/runtime`.
## Configure the Vite Integration
```ts [vite.config.ts]
import { hubWorkspace } from '@vite-hub/workspace/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkspace()],
})
```
The Vite config key is `workspace`.
| Option | Type | Default | Description |
| ------------- | ------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `workspace` | `boolean` or `WorkspaceModuleOptions` | disabled | Enables Workspace discovery and runtime wiring through `vitehub()` with `true` or an options object; `false` leaves it disabled. |
| `root` | `string` | `.vitehub/workspaces` | Runtime Workspace root directory. |
| `projectRoot` | `string` | ViteHub project root | Resolves server-side discovery from a custom project root. |
| `assets` | `WorkspaceModuleOptions['assets']` | package default | Controls build-time Workspace asset materialization. Accepts `false`, `true`, or explicit asset paths. |
| `store` | `WorkspaceStoreOptions` | inferred from development mode, hosting, and environment | Default Workspace Store used by definitions that do not choose one. |
## Store providers
| Store | Configure with | Nuance |
| -------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Local | `{ provider: 'local', root?: string }` | Filesystem-backed Workspace Store. Used by default in development and on hosts without a more specific match. |
| Memory | `{ provider: 'memory' }` | Test or ephemeral runtime storage. |
| Cloudflare Artifacts | `{ provider: 'cloudflare-artifacts', binding?, namespace?, repo?, repoPrefix?, branch? }` | Opt-in, versioned Git storage. Defaults: binding `WORKSPACE_ARTIFACTS`, namespace `vitehub`, repo prefix `vitehub-workspace-`. |
| Vercel Blob | `{ provider: 'vercel-blob', token?, prefix?, access? }` | Blob-backed storage. Defaults: prefix `.vitehub/workspaces`, access `private`; the token can come from `BLOB_READ_WRITE_TOKEN`. |
| GitHub | `{ provider: 'github', repo?, repository?, branch?, root?, token? }` | Repository-backed storage. Defaults: branch `main`, root `.vitehub/workspaces/`. |
| Custom | `WorkspaceStore` | Implement the Workspace Store contract directly. |
Without a `store`, development uses Local. Production uses Memory on Cloudflare, Vercel Blob when `BLOB_READ_WRITE_TOKEN` exists, Memory on Vercel without that token, and Local on other hosts. You must select Cloudflare Artifacts or GitHub yourself.
### Cloudflare Artifacts
Select Cloudflare Artifacts when a deployed Worker needs durable Workspace state:
```ts [vite.config.ts]
import { hubWorkspace } from '@vite-hub/workspace/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [hubWorkspace()],
workspace: {
store: {
provider: 'cloudflare-artifacts',
binding: 'WORKSPACE_ARTIFACTS',
namespace: 'vitehub',
},
},
})
```
The Vite integration adds Artifacts Stores to generated Cloudflare config for the module and discovered definitions. It preserves application bindings and removes only bindings that Workspace generated when the provider changes. Reusing one binding name for different namespaces fails the build. Each named Workspace uses `` unless `repo` selects one repository, so names with repository-unsafe characters remain isolated.
`workspace.snapshot()` commits and pushes the current file tree. Its snapshot id is the pushed Git commit SHA. File metadata is stored in the repository with the Workspace tree so Source-backed write protection and media types survive a fresh Worker instance.
Cloudflare Artifacts is currently a closed beta and is not available on Workers Free, so the Cloudflare default remains the ephemeral `memory` Store. The Worker adapter clones into isolate memory; use it for deliberately small Workspaces rather than assuming the Artifacts repository limit is also a usable Worker checkout size. For large repositories in a sandbox, container, or VM, use Cloudflare's [ArtifactFS](https://developers.cloudflare.com/artifacts/guides/artifact-fs/){rel=""nofollow""} directly.
Artifacts repositories are private Git storage. Use [Blob](https://vitehub.dev/docs/server-primitives/blob) with R2 or another provider when an Agent needs a public delivery URL.
## Define a workspace
Create a Workspace Definition when the app needs durable file-tree behavior.
```ts [server/workspaces/docs.ts]
import { defineWorkspace, glob, github } from '@vite-hub/workspace'
export default defineWorkspace({
sources: {
docs: glob({
cwd: '.',
include: ['README.md', 'docs/**/*.md'],
}),
handbook: github({
repo: 'acme/handbook',
ref: 'main',
root: 'support',
mount: 'handbook',
materialize: 'lazy',
}),
},
rules: {
'/**': { write: false },
'/drafts/**': { write: true, mediaType: 'text/markdown' },
},
})
```
Source keys identify named origins inside the Workspace Source Map. A Source-Backed Path is read-only unless Workspace rules and runtime access allow writes elsewhere in the file tree.
## Workspace Definition options
`defineWorkspace()` accepts these top-level fields. The name comes from the discovered file path.
| Option | Type | Description |
| --------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commit` | `boolean | string` | Auto-commit all Workspace changes, optionally with a custom message. |
| `rootDir` | `string` | Source root used by loaders. |
| `sourceRootDir` | `string` | Source-specific root for Source helpers. |
| `store` | `WorkspaceStoreOptions` | Store for this Workspace. |
| `bindings` | `Record` | Explicit scalar or file-backed values available to Agent Instruction Composition. Values can be `string`, `number`, `boolean`, `null`, or `{ path: string }`. |
| `sources` | `Record` | Workspace Source Bindings. |
| `rules` | `WorkspaceRules` | Read, write, media type, max size, commit, and validation policy by path pattern. |
| `hooks` | `WorkspaceHooks` | Write lifecycle hooks. |
| `plugins` | `WorkspacePlugin[]` | Bundled rules and hooks. |
| `loaders` | `WorkspaceLoader[]` | Build-time or runtime loaders. |
| `publish` | `WorkspacePublisher[]` | Publication behavior after snapshots or sync. |
Instruction text reads scalar bindings with `{{ workspace. }}` and file-backed bindings with `@workspace.`. Only keys declared in `bindings` are available; ViteHub does not expose arbitrary Workspace files to Instruction Composition. See [Agent instructions](https://vitehub.dev/docs/agents/instructions#insert-workspace-bindings).
## Source Binding options
Workspace Source Bindings can wrap Source Package loaders and add Workspace behavior.
| Option | Type | Description |
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mount` | `WorkspaceSourceMount` | Where retrieved items appear in the Workspace file tree. Accepts a path string or Mount options. |
| `materialize` | `WorkspaceMaterializeMode` | Build-time, lazy, or disabled materialization. Values: `build`, `lazy`, `none`. |
| `cache` | `false or WorkspaceCacheOptions` | Source cache policy. Use `false` to disable caching or `{ maxAge }` to set a TTL. |
| `validate` | `WorkspaceValidateMode` | Request validation mode for API-backed Sources. Use `false` or `request`. |
| `sync` | `WorkspaceSourceSyncConfig` | Enables explicit Workspace Source Sync. Accepts `true`, `false`, or a sync policy. |
| `probeKeys` | `string[]` | Known Source item keys used to check bundled-source completeness and intersect path-scoped access without enumerating the whole Source. File-shaped helpers infer this when possible. |
### Fetch Sources
`fetch(options)` declares an HTTP-backed Source. It can expose one read-only Workspace path, or omit `workspacePath` to remain request-only for runtime Source request integrations. `fetch(resolver)` receives the invocation-aware Source Resolution Context and returns the same options or `false`, `null`, or `undefined`.
| Option | Type | Default | Description |
| --------------- | -------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | `string | URL` | required | Request URL. |
| `workspacePath` | `string` | none | Read-only Workspace path for the response. Omitting it creates a request-only Source. |
| `method` | `GET | HEAD | POST` | `GET` | Allowed HTTP method. GET and HEAD cannot declare a body. |
| `responseType` | `json | text` | `json` | Response parser and serialized Workspace content type. |
| `query` | `Record` | URL query | Static query values. Cannot be combined with `querySchema`. |
| `querySchema` | Standard JSON Schema-compatible schema | none | Validates runtime query input and supplies schema defaults. Cannot be combined with `query`. |
| `body` | `unknown` | none | Static POST body. Cannot be combined with `bodySchema`. |
| `bodySchema` | Standard JSON Schema-compatible schema | none | Validates runtime body input and supplies schema defaults. Cannot be combined with `body`. |
| `headers` | `Record` | none | Static request headers. |
| `cookies` | `Record` | none | Static request cookies. |
| `timeout` | `number` | package default | Request timeout in milliseconds. |
| `request` | `FetchSourceRequestOptions | callback` | none | Adds headers, cookies, or timeout at request time. The callback receives request metadata, the Selected Workspace Scope, Source key, and Workspace name. |
| `transform` | `(response) => output` | identity | Transforms parsed response data before ViteHub serializes it. |
| `cache` | `false | { maxAge?: number }` | `false` | Controls Source response caching. |
| `materialize` | `build | lazy | none` | `lazy`, or `none` when sync is enabled | Controls when response content is written into the Workspace Store. |
| `probeKeys` | `string[]` | inferred from `workspacePath` | Overrides the known Source item keys. |
| `sync` | `boolean | WorkspaceSourceSyncPolicy` | `false` | Allows explicit Workspace Source Sync. |
A plain object Source with `url` is inferred as Fetch. In that shorthand, `path` supplies the Workspace path; when neither `path` nor `workspacePath` is present, ViteHub derives a file path from a query-free URL. Use the explicit `fetch()` helper when request-only behavior is intentional.
## Use it at runtime
Read files from server code with `useWorkspace()`.
```ts [server/api/docs.get.ts]
import { useWorkspace } from '@vite-hub/workspace'
export default defineEventHandler(async () => {
const workspace = useWorkspace('docs')
return workspace.fs.glob('**/*.md')
})
```
Request write access only at the call site that needs mutation.
```ts [server/api/drafts.post.ts]
import { useWorkspace } from '@vite-hub/workspace'
export default defineEventHandler(async (event) => {
const workspace = useWorkspace('docs', { mode: 'write' })
const body = await readBody<{ text: string }>(event)
await workspace.fs.writeFile('drafts/summary.md', body.text, {
mediaType: 'text/markdown',
})
return workspace.diff()
})
```
## Runtime facade
`useWorkspace(name)` returns read access. `useWorkspace(name, { mode: 'write' })` returns write access.
| Surface | Methods |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `workspace.fs` read mode | `readFile`, `stat`, `exists`, `list`, `glob`, `search` |
| `workspace.fs` write mode | read methods plus `writeFile`, `appendFile`, `mkdir`, `rm`, `movePath`, `copyPath` |
| writable facade | `diff`, `snapshot`, `history.checkpoint`, `history.rebase`, `materializeSources`, `sync`, `startSession`, optional Store metadata methods `getMeta` and `setMeta`, and `tools` |
| tools | default tools, `tools.inspect(options)`, `tools.write(options)`, `tools.none()` |
### Runtime method options
| Method | Options | Behavior |
| -------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readFile(path, options?)` | `encoding?: 'utf8' | 'binary'` | Defaults to UTF-8 text; binary reads return `Uint8Array`. |
| `writeFile(path, content, options?)` | `mediaType?`, `metadata?` | Writes string or binary content with optional file metadata. |
| `list(path?, options?)` | `recursive?: boolean` | Lists direct children or the complete subtree. |
| `glob(pattern, options?)` | `cwd?: string` | Matches one pattern or an array relative to an optional Workspace directory. |
| `search(query)` | `pattern`, `cwd?`, `paths?`, `regex?`, `caseSensitive?`, `limit?` | Searches text. Defaults to a case-insensitive literal pattern with a limit of `100`. |
| `mkdir(path, options?)` | `recursive?: boolean` | Creates a Workspace directory. |
| `rm(path, options?)` | `recursive?: boolean`, `force?: boolean` | Removes a file or directory under the active write policy. |
| `movePath(from, to, options?)` | `overwrite?: boolean` | Moves a path. Existing destinations fail unless `overwrite` is enabled. |
| `copyPath(from, to, options?)` | `overwrite?: boolean` | Copies a path. Existing destinations fail unless `overwrite` is enabled. |
| `snapshot(options?)` | `name?: string` | Captures the current Workspace tree with an optional snapshot name. |
| `history.rebase(options?)` | `takeRemote?: string[]` | Reloads a remote Store while preserving staged paths. A listed path takes its remote version only when both sides changed; any other overlapping change remains a conflict. |
| `diff(options?)` | `from?: WorkspaceSnapshot` | Compares the current tree with the supplied snapshot or the Store baseline. |
| `materializeSources(options?)` | `abortSignal?`, `onProgress?`, `sources?`, `path?` | Materializes every Source or a selected Source/path subset, with cancellation and progress reporting. |
| `getMeta(key)` / `setMeta(key, value)` | Store-defined | Reads or writes optional Workspace Store metadata when the configured Store implements it. |
## Resolve custom Sources
Use `custom({ files })` when a Custom Source knows its Workspace paths before it loads their content. The shorthand enumerates those paths without resolving content, and path-scoped materialization resolves only the requested file's content callback.
Invocation-aware resolution belongs to Source helpers and custom Source definitions, not to the Source Binding wrapper. Use resolver forms such as `fetch(resolver)` or the resolver accepted by the relevant helper, then add binding behavior such as `mount`, `cache`, or `sync` around the result.
```ts [server/workspaces/support.ts]
import { custom, defineWorkspace } from '@vite-hub/workspace'
const guideSlugs = ['getting-started', 'inventory-planning']
export default defineWorkspace({
sources: {
guides: custom({
cache: { maxAge: 3600 },
materialize: 'lazy',
files: guideSlugs.map(slug => ({
path: `${slug}.md`,
async content() {
const response = await fetch(`https://docs.example.com/${slug}.md`)
if (!response.ok)
throw new Error(`Failed to load ${slug}: ${response.status}`)
return await response.text()
},
})),
}),
},
})
```
ViteHub infers each file's media type from its path unless the descriptor provides `mediaType`. Use a full Custom Source with `getKeys()` and `getItem()` when retrieval needs behavior beyond a fixed file list.
Custom Sources can read existing materialized Workspace files through `ctx.workspaceFiles`. Use this when a Source needs previous generated output, such as a sync report or cached asset metadata, while producing the next materialized files. The view is read-only and does not expose Workspace Stores, provider adapters, snapshots, diffs, or Source materialization.
Sources can resolve their origin and mount for one invocation from trusted runtime context. Use this when the same Source key needs a narrower origin after Access selects a Workspace Scope.
```ts
declare global {
interface ViteHubWorkspaceSourceResolutionContextMap {
channel: { meta?: { customer?: string } }
}
}
github(({ channel, invocation }) => {
const scope = invocation.context.get<{ customers: string[] }>('support.customerScope')
const customer = channel?.meta?.customer ?? scope?.customers[0]
if (!customer)
return false
return {
repo: 'quiverdk/ingestion',
root: `dbt/${customer}`,
mount: `ingestion/${customer}`,
}
})
```
The resolver receives registered invocation context values directly and through `invocation.context`. Register application values through `ViteHubWorkspaceSourceResolutionContextMap`; the Agent package registers `channel` automatically. The resolver reads trusted Agent Invocation Context Values and the selected Workspace Scope, not model output. `access()` still controls authorization, and its selected scope must grant the Source key or Workspace path. ViteHub fingerprints options that affect scope so Source caches don't reuse data across scopes.
Resolved Sources are evaluated at invocation time and default to lazy materialization. A resolver can return a narrowed GitHub `repo`, `root`, and `mount` without also declaring build-time materialization or cache options; the resolved fingerprint includes the Selected Workspace Scope so one scope cannot reuse another scope's source data.
## Sync Sources
Workspace Source Sync copies selected Source-backed paths into the Workspace Store when the Source sync policy permits it.
Only Sources declared with `sync: true` or a sync policy participate in runtime `workspace.sync()`.
```ts [server/tasks/sync-docs.ts]
import { useWorkspace } from '@vite-hub/workspace'
export async function syncDocs() {
const workspace = useWorkspace('docs', { mode: 'write' })
return workspace.sync({
sources: ['handbook'],
snapshot: { message: 'Sync handbook source' },
})
}
```
Build and development integrations materialize Sources at build time. Runtime `sync()` copies Sources into Workspace Stores while the app runs.
### Source sync policy
| Option | Type | Default | Behavior |
| ------------------ | ------------------------------------- | ------- | ----------------------------------------------------------------------------------------------- |
| `sync` | `boolean | WorkspaceSourceSyncPolicy` | `false` | `true` enables sync with default policy; an object configures concurrency and stale paths. |
| `sync.concurrency` | `skip | queue` | `queue` | Queues behind an active sync for the same Source, or reports the overlapping Source as skipped. |
| `sync.stale` | `keep | remove` | `keep` | Keeps files no longer returned by the Source, or removes them during reconciliation. |
### `workspace.sync()` options
| Option | Type | Default | Behavior |
| ---------------- | ------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `sources` | `all | readonly string[]` | required | Selects all sync-enabled Sources or explicit Source keys. |
| `details` | `counts | paths` | `counts` | Returns per-Source counts, with optional per-path results. |
| `snapshot` | `boolean | { name?, message? }` | `false` | Creates a snapshot after successful reconciliation. A message is used as the snapshot name when `name` is absent. |
| `publish` | `boolean` | `false` | Publishes the resulting snapshot through configured Workspace Publishers; enabling it also creates a snapshot. |
| `publishPartial` | `boolean` | `false` | Applies and optionally publishes successful Source plans even when another selected Source fails. |
Source Sync requires a Workspace Store with metadata support. Without `publishPartial`, any planning error skips all otherwise valid plans so the sync does not apply only part of the requested selection.
## Use sessions and Shell
Use a Workspace Session when a command needs a materialized file tree and must produce a diff.
`session.exec()` requires an open Box Session. Workspace handles materialization, diff, commit, and rollback. Box runs the command and manages its lifecycle.
```ts [server/tasks/test-docs.ts]
import { resolveBox } from '@vite-hub/box'
import { useWorkspace } from '@vite-hub/workspace'
export async function testDocs() {
const box = await resolveBox({ runtime: 'trusted-host' }, undefined)
const host = await box.open()
const session = await useWorkspace('docs', { mode: 'write' }).startSession({ host })
try {
await session.exec('pnpm', ['test'])
return await session.diff()
}
finally {
await session.close()
await host.close()
}
}
```
### Session method options
`startSession(options)` combines Workspace state with an open Box Session. `host` is required for execution. `paths` limits materialization and commits, and `target` defaults to `/workspace`. `abortSignal` cancels preparation, while `onProgress` reports materialization phases. Closing the Workspace Session doesn't close the Box host.
Set `writeBack.exclude` to Workspace-relative paths owned by the runtime rather than the invocation. Excluded paths remain usable in the host tree, but their changes are omitted from `diff()` and `commit()` and their pre-Session state is restored by `close()`. Set `writeBack: false` when the runtime must remain writable but its changes must never be published. That mode disables `diff()` and `commit()` and restores the authoritative Workspace on close without first scanning the runtime tree. Read-only Agent Workspaces select it automatically. ViteHub always applies the same excluded-path behavior to `.agent-runs`, `.git`, and `.vitehub`. Integrations that already own a live materialized tree can set `attach: true`; the Session preserves pre-existing live edits, never rematerializes the whole tree, and rolls back only its own uncommitted changes on close.
| Method | Options | Behavior |
| ---------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `readFile`, `writeFile`, `mkdir`, `rm`, `list`, `glob`, `search` | Same file options as the Workspace filesystem | Operate inside the Session path scope. |
| `exec(command, args?, options?)` | `abortSignal?`, `cwd?`, `env?`, `timeout?` | Runs through the supplied Box Session, defaulting cwd to the Workspace target. Basic Sessions without a host reject this method. |
| `diff()` | none | Returns changes inside the Session path scope. |
| `commit(options?)` | `message?: string` | Writes Session changes back and snapshots them with an optional message. |
| `close()` | none | Rolls an uncommitted host tree back to authoritative Workspace state and releases Session resources. |
| `tools?.aiSdk()` | none | Returns runtime-provided AI SDK tools when the Session supports them. |
### Mount a session with MountX
Use `@vite-hub/workspace/mountx` when an Agent, editor, CLI, or VM needs a filesystem path or protocol instead of Workspace methods. ViteHub keeps the transactional session and commit step. MountX exposes that session through local FUSE, 9P, NFS, or S3 transports.
Install MountX directly before importing its transport entry points:
```bash
pnpm add mountx@0.0.2
```
```ts [server/tasks/edit-docs.ts]
import { createWorkspaceDriver } from '@vite-hub/workspace/mountx'
import { mount } from 'mountx/auto'
import { useWorkspace } from '@vite-hub/workspace'
const session = await useWorkspace('docs', { mode: 'write' }).startSession()
try {
const mounted = await mount(createWorkspaceDriver(session), '/tmp/vitehub-docs')
try {
// Any local program can now use /tmp/vitehub-docs.
}
finally {
await mounted.unmount()
}
await session.commit({ message: 'accept projected changes' })
}
finally {
await session.close()
}
```
Pass `{ readOnly: true }` to `createWorkspaceDriver()` for inspection-only consumers. The same driver can be passed to MountX's 9P or NFS server to reach a Linux guest, or to its S3 gateway for S3-compatible clients. The adapter uses MountX's unstorage driver, so it does not project or persist empty directories, and filenames cannot contain `:`, `?`, or end in `$`. Renames use copy then delete and are not atomic. Executable Git files retain their execute bits; Git symlinks are rejected because the unstorage driver cannot preserve symlink semantics. MountX is alpha and unaudited, so keep network transports loopback-only unless the surrounding sandbox or network is the explicit security boundary.
Workspace stores the file tree and commits. Box and Sandbox provide separate execution environments. Provider Agent Drivers materialize a selected Workspace in their local working directory.
### Run sessions from the CLI
During local development, `vitehub workspace dev` runs commands through a Workspace Session exposed by the Vite development server. Use it to materialize the Workspace, run a command, and commit successful changes. Install `@vite-hub/cli` when your project uses the direct `@vite-hub/workspace` package instead of the `vite-hub` distribution.
```bash [Terminal]
pnpm vitehub workspace dev --url http://localhost:5173 docs exec pnpm test --filter api
```
`vitehub agent dev` also accepts `!` input for direct commands through the selected Agent's writable Workspace. Use `!` for local shell work in the same Workspace the Agent sees. Use normal messages when the Agent needs to reason about the task.
```bash [Terminal]
pnpm vitehub agent dev --agent support !pnpm test --filter api
```
## Provider output
The Workspace package discovers definitions, generates Workspace name types, prepares build-time assets, and connects Workspace Stores. A Workspace Store can use Blob, but application code still uses Workspace for file operations.
::note
The Nuxt Workspace handoff is only for hosted Workspace runtime setup and generated registry transport. It does not create Nitro-specific Workspace discovery, public provider store constructors, or a second Workspace authoring model.
::
Add generated types when you want `useWorkspace()` to narrow discovered Workspace names.
```json [tsconfig.json]
{
"include": [
"server/**/*.ts",
"src/**/*.ts",
".vitehub/types/**/*.d.ts"
]
}
```
## Connect Workspace to Agents
Workspace isn't automatically available to a model. Attach `workspaceShell()` when a model needs to inspect or edit files. Use `access()` when trusted invocation identity selects the Workspace Scope.
Read [Workspace and Sources](https://vitehub.dev/docs/concepts/workspace-and-sources) for the mental model and [Workspace context](https://vitehub.dev/docs/agents/workspace-context) for Agent-specific composition.
## Next steps
- Use direct retrieval through [Source](https://vitehub.dev/docs/server-primitives/source).
- Add command inspection with [Shell](https://vitehub.dev/docs/server-primitives/shell).
- Expose file access to models through [Official capabilities](https://vitehub.dev/docs/capabilities/official-capabilities).
# Attachments
Use `useAgentAttachments()` when the application needs validation or wants to keep raw `File` values until submission.
`AgentChatPrompt` accepts the resulting AI SDK file parts. This preview uses a dummy log attachment and does not upload data.
:component-preview{name="ChatPromptExample"}
```ts
const attachments = useAgentAttachments({
accept: "image/*,.pdf",
maxFiles: 5,
maxSize: 10 * 1024 * 1024,
onReject(file, reason) {
toast.add({ title: `${file.name}: ${reason}` });
},
});
async function submit(text: string) {
await sendMessage({
text,
files: await attachments.toFileParts(),
});
attachments.clear();
}
```
The composable exposes `files`, `add()`, `remove()`, `clear()`, `inputProps`, and `toFileParts()`. Image object URLs are revoked when items are removed or the owning Vue scope is disposed.
For hosted uploads, upload the raw files first and construct `FileUIPart` values with permanent URLs. Data URLs are convenient for supported model inputs but are not a persistence strategy.
# Chat
`AgentChat` accepts AI SDK `UIMessage[]` and `ChatStatus`. It renders messages, preserves reader scroll intent during streaming, and exposes slots for application-specific presentation.
::component-preview{flush name="ChatExample"}
::
## Usage
```vue [app/pages/chat.vue]
```
ViteHub's `useChat()` wrapper has the same UI boundary and supplies the ViteHub route and typed Agent metadata.
## Anatomy
```vue
```
## Props
| Prop | Type | Default |
| ------------------ | ---------------------- | ------------------- |
| `messages` | `readonly UIMessage[]` | `[]` |
| `status` | `ChatStatus` | `'ready'` |
| `edgeThreshold` | `number` | Global default `8` |
| `previousItemPeek` | `number` | Global default `64` |
The component does not call `sendMessage()`, `stop()`, or `regenerate()`. Keeping transport outside the rendering tree lets the same UI work with AI SDK, ViteHub Agent routes, persisted sessions, or replayed fixtures.
# Chat Message
`AgentChatMessage` composes Nuxt UI's `UChatMessage` with `AgentMessageParts`.
:component-preview{name="ChatMessageExample"}
## Usage
```vue
```
## Slots
| Slot | Scope | Purpose |
| ---------- | ----------------- | -------------------------------------------------- |
| `default` | `{ message }` | Replace the complete message body. |
| `header` | `{ message }` | Add files or message metadata above the body. |
| `leading` | `{ message }` | Replace the avatar or role marker. |
| `actions` | `{ message }` | Add copy, retry, feedback, or application actions. |
| Part slots | `{ part, index }` | Forwarded to `AgentMessageParts`. |
Use the `ui` prop to pass Nuxt UI slot classes to the underlying `UChatMessage`.
# Chat Prompt
`AgentChatPrompt` retains Nuxt UI's autoresize, IME handling, Enter behavior, Escape blur, error state, and submit button states. ViteHub adds an attachment row and one submit payload.
:component-preview{name="ChatPromptExample"}
```vue
sendMessage({ text, files })"
@reload="reload"
@stop="stop"
/>
```
## Events
| Event | Payload |
| ------------------- | ----------------------------------------------------------- |
| `update:modelValue` | Current prompt text. |
| `update:files` | Current `FileUIPart[]`. |
| `submit` | `{ text, files }`. Empty text is accepted when files exist. |
| `reload` | No payload. Connect it to the AI SDK `reload()` helper. |
| `stop` | No payload. Connect it to the AI SDK `stop()` helper. |
Use `#files`, `#actions`, and `#submit` to replace each built-in section without rebuilding keyboard behavior. The composer, attachment picker, and status-aware submit action have default accessible names; pass `aria-label` to override the composer name.
# Diff
ViteHub provides Vue lifecycle adapters for Pierre's six code and diff views. They use Nuxt UI backgrounds, borders, radius, typography, and semantic colors by default while Pierre continues to own parsing, syntax highlighting, selection, and rendering.
:component-preview{name="DiffExample"}
## Components
| Component | Purpose |
| --------------------- | ----------------------------------------------------- |
| `AgentCodeView` | Render a virtualized list containing files and diffs. |
| `AgentMultiFileDiff` | Compare two `FileContents` values directly. |
| `AgentPatchDiff` | Render one file change from a unified patch string. |
| `AgentFileDiff` | Render pre-parsed `FileDiffMetadata`. |
| `AgentFile` | Render one syntax-highlighted file without a diff. |
| `AgentUnresolvedFile` | Render conflict markers with resolution controls. |
Nuxt registers every component automatically. In Vue with Vite, import them from `@vite-hub/ui`.
## Usage
Render a unified patch:
```vue
```
Compare two files without creating a patch first:
```vue
```
Pass `null` for the missing side of an added or deleted file.
Use a parsed diff when the application already owns parsing or partial diff hydration:
```vue
```
Render a mixed virtualized view. Give the component a height so it can own scrolling:
```vue
```
## Shared diff props
`AgentMultiFileDiff`, `AgentPatchDiff`, and `AgentFileDiff` share these props:
| Prop | Type | Purpose |
| ----------------- | -------------------------- | --------------------------------------------------------------- |
| `options` | `FileDiffOptions` | Configure layout, themes, headers, interactions, and hydration. |
| `lineAnnotations` | `DiffLineAnnotation[]` | Render application-owned content on diff lines. |
| `selectedLines` | `SelectedLineRange | null` | Control the selected line range. |
`AgentFile` uses `FileOptions` and `LineAnnotation[]`. `AgentUnresolvedFile` uses `UnresolvedFileOptions`. `AgentCodeView` accepts `CodeViewItem[]`, `CodeViewOptions`, and a `CodeViewLineSelection`.
The package also exports Pierre's `getSingularPatch`, `parseDiffFromFile`, and `parsePatchFiles` helpers plus the public types used by these components.
## Styling
The default theme maps Pierre's inherited CSS properties to Nuxt UI's `--ui-*` properties. Override a ViteHub property on one view when a product needs a different treatment:
```css
.review-diff {
--vh-ui-bg: var(--ui-bg-elevated);
--vh-ui-success: var(--ui-primary);
}
```
Pass Pierre's `theme` option when you need different syntax token colors. The surrounding backgrounds and semantic diff colors still follow the application theme.
# File Tree
`AgentFileTree` turns a list of repository paths into an interactive `@pierre/trees` view. The direct component path owns the Pierre model and cleans it up when Vue unmounts the tree.
:component-preview{name="FileTreeExample"}
## Usage
```vue
```
## Controlled model
Use `useAgentFileTree()` when application code needs selection, search, rename, drag-and-drop, or mutation methods:
```vue
```
The composables dispose subscriptions and the model with the current Vue scope.
## Props
| Prop | Type | Default | Purpose |
| --------- | -------------------------------- | ------- | ---------------------------------------- |
| `paths` | `readonly string[]` | `[]` | Paths used to create or update the tree. |
| `options` | `Omit` | | Pierre configuration for an owned model. |
| `model` | `FileTree` | | An application-owned Pierre tree model. |
When `model` is present, it is the source of truth. Do not also use `paths` or `options` as controlled inputs.
The inner tree is named `Files` by default. Pass `aria-label` when the surrounding context calls for a more specific name.
# UI
`@vite-hub/ui` is the interface layer for ViteHub applications. It combines AI SDK message contracts, Nuxt UI styling, reusable Vue behavior, and Pierre's code views without replacing any of those foundations.
::u-page-grid{.not-prose.mt-8.sm:grid-cols-2}
:::u-page-card
---
description: Configure the Nuxt module or Vite plugin and load the default styles.
icon: i-lucide-package
title: Install the package
to: https://vitehub.dev/docs/ui/installation
---
:::
:::u-page-card
---
description: Connect an AI SDK or ViteHub chat directly to the component layer.
icon: i-ph-chat-circle-text-light
title: Render a chat
to: https://vitehub.dev/docs/ui/chat
---
:::
:::u-page-card
---
description: Render text, reasoning, tools, files, sources, and typed data parts.
icon: i-lucide-blocks
title: Customize message parts
to: https://vitehub.dev/docs/ui/message-parts
---
:::
:::u-page-card
---
description: Browse sessions, render invocation activity, and inspect the
captured configuration.
icon: i-ph-activity-light
title: Inspect Agent work
to: https://vitehub.dev/docs/ui/invocation
---
:::
::
## Layers
| Layer | Owns |
| ------------ | ---------------------------------------------------------------------------------------------- |
| AI SDK | `UIMessage`, `ChatStatus`, streaming state, tool parts, and transport helpers. |
| Headless Vue | Scroll intent, live-edge following, prepend preservation, and message jumps. |
| Nuxt UI | Theme tokens and established chat, prompt, reasoning, tool, button, and badge components. |
| ViteHub UI | Part dispatch, defaults, Markdown presentation, attachments, Agent inspection, and code views. |
| Pierre | Diff rendering and path-first file trees. |
The package does not own chat transport. Use `useChat()` from `@ai-sdk/vue` or the ViteHub wrapper from `vite-hub/agent/vue`, then pass its reactive values to the UI.
## Components
- [Chat](https://vitehub.dev/docs/ui/chat) and [Chat message](https://vitehub.dev/docs/ui/chat-message)
- [Session](https://vitehub.dev/docs/ui/session)
- [Message parts](https://vitehub.dev/docs/ui/message-parts) and [Markdown](https://vitehub.dev/docs/ui/markdown)
- [Chat prompt](https://vitehub.dev/docs/ui/chat-prompt) and [attachments](https://vitehub.dev/docs/ui/attachments)
- [Message scroller](https://vitehub.dev/docs/ui/message-scroller)
- [Invocation list](https://vitehub.dev/docs/ui/invocation-list), [invocation](https://vitehub.dev/docs/ui/invocation), and [invocation inspector](https://vitehub.dev/docs/ui/invocation-inspector)
- [Diff](https://vitehub.dev/docs/ui/diff), [file tree](https://vitehub.dev/docs/ui/file-tree), and [trace](https://vitehub.dev/docs/ui/trace)
# Installation
## Nuxt
Install the package and its public peers:
```bash
pnpm add @vite-hub/ui @nuxt/ui ai tailwindcss vue
```
Register the module. It installs Nuxt UI, registers ViteHub UI components, and includes the package stylesheet.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ["@vite-hub/ui/nuxt"],
});
```
Components such as `AgentChat`, `AgentChatPrompt`, and `AgentInvocation` are auto-imported.
## Vue with Vite
Use the Vite integration together with the Vue plugin:
```ts [vite.config.ts]
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import viteHubUI from "@vite-hub/ui/vite";
export default defineConfig({
plugins: [vue(), ...viteHubUI()],
});
```
```ts [src/main.ts]
import { createApp } from "vue";
import { createViteHubUI } from "@vite-hub/ui";
import NuxtUI from "@nuxt/ui/vue-plugin";
import "./assets/main.css";
import App from "./App.vue";
createApp(App)
.use(NuxtUI)
.use(createViteHubUI())
.mount("#app");
```
```css [src/assets/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@import "@vite-hub/ui/styles.css";
```
The Vite integration configures Nuxt UI and Comark. Register imported ViteHub components locally or through your preferred component auto-import plugin.
## Defaults
Set package-wide behavior through the Vue plugin or Nuxt module options:
```ts
createViteHubUI({
defaults: {
markdown: { class: "vh-typeset vh-typeset-chat my-markdown" },
messageScroller: { edgeThreshold: 12, previousItemPeek: 72 },
},
});
```
CSS variables such as `--vh-ui-border`, `--vh-ui-bg-elevated`, and `--vh-ui-radius` fall back to Nuxt UI tokens and remain overridable by the application.
# Invocation
`AgentInvocation` turns append-only observations into a coding-session thread. Assistant prose stays unlabelled, user prompts remain visually distinct, and commands, reasoning, tool activity, and file changes expand in place.
::component-preview{flush name="InvocationExample"}
::
## Usage
```vue
```
Set `header` to `false` when the host already renders repository and session navigation above the thread.
## Props and events
| Contract | Type | Purpose |
| ------------ | ------------------------- | ----------------------------------------------------- |
| `invocation` | `AgentInvocationView` | Authorized invocation state and observations. |
| `header` | `boolean`, default `true` | Shows the project and session breadcrumb. |
| `inspect` | `'agent' | 'workspace'` | Requests host-owned inspection for a selected target. |
The `title`, `actions`, and `footer` slots add host controls without changing the transcript renderer.
## Trace content
Rich replay requires a trace log created with `{ content: "content" }`. The default metadata-only policy records activity milestones but strips prompts, message text, tool input, and tool output.
Enable full-content traces only when the store and current viewer may retain and inspect that session content. Agent Invocation journals bound each content string to 64 KiB, each metadata string to 512 characters, collections to 32 items, nesting to four levels, and observations to 256 per invocation.
## Data ownership
The component receives already-authorized data. The application owns loading, polling, realtime updates, replay policy, and authorization.
# Invocation Inspector
`AgentInvocationInspector` presents the configuration captured for one Agent Invocation. Its narrow layout works in a splitter, drawer, or standalone details panel.
:component-preview{name="InvocationInspectorExample"}
## Usage
```vue
```
The inspector keeps the outcome visible, summarizes the run, and groups the captured Agent setup below it. Sources and tools stay compact, while Capability metadata and instructions expand in place. Terminal errors appear with the exact invocation status. Identifiers remain hidden until copied.
## Captured configuration
Pass the sanitized configuration stored with the invocation:
```ts
const invocation = {
...record,
configuration: {
agent: { name: "review", version: "1.0.0" },
capabilities: [{ id: "workspace-shell" }],
driver: { kind: "provider", provider: "codex" },
instructions: [resolvedInstructions],
runtime: { name: "node" },
tools: [{ name: "exec_command" }],
workspace: { mode: "write", name: "review", sources: ["repository"] },
},
};
```
Do not reconstruct configuration from the current Agent Definition. Dynamic Capabilities, instructions, Workspace bindings, Sources, driver, and runtime may have changed since the invocation ran.
Only include instruction content when the current viewer may inspect it. The component does not fetch missing configuration or authorize access.
# Invocation List
`AgentInvocationList` renders session summaries without owning search, routes, or data fetching. It keeps every loaded session in the document so keyboard and assistive-technology users can reach the same navigation choices.
:component-preview{name="InvocationListExample"}
## Usage
```vue
```
## Item data
Each `AgentInvocationListItem` requires `id`, `status`, and `title`. Add project, repository or pull-request context, provider, Agent name, timestamps, and a terminal error description when available.
Status always appears as an icon and a label. The component does not rely on color alone.
## Props
| Prop | Type | Default | Purpose |
| ------------ | ------------------------------------ | ---------------- | ------------------------------------------------- |
| `items` | `readonly AgentInvocationListItem[]` | | Application-loaded session summaries. |
| `selectedId` | `string` | | Marks the session selected by the host. |
| `hasMore` | `boolean` | `false` | Enables the near-end pagination signal. |
| `loading` | `boolean` | `false` | Shows the loading state and pauses pagination. |
| `retryKey` | `string | number` | | Retries the current page after its value changes. |
| `now` | `number` | | Timestamp used for deterministic relative times. |
| `ariaLabel` | `string` | `Agent sessions` | Accessible label for the navigation region. |
## Pagination
The component emits `endReached` once per loaded item count when the viewport nears the end of the loaded sessions. Append the next cursor page to `items`. If loading fails, change `retryKey` from the retry action so the same item count can request another page.
Use `header`, `footer`, `empty`, and `loading` for list states. Use `projectIcon` and `harness` to replace repository and provider presentation without replacing the row behavior. Paginate large histories instead of virtualizing this navigation list.
# Markdown
`AgentMarkdown` wraps `@comark/vue` and applies the `vh-typeset vh-typeset-chat` defaults. The stylesheet uses block-start spacing, which remains stable while streaming content appends new nodes.
:component-preview{name="MarkdownExample"}
```vue
```
## Custom components
Pass Comark components and parser options directly:
```vue
```
## Styling
Override the package defaults globally or add a class per instance. The base rules deliberately avoid styling application chrome; they cover prose rhythm, headings, lists, links, inline code, code blocks, and blockquotes.
```css
.support-answer {
--vh-typeset-flow: 1em;
--vh-typeset-leading: 1.7;
}
```
# Message Parts
`AgentMessageParts` dispatches the parts already present in an AI SDK `UIMessage`. Text uses `AgentMarkdown`; reasoning and tools use Nuxt UI; files and sources use accessible links.
:component-preview{name="MessagePartsExample"}
## Supported parts
| Part | Default rendering |
| ------------------------------------ | --------------------------------------------------------- |
| `text` | Streaming-aware Comark Markdown. |
| `reasoning` | `UChatReasoning`. |
| `tool-*`, `dynamic-tool` | `UChatTool` with input, output, error, and loading state. |
| `file` | Named download or image preview. |
| `source-url`, `source-document` | Source link or document label. |
| `step-start` | No visual output unless slotted. |
| `data-*`, `custom`, `reasoning-file` | `fallback` slot. |
## Customize typed data
```vue
```
Use `#part` to intercept every part before the default dispatcher. Use the narrower `#text`, `#reasoning`, `#tool`, `#file`, `#source`, and `#step` slots when only one rendering needs to change.
# Message Scroller
The message scroller is the headless layer of the package. It follows streaming output only while the reader remains at the live edge, preserves position when older messages prepend, and can jump to a stable message ID.
:component-preview{name="MessageScrollerExample"}
## Anatomy
```vue
{{ message }}
```
Import primitives from `@vite-hub/ui/headless` when you do not want the styled chat component.
## Root props
| Prop | Type | Default |
| ----------------------- | ----------------- | ------- |
| `autoScroll` | `boolean` | `true` |
| `defaultScrollPosition` | `'start' | 'end'` | `'end'` |
| `edgeThreshold` | `number` | `8` |
| `previousItemPeek` | `number` | `64` |
## Composable
`useMessageScroller()` exposes reactive `atEnd` and `isScrollable` values plus `scrollToEnd()` and `scrollToMessage(id)`. Call it under `MessageScrollerRoot`.
## Accessibility
The viewport is a labelled, keyboard-scrollable region and the content is an additions-only log. `AgentChat` marks that log busy while a response is submitted or streaming, which prevents partial updates from being announced as settled content.
`MessageScrollerButton` has a default accessible label and uses a native button by default. It stays mounted but inert at the live edge, moves focus back to the viewport when activated, and replaces smooth scrolling with instant scrolling when the reader requests reduced motion.
# Session
AI SDK defines UI messages and chat state, but it does not impose a persistence schema for sessions. `ViteHubUISession` therefore stays deliberately small: an ID, messages, optional title and timestamps, and application metadata.
::component-preview{flush name="SessionExample"}
::
```ts
const session: ViteHubUISession = {
id: "session_01",
title: "Production deploy failure",
messages,
metadata: { projectId: "project_01" },
};
```
Render it with `AgentSession`:
```vue
```
The component adds session structure around `AgentChat`; it does not fetch, mutate, or persist the record. Applications can extend the session type and retain full control of tenancy and authorization.
# Trace
Use `AgentTrace` when the application already called `deriveTraceRuns()` and wants a compact disclosure for one derived run.
:component-preview{name="TraceExample"}
## Usage
```vue
```
## Props
| Prop | Type | Default | Purpose |
| ------------- | -------------- | ------- | ------------------------------------------- |
| `run` | `TraceRunView` | | Derived run status, duration, and steps. |
| `defaultOpen` | `boolean` | `false` | Opens the run disclosure on initial render. |
Use the `title` slot to replace the run identifier. Use `step` when a known trace schema deserves a richer presentation than the default step name, duration, and attributes.
## Ownership
ViteHub Runtime derives `TraceRunView` records. The UI component only renders the run it receives. Loading, filtering, authorization, and retention stay in the application.
# Build your first Server Primitive
Server Primitives give application code a stable API for infrastructure such as
KV, Blob, Database, Queue, Workflow, Schedule, Sandbox, and Workspace. You
select the provider at the Vite Integration boundary instead of spreading a
provider SDK through product code.
This tutorial adds KV to a small H3 server. One request writes a value, reads it
back, and returns the stored result. The first proof stays local and requires no
account or credential.
::note
You need Node.js 24.15 or newer and
`pnpm`
. The tutorial uses H3 as the HTTP server
and Node.js as the host so every runtime boundary remains visible.
::
## Create the project
Create an empty directory and install ViteHub with the small server used in
this tutorial.
```bash [Terminal]
mkdir vitehub-kv-start
cd vitehub-kv-start
pnpm init
pnpm pkg set type=module
pnpm add vite-hub h3 vite
```
Your project ends with two source files:
```txt [Project]
vitehub-kv-start/
├── src/
│ └── server.ts
└── vite.config.ts
```
## Select the local KV store
Register `vitehub()` in Vite and select the `fs-lite` driver explicitly. ViteHub
stores local values under `.vitehub/data/kv`, while server code only sees the `kv`
Runtime Helper.
```ts [vite.config.ts]
import { resolve } from "node:path"
import { defineConfig } from "vite"
import { vitehub } from "vite-hub"
export default defineConfig({
root: import.meta.dirname,
appType: "custom",
build: {
outDir: "dist",
rollupOptions: {
input: resolve(import.meta.dirname, "src/server.ts"),
output: { entryFileNames: "server.js" },
},
ssr: true,
},
plugins: [
vitehub({
preset: "node",
blob: false,
env: false,
kv: { driver: "fs-lite", base: ".vitehub/data/kv" },
}),
],
})
```
The Vite build compiles one Node.js server entry. `appType: "custom"` tells
Vite that H3, rather than an HTML page, owns the application response.
## Write and read one value
Create one H3 route. The handler writes the request body to `settings`, then
reads the same key before returning.
```ts [src/server.ts]
import { createServer } from "node:http"
import { H3, readBody } from "h3"
import { toNodeHandler } from "h3/node"
import { kv } from "vite-hub/kv"
const app = new H3().post("/settings", async (event) => {
const settings = await readBody<{ theme: string }>(event)
const [writeError] = await kv.set("settings", settings)
if (writeError) throw writeError
const [readError, storedSettings] = await kv.get("settings")
if (readError) throw readError
return { settings: storedSettings }
})
const port = Number(process.env.PORT || 5173)
createServer(toNodeHandler(app)).listen(port, () => {
console.log(`ViteHub KV tutorial listening on http://localhost:${port}`)
})
```
H3 owns HTTP parsing and Node.js owns the listening socket. ViteHub owns the KV
configuration and the stable storage API inside the route.
## Run the proof
Build the server, then start the generated entry.
```bash [Terminal]
pnpm vite build
node dist/server.js
```
Send one request from another terminal.
```bash [Terminal]
curl -X POST http://localhost:5173/settings \
-H 'content-type: application/json' \
-d '{"theme":"system"}'
```
The server returns the value it read through ViteHub:
```json [Response]
{"settings":{"theme":"system"}}
```
The value survives a server restart because the selected driver writes to
`.vitehub/data/kv`. Delete that directory whenever you want to reset the local store.
## What you built
The request crosses three explicit boundaries:
1. H3 receives the HTTP request and parses its JSON body.
2. The `kv` Runtime Helper reads and writes application state.
3. `vitehub()` resolves the concrete store during the Vite build.
Changing providers belongs in `vite.config.ts`. The route keeps importing
`kv` from `vite-hub/kv` when you move to a supported hosted store.
::tip
Keep the local driver while you shape the feature. Choose a hosted driver only
when you know the deployment target and its persistence requirements.
::
## Continue from here
- Read [KV](https://vitehub.dev/docs/server-primitives/kv) for named stores and hosted drivers.
- Read [Runtime Helpers and stable imports](https://vitehub.dev/docs/concepts/runtime-helpers-and-stable-imports) to see how provider changes stay out of server code.
- Build [your first Agent](https://vitehub.dev/blog/agents) when the product needs an actor that can invoke models, coding providers, or application-owned logic.
# Build your first Agent
An Agent Definition names a server-side actor and the Agent Driver that runs
it. The driver can call a model, launch a coding provider, or execute
application-owned logic through the same Agent Invocation boundary.
This tutorial starts with application-owned logic. The deterministic first
proof needs no key, network request, or model billing. After the invocation
works, you can replace the driver with an AI SDK model without changing the
HTTP route.
::note
You need Node.js 24.15 or newer and
`pnpm`
. The first half of the tutorial is
offline and free to run.
::
## Create the project
Create an empty directory and install ViteHub with the H3 server used in this
tutorial.
```bash [Terminal]
mkdir vitehub-agent-start
cd vitehub-agent-start
pnpm init
pnpm pkg set type=module
pnpm add vite-hub h3 vite
```
Create this small file tree:
```txt [Project]
vitehub-agent-start/
├── server/
│ └── agents/
│ └── greeting.ts
├── src/
│ └── server.ts
└── vite.config.ts
```
## Configure the server build
Register `vitehub()` so the framework discovers the Agent Definition and owns
the server integration. The route imports the Definition directly for the
smallest invocation proof.
```ts [vite.config.ts]
import { resolve } from "node:path"
import { defineConfig } from "vite"
import { vitehub } from "vite-hub"
export default defineConfig({
root: import.meta.dirname,
appType: "custom",
build: {
outDir: "dist",
rollupOptions: {
input: resolve(import.meta.dirname, "src/server.ts"),
output: { entryFileNames: "server.js" },
},
ssr: true,
},
plugins: [vitehub({
preset: "node",
agent: true,
blob: false,
env: false,
})],
ssr: {
external: ["vite-hub/agent"],
},
})
```
The Vite build compiles one Node.js server entry and leaves the Agent package
as a Node.js runtime dependency. H3 owns HTTP behavior, while ViteHub owns the
Agent Definition and Agent Invocation contract.
## Define a deterministic Agent
Create a `greeting` Agent with a `driver.run` function. Application-owned
drivers are useful for deterministic orchestration, migrations, and testing
the Agent boundary before you introduce a provider.
```ts [server/agents/greeting.ts]
import { defineAgent } from "vite-hub/agent"
export default defineAgent({
description: "Returns a deterministic greeting for the first tutorial.",
runtime: false,
driver: {
run({ prompt }) {
const name = typeof prompt === "string" ? prompt : "friend"
return {
text: `Hello, ${name}. This result came from an Agent Invocation.`,
}
},
},
})
```
The route imports this Definition directly and needs the completed result for
its HTTP response, so `runtime: false` opts out of the hosted Workflow default.
The file location still keeps the Agent visible and reviewable before any
invocation starts.
## Run one Agent Invocation
Create an H3 route that passes trusted host context separately from invocation
input. `memo` scopes lazily created values to this invocation, while
`waitUntil` receives background work that may finish after the main result.
First create the invocation-scoped memoizer. A factory runs at most once for a
key, and each request gets a fresh cache.
```ts [src/memo.ts]
export function createMemo() {
const values = new Map()
return (key: string, create: () => T): T => {
if (!values.has(key)) values.set(key, create())
return values.get(key) as T
}
}
```
```ts [src/server.ts]
import { createServer } from "node:http"
import { H3, readBody } from "h3"
import { toNodeHandler } from "h3/node"
import { runAgent } from "vite-hub/agent"
import greeting from "../server/agents/greeting"
import { createMemo } from "./memo"
const app = new H3().post("/greet", async (event) => {
const body = await readBody<{ name?: string }>(event) || {}
return await runAgent(greeting, {
memo: createMemo(),
runtime: "vite",
waitUntil: task => { void task.catch(error => console.error(error)) },
}, {
prompt: body.name?.trim() || "friend",
})
})
const port = Number(process.env.PORT || 5173)
createServer(toNodeHandler(app)).listen(port, () => {
console.log(`ViteHub Agents tutorial listening on http://localhost:${port}`)
})
```
Production integrations forward `waitUntil` to the host when that host exposes
a background-lifetime primitive. This standalone Node.js example handles any
background rejection locally.
## Run the proof
Build and start the server.
```bash [Terminal]
pnpm vite build
node dist/server.js
```
Invoke the Agent from another terminal.
```bash [Terminal]
curl -X POST http://localhost:5173/greet \
-H 'content-type: application/json' \
-d '{"name":"Ada"}'
```
The deterministic Agent returns an inspectable result:
```json [Response]
{"text":"Hello, Ada. This result came from an Agent Invocation."}
```
You now have a real Agent Definition and a real Agent Invocation. You have not
paid for a model call or hidden the runtime context behind framework globals.
## Upgrade the driver to a model
The model-backed path materializes model strings through AI Gateway. An Agent Definition can also return `{ id, apiKey }` for invocation-aware credentials or accept any compatible concrete AI SDK model.
Add the Gateway credential to your local environment.
```dotenv [.env]
AI_GATEWAY_API_KEY=your_key_here
```
::warning
The model-backed version sends prompts over the network and may create provider
charges. Configure AI Gateway billing and keep
`AI_GATEWAY_API_KEY`
on the
server before you run it.
::
Replace the deterministic Agent Driver with a model and durable instructions.
```ts [server/agents/greeting.ts]
import { defineAgent } from "vite-hub/agent"
export default defineAgent({
description: "Greets a person with one warm, concise sentence.",
driver: {
instructions: "Greet the named person in one warm, concise sentence.",
model: "openai/gpt-5.1-mini",
},
})
```
Rebuild the server, then restart it with Node.js loading the local environment
file explicitly.
```bash [Terminal]
pnpm vite build
node --env-file=.env dist/server.js
```
Repeat the request. The response still exposes a `text` field, but the wording
now comes from the selected model.
The route does not change because `runAgent()` owns the invocation boundary.
The Agent Definition remains the place to review the driver, instructions, and
future Capabilities.
## Add abilities only when needed
Capabilities give an Agent Driver explicit abilities such as Workspace
inspection, KV access, Blob storage, shell execution, or MCP tools. Model,
provider, and application-owned Drivers receive the selected tool set through
their own Driver boundary. Start with the smallest Capability set that the
product needs; the first Agent does not need any.
## Continue from here
- Read [Agent Definitions](https://vitehub.dev/docs/agents/agent-definitions) for drivers, hooks, and runtime options.
- Read [Invocations](https://vitehub.dev/docs/agents/invocations) for trusted context, streaming, and failure handling.
- Read [Capabilities](https://vitehub.dev/docs/capabilities) before exposing tools or data to an Agent Driver.
- Build [your first Server Primitive](https://vitehub.dev/blog/server-primitives) when application code needs infrastructure without an Agent.
# About ViteHub
ViteHub is an open-source project for Vite applications that need server behavior without tying application code to one framework or deployment provider. It supplies Server Primitives for storage, databases, queues, workflows, schedules, sandboxes, email, authentication, and other runtime needs. Developers can call those primitives directly from server code or compose them into Agent Definitions with explicit Capabilities, Workspaces, Sources, Triggers, and Channels.
## What the project is for
ViteHub is useful when a team wants one inspectable contract for local development and supported production hosts. Definitions stay in the repository. Generated Provider Output can be inspected before deployment. Runtime Helpers keep provider bindings out of application code. The same approach lets a coding agent read the installed types, generated files, command help, and public documentation instead of relying on a private dashboard.
ViteHub is a framework and package ecosystem, not a managed agent service. Installing ViteHub does not create an account on vitehub.dev or send application data to a shared ViteHub runtime. Each application chooses its own hosts, model providers, storage providers, credentials, security rules, and operational controls.
## Open development
The source code, issue tracker, releases, and contribution history live in the [ViteHub GitHub repository](https://github.com/vite-hub/vitehub){rel=""nofollow""}. Packages are published under the `vite-hub` and `@vite-hub/*` names on npm. The project uses the Apache License 2.0. Public documentation at vitehub.dev covers the current contract, including supported hosts and the limits that remain provider-specific.
For questions, bug reports, documentation corrections, or private security reports, use the routes listed on the [ViteHub contact page](https://vitehub.dev/contact).
# Contact ViteHub
ViteHub is maintained as an open-source project. It does not operate a sales desk, paid support queue, or hosted customer account system. Choose a channel based on what you need so maintainers and contributors receive enough context to act without moving sensitive information into a public thread.
## Questions and implementation help
Use the [ViteHub Discord community](https://discord.gg/YTRDsRP3){rel=""nofollow""} for implementation questions, design discussion, and help choosing between Server Primitives and Agents. Include the installed ViteHub package version, the host or framework, the expected behavior, and a minimal example when possible. Remove tokens, personal data, private repository content, and provider credentials before posting.
## Bugs and documentation
Open a [GitHub issue](https://github.com/vite-hub/vitehub/issues){rel=""nofollow""} for a reproducible bug or a documentation gap. Search existing issues first, then describe the observed behavior, the expected contract, and the smallest command or repository that reproduces it. Documentation fixes can also be proposed as pull requests against the public repository.
## Security reports
Do not publish a suspected vulnerability in Discord or a public issue. Use a [private GitHub security advisory](https://github.com/vite-hub/vitehub/security/advisories/new){rel=""nofollow""} so the report, proof, and remediation discussion remain private until maintainers can assess the impact. Include affected versions and a safe reproduction. Do not include credentials or data taken from systems you do not own or have permission to test.
ViteHub does not publish a support phone number or postal office. If the project adopts a legal entity, mailing address, or dedicated support email, this page and the site's structured identity data should be updated together.
# ViteHub privacy
This notice covers the public documentation site at vitehub.dev. It does not cover applications that developers build with ViteHub. Those applications choose their own hosts, model providers, databases, telemetry, authentication, retention rules, and privacy terms. Installing a ViteHub package does not create a vitehub.dev account or route application data through a shared ViteHub service.
## Data handled by this site
The documentation site does not provide user accounts, checkout, advertising, or a contact form. Like any public website, its hosting and network providers process request information needed to deliver and protect the site. That information can include an IP address, request time, requested URL, user agent, protocol details, and security signals. Operational logs may retain some of that information according to provider configuration and policy.
The site exposes documentation search, raw Markdown, a public MCP endpoint, and other machine-readable resources. Requests to those endpoints are ordinary site requests and can appear in operational logs. Do not send secrets, private source code, personal data, or production credentials in a documentation search or MCP request.
## Local preferences and external sites
The rendered documentation can store interface preferences such as color mode in the browser. Links to GitHub, npm, Discord, deployment providers, and other third-party sites leave vitehub.dev. Those services receive the request and apply their own privacy terms. Review their policies before signing in or sharing information.
## Access and corrections
ViteHub cannot inspect or delete data held by an application that merely uses ViteHub packages. Contact that application's operator for its records. For a question about this documentation site or a correction to this notice, use the [ViteHub contact page](https://vitehub.dev/contact). Report suspected security problems through the private channel listed there rather than placing sensitive details in a public issue.
This notice should be updated whenever vitehub.dev adds accounts, forms, analytics, hosted application processing, or a new data provider. Repository history provides the public record of changes to this page.