UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

214 lines (162 loc) 8.49 kB
--- title: "useEveAgent (Vue)" description: "Vue composable that drives an eve agent session from the browser." --- `useEveAgent()` from `eve/vue` is how a Vue app talks to an eve session. It opens a long-lived session, sends turns, and folds every stream event into reactive data you can bind in a template. Nuxt auto-imports it through the [module](./nuxt), and the [frontend overview](./overview) covers the shared model. ## Basic usage Import the composable from `eve/vue`. Its state is exposed as `ComputedRef`s, so you read it unwrapped in templates: ```vue <script setup lang="ts"> import { useEveAgent } from "eve/vue"; const { data } = useEveAgent(); </script> <template> <div v-for="message in data.messages" :key="message.id"> <p>{{ message.role }}: {{ message.parts }}</p> </div> </template> ``` ## What it returns | Property | Type | Description | | --------- | -------------------------------------------------- | ------------------------------------------------------------------------- | | `data` | `ComputedRef<TData>` | Projected state. With the default reducer, `EveMessageData` (`messages`). | | `status` | `ComputedRef<UseEveAgentStatus>` | `"ready"`, `"submitted"`, `"streaming"`, or `"error"`. | | `error` | `ComputedRef<Error \| undefined>` | Last transport-level error. | | `events` | `ComputedRef<readonly HandleMessageStreamEvent[]>` | Raw server events for this session. | | `session` | `ComputedRef<SessionState>` | Snapshot of session state. | | `send` | `(input: SendTurnPayload) => Promise<void>` | Send text or a full turn (multi-part, attachments, HITL responses). | | `stop` | `() => void` | Abort the client's in-flight stream. | | `reset` | `() => void` | Clear state and start a new session. | The first five are `ComputedRef`s; the rest are methods. Destructure whatever you need, since refs keep their reactivity through destructuring. Read them with `.value` in `<script>`, and unwrapped in `<template>`. ## Send a message ```vue <script setup lang="ts"> import { ref } from "vue"; import { useEveAgent } from "eve/vue"; const { send } = useEveAgent(); const message = ref(""); async function handleSubmit() { const text = message.value.trim(); if (!text) return; message.value = ""; await send({ message: text }); } </script> <template> <form @submit.prevent="handleSubmit"> <input v-model="message" placeholder="Type a message..." /> <button type="submit">Send</button> </form> </template> ``` For anything beyond plain text, reach for `send()`. Attachments follow the AI SDK `UserContent` format. Send file data as a base64 `data:` URL so it survives the JSON transport: ```vue <script setup lang="ts"> import { useEveAgent } from "eve/vue"; const { send } = useEveAgent(); async function onFileChange(event: Event) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; const bytes = new Uint8Array(await file.arrayBuffer()); const base64 = btoa(String.fromCodePoint(...bytes)); await send({ message: [ { type: "text", text: "Describe this image." }, { type: "file", data: `data:${file.type};base64,${base64}`, mediaType: file.type }, ], }); } </script> ``` ## Human-in-the-loop prompts A tool opts into approval with `approval` ([Tools](../../tools)). When it triggers, the pending request shows up as a `dynamic-tool` part on the latest message at `part.toolMetadata?.eve?.inputRequest`. Read it, then answer through the same session with `send({ inputResponses })`: ```ts import type { EveDynamicToolPart, EveMessagePart } from "eve/vue"; const { data, send } = useEveAgent(); const isDynamicToolPart = (part: EveMessagePart): part is EveDynamicToolPart => part.type === "dynamic-tool"; const request = data.value.messages .at(-1) ?.parts.filter(isDynamicToolPart) .map((part) => part.toolMetadata?.eve?.inputRequest) .find((value) => value !== undefined); if (request) { await send({ inputResponses: [{ requestId: request.requestId, optionId: "approve" }], }); } ``` The find-and-answer flow is the same across every framework. The [React hook reference](./overview) covers the longer walkthrough. ## Stop, reset, and resume `stop()` aborts the in-flight stream; the server-side turn keeps running and remains attachable — to stop the turn itself, POST the session's [cancel route](../../channels/eve#routes). `reset()` wipes state and starts a fresh session. To survive a reload, pass `initialSession` to restore a saved session and `onSessionChange` to persist the cursor as it moves: ```ts const agent = useEveAgent({ initialSession: savedSessionState, initialEvents: savedEvents, onSessionChange: (session) => { localStorage.setItem("eve-session", JSON.stringify(session)); }, }); ``` ## Custom host and credentials When your eve server lives somewhere other than the same origin, point at it with `host` and attach credentials through `auth` or `headers`. Pass a function and it re-resolves before each request: ```ts const agent = useEveAgent({ host: "https://agent.example.com", headers: async () => ({ authorization: `Bearer ${await getAccessToken()}`, }), }); ``` ## Attach page context per turn `clientContext` adds ephemeral context for the next model call and nothing more. Strings (or an array of strings) become user-role context messages; an object is JSON-serialized into one. It rides along with a message or HITL response, never dispatches a turn on its own, and never lands in durable session history. Pass it to `send()`: ```ts await send({ message: "What should I do on this screen?", clientContext: { route: "/billing", plan: "pro", seatsUsed: 4 }, }); ``` To attach the same context to every turn without threading it through each call site, pass `prepareSend`. It runs right before each send and returns the (possibly augmented) turn: ```ts const agent = useEveAgent({ prepareSend: (input) => ({ ...input, clientContext: { route: location.pathname }, }), }); ``` ## Lifecycle callbacks The composable takes a few per-turn callbacks: - `onEvent(event)`: fires for each eve stream event as it arrives. - `onError(error)`: fires with the last `Error` when a turn fails. - `onFinish(snapshot)`: fires with the final snapshot once a turn settles. - `onSessionChange(session)`: fires when the session cursor advances. Persist it to resume across reloads. ```ts const agent = useEveAgent({ onEvent: (event) => console.debug(event.type), onError: (error) => console.error(error.message), onFinish: (snapshot) => console.log(snapshot.status), }); ``` The `optimistic` option (default `true`) projects submitted user messages into `data` before eve confirms them with a `message.received` event. These are reducer-facing projection events only; `events` stays the authoritative eve stream. ## Custom reducer The default reducer projects events into `{ messages }` (`EveMessageData`). To shape `data` differently, pass a `reducer` implementing `EveAgentReducer<TData>`: ```ts import { useEveAgent } from "eve/vue"; import type { EveAgentReducer } from "eve/vue"; interface ToolLog { readonly toolCalls: number; } const toolCounter: EveAgentReducer<ToolLog> = { initial: () => ({ toolCalls: 0 }), reduce: (data, event) => event.type === "actions.requested" ? { toolCalls: data.toolCalls + 1 } : data, }; const agent = useEveAgent({ reducer: toolCounter }); // agent.data.value is ToolLog ``` `reduce(data, event)` receives both authoritative eve stream events and client projection events (`client.message.submitted`, `client.message.failed`, `client.input.responded`). Handle the client events too if you want optimistic and HITL state in your projection. Otherwise, return `data` unchanged for them. ## What to read next - [Nuxt](./nuxt): module setup - [Frontend overview](./overview): the shared integration model - [Sessions, runs & streaming](../../concepts/sessions-runs-and-streaming)