Packages

What each @intentface/latch-* package does, when you need it, and what it depends on.

Core is the runtime and the seams. Every other package is an implementation behind a seam — add it when you want the capability, skip it and the capability doesn't exist.

PackageAdd it when you want
latch-corealways — the runtime and the seams
latch-drizzlealmost always — storage, on SQLite or Postgres
latch-serveran HTTP surface you don't have to write
latch-mcptools from MCP servers
latch-openapitools from an OpenAPI spec
latch-memoryagents that remember across sessions (experimental)
latch-channelsSlack, Telegram, WhatsApp
latch-agent-builderagents your users create from your UI
latch-oteltracing over OpenTelemetry, Langfuse included
latch-netSSRF protection for your own fetches

latch-core

The runtime, the message model, and every seam. createRuntime is both the composition root and the operations facade — it runs turns, persists messages and runs, and handles approvals, durability, subagents, schedules and compaction.

Dependency-light on purpose: zod is the only runtime dependency, ai (>= 7) is a peer, and there is no HTTP framework, no database driver and no provider SDK anywhere in it.

ts
import { createRuntime, defineAgent, createVault, type ToolContext } from "@intentface/latch-core";

Key exports: createRuntime · defineAgent · ToolContext · createVault · mergeConnections · mergeToolSources · toModelMessages / toClientMessages · staticPricing / computeCost · createIntervalScheduler.

There is also @intentface/latch-core/node for the Node-only key derivation (deriveMasterKey), @intentface/latch-core/harness — the default harness tools (bash/read_file/write_file/glob/grep over the sandbox seam; web_fetch with clean markdown, media passthrough and SSRF gating; askUser; list_schedules/cancel_schedule), as harnessTools: defaultHarnessTools(); web_fetch's turndown + @intentface/latch-net are optional peers loaded on first call — and @intentface/latch-core/models — the model side of a host, so you don't hand-write it: a models.dev-backed catalog (ids, labels, pricing, reasoning capability, max output — filtered to the providers whose SDK is installed and API key is set), provider inference from a model id, and the four RuntimeConfig hooks that depend on the provider.

ts
import { loadModelCatalog, providerDefaults } from "@intentface/latch-core/models";

const catalog = await loadModelCatalog();
createRuntime({
  storage,
  context,
  agents: { assistant: defineAgent({ model: catalog.modelFor("claude-sonnet-4-6"), instructions: "…" }) },
  ...providerDefaults<Principal>(catalog, { principalKey: (p) => p.orgId }),
});

providerDefaults supplies pricing, resolveProviderTools, resolveReasoningOptions and resolvePromptCaching; list any of them after the spread to override it. The provider SDKs (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/google) are optional peers, imported lazily per provider — a host with one API key never loads the other two, and the core root imports none of them.

→ Runtime · Hooks

latch-drizzle

Storage, plus the tables. Implements the StorageAdapter contract for SQLite/libSQL and Postgres, and ships the migrations — tables prefixed latch_*.

ts
import { createSqliteAdapter, migrate, schema } from "@intentface/latch-drizzle";

const storage = createSqliteAdapter<Principal>(db, { owner });

It takes the db, not a connection string — your app owns the connection and the pool.

Beyond the core adapter it also provides the secret store (envelope-encrypted rows behind createVault), the memory job store, and an agent-builder store so DB-defined agents work without you writing that table.

The agent-builder store is versioned: upsert publishes (appending a snapshot unless the content is unchanged), activateVersion rolls back by moving a pointer, listVersions / getVersion / listActivations read the history, and getDraft / saveDraft / discardDraft / listDrafts hold one unpublished working copy per editor. See Agents → Versions, drafts and rollback.

Key exports: createSqliteAdapter · createPostgresAdapter · migrate / migratePostgres · createSqliteSecretStore · createPostgresSecretStore · createMemoryJobStore · createSqliteAgentBuilderStore · schema.

latch-server

One mountable handler for the whole runtime. A framework-agnostic (Request) => Promise<Response | undefined> covering chat, approvals, chat and run lists, history, schedules, and the generic connection OAuth.

createLatchApp({ storage, agents, resolveContext }) is the composition root: durability on, a croner cron evaluator, the models.dev catalog with its four provider hooks, the default harness tools, the handler mounted over the runtime, and an in-process cron driver on a long-running host (disabled on serverless — hit POST /cron/run). agents may be a function of the loaded catalog. App-level options (models, principalKey, cronIntervalMs) live on the config; runtime and handler defaults are overridable through runtime / handler. It returns { runtime, handler, models, scheduler?, stop }; createLatchHonoApp returns the same plus app (a mounted Hono app).

ts
import { createLatchApp, createLatchHandler, createCronerEvaluator } from "@intentface/latch-server";
import { createLatchHono, createLatchHonoApp } from "@intentface/latch-server/hono";

hono stays an optional peer because the adapter lives on a subpath. Also supplies createCronerEvaluator() for the cron seam, since core ships no cron library.

→ Hosting

latch-mcp

MCP servers as agent tools, with two guards core doesn't have: a per-tenant SSRF allowlist (an unlisted host throws before a socket opens) and vault-brokered auth (tokens resolved per tenant, never in agent config).

ts
import { createMcpConnections, defineMcpConnection, oauth, bearer } from "@intentface/latch-mcp";

Handles both auth styles: DCR (dynamic client registration — the MCP default, no pre-registration needed) and a pre-registered client via oauth({ clientId, clientSecret }), which is what Google, Slack and most non-MCP-native providers require.

An MCP tool's own annotations (destructiveHint, readOnlyHint) drive whether it needs approval, so a mutating third-party tool arrives gated without you enumerating it. For servers that ship no annotations there's nameHeuristicApproval.

Key exports: createMcpConnections · defineMcpConnection · createMcpHost · createMcpOAuth · none / bearer / headers / oauth · needsMcpApproval · nameHeuristicApproval.

latch-openapi

Tools from an OpenAPI spec. Same ConnectionRegistry shape as latch-mcp, so an agent's connections: [...] resolves across both kinds — useful for your own internal APIs, which usually have a spec and no MCP server.

Key exports: bearer · apiKey · headers · isHostAllowed. Uses @apidevtools/swagger-parser to resolve specs.

latch-memory

Experimental. The only production store today is a Loredex workspace, and the design may change.

Facts that survive the conversation. Wire the MemoryProvider seam and each turn gets a compiled index appended to its instructions plus memory_search / memory_save tools; background jobs handle extraction and consolidation out of band.

The load-bearing property is that scopes are hard boundaries — agent-user, agent-org and user are stored in disjoint subtrees, tools are path-locked at construction, and consolidation never crosses a scope. A personal agent's memory and a company-wide agent's memory differ by declaration, not convention.

ts
defineAgent<Principal>(() => ({
  model,
  memory: { scope: "agent-user", connection: "loredex" },
}));

Key exports: createMemoryProvider · createLoredexStore · scopeRefOf · the extraction/consolidation prompts and schemas.

→ Memory

latch-channels

Slack, Telegram and WhatsApp. Each adapter verifies the webhook, acknowledges fast, and runs the turn in the background. Approvals render as native platform UI. Per adapter: Slack uses native message streaming with tool calls as task cards, the Agents-pane surface, initiator-owned threads and signed approval buttons. Telegram and WhatsApp are two Surfaces over one turn kernel (createTurnKernel in the package root): dedupe, the identity gate with post-link replay, media folding, the answer posted once at finish (split past the platform cap, with a typing hint meanwhile), approvals, questionnaires and the /new /compact /cancel /status commands are written once — a new DM platform is a webhook parser plus a Surface.

ts
import { slackChannel } from "@intentface/latch-channels/slack";
import { telegramChannel } from "@intentface/latch-channels/telegram";
import { whatsappChannel } from "@intentface/latch-channels/whatsapp";

A channel is just another caller of the runtime — the same handleChat, the same storage, the same approvals as your web app. Only the rendering differs.

You supply the persistence: which agent a bot serves, the identity links, held messages, and delivery dedup. One turn per chat is guarded by core when your storage adapter has the one-active-run-per-chat invariant (the shipped drizzle adapters do): a second live run is refused and the channels render that as "still working". On a custom adapter without it, wire the optional claimTurn / releaseTurn lease — it is then your only turn serialisation.

latch-agent-builder

Agents as data. The AgentRecord model and the AgentBuilderStore seam, which doubles as core's DynamicAgentStore — so records your users create in your UI are runnable by your runtime, resolving to the same AgentConfig a code-declared agent would.

A record carries the model, instructions, connections, per-tool selection and approval overrides, execution capabilities, and a scope (user or org). latch-drizzle ships the storage.

Key exports: createAgentBuilder · normalizeAgentName · sharedReadTools · the AgentBuilderExtension seam for host-specific builder tools.

latch-otel

Tracing over OpenTelemetry, to any backend. One trace per run, its id derived from the run id (traceIdForRun), with the AI SDK's own spans under it in the GenAI semantic conventions: each step, each model call with its messages and token usage, each tool call with its arguments and result. Every span carries the session (the conversation), the user and the run, so the backend can filter on them. Implements core's telemetry seam; omit it and nothing is traced, at zero cost.

ts
import { otelTelemetry } from "@intentface/latch-otel";
import { langfuse } from "@intentface/latch-otel/langfuse";

createRuntime({
  telemetry: otelTelemetry({
    user: (p) => p.orgId,
    export: langfuse(), // or leave out: OTLP to OTEL_EXPORTER_OTLP_ENDPOINT
  }),
});

langfuse() reads LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY and LANGFUSE_BASE_URL and adds what Langfuse shows: the trace name, session, user, tags (agent:, run:, agent-version: …) and environment. Without a preset, spans go over OTLP to whatever OTEL_EXPORTER_OTLP_ENDPOINT points at (Grafana, Honeycomb, Datadog, Jaeger), or pass your own SpanExporter. It runs its own tracer provider, so it doesn't replace one your app has. Install @ai-sdk/otel at the version matching your ai.

Replaces @intentface/latch-langfuse, which also read traces and wrote scores back; that's left to your app or an evaluation tool.

latch-net

SSRF protection. The host blocklist (private ranges, loopback, link-local, IPv6 equivalents) and a pinned fetch on the ./pinned subpath that resolves a host once and connects to that address — closing the DNS-rebinding window between the check and the request.

Used by latch-mcp and latch-openapi. Worth importing directly for any fetch driven by model output or user-supplied URLs.

ts
import { isBlockedHost } from "@intentface/latch-net";
import { pinnedFetch } from "@intentface/latch-net/pinned";

Choosing a starting set

bash
pnpm add @intentface/latch-core @intentface/latch-drizzle ai @ai-sdk/anthropic

A runtime with persistence. You call handleChat from your own routes. No HTTP surface, no tools beyond what you define in code.

All packages version in lockstep, so matching majors and minors across @intentface/latch-* is the supported combination.