> ## 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 reference: agent, respond, run, session

> The full @vendoai/agents surface: agent() config, VendoAgent.respond and VendoAgent.run, RunOptions, AgentRun, RunEvent, AgentReport, and the exported constants.

Everything on this page is exported from `@vendoai/agents`.

```ts theme={null}
import { agent, api, e2b, postgres, tool, THREAD_ID_HEADER } from "@vendoai/agents";
import type {
  AgentConfig, AgentReport, AgentRun, AgentSession, ApiOptions, ApprovalEvent,
  HostTool, McpServerConfig, RespondOptions, RunEvent, RunOptions, SessionOptions,
  ToolConfig, ToolSource, UsageTotals, VendoAgent,
} from "@vendoai/agents";
```

## `agent(config)`

```ts theme={null}
export function agent(config: AgentConfig): VendoAgent;
```

| Key            | Type                         | Notes                                                                                                                         |
| -------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `name`         | `string`                     | **required.** Attributes audit rows and the inbox                                                                             |
| `harness`      | `Harness`                    | who thinks. Unset → `vendo()`, the in-process loop; also `claudeCode()`, or your own via `defineHarness`                      |
| `model`        | `LanguageModel`              | the `default` seat `vendo()` thinks with. `claudeCode()` brings its own brain and ignores it                                  |
| `tools`        | `readonly ToolSource[]`      | `api()`, `tool()`, or any `ToolRegistry`. A name collision is a boot error, never a silent shadow                             |
| `mcp`          | `readonly McpServerConfig[]` | external MCP servers as tool sources                                                                                          |
| `guard`        | `VendoGuard \| GuardRules`   | a built guard wins verbatim; rules are completed with this composition's store. Unset → `createGuard({ store })`              |
| `skills`       | `readonly string[]`          | skill folders, boot-loaded and mounted read-only at `/host/skills`                                                            |
| `egress`       | `EgressConfig`               | outbound allowlist for the box; unset = the harness's minimum                                                                 |
| `store`        | `VendoStore`                 | unset alone → embedded. There is no Cloud store rung today, so unset + `VENDO_API_KEY` is a boot error naming `postgres(url)` |
| `sandbox`      | `SandboxAdapter`             | `e2b()`, or unset + `VENDO_API_KEY` → the Cloud pool. Required by a harness declaring `requires.sandbox`                      |
| `door`         | `DoorConfig`                 | where a thinker running outside this process dials back for tools; unset → `VENDO_BASE_URL`                                   |
| `instructions` | `string`                     | the host's prompt block                                                                                                       |
| `system`       | `SystemPromptHook`           | the last word on the per-turn system prompt; `undefined` means the default assembly                                           |

The smallest useful composition is two keys:

```ts theme={null}
const support = agent({ name: "support", tools: [api()] });
```

### Credentials

One ladder, everywhere: **what you pass always wins, and `VENDO_API_KEY` fills
only the slots you left unset.** There are no key-conditional branches and no
capability checks — a key problem surfaces on the first real service call.

## `VendoAgent`

```ts theme={null}
export interface VendoAgent {
  readonly name: string;
  respond(subject: string, message: string | UIMessage, options?: RespondOptions): Promise<Response>;
  run<T>(task: string, options?: RunOptions<T>): AgentRun<T>;
  session(subject: string, options?: SessionOptions): Promise<AgentSession>;
  readonly door?: (request: Request) => Promise<Response>;
}
```

`door` is present exactly when the harness thinks outside this process
(`requires.toolDoor`). Mount it at `DOOR_PATH` (`/api/vendo/mcp`).

## `respond`

One turn for a waiting person. Returns an AI-SDK UI-message-stream `Response`
with `x-vendo-thread-id` set.

```ts theme={null}
export interface RespondOptions {
  threadId?: string;
  headers?: Record<string, string> | Headers;
  user?: Record<string, Json>;
  context?: Record<string, unknown>;
  signal?: AbortSignal;
}
```

| Key        | Notes                                                                                                                                         |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `threadId` | reopen this conversation. Ownership-checked against the same subject; a foreign or unknown id is `not-found`, never a silent new conversation |
| `headers`  | the request's own headers, forwarded so tool calls reach your API as the signed-in person                                                     |
| `user`     | server-trusted identity facts, model-visible as `[User]`                                                                                      |
| `context`  | guard and tool context: functions run at check-time, data survives parking                                                                    |
| `signal`   | abort the turn                                                                                                                                |

It is `session(subject, options)` plus `stream(message)` collapsed into one
call, with the header stamped. `session()` is unchanged and still available for
a host that wants the session object — and `session.stream()` now stamps the
same header.

## `run`

One non-interactive run.

```ts theme={null}
export interface RunOptions<T> {
  as?: string;
  user?: Record<string, Json>;
  context?: Record<string, unknown>;
  output?: FlexibleSchema<T>;
  maxToolCalls?: number;
  signal?: AbortSignal;
  threadId?: string;
}
```

| Key            | Notes                                                                                                                   |
| -------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `as`           | the subject this run acts as. Give unattended work its own service subject                                              |
| `user`         | server-trusted identity facts, model-visible                                                                            |
| `context`      | guard and tool context                                                                                                  |
| `output`       | an AI-SDK `FlexibleSchema` (a zod schema, typically). The result is **validated**; `T` is inferred. Unset costs nothing |
| `maxToolCalls` | bounded at the guard — spent, further calls are refused before the registry. Default **20**                             |
| `signal`       | the only way to cancel. There is no `cancel()` method                                                                   |
| `threadId`     | continue an existing thread. Ownership-checked against the same subject; otherwise `not-found`                          |

### `AgentRun`

```ts theme={null}
export interface AgentRun<T> extends PromiseLike<AgentReport<T>> {
  readonly threadId: string;
  readonly events: AsyncIterable<RunEvent>;
}
```

`threadId` is readable immediately, before the first event. Awaiting the run
gives the report; iterating `events` does not consume it.

### `RunEvent`

| `type`        | What it marks                                   |
| ------------- | ----------------------------------------------- |
| `text`        | the model's own words, streaming                |
| `status`      | where the run is                                |
| `tool-call`   | a tool the run is about to use                  |
| `tool-result` | what that tool answered                         |
| `error`       | the run failed, with the harness's own sentence |

### `AgentReport`

```ts theme={null}
export interface AgentReport<T> {
  status: "ok" | "error" | "stopped";
  summary: string;
  toolCalls: readonly { call: ToolCall; outcome: ToolOutcome["status"] }[];
  refs: { threadId: string; approvals: readonly string[] };
  output?: T;
  usage: UsageTotals;
}
```

* **`status`** — `"stopped"` covers an aborted run and one that spent its
  `maxToolCalls` budget. Both still return a full report.
* **`summary`** — the model's own closing account, never a sentence of Vendo's.
* **`refs.approvals`** — the ids of approvals this run parked. See below.
* **`usage`** — `{ inputTokens, outputTokens, cacheReadTokens?, cacheWriteTokens?, model? }`.
  This is what you meter on.

### Approvals inside `run`

An `ask` inside a non-interactive `run()` **parks the approval durably**, and
the tool call **returns denied** with the approval's id. The run continues with
that answer and finishes normally.

**There is no resume.** `report.refs.approvals` is the list of ids to put in
front of a human; once someone has decided, re-entry is a **fresh `run()`**.

The interactive lane is unchanged: a turn driven by `respond()` waits about 90
seconds for the person to decide, and an approved call re-dispatches
byte-for-byte.

### Follow-ups

The follow-up to a `run()` is the **same verb** plus the thread id:

```ts theme={null}
const next = await support.run("Now shorten it to five bullets.", {
  as: "svc:nightly-digest",
  threadId: first.refs.threadId,
});
```

Never `respond()` — that lane opens a turn that will wait on a human who is not
there.

## `session`

Unchanged. `session(subject, options)` returns an `AgentSession` with a
`threadId`, a `stream(message, options)` that returns a UI-message-stream
`Response` (now carrying `x-vendo-thread-id` too), and
`on("approval", handler)` for present-user decisions. `SessionOptions` is
`{ user?, context?, headers?, threadId? }`.

## Tools

```ts theme={null}
export function api(options?: ApiOptions): ToolRegistry;
export function tool(config: ToolConfig): HostTool;
```

`api()` serves `.vendo/tools.json` with layered overrides, present-header
forwarding behind the origin gate, and `actAs` for away runs
(`{ dir?, actAs?, baseUrl?, untrustedOriginPolicy?, fetch? }`).

`tool()` takes `{ name, description?, risk?, inputSchema, execute }`.
**`risk` is your call and it is final** — `read`, `write`, or `destructive`.
Omit it and the tool is `ungraded`, which the guard treats like `destructive`
and asks about, which unattended means denied.

## Adapters

```ts theme={null}
export function postgres(url: string, options?: PostgresOptions): VendoStore;
export function e2b(options?: E2bOptions): SandboxAdapter;
```

`postgres(url, { blobs?, encryption?, allowUnencryptedSecrets? })` — the blob
adapter rides beside the store it was configured with.
`e2b({ apiKey?, template?, timeoutMs? })` reads `E2B_API_KEY` as its
credential; the harness's own template always wins over the adapter's.

## Constants

| Export             | Value                                                                            |
| ------------------ | -------------------------------------------------------------------------------- |
| `THREAD_ID_HEADER` | `"x-vendo-thread-id"` — set on every `respond()` and `session.stream()` response |
| `DOOR_PATH`        | `"/api/vendo/mcp"` — where to mount `agent.door`                                 |

<CardGroup cols={2}>
  <Card title="Tools and safety" href="/concepts/tools-and-safety">
    Risk grading, `guard.bind`, approvals, and away-run scopes.
  </Card>

  <Card title="How Vendo works" href="/concepts/architecture">
    Where `@vendoai/agents` sits among the blocks.
  </Card>
</CardGroup>
