Skip to main content
An agent is a file in agents/ that exports a system prompt, optional tools, optional memory, and optional skills. The runtime auto-discovers it on startup and exposes it via getAgent(id) or a route created with createAgUiHandler(). For the normal path, omit model and let runtime conventions choose: local inference by default, Veryfront Cloud when VERYFRONT_API_TOKEN plus project context are set.

Prerequisites

  • A Veryfront project running locally (see Create project).
  • A provider configured for inference (see Providers).
  • The agents/ directory exists. If you customised ai.agents.discovery.paths in Configuration, use that directory instead.

Define an agent

Create a file in agents/:
The id is how you reference the agent later with getAgent("assistant"). You can also define an agent with markdown when the agent only needs persona, model, and step configuration:
The file path provides the agent id. For example, agents/support.md registers support and can be invoked through the same project runtime and control-plane surfaces as agents/support.ts.

Per-agent skills and tools

A markdown agent can own its skills and tools by using a directory instead of a single file. Put the agent definition in AGENT.md and colocate its capabilities beside it:
The directory name is the agent id. The flat agents/{id}.md form still works for agents that do not own skills or tools, and both layouts can coexist. Colocated capabilities are registered with owner metadata and namespaced {agentId}--{name}. Ownership controls visibility everywhere: an agent only ever sees unowned (project-global) capabilities plus its own - never another agent’s. This one rule applies to skills: and tools: for every agent kind (TypeScript, flat markdown, and directory markdown):
  • Omit skills or use skills: true to advertise and authorize every skill visible to the agent. Use skills: [] to advertise none and to authorize no project or configured skill for load_skill.
  • The difference between omitting skills and skills: true shows only when a project has no skills at all. An agent that omitted it gets no skill tools, because there is nothing for them to load. skills: true is a declaration, so the tools stay whatever the registry holds.
  • tools: true - every currently scoped tool is authorized, while non-bootstrap schemas are deferred behind tool_search until the agent searches for them.
  • skills: [..] / tools: [..] - each entry resolves as the agent’s own short name first, then as a global id. A colocated short name that shadows a global id is reported at discovery so the reference stays unambiguous.
  • Use denied-tools: [..] in Markdown agent frontmatter to preserve explicit tool denials. deniedTools is also accepted for serialized definitions. Do not combine either form with tools: true: the serialized runtime cannot represent “all except”, so it fails closed and disables every project tool. List the allowed tools explicitly when you also need denials.
  • Duplicate agent ids (flat file + directory) and agent ids whose sanitized namespaces collide are reported as discovery errors. The same catalog metadata is used by local and hosted runtime paths. Hosted skill loading uses the catalog sourcePath, not a path reconstructed from the namespaced id, so load_skill("researcher--cite") resolves to the actual colocated SKILL.md.

Add tools

Agents call tools to take actions or fetch data. Reference tools by name: the framework resolves them from the tools/ directory:
temperature controls model sampling and defaults to 0. It does not guarantee repeatable output. Runtime provider capabilities may omit or normalize the value for models that reject generic sampling parameters or require mode-specific values. maxSteps limits how many tool-call iterations the agent can perform per request. See Tools for how to define getWeather.

Load broad tool catalogs progressively

The tools selector controls both authorization and initial schema exposure:
  • Omit tools to expose no project tools.
  • Use an explicit map to expose only those selected schemas immediately.
  • Use tools: true to authorize every tool in the current scope while initially exposing only bootstrap tools and tool_search.
A successful search makes matching authorized schemas visible on the next model step.
The framework tool_search fallback is provider-neutral. It searches the authorized tools catalog and configured providerTools that the selected model supports. Provider-native entries contain only a name and description until a search loads them. The runtime attaches the provider’s native schema on the next model step. Search ranks an exact tool name first, followed by normalized substrings in the tool name, description, and input parameter descriptions. It returns at most five names and descriptions. Results never include schemas, and tool_search has no pagination options. Loading a schema never authorizes a tool. The runtime rechecks authorization before execution. It also filters restored loaded-tool state against the currently authorized catalog. You can use deferred loading with a direct provider and its API key without Veryfront Cloud. Hosted durable runs additionally require the Veryfront API durable run-event contract. The hosted runtime stores loaded-tool state in a private checkpoint and waits for that checkpoint before continuing. Private checkpoint data does not appear in public messages or replay. Configured, supported provider-native tools use the same private exposure checkpoint. Provider replay is not part of this feature. See Tools for the search and execution flow.

Enable provider tools

Provider tools are executed by the selected model provider. They are not local tools and they are not MCP tools.
The runtime only enables provider tools that the selected provider/model supports.

Connect MCP servers

Use mcpServers for remote MCP-compatible tool servers. Put visibility policy on the server that owns the tools. When tools is an explicit object, include the remote MCP tool name in tools and authorize it with the server toolPolicy. Explicitly named tools that are not local are resolved from the Veryfront API MCP server when mcpServers is omitted and the server bootstrap is available. This lets a project pulled from Studio run locally without repeating transport configuration. VERYFRONT_API_URL selects the API endpoint; VERYFRONT_API_TOKEN and VERYFRONT_PROJECT_SLUG provide server-side identity. These environment variables do not grant tools by themselves.
Only the explicitly named unresolved tools are requested from the remote MCP catalog. Remote tools/list remains authoritative, and browser AG-UI context cannot replace server identity. Set mcpServers: [] to opt out. An explicit mcpServers list overrides the default; use { kind: "veryfront-api" } with a toolPolicy when the connection policy should travel with the agent.

Use skills

Skills are reusable instruction packs discovered from your project’s skills/ directory. Every agent receives the visible skill catalog and load_skill automatically.
Use skills: ["incident-response", "repo-maintainer"] to advertise and authorize only those skills. Use skills: [] to advertise no skills and to authorize none for load_skill. An explicit selector is an authorization boundary for load_skill, not just a prompt filter. Local and project runtimes also expose load_skill_reference and execute_skill_script. Hosted chat reads an advertised reference through load_skill({ load: { skillId, file } }) and does not execute skill scripts directly. See Project structure for skills/ conventions and Configuration for discovery paths.

Skill execution flow

When an agent uses a skill, the flow is:
  1. Call load_skill({ load: { skillId } }) to load the skill instructions and policy.
  2. Read an advertised reference with load_skill_reference(...) on local and project runtimes, or load_skill({ load: { skillId, file } }) in hosted chat.
  3. On local and project runtimes, optionally call execute_skill_script(...) to run scripts from scripts/.
  4. Continue with normal tool calls. Loading a skill does not change which tools the run may call.
A step may batch load_skill with other tool calls. The runtime runs the calls in the order the model emitted them. A successful load_skill changes only which skill’s instructions are loaded and which reference and script files load_skill_reference and execute_skill_script can reach for later calls. Ordinary tools are unaffected, whether they were emitted before or after load_skill, and a failed load_skill does not block the rest of the batch.

Skill script execution

Skill scripts run in one of two modes, selected automatically:
  • Local (development): When no Veryfront Cloud sandbox credentials are available, scripts run as direct subprocesses on your machine via runCommand(). No remote sandbox is needed.
  • Cloud (production): When SANDBOX_AUTH_TOKEN, VERYFRONT_API_TOKEN, or request-scoped Veryfront credentials are available, scripts are uploaded to and executed inside a remote sandbox session.
Local development does not require sandbox infrastructure. Scripts run as direct subprocesses.

Skill safety model

  • allowed-tools in SKILL.md is not enforced. The Agent Skills specification defines it as pre-approval metadata (tools an agent may run without prompting), not an authorization boundary, so Veryfront records the declaration and does not restrict the run. Narrow a run by configuring the agent’s tools, not by declaring allowed-tools in a skill.
  • Skill file reads are restricted to the skill root and allowed subdirectories: references/, resources/, assets/, and scripts/.
  • Symlinked paths are rejected for skill file access.
  • Script execution timeout defaults to 60000 ms and is capped at 300000 ms.

Connect to a route

Expose a registered agent through createAgUiHandler() when a browser or external client needs AG-UI streaming. Use Create agent for the copyable quick-start route. Use Chat UI to pair that route with useChat(). If a route returns Agent not found, ensure the agent file is in agents/ and its id matches the value passed to createAgUiHandler().

Non-streaming response

For server-side generation (e.g., in getServerData), use generate(). getAgent() returns Agent | undefined, so narrow the result before calling it. Without the guard, the sample fails typecheck under the "strict": true tsconfig that veryfront init writes.

One-shot calls

For a single call with no tools and no follow-up turn - an extraction, a classification, a rewrite - you do not need an agent at all. Use generate from veryfront/llm:
It runs one step with no tools, skills or memory, and takes the same outputSchema an agent does. Reach for an agent instead when you need the thing itself rather than the answer: a registered id other code resolves, tools, memory across turns, or a system prompt built at request time. To hold such an agent to a single tool-free turn, say so:
maxSteps: 1 stops the runtime from taking a second turn it has no use for. skills: false removes the load_skill family from the request in a project that does have skills - an agent with one job should not be offered a catalog it will never open.

Structured output

Set outputSchema to constrain every response to a schema. Veryfront maps it to the selected provider’s native structured-output field, then parses and validates the model’s text back into response.object, typed from the schema with no annotation of your own.
Pass outputSchema to generate() or stream() to constrain a single request instead; a per-call schema replaces the configured one. generate() returns an object typed from the per-call schema when you pass one. A raw JSON Schema object is accepted in both places and is sent to the provider unchanged. A requested schema is never dropped silently. A model runtime that does not support structured output rejects the request, and output that does not parse or does not validate raises rather than returning a partial object. A run that stops at the step limit still returns its partial result instead of raising. On that path the final assistant text is parsed best effort: a successful parse sets response.object, and a parse or validation failure sets response.metadata.outputSchemaError next to the max-steps warning so the failure stays visible.

Runtime UTC context

Veryfront captures UTC once at the start of every generate(), stream(), and respond() run. The runtime adds the same server-authored system block before each model step:
Use these values for time-sensitive instructions. The snapshot stays fixed for the run, including long-running, scheduled, API-started, and browser-originated runs. Browser environment context can add a display timezone, but it does not replace the UTC snapshot. Non-streaming results expose the exact values at result.metadata?.runtimeContext; streaming runs emit them in the initial data event named veryfront.runtime_context for durable replay and diagnostics.

Dynamic system prompts

The system property accepts a string, a function, or an async function:
For step-boundary refresh during a long-lived run, use resolveRuntimeState instead of relying on system() to run again mid-turn. request.system is always a string, so existing text transformations remain compatible. When the runtime has structured system messages, use request.structuredSystem to read their provider metadata and return structuredSystem to replace them without flattening that metadata.

Agent configuration

Each agent middleware invocation receives a single-use next() continuation. Its lifecycle is:
  • Call next() at most once while the middleware is active.
  • The continuation becomes invalid when that middleware’s returned promise settles. Calling it again or after settlement rejects with the registered middleware-error.
  • If you call next() after the middleware function returns, downstream dispatch starts on the next microtask so settlement revocation wins races with an already-fulfilled middleware promise.
  • If you intentionally detach that promise, attach its rejection handler in the same turn or a microtask; detached-failure reporting runs on the next macrotask after that grace window.
  • Handlers attached after the macrotask grace window will not suppress the report. Synchronous throws also settle the middleware invocation and revoke its continuation.
  • Direct self-resolution of a continuation is rejected with TypeError. Indirect promise-adoption cycles are outside this middleware contract and are not diagnosed by the chain.
  • Re-entrant next() calls made by Promise species or other framework-internal result-observation hooks are rejected; they are not middleware-body calls.

Verify it worked

Save the agent file and restart veryfront dev. The quickest server-side check is a throwaway debug route:
The response carries the model’s reply. Remove the debug route before deploying. If generation fails, check the dev-server log for agent registration or provider errors. If AG-UI routing fails, use the route verification in Create agent. A healthy AG-UI stream ends with a RunFinished event.