> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Converse with a person

> chat() is one turn and the answer, handler() is the whole agent on one route, and respond() is one turn as a stream you return yourself.

Someone is on the other end. There are three ways to answer them, and they are
the same turn:

* **`chat(message)`** hands back the answer. No route, no stream, no wiring.
* **`handler({ basePath, resolveUser })`** is the whole agent on one route, for
  a browser to talk to.
* **`respond(subject, message)`** is one turn as an AI SDK UI-message-stream
  `Response`, for when you own the route already.

## Give it hands

`api()` is your own API, read from `.vendo/tools.json`. `tool()` is anything
you would rather write yourself, with its own auth model.

```ts lib/agent.ts focus={11,23} theme={null}
import { agent, api, tool } from "@vendoai/agents";

export const support = agent({
  name: "support",
  instructions: "Answer in the product's voice; never invent account numbers.",
  tools: [
    api(),
    tool({
      name: "refund_order",
      description: "Refund an order in full.",
      risk: "destructive",
      inputSchema: {
        type: "object",
        properties: { orderId: { type: "string" } },
        required: ["orderId"],
      },
      outputSchema: {
        type: "object",
        properties: { refunded: { type: "string" } },
      },
      execute: async (input, ctx) => {
        const { orderId } = input as { orderId: string };
        await refundOrder(orderId, { owner: ctx.principal.subject });
        return { refunded: orderId };
      },
    }),
  ],
});
```

**Label the risk:** `read`, `write`, or `destructive`, and your label is final.
Leave it off and the tool is `ungraded`, which the guard asks about every time,
exactly as it does for `destructive`.

**Scope it to the caller:** the `orderId` came from the model and
`ctx.principal` is who the turn acts as, so bind the two yourself. An unscoped
lookup acts on someone else's order however the approval card was answered.

## One turn, in a line

`chat()` runs the turn and gives you what it answered.

```ts theme={null}
const turn = await support.chat("Where is order A-1001?", { as: user.id });

if (turn.status === "ok") console.log(turn.text);
```

Awaiting it gives the `TurnResult` — what it said, on which thread, with which
tool calls. Skip the `await` and you hold the turn itself instead: `threadId`
and `turnId` are readable straight away, and `events` is the live feed while
it runs. The turn happens either way.

## One route, the whole agent

`handler()` serves the chat turn, the thread list and transcript, and the
approvals wire off one mount. It is a fetch handler, so it goes on any server
that can hand it a `Request`.

```ts app/api/agent/[[...path]]/route.ts focus={4-10} theme={null}
import { support } from "@/lib/agent";
import { auth } from "@/lib/auth";

const handle = support.handler({
  basePath: "/api/agent",
  resolveUser: async (request) => {
    const user = await auth(request);
    return user === null ? null : { subject: user.id, profile: { plan: user.plan } };
  },
});

export { handle as GET, handle as POST, handle as DELETE };
```

`resolveUser` is where identity is configured, once. Return `null` and the
mount answers 401; the `subject` you return is what every thread, grant and
audit row on that request is scoped to, and a thread id belonging to somebody
else reads back as absent.

| Option        | Type                                        | What it does                                                                                                                 |
| ------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `basePath`    | `string`                                    | Where you mounted it                                                                                                         |
| `resolveUser` | `(request) => Promise<HandlerUser \| null>` | Your session, read per request                                                                                               |
| `headers`     | `Record<string, string>` or `false`         | What the turn's tools forward as the caller's authority. Unset forwards this request's own headers; `false` forwards nothing |

In the browser, `useVendoChat` from `@vendoai/ui` talks to that mount and keeps
nothing of its own — the transcript, pending approvals included, is read back
from the server, so a reload loses nothing.

```tsx app/support/page.tsx focus={2-5} theme={null}
const { messages, sendMessage, interruptions, resume, status, stop } =
  useVendoChat({
    api: "/api/agent",
    onThreadId: (id) => router.replace(`/support/${id}`),
  });
```

## Your own route

```ts app/api/support/route.ts focus={13-16} theme={null}
import { support } from "@/lib/agent";
import { auth } from "@/lib/auth";

export async function POST(req: Request) {
  const user = await auth(req);
  if (user === null) return new Response("unauthorized", { status: 401 });

  const { message, threadId } = (await req.json()) as {
    message: string;
    threadId?: string;
  };

  return support.respond(user.id, message, {
    ...(threadId === undefined ? {} : { threadId }),
    headers: req.headers,
    user: { id: user.id, plan: user.plan },
  });
}
```

Three things are doing work there.

* **`user.id` is the subject.** Every thread, every workspace file, and every
  audit row is scoped to it, and Vendo mints no identity of its own.
* **`headers: req.headers`** forwards the caller's own credentials, so a tool
  call reaches your API as the person signed in rather than as the agent.
* **`user`** is server-trusted identity the model may read, printed as `[User]`
  in the prompt. It is facts, not instructions.

| Option     | Type                                  | What it does                                     |
| ---------- | ------------------------------------- | ------------------------------------------------ |
| `threadId` | `string`                              | Reopens a conversation this subject already owns |
| `headers`  | `Headers` or `Record<string, string>` | Forwards the caller's credentials to your API    |
| `user`     | `Record<string, Json>`                | Server-trusted facts the model may read          |
| `context`  | `Record<string, unknown>`             | Data your guard rules and tools read             |
| `signal`   | `AbortSignal`                         | Cancels this turn                                |

## Continue the conversation

The response carries the thread id on `x-vendo-thread-id`. Keep it and hand it
back on the next turn.

<div style={{ display: "flex", flexWrap: "wrap", alignItems: "stretch", gap: 10, margin: "1.25rem 0" }}>
  <div style={{ flex: "1 1 180px", border: "1px solid #e9e6f1", borderRadius: 10, padding: "11px 13px", background: "#fdfcff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#a8a5b4", marginBottom: 5 }}>Turn 1</div>
    <div style={{ fontSize: 13, color: "#4b4857" }}>No thread id in the body. Vendo mints one.</div>
  </div>

  <div style={{ flex: "1 1 180px", border: "1px solid #ddd0ff", borderRadius: 10, padding: "11px 13px", background: "#f5f1ff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#6c3bff", marginBottom: 5 }}>Response</div>
    <div style={{ fontFamily: "ui-monospace, monospace", fontSize: 12, color: "#4a22bd" }}>x-vendo-thread-id: thr\_…</div>
  </div>

  <div style={{ flex: "1 1 180px", border: "1px solid #e9e6f1", borderRadius: 10, padding: "11px 13px", background: "#fdfcff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#a8a5b4", marginBottom: 5 }}>Turn 2</div>
    <div style={{ fontSize: 13, color: "#4b4857" }}>Send that id back. Same conversation.</div>
  </div>
</div>

```ts app/support-client.ts focus={9} theme={null}
import { THREAD_ID_HEADER } from "@vendoai/agents";

let threadId: string | null = null;

const response = await fetch("/api/support", {
  method: "POST",
  body: JSON.stringify({ message, threadId }),
});
threadId = response.headers.get(THREAD_ID_HEADER);
```

A `threadId` is checked against the same subject that owns it. Someone else's
thread, or one that never existed, comes back `not-found` rather than as a
silent new conversation.

## Approvals

A turn can stop for a human. A `destructive` or `ungraded` call raises an
approval, and what happens next depends on which verb is driving.

<Frame>
  <img src="https://mintcdn.com/vendo/Bl9khJxYuQX2mLio/images/maple/panel-approval.png?fit=max&auto=format&n=Bl9khJxYuQX2mLio&q=85&s=e62769e00e046e724dd35382d560f5a9" alt="A Maple turn paused mid-stream on an approval card for a $200 transfer, with Approve and Deny buttons" width="652" height="712" data-path="images/maple/panel-approval.png" />
</Frame>

**`chat()` and `run()` end the turn.** The result comes back `interrupted`
with the cards on it, in the time the turn actually took. Answer them and the
turn carries on from exactly where it parked — a denied call is a refusal the
model reads, never a rerun.

```ts focus={5-10} theme={null}
import type { Decisions } from "@vendoai/agents";

const turn = await support.chat(message, { as: user.id });

if (turn.status === "interrupted") {
  const decisions: Decisions = {};
  for (const ask of turn.interruptions) decisions[ask.id] = await askSomeone(ask);

  const done = await turn.resume(decisions);
}
```

Behind `handler()` this is already wired: `useVendoChat` surfaces the same
cards as `interruptions` and its `resume(decisions)` posts them to the mount's
approvals wire. The `interruptions` survive a reload, because they are read
back from the server rather than kept in the browser.

**`respond()` and `session()` hold the stream open.** The card goes down the
stream and the turn waits 90 seconds for a decision. Approve and the same call
runs with the same arguments; deny, or let the wait expire, and the model is
told so and carries on.

No card to render? `session()` is `respond()` with the object kept, and the
same decision arrives as an event your backend answers itself.

```ts app/api/support/route.ts focus={2-4} theme={null}
const session = await support.session(user.id, { headers: req.headers });
session.on("approval", async ({ request, approve, deny }) => {
  await (request.call.tool === "refund_order" ? deny() : approve());
});
return session.stream(message);
```

`request` carries the call, its arguments, the tool's descriptor, and the
principal the turn acts as. The wait is the same 90 seconds, so decide inside
it. Full shape: [Server API](/reference/server-api).

## Answer it tomorrow

`turn.resume()` is a closure, so it dies with the process. The ask does not: a
server restarts, the turn ran on a queue worker, the person answers on Monday.
`forUser(subject).turns` addresses a parked turn by **id** instead, from
anywhere over the same store.

```ts focus={2,4} theme={null}
const user = support.forUser(subject);
const waiting = await user.turns.list({ status: "interrupted" });

const result = await user.turns.resume(waiting[0]!.turnId, {
  [waiting[0]!.interruptions[0]!.id]: "approve",
});
```

`turns.resume` hands back the **result**, not a turn: it reads the store before
it can start anything, and a `Turn` is itself awaitable, so there would be no
handle left to hold. The answer is prose either way — an `output` schema
belonged to the code that called `run({ output })` and was never persisted, so
resume through the result you are holding to keep the shape.

Every interruption the turn parked needs a decision in the same call; a partial
map is refused, naming the ids it is missing. A parked turn waits **seven
days**, and a resume after that fails saying the ask expired rather than acting
on a week-old yes.

<CardGroup cols={2}>
  <Card title="Run" href="/backend/run">
    The same agent, unattended, through the other verb.
  </Card>

  <Card title="API tools" href="/capabilities/api-tools">
    What `vendo init` extracted, and how to change a grade or a name.
  </Card>
</CardGroup>
