> ## 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.

# Agent SDK: a governed agent in any Node backend

> Compose a guarded agent with agent() from @vendoai/agents, answer a user in one route with respond(), and drive unattended work with run() — no umbrella, no client, no scheduler.

`@vendoai/agents` is Vendo's agent runtime on its own: your tools, one guard,
one store, and a brain — with none of the umbrella's client surface. You
compose it once and reach it through exactly two verbs.

| Verb                        | For                         | Shape                                        |
| --------------------------- | --------------------------- | -------------------------------------------- |
| `respond(subject, message)` | a person waiting on a reply | one turn, streamed back as a `Response`      |
| `run(task, options)`        | work nobody is watching     | one non-interactive run, awaited or streamed |

Everything else — approvals, risk grading, audit, threads — is the same guard
the rest of Vendo runs through. There is no second authority path.

## Install

```bash theme={null}
npm install @vendoai/agents ai
npx vendo init
```

`vendo init` writes `.vendo/tools.json` — your API read into tool definitions,
each with a schema, a risk grade, and its dispatch binding — which is what
`api()` serves. Full inventory: [vendo init](/reference/vendo-init).

## Compose it

Seven lines, and only `name` is required:

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

export const support = agent({
  name: "support",
  instructions: "Answer in the product's voice; never invent account numbers.",
  tools: [api()],
});
```

`harness` is optional and defaults to `vendo()`, the in-process loop. Pass
`claudeCode()` instead when you want Claude Code thinking on a sandbox machine
— see [Reference](/agent-sdk/reference#agent-config).

## Answer a person

`respond()` is one turn: an AI-SDK UI-message-stream `Response`, ready to
return from a route as-is.

```ts theme={null}
// app/api/support/route.ts
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 } = (await req.json()) as { message: string };
  return support.respond(user.id, message, { headers: req.headers });
}
```

The response carries `x-vendo-thread-id`. Send it back as `threadId` on the
next turn and the conversation continues — see
[Converse](/agent-sdk/converse).

## Two credential paths

Pick one. They are the same ladder everywhere in Vendo: **what you pass always
wins, and `VENDO_API_KEY` fills only the slots you left unset.**

<h3 id="vendo-cloud">
  Vendo Cloud — `vendo login`
</h3>

```bash theme={null}
npx vendo login
```

Mints a `VENDO_API_KEY` into `.env.local` — never printed. It fills the
model seat (through the Cloud gateway) and the sandbox. The store is not
one of them: `@vendoai/agents` has no Cloud store rung today, so with a key set
you pass one — `store: postgres(url)` — or `agent()` refuses to boot.

<h3 id="bring-your-own">
  No Vendo key — bring your own
</h3>

```ts theme={null}
import { anthropic } from "@ai-sdk/anthropic";
import { agent, api, postgres } from "@vendoai/agents";

export const support = agent({
  name: "support",
  model: anthropic("claude-sonnet-4-6"),
  store: postgres(process.env.DATABASE_URL!),
  tools: [api()],
});
```

Your own model key and your own store — no Vendo key anywhere. Drop
`store` and you get the embedded one instead. `ANTHROPIC_API_KEY` is what
the provider *you* construct authenticates with. It selects nothing on its
own.

`model` is the `default` seat the built-in `vendo()` harness thinks with.
`claudeCode()` brings its own brain and ignores it.

## Testing

Swap the brain for a scripted one. `defineHarness` is the same authoring seam
a real harness uses, so nothing else in the composition changes — the guard,
the tools, and the store are all the ones that ship.

```ts theme={null}
// support.test.ts
import { agent } from "@vendoai/agents";
import { defineHarness } from "@vendoai/harnesses";
import { expect, it } from "vitest";

const scripted = defineHarness({
  name: "scripted",
  async *run() {
    yield { type: "text" as const, delta: "Two invoices are outstanding." };
  },
});

it("reports what the brain said", async () => {
  const support = agent({ name: "support", harness: scripted, tools: [] });

  const report = await support.run("Check the invoices.", { as: "user:test" });

  expect(report.status).toBe("ok");
  expect(report.summary).toBe("Two invoices are outstanding.");
});
```

<Note>
  `defineHarness` lives in `@vendoai/harnesses`. Add it as a devDependency —
  under strict package linking, importing from a package you have not declared
  is a `TS2307`.
</Note>

## The three jobs

<CardGroup cols={2}>
  <Card title="Converse with a user" href="/agent-sdk/converse">
    `respond()` behind a chat route, with your API as tools and a risk-labeled
    write of your own.
  </Card>

  <Card title="Call it like a function" href="/agent-sdk/call">
    `run()` with an `output` schema returns typed data, and `usage` is what you
    meter on.
  </Card>

  <Card title="Ship it as a product surface" href="/agent-sdk/product-run">
    Forward `run.events` to your own UI over SSE and end on the report.
  </Card>

  <Card title="Reference" href="/agent-sdk/reference">
    Every option, every field, every exported name.
  </Card>

  <Card title="Tools and safety" href="/concepts/tools-and-safety">
    Risk labels, approvals, grants, and the audit trail behind both verbs.
  </Card>
</CardGroup>
