# ViteHub > Portable Agents for Vite, built on Server Primitives. ## Resources for Agents - [API catalog: every service document this site publishes](https://vitehub.dev/.well-known/api-catalog) - [Sitemap (XML)](https://vitehub.dev/sitemap.xml) - [Sitemap (Markdown): every page on the site](https://vitehub.dev/sitemap.md) - [MCP server card: MCP endpoint at https://vitehub.dev/mcp](https://vitehub.dev/.well-known/mcp/server-card.json) - [MCP endpoint (streamable HTTP)](https://vitehub.dev/mcp) - [llms.txt: index of the documentation for LLMs](https://vitehub.dev/llms.txt) - [llms-full.txt: the full documentation as a single file](https://vitehub.dev/llms-full.txt) - [Agent skills index: every skill published by this site](https://vitehub.dev/.well-known/skills/index.json) - [Agent skill: code-review](https://vitehub.dev/.well-known/skills/code-review/SKILL.md) - [Agent skill: resolving-merge-conflicts](https://vitehub.dev/.well-known/skills/resolving-merge-conflicts/SKILL.md) - [Agent skill: vitehub](https://vitehub.dev/.well-known/skills/vitehub/SKILL.md) - [OpenAPI document: every route this site serves to agents](https://vitehub.dev/openapi.json) Every page on this site is available as raw markdown: append `.md` to its URL or send `Accept: text/markdown`. # ViteHub documentation > Server Primitives and Agents for Vite applications, one product section each, deployed on the host you select. ViteHub adds server features to Vite applications. Each **Server Primitive** is one product with one import, one configuration, and one docs section. Trusted application code calls it directly. An **Agent** can receive selected operations from a primitive through its Agent capability page. ```bash [Terminal] pnpm add vite-hub ``` Register `vitehub()` in `vite.config.ts`, select the primitives the application uses, then call the documented import from server code. The [installation guide](https://vitehub.dev/docs/getting-started/installation) covers Vite, Nuxt, and the owner packages. Every product section uses the same pages in the same order: Overview, Get started, Configure, Server API, Agent capability when the primitive has one, Hosts, and Limits and errors. Open a product below, or press the search key to jump to any page. :docs-catalog # Agents > Define a server-side Agent, choose how it runs, and connect it to your application. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="One file under server/agents: a Driver, a Workspace, Capabilities, and Channels, run and recorded on your host."} :agent-demo :: ::product-features :product-feature-item[A model, Codex, Claude Code, typed questions, or your own function.]{icon="i-lucide-cpu" title="Bring any model or coding provider" to="https://vitehub.dev/docs/agents/agent-drivers"}:product-feature-item[A Capability wraps one primitive with a mode, a scope, and a policy. Official tools add MCP, skills, memory, and web access.]{icon="i-lucide-blocks" title="Tools are the APIs your routes already call" to="https://vitehub.dev/docs/agents/capabilities"}:product-feature-item[A Workspace holds files and Sources and persists between runs.]{icon="i-lucide-folder-git-2" title="It works in a real file tree" to="https://vitehub.dev/docs/agents/workspace-context"}:product-feature-item[A Channel starts the Invocation where the input lives.]{icon="i-lucide-radio" title="Reach it from chat, GitHub, Slack, or HTTP" to="https://vitehub.dev/docs/agents/channels"}::product-feature-item{icon="i-lucide-play-circle" title="Call it like a function, inspect it like a trace" to="https://vitehub.dev/docs/agents/invocations"} `runAgent()` returns a result; the Console shows every step. :: :product-feature-item[Babysitter repairs pull requests, waits for checks, merges, and can score repeatable Evals.]{icon="i-lucide-git-pull-request" title="Start from a working harness" to="https://vitehub.dev/docs/agents/babysitter"} :: # Agent Actors > Carry trusted caller identity into one Agent Invocation. An Agent Actor is the trusted identity of the caller for one Invocation. Capabilities use it for access rules, rate limits, and state partitions. The CLI and the Console show it when you inspect an Invocation. Pass an Actor when the Agent must know who is calling. Your application authenticates the request. The Actor carries the result of that check into the Agent. A Channel user or a chat user object is not an authorization decision. ::note The public API keeps the name `invoker`. Configure Actors with `defineAgent({ invoker })` and pass one through `context.invoker`. Callbacks receive the same value as `actor` and `invoker`. :: ## Pass a trusted Actor Authenticate first. Then pass only validated identity facts: ```ts [server/api/support.post.ts] import { runAgent } from 'vite-hub/agent' import { getRuntimeContext } from 'vite-hub/runtime/h3' import support from '../agents/support' export default defineEventHandler(async (event) => { const user = await requireAuthenticatedUser(event) const { prompt } = await readBody<{ prompt: string }>(event) const runtime = getRuntimeContext(event) try { return await runAgent(support, runtime, { prompt, context: { invoker: { id: user.id, kind: 'customer', label: user.email, meta: { customer: user.customerId }, }, }, }) } finally { await runtime.flushWaitUntil().catch(console.error) } }) ``` ViteHub trusts this server-owned value. Never copy unverified request fields into `context.invoker`. The `finally` block drains tracked background work when the host has no lifetime API. See [Runtime Context](https://vitehub.dev/docs/getting-started/concepts/runtime-context#background-work-and-cleanup). Without an Actor, ViteHub uses an anonymous Actor with the id `anonymous:`. A `chat.message` Trigger without `invoker` derives a `chat` Actor from its `user` field. ## Actor fields
Field Required Purpose
id Yes Stable identity for access, limits, state, and inspection. ViteHub rejects an empty id.
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 } . ViteHub omits an invalid value.
meta No Trusted facts that your application owns. Validate them before the Invocation.
## Configure profiles Profiles are 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 a direct call, or with top-level `invokerProfileId` for `chat.message`. An unknown id fails the Invocation. ViteHub does not fall back to another Actor. ## Normalize the Actor Use `resolve` to normalize or reject the trusted input. ViteHub calls it before Capabilities and the Driver 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 }, }) ``` `defaultInvoker` is the Actor from the input, or the anonymous Actor. `selectedProfile` is the profile that the input selected. Return `undefined` to keep the default selection. ## Use Actors for access Actor metadata can select a Workspace scope. Keep the authorization rule in code, not in the model: ```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({ actor }) { return actor.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 let the model choose its own Actor or access scope. Authenticate first, normalize once, and let Capabilities use the trusted result. Read [Access](https://vitehub.dev/docs/agents/capabilities/access) for the scope options. ## 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 the context store context.get('actor') or context.get('invoker')
Public type AgentActor . Invoker-named APIs also expose AgentInvoker .
# Agent Definitions > Declare how one Agent runs, what it can use, and how callers reach it. 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', }) ``` The shorthand uses `permissions: 'ask'`. Provider actions request approval unless the Agent Definition opts into another policy. 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 }`, `{ run }`, or [`{ ask }`](https://vitehub.dev/docs/agents/agent-drivers#use-an-ask-driver). ## 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. AI SDK Model Drivers make one output-only correction call when a known tool receives arguments that fail its input schema. The correction cannot select another tool or execute a provider-defined tool. Set `driver.execution.repairToolCall: false` to disable this behavior. ## Extend an Agent Use `extends` to make one Agent Definition the default for another: ```ts [server/agents/bot-dev/agent.ts] import { defineAgent } from 'vite-hub/agent' import bot from '../bot/agent' export default defineAgent({ extends: bot, driver: { model: 'gpt-5.6-sol' }, workspace: { store: { provider: 'local', root: '.vitehub/workspaces/bot-dev' }, }, }) ``` The child gets a fresh runtime from the parent's configuration. `name` is not inherited; discovery names each Agent from its own file. Configure separate persistent storage when the Agents must keep separate data. An explicitly shared store or adapter remains shared. Child configuration overrides parent defaults. Channels, Sources, Skills, and hooks merge by key, replacing each matching definition or callback as a whole. Static Capabilities merge by `id`: the child replaces a matching Capability and appends new ones. A Capability resolver replaces the inherited list or resolver. Other arrays replace the parent array. A child `driver.launch` replaces the entire inherited launch command or resolver, including `onExit`. If the child omits `launch`, it inherits the parent launch. Changing a Driver kind or store provider replaces that configuration. `extends` accepts one definition created by `defineAgent()` in the same package instance. It does not discover files in the parent's directory. Import shared instructions in TypeScript with `?raw` and pass them through `driver.instructions`. Share Skills through explicit Sources or a directory link. A definition that is not discovered, such as one created in a Schedule, uses the colocated Skills of the discovered Agent it extends. It reads them when it runs, so module import order does not matter. Use `agentWithSkills()` to add Skills to such a definition without an Agent folder: ```ts [server/schedules/changelog.ts] import { agentWithSkills, defineAgent } from 'vite-hub/agent' import botDev from '../agents/bot-dev/agent' import changelogSkill from './changelog-skill.md?raw' const changelogAgent = agentWithSkills( defineAgent({ extends: botDev, name: 'changelog' }), { 'changelog-writing': changelogSkill }, ) ``` Each key is a Skill name, and each value is its `SKILL.md` content. The result keeps the inherited Skills. A Skill with the same name replaces the inherited one. ### Named presets Export ordinary `defineAgent()` definitions from a preset package. Consumers import them and select a local name: ```ts import { defineAgent } from "vite-hub/agent" import { notetaker } from "@example/agents" export default defineAgent({ preset: "notetaker", presets: { notetaker }, name: "meeting-notes", driver: { model: "gpt-5.6-sol" }, }) ``` `preset` must name an own entry in `presets`. Selection uses the same composition as `extends`, including child overrides and a fresh runtime. Specify one parent with either `preset` or `extends`. The map belongs to this definition; it does not register global names or load packages. Neither the map nor its selected name becomes model instructions. Preset packages must declare `@vite-hub/agent` as a peer dependency so their definitions share the application's package instance. Package authors must include instruction content and required assets explicitly; selecting a preset does not discover its package directory. A preset can expose typed options with the same `defineAgent()` function: ```ts export const notetaker = defineAgent({ options: { format: "concise" as "concise" | "detailed", labels: ["notes"] }, configure: ({ format }) => defineAgent({ driver: { kind: "codex", instructions: `Write ${format} notes.` }, }), }) ``` Consumers select the definition and override only the options they need: ```ts const notes = defineAgent({ preset: "notetaker", presets: { notetaker }, options: { format: "detailed", labels: [] }, driver: { model: "gpt-5.4" }, }) notes.options.format // "concise" | "detailed" ``` Configured presets can declare a `configKey` to expose a module-style configuration block. The block keeps the preset's option names visible at the call site: ```ts const notes = defineAgent({ configKey: 'notetaker', options: { format: 'concise' as 'concise' | 'detailed' }, configure: ({ format }) => defineAgent({ driver: { kind: 'codex', instructions: `Write ${format} notes.` }, }), }) const detailed = defineAgent({ extends: notes, notetaker: { format: 'detailed' }, }) ``` Use `extends: [notes, options]` when options belong beside the selected preset, for example `defineAgent({ extends: [notes, { format: 'detailed' }] })`. The named block and tuple can be combined; tuple values take precedence when both provide the same key. The legacy `extends` plus `options` form remains available, but a definition cannot mix it with a named block or tuple. `options` must be a plain record and uses nested defaults. Built-in instance roots are rejected by the types. Custom class roots are rejected at runtime because TypeScript cannot distinguish their structure from plain records with callbacks. Child values replace parent values, including `false`, empty arrays, and callbacks. Arrays never concatenate. Nested values with required methods, including class instances, require complete replacements. Omitted or `undefined` values retain their defaults. Annotate optional fields and literal unions in the defaults to describe the accepted configuration. TypeScript checks options against the selected preset. If options come from untyped input, validate them in `configure`. `configure` runs synchronously when defining or extending the Agent. Return a normal Agent Definition and keep this callback free of network calls and other side effects. The callback receives its own option copy. Copies of standard built-ins preserve their own property descriptors and nested values. Custom class instances and values such as `WeakMap`, `WeakSet`, and `Error` retain their identity across option copies. Detached buffers and their views retain identity. Resizable or growable buffers and their views also retain identity, preserving resize behavior and fixed-length or length-tracking views. Ordinary Agent overrides apply after the callback and remain in effect through further extensions. An inherited Agent name is cleared on each extension. For discovered Agents, a Workspace reference object must use a statically known string `name`; `name: undefined` owns a Workspace. Opaque names require an explicit Workspace ownership marker on the configured definition. A configured Agent exposes its resolved `options` for host setup and inspection; these values do not become model instructions automatically. For folder Agents discovered from `agent.ts`, define `configure` in that file and return a discoverable `defineAgent()` call. Workspace discovery does not execute imported callbacks or opaque helper calls returned by `configure`, including computed calls such as `builders["workspace"]()` and asserted calls such as `build!()` or `(build as Factory)()`. Return `defineAgent()` directly, or declare `workspace: {}` for owned storage or a named Workspace reference on the configured Agent. It rejects an imported `configure` callback because it cannot determine the required Workspace setup. Discovery also rejects unresolved computed settings keys, quoted keys with unsupported escapes, compound `void` expressions, dynamic Workspace values such as `options.workspaceName`, opaque settings spreads such as `...importedSettings`, imported Capability options and preset registries (including object spreads), option-derived Capability lists such as `options.caps`, returned Agent members such as `agents.storage`, imported Agent parents, imported Channel maps, Channels imported from packages, local Channel factories and opaque calls such as `makeChannel()`, first-party Channel helper calls with opaque options such as `github(options.github)` or `github({ pullRequest: options.pullRequest })`, Channel options defined by accessors such as `get pullRequest()`, dynamic preset selections, logical Capability expressions such as `false || [storage]`, constructed Capability lists such as `Array.of(storage)` or `[plain].concat(storage)`, imported Capability values, destructured Capability bindings, and local Capability member access or calls such as `values.storage` and `values.storage()`. If these contribute an owned Workspace, add `workspace: {}` to the Agent definition. Otherwise, define the settings, parents, Channels, and Capabilities locally with direct bindings so discovery can inspect them. Discovery reads first-party Channel helper calls, such as `github({ pullRequest: false })`, without running them. It also follows a Channel imported from a relative module, such as `import portal from '../portal.github.ts'`, and inspects that module's export with the same rules. It follows re-exports from relative modules, such as `export { default } from './inner.ts'` and `export * from './channels.ts'`, and rejects re-exports from packages. `github()` owns a Workspace only when `pullRequest` is enabled and `pullRequest.workspace` is not `false`. `discord()`, `http()`, `slack()`, `teams()`, `telegram()`, and `webChat()` own a Workspace only through their `capabilities`. An Agent whose Channels own no Workspace stays a stateless Agent; do not add `workspace: {}` to it. Discovery rejects Channel option bindings that are mutated or passed to opaque calls, including local callbacks and built-in mutators such as `Object.assign`. It does not execute those calls to inspect their effects. Use unmodified local options, or add `workspace: {}` when the Channel owns a Workspace. Configured presets use the existing layer rules for capabilities, channels, and hooks. A child replaces a capability with the same ID or a channel or hook with the same key. Distinct hooks remain present; same-key hooks do not automatically compose. Option callbacks are values and are also replaced, never invoked by merging. Publish the exported definition on npm and import it into `presets`. There is no second preset factory or global package loader. ## 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 an AI SDK Model Driver returns invalid native structured output, it makes up to three total attempts by default: the original request and two output-only correction calls. Set `driver.output.maxAttempts` to a positive integer to choose the total attempt limit; `maxAttempts: 1` disables correction calls. Corrections keep the invocation's prepared model and provider route, but they do not replay conversation messages or expose tools, so completed tool effects cannot run again. Tool results remain available as bounded evidence for corrected output. Usage records include every model call, with per-call attribution, aggregate token totals, and aggregate cost when provider metadata or configured pricing supplies it. Provider Drivers such as `codex` and `claude-code`, and custom-run Drivers, validate their returned output once. They do not accept `maxAttempts` because a second provider session or application callback would replay work instead of performing an output-only Model Driver correction. ## Accept structured data Set `data` to a Standard Schema when callers pass structured values instead of, or in addition to, model text. ViteHub validates `input.data` before Capabilities, hooks, `intercept`, and the Driver run. Invalid data fails the Invocation: `runAgent()` returns `[Error, null]`, and the Driver does not run. `data` does not replace `prompt` or `messages`. Model Drivers and provider Drivers do not read `data`, so pass the text they need in `prompt` or `messages`. A `prompt` also names Console sessions and gives the `title()` Capability its source text. Hooks, `intercept`, and custom-run Drivers receive the parsed value. Call sites use the schema input type. Hooks and `intercept` receive the schema output type. A custom-run Driver reads the same parsed value from `input.data` with the type `unknown`. ViteHub applies a schema transform once, before any of them run. ViteHub validates changed data again after input hooks and before the Driver runs. It detects in-place changes to arrays, plain records, `Date`, `RegExp`, `Map`, `Set`, `URL`, and `URLSearchParams`. Replace other class instances instead of mutating their internal state, which ViteHub cannot snapshot. Hooks and Capabilities receive the parsed output, but changed data is validated as schema input. Do not replace transformed output with another value; define the desired value in the original input form instead. ## Finish before the Driver Set `intercept` when app code can answer some Invocations without the Driver. The handler receives the same context as an `agent:input` hook plus the parsed `data`. Return `undefined` to continue to the Driver. Return another value to finish the Invocation with that value as its output. Throw to fail the Invocation. ```ts [server/agents/labeller.ts] import * as v from 'valibot' import { defineAgent } from 'vite-hub/agent' const email = v.object({ from: v.string(), subject: v.string() }) const rules = [ { rule: 'github', from: '@github.com', label: 'GitHub' }, { rule: 'billing', from: '@stripe.com', label: 'Billing' }, ] as const export default defineAgent({ data: email, // Config rules decide first. The model runs only when no rule matches. intercept: ({ data }) => { const match = rules.find(rule => data.from.endsWith(rule.from)) return match ? { source: 'rule' as const, rule: match.rule, label: match.label } : undefined }, driver: { model: 'openai/gpt-5.1-mini', instructions: 'Choose one Gmail label for the email.', output: { schema: v.object({ source: v.literal('model'), label: v.string() }) }, }, }) ``` ```ts [server/lib/sync.ts] import { runAgent } from 'vite-hub/agent' import labeller from '../agents/labeller' const [error, output] = await runAgent(labeller, { data: { from: email.from, subject: email.subject }, prompt: `${email.from}: ${email.subject}`, }) if (error) throw error if (output instanceof Response || !('source' in output)) throw new Error('Expected inline labeller output') // output: { source: 'rule', rule: 'github' | 'billing', label: 'GitHub' | 'Billing' } | { source: 'model', label: string } ``` The `runAgent()` output type is the union of the awaited `intercept` return type, excluding the `undefined` fall-through signal, and the `driver.output` schema output. The caller narrows it with the fields of each result and does not parse the output again. As for any Agent, the type also includes `Response` and `AgentWorkflowRun`, so exclude those first. An intercepted Invocation skips the Driver, tools, and start Capabilities. `agent:finish` hooks receive the intercepted value as `result`. The `agent.invocation.finish` trace event sets `agent.intercepted: true` and records the value as `result.output`. Traces record `input.data` and `result.output` only when the trace content policy is `content`; the `metadata` policy keeps only `input.hasData`. The Capability pipeline pauses before its first `prepare` callback or preparation hook, and tool resolution waits for interception. When `intercept` returns `undefined`, the pipeline resumes at that point and keeps the configured Capability and phase order. Phases after that point do not run for an intercepted Invocation, so their input or context changes are not available to `intercept`. Data changed by a resumed phase is validated before the Driver runs. A child Agent that sets `data` or `intercept` replaces the parent value. ViteHub does not merge schemas or compose interceptors. ## 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
extends Inherits configuration from one Agent Definition.
driver Required unless inherited. 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.
box Runs a built-in provider Driver inside a Box with a checkout, private Home, environment, and boot checks. Cannot be combined with workspace .
driver.instructions Configures instructions on the selected Driver; see Instructions .
driver.output Validates structured Agent output.
data Validates structured Invocation input and types data at call sites and in hooks.
intercept Finishes an Invocation with app-computed output before the Driver runs.
channels Declares named Agent Channels and generated routes.
github Sets the Agent GitHub identity, for example createGitHubHost() . Provider Drivers receive its access().env , and pull request checkouts and git() use its token. Defaults to the identity passed as github({ app }) .
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. Finish and error hooks act on the triggering Channel message through event.message .
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 > Choose model-backed, provider-backed, question-backed, or application-owned Agent execution. 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.
Ask A fast decision model answers a fixed set of typed questions.
Built-in `"codex"` and `"claude-code"` values are provider-backed. Application-supplied Drivers use exactly one of `{ model }`, `{ run }`, or `{ ask }`. ## 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. Install only the provider packages selected by your Agent Definitions: ```sh pnpm add @openai/codex@0.149.1 pnpm add @anthropic-ai/claude-agent-sdk@0.3.246 ``` ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' import { loadServerEnv } from '#vitehub/env/server' export default defineAgent({ driver: { credentialProfile: 'review', credentials: async ({ abortSignal }) => (await loadServerEnv(undefined, { signal: abortSignal })).codexAuthJson, kind: 'codex', instructions: 'Review the exact pull request head before changing code.', model: 'gpt-5.5', permissions: 'ask', reasoningEffort: 'high', reasoningSummary: 'detailed', }, workspace: { mode: 'write' }, }) ``` `credentials` accepts the contents of Codex `auth.json`, a sealed Server Env value, or an invocation-time resolver. This lets a Kubernetes secret or an [Env provider](https://vitehub.dev/docs/env/server-api#read-external-env-storage) supply auth without startup file plumbing. ViteHub resolves it for each invocation, validates the JSON object, and projects it as a `0600` file inside a `0700` Codex Home. Provisioned credentials require a POSIX host; ViteHub rejects them on Windows because these file modes cannot guarantee owner-only access there. The value never enters the provider environment or inspection output. A named `credentialProfile` shares one writable Home between Drivers in the process and stores it at `.vitehub/data/codex/`. ViteHub serializes Codex runtime access to the profile, so one Codex process owns that Home at a time. Codex can refresh `auth.json` there. ViteHub fingerprints the last external seed, preserves Codex's refreshed file while that seed is unchanged, and replaces the file on the next invocation when the resolver returns a rotated value. The directory is a complete Codex Home, not a credentials-only store; treat its auth, configuration, session state, and logs as sensitive. A profile supports one ViteHub process writing its volume. In Kubernetes, give each replica its own persistent volume and profile state rather than mounting one profile through a shared multi-writer volume. ViteHub does not write refreshed credentials back to the external Env provider. Without `credentialProfile`, each invocation receives an isolated temporary Home that ViteHub removes after the Codex runtime stops. Provider Drivers require a local Node.js host with credentials available to the process. Codex credentials without an explicit resolver must be available to that process. ViteHub resolves an installed `@openai/codex` or `@anthropic-ai/claude-agent-sdk` package without requiring deployment-specific copy scripts. Production self-hosted Node builds on macOS and Linux copy only the build host's native optional package into `.output/server/node_modules`, including the Linux libc variant, so build on the same host type used for deployment. If Codex is absent, ViteHub falls back to `codex` on the host `PATH`. Claude requires the Agent SDK, while a missing native SDK package at runtime retains T3's host `claude` command fallback. ViteHub does not download provider packages during build or startup. Provider Workspaces also require a POSIX host. Each invocation receives a temporary working directory, optional Workspace files, Codex instructions or a literal Claude prompt file, 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. Set `sessionStorePath` to persist those opaque cursors in a local SQLite file across process restarts. Codex credentials supplied through `credentials` require a named `credentialProfile` before session persistence can be enabled because an invocation-private Codex Home is removed after each run. Relative paths resolve from the process working directory. Dedicate each file to one provider Agent Definition on one persistent process host; the store does not coordinate concurrent ownership of one thread across workers. Chat-backed cursors remain partitioned by origin, invoker, and resolved Chat Session, so a new session cannot inherit provider context from an earlier one. Resolve `env` per invocation when a provider needs short-lived credentials. Use `launch` when the provider process must start through an application-owned wrapper such as a remote execution CLI: ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { env: async ({ abortSignal }) => ({ SANDBOX_TOKEN: await issueSandboxToken({ signal: abortSignal }), }), kind: 'codex', launch: ({ command }) => ({ args: ['exec', '--', command], command: 'sandboxctl', }), permissions: 'allow-all', }, }) ``` ViteHub resolves `env` once, then gives `launch` the selected environment, executable to start in `command`, original provider executable in `providerCommand`, temporary working directory, and abort signal. Use `command` to construct wrapper arguments and `providerCommand` to select a provider-specific runner. They are equal during invocations. The wrapper receives its configured arguments followed by the provider runtime arguments. ViteHub writes an owner-only temporary launcher outside the Workspace and removes it after the provider runtime stops. Launch wrappers require a POSIX host. They change process startup only; authorization, remote isolation, and wrapper credentials remain application-owned. Agent inspection reports whether `env` and `launch` are static or dynamic without resolving either value. Set `requirements` to the commands that the provider's own shell needs, for example `['git', 'gh', 'unzip', 'apply_patch']`. `agent.status()` checks each command with `command -v` where the Driver runs, or `where.exe` for a local Windows Driver. With `launch`, requirements need a resolver. Status inspection resolves it once with `command: 'sh'`. Each provider probe checks the commands in that same shell before it starts the provider, so checking requirements does not allocate another runner. A status probe may execute the resolved launcher more than once for separate provider checks. If the launcher does not report command-check results, requirement readiness remains unknown. Missing commands make the status `unavailable` and appear in `status.missingCommands` and `status.reason`. Invocations do not repeat the check. Resolvers that route by the provider executable must use `providerCommand` before enabling `requirements`. During requirement inspection, `command` is `sh` and `providerCommand` still identifies the provider. An application-owned runner selector can use both fields: ```ts launch: ({ command, providerCommand }) => ({ args: ['exec', '--', command], command: selectRunnerForProvider(providerCommand), }) ``` ### Route model requests through a gateway Set `gateway` to send the provider's model requests to an LLM proxy or gateway. Import a preset from `vite-hub/agent/gateways`. Like a Nuxt module, each preset reads its URL and key from the environment, so the code names only the gateway: ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' import { cliproxy, cloudflareAccess } from 'vite-hub/agent/gateways' export default defineAgent({ driver: { kind: 'codex', model: 'gpt-5.5', // Reads CLIPROXY_URL, CLIPROXY_API_KEY, CF_ACCESS_CLIENT_ID, and CF_ACCESS_CLIENT_SECRET. gateway: cliproxy({ headers: cloudflareAccess() }), }, }) ``` ```sh [.env] CLIPROXY_URL=https://proxy.example.com CLIPROXY_API_KEY=... CF_ACCESS_CLIENT_ID=... CF_ACCESS_CLIENT_SECRET=... ``` Pass a value to override its variable, for example `cliproxy({ url: 'https://proxy.example.com', apiKey: () => useServerEnv().proxyKey })`. Each preset knows the base URL that each Driver expects, so one gateway definition works for Codex and Claude Code:
Preset Codex base URL Claude Code base URL Variables
cliproxy() /v1 CLIPROXY_URL , CLIPROXY_API_KEY
litellm() /v1 LITELLM_URL , LITELLM_API_KEY
ollama() /v1 OLLAMA_URL (default http://localhost:11434 ), OLLAMA_API_KEY (default ollama )
openrouter() https://openrouter.ai/api/v1 https://openrouter.ai/api OPENROUTER_API_KEY
vercel() https://ai-gateway.vercel.sh/codex/v1 https://ai-gateway.vercel.sh/claude-code AI_GATEWAY_API_KEY , then VERCEL_OIDC_TOKEN
openai() https://api.openai.com/v1 OPENAI_API_KEY
anthropic() https://api.anthropic.com ANTHROPIC_API_KEY
`cloudflareAccess()` returns the `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers for a gateway behind Cloudflare Access. It reads `CF_ACCESS_CLIENT_ID` and `CF_ACCESS_CLIENT_SECRET`, or takes `{ clientId, clientSecret }`. Each value first reads its canonical name, `VITEHUB_` and the Server Env path in upper snake case, then the vendor name in the table. `cliproxy()` reads `VITEHUB_CLIPROXY_API_KEY`, then `CLIPROXY_API_KEY`, and `cloudflareAccess()` reads `VITEHUB_CLOUDFLARE_ACCESS_CLIENT_ID`, then `CF_ACCESS_CLIENT_ID`. See [Server Env variable names](https://vitehub.dev/docs/env/configure). ViteHub finds the presets in Agent files and declares their variables in Server Env as `env.server..`, for example `env.server.cliproxy.apiKey`. The Console shows them, `useServerEnv()` types them, and an [Env provider](https://vitehub.dev/docs/env/server-api#read-external-env-storage) can supply them. Your own declaration of a field wins. The values are optional: a server that hosts Agents without the gateway still starts. Without `hubEnv()`, presets read the host variables directly. `url` is the gateway origin. A trailing `/v1` is removed. Every preset accepts `apiKey` and `headers`. `apiKey` and each header value accept a string, a sealed Server Env value, or an invocation-time resolver that may return `undefined`. ViteHub reads values for each invocation. A missing URL, key, or header value fails the invocation with `AGENT_R0980`, `AGENT_R0977`, or `AGENT_R0978` and names the variable to set. A preset that does not serve the selected Driver fails at definition time with `AGENT_R0976`. Use `defineGateway()` for an endpoint without a preset: ```ts import { defineGateway } from 'vite-hub/agent/gateways' export const gateway = defineGateway({ name: 'internal', baseURL: { 'codex': 'https://llm.example.com/openai/v1', 'claude-code': 'https://llm.example.com/anthropic' }, apiKeyEnv: ['INTERNAL_LLM_KEY'], auth: 'x-api-key', }) ``` `auth` selects how the gateway receives the key. `'bearer'`, the default, sends `Authorization: Bearer `. `'x-api-key'` sends `x-api-key: `. `apiKeyEnv` reads host variables, not Server Env. Use `apiKey: () => useServerEnv().internal.key` when the key is declared in Server Env. A `baseURL` entry can also be a resolver, so the URL can come from Server Env for each invocation. ViteHub converts the gateway to each provider's own configuration: - Codex receives a generated `vitehub` model provider through `-c` overrides: `base_url`, `wire_api = "responses"`, `env_key`, and `env_http_headers`. The key and the header values go in the provider environment as `VITEHUB_GATEWAY_API_KEY` and `VITEHUB_GATEWAY_HEADER_`. The launch arguments contain only variable names. The gateway overrides come after `providerSettings.launchArgs`, so they take precedence. - Claude Code receives `ANTHROPIC_BASE_URL`, the key in `ANTHROPIC_AUTH_TOKEN` (bearer) or `ANTHROPIC_API_KEY` (x-api-key) with the other variable set to empty, and `ANTHROPIC_CUSTOM_HEADERS`. The gateway owns these variables. Setting one of them, or `T3CODE_CODEX_LAUNCH_ARGS` for Codex, in `driver.env` fails with `AGENT_R0979`. A gateway replaces `credentials` and `credentialProfile`, including values inherited through `defineAgent({ extends })`. A `launch` wrapper receives the gateway variable names in `requiredEnvironment`, so SSH runners forward them. Agent inspection reports the gateway name and `executionAuthority.credentials: "provisioned"`. `agent.status()` does not cache results for an Agent with a gateway. ViteHub does not forward ambient variables such as `OPENAI_API_KEY`, `ANTHROPIC_BASE_URL`, or `CLIPROXY_API_KEY` to the provider. Use a gateway or `driver.env` to select them. A gateway changes where requests go, not their protocol. The endpoint must accept OpenAI Responses requests from Codex and Anthropic Messages requests from Claude Code, and `model` must be a model ID that the gateway accepts. The key and header values are in the provider process environment, so commands that the provider runs can read them. Treat a gateway key as available to the Agent. Prefer API keys: routing a Claude subscription sign-in through a proxy is not permitted by [Anthropic's terms](https://code.claude.com/docs/en/legal-and-compliance). ### Run in an existing directory By default, each invocation receives a new temporary working directory. Set `cwd` when the provider must run in a directory that the application prepares, such as a disposable local checkout that is also the Workspace's local store root: ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { cwd: '/srv/review/checkout', kind: 'codex', permissions: 'allow-edits', }, }) ``` `cwd` accepts an absolute or relative path, or an invocation-time resolver. Relative paths resolve from the process working directory. The resolved value must be an existing directory; otherwise the invocation fails with `AGENT_R0939` before the provider starts. A `launch` resolver receives this directory as `cwd`. The directory is the working copy. ViteHub still materializes Workspace Sources, but it does not start a Workspace session. It does not copy Workspace files into the directory, create a Git baseline, compute a diff, write changes back, or remove the directory. Generated instruction and Skill files are restored after the provider stops. When the directory is a Git repository root, Git ignores the generated files while the provider runs, so provider commits do not include them. Title and progress summary runs ignore `cwd` and use a temporary directory. The application owns the directory's contents, cleanup, and isolation between concurrent invocations. Agent inspection reports whether `cwd` is static or dynamic without resolving it. ### Provision the project toolchain Set `toolchain` when the provider works on a JavaScript project that pins its own Node.js and package manager. It uses the same declaration as [`box.toolchain`](https://vitehub.dev/docs/agents/boxes#provision-nodejs-and-the-package-manager): ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { kind: 'codex', permissions: 'allow-all', toolchain: 'project', }, }) ``` After ViteHub prepares the pull request checkout, the Workspace, or `cwd`, it reads the pins from that directory, provisions them on this host under `$XDG_CACHE_HOME/vitehub/toolchains`, and checks `node -v` and the package manager version. The toolchain `bin` directories come before the host `PATH` for the provider process, every command it starts, and Workspace commands. With a pull request mounted below the Workspace root, ViteHub reads the pins from that mount. A launcher from `launch` receives the same `PATH` in its environment, so it must run on this host. Provider CLIs that start through `#!/usr/bin/env node`, such as the npm Codex package, also run on the project's Node.js. Title and progress summary runs do not provision a toolchain. `status()` does not report `node`, `npm`, `npx`, `pnpm`, `pnpx`, `yarn`, or `yarnpkg` as missing when `toolchain` is set, because the toolchain exists only after an invocation prepares its checkout. With `box`, declare `box.toolchain` instead; the Agent rejects `driver.toolchain` with `AGENT_R0972`. 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" .
credentialProfile Codex-only stable credential Home name. Requires credentials ; persists under .vitehub/data/codex/ .
credentials Codex-only auth.json string, sealed Server Env value, or invocation-time resolver. Inspection reports that a source is configured without resolving or checking the value.
reasoningEffort Codex-only non-empty model-advertised effort override, such as "low" , "high" , or "xhigh" . Requires an explicit model .
reasoningSummary Codex-only "auto" , "concise" , "detailed" , or "none" override. Requires an explicit model .
model Optional provider model id.
env Explicit environment values or an invocation-time resolver. ViteHub resolves it once and 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.
launch Provider command wrapper or invocation-time resolver. Receives the provider executable, working directory, selected environment, and abort signal.
cwd Optional existing directory or invocation-time resolver. The provider runs there without a Workspace session, write-back, or removal. See Run in an existing directory .
permissions "ask" , "allow-edits" , "allow-edits-unattended" , or "allow-all" ; defaults to "ask" . "allow-edits-unattended" keeps the provider edit mode and denies native permission escalation without prompting. Host-bound MCP tools keep their separate authorization. Set "allow-all" explicitly to run provider actions without approval.
providerSettings Advanced settings passed to the embedded provider runtime. Explicit settings override the installed Codex executable fallback.
requirements Command names that status() checks where the Driver runs. Missing commands are reported in missingCommands .
sessionStorePath Optional SQLite file for provider session cursors. Enables thread continuation after a process restart on the same persistent host volume.
toolchain "project" or { node, packageManager, fallbackNode } . Provisions project-pinned Node.js and package manager for the provider and its commands. See Provision the project toolchain .
output Optional structured Agent output contract.
capacity Optional process-local static or adaptive concurrency and queue limits.
Provider Drivers do not accept 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. ## Run a provider Driver in a Box Set `box` on the Agent Definition to start the provider inside a [Box](https://vitehub.dev/docs/agents/boxes). The Box supplies the working tree, private Home, environment, and boot checks for each invocation: ```ts [server/agents/review/agent.ts] import { defineAgent } from 'vite-hub/agent' export default defineAgent({ box: { runtime: 'trusted-host', requires: ['git'], }, driver: { kind: 'codex', permissions: 'allow-all' }, }) ``` The provider runtime stays in the ViteHub process. Only the provider command runs in the Box. ViteHub adds the provider command to the Box requirements and writes the Driver instructions and Skills to the Box Home. `driver.env` values reach the provider in the Box. With `box`, the Driver rejects `launch`, `credentials`, and `credentialProfile`, and the Agent rejects `workspace`. Put provider credentials in the Box instead. A Box session lasts one invocation, so the provider does not resume a thread from an earlier invocation. ### Cloudflare Workers Provider Drivers start local Codex or Claude Code processes, so they cannot run in a Cloudflare Worker. Worker builds exclude the provider Driver runtime and `@t3tools/provider-runtime`: `@vite-hub/agent` resolves its provider module to a Worker module through the `workerd` and `worker` package conditions. Apps that use only `{ model }` or `{ run }` Drivers do not bundle Node-only provider code. A Worker build fails with `AGENT_B0019` when a server module selects a provider Driver with `driver: 'codex'`, `driver: 'claude-code'`, `{ kind: 'codex' }`, `codexDriver()`, `claudeCodeDriver()`, or a first-party preset that uses Codex. The same check covers a provider Driver passed to the `title()` or `progressSummary()` Capability, including imported aliases and namespace calls. ViteHub checks parsed module syntax, so it ignores comments, strings, regular expressions, type-only imports, and unrelated objects. Computed or indirect selections are not resolved and fail at invocation time with `AGENT_R0928`. Deploy Agents that use provider Drivers to a Node.js host. ## Adaptive process capacity Self-hosted Node applications can admit new work according to current machine pressure instead of relying on a fixed schedule. Keep one shared capacity object in an application module and reuse it across the Agent Definitions that compete for the same host resources: ```ts [server/agent-capacity.ts] import { createProcessAgentCapacity } from 'vite-hub/agent/runtime/process' export const agentCapacity = createProcessAgentCapacity({ concurrency: 6, queue: { maxPending: 100, timeout: 30 * 60_000 }, }) ``` `concurrency` is always the hard maximum. The adapter lowers only new admissions when CPU or memory is under pressure; active invocations continue, queued invocations remain FIFO, and admission resumes after recovery. Linux uses cgroup v2 limits, memory events, and pressure stall information when available. Other hosts fall back to Node's available-memory and parallelism signals. Failed samples or samples exceeding `sampleTimeoutMs` (one second by default) use `fallbackConcurrency`, which defaults to one. `vitehub agent info` reports the current admitted limit, hard maximum, queue depth, and sampling reason. ## 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. ViteHub cannot stop a custom run callback. When a user cancels the Invocation, `input.abortSignal` aborts, and the cancel result reports `notEnforcedBy: 'run'`. Pass the signal to your own I/O, or throw `input.abortSignal.reason`, to stop early. Model-backed and provider-backed Drivers stop on cancel. See [Cancel an invocation](https://vitehub.dev/docs/agents/invocations#cancel-an-invocation). 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. ## Use an ask Driver Use `driver.ask` when the Agent only has to answer a few typed questions, such as a label or a score. The Driver sends every question to [TypeSafe Jev](https://typesafe.ai) in one request through the `advocaat` client. It does not run a chat model, tools, or instructions. Install the client. It is an optional peer dependency, so Agents without `driver.ask` bundle without it: ```sh pnpm add advocaat ``` Declare the `typesafe` Server Env group with `typesafeEnv()`. The Driver always reads this group name: ```ts [vite.config.ts] import { defineConfig } from 'vite' import { vitehub } from 'vite-hub' import { typesafeEnv } from 'vite-hub/env' export default defineConfig({ env: { server: { typesafe: typesafeEnv(), }, }, plugins: [vitehub()], }) ``` Build the questions with `ask`: ```ts [server/agents/labeller.ts] import { ask, defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { ask: { label: ask.choice('Which Gmail label fits this email?', { invoice: 'Bills, receipts, and payment requests.', none: 'No label clearly fits.', }), urgent: ask.if('Does the email need action today?'), }, }, }) ``` The answers are the Invocation output, and their type comes from the questions. The standalone `runAgent` overload returns a tuple, so destructure it before reading the result: ```ts const [error, answers] = await runAgent(labeller, { prompt: 'Invoice 42' }) if (error) throw error answers.label.choice // 'invoice' | 'none' answers.urgent // boolean ``` No output schema or parsing is necessary. Jev reads the Invocation `data` when a caller sets it, else the prompt text, else the latest user message. Pass a function to build the questions for each Invocation. It receives the same context as `driver.run`: ```ts [server/agents/labeller.ts] import { useServerEnv } from '#vitehub/env/server' import { ask, defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { ask: () => { const labels = useServerEnv().labeller.labels.split(',') return { label: ask.choice('Which label fits?', Object.fromEntries(labels.map(label => [label, null]))), } }, }, }) ```
Builder Answer
ask.choice(instructions, criteria) { choice, confidence, probabilities } . criteria maps labels to descriptions, or is a list of 2 to 255 labels.
ask.switch(instructions, criteria) The selected label only.
ask.score(instructions, levels) { score, ratio, confidence, legend, probabilities } for 2 to 10 ordered levels.
ask.chance(instructions, criteria?) { chance } , the probability of yes.
ask.if(instructions, { threshold? }) true when the probability of yes is above threshold . The threshold must be between 0 and 1 and defaults to 0.5 .
State, instructions, and criteria accept JSON values. Root numbers and booleans become text for the `advocaat` Entry contract. Numbers and booleans inside objects or arrays stay native. Score legends retain the original level descriptions. `driver.ask` accepts only `ask` and `capacity`. Missing `advocaat`, a missing `typesafe` group, and a missing TypeSafe API key fail the Invocation with a diagnostic. The Console and `vitehub agent info` show the Driver kind as `ask`. When an ask Driver Agent uses [`llmGate()`](https://vitehub.dev/docs/agents/capabilities/llm-gate) or [`llmRoute()`](https://vitehub.dev/docs/agents/capabilities/llm-route) without a `model`, the decision also uses Jev. ### Provider exit evidence A `launch` resolver can return `onExit({ cwd, abortSignal })` with its command. ViteHub calls this host callback once after the provider and Workspace commands stop, before it restores generated files or deletes the temporary working directory. Auxiliary runs, such as title generation, do not call it. Use it to read the final checkout HEAD and persist evidence in host-owned state. The callback also runs after a failed or cancelled turn when shutdown completes. Cancellation can return before this deferred cleanup finishes. Its signal has a separate teardown deadline; stop all I/O when it aborts. Callback errors fail cleanup without preventing directory removal. The callback is skipped when provider shutdown fails or exceeds the cleanup deadline, and during provider inspection. Missing evidence must remain unknown. This callback does not verify model claims, grant push authority, or make closure state durable across host crashes. Validate the checkout in host code and persist the result before returning when durability is required. # Babysitter > Deploy an Agent that repairs pull requests, waits for checks, and merges ready ones. The Babysitter preset repairs selected pull requests. It addresses review feedback from people and bots, fixes failing checks, and pushes the repair. Then it waits for check and review results without another model pass. It can also merge ready pull requests. ## Add the Agent Select the preset in a discovered Agent. The repository filter is required: the Babysitter looks for open pull requests only in these repositories. ```ts [server/agents/babysitter/agent.ts] import { defineAgent } from 'vite-hub/agent' import { babysitter } from 'vite-hub/agent/presets/babysitter' export default defineAgent({ extends: babysitter, options: { filter: { repository: { allow: ['acme/app'] }, author: { allow: ['octocat'] }, }, merge: 'direct', concurrency: 2, }, driver: { model: 'gpt-5.6-sol' }, }) ``` Add `instructions.md` next to `agent.ts` for project-specific guidance. It fills the preset's instruction slot.
Option Default Purpose
filter {} Pull requests to repair, with the GitHub Channel filter rules . repository.allow is required.
driver 'codex' 'codex' or 'claude-code' . Set the model and provider settings with the ordinary driver field.
merge false false , 'auto' (request GitHub auto-merge), 'direct' (merge a ready PR on the host), or { strategy: 'direct', method, ready } .
reviewChecks [] Check names, such as a review bot's check, that keep a PR waiting while they run.
noFindingsReviews [] Body prefixes of comment-only reviews that report no findings, such as '> ✅ No new issues found.' . They do not wake a waiting PR.
concurrency 1 Pull requests repaired at the same time.
## Share the Console journal The Babysitter uses the invocation journal assigned to its discovered Agent. Set the Console journal in the ViteHub project configuration so the Console and the long-lived worker inspect the same records: ```ts [vite.config.ts] import { defineConfig } from 'vite' import { vitehub } from 'vite-hub' export default defineConfig({ plugins: [vitehub({ preset: 'node', console: { exposure: 'host-managed', authorize: './server/console-authorize.ts', databaseUrl: 'file:/var/lib/babysitter/console.sqlite', }, })], }) ``` Create `server/console-authorize.ts` with your host session policy as shown in [host-managed Console access](https://vitehub.dev/docs/development/console#protect-the-console-route). Every Console data route calls this function before it reads data. The worker records and recovers Invocations under `-worker`, for example `babysitter-worker`. Each discovered Babysitter Agent has its own recovery scope. Other Agents can keep using the same journal without having their active Invocations failed. A standalone process host without an assigned journal keeps its private `dataDir/invocations.sqlite` file. ## Configure GitHub Create a GitHub App with read and write access to contents, pull requests, issues, and checks. Subscribe it to pull request, review, review comment, review thread, issue comment, check run, check suite, status, and push events. Set its webhook URL to `https:///api/_vitehub/agents/babysitter/webhooks/github`. Set these variables on the host, or declare them in `env.server.github`:
Variable Purpose
GITHUB_APP_ID The App ID.
GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_PATH The App private key.
GITHUB_WEBHOOK_SECRET The webhook secret. Deliveries without a valid signature are rejected.
GITHUB_APP_INSTALLATION_ID Optional. Without it, each repository uses its own installation.
VITEHUB_AGENT_STATE_URL Agent State, for example file:/var/lib/babysitter/state.sqlite . Production builds require it.
The Babysitter commits as the App's bot. GitHub tokens stay on the host; the worker reaches GitHub only through tools that are bound to its pull request. ## Deploy The Babysitter runs a long-lived process, so deploy it with the Node server preset on a host with a persistent disk, Git, and the provider CLI. The build fails with `AGENT_B0022` on Cloudflare, Vercel, Netlify, and Deno, and with `AGENT_B0023` without SQL Agent State. ViteHub starts the Babysitter when the server starts. A development server starts it only with `VITEHUB_AGENT_PROCESS_HOSTS=1`, so it does not repair real pull requests by accident.
Route Purpose
GET /api/_vitehub/host/health Health and the pull request queue: working, ready, and waiting.
GET /api/_vitehub/host/drain Drain status: accepting , draining , or drained .
To replace the process, send SIGUSR2, wait until the drain route reports `drained`, then restart. Running passes finish first. ## How it works 1. Signed webhooks update a durable pull request inbox in Agent State. Once a minute, one GraphQL query per repository lists the open pull requests; a pull request whose state changed without a delivery gets a targeted read, so lost deliveries are recovered within about a minute. 2. When a pull request needs work, the host clones its head and starts one repair pass with the Agent. 3. After a repair push, the pass ends within 3 minutes and the pull request waits on the pushed head. 4. Check results that the pass already knew, pending checks, and the push's own events keep it waiting. New feedback, a new failing check, a conflict, or an unresolved review thread wake it. 5. With `merge: 'direct'`, passing required checks also wake it. The host merges when every check passed, every review thread is resolved, and GitHub reports the pull request as clean on the default branch. It never merges into another branch. A pull request that ends three passes on one head without a push waits for new evidence. A stacked pull request whose parent merged into the default branch is moved to the default branch. # Boxes > Run a built-in provider Driver inside a Box with a private Home, checkout, credentials, and process requirements. A Box prepares the process environment for a built-in provider Driver. It declares the working tree, private Home, environment, durable CLI state, and boot checks. The Codex or Claude Code process then starts inside the Box. Use a Box when the provider needs more than a temporary local directory. For example, use a Box when it must work in an exact pull request checkout on another host. A Box does not give model-backed Drivers Workspace tools. [Workspace context](https://vitehub.dev/docs/agents/workspace-context) and Capabilities own that boundary. The provider can read and change the Box working tree with its native file and command tools. Treat that tree as model-visible, and rely on the selected Box runtime for isolation. ## Start on a trusted host Install the Agent and Box packages: ```bash [Terminal] pnpm add @vite-hub/agent @vite-hub/box ``` This Agent gives Codex a private Home and checks the required CLIs before the invocation starts: ```ts [server/agents/review/agent.ts] import { defineAgent } from '@vite-hub/agent' export default defineAgent({ box: { runtime: 'trusted-host', requires: ['git'], toolchain: 'project', }, driver: { kind: 'codex' }, }) ``` For each invocation, ViteHub opens a new Box session. The Box creates a private Home, provisions the Node.js and package manager versions that the project pins, runs its requirements, and starts the provider only after boot succeeds. ViteHub closes the session when the invocation ends. ::warning `trusted-host` isolates Home and declared environment values. It does not isolate the filesystem, network, processes, or installed executables. Use it only when the Agent may act with the authority of the host user. :: ## Pin an exact checkout Box callbacks receive the Agent invocation context. Resolve repository facts from trusted invocation data when every run must inspect an exact commit. ```ts [server/agents/review/agent.ts] import { defineAgent } from '@vite-hub/agent' interface PullRequest { ref: string remote: string sha: string } function pullRequestOf(context: { input: { context?: Record } }) { const value = context.input.context?.pullRequest as PullRequest | undefined if (!value) throw new Error('A trusted pull request context is required.') return value } export default defineAgent({ box: { runtime: { kind: 'crabbox', profile: 'review' }, checkout: { ref: context => pullRequestOf(context).ref, remote: context => pullRequestOf(context).remote, sha: context => pullRequestOf(context).sha, }, env: { GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', }, requires: [ 'git', { name: 'GitHub CLI', command: 'gh', args: ['auth', 'status'] }, ], }, driver: { kind: 'codex' }, }) ``` The Box fetches `ref`, compares the fetched commit with the full `sha`, and starts in a detached Git repository. Use `cwd` when the caller already owns the authoritative directory. `cwd` and `checkout` are mutually exclusive. ## Provision Node.js and the package manager A Box does not assume that the host or image has Node.js, a package manager, or Corepack. Set `toolchain: 'project'` and the Box reads the pins from the checkout or `cwd`: ```ts box: { runtime: { kind: 'crabbox', profile: 'review' }, checkout: { ref, remote, sha }, toolchain: 'project', } ``` Node.js comes from the first match: `package.json` `devEngines.runtime`, `.node-version`, `.nvmrc`, `package.json` `volta.node`, `package.json` `engines.node`, then `toolchain.fallbackNode`. Ranges and aliases such as `lts/*` resolve to the highest matching release. The package manager comes from `package.json` `packageManager` or `devEngines.packageManager`; without one, the project uses the npm bundled with Node.js. Use `{ node: '22', packageManager: 'pnpm@10.2.0' }` to pin versions in the Agent, or `packageManager: false` to keep the bundled npm. ViteHub downloads official archives, verifies the Node.js `SHASUMS256.txt` checksum and the npm registry `sha512` integrity, and caches each version. The provisioned `bin` directories come first on the Box `PATH`, so a different `node` on the host never answers. Boot fails when the project pins no Node.js version and no `fallbackNode` is set. See the [`@vite-hub/box` README](https://github.com/vite-hub/vitehub/tree/main/packages/box#provision-the-project-toolchain) for the cache location of each runtime, mirrors, and limits. Provider CLIs that start through `#!/usr/bin/env node` also run on the provisioned Node.js. ## Add credentials and CLI state Keep secret values outside the repository. Resolve them into Box `env`, Home files, or a first-use seed for writable state. The provider reads its credentials from the Box Home, for example `.codex/auth.json`. ```ts [server/agents/review/agent.ts] import { defineAgent } from '@vite-hub/agent' import { useServerEnv } from '#vitehub/env/server' export default defineAgent({ box: { runtime: { kind: 'trusted-host', stateRoot: '/var/lib/vitehub/boxes', }, env: { GH_TOKEN: () => useServerEnv().githubToken.unseal(), }, home: { files: { '.gitconfig': { from: '.vitehub/box/gitconfig' }, '.codex/config.toml': { from: '.vitehub/box/codex.toml' }, }, state: { '.codex': { key: 'review/codex', seed: { 'auth.json': { contents: () => useServerEnv().codexAuthJson.unseal(), }, }, }, }, }, }, driver: { kind: 'codex' }, }) ```
Declaration Use it for Lifecycle
env Tokens and CLI controls Resolves on every boot and reaches every Box process.
home.files Immutable configuration Writes private files on every boot.
home.state CLI-owned writable directories Persists beneath stateRoot under an exclusive lease.
seed First-use state Resolves only when the durable state directory is absent.
toolchain Project-pinned Node.js and package manager Installs into a shared cache, or the Box Home on remote runtimes, before requirement checks.
requires Executable and authentication checks Runs after materialization and fails boot on error.
Targets are relative POSIX paths below the Box Home. State keys must be stable and project-qualified. Existing state wins over its seed, so a failed authentication check does not restore older credentials. Requirement objects use fixed command and argument arrays. They do not parse shell strings: ```ts requires: [ 'git', 'kubectl', { name: 'GitHub CLI', command: 'gh', args: ['auth', 'status'], timeout: 10_000 }, ] ``` Keep credentials out of arguments because requirement metadata is inspectable. ## Understand what the Driver adds The Driver adds these items to the Box for each invocation: - `driver.instructions` go to `.codex/AGENTS.md` for Codex or `.claude/vitehub-system-prompt.md` for Claude Code in the Box Home. The checked-out tree does not change. - Colocated Skills go to `.agents/skills`, `.codex/skills`, and `.claude/skills` in the Box Home. - The provider command becomes a Box requirement. It is `codex` or `claude` by default. Set `driver.providerSettings.binaryPath` to use another command name or absolute path inside the Box. - `driver.env` values reach the provider process in the Box. They replace Box `env` values with the same name. Do not declare Home files at the paths that the Driver writes. ViteHub rejects the invocation when a declared Home file uses one of these paths. A Box session lasts one invocation, so the provider does not resume its session in the next invocation. Send the necessary context in the prompt, Home files, or the checkout. ## Choose a runtime The Box declaration stays portable while the runtime decides where commands run. The runtime must start long-running processes and forward their input.
Runtime Use it when
trusted-host Trusted work may use the authority of the current machine.
crabbox The same declaration must run through Crabbox Static SSH on a Linux host.
Crabbox requires `cwd` or `checkout`, keeps its private Home on the target, and synchronizes only an authoritative `cwd` back. A disposable checkout stays on the target. Capability tools reach the provider through a loopback MCP endpoint of the ViteHub process. Use them only with a runtime that shares the ViteHub network: `trusted-host`, or `crabbox` with `network: 'direct'`. Other runtimes reject invocations that include tools. ## Understand boot order 1. ViteHub validates names, paths, state keys, and requirement arguments without resolving secrets. 2. The runtime acquires state leases and creates a private Home. 3. It resolves first-use seeds, environment values, and Home files for this invocation. 4. It creates and verifies the checkout when configured. 5. It provisions `toolchain` from the checkout or `cwd`, puts it first on `PATH`, and verifies `node -v` and the package manager version. 6. It runs requirements, including the provider command, `node`, and the package manager, inside the prepared environment. 7. The provider starts in the Box working directory. A failed input or boot check stops the invocation before the provider starts. Box metadata excludes resolved secret values, file contents, physical Home paths, and provider handles. ## Know the limits ViteHub rejects these combinations when you define the Agent:
Combination Reason
box with a model or custom run Driver Only built-in provider Drivers start a process in the Box.
box with driver.launch The Box starts the provider.
box with driver.credentials or driver.credentialProfile Put provider credentials in box.home.files , box.home.state , or box.env .
box with an Agent Workspace The Box cwd or checkout owns the working tree.
box with driver.toolchain Declare box.toolchain so the Box provisions it.
box on a Worker or Deno host Provider Drivers need a Node.js host.
ViteHub rejects these inputs when an invocation starts: - A Windows host, because the provider relay requires a POSIX Node host. - Image attachments, because the provider cannot read host attachment files. - Managed `browser()`, because the browser runs outside the Box. Install the browser in the Box and use `browser({ runtime: 'external' })`. - Capability tools on a runtime that does not share the ViteHub network. The invocation fails when the Box session cannot close. For example, a Crabbox Box with an authoritative `cwd` copies provider changes back when it closes, so a failed copy is reported. Launch diagnostics redact resolved `box.env` values. When `box.home.files` or `box.home.state` has entries, ViteHub omits saved provider stderr and spawn-error details because Home files and persisted state can contain credentials. Provider output still reaches the provider runtime. ## Keep execution boundaries separate
Primitive Owns
Box Process environment, private Home, credentials, checkout, state, and boot checks.
Workspace Agent-visible files, Sources, rules, snapshots, and writeback.
Sandbox Package-project discovery, preparation, invocation, timeout, and lifecycle orchestration.
Use `@vite-hub/box` directly when application code owns the process lifecycle. Use [`sandbox()`](https://vitehub.dev/docs/sandbox/agent-capability) to give a model-backed Agent an allowlisted executable tool. Provider status inspection inside an Agent Box is currently unsupported. `agent.status()` reports `readiness: "unsupported"` for boxed provider Drivers. Invocation execution still uses the configured Box. # Capabilities > Give an Agent tools and behavior without handing it unrestricted server access. 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. ## Choose the API by its caller Installing a Server Primitive does not give an Agent access to it. Attach a Capability when the Agent needs that operation.
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
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. Application code can still call the same Server Primitives through their server APIs. ## 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: ```bash pnpm add @github-tools/eve-extension ``` ```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' }), ], }) ``` `repositoryHost()` and `repositoryHostContext()` are no longer built in. Replace their GitHub tools with the extension above. GitHub Channel invocations still expose trusted pull request context through `pullRequest.read(invocation)` from `vite-hub/agent/channels`; ViteHub no longer creates a provider-neutral repository client or materialized repository context. The Vite plugin reads the package's Eve manifest and fails the build when a declared contract version is unsupported. The bridge supports one mount per extension package, direct default-import factory calls, static tools, and dynamic tools with one `session.started` or `step.started` handler. It preserves tool schemas, output conversion, and Eve's `always`, `never`, and `once` approval modes. ViteHub maps the supported dynamic handler to Capability tool resolution at 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. Eve `once` approvals come only from that Chat state for the current invoker and chat session. The route passes them to the Invocation as a request-scoped grant, so Invocation context values, including values set by `admission.context` or `mapInput`, cannot approve a tool. 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 and turn sequence metadata are unavailable. A `step.started` handler resolves once per ViteHub Agent Invocation, not before every model step. 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. ## Finish without the Driver A Capability `input` phase can finish an Invocation only with a `Response`. When app code decides the result for some inputs, such as mail rules that label an email before a model runs, use `defineAgent({ data, intercept })` instead. `intercept` returns a typed value that becomes the Invocation output, and `runAgent()` types its output as the union of that value and the Driver output. ```ts [server/agents/labeller.ts] export default defineAgent({ data: email, intercept: ({ data }) => data.from.endsWith('@github.com') ? { source: 'rule' as const, label: 'GitHub' } : undefined, driver: { model, output: { schema: modelLabel } }, }) ``` Read [Finish before the Driver](https://vitehub.dev/docs/agents/agent-definitions#finish-before-the-driver) for hooks, traces, and layer rules. ## 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/agents/capabilities/official) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Agent definitions](https://vitehub.dev/docs/agents/agent-definitions) # Access > Admit trusted chat traffic and narrow the Workspace before other Capabilities add tools. `access()` resolves access when an Agent Invocation starts. It can admit or reject chat messages from Channels, and it can narrow the [Workspace](https://vitehub.dev/docs/workspace) to a Workspace Scope before other Capabilities use it. It adds no model-facing tools. The [Workspace server API](https://vitehub.dev/docs/workspace/server-api) covers application code. This page covers how an Agent receives a scoped Workspace. ## Configure access Place `access()` before Workspace and storage Capabilities. The selected scope narrows the Workspace 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' }), ], }) ``` Select a scope from trusted invocation context: ```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({ chat: { resolve: ({ invoker }) => invoker?.kind !== 'anonymous', }, workspace: { defaultScope: 'public', resolve: ({ invoker }) => invoker.meta?.team === 'staff' ? { scope: 'staff', role: 'admin' } : undefined, scopes: { public: { paths: ['public'] }, staff: { all: true }, }, }, }), workspaceShell({ mode: 'read' }), ], }) ``` ## How access works Capabilities run their setup phases in array order, so later Capabilities see the scoped Workspace. ### Workspace Scope 1. ViteHub calls `workspace.resolve`, or uses its static value. When it returns nothing valid, ViteHub uses `defaultScope`. When neither produces a scope, the invocation fails. 2. A selection is a scope name or `{ scope, role, ...grants }`. Inline grants replace the named definition in `scopes`. The role defaults to `"viewer"`. 3. ViteHub resolves path and Source grants to Workspace paths, then replaces the active Workspace with a scoped facade. 4. ViteHub records the scope in invocation context as `access.workspaceScope` with `all`, `paths`, `role`, `scope`, and `sources`. This value is a copy for your code to read. ViteHub keeps the scope that `access()` resolved and enforces only that scope. A value that you or a Capability writes to the `access` key does not change the scope. The scoped facade hides paths outside the scope. Reads, lists, searches, globs, Source materialization, and Workspace Sessions see only the granted paths. A hidden path behaves as not found. Workspace Sources do not own authorization. Grant a Source by key from each scope that may use it, or grant its concrete Workspace path. Invocation-aware Source Resolution can then narrow the Source repository, root, or mount. Access recalculates a Source grant against that resolved shape before it exposes the scoped Workspace. For Provider Drivers, paths that other Capabilities materialize, such as Skill directories, stay visible in the scope. ### Chat admission For each message that an adapter-backed Channel receives, ViteHub calls `chat.resolve` before the Agent Invocation starts. The context includes the resolved `invoker`, the message `input`, the Channel `provider`, the `request`, and the `webhook` registration. When `resolve` returns `false`, ViteHub records the delivery as rejected and does not start the invocation. Any other return value admits the message. ### Instructions Put model-facing guidance for each scope in Agent Driver Instructions or in an imported instruction file. Wrap guidance that depends on Access in an explicit `::capability{key="access"}` block. Scope definitions do not accept instructions. ## Requirements - `access()` requires `chat`, `workspace`, or both. - `access({ workspace })` requires an explicit Workspace on the Agent Definition. - Model-backed and custom-run-backed Agents require a read-only Workspace. Writable Workspace access is supported only for Provider Agent Drivers, and stays limited to the selected scope. - Source grants require a Workspace Definition. - `access({ chat })` must be in the static `capabilities` array, not in a capabilities resolver function. ## Security and approval `access()` adds no tools and has no `policy` option. It narrows what later Capabilities and Drivers can reach. - Resolve scopes and chat admission from trusted Agent Invoker or platform identity metadata. Do not treat model text as access authority. - `all: true` grants the full Workspace and requires `role: 'admin'` in the selection. - Scope paths must stay inside the Workspace. Absolute paths and `..` segments fail. - A root-mounted Source grant fails. Grant explicit paths instead. - `chat.resolve` returns `false` to reject. Returning `undefined` admits the message, so return `false` explicitly for every rejected case. ## Driver support
Agent Driver Support
Model-backed Receives the read-only scoped Workspace and any explicitly authored Agent instructions.
Provider-backed Receives the scoped Workspace, which can be writable. Model-facing instructions require provider support.
Custom-run-backed Receives the read-only scoped Workspace and the invocation context. driver.run decides how to use them.
## Verify access 1. Run an Agent Invocation with `access()` and inspect its traces or run events for the `access` Capability. 2. Confirm that `access.workspaceScope` appears in invocation context and that Workspace tools cannot read outside the selected paths. 3. Trigger scope failures during development. Confirm that a missing scope, a root-mounted Source grant, a missing Workspace, or a path escape fails before model execution. 4. Send a Channel message that `chat.resolve` rejects and confirm that no Agent Invocation starts. ## Options
Option Type Default Description
chat.resolve (context) => boolean | void none Admit or reject a Channel message before the Agent Invocation starts. Return false to reject.
workspace.defaultScope string none Fallback scope name when resolve does not select one.
workspace.resolve string | selection | (context) => selection | undefined none Select a scope from trusted invocation context.
workspace.scopes Record none Named scope definitions.
selection.scope string required Scope name.
selection.role AccessRoleName "viewer" Role for the selected scope. Full-Workspace access requires "admin" .
scope.all boolean false Grant the full Workspace 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 by key.
scope.grants AccessWorkspaceScopeGrant[] none Combine path and Source grants.
A selection object accepts the same grant fields as a scope definition. ## Related pages - [Workspace primitive](https://vitehub.dev/docs/workspace) - [Workspace context](https://vitehub.dev/docs/agents/workspace-context) - [Channels](https://vitehub.dev/docs/agents/channels) - [Actors](https://vitehub.dev/docs/agents/actors) - [workspaceShell()](https://vitehub.dev/docs/workspace/agent-capability) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Chat > Add the chat.message Agent Trigger, Chat History state, and chat finish behavior to an Agent without Channels. `chat()` adds chat behavior to an Agent Definition. It registers the `chat.message` Agent Trigger, requests optional Chat History state, and adds a chat finish extension and final reply delivery. It adds no model-facing tools. The Chat Capability turns message-shaped input into Agent Invocations and exposes the trigger to the CLI Dev Loop. [Channels](https://vitehub.dev/docs/agents/channels) own route admission and delivery for message platforms. ## Configure chat Attach `chat()` to call the Agent from a chat interface through the Agent Trigger API. ```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({ triggerHistory: { source: 'thread', maxMessages: 10 }, }), ], }) ``` Do not combine `chat()` with `defineAgent({ channels })`. That fails with a diagnostic. When an Agent has Channels, ViteHub adds the Chat Capability itself and reads the same settings from `defineAgent({ messages })` and from each Channel's `messages`. Use Channels to deliver messages from Slack, Telegram, Teams, web chat, or another adapter. ## How chat works `chat()` registers the `chat.message` trigger with `ui-message[]` input and `ui-message-stream` output. The trigger builds the Agent Invocation input from the current message and the configured `triggerHistory` window. During preparation, the Capability requests Chat History state as an optional requirement. After the Agent Invocation completes, it provides chat finish data and delivers the final reply. In a durable Agent Workflow, it also delivers the error fallback text when the invocation fails. `vitehub agent dev` uses the `chat.message` trigger by default when the Agent has one. ## Requirements - Chat History state is optional in local development. Configure an Agent State Provider when the deployed stack needs durable sessions or concurrency coordination. - External Chat Platform Adapters are explicit application dependencies that you configure through Channels. - `chat()` does not accept `platforms`, `webhooks`, `meta`, or `metaRevision`. Configure them on Channels. ## Security and approval `chat()` adds no model-facing tool and has no `policy` option. - Chat messages come from end users of the chat interface. Treat them as untrusted model input. - The final reply and the error fallback text go back to the chat interface. - When ViteHub adds Channel message metadata (Channel, sender, username, and send time) to the instructions, it labels the values as context data, not instructions. - For Channel traffic, use [`access({ chat })`](https://vitehub.dev/docs/agents/capabilities/access) or `messages.filter` to reject messages before an Agent Invocation starts. ## Driver support
Agent Driver Support
Model-backed Receives message-shaped input and can stream chat output.
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 1. Run `vitehub agent dev --agent --prompt "hello"` and confirm that the Agent responds through the `chat.message` trigger. 2. Send a second message and verify the invocation origin, Chat Session behavior, and finish extension through traces or run events. 3. For adapter-backed delivery, inspect the Channel webhook registrations for the expected route metadata. ## Options The delivery options below match the Agent-level `messages` settings that [Channels](https://vitehub.dev/docs/agents/channels) describe.
Option Type Default Description
hooks AgentChatEventHooks none Chat event hooks such as onDirectMessage .
lifecycleHooks Record none Lifecycle-hook settings for integrations that read Chat Capability configuration.
event "directMessage" none Chat event binding hint.
triggerHistory "none" | { source: "thread"; maxMessages: number; maxAgeMs?: number } current message only Bounded Chat History Window sent into the chat.message Agent Trigger. maxMessages includes the current message.
threadHistory { maxMessages?: number; ttlMs?: number } inherited Adapter thread backfill and cache. Stores messages but does not 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: strategy ( "manual" , "idle-timeout" , or "hybrid" ), idleTimeoutMs , and metadataKey .
state AgentChatStateResolver runtime state Chat State adapter override.
transcripts Chat SDK TranscriptsConfig none Transcript persistence for adapter-backed delivery.
identity IdentityResolver Channel-qualified user id when transcripts are enabled Resolve the identity that partitions transcripts.
filter (context) => boolean none Return false to drop a message before an Agent Invocation starts. The context has message , deliveryKind , and thread.post .
stream boolean inherited Stream chat trigger output.
streamingUpdateIntervalMs number inherited Minimum interval between streamed Channel message updates.
commentary "hidden" | "message" "hidden" Publish explicit Driver commentary phases as progress messages. Cannot be combined with loading or manual delivery.
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 sends overlapping input to the active Agent Invocation when its Driver supports live steering. Queue retention and failure guarantees come from the configured Chat State runtime.
lockScope "agent" | "channel" | "thread" | string inherited Scope for message locks.
dedupeTtlMs number inherited Time-to-live for Chat SDK duplicate-message keys.
userName string "vitehub" Agent username for adapter-backed Chat SDK delivery.
fallbackStreamingPlaceholderText string | string[] | null | function inherited Placeholder text while streaming starts. An array picks one entry per Agent Invocation. An empty array skips the placeholder.
loading { text: string | string[] | null | function; updates?: "commentary"; intervalMs?: number } none Post a loading message and replace it with the final text when the Agent finishes. Cannot be combined with stream or commentary . intervalMs must be a positive number.
delivery "automatic" | "manual" "automatic" "manual" posts no final text. Finish hooks own all replies with event.reply() . Cannot be combined with stream or commentary .
final { delivery: "new-message" } inherited Post the final reply as a new message before removing the loading message.
timeout number none Positive time limit in milliseconds for inline execution and the durable handoff's typing indicator.
durable boolean inherited Carry the reply across a durable Workflow boundary. Requires loading or delivery: "manual" . Allows only concurrency: "parallel" or "steer" .
errorFallbackText string | null | function inherited Message sent when chat handling fails. A function receives error , publicError , invocation , and defaultText , the text ViteHub sends when this option is not set. invocation is { id, consoleUrl? } when the error belongs to a run.
errorConsoleLink boolean false Add Details: of the failed Invocation to the end of each error reply, also after custom errorFallbackText . ViteHub adds no link when the reply already contains the URL, when the run is unknown, or when no public URL is set. Keep it off for external users, because the link shows internal details. Durable Workflow error replies do not have the link yet.
## Related pages - [Channels](https://vitehub.dev/docs/agents/channels) - [Agent triggers](https://vitehub.dev/docs/agents/triggers) - [Chat History and sessions](https://vitehub.dev/docs/agents/chat-history-sessions) - [access()](https://vitehub.dev/docs/agents/capabilities/access) - [title()](https://vitehub.dev/docs/agents/capabilities/title) - [chatSummary()](https://vitehub.dev/docs/agents/capabilities/chat-summary) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Chat summary > Add a summary command that replaces explicit input with a conversation summary. `chatSummary()` adds a `/summary` Input Command that the user types into the latest message. It adds no model-facing tool. When the command is present, it summarizes the conversation and replaces the command with the summary text before the Agent Driver runs. The summary comes from a custom `execute()` function, an AI SDK model call, or a local heuristic. No Server Primitive is involved. ## Configure summaries Attach `chatSummary()` to an Agent that receives conversation messages, for example with `chat()`. `chat()` is not required; the command works on any message or string prompt input. ```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(), ], }) ``` Without `model` or `execute`, the Capability uses its heuristic summary. Pass `chatSummary({ model })` for a model-generated summary. ## How summaries work `chatSummary()` runs in the input phase. It uses the same command parser as [`inputCommands()`](https://vitehub.dev/docs/agents/capabilities/input-commands). 1. It looks for the command (default `/summary`) in the latest user message, or in the string `prompt` when there are no messages. The command must start at a word boundary. Text after the command name is `args`. 2. It removes the command from the source: - Message input: it summarizes the conversation transcript (`role: text` lines). The command message keeps its other text, or is dropped when only the command remains. - String prompt input: it summarizes the prompt text around the command. 3. It generates the summary with `execute`, or with `model` and the summary instructions. When `args` is set, the model prompt ends with `Focus: `. Without `execute` or `model`, the heuristic summary is the normalized source text cut to `maxLength`. 4. It replaces the command with `Conversation summary:` and the summary on the next line. 5. It sets `{ summary }` in Agent Run Input context under `chatSummary` and `:summary`, and provides it as a finish extension. The finish extension appears only on the invocation that generated the summary. Summary context that a caller sets in advance, or that a later run reuses, does not produce the extension. ## Requirements - The command name must be a valid Input Command name: lowercase, a letter first, then letters, digits, `_`, or `-`. - The command trigger must be a non-empty string without whitespace. - A model-based summary requires the `model` option. The Capability does not use the Agent model. Set `command: false` to turn the command off. The Capability then does not change input and does not register command metadata. ## Security and approval `chatSummary()` adds no model-facing tool and has no `policy` option. The end user controls when the command runs. With `model`, ViteHub sends the conversation transcript, or the prompt text, to that model provider. The heuristic and `execute` paths do not call a model unless your `execute` code does. The summary replaces the command in the model input. Treat it as user-derived content, not as trusted instructions. ## Driver support
Agent Driver Support
Model-backed Receives the transformed input that contains the 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 1. Run `vitehub agent info --agent support --json` and confirm that the `chat-summary` Capability metadata lists the `summary` command and its trigger. 2. Run a chat invocation with `/summary`. Inspect the final Agent Run Input and confirm that the command was replaced with `Conversation summary:` text. 3. Inspect the finish extension and confirm that 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. false turns the Capability off.
command.name string "summary" Command name.
command.trigger string "/" Command prefix.
command.description string "Summarize this conversation." Command description in metadata.
execute (input) => string | { summary?: string } none Custom summary generator. Receives args , input , messages , and text .
fallback string "No conversation to summarize." Summary used when generation returns no usable text.
id string "chat-summary" Capability id, finish extension key, and prefix of the :summary context key.
instructions string generated System instructions for model-backed summaries.
maxLength number 1200 Maximum summary length.
model AI SDK model none (heuristic) Model passed to generateText() for summaries.
## Related pages - [chat()](https://vitehub.dev/docs/agents/capabilities/chat) - [inputCommands()](https://vitehub.dev/docs/agents/capabilities/input-commands) - [title()](https://vitehub.dev/docs/agents/capabilities/title) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Custom capabilities > Build one product-specific Agent ability with requirements, tools, policy, and metadata. 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', title: 'Searched tickets', icon: 'i-lucide-ticket', description: 'Search support tickets by query.', inputSchema: searchTicketsInput, execute: async input => 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. For inline tools, `defineCapability()` infers the handler input from the validator output, including transforms and optional values. TypeScript rejects a handler that requires a different input. Raw JSON Schema has no TypeScript output type, so annotate its handler input. Tools returned by a resolver also check annotated handler inputs against their schemas. Existing AI SDK `tool()` definitions and provider-native tools keep their own contracts. To set a runtime config type, use `defineCapability()({...})`. The inner call infers the Capability and its tools. Replace `defineCapability({...})` calls with this form; the old form could not infer tool schemas. `title` and `icon` are optional display metadata. The model does not receive them, and they do not change the configuration fingerprint.
Field Use
title Short past-tense label for each call, for example Searched tickets . Traces record it as tool.title when the driver event has no title. The Console shows it instead of the tool name.
icon Iconify name, for example i-lucide-ticket . The Console includes the Lucide set and shows the icon on each call and in the tool list.
The tool catalog records `title` as `label` and keeps `label` and `icon` in metadata-only journals. Per-call `tool.title` attributes are trace content, so metadata-only journals omit them; the Console then reads the label from the catalog. 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/agents/capabilities) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) ## Contribute an inspection view The Console's **Capabilities** tab shows the selected Invocation's recorded capabilities. It supplies a default tools and configuration view. Add `inspection` when your Capability needs a different presentation, then publish data with `await context.inspection.set(state)` during its normal lifecycle. ```ts [server/agents/capabilities/tickets.ts] import { defineCapability } from 'vite-hub/agent' export const tickets = defineCapability({ id: 'tickets', inspection: { label: 'Tickets', view: { root: 'summary', elements: { summary: { type: 'Stack', props: {}, children: ['status', 'tools'] }, status: { type: 'KeyValue', props: { label: 'Index', value: { $state: '/status' } }, }, tools: { type: 'Tools', props: {} }, }, }, }, async configure(context) { await context.inspection.set({ status: 'Ready' }) }, }) ``` `inspection.set()` replaces this Capability's state for the current Invocation. Await it to record an update before the next lifecycle step. Keep state serializable and publish only data already produced by the Capability. The Console reads snapshots; opening a view does not run lifecycle hooks, connect to services, or execute tools. The last snapshot remains available after cleanup. Inspection presentation and state do not change the execution configuration fingerprint. The view uses a read-only subset of [JSON Render](https://github.com/vercel-labs/json-render). ViteHub owns the Vue components. Capability code does not import Vue or the Console.
Component Props Purpose
Stack None Arrange child elements vertically.
Section title Group children under a heading.
Text text Display plain text.
KeyValue label , value Display a named value, including structured JSON.
Tools Optional names , mcpServer Show recorded contracts for this Capability's tools. Filter by final tool names or MCP server provenance; omit both to show all of them.
Use `{ $state: '/path' }` to read a state value. A container can repeat its children with `repeat: { statePath: '/items', key: 'id' }`; its descendants use `{ $item: '/field' }` to read the current item. `children` contains element IDs. The exported `AgentCapabilityInspectionView` type checks component names and props. The Console rejects unsupported components, actions, computed expressions, and cyclic element trees, then shows the recorded data and tools instead. Inspection state and view specs follow the Invocation journal's configuration retention policy. Use `configuration: 'content'` to retain them independently of prompts and model output. Credential-shaped state keys are redacted. Existing observation limits still apply; a truncated snapshot may not contain the complete view. A label remains available with metadata-only capture. Telemetry exporters retain custom inspection content only when their `instructions`, `inputs`, and `outputs` content options are all enabled, because custom state can contain any of these. MCP and Title provide custom views. Other official Capabilities use the default view. ### Transform MCP tools Each transform receives a copy of each tool definition that preserves its prototype, property flags, and accessor behavior. Accessors and executors use bound functions to retain their contributed instance as receiver, including when they use private fields. Their function identities can change. Shared definitions registered under different keys remain separate contributions. Rename local tools using the definitions supplied to the callback so inspection can retain their Capability ownership. Tool transforms may remove MCP tools, replace them under the same key, or rename them. Replacements under an existing key inherit its MCP provenance. A rename must preserve `metadata.mcpServer` and `metadata.originalName`, including when it reconstructs the tool: ```ts context.tools.transform(tools => ({ lookup: { name: 'lookup', description: 'Look up a document', inputSchema: tools!.mcp_docs_read!.inputSchema, execute: tools!.mcp_docs_read!.execute, metadata: { ...tools!.mcp_docs_read!.metadata }, }, })) ``` A transform can remove MCP keys and move existing non-MCP tool objects to new keys. These tools keep their original Capability ownership in inspection, including after a same-key replacement in an earlier transform. New keys that contain reconstructed tools without provenance fail with `AGENT_R0923` when MCP keys are removed in the same transform. The runtime cannot infer which server a reconstructed tool came from. To add unrelated tools while removing MCP tools, contribute the new tools with `context.tools.add()` and remove the MCP entries in the transform. # Diagnostics > Report Agent Invocation outcomes and scoped runtime resource observations. `diagnostics()` reports operational events for each Agent Invocation to an application-owned reporter. It always reports one terminal event. With `resources`, it also samples process, host, or service resources through a Runtime inspector. It adds no model-facing tool and does not wrap a Server Primitive. Reporters receive structured events, so the application can write JSON logs, metrics, or another operations sink. ## Configure diagnostics 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), }) ``` ## How diagnostics works When `resources` is set, the Capability starts a resource monitor when the invocation is prepared and stops it when the invocation closes. When the invocation finishes, the Capability reports `agent.invocation.terminal`. Reporter and inspector failures are contained. When an inspector fails or times out, the Capability sends `agent.resource.inspect.failed` to the configured reporter. If reporter delivery itself fails, it emits a local `agent.diagnostics.report.failed` warning. Neither failure replaces a successful Agent result. ### Event contract The Capability reports:
Event When
agent.invocation.terminal The invocation completes, fails, or is cancelled. Includes outcome ( completed , failed , or cancelled ), 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. ## Requirements `diagnostics()` needs no primitive. Resource sampling requires a `RuntimeResourceInspector`, such as `nodeRuntimeResources()` from `vite-hub/runtime/node`. `interval` cannot exceed `heartbeat`. All durations must be positive milliseconds. ## Security and approval `diagnostics()` adds no model-facing tool, so the Agent cannot call it and there is no approval step. Events contain outcome, timing, run IDs, and resource values. They do not contain prompts or model output. A failed invocation's terminal event includes a normalized error with its stack trace. Send events only to a sink that may store server error details. ## Driver support
Agent Driver Support
Model-backed Reports terminal events and resource samples.
Provider-backed Reports terminal events and resource samples.
Custom-run-backed Reports terminal events and resource samples.
## Verify diagnostics Run one invocation with the default reporter and confirm that the server log contains an `agent.invocation.terminal` object with `outcome: 'completed'`. With `resources` set, confirm that `agent.resource.snapshot` events appear at start and finish. ## 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 pages - [OTLP](https://vitehub.dev/docs/agents/capabilities/otlp) - [Invocations](https://vitehub.dev/docs/agents/invocations) - [Runtime context](https://vitehub.dev/docs/getting-started/concepts/runtime-context) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Fetch > Expose named HTTP request tools for developer-approved endpoints. `fetch()` gives the Agent one model-facing tool for each HTTP endpoint that you name and define. Use it for specific endpoints, not for unrestricted web browsing. It does not wrap a Server Primitive. Each tool calls the runtime `fetch` with the request that your tool definition builds. ## 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', }, }, }), ], }) ``` The key in `tools` is the tool name that the Agent sees. ## How requests work ViteHub creates one Agent tool for each entry in `tools`. At invocation time, each tool: 1. Validates the tool input with `inputSchema`, when set. 2. Resolves the request from `url`, `method`, and `request`. A `request` function receives the validated input. 3. Sends the request and parses the response as JSON or text. 4. Validates the parsed data with `schema`, when set. 5. Returns the data, or the result of `transform(data, input)`. Each request attempt times out after 30 seconds by default. Responses are limited to 5 MiB by default. The limit applies to decoded streamed bytes, so a missing or incorrect `Content-Length` cannot bypass it. `GET` and `HEAD` requests retry once after a timeout, a network error, or a `408`, `429`, or `5xx` response. `POST` requests do not retry. A response that is not `2xx` fails the tool call. ## Requirements `fetch({ tools })` requires at least one tool definition. Each tool must provide a URL directly or return one from its `request` resolver. Supported methods are `GET`, `HEAD`, and `POST`. Supported response types are `json` and `text`. ## Security and approval The Agent can call only the endpoints that your tool definitions build. It cannot choose a different host unless your `request` function derives the URL from tool input. The Agent sees the tool name, `description`, and `inputSchema`. It does not see `headers`, `body`, or `query` values that you set in server code, so you can add credentials there. Use `inputSchema` when the endpoint accepts arguments, and `schema` when model behavior depends on the response shape. `fetch()` has no `policy` option. Calls run without an approval step. Use a [custom Capability](https://vitehub.dev/docs/agents/capabilities/custom) with a tool `policy` when a request needs approval. ## Driver support
Agent Driver Support
Model-backed Receives the named fetch tools.
Provider-backed Receives the named fetch tools through the private MCP bridge.
Custom-run-backed Receives the tools in the prepared run context; driver.run decides whether to call them.
## Verify HTTP requests Run `vitehub agent info --agent --json` and confirm that `tools` contains a `fetch` entry. Entries use the Capability id, not the individual tool names. Run one invocation and confirm that the trace shows only the named fetch tools. Run one invocation with invalid input when `inputSchema` is configured. Confirm that the tool fails before the request leaves 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 30000 Timeout for each attempt, including the response body, in milliseconds.
tools.*.request.maxResponseBytes number 5242880 Maximum decoded response size. Explicit limits must not exceed 25 MiB.
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/agents/capabilities/web-search) - [openapi()](https://vitehub.dev/docs/agents/capabilities/openapi) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Git > Give an Agent bounded Git source-history access inside a Workspace Session. `git()` adds one model-facing `shell` tool that runs controlled Git commands in a [Workspace](https://vitehub.dev/docs/workspace) Session. Read mode allows source-history inspection. Write mode also allows local `fetch`, `checkout`, and `switch`. Use it for review, source-history inspection, and local branch selection, not to publish repository history. The [Workspace server API](https://vitehub.dev/docs/workspace/server-api) covers application code. This page covers the Agent tool. ## 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' }), ], }) ``` Require approval for write commands: ```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: 'write', policy: 'require-approval' }), ], }) ``` ## Agent-visible tool contract `shell` accepts this input:
Field Type Description
command string One Git command, for example git status --short . A bare subcommand such as status --short is normalized to start with git .
cwd string Optional Workspace-relative directory. Defaults to the pull request checkout when one exists, otherwise the Workspace root.
The result has `command`, `args`, `cwd`, `exitCode`, `stdout`, `stderr`, and `outputTruncated`. ## How Git access works `shell` accepts one `git` command without shell composition. It rejects `|`, `;`, `<`, `>`, `&`, newlines, and global Git flags such as `git -C`. Use `cwd` to select a directory. Read mode allows `blame`, `cat-file`, `describe`, `diff`, `for-each-ref`, `grep`, `log`, `ls-files`, `merge-base`, `rev-list`, `rev-parse`, `shortlog`, `show`, `show-ref`, and `status`. It rejects options that write files or run external programs, such as `--output`, `-O`, `--ext-diff`, `--textconv`, `--no-index`, `--contents`, and `--open-files-in-pager`. Write mode also allows `fetch`, `checkout`, and `switch`, only on a clean working tree: - `fetch` accepts only remotes that `git remote` lists. It rejects remote URLs, refspecs that update local refs (`+` or `:`), and options such as `--force`, `--tags`, and `--upload-pack`. - `checkout` rejects branch creation and force options such as `-b`, `-B`, `--orphan`, `-f`, and `--merge`. - `switch` rejects branch creation and force options such as `-c`, `-C`, `--create`, `--force`, and `--discard-changes`. ViteHub always blocks `commit`, `push`, `reset`, `rebase`, and `tag`. Arguments must stay inside the Workspace. Git runs with `GIT_TERMINAL_PROMPT=0` and no pager. The Capability reuses its Workspace Sessions within one invocation and closes them when the invocation finishes. A successful write command commits the session changes to the Workspace, except in a pull request checkout. ### Pull request checkouts In a GitHub pull request Invocation, the first command prepares the pull request mount as a Git checkout of the exact head SHA. The checkout and `fetch` use the Agent GitHub identity from `defineAgent({ github })`. Without one, they run without credentials. ## Requirements `git()` requires a Workspace that can start a Workspace Session with `git` available. The Workspace requirement is write mode, even for `mode: 'read'`, because ViteHub may need session-local Git state. ## Security and approval The Agent can run only the commands and options listed above. The `policy` option applies only to write commands (`fetch`, `checkout`, `switch`). Read commands are always allowed. - `"allow"` (default): run write commands without approval. - `"require-approval"`: stop the call with an approval request. The command runs after a user approves it. - `"deny"`: reject write commands. - A function receives `{ name, input }` and returns one of these values. A policy cannot enable a blocked command. `fetch` can use the Agent GitHub identity in a pull request Invocation. No other credentials are added. `maxOutputLength` limits the stdout and stderr returned to the model; it does not limit the command. ## Driver support
Agent Driver Support
Model-backed Receives the shell tool.
Provider-backed Receives the shell tool through the provider MCP bridge.
Custom-run-backed Receives the shell tool in driver.run({ tools }) .
## Verify Git access 1. Run `vitehub agent info --agent --json` and confirm that `tools` contains a `git` entry. Entries use the Capability id. The Agent receives the tool as `shell`. 2. Run `git status --short` through `shell` and confirm a zero `exitCode`. 3. Run `git push` through `shell` and confirm that it fails with `git push is not available through git()`. ## Options
Option Type Default Description
mode "read" | "write" "read" Set to "write" to allow local fetch , checkout , and switch .
maxOutputLength number Infinity Maximum stdout and stderr characters returned per command. Longer output is truncated.
policy AgentToolPolicyDecision | function "allow" Policy for write commands. Read commands remain allowed.
timeout number 60000 Timeout in milliseconds for each Git command in the Workspace Session.
## Related pages - [Workspace primitive](https://vitehub.dev/docs/workspace) - [Workspace shell](https://vitehub.dev/docs/workspace/agent-capability) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Gmail > Let an Agent search and read Gmail and create unsent drafts through a Google Connection. `gmail()` gives an Agent structured tools to search Gmail, read messages, and create unsent drafts. The tools call the Gmail REST API through a Google [Connection](https://vitehub.dev/docs/connections). The Connection holds the OAuth grant, checks access for each call, and records activity. No tool can send a message. The Capability uses only `fetch`, so it runs on Node and Workers. It does not need a Workspace, a CLI, or a Skill. Use [`email()`](https://vitehub.dev/docs/email/agent-capability) for application-owned transactional email through the Email primitive. Use `gmail()` for an operator-owned Gmail account and structured Gmail tools. To run an Agent on each new message and label it, use the [Gmail Channel](https://vitehub.dev/docs/agents/gmail). ## Configure the Agent ::steps{level="3"} ### Define the Google Connection Enable [Connections](https://vitehub.dev/docs/connections/get-started#quick-start) and define a Connection with the `google()` preset. `gmail.readonly` covers search and read. `gmail.compose` covers drafts. ```ts [server/connections/google.ts] import { useServerEnv } from '#vitehub/env/server' import { defineConnection } from 'vite-hub/connections' import { google } from 'vite-hub/connections/google' export default defineConnection({ provider: google({ clientId: () => useServerEnv().google.clientId, clientSecret: () => useServerEnv().google.clientSecret.unseal(), }), scopes: [ 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.compose', ], api: { gmail: ['users.messages.list', 'users.messages.get', 'users.messages.attachments.get', 'users.drafts.create'] }, access: { 'agent:inbox': { read: true, write: ['gmail.users.drafts.create'] }, }, }) ``` The access key is `agent:`. This rule allows reads and asks for approval before creating a draft. Set `approve: false` to allow that declared write without approval. The `api` selection excludes methods that send or delete messages. ### Add the Capability ```ts [server/agents/inbox.ts] import { defineAgent } from 'vite-hub/agent' import { gmail } from 'vite-hub/agent/capabilities' export default defineAgent({ driver: { model }, capabilities: [ gmail({ operations: ['search', 'read', 'draft'] }), ], }) ``` ### Connect the account Open the Console, select **Connections**, and select **Connect** for `google`. Or run `vitehub connections connect google`. :: ## Tools
Tool Operation Connection Operation ids Effect
gmail_search search gmail.users.messages.list , gmail.users.messages.get read
gmail_read read gmail.users.messages.get , gmail.users.messages.attachments.get read
gmail_draft draft gmail.users.drafts.create , and gmail.users.messages.get for a reply write
`gmail_search` returns sender, recipients, subject, date, labels, and snippet for each message. The default query is `in:inbox`. `gmail_read` returns the headers, the decoded text body up to `maxChars`, and attachment names. Gmail stores large bodies as attachments; `gmail_read` fetches them. `gmail_draft` creates a plain-text draft with `to`, `subject`, and `body`. The result always has `sent: false`. Set `replyTo` to a Gmail message id to create a reply draft. The tool reads that message and sets the thread id, `In-Reply-To`, `References`, and `Re: `, so Gmail adds the draft to the thread. Omit `subject` for a reply. A different subject fails. The following Agent-visible definitions are resolved from the real Capability during the docs build. ### Default operations :agent-capability-tools{name="gmail" variant="read"}### With drafts :agent-capability-tools{name="gmail" variant="draft"}Message content is untrusted external data. The tool descriptions tell the Agent to treat it as data, not instructions. ## Access and approval Each Gmail request runs through the Connections client. Its [access rules](https://vitehub.dev/docs/connections/configure#access-rules) govern the Agent actor, for example `agent:inbox`. When `access` is present, unlisted actors are denied. A listed actor needs `read: true` for reads and a matching `write` entry for drafts.
Agent rule gmail_draft result
write: ['gmail.users.drafts.create'], approve: false Creates the unsent draft.
write: ['gmail.users.drafts.create'] Persists an approval and fails with CONNECTION_APPROVAL_REQUIRED . Approve it in the Console or with vitehub connections approvals approve . Connections then executes the stored request once.
No matching write Fails with CONNECTION_DENIED .
A reply draft first reads the original message, so it also requires read access. Setting `read: false` denies reads before a provider request starts. The access rules limit the calls. The OAuth scopes limit the grant. `gmail_draft` cannot send, but the `gmail.compose` and `gmail.modify` scopes also permit sending. Use an explicit `api` selection as shown above to keep send methods unavailable to that Connection. ## Activity Each provider request is recorded as Connection activity, reads included. Entries contain the Agent actor, action id, outcome, and Invocation id. Denied calls are recorded. Pending approvals appear in Connections approval inspection. Activity does not include message bodies, headers, or tokens. See activity in the Console under **Connections**, or run `vitehub connections activity google`. ## Verify Gmail access Run `vitehub agent info --agent inbox --json` and inspect the resolved tools. The default lists `gmail_search` and `gmail_read`. With `draft`, it also lists `gmail_draft`. Start with a test Gmail account. Search for `in:inbox`, create a draft, and make sure in Gmail that the message stays in Drafts and was not sent. Then check the activity entries for the Agent. ## Options
Option Type Default Description
connection string "google" Name of the Google Connection in server/connections/ .
operations Array<"search" | "read" | "draft"> ["search", "read"] Tools to expose. "draft" adds gmail_draft .
## Migrate from `mode` This is a breaking change. `gmail()` no longer uses the `gog` CLI, a Workspace, or a bundled Gmail Skill.
Before After
gmail() gmail()
gmail({ mode: 'draft' }) gmail({ operations: ['search', 'read', 'draft'] }) and a Connection write rule for gmail.users.drafts.create
gog OAuth client, keyring, and GOG_KEYRING_PASSWORD A Google OAuth client in Server Env and a Connection in server/connections/
gmail_auth tool Connect the account in the Console or with vitehub connections connect
workspace: { mode: 'write' } for Gmail Not required
Remove the `gog` installation and its state directories after you migrate. ## Related pages - [Connections](https://vitehub.dev/docs/connections) - [Email Capability](https://vitehub.dev/docs/email/agent-capability) # Input commands > Transform explicit user commands such as /docs before the Agent Driver runs. `inputCommands()` parses explicit user commands, such as `/docs`, in the latest user input before the Agent Driver runs. It adds no model-facing tool. Each command handler is server code that can replace text, update the Agent Run Input, add Invocation context, or answer the request directly. It uses no Server Primitive by itself. Handlers receive the host Runtime Capabilities that the application configures, and `vite-hub` adds Console helpers when the Console is enabled. Use it for commands that transform or enrich the user's prompt, not for host UI state or shell execution. ## 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}`, }, }, }), ], }) ``` With this definition, `/docs billing limits` becomes `Use documentation context for: billing limits` before the Agent Driver runs. ## How input commands work `inputCommands()` runs in the input phase. 1. It reads the string `prompt` when there are no messages. Otherwise it reads the latest user message, or the string `prompt` when no user message exists. 2. It finds configured commands that start at a word boundary, in textual order. `args` is the text after the command name, up to the next configured command or the end. Unknown commands stay unchanged. 3. It calls the matching handler and applies the result. 4. It continues to scan the updated text. It fails with `AGENT_R0103` when expansion does not end.
Handler result Effect
string Replaces the command text and its args . An empty string removes them and the whitespace after them, or before them at the end of the text. It keeps the command when it is the only text.
Partial Merges into the Agent Run Input. context keys merge. A returned prompt or messages value replaces the other input form.
Response Stops the invocation and returns this response. The Agent Driver does not run.
void , or no call Removes the command text. Command names and args are not passed 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. Both hooks receive `args`, `name`, `text`, and `message.reply()`, `message.update()`, and `message.react()` delivery helpers. Reactions are transient by default and are removed at finish; pass `{ transient: false }` to keep one. The Capability records command names, descriptions, Channel allowlists, and the trigger in its metadata. ### Handler context The handler receives `context`, which includes the Capability runtime context, the current Invocation as `context.invocation`, and `context.reply(body)`. Generated hosts add the stable Invocation `id` when one is available. `context.reply(body)` sends an immediate Channel reply and returns an empty `204` response. Return that response from the handler to stop the invocation before the Agent Driver runs. The context also includes the resolved host Runtime Capabilities that the application configures, such as `db`, `kv`, `blob`, `email`, or `connections`. ### Console helpers When you import `inputCommands()` from `vite-hub/agent/capabilities` and enable the Console with `vitehub({ console })`, handlers also receive `context.console.invocations` and `context.console.invocationUrl()` for links to inspected work. `invocationUrl()` requires an invocation with `agentName` and `id`, and either `vitehub({ publicUrl })` or a request context. ```ts [server/agents/support.ts] import { desc } from 'drizzle-orm' import { inputCommands } from 'vite-hub/agent/capabilities' const history = inputCommands({ commands: { history: { async call({ context }) { const { db, schema } = context.console.invocations const [latest] = await db .select({ agentName: schema.invocations.agentName, id: schema.invocations.id, }) .from(schema.invocations) .orderBy(desc(schema.invocations.sequence)) .limit(1) if (!latest?.agentName) return 'No previous invocation.' return await context.reply(context.console.invocationUrl({ agentName: latest.agentName, id: latest.id, })) }, }, }, }) ``` The full `vite-hub` distribution types the Console helpers. The standalone `@vite-hub/agent` package remains host-neutral. ## Requirements - Command names must be lowercase stable identifiers: a letter first, then letters, digits, `_`, or `-`. - A command `description`, when set, must be a non-empty string. - `channels`, when set, must contain non-empty Channel IDs. - The trigger must be a non-empty string without whitespace. - Console helpers require the `vite-hub` import path and `vitehub({ console })`. Input Commands are Capability concerns. Host Commands that change chat, session, UI, or product state belong outside this Capability. ## Security and approval `inputCommands()` adds no model-facing tool, so the model cannot call a command. It has no `policy` option. Any user who can send input to the Agent can run every configured command. Use `channels` to limit a command to selected Channels. A command with `channels` does not run in invocations without a matching Channel ID, and its text stays in the input. Handlers run as server code with the host Runtime Capabilities in `context`, such as `db` and `context.console.invocations`. They do not pass through Capability approval. Check `context.actor` or `context.invoker` in the handler before you read private data or change state. Text that a handler returns becomes model input. Treat `args` as untrusted user input. ## 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.
A handler that returns a `Response` stops the invocation before any Agent Driver runs. ## Verify input commands 1. Run `vitehub agent info --agent support --json` and confirm that the `inputCommands` Capability metadata lists each command, its description, and the trigger. 2. Run an invocation with the configured command text. 3. Inspect the final Agent Run Input and confirm that the command text was replaced or the expected context value was added before the Agent Driver ran. 4. For a command that calls `context.reply()`, confirm a `204` response and that the Agent Driver did not run. ## Options
Option Type Default Description
commands Record required Command map keyed by lowercase stable command names.
commands.*.call (input) => Partial | Response | string | void remove command text Handler that rejects, transforms, enriches, or answers the request. Receives args , command , context , input , message , name , and text .
commands.*.channels string[] all Channels Configured Channel ID allowlist.
commands.*.description string none Command description for metadata and inspection.
commands.*.hooks { 'agent:input'?, 'agent:finish'? } none Command-scoped lifecycle hooks with ctx.message.reply/update/react delivery helpers.
id string "inputCommands" Capability id.
trigger string "/" Non-whitespace command prefix.
## Related pages - [chatSummary()](https://vitehub.dev/docs/agents/capabilities/chat-summary) - [Agent Invocations](https://vitehub.dev/docs/agents/invocations) - [Channels](https://vitehub.dev/docs/agents/channels) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # LLM gate > Allow or reject an Agent Invocation with a model decision before the Agent Driver runs. `llmGate()` classifies the request into one allow or reject category before the Agent Driver runs. It adds no model-facing tool. When the category is a reject category, it stops the Agent Invocation with code `LLM_GATE_REJECTED`. The decision uses one AI SDK model call, or one TypeSafe Jev request for an [ask Driver](https://vitehub.dev/docs/agents/agent-drivers#use-an-ask-driver) Agent. No Server Primitive is involved. ## Configure the gate Define allow and reject categories with stable keys and short descriptions. ```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.', }, }), ], }) ``` Set `message` to change the error message that server code receives on rejection: ```ts llmGate({ allow: { support: 'Support request the Agent can answer.' }, reject: { unrelated: 'Request unrelated to support.' }, message: decision => `Rejected: ${decision.category}`, }) ``` ## How the gate works `llmGate()` runs in the input phase, before the Agent Driver. 1. It reads the latest user text: the string `prompt`, or else the text of the latest user message. 2. It builds a classifier prompt from your `prompt` text, the categories (each description gets an `ALLOW:` or `REJECT:` prefix), the recent history when `history` is set, and the user text. 3. It resolves the model and asks for a structured object with `allowed`, `category`, and optional `confidence` (0 to 1) and `reason`. 4. It validates that `category` is a configured key. ViteHub sets `allowed` from the category group, not from the model output. 5. It stores the decision as the Invocation context value `llm-gate` (or your `id`) and provides it as a finish extension under the same id. 6. If the category is a reject category, it throws `ViteHubError` with code `LLM_GATE_REJECTED`. The Agent Driver does not run. A second writer for the same context value fails early. `history: true` includes the last 10 messages. A number includes that many messages. ### Rejection errors The thrown `ViteHubError` has the `message` option text, or `[vitehub] rejected the request.`, and `details` with `capabilityId`, `category`, `confidence`, and `reason` (up to 16,384 characters). Server code that calls `runAgent()` receives this error. HTTP and Invocation clients receive a public error instead. The HTTP status is `403`. The body is `{ code: 'LLM_GATE_REJECTED', error: 'Agent request was rejected.', details: { capability, category } }`. The `message` text and the model `reason` are not sent to the client. ## Jev decisions An ask Driver Agent sends the gate to TypeSafe Jev when the Capability has no `model` option. It asks one `ask.choice()` question with every category. Jev accepts 2 to 255 choices. The state is the Invocation `data` when a caller sets it. Otherwise it is `{ request, history? }` from the latest user text and the `history` option. The decision has `confidence` and `probabilities`, a map from category key to probability. Jev decisions have no `reason`. Credentials come from the `typesafe` Server Env group, as for the ask Driver. ## Requirements - At least one allow category and one reject category. - Category keys must be stable identifiers: a letter first, then letters, digits, `-`, `_`, or `.`. - A model. Model-backed Agents use the Agent model. Provider-backed and custom-run-backed Agents do not expose an Agent model to Capabilities, so set `model`. Without a model, the invocation fails with "requires a model option or an agent model". - For Jev decisions: the `advocaat` package and the `typesafe` Server Env group. See [Use an ask Driver](https://vitehub.dev/docs/agents/agent-drivers#use-an-ask-driver). ## Security and approval `llmGate()` adds no model-facing tool, so the Agent gets no new authority. It has no `policy` option. The end user controls the classified text. The classifier can be wrong or manipulated by that text, so do not use the gate as the only access control. Use authentication and Capability policies for authority. The gate sends the latest user text, the category descriptions, and the optional history to the configured model provider or to TypeSafe Jev. It does not attach, remove, or grant Capabilities. Later behavior must read the recorded decision explicitly. ## Driver support
Agent Driver Support
Model-backed Uses the Agent model or model . Runs before model execution.
Provider-backed Requires model . Runs before provider execution; rejected requests do not start the provider.
Custom-run-backed Requires model . Runs before driver.run ; rejected requests do not reach custom code.
Ask-backed Without model , asks Jev one ask.choice() question with every category. With model , uses that model. Rejected requests do not reach the Driver.
## Verify the gate 1. Run one allowed invocation. Read the finish extension or context value for `llm-gate` (or your `id`) and confirm `allowed: true` and the expected `category`. 2. Run one rejected invocation with `runAgent()`. Confirm the error has code `LLM_GATE_REJECTED` and that the Agent Driver did not run. 3. Send the rejected request over HTTP. Confirm status `403` and the public body without the `message` text or `reason`. ## Options
Option Type Default Description
allow Record required Allowed categories. The description falls back to label , then the key.
reject Record required Rejected categories. Same shape as allow .
history boolean | number false Include recent messages in the decision. true uses the last 10.
id string "llm-gate" Capability id, Invocation context key, and finish extension key.
message string | (decision) => string "[vitehub] rejected the request." Message of the thrown ViteHubError . Not sent to HTTP clients.
model AgentModelResolver Agent model, or Jev for ask Driver Agents Model used for the decision.
prompt string none Text placed before the generated classifier task.
## Related pages - [llmRoute()](https://vitehub.dev/docs/agents/capabilities/llm-route) - [rateLimit()](https://vitehub.dev/docs/rate-limit/agent-capability) - [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) - [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # LLM route > Choose one developer-defined route with a model decision before the Agent Driver runs. `llmRoute()` chooses one developer-defined route before the Agent Driver runs. It adds no model-facing tool. It records the chosen route as an Invocation context value and does not apply route effects by itself. The decision uses one AI SDK model call, or TypeSafe Jev for an [ask Driver](https://vitehub.dev/docs/agents/agent-drivers#use-an-ask-driver) Agent. No Server Primitive is involved. ## Configure routing Define stable choice keys with short descriptions. Later code reads the recorded value and decides 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 in the input phase, before the Agent Driver. 1. It reads the latest user text: the string `prompt`, or else the text of the latest user message. 2. It builds a decision prompt from your `prompt` text, the choices, the recent history when `history` is set, and the user text. 3. It resolves the model and asks for a structured object with `choice` and optional `confidence` (0 to 1) and `reason`. 4. It rejects a `choice` that is not a configured key. 5. It stores the decision as the Invocation context value `llm-route` (or your `id`) and provides it as a finish extension under the same id. A second writer for the same context value fails early. `history: true` includes the last 10 messages. A number includes that many messages. ## Jev decisions When the Capability has no `model` option, an ask Driver Agent records a single configured choice locally with confidence `1` and probability `1`. This route decision needs no credentials and sends no Jev request. The main ask Driver still needs its configured credentials. With 2 to 255 choices, the Capability asks TypeSafe Jev one `ask.choice()` question. The state is the Invocation `data` when a caller sets it. Otherwise it is `{ request, history? }` from the latest user text and the `history` option. The decision has `choice`, `confidence`, and `probabilities`, a map from choice key to probability. Jev decisions have no `reason`. Credentials come from the `typesafe` Server Env group, as for the ask Driver. ## Requirements - At least one choice. - Choice keys must be stable identifiers: a letter first, then letters, digits, `-`, `_`, or `.`. - A model. Model-backed Agents use the Agent model. Provider-backed and custom-run-backed Agents do not expose an Agent model to Capabilities, so set `model`. Without a model, the invocation fails with "requires a model option or an agent model". - For Jev decisions with two or more choices: the `advocaat` package and the `typesafe` Server Env group. See [Use an ask Driver](https://vitehub.dev/docs/agents/agent-drivers#use-an-ask-driver). ## Security and approval `llmRoute()` adds no model-facing tool, so the Agent gets no new authority. It has no `policy` option. It does not attach, remove, or grant Capabilities. Code that acts on the route must read the decision and apply its own checks. The end user controls the routed text, so the user can influence the choice. Do not use a route as access control. The Capability sends the latest user text, the choice descriptions, and the optional history to the configured model provider or to TypeSafe Jev. ## Driver support
Agent Driver Support
Model-backed Uses the Agent model or model . Records the route before model execution.
Provider-backed Requires model . Records the route before provider execution.
Custom-run-backed Requires model . Records the route before driver.run ; custom code decides how to use it.
Ask-backed Without model , records a single choice locally or asks Jev one ask.choice() question with 2 to 255 choices. A driver.ask function can read the decision from Invocation context.
## Verify routing 1. Run one invocation and read the finish extension or context value for `llm-route` (or your `id`). 2. Confirm that the value has `choice`. Model decisions can also have `confidence` and `reason`. Jev decisions have `confidence` and `probabilities`. 3. If you wrap the model, add a test where the model returns a choice outside the map. Confirm that the invocation fails. ## Options
Option Type Default Description
choices Record required Developer-defined route choices. The description falls back to label , then the key.
history boolean | number false Include recent messages in the decision. true uses the last 10.
id string "llm-route" Capability id, Invocation context key, and finish extension key.
model AgentModelResolver Agent model, or Jev for ask Driver Agents Model used for the decision.
prompt string none Text placed before the generated decision task.
## Related pages - [llmGate()](https://vitehub.dev/docs/agents/capabilities/llm-gate) - [Agent Invocations](https://vitehub.dev/docs/agents/invocations) - [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # MCP > Connect an Agent to external MCP server tools. `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. Use `false`, `null`, or `undefined` when a server is not configured for the current deployment or 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, }, }), ], }) ``` Optional servers do not need a conditional Capability array. Declare credentials that may be absent as optional in Server Env, for example `analyticsToken: env({ optional: true, secret: true })`, then resolve only the server whose credentials are available: ```ts [server/agents/support.ts] import { useServerEnv } from '#vitehub/env/server' import { defineAgent } from 'vite-hub/agent' import { mcp } from 'vite-hub/agent/capabilities' import { remoteMcpServer } from 'vite-hub/agent/mcp' export default defineAgent({ driver: { model }, capabilities: [ mcp({ servers: { analytics: () => { const token = useServerEnv().analyticsToken return token ? remoteMcpServer({ headers: { Authorization: `Bearer ${token.unseal()}` }, url: 'https://analytics.example.com/mcp', }) : undefined }, }, }), ], }) ``` ## Authenticate through a Connection Set `connection` on a client config to use a [Connection](https://vitehub.dev/docs/connections) as the server credential. The Connection sends the `Authorization` header, refreshes the token, checks access for each request, and records activity. The Agent and the MCP client never see the token. ```ts [server/agents/support.ts] mcp({ servers: { docs: { connection: 'docs', transport: { type: 'http', url: 'https://docs.example.com/mcp' }, }, }, }) ``` `connection` requires an `http` or `sse` transport config without `authProvider`. The Connection replaces `transport.fetch`. A resolver can also return a config with `connection`, for example to select the URL for each Invocation. Without `vitehub({ connections: true })`, resolution fails with an error that names the Connection. Each request maps to a Connection Operation id:
Request Operation id Effect Activity
HTTP GET or HEAD request fetch read Every request.
HTTP write request, including a tool call or protocol message fetch write Every request.
The MCP transport uses the native Connection `fetch` client. GET and HEAD requests are reads. POST requests, including tool calls and protocol messages, are writes. Without a matching `fetch` rule, writes are denied, so the Agent rule in the Connection must allow the transport: ```ts [server/connections/docs.ts] access: { 'agent:support': { read: true, write: ['fetch'], approve: false }, }, ``` ViteHub checks access before the tool runs. A denied request fails with `CONNECTION_DENIED`. MCP needs protocol responses in the active session, so its Connection client rejects writes that require approval with `CONNECTION_DENIED` before creating an approval. Set `approve: false` on the Agent access rule; durable approval replay cannot resume MCP discovery or tool calls. ## Executor through `mcp()` Use `mcp()` to connect an [Executor](https://executor.sh/) tool catalog. Executor keeps the upstream integration credentials and policies behind one MCP endpoint. Copy the exact endpoint from Executor's **Connect** card. With an OAuth 2 Connection named `executor`: ```ts [server/agents/support.ts] mcp({ servers: { executor: { connection: 'executor', transport: { type: 'http', url: 'https://executor.sh/acme/mcp' }, }, }, }) ``` Allow the transport in the Connection with `access: { 'agent:support': { read: true, write: ['fetch'], approve: false } }`. The `executor` Connection can use an OAuth client for your app, or an [API key Connection](https://vitehub.dev/docs/connections/configure#api-key-connections) with a personal Executor API key. Without Connections, you can also send an Executor API key from Server Env in a resolver. Use a personal API key: Executor rejects organization keys for MCP sessions. ```ts [server/agents/support.ts] mcp({ servers: { executor: () => { const apiKey = useServerEnv().executorApiKey return apiKey ? remoteMcpServer({ headers: { Authorization: `Bearer ${apiKey.unseal()}` }, url: 'https://executor.sh/acme/mcp', }) : undefined }, }, }) ``` The resolver runs for each Agent Invocation, so a rotated key applies to the next Invocation. Executor tools use normalized names such as `mcp_executor_execute`. Pin them with `integrity: { executor: approvedExecutorTools }`. ## 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. An absent entry contributes no tools and creates no MCP client. Other configured servers still resolve. Transient transport failures during resolution or discovery mark the affected server unavailable and let other servers contribute tools. Authentication, configuration, cancellation, protocol, integrity, cleanup, and other resolver failures remain fatal. Generic MCP client configurations default top-level `protocolVersionDiscovery` to `false` so existing initialize-first servers remain compatible. Set it to `true` on a client configuration when the server supports protocol-version discovery. 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 present entry must resolve to an MCP client or MCP client configuration. `false`, `null`, and `undefined` skip that entry. 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. During resolution and tool discovery, transient transport failures make only the affected server unavailable. Other servers retain their tools. HTTP 408, 409, 429, and 5xx responses, recognized network errors, and bounded timeouts produce entries in the Invocation input context at `vitehub.mcp.warnings`. Each entry records the server, phase, and HTTP status when available. Inspection marks the server `Unavailable`. Authentication, configuration, cancellation, protocol errors, duplicate tools, and tool-definition integrity drift remain fatal. A server with `connection` also requires the [Connections](https://vitehub.dev/docs/connections) primitive and a connected Connection. ## 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, optional absent values, or resolvers keyed by server name.
servers..connection string none Name of a Connection in server/connections/ that authorizes an http or sse client config.
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/agents/capabilities/official) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Connections](https://vitehub.dev/docs/connections) - [AI SDK MCP tool-definition drift](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools#detecting-tool-definition-drift-rug-pull) ## Inspect servers and tools Open an Invocation's **Capabilities** tab and select **MCP**. Each configured server has a recorded discovery status, sanitized connection metadata, and its tools. A tool shows its original MCP name, the name exposed to the Agent, its description, and recorded input and output JSON Schema. The snapshot distinguishes resolved servers, skipped optional servers, and resolution or discovery failures. Resolved servers can have no tools. These are observations from the selected Invocation, not current connection health. Opening the view does not reconnect or call tools. MCP resources mounted through Sources are outside this Capability's inventory. Enable `configuration: 'content'` on the Invocation journal to retain the custom view and tool contracts. Missing schemas are shown as not recorded. The built-in Console journal enables configuration capture. # Memory > Add scoped durable memory records that an Agent can search, read, remember, and delete. `memory()` adds scoped durable records that an Agent can search, read, remember, or delete through configured Memory Stores. It adds `memory_search` and `memory_read`, plus `memory_remember` and `memory_delete` when a store opts into tool writes. Memory is explicit Agent behavior. It is not the same as Chat History. Each store owns its adapter, scope, allowed kinds, read tools, and write policy. The built-in `workspaceJsonlMemoryStore()` keeps records in the Agent [Workspace](https://vitehub.dev/docs/workspace). ## Configure memory Configure at least one store with an explicit scope. ```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' }, write: { mode: 'tool', policy: 'require-approval' }, }, }, }), ], }) ``` ## Agent-visible tool contract
Tool Added when Input
memory_search A store allows search. query (required), store , kinds , tags , after , before , limit
memory_read A store allows exact reads. id (required), store
memory_remember A store sets write.mode: 'tool' . content and kind (required), store , title , tags , metadata , pinned , confidence , supersedes , provenance
memory_delete A store sets write.mode: 'tool' . id (required), store , reason
The Agent can omit `store` when the Capability has one store or a store named `agent`. Otherwise it must pass `store`. ## How memory works When the Capability resolves, `memory()` creates each store adapter and resolves each store scope. Every operation uses the store scope, so the Agent cannot read or write records in another scope. `memory_remember` adds provenance from the current Agent Invocation: `runId`, `threadId`, `source: 'tool'`, and `toolName: 'memory_remember'`. When `supersedes` lists record ids, the store marks those records as superseded. `memory_delete` is a soft delete. The JSONL store appends a tombstone and keeps the original line. `memory_remember` checks `allowKinds` for the selected store. Tool calls for a store that does not allow the operation fail. ### Workspace JSONL store `workspaceJsonlMemoryStore()` appends records, supersede events, and delete tombstones to a JSONL file in the Agent Workspace. Each line has the store name, scope, version, and a digest (SHA-256 when Web Crypto is available). Search counts case-insensitive matches of `query` in the title, content, and tags, and returns 10 results by default. ## Requirements - `memory()` requires a `stores` map. Each store requires an adapter and a scope with at least one non-empty field. - `workspaceJsonlMemoryStore()` requires a Workspace. It requires write access when the Agent remembers or deletes records. - A custom adapter implements `search`, `read`, `append`, `delete`, and `export`. A `MemoryStoreFactory` has a `kind` and a `create(context)` function that returns the adapter. ## Security and approval Read tools never require approval. Write tools exist only for stores with `write.mode: 'tool'`. Set `write.policy` on a store to gate writes: - `"allow"` (default): run `memory_remember` and `memory_delete` without approval. - `"require-approval"`: stop the call with an approval request. The write runs after a user approves it. - `"deny"`: reject writes to that store. The store scope comes from configuration or from a scope function that receives the Capability context. The model cannot change it. Model input can set `kind`, `tags`, `metadata`, and `provenance` fields. Provenance values from the model replace the invocation defaults, so do not treat stored provenance as trusted authority. ## 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 the configured memory tools in driver.run({ tools }) .
## Verify memory 1. Run `vitehub agent dev --agent ` and ask the Agent to search its memory. Confirm that the stream shows a `memory_search` tool step. `agent info` does not list memory tools because the Capability adds them at invocation time. 2. Confirm that `memory_remember` and `memory_delete` appear only when a store sets `write.mode: 'tool'`. 3. For the JSONL store, call `memory_remember` and confirm that the Workspace file has a line with the store 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 | (context) => MemoryScope required Scope for every operation on the store. Fields: agent , environment , project , session , tenant , thread , user , workspace .
stores.*.allowKinds MemoryKind[] all kinds Kinds that memory_remember accepts, for example "episodic" , "procedural" , "profile" , or "semantic" .
stores.*.read.tools.search boolean true Allow memory_search for the store.
stores.*.read.tools.read boolean true Allow memory_read for the store.
stores.*.write.mode "off" | "tool" "off" Add memory_remember and memory_delete when set to "tool" .
stores.*.write.policy AgentToolPolicyDecision "allow" Policy for write tools on the store.
stores.*.retention { export?: boolean; hardDelete?: boolean } none Reserved retention settings. memory() does not read them.
### Workspace JSONL store options
Option Type Default Description
path string "memory/memory.jsonl" Workspace-relative JSONL file path.
## Related pages - [Workspace primitive](https://vitehub.dev/docs/workspace) - [Chat History and sessions](https://vitehub.dev/docs/agents/chat-history-sessions) - [chat()](https://vitehub.dev/docs/agents/capabilities/chat) - [Agent instructions](https://vitehub.dev/docs/agents/instructions) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Official capabilities > Choose an official Capability by what the Agent needs to do. 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, channelDelivery, chatSummary, title, db, email, fetch, git, gmail, inputCommands, kv, llmGate, llmRoute, mcp, memory, openapi, otlp, papercuts, progressSummary, rateLimit, sandbox, schedule, skills, transcribe, usage, diagnostics, webSearch, workspaceShell, } from 'vite-hub/agent/capabilities' ``` ## Catalog ### Invocation
Ability Capability Use it when
Invocation access access() Narrow chat admission or Workspace access from trusted invocation identity.
Chat behavior chat() Start Agent Invocations from chat messages and manage Chat History.
Input commands inputCommands() Transform command-shaped user input before the Agent runs.
### Workspace
Ability Capability Use it when
Browser automation browser() A Provider Agent needs a managed headless browser, browser guidance, and screenshot delivery.
Workspace files workspaceShell() Inspect or edit Workspace files, or run configured Workspace commands.
Git source history git() The Agent needs bounded Git source-history inspection or local Workspace Session git state selection.
Skills file skills() The Agent requires a Workspace skill file at invocation time.
Durable memory memory() The Agent needs scoped durable records across invocations.
### Runtime primitives
Ability Capability Use it when
KV storage kv() The Agent needs scoped key-value read or edit tools.
Blob storage blob() The Agent needs scoped object read or edit tools.
Database db() The Agent needs guarded SQL query, schema, or mutation tools.
Email email() Send authorized plain-text messages through the configured Email primitive.
Channel delivery channelDelivery() Send the Agent result through a Channel to a recipient that the application selects.
Sandbox execution sandbox() The Agent may run an allowlisted executable in an isolated runtime.
Schedules schedule() The Agent declares scheduled invocations or manages Runtime Schedules through tools.
OTLP telemetry otlp() Live Agent Invocation events and completed traces should be exported to an OpenTelemetry receiver.
Papercut reporting papercuts() Let an Agent report small failures and wasted work to an application-owned sink.
Operational diagnostics diagnostics() Invocation outcomes and scoped runtime resource observations should go to an application-owned reporter.
### External context
Ability Capability Use it when
MCP servers mcp() Add tools from external MCP servers, such as an Executor catalog, to the Agent.
Web search webSearch() The Agent needs model web search or normalized web search/read tools.
Fetch tools fetch() The Agent needs named HTTP tools for developer-approved endpoints.
OpenAPI tools openapi() The Agent needs a selected OpenAPI operation catalog exposed as bounded HTTP tools or a generated Capability CLI.
Transcription transcribe() Turn audio input into text before model execution.
Gmail gmail() Search and read Gmail or create unsent drafts through a Google Connection.
### Decisions and output
Ability Capability Use it when
LLM routing llmRoute() Choose one developer-defined route with a model before the invocation.
LLM gate llmGate() Allow or reject a request with a model before the invocation.
Rate limit rateLimit() Consume a trusted invocation budget before the Agent runs.
Title title() Generate a title for Agent output, finish extensions, or Channel threads.
Chat summary chatSummary() Replace a summary command with a conversation summary.
Progress summary progressSummary() Summarize current reasoning and tool activity while an Agent streams.
Usage usage() Request provider usage metadata and expose normalized tokens and cost.
## Next steps - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Capabilities API](https://vitehub.dev/docs/agents/capabilities) - [Agent definitions](https://vitehub.dev/docs/agents/agent-definitions) # OpenAPI > Expose selected OpenAPI operations as bounded Agent tools or a generated Capability CLI. `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 bounded timeout or response-size 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. ## Authenticate through a Connection Set `connection` to send each request with the credentials of an OAuth 2 [Connection](https://vitehub.dev/docs/connections). ```ts [server/agents/support.ts] openapi({ spec: 'https://billing.example.com/openapi.json', operations: ['billingListCustomers', 'billingCreateTicket'], connection: 'billing', }) ``` Connection request bodies must serialize to strings for durable approval replay. JSON bodies are serialized automatically. Hooks that supply Fetch bodies such as `URLSearchParams`, `FormData`, or binary data must serialize them to a string first; otherwise the request fails before dispatch. The Connection adds its credentials after `hooks.request`, so hooks never see the token. The capability sends requests through the Connection client as the `fetch` action. `GET` and `HEAD` requests are reads. Other methods are writes. Without a matching `fetch` rule, writes are denied, so allow them in the Agent rule of the Connection: ```ts [server/connections/billing.ts] access: { 'agent:support': { read: true, write: ['fetch'] }, }, ``` ViteHub checks access before a tool runs. A denied request fails with `CONNECTION_DENIED`. A write that requires approval creates a durable Connection approval and fails with `CONNECTION_APPROVAL_REQUIRED` until an administrator approves it. Each call is recorded as Connection activity with the Agent actor, invocation trace id, and `fetch` action. ## Override the request server 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, timeout, and maxResponseBytes values.
hooks.request.provides { body?, path?, query? } none Runtime-owned OpenAPI input fields to remove from model and generated CLI schemas before caller validation.
connection string none Name of a Connection in server/connections/ . It adds credentials, checks access for fetch , and records activity.
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 30000 Default request and response-body timeout in milliseconds.
maxResponseBytes number 5242880 Maximum decoded size of the spec and operation responses. Hooks may lower or raise it up to 25 MiB.
## Related pages - [Fetch](https://vitehub.dev/docs/agents/capabilities/fetch) - [Access](https://vitehub.dev/docs/agents/capabilities/access) - [Connections](https://vitehub.dev/docs/connections) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [CLI](https://vitehub.dev/docs/development/cli) # OTLP > Export live Agent Invocation events and completed traces to an OpenTelemetry receiver. `otlp()` exports Agent Invocation telemetry to an OpenTelemetry receiver through OTLP/HTTP JSON. It adds no model-facing tool and does not wrap a Server Primitive. It registers a telemetry exporter for the invocation trace. The same Agent Definition works with any receiver that accepts ordinary OpenTelemetry logs and traces. ## Configure OTLP export 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', }) ``` ## How OTLP export works 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. The event's `vitehub.agent.configuration.fingerprint` attribute covers these facts and the final resolved instructions, excluding Capability inspection snapshots. Public configurations use a stable `sha256_...` digest. Configurations containing redacted credentials use an `hmac_sha256_...` digest with a private runtime key to prevent offline credential guessing. Receivers can compare secret-bearing context revisions within the same runtime; these fingerprints change when the runtime restarts. 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. ## 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. ## Requirements `otlp({ endpoint })` requires an absolute `http:` or `https:` base endpoint. The receiver must accept OTLP/HTTP JSON at `/v1/traces`, and at `/v1/logs` when `live` is `true`. ## Security and approval `otlp()` adds no model-facing tool, so the Agent cannot call it and there is no approval step. It sends telemetry from your server to the configured receiver. By default, exported telemetry is metadata-only. 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. When a tool came from a Capability, its start, finish, and error events carry `capability.id`. A configured but unused Capability does not add this attribute. Tool input and output remain excluded unless content export is enabled. 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. ## Driver support
Agent Driver Support
Model-backed Exports the invocation trace, including model and tool activity.
Provider-backed Exports the invocation trace, including normalized provider tool and command activity.
Custom-run-backed Exports the invocation trace that ViteHub records around driver.run .
## Verify OTLP export Run one invocation against a local OpenTelemetry Collector with a debug exporter. Confirm that one trace arrives at `/v1/traces` with a `vitehub.agent.configured` event, or with LogRecords at `/v1/logs` when `live` is `true`. Stop the receiver and run the invocation again. Confirm that the Agent output does not change and that a local export error appears. ## 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.
## Related pages - [Agent invocations](https://vitehub.dev/docs/agents/invocations) - [diagnostics()](https://vitehub.dev/docs/agents/capabilities/diagnostics) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Papercuts > Let an Agent report small failures and wasted work to an application-owned sink. `papercuts()` gives the Agent a `report_papercut` tool. The Agent records small, non-blocking friction and then continues the user's task. It does not wrap a Server Primitive. Your application owns storage and review through a `report` callback or a `backend`. With `cli: true`, the Agent also receives a `papercuts report` Capability CLI command. ## Configure papercut reports Pass a callback that writes each report to your store: ```ts [server/agents/support.ts] import { defineAgent } from 'vite-hub/agent' import { papercuts } from 'vite-hub/agent/capabilities' import { reports } from '../storage/reports' export default defineAgent({ capabilities: [ papercuts({ report: event => reports.put(event.papercut.id, event.papercut), }), ], driver: { model: 'openai/gpt-5.1-mini' }, }) ``` To send reports to PostHog, pass the built-in backend instead of a callback: ```ts [server/agents/support.ts] import { defineAgent } from 'vite-hub/agent' import { papercuts, posthogPapercuts } from 'vite-hub/agent/capabilities' export default defineAgent({ capabilities: [ papercuts({ backend: posthogPapercuts({ apiKey: process.env.POSTHOG_API_KEY! }), }), ], driver: { model: 'openai/gpt-5.1-mini' }, }) ``` ## Agent-visible tool contract This definition is resolved from the real Capability during the docs build. :agent-capability-tools{name="papercuts"}## How papercut reports work ViteHub trims the message and rejects empty text or more than 1,000 characters. It then creates a `Papercut` with an ID, a timestamp, and the Agent, run, and trace metadata of the current invocation. With `report`, ViteHub awaits the callback. A rejected callback fails the tool call. With `backend`, delivery is best effort. ViteHub tries up to 3 times with exponential backoff that starts at 100 ms. If all attempts fail, the tool call still succeeds. By default, a backend also skips a report when the same run already reported the same message. When both are set, `report` is used and `backend` is ignored. ### Report contract Each `PapercutReportEvent` contains the Capability runtime context and a `papercut` value. The Capability fills these fields: ```ts interface Papercut { agent?: { name: string, workspace?: string } createdAt: string id: string message: string run?: AgentRunMetadata source: 'cli' | 'tool' trace?: TraceContext } ``` The type also has optional `title`, `description`, `severity`, `category`, `consoleUrl`, `sourceMetadata`, and `reproduction` fields for custom backends. The tool returns only `{ id, reported: true }`. Generic telemetry records the tool's owning `capability.id`, timing, and outcome. It does not add the papercut message to telemetry attributes. ### Durable delivery `createPapercutReporter()` from `vite-hub/agent/capabilities` journals each report in persistent Agent Invocations before delivery. It replays pending reports after a restart, so delivery is at least once. The destination must deduplicate on the stable delivery `uuid`. Pass `report: event => reporter.report(event)` to `papercuts()`, call `reporter.start()` when the server starts, and await `reporter.stop()` on shutdown. ### Tell the Agent when to report Add a Capability directive to Agent Driver Instructions. ViteHub warns when a configured model-facing Capability has no instruction coverage. ```md [server/agents/support/instructions.md] ::capability{key="papercuts"} Report unexpected failures, stale or missing guidance, and wasted turns when they happen. Continue the user's task after a non-blocking report. :: ``` ## Requirements `papercuts()` requires a `report` callback or a `backend` with a `report` function. `cli` must be a boolean when set. `posthogPapercuts()` requires an `apiKey`. ## Security and approval The Agent can only send a short text message. It cannot read earlier reports or choose the destination. The tool description asks the Agent to leave out secrets and customer data, but ViteHub does not filter the message content. Apply your own retention and access policy in the store. `papercuts()` has no `policy` option. Reports run without an approval step. `posthogPapercuts()` sends the full `Papercut` as PostHog event properties. ## Driver support
Agent Driver Support
Model-backed Receives report_papercut .
Provider-backed Receives report_papercut through the private MCP bridge.
Custom-run-backed Receives the tool in the prepared run context; driver.run decides whether to call it.
With `cli: true`, ViteHub exposes the `papercuts` CLI to compatible Agent Drivers and the Agent Dev Loop. See [Add a Capability CLI](https://vitehub.dev/docs/agents/capabilities/custom#add-a-capability-cli). ## Verify papercut reports Run `vitehub agent info --agent --json` and confirm that `tools` contains a `papercuts` entry. Entries use the Capability id, not the tool name. Ask the Agent to report one papercut, then confirm that your store or PostHog project received a record with the same `id` that the tool returned. ## Options
Option Type Default Description
report (event) => void | Promise none Stores each report. A rejection fails the tool call. Required unless backend is set.
backend PapercutBackend none Best-effort delivery target, such as posthogPapercuts() . Used only when report is not set.
retry.attempts number 3 Backend delivery attempts.
retry.delayMs number 100 First backend retry delay in milliseconds. Each retry doubles it.
dedupe boolean true Skips repeated backend reports with the same message in the same run.
cli boolean false Adds the papercuts report Capability CLI command.
### `posthogPapercuts()` options
Option Type Default Description
apiKey string required Sent as the Authorization bearer token, and as the project key unless projectApiKey is set.
projectApiKey string apiKey PostHog project API key for the api_key field.
host string "https://us.i.posthog.com" PostHog host. ViteHub posts to /capture/ .
distinctId string Agent name, then "vitehub-agent" PostHog distinct_id .
fetch typeof fetch globalThis.fetch Custom fetch implementation.
## Related pages - [OTLP](https://vitehub.dev/docs/agents/capabilities/otlp) - [Agent instructions](https://vitehub.dev/docs/agents/instructions) - [Observability](https://vitehub.dev/docs/agents/observability) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Progress summary > Stream a short user-facing summary of current Agent activity. `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. It adds no model-facing tool. A separate generator writes the status: an independent Agent Driver, an AI SDK model call, or a custom `execute()` function. No Server Primitive is involved. ## Configure 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 safety and evidence instructions. The configured Driver selects the model and execution environment using the same contract as an Agent Definition or `title()`. Use `guidance` to append product-specific direction without replacing those safeguards. ## How progress summaries work The Capability wraps the primary output stream and observes its chunks. It tracks reasoning presence, active tools, and the last 5 completed tools. With the default `intervalMs` of `10000`, generation starts when the first primary stream chunk arrives and then runs on 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. Each tick attempts a summary and suppresses unchanged output. 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. After the first summary, it generates again only when reasoning or tool lifecycle events add new activity. Activity that arrives during a generation schedules one follow-up after it settles. The default prompt includes the latest user request with `` payloads removed (up to 2,000 characters), elapsed time, reasoning presence, sanitized tool names, and the previous summary. The request identifies the subject and language; the summarizer instructions treat it as untrusted data. The prompt does not include raw reasoning, tool input, tool output, or hidden instructions. ViteHub cleans the result, bounds it by `maxLength`, and ends a cut summary with `…`. 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 an `agent.progress-summary.error` trace event without logging the Driver error message. ## 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, }, } ``` `data.id` is present when you set the `id` option. The part is transient, so it does not become conversation history. Keep structured reasoning and tool logs separate when your interface exposes them. ## Channel placeholders With `messages.loading` or 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. ## Prompt templates String templates use `@vite-hub/markdown-template` and receive their values under the `data` namespace: `data.userText`, `data.elapsed`, `data.reasoningActive`, `data.reasoningActiveText`, `data.activeTools`, `data.completedTools`, and `data.previous`. Extra variables are available as `data.`. Function templates receive the structured snapshot fields, `input`, `messages`, `elapsedText`, `activeToolsText`, and `completedToolsText`. Keep sensitive content out of prompts you construct. ## Requirements - The primary Agent Driver must return a compatible async stream or UI message stream. - A generator: `driver`, `model`, or `execute`. Without one of these options, the Capability uses the Agent model, which only model-backed Agents have. With no generator, no summaries appear. - A progress `driver` must return text. `driver.ask` is not supported. - A provider `driver` needs a Node.js host. A Cloudflare Worker build fails with `AGENT_B0019`. ## Security and approval `progressSummary()` adds no model-facing tool and has no `policy` option. The default prompt sends the latest user request, elapsed time, reasoning presence, sanitized tool names, and the previous summary to the generator. It does not send raw reasoning, tool input, or tool output. `execute` and function templates receive the full Agent Run Input and messages. Code in those functions controls what it sends. An independent provider `driver` runs with its own configuration, credentials, and permissions, not the primary Driver's. ## Driver support
Agent Driver Support
Model-backed Uses driver , model , execute , or the Agent model.
Provider-backed Requires driver , model , or execute . An independent provider Driver uses its own model and environment settings.
Custom-run-backed Requires driver , model , or execute . The output must be a compatible stream.
## Verify progress summaries 1. Run an invocation and confirm that the primary stream continues immediately while an initial transient `data-progress-summary` part arrives. 2. Keep the invocation open beyond `intervalMs`, then confirm that a later changed summary arrives with a higher `revision`. 3. 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. Must return text.
execute (input) => string | { summary?: string } none Custom progress generator.
guidance string none Product-specific direction appended to the Capability-owned instructions.
id string "progress-summary" Capability id. When set, also sent as data.id in each part.
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.
## Related pages - [title()](https://vitehub.dev/docs/agents/capabilities/title) - [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) - [Channels](https://vitehub.dev/docs/agents/channels) - [Markdown templates](https://vitehub.dev/docs/reference/markdown-templates) - [Markdown pages](https://vitehub.dev/docs/getting-started/ai-resources/markdown-pages) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Skills > Make a Workspace or external Source Skill available to an Agent. `skills()` makes a Skill file in the [Workspace](https://vitehub.dev/docs/workspace), or from an external [Source](https://vitehub.dev/docs/source), available to an Agent Invocation. It adds no model-facing tools unless `shellExecution` is set. Then model-backed Agents receive the Workspace Shell tools in that mode. The [Workspace server API](https://vitehub.dev/docs/workspace/server-api) and [Source server API](https://vitehub.dev/docs/source/server-api) cover application code. This page covers how an Agent receives a Skill. Agent-owned Skills do not need this Capability. Use a folder Agent Definition whose entry file is `agent.ts`, `agent.js`, `index.ts`, or `index.js` (including the `c` and `m` variants), and place `skills/` beside that entry file. ViteHub discovers and materializes those files. Flat files such as `server/agents/support.ts` do not discover sibling Skills. ## 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 is outside the 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', }), ], }) ``` To add more than one Skill, give each additional `skills()` call a unique `id` that starts with `skills.`, for example `skills.browser`. Duplicate Capability ids fail at definition time. Agent inspection uses the `skills` id or `skills.` prefix to check Skill instruction coverage. ## How Skills are loaded ViteHub validates the Workspace path requirement before the Agent Driver runs. The path can point to a directory or directly to a `SKILL.md` file. With `source`, ViteHub adds the Source to the Agent Workspace at definition time and mounts it at the Skill directory. `path` is the canonical mount, even when the Source helper has its own default mount. ViteHub uses normal Workspace Source materialization, visibility, and Agent inspection metadata. `skills()` does not fetch Source files at invocation time. For Provider Drivers, `skills()` adds the Skill directory to the Provider Workspace session. It adds no instructions or tools. With `shellExecution`, model-backed and custom-run-backed Agents receive the Workspace Shell tools in that mode. With `shellExecution: 'write'`, Workspace Shell writes commit Workspace Session changes back into the Workspace. ### Instruction coverage Put model-facing guidance for a Skill in Agent Driver Instructions or in deterministic imported instruction Markdown. Wrap that guidance in an explicit `::skill{path="..."}` coverage block. For model-backed and custom-run-backed Agents, Agent inspection warns when a configured Skill has no coverage block. Provider-backed Agents do not get this warning. ## Requirements - An explicit Workspace with read access to the configured Skill path. `shellExecution: 'write'` requires write access. - A directory-capable Source for a Skill directory. File Sources are single-file and root-confined. Use `github()`, `custom()`, or a root-confined `glob()` Source for a directory. ## Security and approval `skills()` has no `policy` option. A Skill file is guidance for the Agent. Treat a Skill Source as trusted input, and point it at a repository and root that you control or review. `shellExecution` tools are not limited to the Skill directory. `shellExecution: 'read'` lets the Agent inspect the whole visible Workspace. `shellExecution: 'write'` also lets it change any path that Workspace rules allow, without approval. ## Driver support
Agent Driver Support
Model-backed Validates the Skill path requirement. Reads the Skill through Workspace tools when tools are available. Receives Workspace Shell tools with shellExecution .
Provider-backed Mounts the Skill into the Provider Workspace session. Receives no Workspace Shell tools, even with shellExecution .
Custom-run-backed Validates the Skill path requirement before driver.run . Receives Workspace Shell tools in driver.run({ tools }) with shellExecution .
## Verify the Skill 1. Run `vitehub agent info --agent --json` and confirm that the Capability metadata shows the normalized `path` and `skillPath`, plus `sourceKey` when `source` is set. 2. Check `warnings` for an `instruction-coverage:skill:` entry. Add a `::skill{path=""}` block to clear it. 3. Remove the Skill file and run the Agent. Confirm that it fails before model execution with a Workspace path requirement error. ## Options
Option Type Default Description
path string "skills" Directory or SKILL.md path required in the Workspace. A directory resolves to /SKILL.md .
id string "skills" Capability id. Use a unique id such as skills.browser for each additional Skill.
shellExecution "read" | "write" none Workspace Shell mode for model-backed and custom-run-backed Agents.
source WorkspaceSourceInput none Workspace Source to mount at the Skill directory.
sourceKey string derived from path Workspace Source key used with source , for example skill.agent-browser for skills/agent-browser .
## Related pages - [Workspace primitive](https://vitehub.dev/docs/workspace) - [Source primitive](https://vitehub.dev/docs/source) - [Agent instructions](https://vitehub.dev/docs/agents/instructions) - [workspaceShell()](https://vitehub.dev/docs/workspace/agent-capability) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Title > Generate a short title for an Agent Invocation and attach it to output, finish hooks, and Channel threads. `title()` generates a short title for an Agent Invocation. It adds no model-facing tool. It streams the title as output data, provides `{ title }` as a finish extension, and can deliver it to a Channel thread. Generation uses a custom `execute()` function, a separate title Driver, the inherited provider Driver, an AI SDK model call, or a local heuristic. No Server Primitive is involved. Applications can use the finish extension to name a job, run, artifact, or other durable record without a chat interface. ## Configure titles Attach `title()` to any Agent Definition that needs a generated title. When no generator is available, ViteHub uses 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()` reads the prepared first user message, or `input.prompt` when no user message is present. 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. When the message has no semantic text, such as an attachment-only audio or image message, `title()` waits for the successful Agent reply and uses that text. When the reply is also empty or the invocation fails, it leaves the title unset. ViteHub selects one generator in this order: 1. `execute`, when set. 2. `driver`, when set. 3. The inherited provider Driver, when the Agent uses a provider-backed Driver and `model` is unset or a string. 4. An AI SDK model: `model`, or else the Agent model. 5. A heuristic title from the first six words of the source text. The default prompt follows T3 Code's editorial rules: name the subject and desired outcome in 3 to 8 words, under 40 characters. It requests a JSON object with a `title` field. ViteHub keeps the first line, removes quotes, and bounds the result by `maxLength`. Empty output uses `fallback`. When generation fails, the result depends on the generator: - An explicit `driver` uses `fallback` on any failure or timeout. - The inherited provider Driver uses the heuristic title on any failure or timeout. - `execute` and the AI SDK model use the heuristic title on timeout. Other errors leave the title unset. A title failure does not stop the main answer. ### Stream output `title()` wraps compatible async streams and UI message streams, so the title can arrive alongside the response. It does not wrap the same result twice. - UI message streams receive `{ type: 'data-title', id: 'title', data: { type: 'title', title } }`. A provisional heuristic title can arrive first. The generated title replaces it with the same part id. - Async streams and `fullStream` receive `{ type: 'data', data: { type: 'title', title } }`. Finish hooks read the title with `event.extensions.get('title')`, or with your custom `id`. ### Channel delivery For framework-managed Chat SDK message Channels, ViteHub 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. When a Channel invocation fails after title generation started, ViteHub delivers the title with an `ERROR:` prefix. Plain `runAgent()` and UI invocations without framework-managed Chat SDK delivery still receive stream data and finish extensions per invocation. ### Invocation journals When an Invocation journal is configured, title generation starts beside the main answer. Invocation cleanup waits for this work, subject to the title timeout and the invocation abort signal. Metadata journals retain the generated text only when `metadataContent` includes `vitehub.session.title`. The Console enables this selection. ## Provider-backed title runs On a provider-backed Agent, `title()` starts an auxiliary title run through the inherited provider Driver. The run inherits the provider model, credentials, environment, launch configuration, and permissions. It uses title-specific instructions and does not reuse the main Driver's credential profile. A string `model` changes the inherited provider's model name. A model object or resolver uses the AI SDK path instead. `reasoningEffort` changes reasoning effort only for this run and requires an inherited Codex Driver. The auxiliary title run can add provider usage. ViteHub records title model usage as auxiliary usage on the Invocation usage record. Title generation has a default 20-second timeout, configurable with `timeoutMs`. Consumers that await the title, including finish hooks and Invocation cleanup, can wait for that work to settle. Use `execute` for a custom generator that avoids a provider call. ## Inspect title generation Open an Invocation's **Capabilities** tab and select **Title**. The view shows the recorded generation state, generated title, generation method, configured model, length limit, timeout, trigger, and Channel delivery mode. A custom `id` keeps the same Title view. Snapshots update during title generation and remain available after the Invocation ends. Reading them does not generate another title. The view follows configuration retention; use `configuration: 'content'` on the Invocation journal to keep its data independently of prompt and answer content. ## Requirements - A text `prompt` or message input with at least one user message. - Semantic text from the prepared input or a successful Agent reply. - A title `driver` must return text. `driver.ask` is not supported. - `reasoningEffort` requires an inherited Codex Driver. Other provider Drivers fail with `AGENT_R0924`. - Provider Drivers, including a provider Driver passed to `title({ driver })`, need a Node.js host. A Cloudflare Worker build fails with `AGENT_B0019`. Use `template`, `variables`, or `execute` when the title must include product-specific context. ## Security and approval `title()` adds no model-facing tool and has no `policy` option. The end user controls the source text. For model and Driver prompts, ViteHub sends up to the last 8,000 characters of that text, with Chat entity markup removed. A custom `execute` function receives the complete timed input, so it must enforce its own input limit. The default prompt tells the model to treat the source as data, not instructions. An inherited provider title run has the same permissions as the main provider Driver. Use `execute`, a separate `driver`, or an AI SDK `model` when the title run must have less authority. ## Driver support
Agent Driver Support
Model-backed Uses model or the Agent model. Decorates compatible output streams.
Provider-backed Starts an auxiliary title run through the inherited provider Driver unless execute , driver , or a non-string model overrides it. Decorates compatible output streams.
Custom-run-backed No Agent model is available, so set execute , driver , or model ; otherwise uses the heuristic title. Decorates compatible custom output; driver.run controls the response shape.
## Verify titles 1. Run one Agent Invocation and inspect the stream for a `data-title` part or a `title` data event. 2. Add an `agent:finish` hook and confirm that `event.extensions.get('title')` returns `{ title }`. 3. Use `execute: () => ''` and confirm that the title is the `fallback` value, not an empty string. 4. Open the Invocation in the Console and check the **Title** view. ## 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. Must return text.
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, or when an explicit driver fails.
id string "title" Capability id and finish extension key.
instructions string none System instructions for the AI SDK model path, the inherited provider title run, and an explicit model-backed driver .
maxLength number 39 Maximum title length.
model AgentModelResolver Inherited provider Driver, otherwise Agent model, then heuristic A string overrides the inherited provider model name. A model object or resolver selects the AI SDK path.
reasoningEffort string Inherited provider Driver setting Override reasoning effort only for the inherited Codex title run. When omitted, preserve the Driver's reasoning configuration, including environment-based settings. Does not apply to an explicit title driver , custom execute , or AI SDK model.
template string | function generated Prompt template. String templates can use {{ message }} , {{ source }} , {{ fallback }} , {{ maxLength }} , {{ trigger }} , and variables .
timeoutMs number 20_000 Maximum time for title generation; also respects invocation cancellation.
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/agents/capabilities/chat) - [chatSummary()](https://vitehub.dev/docs/agents/capabilities/chat-summary) - [progressSummary()](https://vitehub.dev/docs/agents/capabilities/progress-summary) - [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) - [Channels](https://vitehub.dev/docs/agents/channels) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Transcribe > Convert audio input parts into transcript text before an Agent runs. `transcribe()` converts audio parts in the input messages into transcript text before the Agent Driver receives the input. It adds no model-facing tool. It runs an AI SDK transcription model, or your own `execute()` function, during the input phase. It can also write transcript and source-audio artifacts into a writable [Workspace](https://vitehub.dev/docs/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. It records one `TranscriptionResult` per audio part in the invocation context and exposes the results as a finish extension. Pass a function instead of an options object to resolve the options at invocation time. When `artifacts` is set, it writes the transcript and the source audio to the Agent's writable Workspace. Both artifacts are on by default. Set `artifacts.transcript: false` or `artifacts.audio: false` to turn one off. Use `artifacts.directory` to keep the transcript and source audio together. The generated paths share a sanitized timestamp and 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 [Workflows primitive](https://vitehub.dev/docs/workflows). 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 `model` or `execute`. The `model` path also requires the `ai` package. Artifact persistence requires an explicit writable Workspace (`workspace: { mode: 'write' }`). 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. ## Security and approval `transcribe()` adds no model-facing tool, so the Agent cannot call it and there is no approval step. The Capability sends audio from the input messages to the configured transcription provider. When an audio part has only a URL, the server downloads that URL. Accept audio URLs only from trusted sources. Audio data must stay within `maxBytes` for the built-in executor. A custom `execute` function receives the audio before ViteHub can resolve or measure it, so it must enforce its own limit for direct data, `fetchData()` results, and downloads. Artifact paths must stay inside the Workspace and cannot target the 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. Inspect the finish extension for the transcription results. When artifacts are enabled, inspect the Workspace for the transcript and audio files. ## Options
Option Type Default Description
model AI SDK transcription model required unless execute is set Model used by AI SDK transcription. Other AI SDK transcribe() options, such as providerOptions , are forwarded.
execute ({ audio }) => string | result none Custom transcription function. Mutually exclusive with model .
maxBytes number 26214400 Maximum accepted audio bytes.
artifacts object none Persists artifacts to the Workspace. Requires a writable Workspace.
artifacts.directory string | function generated Directory for generated transcript and audio artifacts.
artifacts.transcript false | object on when artifacts is set Persist transcript artifacts. Set false to turn off.
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 on when artifacts is set Persist source audio artifacts. Set false to turn off.
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) - [Workspace primitive](https://vitehub.dev/docs/workspace) - [Workflows primitive](https://vitehub.dev/docs/workflows) - [Agent invocations](https://vitehub.dev/docs/agents/invocations) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Usage > Request provider usage metadata and expose normalized tokens and cost at finish. `usage()` requests complete provider usage metadata and exposes ViteHub's normalized Agent Usage Record as a typed `usage` finish extension. It adds no model-facing tool. It reads usage metadata that the model provider or Agent Driver reports, and it estimates missing cost from the public [Models.dev](https://models.dev) catalog. No Server Primitive is involved. ## Configure 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()], }) ``` Read the record in a finish hook. The 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) { const record = event.extensions.get('usage') console.log(record?.usage?.totalTokens) console.log(record?.cost?.usd) }, }, }) ``` ## How usage works For the AI SDK model calls of a model-backed Agent Driver, the Capability sets `providerOptions.openrouter.usage.include` to `true`. Existing provider options and OpenRouter usage settings are preserved. At finish, it reads `event.invocation.usage`. The record can contain normalized token usage, model, execution provider, transport, latency, and cost. Fields remain optional when the provider does not report enough data and ViteHub cannot derive them safely. ```ts { model: 'anthropic/claude-sonnet-4.5', provider: 'openrouter', usage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 }, cost: { usd: '0.00375', display: '~$0.00375', estimated: true, source: 'models.dev', }, } ``` Use `cost.usd` for arithmetic or persistence. Use `cost.display` for UI. For streams, ViteHub resolves pricing when usage becomes available, before it emits usage to clients and before finish hooks run. The finish extension is resolved eagerly, so it does not need a finish hook. The Capability id is always `usage`. ## Control pricing Provider-reported cost remains authoritative. When a provider reports tokens without cost, `usage()` uses the Models.dev catalog to estimate regular input, cache-read, cache-write, context-tier, and output token cost. ViteHub caches a successful catalog response for one hour and bounds each request to ten seconds. Pricing is best-effort. A missing provider or model match, unavailable catalog, timeout, invalid rate, or pricing callback error leaves the usage record and successful Agent Invocation unchanged. Pass `pricing: false` when the application needs tokens without estimated cost. ```ts capabilities: [usage({ pricing: false })] ``` The Capability exposes whether pricing is configured in `metadata.pricing`. The Console uses this flag before recorded cost is available. Provider-recorded cost remains available when pricing is disabled. Pass `pricing` when the application owns its rates 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 { usage, 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: [usage({ pricing })], }) ``` Custom pricing receives the model, execution provider, transport, response metadata, Agent Run metadata, and token usage. Return `undefined` when pricing is unavailable. Keep the result deterministic for those inputs because ViteHub may call it while a stream is consumed. Import `modelsDevPricing()` when application-owned work adds usage after the Capability runs and must apply the same catalog behavior. ```ts import { modelsDevPricing } from 'vite-hub/agent/capabilities' const pricing = modelsDevPricing() ``` `modelsDevPricing()` accepts `catalogUrl` (default `https://models.dev/api.json`), `maxAge` in milliseconds (default one hour), `timeout` in milliseconds (default `10000`), and a custom `fetch`. ## Requirements - The model provider or Agent Driver must report token usage. Without usage, the record stays empty and no cost is estimated. - Models.dev pricing needs outbound network access to the catalog URL. Without it, pricing is skipped. ## Security and approval `usage()` adds no model-facing tool and has no `policy` option. The Agent gets no new authority. The default pricing fetches the public Models.dev catalog. ViteHub sends no prompt, output, or usage data in that request. Use `pricing: false` or custom `pricing` to avoid the outbound request. ## Driver support
Agent Driver Support
Model-backed Requests OpenRouter usage metadata on AI SDK calls, then normalizes and prices the reported usage.
Provider-backed Normalizes and prices the usage that the provider Driver reports. The OpenRouter call setting does not apply.
Custom-run-backed Normalizes and prices the usage that the driver.run result reports.
## Verify usage 1. Invoke the Agent with a model that reports token usage. Confirm that the finish extension matches `event.invocation.usage`. 2. If the provider does not report cost, confirm that a matching Models.dev entry adds estimated cost with `source: 'models.dev'`. 3. Test missing and failing pricing. Both must preserve the successful Agent Invocation and raw usage. 4. Run `vitehub agent info --agent support --json` and confirm that the `usage` Capability metadata has `pricing: true`, or `false` with `pricing: false`. ## Options
Option Type Default Description
pricing AgentUsagePricing | false modelsDevPricing() Resolves estimated cost, or disables estimation when set to false .
## Related pages - [Agent Invocations](https://vitehub.dev/docs/agents/invocations) - [Runtime events](https://vitehub.dev/docs/reference/runtime-events) - [Custom Capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Web search > Add model web search or normalized web search and read tools. `webSearch()` gives the Agent web context through one explicit mode. Model mode adds the provider-native `web_search` tool, and the model provider runs the search. Tool mode adds `web_search` and `web_read` tools that ViteHub runs through one configured search provider and the `@agntn/web` package. It does not wrap a Server Primitive. ## Configure web search Use model mode when the model provider supports provider-native web search. ```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' }), ], }) ``` Use tool mode to choose the search provider. Install `@agntn/web` first. ```bash [Terminal] pnpm add @agntn/web ``` ```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: 'tool', provider: 'tavily' }), ], }) ``` ## How web search works Model mode adds the AI SDK provider-defined tool `openai.web_search` under the name `web_search`. The model provider runs the search and returns results to the model. Tool mode loads `@agntn/web` on the first tool call. `web_search` sends the query to the configured provider and returns normalized results with `title`, `url`, and `snippet`. `web_read` reads one URL and returns its content as Markdown. ## 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.
## Requirements `webSearch()` requires `mode: 'model'` or `mode: 'tool'`. Model mode requires a model-backed Driver whose model provider supports the `openai.web_search` provider tool. Tool mode requires the application to install `@agntn/web` and to configure one provider with the credential that the provider requires. ## Security and approval Both modes let the Agent send queries to an external service. Tool mode also lets the Agent read any URL that it passes to `web_read`. There is no domain allowlist for `web_read`. The credential stays in server code. The Agent sees only tool inputs and results. `webSearch()` has no `policy` option, so calls run without an approval step. Use [`fetch()`](https://vitehub.dev/docs/agents/capabilities/fetch) when the Agent should reach only specific endpoints. ## 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 private MCP bridge in tool mode. Model mode fails because Provider Agent Drivers do not accept Provider Tool contributions.
Custom-run-backed Receives the tool-mode tools in the prepared run context; driver.run decides whether to call them.
## Verify web search Run `vitehub agent info --agent --json`. In tool mode, confirm that `tools` contains a `web-search` entry. Entries use the Capability id. Run one invocation and confirm that the trace shows `web_search` or `web_read` calls. Call `web_search` in tool mode without `@agntn/web` installed during development. Confirm that the call fails with a message that asks you to install `@agntn/web` or to use model mode. ## Options
Option Type Default Description
mode "model" | "tool" required Uses provider-native model web search or ViteHub-managed search and 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 | { unseal() } | function environment Credential for providers that require one.
provider.baseURL string provider default Override the provider endpoint.
## Related pages - [fetch()](https://vitehub.dev/docs/agents/capabilities/fetch) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Channels > Connect an Agent to web chat and messaging platforms without mixing transport with identity. 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/agents/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()`, [`gmail()`](https://vitehub.dev/docs/agents/gmail), `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 })`. ## Act on the Channel message in hooks The Channel defines the connector. The Agent reacts in its hooks. `agent:finish` and `agent:error` hooks receive `event.message`, a handle for the Channel message that started the Invocation. Declare the handle's methods with `defineChannel(kind, { message })`. The Trigger returns JSON `message` data that identifies the provider message. Each method receives the Channel context first. `context.message` is the Trigger's data, validated by the `message.data` Standard Schema when you set one. ```ts [server/agents/labeller.ts] import { defineAgent } from 'vite-hub/agent' import { defineChannel, defineChannelTrigger } from 'vite-hub/agent/channels' import * as v from 'valibot' import { applyLabels, readEmail } from '../lib/mailbox' const mailbox = defineChannel('mailbox', { message: { data: v.object({ id: v.string() }), methods: { label: (context, labels: string[]) => applyLabels(context.message.id, labels), subject: { read: true, handler: async context => (await readEmail(context.message.id)).subject, }, }, }, messages: false, triggers: { received: defineChannelTrigger({ input: v.object({ id: v.string(), subject: v.string() }), invoke(context, email) { return { input: { prompt: `Choose one label for: ${email.subject}` }, message: { id: email.id }, run: { channelId: context.trigger.channelId, origin: 'mailbox', runId: `mailbox:${email.id}` }, } }, }), }, }) export default defineAgent({ channels: { mailbox }, driver: { model: 'openai/gpt-5.1-mini' }, hooks: { async 'agent:finish'(event) { if (event.message?.channel !== 'mailbox' || !event.text) return await event.message.label([event.text.trim()]) }, }, }) ``` `event.message` has these properties:
Property Value
channel The Channel name in the Agent's channels map.
kind The Channel Kind, such as 'mailbox' or 'telegram' .
data The Trigger's message data, typed by message.data .
Methods One async function per method, without the context argument.
TypeScript infers the handle from the Agent's `channels`. With several Channels, `event.message` is a union that `event.message.channel` narrows. Invocations without a Channel have `event.message` set to `undefined`. The names `channel`, `data`, and `kind` are reserved. Built-in Channels add the methods that their provider adapter supports. `discord()`, `slack()`, `teams()`, and `telegram()` provide `reply()` when an adapter is configured and messages are enabled. `github()` provides `reply()`, `reaction()`, and `status()` when it has a GitHub App. `event.reply()` still returns a reply that ViteHub delivers after the hook. For a custom Channel, a `reply` method handles it. ### Dry run Set `dryRun: true` in the Invocation input to run an Agent against real messages without changing them. A Trigger can set it in its returned `input`; a direct caller passes it to `runAgent()`. A method declared as a function is a write. In a dry run, ViteHub does not call write methods or built-in delivery, including the automatic reply. It records each call as a skipped delivery in the Invocation trace, and the call returns `undefined`. Methods declared as `{ read: true, handler }` still run. The Console shows the recorded call, such as `label(["Receipts"])`. The call text is Invocation content. A stored Invocation keeps it only with `content: 'content'` or when `metadataContent` lists `channel.effect.content`. The Console store lists it. ## Replay Channel history Add `history` to a Channel to run an Agent on messages that arrived before the Agent existed, or to run them again after a change. `history.collection` is a [Collection](https://vitehub.dev/docs/source/server-api#expose-a-typed-collection). Each item has the shape of the Channel trigger input, so a replayed message takes the same trigger path as a live one. `history.key` returns a stable key for each item, such as the provider message ID. ```ts [server/agents/labeller.ts] import { defineAgent } from 'vite-hub/agent' import { defineChannel, defineChannelTrigger } from 'vite-hub/agent/channels' import { defineCollection } from 'vite-hub/source' import * as v from 'valibot' import { applyLabels, listEmails } from '../lib/mailbox' const email = v.object({ id: v.string(), subject: v.string() }) // Keep history Collections out of server/collections. That directory is a public read model. const inbox = defineCollection(async ({ cursor, limit, query }) => { return await listEmails({ after: cursor, folder: query.folder, limit }) }, { cursor: message => message.id, cursorSchema: v.string(), querySchema: v.object({ folder: v.optional(v.picklist(['archive', 'inbox']), 'inbox') }), }) const mailbox = defineChannel('mailbox', { history: { collection: inbox, key: message => message.id }, message: { data: v.object({ id: v.string() }), methods: { label: (context, labels: string[]) => applyLabels(context.message.id, labels), }, }, messages: false, triggers: { received: defineChannelTrigger({ input: email, invoke: (context, message) => ({ input: { prompt: `Choose one label for: ${message.subject}` }, message: { id: message.id }, }), }), }, }) export default defineAgent({ channels: { mailbox }, driver: { model: 'openai/gpt-5.1-mini' }, hooks: { async 'agent:finish'(event) { if (event.message?.channel === 'mailbox' && event.text) await event.message.label([event.text.trim()]) }, }, }) ``` Set `history.trigger` when the Channel has more than one trigger. Replay the history from the terminal with [`vitehub channels replay`](https://vitehub.dev/docs/development/cli#download-channel-history): ```sh pnpm vitehub channels replay --agent labeller --channel mailbox --folder archive --dry-run ``` Or call `replayChannel()` from trusted server code: ```ts [server/api/backfill.post.ts] import { replayChannel } from 'vite-hub/agent/server' import { getRuntimeContext } from 'vite-hub/runtime/h3' import labeller from '../agents/labeller' export default defineEventHandler(async (event) => { await requireAdmin(event) const { cursor } = await readBody<{ cursor?: string }>(event) return await replayChannel(labeller, 'mailbox', { cursor, limit: 50, query: { folder: 'inbox' }, runtime: getRuntimeContext(event), }) }) ``` `replayChannel()` validates `query` with the Collection's query schema, then reads pages until it reaches `limit` or the end of the history. It returns `processed`, `skipped`, and `failed` counts, one entry per item, and `nextCursor`. Pass `nextCursor` as `cursor` to continue. It is `null` when no history remains. An unconfirmed pending Invocation held by another claim reports a retryable failure. Workflow dispatch records its attempt before calling the provider. If recovery cannot observe that run, the Invocation stays pending and reports a retryable failure; a later replay checks the provider again. A provider status of `unknown` does not prove that an attempted dispatch was rejected. A reservation that records no dispatch attempt can be recovered after its claim expires. Older journals without this marker remain pending while provider status is unknown. Use `force` only when you intend to start a separate Invocation with a fresh ID. Running Invocations and confirmed Workflow dispatches are skipped. Replay also checks journals created with the earlier `channel-replay::` IDs before starting a new Invocation. Each item gets the Invocation run ID `channel::`; percent signs and colons in each component are escaped. Replay skips terminal Invocations and confirmed Workflow dispatches. An active claim prevents concurrent execution of the same item. Inline runs remain recoverable until their terminal status is stored; after process loss, a retry can claim the item when the lease expires. A live message that a trigger [dispatches](#start-several-invocations-from-one-webhook) with the same key has the same ID. This needs an Invocation journal: configure `invocations` or enable the [Console](https://vitehub.dev/docs/development/console). Pass `force: true` to replay handled items again; each forced item gets a new ID. Pass `dryRun: true` to [record Channel message writes](#dry-run) instead of sending them. Dry runs use `channel-dry-run:` IDs, so they never block a later live replay. An inline Agent runs each item before it reads the next one and reports it as `completed`. An Agent with a [Workflow runtime](https://vitehub.dev/docs/agents/invocations) starts one durable Workflow run per item and reports it as `started`. A trigger error, such as invalid item input, marks that item `failed`, and replay continues. [`gmail()`](https://vitehub.dev/docs/agents/gmail#replay-past-mail) provides `history` from a Gmail search. The other built-in Channels do not. The Telegram Bot API cannot read past messages. Slack, Discord, Teams, and GitHub history are not built in; define a custom Channel with a history Collection when you need them. ### Start several Invocations from one webhook Some providers send one webhook for several messages. A Channel trigger can answer the webhook itself and call `context.dispatch(items, { trigger })` to start one Invocation per item through another trigger of the same Channel. Each item has an `input` for that trigger and a `key`. Dispatched items get the same Invocation IDs as a replay, so an item that already has an Invocation is skipped. On a webhook delivery, `context.channelState` gives the trigger the Channel's State Adapter. Store provider cursors under `context.channelState.keyPrefix`. ```ts triggers: { push: defineChannelTrigger({ async invoke(context, input: { messageIds: string[] }) { const messages = await readMessages(input.messageIds) context.waitUntil(context.dispatch(messages.map(message => ({ input: message, key: message.id })), { trigger: 'received' })) return new Response(null, { status: 204 }) }, }), received: defineChannelTrigger({ input: email, invoke: (context, message) => ({ input: { prompt: message.subject }, message: { id: message.id } }), webhooks: [] }), }, ``` Set `webhooks: []` on the trigger that only receives dispatched items, so the Channel's webhook route does not reach it. ## Publish Agent activity without opening a chat Enable `activity` when an invocation should project its lifecycle into a Channel without treating that Channel as the Agent's conversation transport. With GitHub App webhooks enabled, ViteHub creates the authenticated app-owned comment on `pull_request.opened` unless `pullRequest.reconcile.events` explicitly excludes `opened`; later invocations reuse it. If that event is excluded, the first later invocation creates the comment. The comment claims work with a “Starting” row. One table lists the current and recent sessions, newest first, with links, status, GitHub relative start times, and completed durations. Normalized harness task checkboxes and the latest iteration result appear below it. Previous results stay under a collapsed section. The full transcript stays in the linked session when one is configured. Enable the GitHub App webhook for `pull_request` events and route it to the Agent’s generated webhook endpoint to claim the comment when the PR opens, unless `pullRequest.reconcile.events` explicitly excludes `opened`. If it is excluded or the webhook is not delivered, the first later invocation creates the comment. Activity updates are ordered within one process. Across concurrent hosts, GitHub delivery is best-effort: intermediate updates can coalesce, separate hosts can temporarily create duplicate managed comments, and overlapping writes can briefly replace newer state. A later update reconciles owned duplicates and stale state when possible. Within one process, older updates do not replace the current or terminal run. ```ts [server/agents/reviewer.ts] import { defineAgent } from 'vite-hub/agent' import { github } from 'vite-hub/agent/channels' export const agent = defineAgent({ channels: { github: github({ activity: { publicUrl: 'https://agent.example.com' }, app: true, }), }, driver: { model: 'openai/gpt-5.1-mini' }, }) ``` Set `activity.publicUrl` to the public origin of the Agent's ViteHub Console. GitHub pull request webhook runs then receive a link to their own invocation as soon as they start. Use `activity: true` when the application supplies activity links itself. Select the Channel and its destination when the application starts the invocation. Links are application-owned; use them for the current session, memory, or another inspection surface. ```ts import { runAgent } from 'vite-hub/agent' await runAgent(agent, { memo: (_key, create) => create(), runtime: 'vite', waitUntil: schedule.waitUntil ?? (() => {}), run: { activity: { links: [ { label: 'Session', url: sessionUrl }, { label: 'Memory', url: memoryUrl }, ], target: { repository: 'acme/storefront', issue: 42 }, }, channelId: 'github', runId: schedule.runId, }, }, { prompt: 'Converge this pull request.' }, { schedule, output: 'drained' }) ``` The runtime publishes `queued` before capacity admission, `running` after admission, `waiting` for approval requests, and a terminal state on every exit. `data-agent-plan` stream events update the task list, so Codex and other harnesses that expose normalized plans do not need provider-specific GitHub code. Activity delivery failures are reported without replacing the Agent result. ## Reconcile GitHub pull requests `pullRequest: true` accepts declared slash commands on pull request comments. Add `pullRequest.reconcile` when selected pull request lifecycle events should also start an invocation. Mention commands are opt-in and application-owned, so ViteHub does not reserve a bot name. ```ts [server/agents/reviewer.ts] import { defineAgent } from 'vite-hub/agent' import { github } from 'vite-hub/agent/channels' export default defineAgent({ channels: { github: github({ activity: true, app: true, pullRequest: { reconcile: { concurrencyLimit: 4, events: ['opened', 'reopened', 'ready_for_review', 'synchronize'], mentions: ['@agent'], prompt: 'Review this pull request and make any needed changes.', }, }, }), }, driver: { kind: 'codex', permissions: 'allow-all' }, workspace: { mode: 'write' }, }) ``` Reconciled deliveries use `pullRequest.reconcile.concurrencyLimit` concurrent invocation slots per repository and pull request. The default is `1`. Set a positive integer such as `4` to allow up to four deliveries for the same pull request to run together. Other pull requests have separate limits. ViteHub ignores bot-authored `synchronize` events to prevent a bot push from immediately triggering itself. Existing slash commands still work when reconciliation is enabled. Reconciliation starts work; merge policy and any required human consent remain application-owned instructions or Capabilities. Persisted inline webhook executions have a 15-minute default deadline. Set `messages.timeout` in milliseconds to change it, for example `30 * 60_000`. A timeout on the persisted Invocation input takes precedence. The selected timeout must be positive, finite, and at most `2_147_483_647` milliseconds; invalid values use the 15-minute default. Replayed or rehydrated Invocation input can override the deadline after setup, with elapsed setup time deducted. Setup remains bounded by the initial deadline. The deadline includes workspace preparation and cancels the Invocation when it expires. Queued GitHub reconciliation reloads the PR head, comments, and files before the Driver starts, so each Invocation uses the current PR state. Eligibility is decided when the delivery is accepted and is preserved while queued. Use the default `concurrencyLimit: 1` for tasks that write to the same PR branch. Set `pullRequest.workspace.mount` to the repository path inside the Workspace. Omitting `workspace` mounts at `portal`. Both `workspace: true` and `workspace: {}` mount at the Workspace root. Set `workspace: false` to disable the pull request Workspace contribution. When a declared GitHub Source uses the same repository and the same non-root mount, the pull request checkout replaces it for that Invocation. Reads use the pull request head SHA; the declared Source remains unchanged for other Invocations. Different repositories, overlapping parent or child mounts, and Sources contributed by other Capabilities still produce a conflict. ## 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] ``` `useChat()` also exposes a reactive `invocationId`. It is populated when the generated route accepts a request so an application can link to `/_vitehub/agents/~support/invocations/${invocationId.value}`. 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. Agent chat and webhook routes accept at most 1 MiB by default. Set `route.maxBodyBytes` to a smaller limit or raise it as high as 10 MiB for a web chat with larger JSON payloads. ViteHub checks `Content-Length` and the streamed byte count, so chunked requests cannot bypass the limit. ```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. ### Channel Env Built-in Channels read credentials from Server Env under `env.server.`. ViteHub discovers built-in Channel factories in Agent definitions and declares their fields automatically, so applications usually need no separate Env declaration. Explicit Channel options take precedence over Env values. Declare a field yourself when the host variable name or provider differs from the default. Each field first reads its canonical name, `VITEHUB_` and the path in upper snake case, then its vendor names. `telegram()` reads `VITEHUB_TELEGRAM_BOT_TOKEN`, then `TELEGRAM_BOT_TOKEN`. Use the canonical name when the vendor name is taken, for example `VITEHUB_GITHUB_TOKEN` in CI. See [Server Env variable names](https://vitehub.dev/docs/env/configure). Use [Server Env](https://vitehub.dev/docs/env) to inspect the discovered fields and their required or secret status. When a Channel is defined outside a discovered Agent file, declare its Env fields explicitly. 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 Adapter-backed Channels accept messages without a mention only in a direct conversation with the Agent. Group conversations, channels, and shared chats between people require an explicit Agent mention on every message. A previous mention or thread subscription does not grant permission to answer later unmentioned messages. The same rule applies to queued messages and steering an active invocation. Use `messages.filter` to add application-specific restrictions before an invocation starts. Returning `false` posts no loading message or fallback error because the Agent never started. Accepted deliveries have `deliveryKind: 'direct'` or `deliveryKind: 'mention'`. Set `messages.meta` to a Standard Schema when application-owned Channel metadata must be validated before Capabilities, hooks, or the Driver run. The schema may normalize or add defaults, but its output must be an object. Set `metaRevision` to a stable value and change it whenever the schema contract changes so durable Agent Workflows can reuse parsed metadata across processes. Without a revision, durable execution validates the metadata again. Put both settings on shared Agent message settings or on one Channel to override them for that Channel. ```ts import { defineAgent } from 'vite-hub/agent' import * as v from 'valibot' export default defineAgent({ driver: { run: ({ context }) => context.get('channel')?.meta }, messages: { meta: v.object({ audience: v.optional(v.picklist(['support', 'technical'])) }), metaRevision: '1', }, }) ``` 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. For a progress message that is edited while the Agent works, configure `messages.loading`. Its required `text` accepts a string, rotating string array, callback, or `null`; `intervalMs` controls the minimum update interval. Set `updates: 'commentary'` to project explicit commentary text into that message. `messages.loading` selects manual delivery, so it cannot be combined with `messages.stream` or `messages.commentary`. Set `messages.final.delivery: 'new-message'` to post the final answer separately and then remove the loading placeholder. ```ts messages: { loading: { text: ['Looking into it…', 'Still working…'], intervalMs: 1_500, updates: 'commentary', }, final: { delivery: 'new-message' }, } ``` ## 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. An attachment on the message selected by the current reply is also current Agent input automatically. Replies reached only through fetched history remain metadata references, so ViteHub does not repeatedly download old media. Model-backed Drivers can consume inline data and adapter-owned `fetchData` within one invocation-wide byte budget. The default is 25 MiB; set `driver.execution.attachments.maxBytes` to lower it. A URL-only attachment remains a typed reference; ViteHub does not fetch it from the Agent server. Resolve private or provider-owned files in the Channel adapter so its authentication, destination checks, timeouts, and size limits stay in force. This rule is adapter-neutral: every Channel that supplies normalized `replyTo.attachments` receives it without another ViteHub option. Thread ids, link previews, and URLs in message text are not reply content and are never resolved implicitly. If a provider does not normalize its native reply into `replyTo`, fix that provider adapter instead of inferring a thread root in the Agent. 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. Provider download URLs and rehydration metadata are removed after adapter-owned content is available so signed locators do not enter durable Workflow input. ## 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
## Channels and Invocations A Channel and an Agent Invocation are separate records. One Agent Definition can run behind several Channels because of this split.
Channel Agent Invocation
Describes Message origin and delivery Actor, 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.
Use verified Channel metadata to identify the Agent Actor, choose a Capability, or select a Workspace Scope. When a message reaches the wrong Agent, carries the wrong identity, or loses delivery data, inspect the Channel and the [Invocation](https://vitehub.dev/docs/agents/invocations) together. To send an application message without an Agent, use the [Channels Server Primitive](https://vitehub.dev/docs/channels). # Chat History and sessions > Select prior conversation messages without confusing them with durable Agent Memory. Chat History is the ordered list of earlier messages that one chat Invocation can see. A Chat Session marks where one conversation starts and ends. Use them when the Agent must continue a conversation. Neither one is durable Agent Memory. By default, ViteHub sends only the current message to the Agent. Set `triggerHistory` on the Chat Capability to send a bounded window of earlier messages. A Channel adapter's `threadHistory` setting controls backfill and caching only. It does not select model input.
Need Use
Continue the visible thread Thread-backed Chat History
Continue a conversation when the transport thread changes A Chat Session
Keep knowledge or preferences across conversations Memory Capability
## 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: { maxAgeMs: 30 * 60 * 1000, maxMessages: 20, source: 'thread', }, }), ], }) ``` The window limits the messages that the next Invocation receives. It does not delete stored history. - `maxMessages` includes the current message. - `maxAgeMs` is optional. It removes older messages, measured from the current message. A message without a timestamp starts a new window. Thread scope is the usual choice for Discord threads, Slack threads, Teams conversations, GitHub comment threads, and support chats in your application. Adapter-backed Channels load thread history themselves. A route that calls `runAgentTrigger()` or `streamAgentTrigger()` must supply the ordered messages of the current thread, including the new message. `triggerHistory` only limits that array. It does not load messages from `threadId` or from a session id. See [Consume a Capability Trigger](https://vitehub.dev/docs/agents/triggers#consume-a-capability-trigger). ## Add a session Use a session when the product has a stable conversation id that does not depend on 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', }, }) ```
strategy A new session starts when
'manual' A trusted host passes a new session id. sessions: true selects this strategy.
'idle-timeout' The conversation is inactive for idleTimeoutMs .
'hybrid' Either of the two happens.
Without `strategy`, ViteHub uses `'idle-timeout'` when `idleTimeoutMs` is set, and `'manual'` otherwise. ViteHub reads the session id of each message from its metadata, at `metadataKey`. The `chat.message` input can also select a session: `session: { action: 'switch', id }` switches to a session, and `session: { action: 'new' }` starts one with only the current message. The authenticated route or Channel supplies the session id. Do not accept a session id from an untrusted request. It can expose the history of another conversation. ## Partition transcripts Align transcript keys with the product boundary. Use thread keys when each platform thread is independent. Include the Channel or tenant identity when ids can be the same across providers. History selection and storage are separate. The Chat Capability selects a bounded window. The configured Agent State store owns durability, order, retention, and deletion.
Host output Default Agent State
Cloudflare The generated Durable Object binding, also for Channel handlers that generated Workflows invoke. An explicit state provider still wins. A state configuration with only url keeps automatic libSQL selection.
Other production output None. Set a durable VITEHUB_AGENT_STATE_URL or an explicit Agent State provider URL before stateful traffic.
Cloudflare, Vercel, and Netlify production output reject `file:` URLs because their compute filesystems are temporary. ## Keep SQLite transcripts Pass `transcripts: { retention: 'forever' }` to `createLibsqlAgentState()` from `vite-hub/agent/state/sqlite`. The adapter then clears transcript expiry during startup cleanup, including rows that already expired, and ignores later transcript TTLs. Other cached state keeps its normal expiry. ViteHub cannot recover rows that it deleted before you enabled this option. ## Inspect the result Send two messages through the same thread or session. Then inspect the second Invocation with the [CLI](https://vitehub.dev/docs/development/cli). The prepared input contains the bounded earlier messages and the current message. A different thread or session starts without that history. # Child invocations > Start, inspect, respond to, and cancel child Agent work from trusted code. `startAgentInvocation()` starts an Agent and returns a controller. Trusted code uses the controller to inspect the child, cancel it, or send it input while it runs. Use it when a parent Agent, a Capability tool, or host code must control child work after it starts. Use [`runAgent()`](https://vitehub.dev/docs/agents/invocations) when you only need the result. `runAgent()` follows the configured runtime: an inline runtime returns the Agent output, and a Workflow runtime returns a Workflow Run. The controller from `startAgentInvocation()` does not return a final result that you can await. It gives control and inspection. ## 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) } ``` Each start gets a new, stable `child.id`. `inspect()` returns an `available` snapshot or an explicit `unavailable` outcome. An available snapshot has one of these states: `pending`, `running`, `completed`, `failed`, or `cancelled`. An inline or serverless runtime can become unavailable after its process ends. A Workflow-backed child forwards inspection to its Workflow Run while you keep the controller. ViteHub does not keep a registry of controllers, and you cannot look one up by id later. ## 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 that the runtime accepted the request. Inspect again to see the terminal state. A provider can return `unsupported`. A child that already finished returns `invalid-state`. ## Respond to provider requests Check `child.support` before you send input. Then check the result, because support can change with the lifecycle state. ```ts if (child.support.respond) { const result = await child.sendInput( { messages: [responseMessage] }, { mode: 'respond' }, ) } ```
Mode Use it to Support
'respond' Answer a pending provider approval or a data-agent-input question. Inline provider runtimes, while the matching request is pending.
'steer' Add text to the active provider turn. The prompt can be a string or a text-only Message[] . When child.support.steer is true .
'follow-up' Start another turn. Not supported yet.
Steering returns `accepted` when the provider adds the input to the active turn. It returns `unsupported` when the provider cannot do this safely, for example for input with attachments. An unclear submission or cancellation failure returns `invalid-state`. Do not send that input again automatically. Workflow-backed children do not accept input yet. Their runtime adapters do not give the same order and lifecycle guarantees. ## Keep child selection in trusted code The model can call a named application tool. Trusted code selects the Agent Definition, and ViteHub assigns the child id. Do not let model input choose which Agent runs. When a tool uses `runAgent()` instead, handle its runtime-specific return value. Do not assume that every runtime returns a completed result. ## Hand off input resources A host integration that allocates input resources, such as uploaded files, can pass `onInputHandoff` in the fourth argument: ```ts const child = await startAgentInvocation(researcher, runtimeContext, input, { onInputHandoff() { markUploadsAsOwnedByRuntime() }, }) ``` This synchronous callback runs immediately before input can reach the inline runtime or the Workflow provider. - If the start fails before the callback, the host can remove its new input resources. - After the callback, the runtime can keep the input even if the start fails. Keep those resources until you know who owns them. The callback does not confirm durable acceptance. It does not replace cleanup after a process interruption. # Evals > Run repeatable scenarios against an Agent Definition and score its behavior. An Agent Eval sends repeatable input to a real Agent Definition and scores the result. Use Evals to protect behavior that must keep working: grounded answers, expected tool use, and refusals when evidence is missing. An Eval keeps the Driver, Capabilities, and Workspace of the Agent, so it tests more than a model prompt alone. Evals run the Agent inline. Verify Workflow scheduling, durability, and the provider lifecycle separately on the configured host. ## Add one behavior check Install the Eval runner dependencies: ```bash [Terminal] pnpm add -D evalite vitest ``` Create the Eval beside the Agent that 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: ```bash [Terminal] pnpm vitehub agent eval server/agents/support.eval.ts ``` The Eval passes when the Invocation completes and the reply contains `billing`. The command then exits with code `0`. A failed Invocation or a missing text fails the Eval, and the command exits with a non-zero code. You can omit `agent`. A `support.eval.ts` file uses the sibling `support.ts`. A folder `eval.ts` file uses the sibling `agent.ts`. Keep the explicit import when it makes the relation easier to see. ## Test several scenarios Use `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'), ], }, ], }) ``` A scenario accepts normal Agent Invocation input, including `prompt`, `messages`, `context`, call options, timeout, and abort signal. Put unrelated behavior in separate scenarios, so a failure shows which boundary changed. Scorers set on the Eval itself apply to every scenario.
Scorer Passes when
textContains(value) The response text contains a string or matches a regular expression.
callsTool(name) / doesNotCallTool(name) The tool steps include or exclude the tool.
hasCapabilityExtension(id, key?) The Capability reported a finish extension.
staysUnderTokenBudget(limit) Total tokens stay at or below limit .
doesNotLeakSource() The response does not appear to contain source code.
A custom scorer is an object with `name` and a `score(observation)` function that returns `{ score, passed?, reason? }`. ## Test a conversation Use `test(t)` for a conversation. Each `t.send()` call keeps the Chat History of that test. The helpers check the latest observation:
Helper Check
completed() The latest Invocation completed.
textContains(value) The response text contains a string or matches a regular expression.
calledTool(name) / doesNotCallTool(name) The tool steps include or exclude a tool.
hasCapabilityExtension(id, key?) A Capability finish extension exists.
capabilityExtension(id, key?) Returns the Capability finish extension value.
expect(scorer) A scorer passes.
observation / reply The latest normalized observation or response text.
## Compare model variants Variants run the same cases with a different model or instructions: ```ts [server/agents/support.eval.ts] export default defineEval({ agent: support, scenarios, variants: [ { name: 'baseline' }, { name: 'strict', instructions: 'Answer only from inspected evidence.', }, ], }) ``` A variant can replace the model or the instructions of a model-backed Driver, or of a Codex or Claude Code Driver. A provider Driver variant needs a string model id. Use a separate Agent Definition when the change affects Capabilities, Workspace context, custom `driver.run` behavior, or host configuration. ## Configure the runner ViteHub finds Eval files named `*.eval.ts`, `*.eval.mts`, `*.eval.tsx`, and folder `eval.*` files. The `vitehub agent eval` command is available only when at least one Eval file exists. Set defaults for every run under `vitehub({ agent: { eval } })`: ```ts [vite.config.ts] import { defineConfig } from 'vite' import { vitehub } from 'vite-hub' export default defineConfig({ plugins: [ vitehub({ agent: { eval: { cache: true, maxConcurrency: 2, scoreThreshold: 85, testTimeout: 60_000, }, }, }), ], }) ``` Use these flags for one run: `--watch`, `--threshold `, `--output `, `--hide-table`, and `--no-cache`. A CLI flag overrides the configured default. ## Score product behavior Score the behavior that matters to the product: grounded answers, expected tool use, refusal when evidence is missing, Capability finish effects, and changes in usage or latency. Read normalized usage from `observation.usage` and the final trace from `observation.trace`. Keep provider credentials, model selection, permissions, and runtime selection in the Agent Definition. An Eval owns scenarios and scores. If you copy runtime setup into the Eval, you test a different system from the one the application runs. An inline Workspace uses the Eval file directory as its Source root, also when `agent` is an explicit Agent Definition or an async factory. When that directory contains a `workspace/` directory, ViteHub uses that directory. Set `workspace.sourceRootDir` when an imported Agent needs Sources from another directory. A named Workspace keeps its registered configuration. # Gmail Channel > Run an Agent on each new Gmail message and let hooks label, archive, or trash it. `gmail()` connects one Gmail mailbox to an Agent. Gmail sends a Pub/Sub push notification when mail arrives. The Channel verifies the push, reads the new Inbox messages, and starts one Invocation per message. Agent hooks act on the message through `event.message`. The Channel owns the Gmail plumbing: push authentication, the history cursor, the watch renewal, label names, and history replay. Your application writes the Agent and its hooks. On hosts that supply `waitUntil`, the Channel acknowledges an authenticated notification before Gmail API work. Without `waitUntil`, the webhook drains its background work before returning. The first notification verifies the OAuth mailbox in background work before changing the history cursor or handling messages. Later notifications reuse that identity while the configured OAuth credentials are unchanged. Custom `client` functions can rotate credentials behind the same function, so each notification verifies their current mailbox in background work. An authenticated notification for another mailbox is discarded before state or message changes; if the mailbox from configured OAuth credentials is already cached, the webhook rejects it with HTTP 400. The Channel calls the Gmail REST API with `fetch` and a Google OAuth refresh token. It runs on Node.js, Cloudflare Workers, and Vercel. It is separate from the [`gmail()` Capability](https://vitehub.dev/docs/agents/capabilities/gmail) from `vite-hub/agent/capabilities`, which gives an Agent search and draft tools through the `gog` CLI. ## Label new email This Agent chooses one label for each new Inbox message. The hook applies it. ```ts [server/lib/inbox.ts] import { gmail } from 'vite-hub/agent/channels' export const inbox = gmail({ labels: { Work: { description: 'Email from colleagues, clients, and company tools', color: { textColor: '#000000', backgroundColor: '#fad165' }, }, Receipts: { description: 'Purchases, receipts, and invoices', color: { textColor: '#ffffff', backgroundColor: '#16a766' }, }, Newsletters: { description: 'Newsletters and product updates', color: { textColor: '#000000', backgroundColor: '#c9daf8' }, }, }, }) ``` ```ts [server/agents/labeller.ts] import * as v from 'valibot' import { defineAgent } from 'vite-hub/agent' import { inbox } from '../lib/inbox' export default defineAgent({ channels: { gmail: inbox }, driver: { model: 'openai/gpt-5.1-mini', instructions: 'Choose the Gmail label that fits the email. Choose "none" when no label clearly fits.', output: { schema: v.object({ label: v.picklist(['Work', 'Receipts', 'Newsletters', 'none']) }), }, }, hooks: { async 'agent:finish'({ message, result }) { if (message?.channel !== 'gmail' || !result || result.label === 'none') return await message.label(result.label) if (result.label === 'Newsletters') await message.archive() }, }, }) ``` The Channel adds instructions to the Agent: the email content is untrusted data, and the response does not send an email. It also lists each label that has a `description`. Gmail does not store label descriptions. ## Message data and methods `event.message.data` is the message that started the Invocation:
Field Value
id , threadId Gmail message and thread IDs.
from , to , cc , subject Complete header values. to and cc are arrays of addresses.
date ISO 8601 time when Gmail received the message.
snippet Gmail's short preview.
labelIds Label IDs when Gmail delivered the message.
body Plain-text body, decoded with its MIME charset and capped at bodyLimit characters (default 10000). HTML-only mail is converted to text. Attachment-backed MIME bodies are fetched before decoding and limiting the text. Filename-bearing and attachment-disposition subtrees are excluded.
attachments { attachmentId?, filename, mimeType, size } for each filename-bearing attachment. Inline attachments have no attachmentId . The data is not downloaded.
headers Headers by lowercase name, such as list-id , capped at 1000 characters per value. Transport headers such as received and dkim-signature are omitted.
Body decoding supports ISO-8859-1 and the host’s `TextDecoder` encodings. Missing or unsupported charsets use UTF-8 with replacement characters for invalid bytes, so one unsupported charset does not stop mailbox sync. ISO-8859-1 bytes retain their Latin-1 values; Windows-1252 uses its own character mapping. The default prompt lists the headers, then the body. Pass `prompt: message => string` to build your own. `event.message` has these methods:
Method Gmail call
get() Reads the current message. Returns undefined when it was deleted.
thread() Reads every message of the conversation.
label(name) or label([names]) Adds labels.
modify({ addLabels, removeLabels }) Adds and removes labels in one call.
archive() Removes INBOX .
markRead() Removes UNREAD .
star() Adds STARRED .
trash() Moves the message to Trash. Gmail deletes it after 30 days.
Methods take label names. The Channel maps names to IDs with a cached label list. System labels such as `INBOX`, `UNREAD`, and `STARRED` use their IDs as names. A label that you declare in `labels` and that does not exist yet is created on first use with its color. A name that is neither declared nor in Gmail fails the call. `get()` and `thread()` are read methods. The others are writes: in a [dry run](https://vitehub.dev/docs/agents/channels#dry-run) they are recorded in the Invocation trace and do not change Gmail. Set `gmail({ dryRun: true })` to dry-run every pushed message while you check the Agent's decisions in the Console. ## Set up Google Cloud You need a Google Cloud project, an OAuth client for the mailbox, and a Pub/Sub topic that Gmail publishes to. 1. Create a Google Cloud project and enable the **Gmail API** and the **Cloud Pub/Sub API**. 2. Configure the **OAuth consent screen**. Add the scope `https://www.googleapis.com/auth/gmail.modify`. For a personal Gmail account, use the External user type and add yourself as a test user. Google expires refresh tokens after seven days while the app is in the Testing publishing status. Publish the app to keep the token. 3. Create an **OAuth client ID**. Its client ID and client secret are `GMAIL_CLIENT_ID` and `GMAIL_CLIENT_SECRET`. 4. Get a refresh token for the mailbox with the `gmail.modify` scope, for example with the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground) and your own client credentials. The refresh token is `GMAIL_REFRESH_TOKEN`. 5. Create the topic and let Gmail publish to it:```sh gcloud pubsub topics create gmail-push gcloud pubsub topics add-iam-policy-binding gmail-push \ --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \ --role=roles/pubsub.publisher ``` 6. Create a service account that signs the push requests, and a push subscription with OIDC authentication. The push endpoint is the Agent's webhook route: `/api/_vitehub/agents//webhooks/`.```sh gcloud iam service-accounts create gmail-push-invoker gcloud pubsub subscriptions create gmail-push \ --topic=gmail-push \ --push-endpoint=https://mail.example.com/api/_vitehub/agents/labeller/webhooks/gmail \ --push-auth-service-account=gmail-push-invoker@my-project.iam.gserviceaccount.com \ --push-auth-token-audience=https://mail.example.com/api/_vitehub/agents/labeller/webhooks/gmail ``` :brProjects created before April 8, 2021 must also grant the Pub/Sub service agent the Service Account Token Creator role on the invoker service account. ## Configure the credentials Declare the settings in Server Env under `gmail`. Without a Server Env declaration, the Channel reads the environment variables directly. ```ts [vite.config.ts] import { defineConfig } from 'vite' import { vitehub } from 'vite-hub' import { env } from 'vite-hub/env' export default defineConfig({ plugins: [vitehub()], env: { server: { gmail: { clientId: env({ source: env.source('GMAIL_CLIENT_ID') }), clientSecret: env({ secret: true, source: env.source('GMAIL_CLIENT_SECRET') }), refreshToken: env({ secret: true, source: env.source('GMAIL_REFRESH_TOKEN') }), pubsubTopic: env({ source: env.source('GMAIL_PUBSUB_TOPIC') }), pubsubAudience: env({ source: env.source('GMAIL_PUBSUB_AUDIENCE') }), pubsubServiceAccount: env({ source: env.source('GMAIL_PUBSUB_SERVICE_ACCOUNT') }), pubsubSubscription: env({ source: env.source('GMAIL_PUBSUB_SUBSCRIPTION') }), }, }, }, }) ```
Server Env key Environment variable Value
clientId GMAIL_CLIENT_ID OAuth client ID.
clientSecret GMAIL_CLIENT_SECRET OAuth client secret.
refreshToken GMAIL_REFRESH_TOKEN Refresh token of the mailbox, with the gmail.modify scope.
pubsubTopic GMAIL_PUBSUB_TOPIC Topic of the watch, such as projects/my-project/topics/gmail-push .
pubsubAudience GMAIL_PUBSUB_AUDIENCE Audience of the push subscription's OIDC token.
pubsubServiceAccount GMAIL_PUBSUB_SERVICE_ACCOUNT Email of the service account that signs the push requests.
pubsubSubscription GMAIL_PUBSUB_SUBSCRIPTION Full name of the push subscription, such as projects/my-project/subscriptions/gmail-push .
The Channel caches access tokens in process memory. It does not write them to the State Adapter. To call Gmail through another credential owner, pass `client`. It receives `{ method, path, query, body }`, with `path` relative to `https://gmail.googleapis.com/gmail/v1/users/me/`, and returns the JSON response body. Throw an error with a numeric `status` for an HTTP error. ```ts gmail({ client: request => broker.call('gmail', request), }) ``` ## Start the watch Deploy the application first. Then create the managed labels and start the Gmail watch on the topic: ```sh pnpm vitehub channels sync --stage production --channel gmail pnpm vitehub channels sync --stage production --channel gmail --apply ``` The first command prints the plan. The Gmail Channel does not need `--url`, because its resources belong to the Gmail account, not to the deployment. See [CLI channel synchronization](https://vitehub.dev/docs/development/cli#synchronize-channel-webhooks). Gmail permits one active watch per mailbox. `channels sync` reads the mailbox profile and rejects multiple selected Gmail Channels for that mailbox before creating labels or renewing watches, even when the Channels use different credentials. Gmail stops the watch seven days after the last renewal. The push route renews it when the stored expiration is less than a day away, but a quiet mailbox sends no pushes. Renew it from a daily [Schedule](https://vitehub.dev/docs/schedule) with `syncGmailChannel()`, which runs the same logic as `channels sync`: ```ts [server/schedules/gmail-watch.ts] import { defineSchedule } from 'vite-hub/schedule' import { syncGmailChannel } from 'vite-hub/agent/channels' import { inbox } from '../lib/inbox' export default defineSchedule({ cron: '0 6 * * *', async handler() { await syncGmailChannel(inbox, { apply: true }) }, }) ``` ## How pushes become Invocations 1. Pub/Sub sends a push request with a Google-signed OIDC token. The Channel verifies the signature with Google's public keys, the issuer, the audience, the expiry, and the verified service account email. It then checks the subscription name. A failed check returns `401` or `400`. 2. The Channel acknowledges the push with `204` and continues in the background. 3. It reads `history.list` from the stored history cursor, checks each message’s current labels with a metadata-only request, and fetches bodies for messages still in Inbox. Historical labels do not exclude a message restored to Inbox before that check. Failed dispatches remain eligible for retry after a hook removes Inbox. 4. It starts one Invocation per message through the `received` trigger. The Invocation run ID is `channel::`. A terminal Invocation or confirmed Workflow dispatch is skipped. An active claim prevents concurrent execution. A claim for an unconfirmed pending Invocation remains retryable and does not allow the history cursor to advance. 5. It saves page tokens and remaining message IDs in the Channel's State Adapter as synchronization proceeds. The next push resumes unfinished work without fetching completed message bodies again. After every message has an Invocation, it stores the new history cursor and clears the saved progress in one transaction. Mailbox synchronization requires a State Adapter with `mutateWithLock(lock, mutations)`. The operation must check the stored lock token and expiry and apply cache writes or deletions in one transaction. ViteHub's memory, SQLite/libsql, and Cloudflare Durable Object adapters implement this atomic contract. Gmail push routes also require persistent State and return HTTP 503 for missing or in-memory State. Use persistent SQLite/libSQL storage or Cloudflare Durable Objects. Custom adapters must implement `AtomicAgentStateLockAdapter` and declare `readonly durable: true` through `DurableAgentStateAdapter`; a separate lease check followed by `set()` or `delete()` is insufficient. Cloudflare mailbox locks use the existing cache actor, preserving stored cursors and watch state even when other thread locks are sharded. Google OIDC and the Pub/Sub envelope are verified before delivery State is opened or written. Custom SQLite drivers and libSQL clients must set `durable: true` only when their storage survives host restarts. Built-in file and remote libSQL URLs declare persistence automatically; memory URLs remain ineligible. The first push only stores the cursor. Earlier mail belongs to [replay](#replay-past-mail). When Gmail no longer has the stored history, about a week later, the Channel reads Inbox mail from the last two days and continues from the current history. One process at a time reads the mailbox. A push that arrives during a sync marks it pending, and the running sync reads once more. Completed messages are skipped on repeated pushes. Inline runs remain eligible for retry until their terminal status is stored; after process loss, the retry can start when the claim expires. Skipping handled messages needs an Invocation journal: configure `invocations` or enable the [Console](https://vitehub.dev/docs/development/console). The background work runs in the host's `waitUntil`. Cloudflare Workers stop it 30 seconds after the response. For large bursts on Workers, use a [Workflow runtime](https://vitehub.dev/docs/agents/invocations) so each message starts a durable run. The Channel watches the Inbox of one mailbox. Mail that skips the Inbox through a Gmail filter does not start an Invocation. ## Replay past mail The Channel provides `history`: past messages from a Gmail search, in the same shape as a pushed message. Replayed and pushed messages share Invocation IDs, so a message runs once however it arrives. ```sh pnpm vitehub channels replay --agent labeller --channel gmail --query "in:inbox newer_than:30d" --dry-run --limit 50 ```
Query key Value
query Gmail search syntax. Defaults to in:inbox .
labelIds Label IDs that every message must have. Repeat the flag for several.
See [Replay Channel history](https://vitehub.dev/docs/agents/channels#replay-channel-history) for `replayChannel()`, cursors, and `--force`. # Instructions > Write durable model-facing behavior and policy for an Agent. Instructions tell a model or a coding provider how to behave. Use them for durable behavior: which evidence to trust, when to escalate, and what to say when evidence is missing. Model-backed and provider-backed Drivers read instructions. Custom `run` and `ask` Drivers do not. Keep tool schemas in Capabilities and files in the [Workspace](https://vitehub.dev/docs/agents/workspace-context). Instructions describe how to use them. ## 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', }, }) ``` The colocated document is the default when `driver.instructions` is absent. ViteHub parses it as Markdown through Comark. Provider Drivers receive the rendered document as `AGENTS.md` for Codex, or as a prompt file for Claude Code. Use `driver.instructions` for short 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.', ], }, }) ``` `driver.instructions` accepts a string, an array of strings, or a callback that returns them for each Invocation. ## Split reusable guidance Import authored Markdown with `?raw` and combine the parts in `driver.instructions`: ```ts [server/agents/support/agent.ts] import { defineAgent } from 'vite-hub/agent' import sharedStyle from './shared-style.md?raw' import escalationPolicy from './escalation-policy.md?raw' export default defineAgent({ driver: { model: 'openai/gpt-5.1-mini', instructions: [sharedStyle, escalationPolicy], }, }) ``` ViteHub does not follow file references. Text such as `@./path.md` stays literal in the document. ## Extend inherited instructions A parent Agent can reserve one slot for the instructions of each child. Put exactly one `{{{ instructions }}}` marker in `template`, outside code: ```ts [server/agents/reviewer/agent.ts] import { defineAgent } from 'vite-hub/agent' export default defineAgent({ driver: { kind: 'codex', instructions: { template: 'Inspect the request.\n\n{{{ instructions }}}\n\nExplain the result.', content: 'Use concise language.', }, }, }) ``` ```ts [server/agents/migration-reviewer/agent.ts] import { defineAgent } from 'vite-hub/agent' import reviewer from '../reviewer/agent' export default defineAgent({ extends: reviewer, driver: { instructions: 'Check migration safety.' }, }) ``` The child text replaces the slot content. It does not append to it. A child that sets no instructions keeps the parent `content`. A colocated `instructions.md` fills the slot when the template has no `content`. To discard the inherited template, set `instructions: { mode: 'replace', value: '...' }`. A new `{ template, content }` object also replaces it. Without a template, child instructions replace the parent document. Read [Extend an Agent](https://vitehub.dev/docs/agents/agent-definitions#extend-an-agent) for the other inheritance rules. ## Insert trusted invocation values Read Invocation values through `data.context.*` bindings. Use `:insert` for trusted Markdown: ```md [server/agents/support/instructions.md] Answer for {{ data.context.customerName }}. :insert{:markdown="data.context.supportPolicy"} ``` The caller or a Capability must set these values before composition. A missing binding fails the Invocation. ViteHub does not render it as empty text. Templates cannot read request fields, environment variables, or JavaScript expressions. Use conditions for small policy branches: ```md [server/agents/support/instructions.md] ::if{:value="data.context.audience" eq="technical"} Include implementation details and cite file paths. ::else Prefer customer-facing language and next actions. :: :: ``` Conditions bind `data.context.*` paths and support the `eq`, `neq`, `gt`, `gte`, `lt`, and `lte` props. Compute a compound condition in TypeScript and pass a boolean. See [Markdown templates](https://vitehub.dev/docs/reference/markdown-templates) for the full syntax. ## 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 {{ data.workspace.tone }} tone.', ':insert{:markdown="data.workspace.policy"}', ], }, workspace: { bindings: { tone: 'short', policy: { path: 'policies/support.md' }, }, }, }) ``` `:insert` adds the declared Markdown as it is. ViteHub does not evaluate bindings, conditions, or coverage directives inside it. Render dynamic content before you pass it as a fragment. ViteHub does not scan or load other Markdown files in the Workspace. ## Cover configured primitives Say how to use each configured Source, Capability, and Skill. ViteHub records this coverage for inspection. It 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 removes the wrapper directives before the model runs and keeps their text. A file in the Workspace does not count as coverage. A custom Capability with no model-facing behavior can turn off this warning with `instructionCoverage: false`. Use this for accounting and telemetry Capabilities, so they do not add empty text to the prompt. ## Choose where instructions live
Instruction source Use it for
Colocated instructions.md Durable guidance for model-backed and provider-backed execution.
Model driver.instructions Model-facing behavior, including callbacks and bindings that run for each Invocation.
Provider driver.instructions Invocation-scoped policy that ViteHub writes into the provider working directory.
Custom driver.run Application code reads the prepared context directly. ViteHub does not build a model prompt.
Read [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) for Driver-specific behavior. Run `vitehub agent info --agent ` to see the resolved instructions and coverage warnings. See the [CLI development loop](https://vitehub.dev/docs/development/cli). # Invocations > Run, stream, and observe one request to an Agent. An Agent Invocation is one request to an Agent. ViteHub prepares its input, Actor, Capabilities, Workspace, and Driver, then returns or streams the result. ## What happens during an Invocation An Agent Definition describes reusable behavior. An Invocation records one execution of that behavior.
Stage What happens
Entry A route, Channel, schedule, webhook, CLI command, or another caller provides input.
Actor ViteHub resolves the trusted Agent Actor .
Capabilities The Definition and invocation context select the abilities for this request.
Context ViteHub prepares tools, policy, context values, and the Workspace Scope.
Execution The Agent Driver runs the prepared request.
Result ViteHub returns or streams the output and records events and usage.
The Agent can use only the Capabilities selected for that Invocation. A Capability that is not selected adds nothing to the request.
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 Invocation record. Run `vitehub agent info` to inspect the resolved Agent Definition. Run `vitehub agent dev` to talk to the Agent through a running Vite development server. Read [Runtime policy, approvals, and traces](https://vitehub.dev/docs/getting-started/concepts/runtime-policy-approvals-and-traces) for the records produced during execution. ## Run an Agent Use `runAgent()` when the caller needs to invoke the Agent directly. Inline runtimes may return a native `Response` when the Agent produces an HTTP-shaped result. Workflow runtimes return a Workflow Run for durable inspection and control. Structured Agent outputs remain typed values, and streaming uses the separate stream contract below. ```ts [server/api/support.post.ts] import { runAgent } from 'vite-hub/agent' import { getRuntimeContext } from 'vite-hub/runtime/h3' import support from '../agents/support' export default defineEventHandler(async (event) => { const { prompt } = await readBody<{ prompt: string }>(event) const user = await requireAuthenticatedUser(event) const runtime = getRuntimeContext(event) try { return await runAgent(support, runtime, { prompt, context: { invoker: { id: user.id, kind: 'customer', label: user.email, }, }, }) } finally { await runtime.flushWaitUntil().catch(console.error) } }) ``` 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/getting-started/concepts/runtime-context); the third is invocation input. The H3 `getRuntimeContext()` adapter supplies `runtime`, a fresh `memo` cache, and tracked `waitUntil` work. The example drains background work before returning and reports background failures separately. ### Run without a host context For a script or direct invocation, pass the invocation input as the second argument: ```ts const [error, result] = await runAgent(support, { prompt: 'Summarize the support policy.' }) if (error) throw error ``` This form creates a fresh memo cache and run ID, uses the `unknown` runtime, and returns `[null, result]` or `[Error, null]`. It drains background work registered before the call settles. A background failure returns an error tuple; an invocation failure takes precedence if both fail. Non-Error thrown values become an `Error` with the original value as its `cause`. Results follow the configured Agent runtime: inline output stays unchanged, and an explicit Workflow binding returns its Workflow Run. Agents that rely on default host Workflow discovery return an error tuple. Set `runtime: false` for inline execution, configure an explicit `workflow("name")` binding, or use the three-argument form with a host context. This form does not supply request metadata, runtime configuration, or a host background lifetime. Use the three-argument form when those are required, including streams that schedule work during later consumption. The tuple covers the call itself; errors from consuming a returned stream or Response body still occur during consumption. ## 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 { getRuntimeContext } from 'vite-hub/runtime/h3' import support from '../agents/support' 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. Streaming routes must provide a real host `waitUntil` lifetime through the event or the adapter options. A drain before returning cannot cover work scheduled when the caller consumes or cancels the stream. See [Runtime Context](https://vitehub.dev/docs/getting-started/concepts/runtime-context#background-work-and-cleanup). 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 { getRuntimeContext } from 'vite-hub/runtime/h3' import support from '../agents/support' 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. Set `defineAgent({ data })` when callers pass structured values. ViteHub validates `input.data` with the schema before hooks and the Driver run, and returns an error tuple from `runAgent()` for invalid data. See [Accept structured data](https://vitehub.dev/docs/agents/agent-definitions#accept-structured-data). ## 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)) }, }, }) ``` When a Channel started the Invocation, both hooks also receive `event.message`. Use its methods to act on the provider message, such as a reply or a label. Set `dryRun: true` in the Invocation input to record write calls in the trace instead of calling the provider. See [Act on the Channel message in hooks](https://vitehub.dev/docs/agents/channels#act-on-the-channel-message-in-hooks). 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. Finish events set `event.invocation.cancelled` to `true` when output consumption is cancelled. Finish effects can use this field to distinguish cancellation from normal completion. Capability setup callbacks (`configure`, `prepare`, `bind`, `input`, `resolve`, `output`) and `close` emit one `agent.capability.` event when the callback settles. These events measure the callback itself with a monotonic clock, excluding before/after hooks and trace persistence. Read `agent.capability.id`, `agent.capability.phase`, `agent.capability.outcome`, and `agent.capability.durationMs` to distinguish slow integration setup from model execution. Outcomes are `success`, `error`, or `cancelled`; cancellation means the callback failed while the effective invocation input's abort signal was set. Timing events carry the available invocation, run, and trace identifiers. They contain no callback arguments, results, configuration, or thrown error messages, and follow the normal trace and journal retention policy. A missing event means timing is unavailable, not zero. Emission does not await arbitrary trace sinks: pending or rejected persistence cannot change callback results or prevent cleanup. The host still owns its trace sink's flushing and retention policy. 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/agents/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({ maxAgeMs: 30 * 24 * 60 * 60 * 1000, maxRecords: 10_000, url: 'file:./.data/invocations.db', }), }) export default defineAgent({ driver: { model: 'openai/gpt-5.1-mini' }, invocations, }) ``` For a local database file, the SQLite adapter sets the WAL journal mode and `synchronous = NORMAL`. A process crash does not lose committed updates, but a power loss can drop the latest commits. Remote libSQL URLs keep their own settings. While an Invocation runs, the adapter stores each new observation in its own row in the `vitehub_agent_invocations_observations` table, so an update does not rewrite the complete record. The terminal update moves all observations into the `record` column. If you query the tables directly, read running records through `invocations.get(id)`. Use `invocations.getSummary(id)` to read metadata without observation payloads. It returns `undefined` when the Invocation does not exist. Every `AgentInvocationStore` must implement `getSummary(id)`; `get(id)` returns the full record. Custom stores must enforce `claim(id, claimId, leaseMs, { expectedClaimIds })` atomically. Unless `replaceExisting` is `true`, this form can claim an unclaimed record or replace one of the listed claim IDs. It must reject a different owner, including an expired claim. Journals rotate the claim ID for handoff and after an uncertain renewal so cleanup of a timed-out attempt cannot release a newer claim. `release(id, claimId)` must release only that claim ID. The memory, libSQL, and D1 stores enforce these rules. Pass `observationNames` to read only the observations needed for an inspection: ```ts const record = await invocations.get(invocationId, { observationNames: ['agent.invocation.finish'], }) ``` Names match exactly, and matching observations keep their journal order. Other record fields remain unchanged. An empty array returns no observations; omitting `observationNames` returns all retained observations. Both forms return `undefined` for a missing Invocation. D1 and libSQL filter observation payloads in storage. Custom stores may ignore the optional read options; the Invocations wrapper still filters the returned record. The SQLite adapter keeps at most 10,000 terminal records from the last 30 days by default. Pending and running invocations remain available until they reach a terminal state. Set `maxAgeMs` or `maxRecords` to `false` to disable that limit. Retention runs on creates and terminal transitions, so a journal without either event may retain an expired record. The age limit reads only the records it deletes. The count limit reads about `maxRecords` rows, so the SQLite and D1 adapters apply it on about 1 in `ceil(maxRecords / 100)` of these writes. Between runs, the terminal record count can exceed `maxRecords` by about 1%. `invocations.prune()` applies both limits immediately. Delete or prune terminal records on demand: ```ts await invocations.delete(invocationId) // 'deleted' | 'not-found' | 'not-terminal' await invocations.prune({ olderThanMs: 7 * 24 * 60 * 60 * 1000, dryRun: true }) await invocations.prune() // applies the store's maxAgeMs and maxRecords now ``` `delete(id)` keeps a pending or running record and returns `'not-terminal'`. `prune()` deletes completed, failed, and cancelled records last updated before `olderThanMs`. The age must be a non-negative safe integer that produces a cutoff within JavaScript's Date range. Invalid ages fail with `AGENT_R0929`. Without `olderThanMs`, it applies the store's configured retention. Both return the affected IDs, and `dryRun: true` lists them without deleting. The SQLite and D1 adapters and the memory store implement both operations. A custom store must implement `store.delete()` and `store.prune()` to support them. Deletion removes the journal record and its claim. Artifacts that a Capability wrote to Blob storage are not keyed by the journal record, so the application owns their retention. Read the recorded prompt of a finished record to start it again: ```ts import { agentInvocationRerunInput } from 'vite-hub/agent' const record = await invocations.get(invocationId) const input = record ? agentInvocationRerunInput(record) : undefined if (input?.available) { await runAgent(agent, context, { prompt: input.prompt, ...(input.invokerProfileId ? { context: { invokerProfileId: input.invokerProfileId } } : {}), }) } ``` The result has `available: false` and a `reason` when the record cannot reproduce its input. Pending and running records return `invocation-not-terminal`; terminal records can return: `input-not-captured` for a missing prompt, `replay-metadata-unavailable` for legacy or incomplete replay metadata, `input-has-invoker` for a direct invoker or actor identity, `input-has-data` for structured input, `input-has-options` for call options, `input-has-messages` for singular or prior Messages, `input-redacted` for changed input or Invoker Profile replay metadata, or `input-truncated` for a bounded prompt. Direct invoker identities, structured input, and call options are not replayed. A resolver-derived Invoker without a selected Invoker Profile returns `input-has-invoker`; a selected profile is resolved again when the new Invocation starts. The journal keeps `input.prompt` only when `metadataContent` or `content: 'content'` includes it. When the start observation recorded an Invoker Profile, `invokerProfileId` holds the selected profile ID, even when an invoker resolver changes the identity. Direct invocation context beyond an Invoker Profile selection, runtime run metadata beyond the run ID, and timeouts are unavailable for rerun because Console cannot reproduce them. Calls in dry-run mode are unavailable for rerun and return `input-has-dry-run`. Calls with a caller-provided cancellation signal are unavailable for rerun and return `input-has-abort-signal`. A prompt changed by a Capability or input hook is unavailable for rerun and returns `input-prompt-changed`. These cases return `input-has-context`, `input-has-run-metadata`, and `input-has-timeout`. Use `configuration: 'content'` to retain resolved instructions and tool descriptions/schemas independently of other trace content. The default is `configuration: 'metadata'`. Console journals enable configuration retention for inspection; existing records cannot recover contracts that were not saved. Recorded configuration still uses the journal's observation limits and marks truncated values. 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. ### Redact stored evidence Use `redact` to rewrite or drop an observation before the store receives it. Use `redactError` to rewrite the error of a failed record: ```ts [server/agents/support.ts] const invocations = defineAgentInvocations({ store, redact(observation) { if (observation.name === 'agent.tool.result') return undefined const { 'tool.input': _input, ...attributes } = observation.attributes ?? {} return { ...observation, attributes } }, redactError: error => ({ message: error.message, name: error.name }), }) ``` `redact` runs after the content policy and before the journal bounds the observation. It applies to streamed observations, persisted observations, and `appendObservation()` evidence. Return `undefined` to drop evidence; `redactError` can likewise return `undefined` to store no error details while the record remains `failed`. Both hooks are synchronous. 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. Trace persistence failures do not change the Agent Invocation result. The initial cancellation check must succeed before setup; a rejected, missing, or timed-out read fails startup with `AGENT_R0973`. Use `triggeredBy` to filter persisted summaries by the person label recorded in `annotations.triggeredBy`. It matches the trimmed label exactly and composes with Agent, Capability, status, and text filters: ```ts const people = await invocations.listTriggeredBy('support') const page = await invocations.list({ agentName: 'support', triggeredBy: 'Alex', limit: 50, }) const nextPage = page.cursor ? await invocations.list({ agentName: 'support', triggeredBy: 'Alex', limit: 50, cursor: page.cursor, }) : undefined ``` `listTriggeredBy(agentName?)` returns sorted, distinct, non-empty person labels from persisted history, with surrounding whitespace removed. Omit the Agent name to list labels across all Agents. Records without a person label do not match a non-empty `triggeredBy` filter; an empty or whitespace-only filter is ignored. Keep the same filters when following a page cursor. Custom stores can implement `listTriggeredBy()` for a direct lookup; otherwise the Invocations wrapper collects labels by paging through summaries. Use `observations` to set limits for long traces: ```ts const invocations = defineAgentInvocations({ content: 'content', observations: { maxCount: 512, maxStringLength: 256 * 1024, maxBytes: 2 * 1024 * 1024, flushTimeoutMs: 10_000, }, store: createLibsqlAgentInvocationStore({ url: 'file:./.data/invocations.db' }), }) ``` Defaults are 32,768 observations, 65,536 UTF-16 code units of content strings per observation value budget, 16 MiB of serialized observation data, and a 1-second finish drain. Maximum values are 32,768 observations, 1,048,576 code units, 64 MiB, and 60 seconds. The byte limit counts the UTF-8 encoded observations array after privacy filtering. It does not limit provider output, the live trace log, or total process memory. A record keeps its resolved limits when another process resumes it. When a limit is reached, the journal marks `observationsTruncated` and gives lifecycle outcomes priority over ordinary observations. It can strip large outcome content to retain the outcome within the byte limit. `flushTimeoutMs` controls how long finish waits for queued observations; each individual store operation remains bounded to one second. A longer drain can preserve a long queue of successful writes but cannot make an unavailable store reliable. 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. `vitehub agent invocations cancel INVOCATION_ID` sends a cancel request into the Nitro runtime of a Vite + Nitro Development Server. A deployed URL, such as `--url https://app.example.com`, uses that deployment's Console for `list`, `show`, `tail`, and `cancel`. See [Cancel an Agent Invocation](https://vitehub.dev/docs/development/cli#cancel-an-agent-invocation) and [Inspect and cancel on a deployed app](https://vitehub.dev/docs/development/cli#inspect-and-cancel-on-a-deployed-app). Delete and prune open a SQLite or libSQL journal directly: ```sh vitehub agent invocations delete INVOCATION_ID vitehub agent invocations prune --older-than 30d --dry-run --json vitehub agent invocations prune --database file:./.data/invocations.db --older-than 12h ``` Without `--database` or `VITEHUB_AGENT_INVOCATIONS_DATABASE_URL`, both commands use the Console journal: `VITEHUB_CONSOLE_DATABASE_URL`, or `.vitehub/data/console.sqlite` in the project root. Set `VITEHUB_AGENT_INVOCATIONS_DATABASE_AUTH_TOKEN` for an authenticated libSQL endpoint. Durations accept `ms`, `s`, `m`, `h`, `d`, and `w`. `--older-than` defaults to `30d`. Use `--table-prefix` when the store sets `tablePrefix`. The commands refuse a missing database file and never print URL credentials or tokens. For D1, call `invocations.prune()` from a Worker instead. Configured journals also retain failures and cancellation during Workflow preparation, before provider dispatch. Fresh manual starts get distinct invocation IDs. Durable Channel deliveries keep their delivery run ID across preparation attempts. 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. ### Store invocations in Cloudflare D1 Use the D1 adapter when the application already has a D1 database. The binding can be resolved for each operation, so an Agent Definition does not need access to request bindings at module load: ```ts [server/invocations.ts] import { env } from 'cloudflare:workers' import { createD1AgentInvocationStore } from 'vite-hub/agent/invocations/d1' import { defineAgentInvocations } from 'vite-hub/agent/server' export const invocations = defineAgentInvocations({ store: createD1AgentInvocationStore({ database: () => env.DB, maxAgeMs: 30 * 24 * 60 * 60 * 1000, maxRecords: 10_000, }), }) ``` The store creates its table and indexes on first use of each binding in a Worker isolate. The statements use `CREATE ... IF NOT EXISTS`, so concurrent isolates and existing tables are safe. A failed creation is retried on the next operation. To manage the schema with your own migrations, set `migrate: false` and apply `d1AgentInvocationSchema()` before the first request: ```ts [scripts/invocation-schema.ts] import { d1AgentInvocationSchema } from '@vite-hub/agent/invocations/d1' console.log(d1AgentInvocationSchema().join(';\n') + ';') ``` `tablePrefix` defaults to `vitehub_agent_`; pass the same prefix to the schema function and store to use another table name. The table is outside your Drizzle schema, so `vitehub db generate` does not create or drop it. These statements create a new ViteHub-owned schema. They do not convert a custom application journal or the libSQL adapter's tables. Keep an existing journal until its records have been migrated explicitly. D1 batches make creation and retention atomic. Conditional updates retry when another Worker changes the record, so concurrent observations are preserved. Claims use the database clock and fence updates after ownership changes. After 32 concurrent write conflicts, an update rejects instead of overwriting another writer. The store uses the same terminal-record retention defaults and observation deduplication as the libSQL store. It supports Agent, Capability, triggering-person, status, and text filters, lists recorded person labels with `listTriggeredBy()`, and reads summaries without observation payloads. Use `get(id, { observationNames })` to select observation payloads by name. [D1 limits a row to 2 MB](https://developers.cloudflare.com/d1/platform/limits/). The adapter caps retained observations at 1,000,000 UTF-8 bytes, even when the journal requests a larger limit. Each record exposes this resolved limit in `observationLimits`. It also checks the full row, including repeated summary and search text. If that row is too large, it removes ordinary observations and marks `observationsTruncated` while keeping lifecycle fields and previously appended evidence. If the remaining row still cannot fit, the update rejects before a database write. Use another store when the complete long trace must be retained. The adapter targets D1. It does not provide transactions for other Database providers. The database binding stays owned by the host; the store does not open or close it. Use [`redact`](#redact-stored-evidence) to remove sensitive values before they reach D1. Route authorization remains application policy. On the Cloudflare preset, the Console journal uses this store with the D1 Database binding when no Agent Definition configures `invocations`. See [Cloudflare journal](https://vitehub.dev/docs/development/console#know-what-the-console-stores). Local D1 tests cover the SQL and concurrency contract; they do not measure production D1 limits or latency. ## Append delivery evidence Use `appendObservation()` when a host must record an external delivery before or after an Invocation finishes: ```ts const record = await invocations.appendObservation(invocationId, { name: 'report.delivered', type: 'capability', attributes: { 'report.id': reportId }, }, { id: `report-delivered:${reportId}` }) ``` The observation ID is required, must be at most 512 characters, and makes retries idempotent within that Invocation. The store assigns the sequence atomically. This operation does not change Invocation status or its active lease, and it applies the configured content policy. The result is the stored record, or `undefined` when the Invocation does not exist. A failed write or full observation capacity throws, so a caller cannot mistake an omitted event for durable evidence. An append uses the observation limits saved with the record, including after restart. It rejects before changing the record if count, byte, or provider row capacity would remove evidence. Accepted appends remain intact under later trace pressure until the whole Invocation is removed by retention. Keep the same observation ID when retrying a write whose result is unknown. Custom stores must implement the `appendObservation` field on `AgentInvocationStoreUpdateInput`; a store that ignores it fails explicitly. ## Cancel an invocation Call `invocations.cancel(id)` to cancel a pending or running Invocation. The journal records `cancelRequestedAt` first, so every process that shares the store can read the request. ```ts const result = await invocations.cancel(id) if (result.notEnforcedBy) console.warn(`Cancel requested, not enforced by ${result.notEnforcedBy}`) ```
outcome Meaning
requested The request was recorded or sent locally. delivery: 'local' means a run in this process received an aborted signal. delivery: 'journal' means the store retained the request for a current or future execution owner. Neither confirms that execution stopped. A missing journal record still returns requested with local delivery if a local run received the request.
terminal The journal has a final state in status . A stale Driver may still be active. Local abort delivery and durable or local notEnforcedBy warnings remain in the result. A durable warning describes the Driver enforcement contract; it does not prove execution is still active.
not-found The journal has no Invocation with this id and no local run received the request.
unavailable The store did not keep the request and no run in this process holds the Invocation.
Before setup, active runs check for a journal cancellation request. A rejected or missing read, or a check that exceeds one second, fails startup with `AGENT_R0973`. After startup, active runs read journal requests every 10 seconds, including after a lost lease stops claim renewal. A crashed owner or expired lease does not prove that a Driver stopped. An orphaned record remains pending or running until execution recovery observes its request; cancellation does not recover orphaned work or promise a completion deadline. Repeating the request does not change that state. Read the final journal status to confirm cancellation. The Development Server command `vitehub agent invocations cancel ` checks all registered journals before cancellation. If distinct journals contain the same ID, it rejects the request with HTTP `409` without changing either record. Call the intended Agent's `invocations.cancel(id)` to select its journal. Registry entries that share one journal are checked once. If any journal lookup fails, the request fails before cancellation because uniqueness cannot be verified. If the durable request write fails, cancellation still aborts a local run and propagates the storage error. This does not create a durable request for other processes. A local abort can signal a stale execution owner after lease replacement. Cancellation results also retain the current journal owner's durable Driver warning. The pending marker includes `cancelWarningOwnerId`, which identifies its execution owner. A local handle for another owner must verify that marker before reporting success. Custom stores preserve both pending fields; clearing `cancelWarningPending` also clears its owner ID. An observed cancel request aborts the Invocation abort signal. That signal stops the Driver capacity wait, the Driver run, tool calls, queued webhook executions, and scheduled turns. A webhook failure caused by cancellation completes its delivery without a retry. An unrelated failure still retries, even if the Invocation has a historical cancellation request. The record then moves to `cancelled` through the usual `agent.invocation.cancelled` event.
Driver Cancel
model Enforced. The AI SDK request receives the abort signal.
provider ( claude-code , codex ) Enforced. The provider session receives the abort signal.
run Not enforced. The handler receives context.input.abortSignal and can stop on its own. ViteHub cannot stop it.
Cancellation before Driver dispatch stops startup without a `notEnforcedBy` warning. New custom Driver journals persist `cancelWarningPending` at creation until dispatch can be verified. A reused record must confirm this pending state before dispatch if its running update fails. After dispatch, they retry the warning write for up to one minute. A cancellation caller in another process waits up to five seconds for a pending state to become a durable warning or a terminal record. If it cannot verify that state, `cancel()` throws `AGENT_R0974`; the cancellation request is already recorded. Inspect the journal again to confirm the result. Terminal writes preserve `cancelNotEnforcedBy` even if an earlier warning write failed. Custom stores must preserve `cancelWarningPending` on creation and update and clear it when `applyAgentInvocationStoreUpdate()` receives `false`. When a started Driver does not enforce cancel, the result has `notEnforcedBy`, and the record keeps `cancelNotEnforcedBy` and `cancelRequestedAt`. The record stays `running` until the Driver returns. ViteHub does not report the Invocation as cancelled. ## 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, RPC endpoint, 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. A model-facing application Capability tool can use that controller when it needs child control, or `runAgent()` when it handles the configured runtime's return value. ## Scheduled results and process recovery For a scheduled invocation, pass the schedule context and request a drained result: `runAgent(agent, runtimeContext, input, { schedule, output: 'drained' })`. This adds the schedule metadata and consumes streamed driver output before returning. The final response, completion hooks, and capacity release finish together. Configure `driver.output.schema` for a validated result that the scheduler can use directly. A thrown stream or schema error remains a failed invocation; applications do not need to reconstruct results from telemetry. Trusted code can install tools for one inline invocation with the options argument: `runAgent(agent, runtimeContext, input, { tools: { send_message: toolDefinition } })`. For a standalone scheduled call, `runAgent(agent, { prompt }, { schedule, tools: { send_message: toolDefinition }, output: 'drained' })` returns `[error, text]` after the stream finishes. These tools join the agent's tool set for that invocation and are never serialized into `input`. A name collision with an existing tool fails setup. Callable tools select inline execution when the agent only has a host-discovered Workflow default; an explicit `workflow(...)` binding rejects them before starting. Workflow runs continue to return a Workflow Run for calls with a Runtime Context, so `output: 'drained'` only drains inline output. Invocation tools accept JSON Schema, Valibot, and Zod schemas directly. Valibot schemas are converted to JSON Schema for model tool discovery while their Standard Schema validator still checks input before `execute`. Unsupported Valibot actions may be omitted from the model-facing schema; validation remains authoritative. Zod schemas use their Standard JSON Schema converter. Other Standard Schema implementations must provide `~standard.jsonSchema.input` for Provider Agent tools. For a process-owned store, `createProcessAgentInvocations` from `vite-hub/agent/runtime/process` runs interrupted-invocation recovery before returning the journal. Pass the normal `defineAgentInvocations` options and a `recovery` object with a `recover(invocation)` ownership predicate. Use `recover: () => true` only when the database belongs exclusively to that service. Recovery failure rejects startup. With a libSQL Agent state provider, a persistent Nitro server runs this recovery at startup for each Agent journal, before it resumes queued webhook deliveries. It fails the Agent's pending or running invocations that started before the process, except invocations that a persisted queued delivery runs again under the same run ID. Agents with a durable Workflow runtime are skipped. A recovery failure is logged and the queue still resumes. `agentInvocationId(runId, agentName)` from `vite-hub/agent/server` resolves the canonical invocation ID before admission, allowing applications to include a live Console link in Channel activity. For GitHub-backed sessions, `createGitHubWorkspaceInspector(host)` from `@vite-hub/agent/server/github` exposes `list({ repository, revision })` and `read({ repository, revision }, path)`. It requires a full commit SHA, rejects unsafe paths, truncated trees, oversized files, and binary previews, and does not retain disposable checkouts. ## Durable retry budgets On Node hosts, the GitHub inbox can bound repeated provider dispatches and PR work in its SQLite storage. Every inbox method is asynchronous. This is an explicit scheduler API; configuring it does not intercept Agent invocations or classify errors automatically. ```ts import { PullRequestInbox } from 'vite-hub/agent/server/github-inbox' const inbox = new PullRequestInbox({ path: './data/inbox.sqlite', repositories: ['acme/project'], budgets: { providerRetries: 3, noProgress: 3 }, }) ``` Reserve each dispatch, including retries, using a scope shared by workers that use the same provider account. Do not put credentials in the scope. Four reservations are available: one initial attempt plus three retries. Pending reservations count toward that bound, so a worker must wait for other in-flight results when all slots are occupied. Check `providerBudget(scope)` to distinguish pending work from four recorded failures. ```ts const token = await inbox.reserveProviderAttempt('codex:primary-account') if (!token) { // Leave the PR claim unstarted. Inspect pending attempts or exhausted failures. return } const [claim] = await inbox.claim(1) if (!claim) { await inbox.finishProviderAttempt(token, 'other-failure') return } let result try { result = await invokeRepairAgent(claim) } catch (error) { // Application-owned classification: only known retryable provider failures count. const retryable = isRetryableProviderFailure(error) await inbox.finishProviderAttempt(token, retryable ? 'retryable-failure' : 'other-failure') await inbox.release(claim) throw error } await inbox.finishProviderAttempt(token, 'success') try { // Compare GitHub/provider state before and after the invocation. Do not parse prose. const evidence = await verifyNewProgress(claim, result) await inbox.finish(claim, { text: result.text, retry: !evidence, progress: evidence ? { kind: 'verified', evidence } : { kind: 'no-progress' }, }) } catch (error) { await inbox.release(claim) throw error } ``` `invokeRepairAgent`, `isRetryableProviderFailure`, and `verifyNewProgress` above are application functions. Concrete progress can be a newly pushed commit, a verified thread resolution, or a completed merge. A valid external wait belongs in durable scheduler state; if another invocation repeats that unchanged wait, it is no progress. Use evidence identifiers for actual transitions. Credited identifiers persist for that head, so even nonconsecutive replay after a restart or manual reset does not reset the counter. Result text and a successful model response alone are not progress. Three no-progress completions stop claims for the same head, even if result status is completed or another webhook arrives. New head state starts with a fresh budget. Stale claim completions cannot charge or reset the new head. `summary()` exposes `progressBudget` with its head, limit, count, exhaustion state and last verified evidence. The first recorded progress outcome saves the configured limit for that head. Workers retain this limit across configuration changes and restarts until an explicit reset adopts the current limit. A new head uses the current configuration. Provider successes clear only failures from earlier dispatches; late responses cannot clear newer failures. Other errors release their reservation without counting as quota failures. Duplicate completion tokens are ignored. SQLite transactions coordinate workers that open the same database. These guarantees do not extend to hosts with separate databases. There is no time-based unblock. Failure counts and uncompleted reservations survive restart. After inspecting interrupted work or restored quota, an operator can call `resetProviderBudget(scope, reason)`. This invalidates outstanding tokens and adopts the current retry limit. Existing provider scopes retain their limit until reset. For a stopped PR, use `resetProgressBudget(repository, number, expectedHead, reason)`; it rejects an active lease, a closed PR, or an outdated head. It records the reset reason separately and preserves already credited evidence IDs. Keep these operations behind the application's operator authorization. Do not call reset on every webhook or deployment. The inbox cannot prove an application's evidence or authorization. An expired execution lease does not prove that its Driver stopped. Cancellation of an unowned running Invocation remains a durable request until the execution owner observes it. Cancellation of an unowned pending Invocation remains a durable request. A later worker reads the request before starting its Driver and records cancellation. Pending journal status alone does not prove that an earlier Driver never started. # Observability > Export Agent lifecycle events, request failures and durable papercut reports through evlog. Configure observability once in `vitehub()`. Install `evlog` in the host. Add `posthog-node` to export to PostHog. ```ts [vite.config.ts] import { vitehub } from 'vite-hub' import { env } from 'vite-hub/env' export default defineConfig({ plugins: [ vitehub({ preset: 'node', agent: true, console: true, publicUrl: 'https://agents.example.com', observability: { service: 'support-agent', posthog: { apiKey: env({ optional: true, secret: true, source: env.source('POSTHOG_API_KEY') }), host: 'https://eu.i.posthog.com', }, evlog: { sampling: { rates: { info: 0, warn: 100, error: 100 } } }, papercuts: true, }, }), ], }) ``` ViteHub then: - registers the evlog Nitro module with `evlog` as its options and `service` and `environment` as its `env`, in `vite dev` and `vite build`; - declares `posthog.apiKey` as Server Env `observability.posthog.apiKey` and reads it at startup, so the Console Env page shows it; - generates a Nitro plugin that installs one host instance, assigns request IDs, connects the evlog drain and HTTP error hooks, and flushes on shutdown; - adds the observability Capability to every Agent, and with `papercuts`, the `report_papercut` tool; - reports `status()` as `observability` in the Console status endpoint, `/api/_vitehub/console/status`. Remove an existing evlog Nitro module and host plugin when you adopt this option. Without an API key, events still reach local evlog output, `status().configured` is `false`, and `capture()` rejects. Read the instance in server code: ```ts import { useObservability } from 'vite-hub/agent/observability' const observability = useObservability() observability.event('import.completed', { rows: 120 }) await observability.capture('invoice.sent', { invoice_id: id }) observability.exception(error, { operation: 'import' }) observability.status() // { configured, accepted, failed, dropped, pending, closed, papercuts? } ``` `useObservability()` throws when `vitehub({ observability })` is not set. The Capability emits one terminal `$ai_trace` event per invocation through the Agent telemetry lifecycle, including failed preparation. Terminal events include duration, invocation identity, available usage and cost, and completion status. Streaming usage updates do not emit extra terminal events. Internal Agent events bypass the global drain when an exporter is set, to avoid duplicate exports. Hosts without `vitehub()` call `installObservability(options)` from `@vite-hub/agent/observability/host` and pass the returned plugin the Nitro app. `posthog(options)` from `@vite-hub/agent/observability/posthog` creates the exporter. Terminal event delivery runs in the background instead of delaying the final response. Honor the Agent runtime's `waitUntil` tasks. On shutdown, the plugin stops papercut replay, then flushes the exporter. Flush closes the exporter; subsequent ordinary events are dropped and explicit delivery fails. Export calls and the final exporter flush each have a ten-second deadline. Custom exporters receive an abort signal and must stop their I/O when it aborts. Deadlines bound waiting even if an exporter ignores the signal, but cannot terminate arbitrary application code. HTTP request log export includes only request records with a warning or error level or a status of 400 or higher. Request records are identified by evlog's `DrainContext.request` metadata, which the Nitro integration supplies. Other application logs, Agent events and exception delivery are unaffected. Ordinary logs and events are best effort. `maxPending` defaults to 1,000 for event deliveries and independently for the log buffer. `status()` exposes accepted, failed, dropped, pending and closed state. Counters include log records and event deliveries. Raw prompts, model outputs, tool payloads, credentials and common personal identifiers are excluded from exported properties. Unknown error messages are replaced with generic text. This filtering reduces accidental disclosure; it is not a general detector for sensitive prose. Only send properties intended for telemetry. Observability currently requires Nitro-hosted Agents. Netlify and Deno standalone Agent output are not supported. Close the current host before installing another observability instance. ## Deliver papercut reports durably Set `papercuts: true`, or `{ eventPrefix, uuidNamespace, intervalMs }`. It requires `agent` and `console`, because reports are journaled in the Console invocation store. The reporter starts only when an exporter is configured. Use a persistent Agent Invocation store with content retention enabled. Reports require a stored invocation identity. The reporter stores the sanitized envelope before sending it, and only records delivery after the destination acknowledges it. Failed delivery remains available for replay after a restart. Replay reads 100 invocations per page and skips live invocations. Delivery is at least once. A crash between destination acceptance and the persisted acknowledgement can cause a retry, so the destination must deduplicate the stable UUID. PostHog delivery requires an affirmative ingestion response and uses the same UUID and timestamp on retries. One reporter coalesces concurrent sends; separate processes rely on destination deduplication. `eventPrefix` and `uuidNamespace` let existing consumers retain their stored event names and report identities. Keep these stable across deployments. On shutdown the reporter waits up to ten seconds for active work. A timed-out report is not marked as delivered. `status().papercuts` counts deliveries in flight, delivered and failed since start; undelivered reports stay in the journal for replay. Invocation links in events and reports use `vitehub({ publicUrl })`. Without it, events have no `session_url`. Quota checks, application routing and deployment revision labels remain application configuration. # Provider status > Inspect provider authentication and subscription quota without running an Agent. Agent Definitions expose `status(context, { abortSignal })` for provider inspection. Built-in Codex and Claude Code Drivers reuse T3's account and quota probes. Inspection does not create an Agent Invocation, send a model prompt, or open a conversation. The Console shows this evidence on Usage and exposes it through its authenticated `status` RPC operation and `GET /api/_vitehub/console/status`. The optional `agent` query selects a discovered Agent Definition. Console access rules protect this endpoint in production, including when the application uses a custom base path. Inspection remains available when `console.invoke` is disabled; task execution still requires its explicit opt-in. A result includes `agent`, `provider`, `checkedAt`, `stale`, `installed`, `authenticated`, `readiness`, and optional subscription `usageLimits`. Quota windows contain percentages and reset times. They are separate from monetary usage costs. - `ready` means the executable, authentication, and available quota evidence passed inspection. - `unavailable` means an executable is missing, authentication failed, the provider reported an error, or a quota window is exhausted. - `unknown` means inspection could not establish readiness, including when quota is unsupported or its probe failed. - `unsupported` means the Driver does not implement account inspection. Readiness is evidence at `checkedAt`, not a guarantee that a later model request succeeds. Console shares concurrent probes for each Agent Definition, caches results for 30 seconds, and aborts probes after 15 seconds. A failed refresh preserves previous evidence with `stale: true` and `readiness: "unknown"`. The built-in Drivers also keep each probe result for 30 seconds per Agent Definition. Results are grouped by credential source when `driver.credentials` is set, and by the host account otherwise. An invocation preflight in that window reuses the result and does not start a second probe. Inspection resolves the same Driver credentials, credential profile, environment, and launcher as invocations. Provider resolvers receive `purpose: "inspection"` and no actor, invoker, or mounted Workspace. Resolvers that need those invocation fields must handle their absence. A custom launcher receives a temporary working directory and no tool environment requirements. Credential files and launcher files are released after the probe exits. Raw provider diagnostics, account emails, and credentials are excluded from Console status responses. Operational liveness endpoints remain application-owned. # Triggers > Translate product events into Agent Invocations while the Driver continues to own execution. 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 'vite-hub/runtime/h3' export default defineEventHandler(async (event) => { const { prompt } = await readBody<{ prompt: string }>(event) const runtime = getRuntimeContext(event) try { return await runAgent(support, runtime, { prompt }) } finally { await runtime.flushWaitUntil().catch(console.error) } }) ``` Use this direct call for ordinary authenticated server routes and scheduled application code. Without a host lifetime API, drain tracked work before returning. The example reports background failures without replacing the Agent result or error. ## 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 'vite-hub/runtime/h3' 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' }, ) }) ``` This streaming route requires a host lifetime API that stays active until the stream is consumed or cancelled. Calling `flushWaitUntil()` before returning the stream does not cover work scheduled later. See [Runtime Context](https://vitehub.dev/docs/getting-started/concepts/runtime-context#background-work-and-cleanup) for host lifetime ownership. `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, defineChannelTrigger } from 'vite-hub/agent/channels' import * as v from 'valibot' const ticketOpened = v.object({ ticketId: v.string(), summary: v.pipe(v.string(), v.trim(), v.minLength(1)), }) const ticketing = defineChannel('ticketing', { messages: false, triggers: { 'ticket.opened': defineChannelTrigger({ input: ticketOpened, invoke(context, event) { 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' }, }) ``` `defineChannelTrigger()` infers `event` from any [Standard Schema](https://standardschema.dev/) implementation. ViteHub validates and applies schema transforms before `invoke()`. Webhook authentication runs first, and invalid webhook input receives a generic `400 invalid_payload` response without exposing schema details. The Trigger translates the validated event and attaches trusted context. Keep model selection, tools, and execution behavior in the Agent Definition. When the Channel declares message methods, also return `message`: JSON data that identifies the provider message. Hooks use it through `event.message`. See [Act on the Channel message in hooks](https://vitehub.dev/docs/agents/channels#act-on-the-channel-message-in-hooks). ### Report a failed webhook delivery When a Trigger result sets `webhook: { deliveryId, concurrencyLimit }` and the Agent has a state provider, ViteHub queues the webhook delivery and retries a failed execution. A delivery gets three execution attempts. Add `failed` to the Trigger to report the failure to the provider when the queue stops retrying. ```ts defineChannelTrigger({ input: ticketOpened, invoke: (context, event) => ({ input: { prompt: `Triage ticket ${event.ticketId}: ${event.summary}` }, run: { runId: event.ticketId }, webhook: { concurrencyLimit: 1, deliveryId: event.ticketId }, }), async failed(event) { await commentOnTicket(event.run?.runId, `Triage failed: ${event.publicError.error} ${event.invocation?.consoleUrl ?? ''}`) }, }) ``` ViteHub dispatches `failed` at most once per delivery: after the last failed attempt, after an execution timeout, or when the delivery used all its execution leases because the process stopped during each attempt. It does not call `failed` for an attempt that the queue retries, or for a cancelled Invocation. The event has `attempts`, `deliveryId`, `error`, `publicError`, and, when they are known, `input`, `run`, and `invocation: { id, consoleUrl? }`. `consoleUrl` needs a [public URL](https://vitehub.dev/docs/getting-started/concepts/vite-integrations-and-provider-output#public-url). The delivery is already marked as failed when `failed` runs. ViteHub logs an error from `failed` and does not retry the delivery or change its outcome. Keep the callback short, because the queue worker waits for it. With the built-in SQLite state providers, a pending notification survives a restart until a worker claims it. Its `failure.notificationStarted` dispatch marker is permanent, so another worker cannot repeat the callback after a lease expires. Queue ownership still expires: recovery completes the terminal delivery and records the channel failure without replaying `failed`, even if the original callback is paused or its process exited before recording completion. Queue completion therefore does not prove that the callback finished or that its external effects succeeded. Reconcile uncertain effects with the destination using `deliveryId`. `webhookDeliveries(scope)` exposes the dispatch marker until queue completion. Custom queue adapters without durable notification claims provide best-effort, at-most-once dispatch. ## 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 and its Trigger
A model delegates through a trusted application tool A Capability tool backed by startAgentInvocation() for control or runAgent() , handling its runtime-specific return value
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 > Give an Agent scoped files and Sources with explicit read and write authority. 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' }, }) ``` ## Preserve GitHub PR history A Node process host can use `createGitHubHost()` from `@vite-hub/agent/server/github` to prepare an exact PR head with Git. Supply `repository`, `number`, `headSha`, `headRepository`, and `headRef` to `withPullRequestCheckout()`. The host fetches the source branch directly and rejects a changed head. Each pass uses a fresh temporary checkout. The host removes its contents when the callback finishes, including after failure, cancellation, or timeout. Cleanup is best effort and preserves the callback result or error if filesystem discovery or deletion fails. Cleanup retains an empty temporary directory to preserve concurrent pathname replacements. When a provider materializes a separate directory, call the checkout callback's `prepareWorkspace(cwd)` in the provider launch hook. It restores independent Git history and removes saved authentication configuration. Set Workspace `commit: false` if the host will push the provider's commits instead of copying files back through Workspace writeback. When `commit: false` leaves no Workspace or Capability rule with `commit`, the invocation also skips the whole-tree Workspace diff after the run. After the provider finishes, the host can call `checkout.push(cwd)`. This refreshes source credentials, verifies ancestry, and rejects a source branch that changed after preparation. Do not give the provider the host's GitHub credentials when pushes require host approval. Checkout preparation applies only to GitHub PR workspaces and requires Git, not the GitHub CLI. ## 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. # Auth > Configure Better Auth server behavior, route exposure, runtime options, and session checks. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="Better Auth sessions and route guards from one Auth Definition that ViteHub discovers and mounts for your host."} ::code-group ```ts [Definition] import { defineAuth } from '@vite-hub/auth' export default defineAuth(({ env, requestOrigin }) => ({ appName: 'Acme', baseURL: requestOrigin, secret: env.auth.secret.unseal(), socialProviders: { github: { clientId: env.auth.github.clientId, clientSecret: env.auth.github.clientSecret.unseal(), }, }, })) ``` ```ts [Route] import { auth } from '@vite-hub/auth/server' export default defineEventHandler(async (event) => { const headers = new Headers(getRequestHeaders(event)) return auth.api.getSession({ headers }) }) ``` ```ts [Access] 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, }, ], }, }) ``` ```ts [Agent] import { defineAgent } from '@vite-hub/agent' import { authenticated } from '@vite-hub/auth/agent' export default defineAgent({ invoker: authenticated(), driver: { run: ({ invoker }) => ({ invoker }), }, }) ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-user-check" title="Read the session in any server route" to="https://vitehub.dev/docs/auth/server-api"} Better Auth at `/api/auth/**`; guard routes with `withAuth()`. :: ::product-feature-item{icon="i-lucide-shield-check" title="Guard listed routes, decide roles in code" to="https://vitehub.dev/docs/auth/server-api#authorize-access-routes"} `authorize` returns `true`, `false` for `403`, or a `Response`. :: ::product-feature-item{icon="i-lucide-key-round" title="Secrets come from Server Env at request time" to="https://vitehub.dev/docs/auth/configure#runtime-options"} Read `secret` and `baseURL` from Server Env, never Public Env. :: :product-feature-item[Return a concrete Better Auth adapter to persist sessions.]{icon="i-lucide-database" title="Placement metadata is not a database adapter" to="https://vitehub.dev/docs/auth/configure#storage-placement-metadata"}::product-feature-item{icon="i-lucide-bot" title="Map the signed-in user to an Agent Invoker" to="https://vitehub.dev/docs/getting-started/concepts/auth-users-and-agent-invokers"} `authenticated()` maps the session to an `authUser` Invoker. :: :product-feature-item[Route guards can read the authenticated user without provider details.]{icon="i-lucide-user-round-check" title="Sessions carry identity" to="https://vitehub.dev/docs/auth/server-api"} :: # Auth configuration > Write the Auth Definition, supply runtime options, and record storage placement. ## Auth Definition Create one Primary Auth Definition in `server/auth.ts` or `server.auth.ts`. ViteHub also accepts the `.mts`, `.cts`, `.js`, `.mjs`, and `.cjs` extensions. More than one Definition fails the build. ```ts [server/auth.ts] import { defineAuth } from '@vite-hub/auth' export default defineAuth({ appName: 'Acme', emailAndPassword: { enabled: true, }, }) ``` `defineAuth()` accepts Better Auth server options at the top level, plus fields that ViteHub owns.
Option Type Default Description
Better Auth options AuthBetterAuthOptions Better Auth defaults Passed to betterAuth() after ViteHub-owned fields are removed.
database true | { name, dedicated? } Default Database metadata Records the intended Default or Named Database for inspection. It does not create a Better Auth database adapter.
secondaryStorage true | { store } Disabled Records the intended Default or named KV Store for inspection. It does not create a Better Auth secondary storage adapter.
basePath string /api/auth Sets the Auth Base Path. It must start with / .
route false Enabled Disables automatic Auth route exposure.
access.routes AuthAccessRoute[] [] Routes guarded by generated Auth access middleware. Use a route string, or an object with route , optional method , and optional authorize .
access.signIn { provider, callbackURL?, errorCallbackURL?, requestSignUp?, scopes? } None Redirect behavior for HTML requests that withAuth() rejects.
runtime Options object or (context) => options None Supplies runtime-only Better Auth values such as baseURL , secret , secrets , and storage adapters.
`baseURL`, `secret`, and `secrets` are runtime-only. Return them from a Definition callback or put them under `runtime`, not as static top-level fields. `access.routes` must be static so the Vite Integration can register middleware. ## Runtime options Use a Definition callback or the `runtime` field when values depend on the request, Server Env, provider credentials, or request origin. Declare the secrets in [Env](https://vitehub.dev/docs/env) first. ```ts [vite.config.ts] import { hubAuth } from '@vite-hub/auth/vite' import { env, hubEnv } from '@vite-hub/env/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [hubEnv(), hubAuth()], env: { server: { auth: { github: { clientId: env({ source: env.source('GITHUB_CLIENT_ID') }), clientSecret: env({ secret: true, source: env.source('GITHUB_CLIENT_SECRET') }), }, secret: env({ secret: true, source: env.source('BETTER_AUTH_SECRET') }), }, }, }, }) ``` ```ts [server/auth.ts] import { defineAuth } from '@vite-hub/auth' export default defineAuth(({ env, requestOrigin }) => ({ appName: 'Acme', baseURL: requestOrigin, secret: env.auth.secret.unseal(), socialProviders: { github: { clientId: env.auth.github.clientId, clientSecret: env.auth.github.clientSecret.unseal(), }, }, })) ```
Callback input Value
env Typed Server Env when the Env Vite Integration is registered. vitehub() registers it by default.
requestOrigin The origin of the current request. Same-origin apps do not need a separate auth URL variable.
request The current request headers and URL, when a request exists.
Without a `baseURL`, Auth uses `vitehub({ publicUrl })`, then the request origin. Each Auth request or route guard resolves the Definition and `runtime` callbacks once. A later request resolves fresh options. ## Storage placement metadata The `database` and `secondaryStorage` fields describe intended ViteHub primitive placement for inspection. ViteHub removes these values before it calls `betterAuth()`. They do not connect Better Auth to `@vite-hub/database` or `@vite-hub/kv`.
Value Records
database omitted or true The Default Database.
database: { name: 'auth' } The Named Database auth .
database: { name: 'auth', dedicated: true } The Named Database auth , dedicated to Auth.
secondaryStorage omitted No secondary storage.
secondaryStorage: true The Default KV Store.
secondaryStorage: { store: 'auth' } The named KV Store auth .
```ts [server/auth.ts] import { defineAuth } from '@vite-hub/auth' export default defineAuth({ appName: 'Acme', database: { name: 'auth', dedicated: true }, secondaryStorage: { store: 'auth' }, }) ``` To persist Better Auth data, return a concrete Better Auth `database` or `secondaryStorage` adapter from the Definition callback or its `runtime` field. The placement metadata does not substitute for that adapter. ## Vite Integration options `hubAuth()` accepts `false` to disable Auth integration. The Vite config key `auth` overrides the plugin argument. ```ts [vite.config.ts] export default defineConfig({ plugins: [hubAuth()], auth: false, }) ``` # Auth get started > Install Auth, register the Vite integration, and create the first session. ## 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 }, }) ``` Start the Vite dev server. ViteHub mounts Better Auth at `/api/auth/**`, so `POST /api/auth/sign-up/email` creates a user and a session. :: With the `vite-hub` package, enable Auth with `vitehub({ preset, auth: true })` and import from `vite-hub/auth` and its `server`, `agent`, and `vue` subpaths. This default proves discovery and the session route. It is not durable storage. Supply a Better Auth database adapter before you rely on sessions across restarts or replicas. See [Storage placement metadata](https://vitehub.dev/docs/auth/configure#storage-placement-metadata). # Auth hosts > See what Better Auth owns and what Auth generates for each host. ## Providers Better Auth is the only Auth provider. ViteHub does not replace its storage, OAuth providers, or client plugins.
Concern Owner
Sign-in methods, OAuth providers, plugins Better Auth options in the Auth Definition.
User and session storage A Better Auth database adapter that you return from the callback or runtime .
Route mounting hubAuth() in development and a generated Nitro handler in builds. Manual hosts mount #vitehub/auth/server .
Roles and permissions Your authorize callbacks. ViteHub defines no roles.
## Provider output Auth generates the Definition module, route handler, access middleware, and ambient types (`.vitehub/types/auth.d.ts`) that the host integration needs. Application code uses `@vite-hub/auth/server` or Better Auth clients, not generated files. Set `route: false` only when a host integration or a manual route mounts the Auth handler itself. ```ts [server/auth.ts] import { defineAuth } from '@vite-hub/auth' export default defineAuth({ appName: 'Acme', route: false, }) ``` # Auth limits and errors > Check Auth before production and handle Auth error codes. ## Production checks - Supply a strong Better Auth secret from Server Env at request time. Never put it in Public Env, client code, Agent input, logs, or traces. - Set a request-aware `baseURL`, use `requestOrigin`, or set `vitehub({ publicUrl })`, so callback and cookie origins match the deployed host. - Configure a concrete Better Auth database adapter and its migrations. - Protect application and Console pages and APIs explicitly. A session proves identity. Your `authorize` callbacks define roles and permissions. - Keep automatic `/api/auth/**` exposure unless the host mounts the handler itself. Use the same `basePath` on the server and the client. ## Handle required authentication `authenticated()` throws `ViteHubError` with code `AUTHENTICATION_REQUIRED` when a required Auth Session does not exist. 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()) ```
Code When
AUTHENTICATION_REQUIRED authenticated() requires a session and none exists.
AUTH_PROVIDER_OPERATION_FAILED A default Better Auth request or session operation fails. Safe operation details are included. Raw provider diagnostics stay in cause .
AUTH_C#### , AUTH_B#### , AUTH_R#### Missing APIs, malformed responses, invalid Auth Definitions, invalid authenticated() configuration, and invalid custom callback results.
Existing ViteHub errors and structural `AbortError` objects keep their identity. # Auth server API > Read sessions, call Better Auth, and guard routes from server and Vue code. ## Public imports
Import Use
defineAuth from @vite-hub/auth Declare the Primary Auth Definition.
hubAuth from @vite-hub/auth/vite Register Auth discovery, route exposure, and generated server aliases.
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.
withAuth from @vite-hub/auth/server Run a server handler only with an Auth Session.
requireAuthAccessRoutes from @vite-hub/auth/server Guard selected configured access routes.
withAuthorization from @vite-hub/auth/server Run a resource handler only after true or an authorize callback accepts the request. Never redirects to sign-in.
useUserSession , useSession , createAuthClient from @vite-hub/auth/vue Read session state and call Better Auth from Vue.
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.
## Use it at runtime The default Auth route is `/api/auth/**`. ViteHub mounts it automatically, so same-origin apps do not need a manual route file. Vue apps can use the same-origin ViteHub Auth client and normalized session state directly. `useUserSession()` exposes `user`, `session`, sign-in and sign-out actions, and `loggedIn`, `pending`, and `ready` refs. ```ts [lib/auth-client.ts] import { useUserSession } from '@vite-hub/auth/vue' export const userSession = useUserSession() ``` Import `createAuthClient` from the same entry when you use a custom `basePath` or Better Auth client plugins. Pass the same `basePath` to the client and the Definition. 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 }) }) ``` To protect a route, wrap its handler. The guard checks the request first. Your handler runs only when the check passes, and it receives the checked `authorization`. You do not branch on the check result yourself, so a missing `if` cannot run the protected action. ```ts [server/api/profile.get.ts] import { withAuth } from '@vite-hub/auth/server' export default defineEventHandler(withAuth((event, { user }) => ({ id: user.id }))) ``` ## 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.
createAuthAccessHandler(routes, definition?) Creates a handler that matches discovered route metadata, then authenticates and runs every matching authorization rule.
withAuth(handler, definition?) Returns a handler that calls handler(input, authorization) when a session exists. Otherwise it returns an unauthorized or sign-in response.
withAuthorization(authorize, handler, definition?) Returns a handler that calls handler(input, authorization) when allowed. Otherwise it returns JSON 401 without a session, 403 when authorize returns false , or the callback's Response .
The `authorization` argument is a frozen `{ request, session, user }` object. Only the guard creates it. A caller cannot pass its own value to the returned handler. ### Authorize access routes Add `authorize` when a session alone is not enough. ViteHub calls it only after authentication.
authorize returns Result
true The request continues.
false 403 .
A Response That response, as-is.
```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. Map your own role or permission model here. The same callback signature protects [Blob serve routes](https://vitehub.dev/docs/blob/configure#protect-served-objects) and [Collections](https://vitehub.dev/docs/source/server-api#protect-a-collection). Their generated routes wrap the protected read with `withAuthorization(authorize, handler)`. Unlike `withAuth()`, it does not start a sign-in redirect, so image and fetch requests receive a status code. Generated access middleware calls `createAuthAccessHandler(routes, definition?)`. Manual hosts can use it with a Web `Request` or `{ req: Request }`. Pass `{ route, method?, authorize?: true }` metadata in the same order as the Definition's `access.routes`. A route ending in `/**` matches its base path and descendants. Exact routes match only that path. All matching rules apply, and `authorize: true` requires the corresponding runtime callback. Unmatched requests do not load the Auth Definition or session. `requireAuthAccessRoutes(input, routeIndexes, definition, requiredAuthorizeRouteIndexes, { redirectToSignIn: false })` returns `401` for an unauthenticated browser request instead of starting the configured provider sign-in redirect. Use it when the host shows its own sign-in page and starts provider sign-in after an explicit action. By default, it keeps the `access.signIn` redirect. Read [Console](https://vitehub.dev/docs/development/console#protect-the-console-route) for its page, RPC endpoint, provider status route, and disabled behavior. The Console can also use its own Console Auth instead of these access routes. On Cloudflare Workers, the [Cloudflare Access provider](https://vitehub.dev/docs/development/console#protect-the-console-route) protects the Console without a Better Auth database. # Blob > Store uploads, generated files, binary objects, and metadata with one object-storage API. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="One object-storage import for uploads and generated files across local and hosted object stores."} ::code-group ```ts [server/api/upload.post.ts] import { blob } from '@vite-hub/blob' export default defineEventHandler(async (event) => { // Authorize the request here. The route decides who may upload. const [error, objects] = await blob.handleUpload(event, { formKey: 'files', ensure: { maxSize: '8MB', types: ['image'] }, put: { prefix: 'avatars', addRandomSuffix: true }, }) if (error) throw error return objects }) ``` ```ts [vite.config.ts] import { hubBlob } from '@vite-hub/blob/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [hubBlob()], blob: { stores: { default: { driver: 'fs' }, reports: { driver: 'vercel-blob', access: 'private' }, }, }, }) ``` ```ts [server/agents/publisher.ts] import { defineAgent } from 'vite-hub/agent' import { blob } from 'vite-hub/agent/capabilities' export default defineAgent({ capabilities: [ blob({ assetPaths: ['artifacts'], mode: 'write', policy: 'deny' }), ], }) ``` ```bash [Terminal] pnpm vitehub blob list --prefix avatars/ pnpm vitehub blob head avatars/ada.png --json pnpm vitehub blob put avatars/ada.png ./ada.png pnpm vitehub blob get avatars/ada.png --output ./copy.png pnpm vitehub blob del avatars/ada.png ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-code-2" title="Every method returns an error and a value" to="https://vitehub.dev/docs/blob/server-api"} `put`, `get`, `head`, `list`, `del`, and `sign` return `[error, value]`. :: ::product-feature-item{icon="i-lucide-shield-check" title="Validate and store uploads in one call" to="https://vitehub.dev/docs/blob/server-api#upload-files"} `blob.handleUpload()` checks size and type before anything is stored. :: ::product-feature-item{icon="i-lucide-sliders-horizontal" title="Pick a driver per store, keep the import" to="https://vitehub.dev/docs/blob/configure"} Set `driver` for S3 or Azure; otherwise the host selects it. :: ::product-feature-item{icon="i-lucide-bot" title="An Agent can publish the files it creates" to="https://vitehub.dev/docs/blob/agent-capability"} `assetPaths` publishes linked files and rewrites the links to public URLs. :: :product-feature-item[Run Blob commands against the Vite Development Server.]{icon="i-lucide-terminal" title="Objects from a terminal" to="https://vitehub.dev/docs/blob/hosts"}:product-feature-item[Set content type and custom metadata while storing the object.]{icon="i-lucide-tag" title="Metadata travels with objects" to="https://vitehub.dev/docs/blob/server-api"} :: # Blob capability > Give an Agent a Blob read tool and, in write mode, a tool that puts or deletes objects. `blob()` gives an Agent the `blob_read` tool for get, head, and list operations. In write mode, it also gives the `blob_edit` tool for put and delete operations. Both tools call the configured [Blob primitive](https://vitehub.dev/docs/blob). For Provider-backed Agents, `assetPaths` also publishes files that the final answer references. The [Blob server API](https://vitehub.dev/docs/blob/server-api) covers application code. This page covers the Agent tools. ## Configure Blob access Attach Blob in read mode until the Agent must write objects. ```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' }), ], }) ``` ## Agent-visible tool contract These definitions are resolved from the real Capability during the docs build. ### Read mode :agent-capability-tools{name="blob" variant="read"}### Write mode :agent-capability-tools{name="blob" variant="write"}## How Blob access works The Capability resolves the Blob store when the Agent calls a tool, not when the tools are listed. With `store`, it calls `store(name)` on the Blob handle. `blob_read` runs one operation for each call: - `get` returns the object at `pathname`. - `head` returns the metadata of the object at `pathname`. - `list` requires a non-empty `prefix`. It passes `cursor`, `folded`, and `limit` to the Blob handle. `limit` defaults to 25. Values above 100 are reduced to 100. `blob_edit` runs one operation for each call: - `put` writes to `pathname` from exactly one source: inline `body`, a current input attachment (`attachmentId`), or a Workspace file (`workspacePath`). `options` passes write options, such as `contentType`, to the Blob handle. For an attachment, the attachment media type replaces `options.contentType`. - `delete` removes the object at `pathname` and returns `{ pathname, deleted: true }`. When the current input has attachments, the `blob_edit` description lists their IDs and media types. ## Provider artifacts Declare the directories where a Provider-backed Agent may write public artifacts. The Agent uses its normal filesystem workflow, then references 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 `![Preview](artifacts/preview.png)` to its final answer, ViteHub publishes the file through Blob under a `vitehub-agent-artifacts/` prefix, records it in `AgentRunResult.artifacts`, and rewrites that exact Markdown destination in the final text. Publication is bounded: - `assetPaths` applies only in write mode and only to Provider-backed Drivers. In read mode, ViteHub ignores it. - Each path must be Workspace-relative and must not contain `.` or `..` segments. `assetPaths: true` uses `screenshots`. - ViteHub accepts only Markdown links or images under `assetPaths`. It publishes only files that the current Provider Workspace write-back added or modified. It ignores bare paths, stale files, removed files, and paths outside the declared roots. - `policy` controls only the model-facing `blob_edit` tool. Host-owned artifact publication does not need that tool. The example sets `policy: 'deny'`, so the Agent cannot call `blob_edit`, but artifacts are still published. Configure Blob serving or a Blob driver that returns public URLs. Publication fails when `put()` returns no `url`. When `blob.serve` returns a route-relative URL, ViteHub resolves it against the Agent Invocation request URL. ## Workspace uploads Use `workspacePath` to upload a Workspace file, for example a screenshot that another Capability wrote. The path is Workspace-relative. The Capability reads from the active Agent Workspace Session first, then from the Workspace file system. ```ts [Agent tool call] await blob_edit({ operation: 'put', pathname: 'review/screenshots/home.png', workspacePath: 'screenshots/home.png', options: { contentType: 'image/png' }, }) ``` ## Requirements - Configure the Blob primitive. Generated Agent routes pass `blob` from `@vite-hub/blob` to the Capability when the Blob Vite integration is active. - Without a `blob` handle, the Capability imports `blob` from an installed `@vite-hub/blob` package. If that import fails, the first tool call fails. The tools are listed before this check. - `store` requires a Blob handle that exposes `store()`. - `workspacePath` requires a Workspace file system or an active Agent Workspace Session. ## Security and approval - Read mode lets the Agent get and inspect any pathname, and list objects under any non-empty prefix, in the selected store. The `prefix` description says "developer-provided prefix", but the Capability does not enforce a prefix scope. To limit what the Agent can see, select a dedicated store with `store`. - Write mode lets the Agent put or delete any pathname in the selected store. It can upload current input attachments and Workspace files. - `policy` applies only to `blob_edit`. `blob_read` has no policy gate. - `policy` accepts `'allow'`, `'require-approval'`, `'deny'`, `'retryable-failure'`, or a function that receives `{ name, input }` and returns one of these values. - Without `policy`, `blob_edit` runs when the Agent calls it. - `'require-approval'` stops the call with `APPROVAL_REQUIRED` and an Approval Request. The write runs only after approval. See [Runtime policy, approvals, and traces](https://vitehub.dev/docs/getting-started/concepts/runtime-policy-approvals-and-traces). - Published artifacts get public URLs. Declare only `assetPaths` whose files may be public. ## Driver support
Agent Driver Support
Model-backed Receives blob_read and, in write mode, blob_edit . assetPaths has no effect.
Provider-backed Receives the same tools through the provider MCP bridge. In write mode, assetPaths also publishes current-run files that the final Markdown references.
Custom-run-backed driver.run receives the tools in context.tools and decides whether to call them. assetPaths has no effect.
## Verify Blob access 1. Start the Vite development server. 2. Run `vitehub agent info --agent support --json`. Confirm that `tools` contains an entry with `name: "blob"`. Inspection lists one entry for each Capability, not one entry for each tool. 3. Run `vitehub agent dev "List the objects under reports/" --agent support`. Confirm that the output shows a `[tool] blob_read` call with `operation: "list"`. 4. For Provider artifacts, run the Agent so that it writes a file under an `assetPaths` root and links it in the final answer. Confirm that the final text contains the published URL. ## Options
Option Type Default Description
mode "read" | "write" "read" Adds blob_edit when set to "write" . Other values fail when the Agent Definition loads.
assetPaths boolean | string | string[] false Write mode and Provider-backed Drivers only. Materializes these Workspace paths and publishes current-run files that the final Markdown references. true uses screenshots .
store string default store Selects a named Blob Store through store() on the Blob handle.
policy AgentToolPolicyDecision | (context) => AgentToolPolicyDecision | Promise none (calls run) Policy for blob_edit . Has no effect on artifact publication.
## Related pages - [Blob primitive](https://vitehub.dev/docs/blob) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) - [Runtime policy, approvals, and traces](https://vitehub.dev/docs/getting-started/concepts/runtime-policy-approvals-and-traces) # Blob configuration > Configure Blob Stores, the opt-in serve route, and provider options. ## 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?, authorize? } } 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. authorize: true requires a signed-in Auth session.
### Protect served objects The serve route is public by default. Set `serve.authorize: true` to require a signed-in [Auth](https://vitehub.dev/docs/auth) session before ViteHub reads the store. The app must enable Auth and define `server/auth.ts`, or the build fails with `BLOB_B0002`. ```ts [vite.config.ts] export default defineConfig({ plugins: [vitehub({ auth: true, blob: { serve: { route: '/photos', authorize: true } }, })], }) ``` To decide each request, export `authorize` from `server/blob.ts`. It uses the Auth access signature: it receives `{ request, session, user }` and returns `true`, `false`, or a `Response`. ```ts [server/blob.ts] import type { BlobServeAuthorize } from 'vite-hub/blob' export const authorize: BlobServeAuthorize = ({ request, user }) => new URL(request.url).pathname.startsWith(`/photos/${user.id}/`) ``` A request without a session returns JSON `401`. A `false` result returns `403`, and a `Response` is returned as-is. ViteHub checks access before the store read and before conditional `304` handling, so a cached `ETag` does not bypass the check. Browsers send the same-origin session cookie with ``, so image tags work without extra headers. Authorized responses use `Cache-Control: private, no-cache` unless `serve.headers` sets `Cache-Control`. If `server/blob.ts` exports `authorize` while `serve.authorize` is not `true`, the build fails with `BLOB_B0001`. Restart the dev server after you add or remove `server/blob.ts`. ## 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. ## 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.
# Blob get started > Install Blob, register the Vite integration, and write the first object. ## 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 }) ``` :: Read [Server API](https://vitehub.dev/docs/blob/server-api) for every Blob method. # Blob hosts > Set up provider output, development access, Cloudflare R2, S3-compatible storage, and MinIO. ## 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. ## Read and write blobs during development `hubBlob()` contributes the `vitehub blob` CLI namespace. Start the Vite Development Server, then read and write blobs from another terminal. ```bash [Terminal] pnpm vitehub blob list --prefix avatars/ pnpm vitehub blob head avatars/ada.png --json pnpm vitehub blob put avatars/ada.png ./ada.png pnpm vitehub blob get avatars/ada.png --output ./copy.png pnpm vitehub blob del avatars/ada.png ``` The commands call the same Blob storage as the running app. Pass `--store ` for a named store. Each write command prints what it changed. `get` writes the bytes unchanged to a file or to stdout. `put` sends the file as base64 JSON through the dev endpoint, so it accepts files up to 8 MiB. The commands do not print blob URLs. The commands call a guarded endpoint that exists only on the Vite Development Server. Nuxt and plain Vite do not run Nitro in the Vite process, so the endpoint returns status 501 there. Read [CLI](https://vitehub.dev/docs/development/cli#read-and-write-blobs) for every command and option. ## 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 its bundled Files SDK adapter. 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 @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 @aws-sdk/client-s3 @aws-sdk/lib-storage @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 @aws-sdk/client-s3 @aws-sdk/lib-storage @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. # Blob server API > Read, write, list, upload, sign, and validate objects from server code, and check production rules. ## 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. ## 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') ``` ## 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.handleUpload(event, options?) Stores the files of a multipart/form-data request. Read Upload files .
blob.createMultipartUpload(pathname, options?) Starts a multipart upload. Read Multipart uploads .
blob.resumeMultipartUpload(pathname, uploadId) Continues a multipart upload in a later request.
blob.handleMultipartUpload(event, options?) Serves the requests that the multipart client sends.
blob.store(name) Selects a named Blob Store.
Pass the cursor returned by `blob.list()` unchanged to the next list call on the same Blob Store. Keep `prefix` and `folded` unchanged. Netlify Blobs listings and folded files-sdk listings fail if they cannot decode the cursor. ## 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.
## Upload files `blob.handleUpload()` reads a `multipart/form-data` request and calls `put()` for each file. The file name, without any directory part, is the pathname. It returns `[error, objects]`. A request without files, with too many files, or with a file that fails `ensure` throws an H3 400 error before anything is stored. ```ts [server/api/files.post.ts] import { blob } from '@vite-hub/blob' export default defineEventHandler(async (event) => { // Authorize the request here. The route decides who may upload. const [error, objects] = await blob.handleUpload(event, { formKey: 'files', ensure: { maxSize: '8MB', types: ['image'] }, put: { prefix: 'avatars', addRandomSuffix: true }, }) if (error) throw error return objects }) ```
Option Default Description
formKey "files" Form field that holds the files.
multiple true Set false to accept one file.
ensure none Options for ensureBlob() , checked for each file.
put none Write options for each put() .
In Vue, `useUpload()` from `@vite-hub/blob/vue` sends the form. The Nuxt module auto-imports it when `blob` is enabled. ```vue [app/components/AvatarUpload.vue] ``` `useUpload()` accepts a `File`, a `FileList`, an array of files, or an element with `files`. It resolves with serialized objects, where `uploadedAt` is an ISO string. Pass `headers` or `fetch` to add authentication. ## Multipart uploads Use multipart uploads for files that are too large for one request. The client splits the file into parts, and the server stores each part until `complete()` joins them. The `fs`, `cloudflare-r2` (with the R2 binding), and `vercel-blob` drivers support it. Other drivers throw `BLOB_R0030`. Create one route with `action` and `pathname` params: ```ts [server/api/files/multipart/[action]/[...pathname].ts] import { blob } from '@vite-hub/blob' export default defineEventHandler(async (event) => { // Authorize the request here. The client chooses the pathname. const [error, result] = await blob.handleMultipartUpload(event) if (error) throw error return result }) ``` The route answers four requests:
Action Method Request Result
create POST JSON { contentType? } { action, pathname, uploadId }
upload PUT ?uploadId=…&partNumber=… , part bytes { action, part }
complete POST ?uploadId=… , JSON { parts } { action, object }
abort DELETE ?uploadId=… { action }
Pass `{ create: { contentType, prefix, addRandomSuffix } }` to set the create options on the server. A `contentType` set here replaces the type that the client sends. In Vue, `useMultipartUpload()` sends the parts and tracks progress: ```vue [app/components/VideoUpload.vue] ``` `partSize` defaults to 10 MiB. Vercel Blob and R2 need at least 5 MiB for every part except the last. `task.abort()` stops the upload and sends the `abort` request; `completed` then resolves with `undefined`. Server code can also drive an upload directly: ```ts [server/utils/copy-large-file.ts] import { blob } from '@vite-hub/blob' const [createError, upload] = await blob.createMultipartUpload('exports/report.csv') if (createError) throw createError const [partError, part] = await upload.uploadPart(1, firstChunk) if (partError) throw partError const [completeError, object] = await upload.complete([part]) if (completeError) throw completeError ```
Driver Upload state Notes
fs Parts under /.vitehub/multipart// complete() checks each part's etag. abort() deletes the parts.
cloudflare-r2 R2 multipart upload Requires the R2 binding. The HTTP fallback has no multipart support.
vercel-blob Vercel multipart upload The upload ID encodes the Vercel key. Access always comes from the store config. abort() does nothing; Vercel discards unfinished uploads.
The framework-neutral client is `@vite-hub/blob/client`, with `uploadFiles()` and `createMultipartUploader()`. The Vue composables use it. ## 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/) accept expiries from 1 second through 7 days, and the [S3 compatibility contract](https://developers.cloudflare.com/r2/api/s3/api/) 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 .
## 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. # Browser > Define provider-backed browser operations without exposing provider setup to application code. ::product-hero{hosts="Cloudflare" tagline="Name a browser operation and run it from a route, Queue, or Workflow, with no Cloudflare imports or credentials."} ::code-group ```ts [Definition and route] // 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() }) // server/api/page-title.post.ts import { runBrowser } from 'vite-hub/browser' export default defineEventHandler(async (event) => { const input = await readBody<{ url: string }>(event) return await runBrowser('page-title', input) }) ``` ```ts [vite.config.ts] import { vitehub } from 'vite-hub' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ vitehub({ preset: 'cloudflare', browser: { binding: 'RENDER_BROWSER', remote: true, }, }), ], }) ``` ```ts [Agent] import { defineAgent } from 'vite-hub/agent' import { browser } from 'vite-hub/agent/capabilities' export default defineAgent({ driver: { kind: 'codex', model: 'gpt-6-astra' }, workspace: { mode: 'write' }, capabilities: [browser()], }) ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-code-2" title="One file per browser operation" to="https://vitehub.dev/docs/browser/server-api"} `runBrowser()` runs a Definition with inferred input and returns a `Response`. :: ::product-feature-item{icon="i-lucide-monitor" title="Several interactions share one page" to="https://vitehub.dev/docs/browser/server-api#keep-a-page-session-open"} `browser.open()` gives a page session that ViteHub closes for you. :: ::product-feature-item{icon="i-lucide-play-circle" title="One stateless call needs no Definition" to="https://vitehub.dev/docs/browser/server-api#browser-actions"} Content, Markdown, links, screenshot, or PDF through `runBrowserAction()`. :: ::product-feature-item{icon="i-lucide-cloud-cog" title="Enable it on the Cloudflare preset" to="https://vitehub.dev/docs/browser/configure"} `browser: true` writes the Browser Run binding to `wrangler.json`. :: ::product-feature-item{icon="i-lucide-bot" title="A Provider Agent gets its own browser CLI" to="https://vitehub.dev/docs/browser/agent-capability"} `browser()` gives a Provider Agent the `agent-browser` CLI and Chromium. :: ::product-feature-item{icon="i-lucide-plug" title="Model-backed Agents need a custom Capability" to="https://vitehub.dev/docs/agents/capabilities/custom"} Write a custom Capability that calls `runBrowser()`. :: :: # Browser capability > Give a Provider Agent the agent-browser CLI, a headless Chromium, and the official browser Skill. `browser()` gives a Provider Agent the `agent-browser` CLI, Chromium, and the official browser Skill. The Agent runs the CLI through the provider's native shell. The Capability adds no model-facing tools. The [Browser server API](https://vitehub.dev/docs/browser/server-api) covers Browser Sessions that trusted application code owns. This page covers the browser runtime for a Provider Agent. ## Configure the browser ```ts [server/agents/review.ts] import { defineAgent } from 'vite-hub/agent' import { browser } from 'vite-hub/agent/capabilities' export default defineAgent({ driver: { kind: 'codex', model: 'gpt-6-astra' }, workspace: { mode: 'write' }, capabilities: [browser()], }) ``` ## How the browser works In managed mode, ViteHub prepares the runtime before the provider starts, reuses its cache, and checks that Chromium can launch. In external mode, the provider environment must already have the CLI and browser. The Skill is materialized at `.agents/skills/agent-browser/SKILL.md`. In managed mode, the Skill is the official Skill from the installed CLI. It directs the provider to load the version-matched workflow with `agent-browser skills get core`. The Skill tells the provider to run browser commands only when `VITEHUB_BROWSER_ACTIVE` is `1`. In managed mode, each Agent Invocation receives its own browser session through `AGENT_BROWSER_SESSION`. The Capability closes the session when the invocation finishes. ### Screenshots The Capability lets the Agent write and commit files under `screenshots/`. Save screenshots there. A final-reply marker such as `![Login page](screenshots/login.png)` attaches the file to the reply and removes the marker from the text. Only `.png`, `.jpg`, `.jpeg`, and `.webp` files that the invocation added or changed are attached. Use an artifact publisher for other files or permanent public links. ### Managed runtime The managed runtime supports local Node deployments on Linux x64 and macOS. The first preparation requires `npm` and network access. It installs pinned CLI dependencies into a versioned cache. Later invocations reuse them. On Linux, the runtime includes Chromium's shared libraries and fonts, so Debian slim containers do not need browser-specific APT packages. The managed Linux browser runs without Chromium's sandbox and inherits the execution boundary of the enclosing provider. The cache uses `VITEHUB_CACHE_DIR`, otherwise `$XDG_CACHE_HOME/vitehub`, otherwise `~/.cache/vitehub`. Keep this directory on persistent storage in containers. Browser sockets use a short private temporary directory. The Capability supplies its environment only to the enclosing invocation. It does not replace the provider's home directory or credentials. A missing executable, a corrupt cache, or a failed launch is detected before the provider starts. A failed installation can be retried. For Codex, the managed runtime disables login shells so shell profiles cannot replace the browser CLI path. Other shell commands keep the invocation environment. ### External runtime Use the external runtime on other platforms and for remote launchers. For SSH launchers, install the CLI and browser on the remote host. When the Driver sets `launch` and `runtime` is not set, `browser()` uses the external runtime for that invocation. A custom `command` also selects the external runtime unless `runtime` is set. The Capability still supplies the Skill and screenshot delivery. The remote provider owns executable discovery and browser setup. Inspection metadata reports `runtime: 'auto'` when the runtime depends on the Driver. ## Requirements - A Provider Agent Driver. Other Drivers fail with `browser() requires a Provider Agent Driver.` - A Workspace with `mode: 'write'`. - For the managed runtime: Linux x64 or macOS, `npm`, and network access for the first preparation. - For the external runtime: the `agent-browser` CLI, or the configured `command`, and a browser prepared in the provider environment. ## Security and approval `browser()` adds no model-facing tool and has no `policy` option. The provider runs the browser CLI with its native shell, so the Driver's own permission settings control those commands. - The browser can reach any URL that the provider host can reach. The Capability does not restrict network access. - The managed Linux browser runs without Chromium's sandbox. The provider's execution boundary is the isolation boundary. - The Workspace rule for `screenshots/**` allows writes and commits. Other paths follow the Workspace rules. - `command` must be a single executable name. `runtime: 'managed'` with a custom `command` fails at definition time. `runtime: 'managed'` with `driver.launch` fails before the provider starts. ## Driver support
Agent Driver Support
Model-backed Not supported. The Driver does not own a provider shell.
Provider-backed Supported. Receives the CLI, browser runtime, Skill, and screenshot delivery.
Custom-run-backed Not supported. Use the Browser primitive in driver.run .
## Verify the browser 1. Run `vitehub agent info --agent --json` and confirm that the `browser` Capability metadata shows `command`, `runtime`, `skillPath`, and `sourceKey`. 2. Run the Agent once with `vitehub agent dev --agent --prompt "Open https://example.com and take a screenshot"`. 3. Confirm that the reply text has no screenshot marker and that the result lists the `screenshots/...` file as an artifact with `placement: 'attachment'`. 4. For the managed runtime, confirm that the cache directory contains the prepared runtime. ## Options
Option Type Default Description
runtime "managed" | "external" "external" with a custom command or driver.launch , otherwise "managed" Prepare a local runtime, or use an externally prepared one.
command string "agent-browser" Executable name used in the default Skill. A custom command selects an external runtime unless runtime is set.
skillContent string official installed Skill in managed mode Override the mounted Markdown guidance.
skillPath string ".agents/skills/agent-browser/SKILL.md" Workspace path for the Skill.
sourceKey string "skill.browser" Workspace Source key for the Skill file.
## Related pages - [Browser primitive](https://vitehub.dev/docs/browser) - [Agent Drivers](https://vitehub.dev/docs/agents/agent-drivers) - [skills()](https://vitehub.dev/docs/agents/capabilities/skills) - [Official Capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Browser configuration > Configure the Browser Run binding, the session engine, and remote local development. ## Configuration `browser: true` enables Cloudflare Browser Run actions. Use an object only to change the binding, the session engine, or to connect local development to the hosted service. ```ts [vite.config.ts] import { vitehub } from 'vite-hub' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ vitehub({ preset: 'cloudflare', browser: { binding: 'RENDER_BROWSER', remote: true, }, }), ], }) ```
Option Type Default Description
binding string 'BROWSER' Cloudflare Browser Run binding name. Must be a valid binding identifier.
engine 'kitesurf' | 'chromium' 'kitesurf' Session engine for browser.open() . 'chromium' selects a persistent Chromium session.
remote boolean false Connect local Wrangler development to the hosted Browser Run service.
`browser: false` disables Browser Provider Output. Without `vite-hub`, pass the same options to `hubBrowser()` from `@vite-hub/browser/vite`. # Browser get started > Install ViteHub, enable Browser on the Cloudflare preset, and run a first Browser Definition. ## 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 { vitehub } from 'vite-hub' export default { 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) return await runBrowser('page-html', input) }) ``` :: The generated Browser registry infers each Definition's input type. `runBrowser()` returns a native `Response`. Discovery and provider failures return a non-2xx JSON `Response`. # Browser hosts > Browser providers, host support, and Cloudflare Provider Output. ## Providers
Path Provider Host support
Browser Definitions and actions Cloudflare Browser Run Cloudflare preset only. Other presets throw a configuration error.
createBrowser() with cloudflareBrowser() Cloudflare Browser Run Cloudflare Workers with a Browser binding.
createBrowser() with localBrowser() Local Chromium process Trusted Node hosts. browser: true never selects it.
The Cloudflare preset writes the Browser Run binding, a default `compatibility_date`, and the `nodejs_compat` flag to the generated `wrangler.json`. Both the root integration and `hubBrowser()` preserve unrelated Wrangler fields. Cloudflare's Worker `quickAction()` requires remote mode during local development. Set `remote: true` when local Wrangler development must call Browser Run. This uses a Cloudflare account and network access. Install `@cloudflare/playwright` and `playwright-core` when you set `engine: 'chromium'`. Stateless actions and the default Kitesurf engine do not need these optional peers. # Browser limits and errors > Browser timeouts, handoff limits, and production checks. ## Limits - Browser Definitions and actions require the Cloudflare preset. - A Browser action waits 30 seconds by default. Timeout fields in the action input extend the wait, up to 6 minutes plus a 30-second grace period. - The Kitesurf engine has no idle timeout and no live handoff. - Handoff refs expire after 60 seconds by default. Set `policy.handoffTtl` on `createBrowser()` or pass `ttl` to `session.handoff()`. ## Production checks Run browser automation only from trusted server code. Browser sessions can observe authenticated pages, cookies, screenshots, network responses, and rendered private UI. When a request supplies the destination URL, validating only the first URL is not sufficient. Enforce protocol, host, and resolved-address policy for every browser request, including redirects and subresources, or restrict browser egress at the provider. Treat the returned page as untrusted input. Do not log provider session ids, CDP endpoints, cookies, authorization headers, or raw handoff refs. Treat screenshots and downloaded files as user data. Route them through the same storage, retention, and approval policies as other artifacts. Inspect the generated `wrangler.json` before deployment, and test the deployed Worker. A successful build proves imports and generated output, not provider availability. Run `vitehub inspect definitions` to list discovered Browser Definitions. # Browser server API > Define, run, and control browser operations from server code. Browser Definitions are the default path. Use Browser actions for one stateless call, and low-level sessions when your code owns the provider and cleanup. ## Public imports
Import Use
defineBrowser , runBrowser from vite-hub/browser Declare and run Browser Definitions.
runBrowserAction , runBrowserContent from vite-hub/browser/actions Run one stateless Browser Run action.
createBrowser from vite-hub/browser Create a low-level Browser Client.
cloudflareBrowser from vite-hub/browser/providers/cloudflare Low-level Cloudflare Browser Run provider.
localBrowser from vite-hub/browser/providers/local Low-level local Chromium provider for trusted Node hosts.
playwright , cdp from vite-hub/browser/controllers/playwright and vite-hub/browser/controllers/cdp Attach Playwright or CDP control to a low-level session.
hubBrowser from @vite-hub/browser/vite Register the Vite Integration without the vite-hub distribution.
Libraries and custom Vite compositions import the same APIs from `@vite-hub/browser` and its subpaths. The package requires Node.js 24.15 or newer. ## Definition API
API Description
defineBrowser(handler) Defines one discovered browser operation. The handler receives (input, { browser }) .
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 with inferred input and returns a native Response .
## Browser actions Use a Browser action directly when the operation does not need a page session: ```ts [server/render-og.ts] import { runBrowserContent } from 'vite-hub/browser/actions' const html = await runBrowserContent('https://example.com') ``` `runBrowserAction(action, input)` returns the raw `Response`. Provider failures return a non-2xx JSON `Response`. `runBrowserContent(input)` reads the `content` action as text and throws when the response is not successful. The input is a URL string or an object with `url` and provider options. Supported actions: `accessibilityTree`, `content`, `json`, `links`, `markdown`, `pdf`, `scrape`, `screenshot`, and `snapshot`. Inside a Browser Definition, use `browser.content(input)` or `browser.run(action, input)` for the same actions. 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()` to 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 run one at a time because either operation can replace the active document. A timeout that leaves page state unclear invalidates the page. Later operations do not reuse that state. ## Low-level sessions `createBrowser()` is for libraries and standalone integrations that own provider selection, controller attachment, and cleanup. Use it when an application needs Playwright, mutable page state, downloads, or CDP. ```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() try { const control = await session.attach(playwright()) try { await control.client.page.goto('https://example.com') } finally { await control.release() } } finally { await session.close() } ``` If reading the controller client or attachment tracing fails, ViteHub releases the controller before rejecting `session.attach()`. If controller cleanup also fails, the session blocks another attachment or handoff. Call `session.close()` to retry failed rollback cleanup before terminating the provider session. Closure waits for pending controller rollback cleanup before terminating the provider; if it fails again, closure rejects and can be retried. Detach tracing does not delay provider termination after controller cleanup succeeds. Install `@cloudflare/playwright` and `playwright-core` when using the Playwright controller on Cloudflare. Cloudflare builds select the `workerd` export condition and exclude the Node Playwright loader. Standalone Worker bundlers must also select `workerd`. The built-in Playwright CDP adapter requires Node.js. `localBrowser({ executablePath })` from `@vite-hub/browser/providers/local` starts a local Chromium process on a trusted host. It supports CDP control and live handoff. `playwright-core` supplies the controller but does not download a browser. ViteHub does not sandbox the browser process. ## Live handoff Low-level sessions can transfer one provider session through an opaque reference tied to an audience. The default Kitesurf engine does not support live handoff. Use `engine: 'chromium'` on Cloudflare, or `localBrowser()`. ```ts [server/browser-handoff.ts] import { createBrowser } from '@vite-hub/browser' import { cdp } from '@vite-hub/browser/controllers/cdp' import { cloudflareBrowser } from '@vite-hub/browser/providers/cloudflare' const browser = createBrowser({ provider: cloudflareBrowser({ binding: 'BROWSER', engine: 'chromium' }), }) 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 (async () => { try { return await session.handoff({ audience: 'review-agent-run-42', mode: 'live', }) } catch (error) { await session.close() throw error } })() ``` The receiver calls `browser.claim(ref, { audience })` on the same Browser Client. Refs are one-time and do not cross clients or processes. Use the CDP controller when live preservation matters. A Playwright attachment is lifecycle-scoped and cannot be handed off after release. # Channels > Define named server delivery channels and send through a selected connector. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="Send outbound messages to a named destination from server code, through connectors you write and select per call."} ::code-group ```ts [Definition] import { defineOutboundChannel } from 'vite-hub/channels' import { useServerEnv } from '#vitehub/env/server' type TelegramOptions = { chatId: string } export default defineOutboundChannel({ 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() } }, }, }, }) ``` ```ts [Route] import { defineEventHandler } from 'h3' import { useChannel } from 'vite-hub/channels/server' export default defineEventHandler(async () => { const [error, receipt] = await useChannel('alerts').send('Build finished.', { connector: 'telegram', chatId: 'build-room', }) if (error) throw error return receipt }) ``` ```ts [Agent] import { defineAgent } from 'vite-hub/agent' import { channelDelivery } from 'vite-hub/agent/capabilities' import { useChannel } from 'vite-hub/channels/server' import bot from '../agents/bot/agent' const reportAgent = defineAgent({ extends: bot, name: 'weekly-report', capabilities: [ channelDelivery({ channel: useChannel('teams'), options: { recipient: 'user:7b0bff9d' }, description: 'Send the finished weekly report. Call once.', required: true, }), ], }) ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-plug" title="You write the connector, Channels selects it" to="https://vitehub.dev/docs/channels/get-started"} Discovers `server/channels` and `.channel.ts`; each connector owns its `send()`. :: ::product-feature-item{icon="i-lucide-code-2" title="send() returns a result tuple, not a throw" to="https://vitehub.dev/docs/channels/server-api"} Returns `[null, receipt]` or `[error, null]` with the delivery id. :: ::product-feature-item{icon="i-lucide-blocks" title="One destination, several providers" to="https://vitehub.dev/docs/channels/server-api#add-another-connector"} Name `connector` at each call site, such as `telegram` or `slack`. :: ::product-feature-item{icon="i-lucide-activity" title="Each delivery logs events without the message text" to="https://vitehub.dev/docs/channels/server-api#send-from-an-h3-or-nitro-handler"} `outbound.*` events omit message text and connector options. :: ::product-feature-item{icon="i-lucide-bot" title="The application picks the recipient" to="https://vitehub.dev/docs/channels/agent-capability"} `channelDelivery()` fixes Channel and options; the model writes the message. :: :product-feature-item[Use Agent Channels when the destination starts an Invocation.]{icon="i-lucide-radio" title="Outbound only: inbound needs Agent Channels" to="https://vitehub.dev/docs/agents/channels"} :: # Channels capability > Let an Agent send its result through a Channel to a recipient that the application selects. `channelDelivery()` gives an Agent one tool, `send_message` by default. The tool sends a message through a [Channel](https://vitehub.dev/docs/channels/server-api) client to a recipient that the application selects. The model only writes the message. Use it when an Agent Invocation must deliver its result somewhere other than the Channel that started it. Examples are a Schedule that sends a report to a Teams user, or a webhook Agent that sends a draft to a reviewer. ## Configure Channel delivery Pass the Channel client from `useChannel()` and the send options. Set `required` when the Agent Invocation must fail without a successful send: ```ts [server/schedules/weekly-report.ts] import { defineSchedule } from '@vite-hub/schedule' import { defineAgent, runAgent } from 'vite-hub/agent' import { channelDelivery } from 'vite-hub/agent/capabilities' import { useChannel } from 'vite-hub/channels/server' import bot from '../agents/bot/agent' const reportAgent = defineAgent({ extends: bot, name: 'weekly-report', capabilities: [ channelDelivery({ channel: useChannel('teams'), options: { recipient: 'user:7b0bff9d' }, description: 'Send the finished weekly report. Call once.', required: true, validate: (message) => { if (message.split(/\s+/u).length > 300) throw new Error('The report must be at most 300 words.') }, }), ], }) export default defineSchedule({ cron: '0 15 * * 5', handler: async (schedule) => { const [error] = await runAgent(reportAgent, { prompt: 'Write the weekly report.' }, { schedule, output: 'drained' }) if (error) throw error return new Response('Report sent') }, }) ``` Capabilities attach through the Agent Definition, so the Schedule extends the Agent. Invocation `tools` passed to `runAgent()` can run next to the Capability. ## Agent-visible tool contract The tool accepts one field, `message`, a non-empty string. On success, it returns `{ deliveryId, sent: true }`. Without `description`, the tool description names the Channel and states how many times the Agent can call the tool. ## How Channel delivery works - The tool trims the message. `validate` can throw to reject it. The model receives the error and can retry. A rejected message does not count as a call. - `format` builds the Channel text from the message. It receives the Capability context, so it can read the Agent Invocation input context. A formatter failure, or formatted text that is empty, does not count as a send attempt. - The tool calls `channel.send(text, options)`. A send error is thrown to the model. - The tool counts each send attempt before it sends. After `maxCalls` attempts, the tool returns `CHANNEL_DELIVERY_LIMIT` before validation or formatting. A failed send also counts, because it can still reach the recipient. - With `required: true`, the Agent Invocation fails with `CHANNEL_DELIVERY_REQUIRED` when the Agent finishes without a successful send. The requirement also applies when an input Capability handles the Agent Invocation before the Driver runs, regardless of Capability order. Streams and `Response` bodies are checked when they finish normally. When the Agent Invocation fails with another error or is cancelled, delivery is not required. - Counters are per Agent Invocation. One definition can serve many Agent Invocations. - The delivery tool name must be unique among Capability tools. A collision fails setup with `CHANNEL_DELIVERY_TOOL_CONFLICT` before the Driver runs. ## Requirements - A Channel client with `send(text, options)` that returns `[error, null]` or `[null, { deliveryId }]`. `useChannel(name)` from `vite-hub/channels/server` returns one. - `maxCalls` must be a positive integer. - `channelDelivery()` throws a `TypeError` when the Agent Definition loads if `channel` has no `send()` function or `maxCalls` is invalid. ## Security and approval - The model controls only `message`. The application fixes the Channel and the send `options`, such as the recipient. The model cannot change them. - The message text leaves the application through the Channel provider. - `validate` and `format` are the application checks on the message. `maxCalls` limits the number of send attempts for each Agent Invocation. - `channelDelivery()` has no `policy` option. A valid call sends at once, without approval. ## Driver support
Agent Driver Support
Model-backed Receives the delivery tool.
Provider-backed Receives the delivery tool through the provider MCP bridge.
Custom-run-backed driver.run receives the delivery tool in context.tools and decides whether to call it.
## Verify Channel delivery 1. When the Capability is on a discovered Agent, start the Vite development server. 2. Run `vitehub agent info --agent --json`. Confirm that `tools` contains an entry with `name: "send_message"`, and that `capabilities` contains `{ id: "channel-delivery.send_message" }` with the expected `channel`, `maxCalls`, and `required` metadata. 3. Run the Agent with a test recipient. Confirm that the tool result contains a `deliveryId` and that the message arrives in the Channel. 4. With `required: true`, run the Agent with a prompt that does not ask for a send. Confirm that the Agent Invocation fails with `CHANNEL_DELIVERY_REQUIRED`. ## Options
Option Type Default Description
channel { name?: string, send(text, options) } required The Channel client, for example useChannel('teams') .
options send options required Options passed to send() , such as the recipient. The model cannot change them.
name string "send_message" Tool name. The Capability ID is channel-delivery. .
description string generated Tool description for the model.
required boolean false Fail the Agent Invocation without a successful send.
maxCalls number 1 Maximum send attempts per Agent Invocation.
format (message, context) => string | Promise none Build the Channel text from the message.
validate (message) => void | Promise none Throw to reject a message.
## Related pages - [Agent Channels](https://vitehub.dev/docs/agents/channels) - [Channels Server Primitive](https://vitehub.dev/docs/channels) - [Schedule Capability](https://vitehub.dev/docs/schedule/agent-capability) - [Schedule primitive](https://vitehub.dev/docs/schedule) - [Custom capabilities](https://vitehub.dev/docs/agents/capabilities/custom) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) # Channels get started > Enable Channel discovery and define the first named Channel. ## 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` with `defineOutboundChannel()`. 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 { defineOutboundChannel } from 'vite-hub/channels' import { useServerEnv } from '#vitehub/env/server' type TelegramOptions = { chatId: string } export default defineOutboundChannel({ 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. Next, send a message from server code. Read [Send from an H3 or Nitro handler](https://vitehub.dev/docs/channels/server-api#send-from-an-h3-or-nitro-handler). # Channels server API > Send messages through a named Channel and read the delivery receipt. ## Send from an H3 or Nitro handler `useChannel()` returns immediately. `send()` performs the connector call and returns `[null, receipt]` on success or `[error, null]` on failure. The receipt has the Channel name, connector name, ViteHub delivery id, and optional provider message id. Check the error before using the receipt. Named clients share one definition load for the active runtime registry. A failed load can retry on a later send. Replacing the runtime registry invalidates the cached definition for existing clients. ```ts [server/api/build-finished.post.ts] import { defineEventHandler } from 'h3' import { useChannel } from 'vite-hub/channels/server' export default defineEventHandler(async () => { const [error, receipt] = await useChannel('alerts').send('Build finished.', { connector: 'telegram', chatId: 'build-room', }) if (error) throw error return receipt }) ``` The handler returns a result like this: ```json { "channel": "alerts", "connector": "telegram", "deliveryId": "e4875238-2922-4787-9f7f-b13e2e7839be", "id": "1730000000000" } ``` For each connector delivery, Channels attempts to write `outbound.started` and either `outbound.completed` or `outbound.failed` JSON events under the `vitehub.channel.send` scope. Input validation and definition-loading failures return before delivery logging starts. Logging is best effort. A logging failure does not change the send result. The events include `deliveryId`, Channel, connector, and, when available, a provider message id or error message. ViteHub omits message text and connector options. Failed events include up to 2,000 characters of the thrown error message, so connectors must redact credentials and message content before throwing provider errors. Configure the application's log drain to retain received events. This outbound-only package has no State Adapter. 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 const [error] = await useChannel('alerts').send('Build finished.', { connector: 'slack', channelId: 'builds', threadTs: '1730000000.000100', }) if (error) throw error ``` Keep `connector` explicit when a Channel has more than one delivery path. This makes the delivery choice visible at each call site. # Connections > Connect provider accounts with OAuth 2 or an API key, call their APIs with access rules, and record how routes and Agents use them. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="Call provider APIs as an app-owned account: OAuth 2 or API key, sealed in the app database, checked per call."} ::code-group ```ts [Definition] import { useServerEnv } from '#vitehub/env/server' import { defineConnection } from 'vite-hub/connections' import { google } from 'vite-hub/connections/google' export default defineConnection({ provider: google({ clientId: () => useServerEnv().google.clientId, clientSecret: () => useServerEnv().google.clientSecret.unseal(), }), scopes: ['https://www.googleapis.com/auth/gmail.modify'], access: { server: { write: ['gmail.*'] }, agents: { labeller: { write: ['gmail.messages.*', 'gmail.labels.list', 'gmail.drafts.create'], approve: true }, }, }, }) ``` ```ts [Route] import { useConnection } from 'vite-hub/connections/server' export default defineEventHandler(async (event) => { const connection = useConnection('google') return await connection.gmail.users.labels.list() }) ``` ```ts [API key] import { apiKey, defineConnection } from 'vite-hub/connections' export default defineConnection({ provider: apiKey({ id: 'executor', origins: ['https://executor.sh'] }), }) ``` ```bash [CLI] vitehub connections list vitehub connections status google vitehub connections activity google vitehub connections connect google vitehub connections refresh google printf %s "$EXECUTOR_API_KEY" | vitehub connections set-key executor ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-shield-check" title="Writes are denied until a rule allows them" to="https://vitehub.dev/docs/connections/configure#access-rules"} With an `access` map, actors without a matching rule are denied. :: ::product-feature-item{icon="i-lucide-shield-alert" title="The credential goes only to declared origins" to="https://vitehub.dev/docs/connections/configure#provider-origins"} Other origins fail with `CONNECTIONS_ORIGIN_NOT_ALLOWED` and get no credential. :: ::product-feature-item{icon="i-lucide-key-round" title="API keys get the same rules and activity" to="https://vitehub.dev/docs/connections/configure#api-key-connections"} `apiKey()` seals a static key; set it in the Console or CLI. :: :product-feature-item[Activity stores actor, Operation, and outcome, never bodies or tokens.]{icon="i-lucide-activity" title="Every write, denial, and failure is recorded" to="https://vitehub.dev/docs/connections/server-api"}::product-feature-item{icon="i-lucide-bot" title="Agents call it under their own rule" to="https://vitehub.dev/docs/agents/capabilities/gmail"} An `approve` match makes the Agent tool ask for approval. :: :product-feature-item[Store provider credentials with an explicit account and grant policy.]{icon="i-lucide-shield-check" title="Access stays scoped" to="https://vitehub.dev/docs/connections/configure"} :: # Connections configuration > Configure provider origins, API key Connections, access rules, and Connections options. ## Provider origins Each provider declares the API origins that may receive its credential. `call` and `fetch` fail with `CONNECTIONS_ORIGIN_NOT_ALLOWED` for any other origin, and ViteHub records the attempt as denied. `google()` allows `https://*.googleapis.com`. Set `origins` for `oauth2()`: ```ts [server/connections/crm.ts] import { defineConnection, oauth2 } from 'vite-hub/connections' export default defineConnection({ provider: oauth2({ authorizationUrl: 'https://crm.example.com/oauth/authorize', client: ({ event }) => useServerEnv(event).crm, id: 'crm', origins: ['https://api.crm.example.com'], tokenUrl: 'https://crm.example.com/oauth/token', }), scopes: ['contacts.read'], }) ``` An origin is `https://host`, `https://host:port`, or `https://*.host` for subdomains. `http` is accepted only for loopback hosts such as `localhost` and `127.0.0.1`. The `userInfoUrl` of `oauth2()` receives the access token, so its origin must be in `origins` too. ## API key Connections `apiKey()` defines a Connection whose credential is a static key. An admin sets the key at runtime, so it is not in code or Server Env: ```ts [server/connections/executor.ts] import { apiKey, defineConnection } from 'vite-hub/connections' export default defineConnection({ provider: apiKey({ id: 'executor', origins: ['https://executor.sh'] }), }) ``` `verify` checks a new key before ViteHub stores it. Return `false` to reject the key, or `{ account }` to label the Connection: ```ts [server/connections/crm.ts] export default defineConnection({ provider: apiKey({ id: 'crm', origins: ['https://api.crm.example.com'], verify: async (key, { fetch, signal }) => { const response = await fetch('https://api.crm.example.com/me', { headers: { authorization: `Bearer ${key}` }, signal }) return response.ok }, }), }) ```
Option Default Description
origins Required Origins that fetch may send the key to. Use https , or http for a loopback host.
header authorization Request header that carries the key.
scheme Bearer for authorization , none for other headers Text before the key. Set '' to send the bare key.
apis {} Typed API catalogs. Their root URLs also receive the key.
verify None Checks a new key before ViteHub stores it. It receives the runtime fetch and a 30-second abort signal.
id 'api-key' Provider id in the Console and inspection.
Set the key in the Console with **Set key**, or pipe it to the CLI: ```bash [Terminal] printf %s "$EXECUTOR_API_KEY" | vitehub connections set-key executor ``` The CLI reads the key only from stdin, so it does not stay in the shell history. The CLI and the Console send the key only over HTTPS or to a loopback host. ViteHub seals the key like an OAuth grant. Access rules, approvals, dry run, and activity work as for OAuth Connections. An API key has no scopes and no refresh: a `401` response does not change the status. When a redirect leaves the first origin, ViteHub removes the key header. Revoking the Connection deletes the stored key; revoke the key at the provider too. ## Access rules `access` has rules for `server`, `routes` (by route id such as `POST /api/sync`), and `agents` (by Agent id). A route without its own rule uses `server`. Each rule has `read`, `write`, and `approve` settings. When an `access` map is present, actors without a matching rule are denied. Without the map, reads and ordinary server writes are allowed, while high-risk writes and fetches are denied. ViteHub checks the actor rule before the operation. A write list can contain exact Operation ids or `*` patterns; high-risk writes require an explicit matching pattern.
Decision Result
allow The call runs.
require-approval Server code fails with CONNECTIONS_APPROVAL_REQUIRED . An Agent tool asks for tool approval. When a user approves it in a provider Agent session, the call runs. deny rules still apply.
deny The call fails with CONNECTIONS_DENIED .
## Configuration options
Option Default Description
database 'default' Database that stores grants and activity.
encryptionKey env({ secret: true, optional: true, source: env.source('VITEHUB_CONNECTIONS_KEY') }) Secret Env declaration for the key. It cannot use env.provider() or a default.
projectRoot Vite root Where ViteHub discovers server/connections/ .
`vitehub({ connections })` requires `database`. The Nuxt module does not support Connections yet. # Connections get started > Enable Connections, define a Connection, connect the account, and call the provider. ## Quick start ::steps{level="3"} ### Configure Connections need [Database](https://vitehub.dev/docs/database) and a 32-byte encryption key. ```ts [vite.config.ts] import { vitehub } from 'vite-hub' import { defineConfig } from 'vite' export default defineConfig({ plugins: [vitehub({ preset: 'node', console: true, database: true, connections: true })], }) ``` ```bash [Terminal] # 32 random bytes, base64url node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))" ``` Set the result as the secret `VITEHUB_CONNECTIONS_KEY` on the host. If the key changes, every Connection must be connected again. ### Define a Connection The file name is the Connection name. `google()` adds the `openid` and `email` scopes to show the connected account. Register an OAuth client at the provider with the redirect URI `/_vitehub/connections/callback`. ```ts [server/connections/google.ts] import { useServerEnv } from '#vitehub/env/server' import { defineConnection } from 'vite-hub/connections' import { google } from 'vite-hub/connections/google' export default defineConnection({ provider: google({ clientId: () => useServerEnv().google.clientId, clientSecret: () => useServerEnv().google.clientSecret.unseal(), }), scopes: ['https://www.googleapis.com/auth/gmail.modify'], access: { server: { write: ['gmail.*'] }, agents: { labeller: { write: ['gmail.messages.*', 'gmail.labels.list', 'gmail.drafts.create'], approve: true }, }, }, }) ``` ### Connect the account Open the Console, select **Connections**, and select **Connect**. You can also print a single-use connect URL from the CLI while the development server runs: ```bash [Terminal] vitehub connections connect google ``` ### Call the provider ```ts [server/api/labels.get.ts] import { useConnection } from 'vite-hub/connections/server' export default defineEventHandler(async (event) => { const connection = useConnection('google') return await connection.gmail.users.labels.list() }) ``` :: # Connections limits and errors > Connections storage, security guarantees, and error codes. ## Storage and security - Grants are sealed with AES-GCM and the encryption key. Each row stores the key id. With a different key, the status is `needs-reconnect`. - The connect flow uses PKCE (`S256`), a single-use ticket that expires after 10 minutes, and a `state` cookie. Tokens never go to the browser or the CLI. - Connect, callback, and management routes exist on the development server, and in production only with `connections: { management: true }`. Each route runs the Console access policy itself: Console Auth, app Auth, Cloudflare Access, or the host `console.authorize` function. With `console: true`, the routes work only on the development server. See [Management routes](https://vitehub.dev/docs/connections/server-api#management-routes). - The OAuth callback accepts a `state` one time only, from the browser and the manager that started the flow. - The Console preserves the Vite `base` in management, connect, callback, and return URLs. - Concurrent refreshes use a database lease, so only one request refreshes a rotating refresh token. ## Errors
Code Cause
CONNECTIONS_NOT_CONFIGURED The database or the encryption key is missing.
CONNECTIONS_INVALID The management or connect request is not valid, or the ticket or state does not match.
CONNECTIONS_NOT_FOUND No Connection Definition has this name.
CONNECTIONS_MISSING The Connection is not connected.
CONNECTIONS_NEEDS_RECONNECT The provider rejected the refresh token.
CONNECTIONS_KEY_MISMATCH The grant was sealed with a different key.
CONNECTIONS_DENIED , CONNECTIONS_APPROVAL_REQUIRED Access rules blocked the call.
CONNECTIONS_ORIGIN_NOT_ALLOWED The request URL is not in the provider origins . ViteHub did not send the credential.
CONNECTIONS_PROVIDER_FAILED The provider returned an error. details.status has the HTTP status.
CONNECTIONS_UNAVAILABLE Another request holds the refresh lease. Try again.
CONNECTION_FORBIDDEN The management request has no authorized manager, or it is cross-origin.
CONNECTION_AUTH_REQUIRED The management routes use the 'development' policy and the server is not a development server.
When you change the provider of a Connection, the stored grant belongs to the old provider. The status becomes `needs-reconnect` with `lastError: 'CONNECTIONS_PROVIDER_CHANGED'`, and ViteHub never sends that grant to the new provider. Reconnect the Connection. # Connections server API > Call a Connection from server code, read its activity, and manage Connections from the CLI. ## Public imports
Import Use
defineConnection , oauth2 , apiKey , useConnection from vite-hub/connections Define a Connection, use a generic OAuth 2 or API key provider, and call a Connection from server code.
google , gmail , gmailOperations from vite-hub/connections/google Google OAuth preset and typed Gmail REST Operations.
createConnectionsRuntime , useConnectionsRuntime from vite-hub/connections/server Runtime access for hosts and tests.
createConnectionsHandler from vite-hub/connections/http Management, connect, and callback routes. The Console mounts them. It requires an actor access policy.
hubConnections from @vite-hub/connections/vite Standalone Vite integration.
## Management routes The management routes are `POST /_vitehub/connections`, `GET /_vitehub/connections/connect/:name`, and `GET /_vitehub/connections/callback`. With a Vite `base`, they start with that base. Each route checks access itself before it reads or changes a Connection. With the Console, it runs the same access policy as the Console data routes, then names the manager for activity:
Console access Check Manager
Console Auth The Console Auth session and its authorize callback. The signed-in user.
access: 'auth' with an Auth Definition The app Auth access routes that protect the Console. The signed-in user.
Cloudflare Access The Cloudflare Access token. user:cloudflare-access
exposure: 'host-managed' The host console.authorize function. user:host-managed
console: true Development server only. user:local
A rejected request returns `403` with `CONNECTION_FORBIDDEN`. With `createConnectionsHandler()`, pass `actor`: a function that returns `user:` for an authorized manager, or `'development'`. `'development'` allows `user:local` only when `NODE_ENV` is `development`, and returns `500` with `CONNECTION_AUTH_REQUIRED` elsewhere. The OAuth callback is reached by a redirect from the provider. It completes the flow only for the browser that started it (the `state` cookie) and for the same manager. The `state` works one time only and expires after 10 minutes. A `state` from another manager returns `403`. ## Calls `useConnection(name, options)` returns a client:
Method Behavior
call(operation, input) Runs a typed Operation. An Operation has an id , an effect ( read or write ), and a request(input) builder.
fetch(url, init) Sends a request with the access token. GET and HEAD are reads with the id fetch.get or fetch.head . Other methods are writes.
status() Returns the Connection summary without tokens.
Each call checks access, gets a valid access token, and sends the request. ViteHub refreshes the token 60 seconds before it expires. When the provider returns `401`, ViteHub refreshes once and retries once. When the provider rejects the refresh token, the status changes to `needs-reconnect`. Options:
Option Default Description
event None The request event. The route becomes the actor, for example GET /api/labels .
actor Route from event , else { id: 'server', kind: 'service' } The actor for access rules and activity. An agent: actor fails with CONNECTION_INVALID : Agent actors come only from the ViteHub Agent runtime, through the Connection capabilities of the Agent.
dryRun false Write Operations return { skipped: 'dry-run', operation } and do not call the provider. fetch returns 204 with the x-vitehub-connection-skipped header.
audit 'changes' 'changes' records writes, denials, skipped calls, and failures. 'all' also records reads.
signal None Cancels token refresh, refresh-lease waits, and the Operation request, including its response body.
trace None traceId , invocationId , runId , and tool to link activity to a trace.
## Activity ViteHub stores activity in the `vitehub_connection_activity` table. Each entry has the actor, action (`call`, `connect`, `refresh`, `disconnect`), Operation id, effect, outcome, provider status, duration, target host and path, and trace ids. Activity never contains request bodies, response bodies, headers, or tokens. Agent tools record every call, reads included. MCP protocol messages record successful, denied, and failed requests. The Console shows activity for each Connection. ## CLI The CLI calls the management route of a running development server with the Console enabled. `--url` defaults to `VITEHUB_DEV_SERVER_URL`, then `http://localhost:5173`.
Command Description
vitehub connections list List Connections and their status.
vitehub connections status Show one Connection.
vitehub connections activity [name] Show recent activity.
vitehub connections connect Print a single-use connect URL.
vitehub connections set-key Store the key of an API key Connection from stdin.
vitehub connections refresh Refresh the access token now.
vitehub connections disconnect Revoke the grant at the provider and delete it. If revocation fails, ViteHub still deletes the local grant and records the error in the disconnect activity. Revoke the app at the provider then.
# Content > Parse, query, search, and serve Source content with Comark Content. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="Parsed Markdown, JSON, YAML, and media from Sources, with navigation and search, served from one generated route on every preset."} ::code-group ```ts [Definition] import sqlite from 'comark-content/database/sqlite-node' import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search' import { defineContent } from 'vite-hub/content' import { glob } from 'vite-hub/source/glob' export const content = defineContent({ plugins: [sqliteFullTextSearch({ database: sqlite() })], sources: { docs: glob({ cwd: 'docs', include: '**/*.md' }), }, }) ``` ```ts [Route] import { content } from '../content' export default defineEventHandler(async () => { return { page: await content.get('/guide'), navigation: await content.navigation(['docs']), results: await content.search(['docs'], 'runtime'), } }) ``` ```ts [Client] import searchClient from 'comark-content/plugins/sqlite-full-text-search/client' import { createContentClient } from 'vite-hub/content/client' export const content = createContentClient({ plugins: [searchClient()], }) await content.search('runtime', { instances: ['docs'] }) ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-folder-input" title="One Source definition feeds Content and direct reads" to="https://vitehub.dev/docs/content/configure"} Each named Source in `defineContent()` becomes one Comark instance. :: ::product-feature-item{icon="i-lucide-code-2" title="Documents, navigation, and search from server code" to="https://vitehub.dev/docs/content/server-api"} `content.get()`, `content.navigation()`, and `content.search()` run in server code. :: ::product-feature-item{icon="i-lucide-search" title="The search index lives in SQLite" to="https://vitehub.dev/docs/content/configure#providers"} The `sqlite-full-text-search` plugin uses Node SQLite or SQLite WASM. :: ::product-feature-item{icon="i-lucide-radio" title="The client reads the generated route directly" to="https://vitehub.dev/docs/content/server-api#client"} `createContentClient()` calls `/api/content/**`, served by the Vite plugin or Nuxt module. :: ::product-feature-item{icon="i-lucide-bot" title="Give an Agent the files through Workspace" to="https://vitehub.dev/docs/workspace/agent-capability"} Bind the same Source to a Workspace and attach `workspaceShell()`. :: :product-feature-item[Render Markdown and data with one server-side content import.]{icon="i-lucide-file-code-2" title="Templates stay typed" to="https://vitehub.dev/docs/content/server-api"} :: # Content configuration > Define Content Sources, plugins, and Comark options with defineContent(), select a database, and check definition limits. ## Definition options `defineContent(options)` accepts one Source or a map of named Sources. A single `source` becomes an instance named `default`. Named `sources` become one Comark instance each, composed with Comark's `contentHub()`.
Option Type Description
source Content Source input One Source. Cannot be combined with sources .
sources Record Named Sources. Each name becomes a Content instance.
plugins Comark Content plugins Applied to each instance. Markdown parsing is built in. Add json() , yaml() , media() , sql-query , or sqlite-full-text-search plugins when you need them.
Other Comark options cache , hooks , markdown , logger , onError , baseURL Forwarded to each Comark Content instance. ViteHub sets basePath to /api/content .
Plugin options are evaluated when the runtime is created. Recreate or rebuild the runtime to change them. ### Source inputs
Input Behavior
Source definition, such as glob() or defineSource() Opens a new Reader for each load. Overlapping refreshes, fresh snapshots, and fresh document reads keep their selected revisions.
Registered Source name Opens useSource(name) for each load.
Reader factory () => reader Calls the factory for each load.
Explicit reader with items() Keeps its caller-owned lifecycle and selected revision across refreshes.
Native Comark Source, such as comark-content/sources/fs Keeps its own loading behavior.
Each item's public path comes from its `path`, or from its key. Items need `content` or `data`. ViteHub serializes structured `data` as JSON before Comark parses it. Use `contentSource(input, { prefix, schema })` to set Comark options for one Source. A direct adapter selects a Reader when `keys()` starts an enumeration. Its later `getItem()` calls use that Reader until the next enumeration. Pass the adapter through `defineContent()` to isolate overlapping loads. ## Providers Content has no hosted provider. Comark Content stores its query and search index in the database that you pass to a plugin.
Database Import Use
Node SQLite comark-content/database/sqlite-node Node runtimes with built-in SQLite.
SQLite WASM comark-content/database/sqlite-wasm Runtimes where Node SQLite is unavailable.
The `vitehub()` Vite plugin generates the Content route for every preset. The Nuxt module installs the same behavior. Libraries that use owner packages get it from `hubSource()` in `@vite-hub/source/vite`. Each Source loader must also work on the host. For example, `glob()` reads the local file system at runtime. ## Limits - Define either `source` or `sources`. Passing both throws. - A project has one Content file. More than one `content.ts` (or `.js`, `.mjs`, `.cjs`, `.mts`, `.cts`) across server directories fails route generation with `SOURCE_B0007`. The file must export `content`, or generation fails with `SOURCE_B0008`. - Public paths must stay inside the Source root. ViteHub rejects absolute paths, `.` and `..` segments, Windows drive paths, and paths that start with `.git` or `.vitehub` (`CONTENT_R0001`). - Two items with the same public path fail the load (`CONTENT_R0003`). - An item without `content` or `data` fails to parse (`CONTENT_R0002`). - Raw media uses the newest Source revision that finished enumeration. - `defineContent()` uses `AsyncLocalStorage` from `node:async_hooks`. The runtime must provide it. # Content get started > Install Comark Content, define server/content.ts, and read the first document. ## Quick start ::steps{level="3"} ### Install `comark-content` is an optional peer dependency of `vite-hub`. Install it with the framework package. ```bash [Terminal] pnpm add vite-hub comark-content ``` ### Configure Content needs no extra configuration key. The `vitehub()` Vite plugin and the Nuxt module discover `server/content.ts` and serve its exported `content` instance at `/api/content/**`. Do not add a framework route or a `fetch()` wrapper. ### Start using it ```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/content' import { glob } from 'vite-hub/source/glob' export const content = defineContent({ plugins: [sqliteFullTextSearch({ database: sqlite() })], sources: { docs: glob({ cwd: 'docs', include: '**/*.md' }), }, }) ``` Read it from server code: ```ts [server/api/guide.get.ts] import { content } from '../content' export default defineEventHandler(async () => { return await content.get('/guide') }) ``` Or read the generated route from the client: ```ts [app/utils/content.ts] import searchClient from 'comark-content/plugins/sqlite-full-text-search/client' import { createContentClient } from 'vite-hub/content/client' export const content = createContentClient({ plugins: [searchClient()], }) await content.search('runtime', { instances: ['docs'] }) ``` :: Read [Server API](https://vitehub.dev/docs/content/server-api) for every Content method. # Content server API > Read documents, navigation, search results, and cache entries from the Content runtime. ## Public imports
Import Use
defineContent from vite-hub/content Define the Content runtime that ViteHub serves from server/content.ts .
contentSource from vite-hub/content Adapt one Source and set Comark options such as prefix and schema .
defineContentHandler from vite-hub/content Adapt a Content runtime to an H3 or Nitro route in another server.
createContentClient , defineContentClientPlugin from vite-hub/content/client Use or extend the typed Comark Content client.
comark-content/database/* , comark-content/plugins/* , comark-content/sources/* Comark databases, parser and search plugins, and native Comark Sources.
Libraries can install `@vite-hub/content` and use the matching `@vite-hub/content` and `@vite-hub/content/client` imports. ## Runtime API `defineContent()` returns a Comark Content hub with ViteHub adapters for named Sources.
Method Behavior
content.get(path) Returns one parsed document by public path, such as /guide .
content.list(names?) Lists documents. Accepts one instance name or an array.
content.navigation(names?) Builds one navigation tree across the selected instances.
content.search(names, query, options?) Runs full-text search across the named instances. Requires the sqlite-full-text-search plugin.
content.init() Loads and parses every instance.
content.getSource(name?) Returns the Comark Source for one instance. Defaults to default .
content.handler(request) Answers a Web Request . The generated route calls it through defineContentHandler() .
```ts await content.get('/guide') await content.navigation(['docs']) await content.search(['docs'], 'runtime') ``` Methods that plugins add, such as SQL queries, stay available on the returned runtime. ### Cache Comark Content owns parsed document cache entries. ViteHub routes each cache call to the instance that owns it.
Method Behavior
content.cache.refresh(name) Reloads one instance from its Source.
content.cache.invalidate(key) , content.cache.expire(key) Invalidates or expires one cache entry. Keys start with the instance name, such as docs:guide/index.md .
content.cache.get(key) , content.cache.set(key, value) Reads or writes one cache entry.
content.cache.keys(name?) , content.cache.clear(name?) Lists or clears entries for one instance or all instances.
content.cache.snapshot(name, options?) Refreshes one instance and builds a Comark cache artifact.
### Client `createContentClient()` calls `/api/content` by default. Pass `instances` to search specific named Sources. Client plugins, such as `comark-content/plugins/sqlite-full-text-search/client`, add methods that match server plugins. # Database > Define relational data with Drizzle and query it through generated ViteHub imports. ::product-hero{hosts="Node, Docker, Cloudflare, Vercel, Netlify, Deno" tagline="Relational tables defined with Drizzle and queried through one typed client on local SQLite, hosted libSQL, and Cloudflare D1."} ::code-group ```ts [Route] import { useDatabase } from '@vite-hub/database/drizzle' export default defineEventHandler(async (event) => { const { title, body } = await readBody<{ title: string, body: string }>(event) const app = useDatabase('default') const [note] = await app.db.insert(app.schema.notes).values({ title, body }).returning() // A Named Database has its own typed client and connection. const analytics = useDatabase('analytics') await analytics.db.insert(analytics.schema.events).values({ name: 'note.created' }) return note }) ``` ```ts [Definition] 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(), }), }, }) ``` ```ts [Agent] import { defineAgent } from 'vite-hub/agent' import { db } from 'vite-hub/agent/capabilities' export default defineAgent({ driver: { model: 'openai/gpt-5.1-mini' }, capabilities: [ db({ mode: 'write', policy: 'require-approval' }), ], }) ``` ```bash [CLI] pnpm vitehub db generate pnpm vitehub db migrate ``` :: :: ::product-features ::product-feature-item{icon="i-lucide-code-2" title="The Drizzle schema is the source of truth" to="https://vitehub.dev/docs/database/configure"} ViteHub discovers `src/database.ts` and generates the Drizzle artifacts. :: ::product-feature-item{icon="i-lucide-terminal" title="Generate migrations from the Definition" to="https://vitehub.dev/docs/database/get-started"} `vitehub db generate` writes migrations; `vitehub db migrate` applies them. :: ::product-feature-item{icon="i-lucide-database" title="One typed client for each database name" to="https://vitehub.dev/docs/database/server-api"} `useDatabase()` returns `db` and `schema` for `default` or a Named Database. :: ::product-feature-item{icon="i-lucide-bot" title="Guarded SQL for an Agent" to="https://vitehub.dev/docs/database/agent-capability"} `db_query` and `db_schema`, plus `db_exec` in write mode. :: :product-feature-item[Local SQLite, hosted libSQL, or Cloudflare D1, with the same imports.]{icon="i-lucide-cloud-cog" title="Change the connection, keep the route" to="https://vitehub.dev/docs/database/hosts"}:product-feature-item[Keep joins, constraints, and migrations in the database layer.]{icon="i-lucide-table-2" title="Queries return typed rows" to="https://vitehub.dev/docs/database/server-api"} :: # Database capability > Give an Agent guarded SQL query and schema tools and, in write modes, one mutation tool. `db()` gives an Agent the `db_query` tool for one read-only SQL statement and the `db_schema` tool for schema inspection. When `mode` or `schemaMode` is `"write"`, it also gives the `db_exec` tool for one mutation statement with a rationale. All tools call the configured [Database primitive](https://vitehub.dev/docs/database). The [Database server API](https://vitehub.dev/docs/database/server-api) covers application code. This page covers the Agent tools. ## Configure database access Attach the Database Capability in read mode until the Agent needs guarded mutations. ```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' }), ], }) ``` To allow data changes but not DDL, set `mode: 'write'` and keep `schemaMode: 'read'`. ```ts [server/agents/support.ts] db({ mode: 'write', policy: 'require-approval' }) ``` ## Agent-visible tool contract These definitions are resolved from the real Capability during the docs build. They are the names, descriptions, and JSON Schemas supplied to the Agent model. ### Read mode :agent-capability-tools{name="db" variant="read"}### Data and schema write mode :agent-capability-tools{name="db" variant="write"}## How database access works When the Agent Invocation resolves its tools, the Capability resolves the database handle. With `database`, it calls `database(name)` on the handle. - `db_query` accepts one `SELECT`, one `WITH ... SELECT`, or one read-only introspection `PRAGMA` (`table_info`, `table_xinfo`, `table_list`, `index_list`, `index_info`, `index_xinfo`, `foreign_key_list`, `foreign_key_check`). It calls `query(statement)`. - `db_schema` takes no input. It returns `{ database, schema }`, where `schema` comes from the `schema` value or `schema()` function on the database handle. - `db_exec` requires a non-empty `rationale` and exactly one statement. It rejects read-only SQL. DDL (`ALTER`, `CREATE`, `DROP`, `REINDEX`, `VACUUM`) requires `schemaMode: 'write'`. Data mutations (`INSERT`, `UPDATE`, `DELETE`, `REPLACE`) require `mode: 'write'`. It rejects other statements. It calls `exec(statement)`. The guard rejects multi-statement input before the statement reaches the database handle. `WITH` statements are classified by their final statement keyword. ## Requirements - Configure the Database primitive. Generated Agent routes pass `agentDb` from `@vite-hub/database/drizzle` to the Capability when the Database Vite integration (`hubDb()`) is active. - Without a `db` handle, the Capability imports `agentDb` from an installed `@vite-hub/database/drizzle` package. If that import fails, tool resolution fails and the Agent Driver does not receive Database tools. - The database handle must expose raw string `query()` for `db_query` and `exec()` for `db_exec`. - A `database` value other than `"default"` requires a handle that exposes `database()`. Otherwise the tools fail with `Database "" is not available.` ## Security and approval - `db_query` and `db_schema` have no policy gate. The Agent can read every table that the database handle can read. - `db_exec` is present only when `mode` or `schemaMode` is `"write"`. Each mode allows only its own kind of statement. - The guard classifies statements by their keywords. It is not a database permission system. Use database credentials with the smallest set of privileges that the Agent needs. - `policy` applies only to `db_exec`. The single-statement, rationale, and SQL-kind checks run also when `policy` allows the call. - `policy` accepts `'allow'`, `'require-approval'`, `'deny'`, `'retryable-failure'`, or a function that receives `{ name, input }` and returns one of these values. - Without `policy`, an enabled `db_exec` call runs when the Agent calls it. - `'require-approval'` stops the call with `APPROVAL_REQUIRED` and an Approval Request. The request input contains the statement and the rationale. See [Runtime policy, approvals, and traces](https://vitehub.dev/docs/getting-started/concepts/runtime-policy-approvals-and-traces). ## Driver support
Agent Driver Support
Model-backed Receives db_query , db_schema , and db_exec when a write mode is enabled.
Provider-backed Receives the same tools through the provider MCP bridge.
Custom-run-backed driver.run receives the tools in context.tools and decides whether to call them.
## Verify database access 1. Start the Vite development server. 2. Run `vitehub agent info --agent support --json`. Confirm that `tools` contains an entry with `name: "db"`, and that `capabilities` contains `{ id: "db", metadata: { schemaMode: "read" } }`. 3. Run `vitehub agent dev "How many users are there?" --agent support`. Confirm that the output shows a `[tool] db_query` call. 4. Ask the Agent to run two statements in one `db_query` call. Confirm that the tool rejects the input before it reaches the database. ## Options
Option Type Default Description
database string "default" Selects a named database through database() on the database handle.
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 | (context) => AgentToolPolicyDecision | Promise none (calls run) Policy for db_exec .
## Related pages - [Database primitive](https://vitehub.dev/docs/database) - [Official capabilities](https://vitehub.dev/docs/agents/capabilities/official) - [Runtime policy, approvals, and traces](https://vitehub.dev/docs/getting-started/concepts/runtime-policy-approvals-and-traces) # Database configuration > Set Database integration options and define Default and Named Databases. ## Configure the Vite Integration `hubDb()` accepts the integration options below. You can also set them under the `database` key in `vite.config.ts`. That key takes precedence over the options passed to `hubDb()`. ```ts [vite.config.ts] import { hubDb } from '@vite-hub/database/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [hubDb()], database: { cli: { generate: false }, }, }) ```
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 the package-owned db generate command.
database.cli.migrate false enabled Disables the package-owned db migrate command.
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 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.
With `vitehub()`, a discovered Database Definition does not enable Database. Without the `database` option, a build or `vite dev` that imports `vite-hub/database/drizzle` fails with `VITE_HUB_B0013`, and `vitehub db generate` and `vitehub db migrate` exit with an error. Add `database: true` or an options object to enable them. A directly composed `hubDb()` also provides Database. ## Define a database A Database Definition keeps the Database Table Schema next to the server code that uses it. ViteHub discovers Definitions from these files:
File Database
src/database.ts or server/databases/config.ts Default Database, addressed as default .
src/.database.ts or server/databases//config.ts Named Database, addressed as . The Definition must set name: '' .
A project uses either one Default Database or a set of Named Databases. Mixing both modes, or declaring two Default Databases, fails discovery. ```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(), }), }, }) ``` ## 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 or libSQL connection URL. Defaults to file:.vitehub/data/database/sqlite.db for a Default Database and file:.vitehub/data/database/.sqlite.db for a Named Database.
connection.authToken DatabaseConfigValue No Hosted database auth token.
cloudflare.binding string No D1 binding. Defaults to DB for a Default Database and DB_ for a Named Database.
cloudflare.databaseId DatabaseConfigValue No D1 database id.
cloudflare.http true | { url, authToken } No 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 only public `DatabaseDialect`. # Database get started > Install Database, define a schema, apply the first migration, and query it. ## 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 Define the schema in `src/database.ts`: ```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(), }), }, }) ``` Generate and apply the first migration: ```bash [Terminal] pnpm vitehub db generate pnpm vitehub db migrate ``` Query it from server code: ```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) }) ``` :: ## Generate and apply migrations The Database integration adds the `db` 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 ```
Command Effect
vitehub db generate Refreshes the generated Drizzle config and creates migrations from your Database Definitions. Pass --name to name the migration or --custom to create an empty migration.
vitehub db migrate Refreshes the generated Drizzle config and applies pending migrations.
Migrations go to a `migrations` directory next to each Database Definition file. # Database hosts > Select local SQLite, hosted libSQL, or Cloudflare D1, read what each host output contains, and check production rules. ## Providers
Provider or runtime Configure with Notes
Local SQLite connection.url or no connection config Default for local development and generated Drizzle artifacts.
Hosted libSQL (for example Turso) 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 you set cloudflare.http .
### 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 }, }) ``` Set `cloudflare.http: { url, authToken }` to send the same raw query wire format to an authenticated HTTP(S) proxy. Both values are required at runtime. 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 }, }) ``` D1 HTTP also generates Drizzle Kit `d1-http` credentials. Migration and inspection commands call Cloudflare's API with `CLOUDFLARE_ACCOUNT_ID`, the database id, and `CLOUDFLARE_API_TOKEN`. ViteHub never embeds these credentials 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/). :: If you omit `cloudflare.http`, ViteHub keeps local SQLite and the hosted libSQL selection, even when `cloudflare.databaseId` is set. ### 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 keep 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 its own `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 and schema artifacts, and wires provider-specific output. Provider bindings are integration details. The public database identity is the Default Database or Named Database name. 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. Run `vitehub inspect definitions --kind database` to list the Definitions that ViteHub discovered. A Vite app that builds with the Nitro Vite plugin and a Cloudflare preset gets each Database Definition's resolved `cloudflare` binding in `nitro.cloudflare.wrangler.d1_databases`. You do not repeat it in Nitro config. A binding with the same name in Nitro config is replaced by the Definition's binding; other bindings stay. A Definition is added only when it has a `databaseName` and a D1 id from `databaseId` or provision state. Its migration SQL is copied beside the generated Wrangler config. 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`. A Cloudflare production build fails before deployment when it cannot get the id from provision state, `database.databaseId`, or an existing complete matching Nitro Wrangler binding. ::note `@vite-hub/database/nuxt` is a narrow Nuxt lifecycle bridge for one D1 Database Host Resource. Its main use is to keep Nuxt Content and Cloudflare `wrangler.d1_databases` in sync. Discovered Database Definitions still own the Drizzle Runtime Surface. For Cloudflare and Vercel builds, the module writes the hosted runtime, schema modules, and Definition defaults before Nitro resolves their imports. Explicit Nitro aliases take precedence. :: ## Production checks - The Database Table Schema is the schema in code. The live schema can differ when migrations have not run or when an Agent has schema write permission. - `sqlite` is the only dialect. Local SQLite, hosted libSQL, and Cloudflare D1 all use SQLite-compatible SQL. - 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, backups, and hosted database lifecycle in deployment workflows, not in route code. # Database server API > Query a discovered database from server code with a typed Drizzle client. ## 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 the 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`. ## Use it at runtime Call `useDatabase()` from server code with the discovered database name. Use `default` for a Default Database. It returns the Drizzle client as `db` and the Definition schema as `schema`. ```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 ViteHub Vite Integration resolves `@vite-hub/database/drizzle` for server code and Provider Output. Do not run files that import it with plain `node`. Run them through your Vite-built server or Provider Output. ### Use Named Databases Use Named Databases when the app needs more than one independent database. Name a database for a real data or deployment split, not for a source-code folder. ```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 }, }) ``` ```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) }) ``` # Local development > Run ViteHub locally, inspect generated files, and verify the behavior you changed. Use this section to run a ViteHub application on your machine and check its behavior before you deploy it. ViteHub uses the same Vite config to discover Definitions, write generated files, and resolve Agent metadata in development and in production builds. ## Choose a local tool
Tool Use it for Output to inspect
Vite dev server Definition discovery, server imports, Agent streams, and local providers Terminal output and responses from your routes
ViteHub CLI Package-owned commands such as inspect , agent dev , agent eval , and provision Exit code, terminal output, and optional JSON
ViteHub Console Discovered Agents, retained sessions, invocation events, and stored data The /_vitehub page in the browser
Generated files Registries, generated types, Provision State, and host output .vitehub/** and the preset's output directory
Application tests Server API behavior and application regressions Test output
## Run the local app Start the application with its normal development command. ViteHub integrations run during Vite startup, so discovery and generated files use the application root. ```bash [Terminal] pnpm dev ``` When discovery works, run the application's tests and a production build. ```bash [Terminal] pnpm test pnpm build ``` ## Inspect what ViteHub discovered Run `vitehub inspect` while the app is configured. It lists the Definitions that each enabled package discovered and the host output files that a build writes. It does not start a server or call a provider. ```bash [Terminal] pnpm vitehub inspect definitions pnpm vitehub inspect provider-output ``` Use [`vitehub agent dev`](https://vitehub.dev/docs/development/cli#talk-to-an-agent-during-development) to talk to an Agent through the running dev server. Enable the [Console](https://vitehub.dev/docs/development/console) when you need session search and invocation events in a browser. ## Verify before deploy Run the narrowest check that covers the behavior you changed: - `vitehub agent eval` for Agent behavior. - `vitehub provision run --provider --dry-run` for provider resources. - `pnpm build` when generated host output changed. [Verification](https://vitehub.dev/docs/development/verification) explains each check and what it does not cover. ## Next steps - Open [CLI](https://vitehub.dev/docs/development/cli) for every command and flag. - Open [Generated files](https://vitehub.dev/docs/development/generated-files) when a registry or host output looks wrong. - Open [Troubleshooting](https://vitehub.dev/docs/development/troubleshooting) when a check fails. - Open [Errors and diagnostics](https://vitehub.dev/docs/reference/errors-diagnostics) for failure families. # CLI > Run package-owned development workflows through the local Vite config. The ViteHub CLI loads the local Vite config and collects package-contributed command namespaces. Each invocation resolves each active plugin's CLI contributor once and uses its command namespaces and Provision Steps together. A later invocation resolves fresh contributions. 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) 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 CLI owns the `inspect` namespace. Plugin command contributions with that name are ignored. The Agent Package contributes `agent` and `channels` when `hubAgent()` is active, Blob contributes `blob` when `hubBlob()` is active, Database contributes `db` when `hubDb()` is active, Env contributes `env` when `hubEnv()` is active, KV contributes `kv` when `hubKv()` is active, Schedule contributes `schedule` when `hubSchedule()` is active, Workflow contributes `workflow` when `hubWorkflow()` is active, Workspace contributes `workspace` when `hubWorkspace()` is active, and the Console integration contributes `console` when `console` is enabled. The framework contributes `types` and `box`, and the CLI includes the built-in `inspect` and `provision` namespaces. `box` does not load the project config, so it also runs in a deployed container without Vite. ```txt [Output] Usage: vitehub [args...] Available namespaces: workspace Workspace development workflows. console Console development workflows. agent Agent development workflows. blob Read and write blobs of the Blob stores in a running Vite + Nitro Development Server. channels External Channel registration workflows. db Database development workflows. kv Read and write keys of the KV stores in a running Vite + Nitro Development Server. env Server Env inspection workflows. email Inspect and preview development Email messages. schedule Run Static Schedule Definitions and inspect or control Runtime Schedules. workflow Start and inspect Workflow runs in development. types Generate ViteHub TypeScript declarations. inspect Inspect discovered Definitions and generated Provider Output. provision Idempotently create missing provider resources. box Serve and check an SSH Box runner. Does not load the project config. ``` ## 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 agent invocations Available Agent Package List, inspect, follow, or cancel records in the application's Agent Invocation journal, locally or on a deployed Console.
vitehub blob list Available Blob Package List blobs of a Blob store, one page at a time.
vitehub blob head Available Blob Package Show the metadata of one blob.
vitehub blob get Available Blob Package Download one blob to a file or to stdout, byte for byte.
vitehub blob put Available Blob Package Upload one file as a blob and print what changed.
vitehub blob del Available Blob Package Delete one blob and print what changed.
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 channels replay Available Agent Package Replay stored Channel history through an Agent from a development server.
vitehub console dev Available Console integration Start the app's development command with deterministic Console fixture data.
vitehub connections Available Connections Package Connect OAuth accounts, set API keys, list Connections, read activity, and approve or deny writes.
vitehub env inspect Available Env Package List declared Server Env variables and their status without values.
vitehub env check Available Env Package Fail CI or a deploy step when Server Env would not load for a stage.
vitehub email outbox Available Email Package List, show, or clear messages captured by a running Vite + Nitro Development Server.
vitehub email preview Available Email Package Render a server/emails template locally without sending it.
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 kv list Available KV Package List the keys of one KV store, one page at a time.
vitehub kv get Available KV Package Print the value of one key.
vitehub kv has Available KV Package Check if a key exists. The exit status is 0 or 1.
vitehub kv set Available KV Package Write the value of one key and print what changed.
vitehub kv del Available KV Package Delete one key and print what changed.
vitehub schedule list Available Schedule Package List Runtime Schedules with enabled state, next due time, and last run.
vitehub schedule get Available Schedule Package Show one Runtime Schedule.
vitehub schedule runs Available Schedule Package List the recorded runs of one Schedule, newest first.
vitehub schedule attempts Available Schedule Package List the attempts of one Schedule Run.
vitehub schedule run Available Schedule Package Run a manual Static Schedule Definition locally or through the deployed Console.
vitehub schedule run-runtime Available Schedule Package Run one Runtime Schedule now in the development runtime.
vitehub schedule enable Available Schedule Package Enable one Runtime Schedule.
vitehub schedule disable Available Schedule Package Disable one Runtime Schedule.
vitehub box check Available Box Package Start the provider Driver through the SSH runner and report readiness.
vitehub box serve Available Box Package Serve authenticated SSH commands from this machine.
vitehub workflow start Available Workflow Package Start a discovered Workflow in the local development runtime.
vitehub workflow get Available Workflow Package Read a Workflow run.
vitehub workflow cancel Available Workflow Package Cancel a run when the provider supports it.
vitehub workflow resume Available Workflow Package Resume a Workflow signal by hook token.
vitehub workspace dev Available Workspace Package Run commands through a Workspace Session exposed by a Compatible Vite Development Server.
vitehub types prepare Available ViteHub Framework Prepare generated TypeScript declarations for editors and type checking.
vitehub inspect definitions Available ViteHub CLI plus package inspection contributors List the Definitions that each active package discovered.
vitehub inspect provider-output Available ViteHub CLI plus package inspection contributors List generated Provider Output files with secrets redacted.
vitehub provision run Available ViteHub CLI plus package Provision Steps Create missing provider resources idempotently.
vitehub provision status Available ViteHub CLI plus package Provision Steps Inspect the latest provider provisioning result.
## Read and write blobs Start the app's Vite Development Server, then run `vitehub blob` from another terminal. The commands call the same Blob storage as the running app, so they read and write the blobs that the app uses. ```bash [Terminal] pnpm vitehub blob list --prefix avatars/ --limit 20 pnpm vitehub blob head avatars/ada.png pnpm vitehub blob get avatars/ada.png --output ./ada.png pnpm vitehub blob get reports/2026.csv > report.csv pnpm vitehub blob put avatars/ada.png ./ada.png --content-type image/png pnpm vitehub blob del avatars/ada.png ``` Each write command prints what it changed: ```txt [Output] Created blob avatars/ada.png in store default (48213 B, image/png). Deleted blob avatars/ada.png from store default. ``` Every command accepts `--store `, `--json`, `--url ` when Vite does not listen on `http://localhost:5173`, and `--timeout `. The commands use the Default Blob Store. Pass `--store` to select a named store from `blob.stores`. An unknown store fails and lists the configured stores, with `default` first, as the Console does. - `list` prints a table of pathname, size, content type, and upload time. `--limit` defaults to 100 and has a maximum of 250. When more blobs exist, stderr shows the `--cursor` value for the next page. - `head` prints the metadata of one blob. A missing blob exits with status 1. - `get` writes the file bytes unchanged. Without `--output`, the bytes go to stdout, so redirect them to a file or a pipe. With `--output `, the command writes the file and prints a summary. `--json` needs `--output`, because stdout carries the file bytes otherwise. - `put` uploads a file relative to the current directory. Without `--content-type`, the Blob storage detects the type from the pathname. The output says if the blob was created or replaced. The created/replaced label is best-effort because it is based on a metadata read immediately before the write; eventual consistency and concurrent writers can make it stale. - `del` says if the blob existed. Deleting a missing blob changes nothing and exits with status 0. The existed/missing label is best-effort for the same reason, and a concurrent writer can change the object between the metadata read and delete. The Vite dev endpoint forwards a JSON request body, so `put` sends the file as base64 and accepts files up to 8 MiB. The CLI checks the size before it reads the file. `get` returns the raw bytes as a stream. There is no `sign` command. Errors go to stderr, or into `{ "error": { "code", "message" } }` on stdout with `--json`. The commands do not print blob URLs, because a signed URL can carry credentials. Metadata values under secret names, such as `token`, are redacted, as the Console Blob page does. The commands use a guarded dev endpoint that `hubBlob()` registers only on the Development Server. The endpoint forwards each operation into the Nitro dev environment, which owns the Blob storage. Nuxt and plain Vite do not run Nitro in the Vite process, so the endpoint returns status 501 and the CLI prints that the host is not supported. Deployed runtimes do not expose the endpoint. ## Inspect the Email development outbox When a Vite development server has Email enabled, inspect the in-memory outbox from another terminal: ```bash [Terminal] pnpm vitehub email outbox list pnpm vitehub email outbox show --html pnpm vitehub email outbox clear ``` Use `--json` for machine-readable output. The outbox is available only in `vite dev` and is cleared when the server restarts. Use `vitehub email preview