AI agents by framework / JavaScript and TypeScript

How to add an AI agent to a Vue app

A Vue app runs in the browser, so its AI agent lives on your backend: an endpoint that holds the model key, runs a tool-calling agent as the signed-in user and streams the reply. A single-file component uses the useChat composable from @ai-sdk/vue to send messages, render the stream and ask for approval before writes. If the agent only needs to do what your app already does, you can embed one instead.

What an agent in a Vue app is made of

  • A backend endpoint: a POST /api/chat route on the server your Vue app already calls. The model API key stays there; the Vue build, including every VITE_ variable, is public.
  • An agent definition: the model, instructions, tools and stop condition in one object on the backend. Each tool wraps an operation your API already performs for the signed-in user.
  • The agent loop: the agent calls tools and reads their results until it can answer or reaches its step limit.
  • A chat component: a single-file component that calls useChat, which returns reactive refs for the messages, status and error.
  • Authorization and approval: the session cookie identifies the user on the backend, a CSRF token accompanies each request, and the template renders approve and deny buttons before a write runs.

Ways to add an agent to Vue

ApproachGood forWhat you take on
AI SDK: @ai-sdk/vue with a backend routeA custom agent in an existing Vue single-page app and its API server.The backend endpoint, authorization in every tool, approval UI, rate and cost limits.
AI SDK in a Nuxt server routeVue apps that already run on Nuxt, where the endpoint can live in the same project.The same tools, authorization and limits, in Nuxt's server directory.
An agent framework on the backend (for example Mastra or LangGraph.js)Long-running or multi-agent workflows with memory and orchestration.Another layer to learn and deploy, plus the endpoint, authorization and limits.
Hosted answer-only chat widgetQuestions about your docs or help center.Keeping its content current. It does not act in your app.
Embedded in-app agent (Bunkhouse)Letting signed-in users get tasks done through the pages and API you already have.A script tag, an identity endpoint and hints for operations discovery cannot see.

The AI SDK's Nuxt quickstart covers the server-route variant. This guide keeps Vue as a single-page app with a separate Express backend.

Build an agent with the AI SDK

The AI SDK UI package for Vue is @ai-sdk/vue. In AI SDK 7 its useChat composable replaces the older Chat class, as the 7.0 migration guide explains. On the backend, the example uses a ToolLoopAgent, which bundles the model, tools and loop settings. The examples use Vue 3 with Vite and an Express 5 backend:

Terminal
# in the Vue app
npm install ai @ai-sdk/vue

# in the Express backend
npm install ai zod

The model string "anthropic/claude-sonnet-5.5" goes through the Vercel AI Gateway, which reads the API key from the AI_GATEWAY_API_KEY variable on the backend. To call a provider directly, install its package (for example @ai-sdk/mistral) and pass its model instead.

1. The agent on your backend

The function ordersAgent builds an agent for one user, so both tools are bound to the ID that the session middleware from the Express guide put on res.locals. The model can choose an order ID, never the user. The condition isStepCount(5) caps the loop, the setting toolApproval holds cancelOrder until the user answers, and the option experimental_toolApprovalSecret signs approval requests so a client cannot forge one. The helper pipeAgentUIStreamToResponse validates the incoming messages against the agent's tools, runs the agent and streams its output into the Express response.

server/agent.ts
import { isStepCount, pipeAgentUIStreamToResponse, tool, ToolLoopAgent } from "ai";
import { Router } from "express";
import { z } from "zod";
import { cancelOrder, listOrders } from "./orders.ts"; // your existing data layer

// One agent per request: the tools close over the user the session authenticated.
function ordersAgent(userId: string) {
  return new ToolLoopAgent({
    model: "anthropic/claude-sonnet-5.5",
    instructions: "You help the signed-in user manage their orders. Use the tools; never guess order data.",
    tools: {
      listOrders: tool({
        description: "List the signed-in user's recent orders",
        inputSchema: z.object({ status: z.enum(["open", "shipped", "cancelled"]).optional() }),
        execute: async ({ status }) => listOrders(userId, { status }),
      }),
      cancelOrder: tool({
        description: "Cancel one of the signed-in user's open orders",
        inputSchema: z.object({ orderId: z.string() }),
        execute: async ({ orderId }) => cancelOrder(userId, orderId),
      }),
    },
    toolApproval: { cancelOrder: "user-approval" },
    experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
    stopWhen: isStepCount(5),
  });
}

// Mounted at /api/chat behind your session, CSRF and rate-limit middleware.
export const chatRouter = Router().post("/", async (req, res) => {
  await pipeAgentUIStreamToResponse({
    response: res,
    agent: ordersAgent(res.locals.userId),
    uiMessages: req.body?.messages ?? [],
  });
});

During development, run the Vite dev server with a server.proxy entry that forwards /api to Express, so the session cookie stays on one origin. In production, serve the build from the same host as the API.

2. The chat component

The composable useChat posts the conversation through the transport, whose headers function fetches a CSRF token before each request. The refs it returns unwrap in the template, so messages and busy read like plain values there. When cancelOrder reaches the state approval-requested, the template shows the two buttons; once every approval has an answer, the option sendAutomaticallyWhen sends the conversation back so the backend runs or skips the tool.

src/components/OrdersAgent.vue
<script setup lang="ts">
import { useChat } from "@ai-sdk/vue";
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
import { computed, ref } from "vue";

const input = ref("");
const { messages, sendMessage, addToolApprovalResponse, status, error } = useChat({
  transport: new DefaultChatTransport({
    api: "/api/chat",
    headers: async () => {
      const res = await fetch("/api/csrf-token");
      const { token }: { token: string } = await res.json();
      return { "x-csrf-token": token };
    },
  }),
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});
const busy = computed(() => status.value === "submitted" || status.value === "streaming");

function send() {
  if (!input.value.trim() || busy.value) return;
  sendMessage({ text: input.value });
  input.value = "";
}
<\/script>

<template>
  <section aria-label="Order assistant">
    <div v-for="message in messages" :key="message.id">
      <template v-for="(part, i) in message.parts" :key="i">
        <p v-if="part.type === 'text'">{{ part.text }}</p>
        <div
          v-else-if="part.type === 'tool-cancelOrder' && part.state === 'approval-requested' && !part.approval.isAutomatic"
          role="group"
          aria-label="Approval needed"
        >
          <p>Cancel order {{ JSON.stringify(part.input) }}?</p>
          <button @click="addToolApprovalResponse({ id: part.approval.id, approved: true })">Cancel it</button>
          <button @click="addToolApprovalResponse({ id: part.approval.id, approved: false })">Keep it</button>
        </div>
      </template>
    </div>
    <p v-if="error" role="alert">Something went wrong. Try again.</p>
    <form @submit.prevent="send">
      <input v-model="input" aria-label="Message" />
      <button type="submit" :disabled="busy">Send</button>
    </form>
  </section>
</template>

If the options depend on reactive state, such as a conversation ID from the route, pass a getter to useChat; the composable recreates the chat when the getter's result changes.

Production checklist for Vue

  • Keep secrets out of the build. Vite exposes VITE_-prefixed variables to client code. The model key, the approval secret and the session secret belong in backend environment variables only.
  • Authorize on the backend. A Vue Router navigation guard decides what the user sees, not what the API allows. The endpoint checks the session and every tool scopes its queries to that user; the Express checklist covers the server side.
  • Render model output with interpolation. Mustache interpolation escapes HTML. Do not bind model output with v-html, which the Vue security guide warns against for untrusted content; if you render Markdown, use a renderer that does not pass raw HTML through.
  • Decide where the conversation lives. The state belongs to the component that called useChat. To keep a conversation across routes, call it in a component that stays mounted, such as the layout around <RouterView>.
  • Treat tool input and tool results as untrusted. The model chooses tool arguments, and data a tool returns (user-written text, emails, web pages) can contain instructions. Validate arguments with the schema and your business rules, and require approval for anything irreversible.
  • Limit cost. Cap steps, rate-limit the endpoint per user and set spending limits with your model provider.

Option: embed an in-app agent with Bunkhouse

If the agent's job is to help signed-in users do work your Vue app already supports, you may not need to build the tools, agent loop and chat UI yourself. Bunkhouse adds an Assistant pane to an existing web app with one script tag. When a user opens it, the assistant works with what the page already offers: buttons, links, form fields and tables, the API requests your app makes, a hints file you publish for operations it cannot see, and WebMCP tools your page declares.

The model writes small programs that run in a sandbox on the embed origin, not in your page. They can only call capabilities the assistant has discovered and your configuration allows. Write actions are shown to the user for review before they run, the model never sees credentials, and your Vue backend keeps enforcing its own authorization on every request.

In a Vue app built with Vite, put the loader in the head of index.html, after the identity hook and before the entry module. Do not add it from onMounted or a component: it would run after the app's first requests and again whenever the component mounts. Serve GET /api/agent-identity from your backend on the same origin; it signs a short-lived token for the user the backend session already authenticated, and the signing secret never enters the Vue build.

index.html (excerpt)
<head>
  <meta charset="utf-8" />
  <!-- The identity hook, then the loader, before the app's entry script. -->
  <script src="/agent-identity.js"><\/script>
  <script src="https://embed.bunkhouse.ai/embed.js" data-public-key="pk_YOUR_SITE_PUBLIC_KEY"><\/script>
  <script type="module" src="/src/main.ts"><\/script>
</head>

The tested Vue integration guide has the complete code for each session mode (signed identity token, server mint and anonymous), CSRF headers and Content Security Policy settings.

When Bunkhouse fits, and when to build it yourself

  • Bunkhouse fits when users want tasks done inside your existing pages, such as filtering, editing or submitting through the controls and API you already have, and you would rather not write and maintain a tool definition for each action, a sandbox for model-written code, an approval UI and usage metering.
  • Build it yourself when the agent must run without a browser page (background jobs, email, chat platforms, scheduled work), when you need to choose the model provider, use your own keys or run inference on your own infrastructure, or when the conversation must be part of your own UI components.
  • Plan for the limits: discovery cannot see server-only routes, workflows the user has not visited, closed shadow roots or cross-origin frames (describe those in a hints file); some controls need a real user gesture; password, payment and file fields are never read, and you can mark other content private.
  • Cost: production use is $49 per month, including $20 of usage credit per paid billing period, then $1 per million input tokens and $3 per million output tokens. See billing.

Try it on your Vue app first

A coding agent such as Claude Code, Codex or Cursor can run a free, disposable agent trial on a dummy page of your Vue app. No account, card or model API key is needed: your coding agent acts as the assistant's model. A trial lasts 24 hours and allows 100 turns. Give your agent this instruction, changing the address if your app runs elsewhere:

Agent trialPrompt for your coding agent
Read https://api.bunkhouse.ai/agent-trial.md and follow it to run a Bunkhouse agent trial on my dummy Vue page at http://localhost:5173.

Use a page with disposable data: everything the assistant reads is sent to your agent.

The trial evaluation checklist shows how to test a read, a rejected write and an approved change, and what a trial does not prove about production use.