@crux/convex
Convex CruxStore adapter, Convex runtime profile, Convex Agent integration, flow helpers, and conversation compaction.
Peer dependency: convex >=1.0.0
Optional peer dependency for agent tool bridging: @convex-dev/agent >=0.6.0
import {
compactConversation,
createContextHandler,
createCruxConvex,
cruxConvexStore,
convexWorkspaceBlobStore,
} from '@crux/convex'
import { action, internalAction, query, mutation, flow } from '@crux/convex/server'
import { Agent, convexAgent, createAgent, createTool, convexTools, wrapConvexTool } from '@crux/convex/agent'
import { memory, recentMessages, workingState } from '@crux/convex/memory'
import { skill } from '@crux/convex/skill'
import { tool } from '@crux/convex/tools'For end-to-end Convex setup, memory, workspaces, Agent integration, flows, swarms, and devtools, see the Convex guide. This page is API reference.
Convex Runtime Profile
@crux/convex intentionally mirrors core Crux subpaths where possible. Use these imports in Convex code so Crux primitives can late-bind Convex runtime plumbing:
| Subpath | Classification | Export behavior |
|---|---|---|
@crux/convex/context | Identical re-export | Same API as @crux/core/context. |
@crux/convex/skill | Identical re-export + Convex adapter | Same skill authoring/session API as @crux/core/skill; adds convexSkillActivationPersistence() for Convex store snapshots. |
@crux/convex/memory | Convex-bound drop-in | Re-exports memory block helpers and changes only memory() defaults. |
@crux/convex/tools | Convex-bound drop-in | Mirrors tool() and injects Convex runtime metadata into execution. |
@crux/convex/agent | Mixed | Low-level Agent compatibility plus high-level Convex-only convexAgent(). |
The package root is curated. It exports Convex APIs plus common prompt authoring helpers (prompt, context, createPrompts, createContexts, and sanitization helpers), but it does not blanket re-export every @crux/core API. If a Convex mirror exists, prefer it; if no mirror exists yet, import the SDK-agnostic primitive from @crux/core.
createCruxConvex(options)
Create a reusable Convex runtime profile from the Crux Convex component and Convex Agent component:
import { createCruxConvex } from '@crux/convex'
import { components } from './_generated/api'
export const crux = createCruxConvex({
components: {
crux: components.crux,
agent: components.agent,
},
})The returned profile exposes:
| Method | Classification | Description |
|---|---|---|
store(ctx) | Convex-only API | Creates the request-scoped default CruxStore. Returns a promise only when a custom store factory is async. |
run(ctx, target, fn) | Convex-bound drop-in boundary | Binds ctx, target, store, namespace, memory, and tools for lower-level Crux work. |
convexAgent(config) | Convex-only API | Creates the high-level Convex Agent wrapper without repeating component wiring. |
bridge(http, cruxConfig, options?) | Convex-only API | Registers the HTTP Runtime Bridge through the same profile store path. |
Use run() when lower-level integration code needs a Convex-bound store or mirrored @crux/convex/* helpers outside the high-level agent wrapper:
await crux.run(ctx, { threadId }, async ({ store }) => {
await store.set(`blackboard:${threadId}`, { status: 'ready' })
})Advanced apps can override store construction once at the profile boundary. The override feeds run(), profile-created agents, and crux.bridge():
export const crux = createCruxConvex({
components: { crux: components.crux, agent: components.agent },
store: {
vectorIndexName: 'by_embedding',
create(ctx, defaults) {
return defaults.createComponentStore(ctx)
},
},
})cruxConvexStore(config)
Create a CruxStore backed by the crux Convex component.
| Field | Type | Description |
|---|---|---|
component | ComponentApi | components.crux from the installed Convex component |
ctx | ActionCtx | MutationCtx | Convex context with runQuery and runMutation |
vectorIndexName | string? | Optional vector index name for ctx.vectorSearch(). Defaults to 'by_embedding'. |
semanticCache | { isolatedVectorNamespace?: boolean }? | Capability metadata for semantic-cache stores with a dedicated vector namespace. |
Returns: CruxStore
| Method | Description |
|---|---|
.get(key) | Retrieve entry by key |
.set(key, entry) | Store or update an entry |
.delete(key) | Remove an entry |
.list(options?) | List entries with pagination |
.vectorSearch(embedding, options?) | Dense similarity search via Convex vector search |
.searchVectors({ dense }) | Dense retrieval through the generalized retrieval API |
cruxConvexStore() is intentionally dense-only today. It throws explicit errors for sparse or hybrid searchVectors() queries instead of silently degrading.
Store values are written as _cruxDoc records with JSON content, optional top-level embedding, updatedAt, and an _expiresAt value inside the JSON payload when ttl is provided. The component list query is a narrow Convex I/O contract: components.crux.memory.list accepts prefix, limit, and cursor, reads the by_key index, and returns { docs, cursor }.
The same internal document boundary is used by cruxConvexStore(), the HTTP bridge, and createConvexTransport(): imperative reads suppress expired entries and lazily delete them, vector hits map Convex _score to ScoredEntry.score, and list/vector filters use top-level exact-match semantics. Filtered list() calls with a limit fill from additional component pages so limit means "up to N matching entries." React transport reads are strict about _cruxDoc records so malformed transport documents fail clearly instead of being treated as legacy memory records.
convexWorkspaceBlobStore(config)
Create a BlobStore backed by Convex file storage. Use it with workspace() when agents write PDFs, images, CSVs, or other binary/large outputs.
import { workspace } from '@crux/core/workspace'
import { storage } from '@crux/core/storage'
import { cruxConvexStore, convexWorkspaceBlobStore } from '@crux/convex'
const ws = workspace({
id: 'thread-workspace',
namespace: threadId,
storage: storage({
data: cruxConvexStore({ component: components.crux, ctx }),
blobs: convexWorkspaceBlobStore({ ctx }),
}),
})Workspace metadata stays in the Convex-backed DataStore; binary and oversized payloads go to Convex file storage. If the current Convex runtime cannot read blobs, get() throws clearly instead of silently dropping file content.
convexAgent(config)
High-level Crux-native Convex Agent wrapper:
import { createCruxConvex, prompt } from '@crux/convex'
import { memory, recentMessages } from '@crux/convex/memory'
import { tool } from '@crux/convex/tools'
import { z } from 'zod'
import { components } from './_generated/api'
const threadMemory = memory({
id: 'support-memory',
blocks: [recentMessages({ id: 'recent', maxMessages: 10 })],
})
const lookupOrder = tool({
name: 'lookupOrder',
description: 'Look up an order.',
input: z.object({ orderId: z.string() }),
execute: async ({ input, ctx }) => getOrder(ctx, input.orderId),
})
const supportPrompt = prompt({
id: 'support-agent',
input: z.object({ message: z.string() }),
use: [threadMemory],
tools: { lookupOrder },
prompt: ({ input }) => input.message,
})
export const crux = createCruxConvex({
components: {
crux: components.crux,
agent: components.agent,
},
})
export const supportAgent = crux.convexAgent({
name: 'Support',
prompt: supportPrompt,
languageModel: model,
})
await supportAgent.generateText(ctx, { threadId, userId }, { input: { message } })convexAgent() resolves the prompt per turn, builds the Convex Agent tool registry from resolved Crux tools plus optional extra tools, installs the Convex Crux runtime, captures resolved memory after completed turns, and persists skill activation snapshots in the Crux store for later turns. It also supports Convex Agent's threaded flow:
The public surface remains Convex-Agent-shaped: app code calls generateText(), streamText(), and continueThread() with the same mental model as Convex Agent. Internally, Crux owns a profile-backed lifecycle that binds the request-scoped store, composes prepare() overrides, resolves prompt use[], adapts Crux and direct Convex Agent tools once, rebinds tool-call runtime metadata, handles best-effort skill/memory persistence, and records observability evidence around the turn.
Use languageModel for new code to mirror Convex Agent examples. Existing model call sites remain supported as a compatibility alias.
The exported ConvexAgentConfig type requires one of those model fields, with ConvexAgentBaseConfig and ConvexAgentModelConfig available for typed profile wrappers.
const { thread } = await supportAgent.continueThread(ctx, { threadId, userId }, { input: { message } })
await thread.streamText({ stopWhen })prepare(args) is the context-aware extension point for Convex Agent threads. It receives { ctx, target, input, messages } and may return input, runtime use[], extra tools, a prompt override, a token budget, and captureMessages. Runtime use[] entries are composed onto the active prompt even when a prompt override is returned. Use it when app data, request-scoped memory, retrievers, blackboards, or tool sets depend on the current Convex thread messages.
Agent
Crux-aware Convex Agent wrapper:
import { Agent } from '@crux/convex/agent'Agent preserves the Convex Agent thread API while adding Crux observability. thread.generateText(), thread.streamText(), generateObject(), and streamObject() emit canonical generation.call / generation.stream spans. The wrapper copies the configured languageModel.modelId / languageModel.provider onto the aggregate generation span and onto each streamed AI SDK step span, so RunDetail can show the model for agent turns even when Convex owns the provider call. Nested tool-call or flow generations still emit their own model/provider independently. Streaming spans close when the Convex Agent stream call returns, its finish callback fires, or the AI SDK step lifecycle reports a finished step, including tool-calls; tool-call metadata is recorded from awaited lifecycle callbacks and already-materialized returned values. Promise-valued returned stream metadata is never awaited, so unresolved metadata promises cannot keep a trace visually running or create late spans after the final flush. Wrapped tools flush after completion so nested generation and flow records are delivered before the tool result returns to the agent loop. If a wrapped tool throws, its tool.call span records phase: "tool.execute", errorKind: "execute_error", stack/raw artifacts, and the original error is rethrown to Convex Agent. Usage is recorded as a usage.observed event when the Convex Agent result exposes token usage, and interactive tool-call parts that do not execute a handler are represented as tool.call spans with executed: false.
convexTools(tools)
Convert Crux prompt-resolved tools into Convex Agent tools:
import { convexTools } from '@crux/convex/agent'
const board = createThreadBlackboard(threadId, ctx)
const assistant = createAssistantPrompt([board])
const resolved = await assistant.resolve({ input })
const tools = {
...businessTools,
...convexTools(resolved.tools),
}This is the bridge for primitives that contribute tools through use, such as
blackboard(). A directly used blackboard exposes readBlackboard,
writeBlackboard, patchBlackboard, and clearBlackboard.
For tools authored directly with Convex Agent's createTool(), wrap them with
wrapConvexTool(tool, { name }). The wrapper opens a canonical tool.call
span for the duration of execute(), propagates that span to nested delegates
or ctx.crux.runAction() calls, and records both the readable toolName and the
provider toolCallId. Execute failures keep the original throw behavior while
attaching normalized error evidence to the same span.
import { createTool, wrapConvexTool } from '@crux/convex/agent'
const research = wrapConvexTool(createTool({ description, inputSchema, execute }), {
name: 'research',
})compactConversation(args)
Stateless conversation compaction for Convex actions.
| Field | Type | Description |
|---|---|---|
evictedMessages | Message[] | Messages that fell out of the recent window |
existingSummary | string | Existing running summary, or an empty string |
generate | GenerateTextFn | Text generation function |
model | unknown | Model for summarization |
summaryBudget | number? | Max summary tokens. Defaults to 1000 |
Returns: Promise<{ summary: string, tokensBefore: number, tokensAfter: number, ratio: number }>
Usage
import { cruxConvexStore } from '@crux/convex'
import { episodes, memory } from '@crux/convex/memory'
import { components } from './_generated/api'
const store = cruxConvexStore({
component: components.crux,
ctx,
})
const history = episodes({ id: 'chat-log', embed: dense })
const mem = memory({ id: 'assistant', store, namespace: 'thread:1', blocks: [history] })createContextHandler(config)
Create a low-level Convex Agent context handler that resolves Crux Context objects into a single system message.
This is the manual bridge between Crux context objects and the Convex Agent SDK. Prefer crux.convexAgent({ prompt, prepare }) for Crux-native agents; it resolves prompt use[], memory, skills, and tools together. Use createContextHandler() only when you intentionally assemble a raw Convex Agent and need to pass already-expanded Context objects:
const contextHandler = createContextHandler({
handler: async (ctx, args) => {
const message = String(args.inputPrompt.at(-1)?.content ?? '')
return {
contexts: [
retriever.asContext({
priority: 60,
query: ({ message }) => String(message ?? ''),
}),
mem.asContext({ priority: 80 }),
],
input: { message },
}
},
})That keeps retrieval, memory, and Convex Agent composition on the Crux side instead of hand-building system strings in every agent.
createConvexTransport(config)
Create a CruxTransport for reactive hooks using Convex's native useQuery() reactivity.
import { useQuery } from 'convex/react'
import { createConvexTransport } from '@crux/convex/react'
import { CruxProvider } from '@crux/react'| Field | Type | Description |
|---|---|---|
api | CruxComponentApi | Convex component API (api.crux or components.crux) |
useQuery | UseQueryFn | Convex useQuery hook |
Returns: CruxTransport
const transport = createConvexTransport({ api: api.crux, useQuery })
<ConvexProvider client={convex}>
<CruxProvider transport={transport}>
<App />
</CruxProvider>
</ConvexProvider>Plans, task lists, and tasks stored via cruxConvexStore() are automatically reactive through the Convex transport —
no polling or SSE needed. The transport consumes the component's { docs, cursor } page shape and applies
decoded-value filters locally.
@crux/convex/server
Crux-aware Convex function builders. These preserve Convex's native function shape while adding the hidden __crux propagation envelope, ctx.crux helper surface, context restoration, and bounded action flushing.
import { action, internalAction, query, mutation, flow } from '@crux/convex/server'Use action() and internalAction() for AI runtime entrypoints. Use ctx.crux.runAction() for related child actions, ctx.crux.runQuery() / runMutation() for context-preserving reads/writes, and ctx.crux.scheduler.runAfter() for scheduled resumes. Queries and mutations restore incoming context but do not create standalone Runs by default.
ctx.crux.runAction() is a two-sided boundary. The parent opens and flushes a runtime.convex.action span, then sends a stable boundary id in the hidden __crux envelope. The receiving Crux-aware action emits runtime.convex.boundary.received and runtime.convex.boundary.completed / runtime.convex.boundary.failed events on that boundary span. The devtools backend uses those acknowledgements to reconcile a missing parent-side boundary end when Convex loses the caller's final delivery after the child worker already completed.
export const research = action({
args: { question: v.string() },
handler: async (ctx, args) => {
return ctx.crux.runAction('research planner', internal.research.plan, args)
},
})flow({ name, args, handler }) returns { name, args, handler, action, signal }. Export .action for internal start/resume, or wrap .handler in your own public action for auth. Direct .handler() calls perform the same bounded observability flush after a flow result, so suspended flows are visible immediately at their suspend point.
const researchFlow = flow({
name: 'research',
args: { question: v.string() },
handler: async (flow, args, ctx) => {
const plan = await flow.step('plan', () => planResearch(args.question))
return flow.step('synthesize', () => synthesize(plan))
},
})
export const research = researchFlow.actionflushObservability(options?)
Await queued canonical graph deliveries from a Convex or serverless action with a bounded timeout.
import { flushObservability } from '@crux/convex/observability'
await flushObservability()withObservabilityFlush(handler, options?)
Wrap a Convex action handler so observability flushes in finally, including error paths.
import { withObservabilityFlush } from '@crux/convex/observability'
export const run = internalAction({
args: {},
handler: withObservabilityFlush(async (ctx, args) => {
// Crux work here
}),
})Types
import type {
CompactConversationArgs,
ContextHandlerConfig,
ConvexCtxPort,
ConvexMemoryStoreConfig,
CreateCruxConvexOptions,
CruxConvexProfile,
} from '@crux/convex'
import type { ConvexWorkspaceBlobStoreConfig } from '@crux/convex/workspace'Related
- Guide: Convex
- Reference: Store interface