Skip to main content
A governed agent in any Node backend, from an empty project. One package, no CLI, no init, no files moved around. The UI stays yours.
1

Compose the agent

Install one package and write one agent() call at module scope. Only name is required.
lib/agent.ts
description is required, and it is the only thing the model reads when it decides whether to call the tool. risk is read, write, or destructive, and your label is final. Grade it: leave risk off and the tool is ungraded, which the guard asks a person about every time — so the very first chat() comes back interrupted with nothing run.Nothing else is configured. Left unset, the agent thinks in this process, and threads and audit rows are persisted automatically, with zero setup.

tools

tool() for your own code, api() for your existing HTTP API.

guard

Risk grade, approval, and an audit row on every call.

store

Threads and audit rows. Embedded, your Postgres, or Cloud.

harness

vendo() by default, claudeCode() on request.
Already have an HTTP API? vendo init reads it into tool definitions and tools: [api()] hands the whole thing to the agent, graded, in one line. It is an addition to this page, not a prerequisite for it — see API tools.
2

Ask it something

chat() is one turn and the answer, with no route and no stream in the way.
That is the whole hello world. The agent needs a model to think with, and the shortest way to give it one is npx vendoai@latest login, which mints a VENDO_API_KEY into .env.local and never prints it..env.local is a Next.js convention, and nothing in @vendoai/agents reads it. A plain Node backend — which this page is — has to be handed the variable:
To bring your own model instead, install @ai-sdk/anthropic — the major that pairs with your ai, 4 with ai@7 and 3 with ai@6 — and pass it beside name. That is two lines:
lib/agent.ts
Either way the credential is read at the first turn, never at build time — see Model credentials.turn is a TurnResult: read status once and everything you then touch is there. ok carries the typed output; interrupted carries the interruptions a person has to answer and a resume() that carries on from where the turn parked. Converse covers both.
3

Put it on HTTP

handler() is the whole agent as one fetch handler — the chat turn, the thread list and transcript, and the approvals wire. Mount it on one catch-all route.
app/api/agent/[[...path]]/route.ts
In the browser, useVendoChat from @vendoai/ui speaks to that mount and keeps nothing of its own:
app/support/page.tsx
Render messages however your app renders anything else, and interruptions as approve/deny cards that call resume.
Prefer to own the route yourself? respond() is one turn as a streamed Response you return unchanged, and it is not going anywhere. Converse has both.
4

Run unattended work

run() is work nobody is watching. The same object is awaitable and iterable, so run.events is the live feed and awaiting it is the result.
app/api/reports/route.ts
run.threadId is there before the first event, so the header goes out with the response. signal: req.signal ties the run to the connection, and closing the tab stops it.
Away, the menu is already narrower: destructive and ungraded tools are never offered, and a read or write call still needs authority a person captured while they were present — a grant. Without one the call parks, and the run comes back interrupted carrying the cards for someone to answer. Run covers what an unattended run may touch.

One run, seen from your own UI

Events leave your server on the left. Your own progress screen fills on the right.
POST /api/reports · text/event-stream
status started
tool-call listInvoices
tool-result 42 rows
tool-call getPayments
tool-result 38 rows
text Three invoices are overdue.
resultok · 4 tool calls
app.maple.com/reports
Monthly closeDone
August 20264 steps
Pulled invoices 42
Matched payments 38
Drafted the summary 1
Overdue3 invoices
$12,480

your chrome, not Vendo’s


Where to go next

Every verb in depth, and the surface you build around them.

Converse

Threads, forwarded credentials, and approvals with a person present.support.chat(message)

Run

Typed output from a schema, plus the usage you meter on.await support.run(task)

Your own surface

Forward the event feed over SSE and end on the result.for await (const e of run.events)
The whole of this page as a project you can run is examples/standalone-agent: one agent(), one tool(), chat() in the terminal, and handler() on a Node server.