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.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:@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
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
useVendoChat from @vendoai/ui speaks to that mount and
keeps nothing of its own:app/support/page.tsx
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.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.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)examples/standalone-agent:
one agent(), one tool(), chat() in the terminal, and handler() on a
Node server.