vendo_* tool answers your loop with either plain data or a small versioned ref. One component turns that ref into the right surface, so your loop never learns what an app or an approval is.
The dispatch
The envelope
status on an app ref is always "building", win or lose. A build that fails terminally is never wrapped in one, so the ref never means done.
Readers tolerate unknown extra fields. Anything additive stays inside @1, and a breaking change bumps the kind.
The three embeds
Import all three from@vendoai/vendo/react, which re-exports them so you need no second package. Each finds the wire on its own and polls until it reaches a terminal state.
None of the three takes config props beyond the one shown. Wire, auth and theme are settings they read from around them, and a failure speaks the failed or expired vocabulary rather than a silent blank.
Default to
<VendoToolResult> wherever you render a finished tool part. Reach for the other two only when you hold a ref that did not come from a live tool call, such as one you stored and want to re-render later.
No setup
Drop an embed in and it works. With no provider around it, it reads the wire at/api/vendo, sends your host session cookie the way the browser already would, and paints on the --vendo-* tokens. Every embed on the page shares one client, so ten of them are still one connection.
app/page.tsx
The provider, when you need it
VendoProvider is settings for everything inside it, and it wins over the defaults. Wrap your chat once when any of them is wrong for you: a wire mounted under a base path, a client of your own, brand tokens, your host components.
app/page.tsx
Approvals never block the loop
A guarded call that needs approval does not throw and does not stall your turn. The tool returns an approval ref right away, your model reads “pending, the person has to approve this”, and the call itself parks server-side. When the person approves in the card, the wire runs the parked call and the card resolves in place. Deny throws it away. A call parked at the MCP door is the one exception: approving there grants the call, and the outside agent’s own retry of it is what runs it. The card waits through that gap on its working beat, then settles on “Approved — ran” once the retry spends the approval.
createVendo({ guard: guard({ approvals: { parkedCallTtlMs } }) }), where 0 means never.
Dispatching it yourself
parseVendoToolEnvelope(output) returns the typed envelope or null, which is exactly what <VendoToolResult> calls. Use it server-side, or anywhere you want the branch in your own code.
@vendoai/core. Add that package to your dependencies to import it.
A malformed envelope returns null too. The tool pack is the only writer, so a bad shape is a bug there rather than something your chat should half-render.