/** * The front door. Assemble, validate, return — a Vendo Cloud key fills every * slot left unset (adapter rule, no second code paths), an explicit adapter * always wins, and every failure is a boot error with a way out. */ import { type SandboxAdapter } from "../apps/index.js"; import { type FilesAdapter, type Harness, type SeatModels, type Skill, type ToolRegistry, type When } from "../core/index.js"; import { type GuardRules, type VendoGuard } from "../guard/index.js"; import { type VendoStore } from "../store/index.js"; import type { LanguageModel, UIMessage } from "ai"; import { type OnOptions } from "./automations.js"; import { type RunOptions } from "./away.js"; import { type ChatOptions, type Turn } from "./turn.js"; import { type DoorConfig } from "./door.js"; import { type HandlerOptions } from "./handler.js"; import { type EgressConfig } from "./egress.js"; import { type AgentUser, type UserOptions } from "./facade.js"; import { type MemoryAdapter } from "./memory.js"; import { type AgentPrincipal } from "./permissions.js"; import type { SystemPromptHook } from "./prompt.js"; import { type AgentSession, type RespondOptions, type SessionOptions } from "./session.js"; import { type McpServerConfig, type ToolSource } from "./tools.js"; export interface AgentConfig { /** Audit and inbox attribution. */ name: string; /** The brain; its knobs (effort, machine, template) bind at construction. * Unset → `vendo()`, the in-process default. */ harness?: Harness; /** What `vendo()` thinks with (the `default` seat). A harness that brings its * own brain — `claudeCode()` — ignores it. */ model?: LanguageModel; tools?: readonly ToolSource[]; mcp?: readonly McpServerConfig[]; /** A built guard, or the rules for one — `guard({ policy, judge, approvals })`, * which this composition completes with its own store. An instance always * wins verbatim; unset → default `createGuard({ store })`. */ guard?: VendoGuard | GuardRules; /** Skill folders, boot-loaded; deploy = update the folder. */ skills?: readonly string[]; /** Agent-level outbound allowlist; unset = the harness's minimum. */ egress?: EgressConfig; /** Unset + `VENDO_API_KEY` → Cloud tenant Postgres; unset alone → embedded. */ store?: VendoStore; /** Unset + key → the ladder (E2B key, Cloud pool). */ sandbox?: SandboxAdapter; /** * What this agent remembers about each of its users, per user and never * across them. `true` → the store-backed default, on this composition's own * store; a {@link MemoryAdapter} is used verbatim (BYO). Unset is no memory at * all: no `[Memory]` block in any prompt, and no `remember` tool for the model * to call. * * Reads are automatic (a capped `[Memory]` block, read as the user's words, * never as instruction); writes are the visible, audited, guard-checked * `remember` tool, scoped to the turn's own principal. */ memory?: MemoryAdapter | true; /** Where a thinker that runs outside this process dials back to reach your * tools; unset → `VENDO_BASE_URL`. Required by any harness that declares * `requires.toolDoor` — see {@link resolveDoor}. */ door?: DoorConfig; /** Who is asking, for {@link VendoAgent.permissions}. Unset → those routes * 401: a person's own asks and grants need a person. */ principal?: AgentPrincipal; /** The host's prompt block. */ instructions?: string; /** * The last word on the per-turn system prompt. Called once per turn with the * ctx and this package's own assembly; a returned string is used VERBATIM * (even `""`), `undefined` means the default assembly. * * ONE hook, both venues — `ctx.venue` says which — so a chat turn and an away * firing cannot drift into two agents wearing one name. `undefined` meaning * "the default" is what keeps a conditional that falls through from silently * stripping the rules; replacing wholesale hands the base rules and the * forgery-safe `[User]`/`[Context]` blocks to the host, to keep or to drop. */ system?: SystemPromptHook; } export interface VendoAgent { readonly name: string; /** * ONE turn of a conversation — the answer, not a stream. Bare, the agent talks * as itself; `as` names the user it is acting for. Venue "chat", presence * "present", so it sees the whole present-user tool surface — and a call the * guard wants a person for ENDS the turn as `interrupted` rather than blocking * on it, with `resume()` to carry on once they answer. */ chat(message: string, options?: ChatOptions): Turn; /** * Everything this agent does FOR ONE PERSON, with who they are bound once — * their turns, their conversations, and what it remembers about them. * * const user = support.forUser("u_42", { * profile: { name: "Dana", plan: "pro" }, * context: { tenantId }, * }); * await user.chat("where is my refund?", { headers: request.headers }); * * `profile` and `context` are FACTS about the person and are bound here. * `headers` are the authority of ONE request, so they ride per call and are * never kept — see facade.ts. */ forUser(subject: string, options?: UserOptions): AgentUser; /** * `respond` answers a person over HTTP: one turn, an AI-SDK UI-message-stream * `Response` to return from your route, with the conversation's id on * `x-vendo-thread-id`. * * It is exactly `session(subject, options)` followed by `stream(message)` — * reach for `session()` when you want the object (approval events, several * turns on one thread), and this when you want the Response. */ respond(subject: string, message: string | UIMessage, options?: RespondOptions): Promise; /** One unattended run: no screen, the same {@link Turn} `chat` answers with. * Venue "automation", presence "away" — so every ask parks and a run that * needed consent answers `interrupted`, carrying the cards to answer. */ run(task: string, options?: RunOptions): Turn; /** * Declare an automation this agent runs unattended. A bare string is a cron * expression; the other four shapes are `{ every }`, `{ at }`, `{ event }` and * `{ webhook }`. * * support.on("0 9 * * 1", "summarize the week and email ops"); * support.on({ every: "1d" }, "refresh credit scores"); * support.on({ event: "payment.failed" }, "triage and notify the user"); * support.on("0 2 * * *", "rebuild the digest", { id: "nightly-digest" }); * * Returns void because it is a DECLARATION: it is validated here and now — a * bad cron throws at module load, with what, why, a did-you-mean and the docs * — and reconciled against the store by a lifecycle, `serve({ agents })` or * `createVendo`'s boot. INERT until one of them runs. The code is the consent, * so deleting the call disarms the automation on the next deploy; `disable()` * by a person outlives every redeploy. */ on(when: When, task: string, options?: OnOptions): void; /** @deprecated A session is request-lifetime and the THREAD is what outlives * it, so the durable noun is the one to hold: `agent.forUser(subject)` for * the turns, `user.threads` for the conversations. `respond()` is unchanged. * Still supported — nothing about this call has changed. */ session(subject: string, options?: SessionOptions): Promise; /** * This whole agent over HTTP, as ONE fetch handler for the host to mount — * the chat turn, the thread lifecycle, and the door and permission planes * below. See {@link agentHandler}, which is the same thing for a caller * holding the options rather than the agent. */ handler(options: HandlerOptions): (request: Request) => Promise; /** * This agent's MCP door, present exactly when its harness thinks outside this * process (`requires.toolDoor`). A library cannot add a route to the host's * server, so MOUNT THIS at `DOOR_PATH` (`/api/vendo/mcp`) — it is where the * box dials back to reach your tools, and it answers nothing but a live * turn's own credential. */ readonly door?: (request: Request) => Promise; /** * This agent's approvals and grants wire — what `@vendoai/vendo/ui`'s consent * surfaces already post to. MOUNT THIS at `PERMISSIONS_PATH` * (`/api/vendo`); `undefined` comes back for every path it does not own, * `DOOR_PATH` included, so ONE catch-all route can serve both. */ readonly permissions: (request: Request) => Promise; } /** * What `agent()` composed, for the one consumer that composes AROUND it: * `createVendo({ agent })`, where the embed adopts the agent's brain, its * persistence and its venue instead of resolving a second set. Read through a * WeakMap — the same shape `harnessAdapters()` uses — so the public agent * object stays exactly `{ name, session }`. */ export interface AgentComposition { /** WHICH agent this composed — `agent({ name })`. Declared here because a * consumer that scopes rows to one agent (`createTurns`) reads it off the * composition, never off a name a caller passes in beside it. */ agent: string; harness: Harness; store: VendoStore; files: FilesAdapter; guard: VendoGuard; /** Guard-bound already — the one choke point. */ tools: ToolRegistry; skills: readonly Skill[]; /** Present only for a harness that thinks on a machine. */ sandbox?: SandboxAdapter; /** Present exactly when the host asked for memory — the adapter a surface over * this agent reads and forgets a person's memories through. */ memory?: MemoryAdapter; /** The seats a harness that does NOT bring its own brain reads (`vendo()`). */ models?: SeatModels; instructions?: string; /** Carried so `awayRunner(agentComposition(agent))` speaks in the same voice * the agent's own turns do. */ system?: SystemPromptHook; } /** Undefined for anything this package did not build. */ export declare const agentComposition: (agent: VendoAgent) => AgentComposition | undefined; /** * The Cloud rungs. Their concrete shapes ship with the Cloud wiring, so this * package holds only the seam: an interface that returns a store/adapter. * `createVendo` (or the host) fills it; unfilled, the rung is a clear error. */ export interface CloudAdapters { sandbox?: (key: { apiKey: string; baseUrl?: string; }) => SandboxAdapter; } export declare function provideCloudAdapters(adapters: CloudAdapters): void; export interface PostgresOptions { /** Where workspace blobs land; unset → the store's own rows (≤ 5 MiB each). */ blobs?: FilesAdapter; encryption?: { key: string; }; allowUnencryptedSecrets?: boolean; } export declare function postgres(url: string, options?: PostgresOptions): VendoStore; export interface E2bOptions { apiKey?: string; /** Default box template when the harness names none. */ template?: string; timeoutMs?: number; } /** The harness's own template always wins; the adapter's is the fallback. */ export declare const withDefaultTemplate: (adapter: SandboxAdapter, template: string) => SandboxAdapter; export declare function e2b(options?: E2bOptions): SandboxAdapter; export declare function agent(config: AgentConfig): VendoAgent;