npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synoped/ag-ui-vue

v0.1.2

Published

AG-UI protocol integration for Vue 3 — composables, state management, and frontend tool calling

Readme

@synoped/ag-ui-vue

Vue 3 composables for the AG-UI protocol. Connect your Vue app to any AG-UI compatible AI agent with reactive messages, streaming, frontend tool execution, and human-in-the-loop confirmation.

Install

npm install @synoped/ag-ui-vue vue

vue >= 3.5 is a peer dependency.

Quick Start

The fastest way to get a chat working:

<script setup lang="ts">
import { useChat } from "@synoped/ag-ui-vue";

const { messages, status, send } = useChat({
  url: "http://localhost:8000",
});

const input = ref("");

async function handleSend() {
  const text = input.value.trim();
  if (!text) return;
  input.value = "";
  await send(text);
}
</script>

<template>
  <div v-for="msg in messages" :key="msg.id">
    <strong>{{ msg.role }}:</strong> {{ msg.content }}
  </div>
  <input v-model="input" @keydown.enter="handleSend" />
  <button @click="handleSend" :disabled="status !== 'ready'">Send</button>
</template>

That's it. useChat handles the connection, message state, and streaming for you.


API

useChat — High-Level Chat

The main composable. Manages messages, status, tool execution, and confirmation in one call.

const {
  messages,          // ShallowRef<Message[]>
  status,            // Ref<'ready' | 'submitted' | 'streaming' | 'error'>
  error,             // ShallowRef<Error | null>
  state,             // ShallowRef<State> — shared agent state
  reasoning,         // ShallowRef<{ isActive: boolean; content: string }>
  toolCallTrackers,  // ShallowRef<Map<string, ToolCallTracker>>
  pendingToolCalls,  // ComputedRef<PendingToolCall[]>
  send,              // (content: string, opts?) => Promise<void>
  stop,              // () => void — abort the current run
  approve,           // (toolCallId: string) => void
  reject,            // (toolCallId: string, reason?: string) => void
  agent,             // the underlying AbstractAgent instance
} = useChat(options);

Options

useChat({
  url: "http://localhost:8000",       // required — your AG-UI backend
  headers: { Authorization: "..." },  // optional — custom headers
  threadId: "thread-123",             // optional — resume a conversation
  initialMessages: [],                // optional — seed messages
  initialState: {},                   // optional — seed state
  forwardedProps: { userId: "abc" },  // optional — extra data sent each run
  debug: true,                        // optional — log events
  middleware: [myMiddleware],          // optional — AG-UI client middleware
  onError: (err) => {},               // optional — error callback
  onFinish: (result) => {},           // optional — run complete callback
  onCustomEvent: (event) => {},       // optional — custom event handler
  onRawEvent: (event) => {},          // optional — raw event handler
});

Status Lifecycle

ready → submitted → streaming → ready
                  ↘ error
  • ready — idle, can send a message
  • submitted — message sent, waiting for first response
  • streaming — receiving text or tool call data
  • error — something went wrong (check error.value)

useAgent — Low-Level Agent Access

If you need full control over the agent without the chat layer:

import { useAgent } from "@synoped/ag-ui-vue";

const {
  agent,             // AbstractAgent — call agent.runAgent() yourself
  messages,          // ShallowRef<Message[]>
  state,             // ShallowRef<State>
  isRunning,         // Ref<boolean>
  reasoning,         // ShallowRef<{ isActive: boolean; content: string }>
  toolCallTrackers,  // ShallowRef<Map<string, ToolCallTracker>>
  trackerStore,      // internal tracker store
} = useAgent({ url: "http://localhost:8000" });

// Run manually
await agent.runAgent({
  tools: [{ name: "search", description: "Search the web", parameters: {} }],
  context: [],
});

useFrontendTool — Register Browser-Side Tools

Let the agent call functions that run in the browser. Registered tools are automatically included in every send() call and cleaned up when the component unmounts.

<script setup lang="ts">
import { useChat, useFrontendTool } from "@synoped/ag-ui-vue";

const chat = useChat({ url: "http://localhost:8000" });

useFrontendTool({
  tool: {
    name: "get_location",
    description: "Get the user's current city",
    parameters: {
      type: "object",
      properties: {
        format: { type: "string", enum: ["city", "full"] },
      },
    },
    handler: async (args) => {
      return JSON.stringify({ city: "Toronto", country: "Canada" });
    },
  },
  chat,
});
</script>

When the agent decides to call get_location, the handler runs locally in the browser and the result is sent back to the agent automatically. The agent then continues with the tool's output.

Tools with Confirmation

Add requireConfirmation: true to pause and ask the user before executing:

<script setup lang="ts">
import { useChat, useFrontendTool } from "@synoped/ag-ui-vue";

const chat = useChat({ url: "http://localhost:8000" });
const { pendingToolCalls, approve, reject } = chat;

useFrontendTool({
  tool: {
    name: "delete_file",
    description: "Delete a file from the user's workspace",
    parameters: {
      type: "object",
      properties: { path: { type: "string" } },
    },
    requireConfirmation: true,
    handler: async (args) => {
      // only runs after user approves
      await deleteFile(args.path as string);
      return "File deleted";
    },
  },
  chat,
});
</script>

<template>
  <!-- Confirmation UI -->
  <div v-for="tc in pendingToolCalls" :key="tc.toolCallId">
    <p>Agent wants to run <strong>{{ tc.toolName }}</strong></p>
    <pre>{{ JSON.stringify(tc.args, null, 2) }}</pre>
    <button @click="approve(tc.toolCallId)">Allow</button>
    <button @click="reject(tc.toolCallId, 'Not allowed')">Deny</button>
  </div>
</template>

useAgentContext — Send Context to the Agent

Provide extra context that gets included with every agent run. Automatically cleaned up on unmount.

<script setup lang="ts">
import { useChat, useAgentContext } from "@synoped/ag-ui-vue";

const chat = useChat({ url: "http://localhost:8000" });

useAgentContext({
  context: {
    description: "Current page info",
    value: JSON.stringify({
      route: "/dashboard",
      selectedItems: [1, 2, 3],
    }),
  },
  chat,
});
</script>

useAgentState — Typed Shared State

Read and write shared state between your app and the agent with type safety:

<script setup lang="ts">
import { useChat, useAgentState } from "@synoped/ag-ui-vue";

interface AppState {
  counter: number;
  theme: "light" | "dark";
}

const chat = useChat({
  url: "http://localhost:8000",
  initialState: { counter: 0, theme: "light" },
});

const { state, setState } = useAgentState<AppState>({
  state: chat.state,
  agent: chat.agent,
});

function increment() {
  setState({ ...state.value, counter: state.value.counter + 1 });
}
</script>

<template>
  <p>Counter: {{ state.counter }}</p>
  <button @click="increment">+1</button>
</template>

The agent can also update this state from its side. Changes flow both ways.


useToolConfirmation — Confirmation Helper

A convenience wrapper if you want to handle confirmation in a separate component:

<script setup lang="ts">
import { useToolConfirmation } from "@synoped/ag-ui-vue";

const props = defineProps<{ chat: ReturnType<typeof useChat> }>();

const { pendingToolCalls, approve, reject } = useToolConfirmation(props.chat);
</script>

<template>
  <div v-for="tc in pendingToolCalls" :key="tc.toolCallId">
    <p>{{ tc.toolName }}: {{ JSON.stringify(tc.args) }}</p>
    <button @click="approve(tc.toolCallId)">Yes</button>
    <button @click="reject(tc.toolCallId)">No</button>
  </div>
</template>

Adapters — AI Elements / Vercel AI SDK Compatibility

If you're using ai-elements-vue or components that expect Vercel AI SDK message shapes, these adapters bridge the gap:

import { useChat, toUIMessages, toChatStatus } from "@synoped/ag-ui-vue";

const chat = useChat({ url: "http://localhost:8000" });

// Convert AG-UI messages → ai-elements-vue compatible format
const uiMessages = computed(() =>
  toUIMessages(chat.messages.value, chat.toolCallTrackers.value)
);

// Convert status → ai-elements-vue compatible status
const chatStatus = computed(() => toChatStatus(chat.status.value));

toUIMessages(messages, trackers?)

Converts AG-UI Message[] into AdaptedUIMessage[] with parts and toolInvocations — the shape that ai-elements-vue components expect.

toChatStatus(status)

Maps internal status to the adapted format ('idle' becomes 'ready').

toToolUIParts(toolCalls, trackers)

Converts tool calls + tracker state into AdaptedToolUIPart[] for rendering tool invocation UI.


Plugin / Provider — App-Level Config

Two ways to share AG-UI config across your app:

Option A: Vue Plugin

// main.ts
import { createApp } from "vue";
import { createAGUI } from "@synoped/ag-ui-vue";
import App from "./App.vue";

const app = createApp(App);
app.use(createAGUI({ url: "http://localhost:8000" }));
app.mount("#app");
<!-- Any descendant component -->
<script setup lang="ts">
import { useAGUI } from "@synoped/ag-ui-vue";

const { config } = useAGUI();
// config.url === 'http://localhost:8000'
</script>

Option B: Composable Provider

<!-- Layout.vue -->
<script setup lang="ts">
import { useProvideAGUI } from "@synoped/ag-ui-vue";

const agui = useProvideAGUI({ url: "http://localhost:8000" });
</script>
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { useAGUI } from "@synoped/ag-ui-vue";

const { config } = useAGUI();
</script>

Full Example: Chat with Tools and Confirmation

<script setup lang="ts">
import { ref, computed } from "vue";
import {
  useChat,
  useFrontendTool,
  useAgentContext,
  toUIMessages,
} from "@synoped/ag-ui-vue";

const chat = useChat({ url: "http://localhost:8000" });
const { messages, status, send, pendingToolCalls, approve, reject, toolCallTrackers } = chat;

const input = ref("");

// Register a frontend tool
useFrontendTool({
  tool: {
    name: "get_weather",
    description: "Get current weather for a city",
    parameters: {
      type: "object",
      properties: { city: { type: "string" } },
      required: ["city"],
    },
    handler: async (args) => {
      const city = args.city as string;
      return JSON.stringify({ city, temp: "22°C", condition: "Sunny" });
    },
  },
  chat,
});

// Register a tool that needs user approval
useFrontendTool({
  tool: {
    name: "send_email",
    description: "Send an email",
    parameters: {
      type: "object",
      properties: {
        to: { type: "string" },
        subject: { type: "string" },
        body: { type: "string" },
      },
    },
    requireConfirmation: true,
    handler: async (args) => {
      // runs only after user approves
      return "Email sent";
    },
  },
  chat,
});

// Provide context about the current page
useAgentContext({
  context: {
    description: "User info",
    value: JSON.stringify({ name: "Alice", plan: "pro" }),
  },
  chat,
});

// For ai-elements-vue compatibility
const uiMessages = computed(() =>
  toUIMessages(messages.value, toolCallTrackers.value)
);

async function handleSend() {
  const text = input.value.trim();
  if (!text) return;
  input.value = "";
  await send(text);
}
</script>

<template>
  <div class="chat">
    <!-- Messages -->
    <div v-for="msg in messages" :key="msg.id" :class="msg.role">
      <strong>{{ msg.role }}:</strong> {{ msg.content }}
    </div>

    <!-- Pending confirmations -->
    <div v-for="tc in pendingToolCalls" :key="tc.toolCallId" class="confirmation">
      <p>Agent wants to call <strong>{{ tc.toolName }}</strong></p>
      <pre>{{ JSON.stringify(tc.args, null, 2) }}</pre>
      <button @click="approve(tc.toolCallId)">Approve</button>
      <button @click="reject(tc.toolCallId)">Reject</button>
    </div>

    <!-- Input -->
    <div class="input-row">
      <input
        v-model="input"
        @keydown.enter="handleSend"
        placeholder="Type a message..."
        :disabled="status === 'submitted' || status === 'streaming'"
      />
      <button @click="handleSend" :disabled="status !== 'ready'">
        {{ status === 'streaming' ? 'Streaming...' : 'Send' }}
      </button>
    </div>
  </div>
</template>

How Tool Execution Works

When you register a frontend tool and the agent decides to call it, this happens automatically:

  1. Agent requests tool call — the backend streams a tool call event
  2. Args stream in — tracked in toolCallTrackers with state input-streaminginput-available
  3. Confirmation check — if requireConfirmation is set, execution pauses. The tool appears in pendingToolCalls. Your UI calls approve() or reject()
  4. Handler runs — the tool's handler function executes in the browser
  5. Result sent back — a tool message is added and the agent re-runs with the result
  6. Repeat — if the agent calls more tools, the cycle continues until no more frontend tools are needed

Types

interface FrontendTool {
  name: string;
  description: string;
  parameters: Record<string, unknown>;          // JSON Schema
  handler: (args: Record<string, unknown>) => Promise<string>;
  requireConfirmation?: boolean;
}

type ToolCallState =
  | "input-streaming"     // args are being received
  | "input-available"     // args complete, ready to execute
  | "approval-requested"  // waiting for user confirmation
  | "approval-responded"  // user responded, executing
  | "output-available"    // handler returned successfully
  | "output-error"        // handler threw an error
  | "output-denied";      // user rejected the tool call

interface ToolCallTracker {
  toolCallId: string;
  toolName: string;
  args: string;           // raw JSON string
  state: ToolCallState;
  output?: string;
  error?: string;
}

interface PendingToolCall {
  toolCallId: string;
  toolName: string;
  args: Record<string, unknown>;  // parsed
  state: ToolCallState;
}

interface AGUIConfig {
  url: string;
  headers?: Record<string, string>;
  initialMessages?: Message[];
  initialState?: State;
  debug?: boolean;
  middleware?: Middleware[];
}

Example Backend (Python)

The repo includes a minimal Python backend using Google ADK + AG-UI:

# examples/backend/main.py
from google.adk.agents import Agent
from ag_ui_adk import ADKAgent, create_adk_app
from fastapi.middleware.cors import CORSMiddleware

adk_agent = Agent(
    name="assistant",
    model="gemini-2.0-flash",
    instruction="You are a helpful assistant.",
)

agent = ADKAgent(adk_agent=adk_agent, app_name="my-app", user_id="default-user")
app = create_adk_app(agent)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)
# Install deps
pip install google-adk ag-ui-adk fastapi uvicorn

# Set your API key
export GOOGLE_API_KEY=your-key-here

# Run
uvicorn main:app --host 0.0.0.0 --port 8000

Or use Docker Compose:

docker compose --profile example up -d

Development

This project uses Docker Compose for development:

# Start dev container
docker compose up -d ag-ui-vue

# Run tests
docker compose exec ag-ui-vue npx vitest run

# Run tests in watch mode
docker compose exec ag-ui-vue npx vitest

# Type check
docker compose exec ag-ui-vue npx vue-tsc --noEmit

# Build
docker compose exec ag-ui-vue npx vite build

# Lint & format
docker compose exec ag-ui-vue npx eslint src/
docker compose exec ag-ui-vue npx prettier --write src/