oneie
Version:
Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.
1,716 lines (1,443 loc) • 51 kB
Markdown
---
title: Effect Components
dimension: things
category: plans
tags: agent, ai, backend
related_dimensions: events, knowledge
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the things dimension in the plans category.
Location: one/things/plans/effect-components.md
Purpose: Documents production integration guide: effect.ts + confect + convex components for ai agents
Related dimensions: events, knowledge
For AI agents: Read this to understand effect components.
---
# Production Integration Guide: Effect.ts + Confect + Convex Components for AI Agents
**Building sophisticated multi-agent systems with functional patterns, type safety, and durable workflows**
Effect.ts and Confect bring powerful functional programming patterns to Convex's already robust backend platform. While no official Effect integrations exist yet for Convex components, this guide provides production-ready patterns for combining these technologies to build reliable, type-safe AI agent systems.
## Integration Philosophy: Effect Wraps Components, Not Replaces
The key insight is that **Effect.ts enhances business logic composition while Convex components handle infrastructure concerns**. Components like Agent, Workflow, and RAG provide production-tested patterns that you wrap with Effect for better error handling, dependency injection, and testability.
```typescript
// WRONG: Trying to replace components with Effect
const myOwnAgentLogic = Effect.gen(function* () {...})
// RIGHT: Wrapping components with Effect for better composition
const agentWithEffect = (agent: Agent) => Effect.gen(function* () {
const result = yield* Effect.tryPromise({
try: () => agent.generateText(ctx, { threadId }, { prompt }),
catch: (error) => new AgentExecutionError({ cause: error })
})
return result
})
```
## Integration Patterns by Component
### 1. @convex-dev/agent with Effect Integration
**Where Effect Adds Value**: Business logic orchestration, error handling, composing multiple agent calls, dependency injection for services used by tools.
**Where to Use Component Directly**: Thread/message management, streaming, built-in RAG, tool calling infrastructure.
#### Pattern: Effect-Wrapped Agent Execution
```typescript
import { Effect, Context } from "effect";
import { Agent } from "@convex-dev/agent";
import { openai } from "@ai-sdk/openai";
import { components } from "./_generated/api";
// Define errors as tagged classes
class AgentError extends Data.TaggedError("AgentError")<{
agentName: string;
cause: unknown;
}> {}
// Service definition for agent
class AgentService extends Context.Tag("AgentService")<
AgentService,
{
readonly generateResponse: (
ctx: ActionCtx,
threadId: string,
prompt: string,
) => Effect.Effect<string, AgentError>;
}
>() {}
// Implementation with Effect
const AgentServiceLive = Layer.effect(
AgentService,
Effect.gen(function* () {
// Agent component handles infrastructure
const supportAgent = new Agent(components.agent, {
name: "Support Agent",
chat: openai.chat("gpt-4o-mini"),
instructions: "You are a helpful assistant.",
tools: {
/* ... */
},
});
return {
generateResponse: (ctx, threadId, prompt) =>
Effect.tryPromise({
try: async () => {
const { thread } = await supportAgent.continueThread(ctx, {
threadId,
});
const result = await thread.generateText({ prompt });
return result.text;
},
catch: (error) =>
new AgentError({
agentName: "Support Agent",
cause: error,
}),
}),
};
}),
);
```
#### Pattern: Composing Multiple Agents with Effect
```typescript
// Multi-agent orchestration with Effect
export const multiAgentPipeline = action({
args: { prompt: v.string(), userId: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
const agentService = yield* AgentService;
const ragService = yield* RAGService;
// Sequential with automatic error propagation
const context = yield* ragService.search(args.prompt);
const classification = yield* agentService.classify(ctx, args.prompt);
// Conditional routing
const response = yield* classification.intent === "technical"
? agentService.technicalAgent(ctx, args.prompt, context)
: agentService.generalAgent(ctx, args.prompt, context);
// Parallel quality checks
const [grammar, factuality] = yield* Effect.all(
[
agentService.checkGrammar(response),
agentService.checkFactuality(response, context),
],
{ concurrency: 2 },
);
return { response, grammar, factuality };
}).pipe(
Effect.provide(Layer.merge(AgentServiceLive, RAGServiceLive)),
Effect.catchAll((error) => {
console.error("Multi-agent pipeline failed:", error);
return Effect.succeed({
response: "I'm having trouble processing that request.",
error: true,
});
}),
),
});
```
#### Pattern: Effect-Based Tool Definitions
```typescript
import { createTool } from "@convex-dev/agent";
import { z } from "zod";
// Define service for external integrations
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
readonly send: (
to: string,
subject: string,
body: string,
) => Effect.Effect<void, EmailError>;
}
>() {}
const EmailServiceLive = Layer.succeed(EmailService, {
send: (to, subject, body) =>
Effect.tryPromise({
try: () => sendgrid.send({ to, subject, body }),
catch: (error) => new EmailError({ cause: error }),
}),
});
// Tool that uses Effect internally
export const emailTool = createTool({
description: "Send an email to a user",
args: z.object({
userEmail: z.string().email(),
subject: z.string(),
message: z.string(),
}),
handler: async (toolCtx, args) => {
// Convert Effect to Promise for tool handler
const program = Effect.gen(function* () {
const emailService = yield* EmailService;
yield* emailService.send(args.userEmail, args.subject, args.message);
return "Email sent successfully";
});
return await Effect.runPromise(
program.pipe(
Effect.provide(EmailServiceLive),
Effect.catchAll((error) =>
Effect.succeed(`Failed to send email: ${error.message}`),
),
),
);
},
});
const agent = new Agent(components.agent, {
chat: openai.chat("gpt-4o"),
tools: { sendEmail: emailTool },
});
```
**Type Safety Considerations**:
- Effect's error channel tracks all possible failures
- Tool context provides typed access to `userId`, `threadId`, `messageId`
- Convert Effects to Promises at the tool boundary using `Effect.runPromise`
- Use `Effect.tryPromise` to wrap agent component calls
---
### 2. @convex-dev/persistent-text-streaming with Effect
**Where Effect Adds Value**: Error recovery during streaming, composing stream generation with other services, handling stream lifecycle.
**Where to Use Component Directly**: WebSocket delivery, delta storage, client subscription.
#### Pattern: Effect-Managed Streaming Lifecycle
```typescript
import { PersistentTextStreaming } from "@convex-dev/persistent-text-streaming";
import { Effect, Stream } from "effect";
class StreamingService extends Context.Tag("StreamingService")<
StreamingService,
{
readonly generateStream: (
ctx: ActionCtx,
prompt: string,
streamId: string,
) => Effect.Effect<void, StreamingError>;
}
>() {}
const StreamingServiceLive = Layer.effect(
StreamingService,
Effect.gen(function* () {
const persistentTextStreaming = new PersistentTextStreaming(
components.persistentTextStreaming,
);
const llmService = yield* LLMService;
return {
generateStream: (ctx, prompt, streamId) =>
Effect.gen(function* () {
// Fetch context with Effect error handling
const context = yield* Effect.tryPromise({
try: () => ctx.runQuery(api.chat.getContext, { streamId }),
catch: () => new ContextFetchError(),
});
// Stream with Effect composition
const stream = yield* llmService.streamCompletion(prompt, context);
// Handle chunks with backpressure
yield* Stream.fromAsyncIterable(
stream,
() => new StreamProcessingError(),
).pipe(
Stream.mapEffect((chunk) =>
Effect.tryPromise({
try: async () => {
await persistentTextStreaming.appendChunk(
ctx,
streamId,
chunk,
);
},
catch: () => new ChunkAppendError({ chunk }),
}),
),
Stream.runDrain,
);
// Mark complete
yield* Effect.tryPromise({
try: () => ctx.runMutation(api.chat.markComplete, { streamId }),
catch: () => new StreamCompletionError(),
});
}).pipe(
Effect.retry(
Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(3)),
),
),
),
};
}),
);
```
**Recommendation**: Use Agent component's built-in streaming (`saveStreamDeltas: true`) for most use cases. Only use persistent-text-streaming if you need HTTP streaming for the initiating client specifically.
---
### 3. @convex-dev/rate-limiter with Effect
**Where Effect Adds Value**: Composing rate limiting with business logic, handling rate limit errors functionally, coordinating multiple rate limits.
**Where to Use Component Directly**: Token bucket/fixed window algorithms, per-user isolation, capacity reservation.
#### Pattern: Effect Service Layer with Rate Limiting
```typescript
import { RateLimiter, MINUTE } from "@convex-dev/rate-limiter";
import { Effect, Schedule } from "effect";
class RateLimitedLLMService extends Context.Tag("RateLimitedLLMService")<
RateLimitedLLMService,
{
readonly generateCompletion: (
ctx: ActionCtx,
userId: string,
prompt: string,
) => Effect.Effect<string, RateLimitError | LLMError>;
}
>() {}
const RateLimitedLLMServiceLive = Layer.effect(
RateLimitedLLMService,
Effect.gen(function* () {
const rateLimiter = new RateLimiter(components.rateLimiter, {
llmRequests: {
kind: "token bucket",
rate: 20,
period: MINUTE,
capacity: 30,
},
llmTokens: {
kind: "token bucket",
rate: 40000,
period: MINUTE,
shards: 10,
},
});
return {
generateCompletion: (ctx, userId, prompt) =>
Effect.gen(function* () {
// Check request limit
const requestLimit = yield* Effect.tryPromise({
try: () =>
rateLimiter.limit(ctx, "llmRequests", {
key: userId,
reserve: true,
}),
catch: () => new RateLimiterError(),
});
if (!requestLimit.ok) {
return yield* Effect.fail(
new RateLimitError({
type: "requests",
retryAfter: requestLimit.retryAfter,
}),
);
}
// Estimate token usage
const estimatedTokens = Math.ceil(prompt.length / 4);
const tokenLimit = yield* Effect.tryPromise({
try: () =>
rateLimiter.limit(ctx, "llmTokens", {
key: userId,
count: estimatedTokens,
}),
catch: () => new RateLimiterError(),
});
if (!tokenLimit.ok) {
return yield* Effect.fail(
new RateLimitError({
type: "tokens",
retryAfter: tokenLimit.retryAfter,
}),
);
}
// Make LLM call
const result = yield* Effect.tryPromise({
try: () =>
openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
}),
catch: (error) => new LLMError({ cause: error }),
});
return result.choices[0].message.content;
}).pipe(
// Automatic retry with exponential backoff on rate limits
Effect.retry(
Schedule.exponential("1 second").pipe(
Schedule.whileInput(
(error: RateLimitError | LLMError) =>
error._tag === "RateLimitError",
),
Schedule.recurs(5),
),
),
),
};
}),
);
```
#### Pattern: Multi-Level Rate Limiting with Effect
```typescript
// Service that coordinates user and global rate limits
export const protectedAgentCall = action({
args: { userId: v.string(), prompt: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
const rateLimitService = yield* RateLimitService;
// Check multiple limits in parallel
const [userLimit, globalLimit] = yield* Effect.all(
[
rateLimitService.checkUserLimit(ctx, args.userId),
rateLimitService.checkGlobalLimit(ctx),
],
{ concurrency: 2 },
);
// Fail fast if any limit exceeded
if (!userLimit.ok) {
return yield* Effect.fail(
new UserRateLimitExceeded({
retryAfter: userLimit.retryAfter,
}),
);
}
if (!globalLimit.ok) {
return yield* Effect.fail(
new GlobalRateLimitExceeded({
retryAfter: globalLimit.retryAfter,
}),
);
}
// Execute with agent
const agentService = yield* AgentService;
const result = yield* agentService.generateResponse(
ctx,
args.userId,
args.prompt,
);
return result;
}).pipe(
Effect.provide(Layer.merge(RateLimitServiceLive, AgentServiceLive)),
Effect.catchTag("UserRateLimitExceeded", (error) =>
Effect.succeed({
error: "You've reached your request limit. Please try again later.",
retryAfter: error.retryAfter,
}),
),
),
});
```
**Type Safety Considerations**:
- Effect's error channel explicitly tracks rate limit vs other errors
- Use `Effect.retry` with `Schedule.whileInput` for smart retry logic
- `Effect.all` enables parallel rate limit checks
- Convert Promise-based rate limiter to Effect with `Effect.tryPromise`
---
### 4. @convex-dev/retrier with Effect
**Where Effect Adds Value**: Coordinating retries across multiple operations, custom retry policies, composing retry logic with other effects.
**Where to Use Component Directly**: Action retries with persistent state, status tracking, completion callbacks.
#### Pattern: Effect-Based Retry Orchestration
```typescript
import { ActionRetrier } from "@convex-dev/action-retrier";
import { Effect, Schedule } from "effect";
class ResilientExecutionService extends Context.Tag(
"ResilientExecutionService",
)<
ResilientExecutionService,
{
readonly executeWithRetry: <A>(
ctx: MutationCtx,
action: FunctionReference<"action">,
args: any,
) => Effect.Effect<A, ExecutionError>;
}
>() {}
const ResilientExecutionServiceLive = Layer.effect(
ResilientExecutionService,
Effect.gen(function* () {
const retrier = new ActionRetrier(components.actionRetrier, {
initialBackoffMs: 250,
base: 2,
maxFailures: 4,
});
return {
executeWithRetry: (ctx, action, args) =>
Effect.gen(function* () {
// Start retry execution
const runId = yield* Effect.tryPromise({
try: () => retrier.run(ctx, action, args),
catch: () => new RetrierStartError(),
});
// Poll for completion with Effect
const result = yield* Effect.repeat(
Effect.gen(function* () {
const status = yield* Effect.tryPromise({
try: () => retrier.status(ctx, runId),
catch: () => new StatusCheckError(),
});
if (status.type === "inProgress") {
return yield* Effect.fail(Option.none()); // Continue polling
}
return status.result;
}),
Schedule.spaced("500 millis").pipe(
Schedule.compose(Schedule.recurs(60)), // Max 30 seconds
),
).pipe(
Effect.catchAll((error) =>
Effect.fail(new ExecutionTimeout({ runId })),
),
);
// Handle result
if (result.type === "success") {
return result.returnValue;
} else if (result.type === "failed") {
return yield* Effect.fail(
new ExecutionFailed({
error: result.error,
}),
);
} else {
return yield* Effect.fail(new ExecutionCanceled());
}
}),
};
}),
);
```
#### Pattern: Combining Retrier with Custom Effect Retry
```typescript
// Use retrier for infrastructure-level retries,
// Effect for application-level retries
export const reliableWorkflow = mutation({
args: { userId: v.string(), data: v.any() },
handler: (ctx, args) =>
Effect.gen(function* () {
const resilientService = yield* ResilientExecutionService;
// Effect-level retry for transient errors
const validated = yield* Effect.retry(
validateUserData(args.data),
Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(3)),
),
);
// Retrier-level retry for action execution
const processed = yield* resilientService.executeWithRetry(
ctx,
internal.processing.processData,
{ userId: args.userId, data: validated },
);
return processed;
}).pipe(
Effect.provide(ResilientExecutionServiceLive),
Effect.catchAll((error) => {
console.error("Workflow failed:", error);
return Effect.fail(new ConvexError("Workflow execution failed"));
}),
),
});
```
**Recommendation**: Use component's retrier for long-running actions with persistent state. Use Effect's `Effect.retry` for in-memory retries of short operations.
---
### 5. @convex-dev/workpool with Effect
**Where Effect Adds Value**: Composing work queue operations, error handling for enqueued tasks, coordinating multiple workpools.
**Where to Use Component Directly**: Task queuing, parallelism control, completion callbacks.
#### Pattern: Effect Service Layer for Workpool
```typescript
import { Workpool } from "@convex-dev/workpool";
import { Effect, Queue } from "effect";
class TaskQueueService extends Context.Tag("TaskQueueService")<
TaskQueueService,
{
readonly enqueueTask: <A>(
ctx: MutationCtx,
priority: "high" | "low",
action: FunctionReference<"action">,
args: any,
) => Effect.Effect<string, EnqueueError>;
readonly getTaskStatus: (
ctx: QueryCtx,
taskId: string,
) => Effect.Effect<TaskStatus, StatusError>;
}
>() {}
const TaskQueueServiceLive = Layer.effect(
TaskQueueService,
Effect.gen(function* () {
const highPriorityPool = new Workpool(components.highPriorityWorkpool, {
maxParallelism: 20,
retryActionsByDefault: true,
});
const lowPriorityPool = new Workpool(components.lowPriorityWorkpool, {
maxParallelism: 5,
retryActionsByDefault: true,
});
return {
enqueueTask: (ctx, priority, action, args) =>
Effect.gen(function* () {
const pool = priority === "high" ? highPriorityPool : lowPriorityPool;
const taskId = yield* Effect.tryPromise({
try: () =>
pool.enqueueAction(ctx, action, args, {
onComplete: internal.tasks.handleCompletion,
context: { priority },
}),
catch: (error) => new EnqueueError({ cause: error }),
});
return taskId;
}),
getTaskStatus: (ctx, taskId) =>
Effect.tryPromise({
try: () =>
highPriorityPool
.status(taskId)
.then((status) => status ?? lowPriorityPool.status(taskId)),
catch: () => new StatusError({ taskId }),
}),
};
}),
);
```
#### Pattern: Batch Task Enqueuing with Effect
```typescript
export const processBatch = mutation({
args: { items: v.array(v.any()) },
handler: (ctx, args) =>
Effect.gen(function* () {
const taskQueue = yield* TaskQueueService;
// Enqueue all tasks in parallel
const taskIds = yield* Effect.all(
args.items.map((item) =>
taskQueue.enqueueTask(ctx, "low", internal.tasks.processItem, item),
),
{ concurrency: 10 },
);
return { taskIds, count: taskIds.length };
}).pipe(
Effect.provide(TaskQueueServiceLive),
Effect.catchAll((error) => {
console.error("Batch processing failed:", error);
return Effect.succeed({ taskIds: [], count: 0, error: true });
}),
),
});
```
---
### 6. @convex-dev/workflow with Effect
**Where Effect Adds Value**: Business logic in workflow steps, error handling across steps, service composition in workflows.
**Where to Use Component Directly**: Durable execution, journaling, long-running operations, retry policies.
#### Pattern: Effect Services in Workflow Steps
```typescript
import { WorkflowManager } from "@convex-dev/workflow";
import { Effect } from "effect";
const workflow = new WorkflowManager(components.workflow, {
defaultRetryBehavior: {
maxAttempts: 3,
initialBackoffMs: 1000,
base: 2,
},
});
export const aiAgentWorkflow = workflow.define({
args: { userId: v.string(), prompt: v.string() },
handler: async (step, args): Promise<WorkflowResult> => {
// Step 1: Fetch user context (query)
const userContext = await step.runQuery(internal.workflows.getUserContext, {
userId: args.userId,
});
// Step 2: Generate initial response with Effect composition (action)
const initialResponse = await step.runAction(
internal.workflows.generateResponseWithEffect,
{ prompt: args.prompt, context: userContext },
);
// Step 3: Quality check in parallel (actions)
const [grammarScore, factualityScore] = await Promise.all([
step.runAction(internal.workflows.checkGrammar, {
text: initialResponse,
}),
step.runAction(internal.workflows.checkFactuality, {
text: initialResponse,
}),
]);
// Step 4: Refine if needed (action with conditional logic)
const finalResponse =
grammarScore < 0.8 || factualityScore < 0.8
? await step.runAction(
internal.workflows.refineResponse,
{ original: initialResponse, context: userContext },
{ retry: { maxAttempts: 2 } },
)
: initialResponse;
// Step 5: Save result (mutation)
await step.runMutation(internal.workflows.saveResult, {
userId: args.userId,
response: finalResponse,
});
return {
response: finalResponse,
refined: finalResponse !== initialResponse,
};
},
});
// The action called by the workflow uses Effect internally
export const generateResponseWithEffect = internalAction({
args: { prompt: v.string(), context: v.any() },
handler: async (ctx, args) => {
// Use Effect for business logic composition
const program = Effect.gen(function* () {
const agentService = yield* AgentService;
const ragService = yield* RAGService;
// Enrich context with RAG
const enrichedContext = yield* ragService.search(args.prompt);
// Generate with agent
const response = yield* agentService.generateResponse(ctx, args.prompt, {
...args.context,
rag: enrichedContext,
});
return response;
});
return await Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(AgentServiceLive, RAGServiceLive)),
Effect.catchAll((error) => {
console.error("Generation failed:", error);
return Effect.succeed(
"I apologize, but I'm having trouble generating a response.",
);
}),
),
);
},
});
```
**Key Pattern**: Workflow component handles durability and retry at the step level. Use Effect within actions for business logic composition and error handling.
---
### 7. @convex-dev/rag with Effect
**Where Effect Adds Value**: Composing RAG with other services, custom embedding generation, error handling for vector operations.
**Where to Use Component Directly**: Vector storage, semantic search, chunking, filtering.
#### Pattern: Effect Service Layer for RAG
```typescript
import { RAG } from "@convex-dev/rag";
import { openai } from "@ai-sdk/openai";
import { Effect } from "effect";
class RAGService extends Context.Tag("RAGService")<
RAGService,
{
readonly addDocument: (
ctx: ActionCtx,
namespace: string,
content: string,
metadata: Record<string, any>,
) => Effect.Effect<string, RAGError>;
readonly search: (
ctx: ActionCtx,
namespace: string,
query: string,
limit?: number,
) => Effect.Effect<SearchResults, RAGError>;
}
>() {}
const RAGServiceLive = Layer.effect(
RAGService,
Effect.gen(function* () {
const rag = new RAG(components.rag, {
textEmbeddingModel: openai.embedding("text-embedding-3-small"),
embeddingDimension: 1536,
});
return {
addDocument: (ctx, namespace, content, metadata) =>
Effect.gen(function* () {
// Pre-process content with Effect
const processed = yield* preprocessContent(content);
// Add to RAG
const entryId = yield* Effect.tryPromise({
try: () =>
rag.add(ctx, {
namespace,
text: processed,
filterValues: Object.entries(metadata).map(([name, value]) => ({
name,
value,
})),
onComplete: internal.rag.handleComplete,
}),
catch: (error) => new RAGAddError({ cause: error }),
});
return entryId.entryId;
}),
search: (ctx, namespace, query, limit = 10) =>
Effect.tryPromise({
try: () =>
rag.search(ctx, {
namespace,
query,
limit,
chunkContext: { before: 1, after: 1 },
vectorScoreThreshold: 0.6,
}),
catch: (error) => new RAGSearchError({ cause: error }),
}),
};
}),
);
```
#### Pattern: Composing RAG with Agent Using Effect
```typescript
// Agent tool that uses RAG service
export const contextualAnswerAgent = action({
args: { userId: v.string(), question: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
const ragService = yield* RAGService;
const agentService = yield* AgentService;
// Fetch relevant context
const searchResults = yield* ragService.search(
ctx,
args.userId, // User-specific namespace
args.question,
8,
);
// Generate answer with context
const answer = yield* agentService.generateWithContext(
ctx,
args.question,
searchResults.text,
);
return {
answer,
sources: searchResults.entries.map((e) => ({
id: e.entryId,
title: e.title,
relevance: e.score,
})),
};
}).pipe(
Effect.provide(Layer.merge(RAGServiceLive, AgentServiceLive)),
Effect.catchAll((error) => {
console.error("Contextual answer failed:", error);
return Effect.succeed({
answer: "I'm having trouble finding relevant information.",
sources: [],
error: true,
});
}),
),
});
```
---
### 8. @convex-dev/crons with Effect
**Where Effect Adds Value**: Scheduled job logic composition, error handling in cron jobs, service orchestration.
**Where to Use Component Directly**: Dynamic cron registration, schedule management.
#### Pattern: Effect-Based Cron Job Logic
```typescript
import { Crons } from "@convex-dev/crons";
import { Effect } from "effect";
// Register cron that executes Effect-based business logic
export const setupDailyCron = internalMutation({
handler: async (ctx) => {
const crons = new Crons(components.crons);
await crons.register(
ctx,
{ kind: "cron", cronspec: "0 0 * * *" }, // Daily at midnight
internal.crons.dailyMaintenanceWithEffect,
{},
"daily-maintenance",
);
},
});
// Cron job uses Effect for composition
export const dailyMaintenanceWithEffect = internalAction({
handler: async (ctx) => {
const program = Effect.gen(function* () {
const ragService = yield* RAGService;
const monitoringService = yield* MonitoringService;
// Clean up old content
yield* Effect.tryPromise({
try: () => ctx.runMutation(internal.rag.cleanupOldContent, {}),
catch: () => new CleanupError(),
});
// Generate daily report
const report = yield* monitoringService.generateDailyReport(ctx);
// Send to admin
yield* Effect.tryPromise({
try: () => ctx.runAction(internal.email.sendReport, { report }),
catch: () => new EmailError(),
});
return { success: true, report };
});
await Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(RAGServiceLive, MonitoringServiceLive)),
Effect.catchAll((error) => {
console.error("Daily maintenance failed:", error);
return Effect.succeed({ success: false, error });
}),
),
);
},
});
```
---
## Confect Integration: Full Effect.ts Integration for Convex
**Confect** (by rjdellecese) provides comprehensive Effect.ts integration with Convex, enabling Effect-native function definitions and database operations.
### When to Use Confect
**Use Confect if:**
- Your team is already invested in Effect.ts patterns
- You want Effect schemas instead of Convex validators
- You need deep Effect integration throughout your codebase
- You want `Option<A>` instead of `A | null` everywhere
- You're building a new project from scratch
**Use manual Effect integration (patterns above) if:**
- You have an existing Convex project
- You want to adopt Effect gradually
- Your team is learning Effect.ts
- You need to maintain compatibility with existing Convex patterns
### Confect Architecture
```typescript
// convex/schema.ts - Define schema with Effect Schema
import { Id, defineSchema, defineTable } from "@rjdellecese/confect/server";
import { Schema } from "effect";
export const confectSchema = defineSchema({
users: defineTable(
Schema.Struct({
username: Schema.String,
email: Schema.String.pipe(Schema.maxLength(100)),
role: Schema.Literal("admin", "user"),
}),
),
threads: defineTable(
Schema.Struct({
userId: Id.Id("users"),
title: Schema.String,
messages: Schema.Array(
Schema.Struct({
role: Schema.Literal("user", "assistant"),
content: Schema.String,
}),
),
}),
).index("by_user", ["userId"]),
});
export default confectSchema.convexSchemaDefinition;
```
```typescript
// convex/confect.ts - Generate function constructors
import {
ConfectActionCtx as ConfectActionCtxService,
type ConfectActionCtx as ConfectActionCtxType,
ConfectMutationCtx as ConfectMutationCtxService,
type ConfectMutationCtx as ConfectMutationCtxType,
ConfectQueryCtx as ConfectQueryCtxService,
type ConfectQueryCtx as ConfectQueryCtxType,
makeFunctions,
} from "@rjdellecese/confect/server";
import { confectSchema } from "./schema";
export const {
query,
mutation,
action,
internalQuery,
internalMutation,
internalAction,
} = makeFunctions(confectSchema);
type ConfectDataModel = ConfectDataModelFromConfectSchemaDefinition<
typeof confectSchema
>;
export const ConfectQueryCtx = ConfectQueryCtxService<ConfectDataModel>();
export type ConfectQueryCtx = ConfectQueryCtxType<ConfectDataModel>;
export const ConfectMutationCtx = ConfectMutationCtxService<ConfectDataModel>();
export type ConfectMutationCtx = ConfectMutationCtxType<ConfectDataModel>;
export const ConfectActionCtx = ConfectActionCtxService<ConfectDataModel>();
export type ConfectActionCtx = ConfectActionCtxType<ConfectDataModel>;
```
```typescript
// convex/users.ts - Write functions with Effect
import { Effect, Option } from "effect";
import {
ConfectMutationCtx,
ConfectQueryCtx,
mutation,
query,
} from "./confect";
import { Schema } from "effect";
// Define args and returns with Effect Schema
const CreateUserArgs = Schema.Struct({
username: Schema.String,
email: Schema.String,
});
const CreateUserResult = Schema.Struct({
userId: Schema.String,
});
export const createUser = mutation({
args: CreateUserArgs,
returns: CreateUserResult,
handler: ({ username, email }) =>
Effect.gen(function* () {
// Inject Convex context via Effect
const { db } = yield* ConfectMutationCtx;
// Database operations return Effects, not Promises
const existing = yield* db
.query("users")
.withIndex("by_username", (q) => q.eq("username", username))
.first();
// Option<Doc<"users">> instead of Doc<"users"> | null
if (Option.isSome(existing)) {
return yield* Effect.fail(new UserAlreadyExistsError({ username }));
}
const userId = yield* db.insert("users", {
username,
email,
role: "user",
});
return { userId };
}),
});
```
### Confect + Convex Components Pattern
```typescript
// Combine Confect with Convex components using custom services
import { Effect, Context, Layer } from "effect"
import { Agent } from "@convex-dev/agent"
class ConvexAgentService extends Context.Tag("ConvexAgentService")<
ConvexAgentService,
{
readonly createThread: (
ctx: ConfectActionCtx,
userId: string
) => Effect.Effect<string, AgentError>
}
>() {}
const ConvexAgentServiceLive = Layer.effect(
ConvexAgentService,
Effect.gen(function* () {
const agent = new Agent(components.agent, {
chat: openai.chat("gpt-4o"),
instructions: "You are a helpful assistant."
})
return {
createThread: (ctx, userId) =>
Effect.gen(function* () {
// Convert ConfectActionCtx to native Convex context
const nativeCtx = ctx // Confect provides compatible context
const result = yield* Effect.tryPromise({
try: async () => {
const { threadId } = await agent.createThread(nativeCtx, { userId })
return threadId
},
catch: (error) => new AgentError({ cause: error })
})
return result
})
}
})
)
// Confect action using agent service
export const createAgentThread = action({
args: CreateThreadArgs,
returns: CreateThreadResult,
handler: ({ userId }) =>
Effect.gen(function* () {
const agentService = yield* ConvexAgentService
const threadId = yield* agentService.createThread(/* ... */, userId)
return { threadId }
}).pipe(
Effect.provide(ConvexAgentServiceLive)
)
})
```
---
## Complete Production Architecture
### Recommended Service Layer Structure
```
convex/
├── schema.ts # Database schema
├── confect.ts # Confect setup (if using)
│
├── services/
│ ├── agent.service.ts # AgentService layer
│ ├── rag.service.ts # RAGService layer
│ ├── rate-limit.service.ts # RateLimitService layer
│ ├── workflow.service.ts # WorkflowService layer
│ └── layers.ts # Combined layers
│
├── domain/
│ ├── users/
│ │ ├── types.ts # Domain types
│ │ ├── errors.ts # Domain errors
│ │ └── logic.ts # Business logic (Effect)
│ │
│ └── agents/
│ ├── types.ts
│ ├── errors.ts
│ └── orchestration.ts # Multi-agent logic
│
└── api/
├── users.ts # User queries/mutations
├── agents.ts # Agent endpoints
└── workflows.ts # Workflow triggers
```
### Complete Example: Customer Support Agent System
```typescript
// convex/services/agent.service.ts
import { Effect, Context, Layer } from "effect"
import { Agent } from "@convex-dev/agent"
import { openai } from "@ai-sdk/openai"
class SupportAgentService extends Context.Tag("SupportAgentService")<
SupportAgentService,
{
readonly createSupportThread: (
ctx: ActionCtx,
userId: string,
issue: string
) => Effect.Effect<ThreadResult, AgentError>
readonly continueSupportThread: (
ctx: ActionCtx,
threadId: string,
message: string
) => Effect.Effect<AgentResponse, AgentError>
}
>() {}
const SupportAgentServiceLive = Layer.effect(
SupportAgentService,
Effect.gen(function* () {
const ragService = yield* RAGService
const rateLimitService = yield* RateLimitService
const agent = new Agent(components.agent, {
name: "Support Agent",
chat: openai.chat("gpt-4o-mini"),
textEmbedding: openai.embedding("text-embedding-3-small"),
instructions: "You are a helpful customer support agent.",
tools: {
searchKnowledgeBase: createTool({
description: "Search the knowledge base for relevant articles",
args: z.object({ query: z.string() }),
handler: async (toolCtx, { query }) => {
const program = ragService.search(/* ... */, query)
return await Effect.runPromise(program)
}
})
},
usageHandler: async (ctx, args) => {
await ctx.runMutation(internal.billing.trackUsage, args)
}
})
return {
createSupportThread: (ctx, userId, issue) =>
Effect.gen(function* () {
// Check rate limit
const rateLimitOk = yield* rateLimitService.checkUserLimit(ctx, userId)
if (!rateLimitOk) {
return yield* Effect.fail(new RateLimitError())
}
// Fetch user context
const userContext = yield* Effect.tryPromise({
try: () => ctx.runQuery(api.users.getContext, { userId }),
catch: () => new ContextFetchError()
})
// Create thread
const result = yield* Effect.tryPromise({
try: async () => {
const { threadId, thread } = await agent.createThread(ctx, { userId })
const response = await thread.generateText({
prompt: `User issue: ${issue}\n\nUser context: ${JSON.stringify(userContext)}`
})
return { threadId, text: response.text }
},
catch: (error) => new AgentError({ cause: error })
})
return result
}),
continueSupportThread: (ctx, threadId, message) =>
Effect.gen(function* () {
const result = yield* Effect.tryPromise({
try: async () => {
const { thread } = await agent.continueThread(ctx, { threadId })
const response = await thread.generateText({ prompt: message })
return { text: response.text, usage: response.usage }
},
catch: (error) => new AgentError({ cause: error })
})
return result
})
}
})
)
// convex/api/support.ts
export const createSupportTicket = action({
args: { userId: v.string(), issue: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
const supportAgent = yield* SupportAgentService
// Create thread with agent
const threadResult = yield* supportAgent.createSupportThread(
ctx,
args.userId,
args.issue
)
// Save to database
yield* Effect.tryPromise({
try: () => ctx.runMutation(internal.tickets.create, {
userId: args.userId,
threadId: threadResult.threadId,
initialResponse: threadResult.text
}),
catch: () => new DatabaseError()
})
return threadResult
}).pipe(
Effect.provide(Layer.merge(
SupportAgentServiceLive,
RAGServiceLive,
RateLimitServiceLive
)),
Effect.catchAll((error) => {
console.error("Support ticket creation failed:", error)
return Effect.succeed({
error: true,
message: "Failed to create support ticket. Please try again."
})
}),
Effect.runPromise
)
})
```
### Multi-Agent Research System
```typescript
// convex/domain/agents/research-orchestration.ts
export const researchWorkflow = workflow.define({
args: { query: v.string(), userId: v.string() },
handler: async (step, args): Promise<ResearchResult> => {
// Step 1: Classify query type
const classification = await step.runAction(
internal.research.classifyQueryWithEffect,
{ query: args.query },
);
// Step 2: Parallel research with multiple agents
const [webResults, academicResults, newsResults] = await Promise.all([
step.runAction(internal.research.webSearchAgent, { query: args.query }),
step.runAction(internal.research.academicAgent, { query: args.query }),
step.runAction(internal.research.newsAgent, { query: args.query }),
]);
// Step 3: Synthesize findings
const synthesis = await step.runAction(
internal.research.synthesisAgentWithEffect,
{
query: args.query,
webResults,
academicResults,
newsResults,
},
);
// Step 4: Generate final report
const report = await step.runAction(
internal.research.reportGeneratorWithEffect,
{ synthesis, classification },
);
// Step 5: Save and notify
await step.runMutation(internal.research.saveReport, {
userId: args.userId,
query: args.query,
report,
});
return report;
},
});
export const synthesisAgentWithEffect = internalAction({
args: {
/* ... */
},
handler: async (ctx, args) => {
const program = Effect.gen(function* () {
const agentService = yield* AgentService;
const ragService = yield* RAGService;
// Fetch relevant background context
const background = yield* ragService.search(ctx, "global", args.query, 5);
// Synthesize with agent
const synthesis = yield* agentService.generateResponse(
ctx,
`Synthesize these research findings:\n\n${formatResults(args)}\n\nBackground: ${background.text}`,
);
return synthesis;
});
return await Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(AgentServiceLive, RAGServiceLive)),
Effect.retry(
Schedule.exponential("1 second").pipe(
Schedule.compose(Schedule.recurs(3)),
),
),
),
);
},
});
```
---
## Production Patterns Summary
### Error Handling
**Pattern: Layered Error Handling**
```typescript
// Domain errors (typed)
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
userId: string;
}> {}
// Infrastructure errors (typed)
class AgentError extends Data.TaggedError("AgentError")<{
agentName: string;
cause: unknown;
}> {}
// Handle at appropriate layer
Effect.gen(function* () {
const result = yield* agentService.generateResponse(/* ... */);
return result;
}).pipe(
// Handle domain errors specifically
Effect.catchTag("UserNotFoundError", (error) =>
Effect.succeed({ error: "User not found" }),
),
// Handle infrastructure errors with retry
Effect.catchTag("AgentError", (error) =>
Effect.retry(
Effect.fail(error),
Schedule.exponential("1 second").pipe(Schedule.recurs(2)),
),
),
// Catch-all for unexpected errors
Effect.catchAll((error) => {
console.error("Unexpected error:", error);
return Effect.succeed({ error: "Something went wrong" });
}),
);
```
### Monitoring and Observability
**Pattern: Usage Tracking with Effect**
```typescript
class MonitoringService extends Context.Tag("MonitoringService")<
MonitoringService,
{
readonly trackAgentUsage: (data: UsageData) => Effect.Effect<void>;
readonly trackError: (error: unknown) => Effect.Effect<void>;
}
>() {}
const MonitoringServiceLive = Layer.succeed(MonitoringService, {
trackAgentUsage: (data) =>
Effect.sync(() => {
console.log("Agent usage:", data);
// Send to analytics service
}),
trackError: (error) =>
Effect.sync(() => {
console.error("Error tracked:", error);
// Send to error tracking service
}),
});
// Wrap service calls with monitoring
const monitoredAgentCall = (
agentService: AgentService,
ctx: ActionCtx,
prompt: string,
) =>
Effect.gen(function* () {
const monitoring = yield* MonitoringService;
const startTime = Date.now();
const result = yield* agentService.generateResponse(ctx, prompt).pipe(
Effect.tap((response) =>
monitoring.trackAgentUsage({
duration: Date.now() - startTime,
tokenCount: response.usage.totalTokens,
model: "gpt-4o-mini",
}),
),
Effect.tapError((error) => monitoring.trackError(error)),
);
return result;
});
```
### Testing Strategies
**Pattern: Test Layers for Components**
```typescript
// Test implementation of AgentService
const TestAgentServiceLive = Layer.succeed(AgentService, {
generateResponse: (ctx, prompt) =>
Effect.succeed({
text: "Mock response",
usage: { totalTokens: 100, promptTokens: 50, completionTokens: 50 },
}),
createThread: (ctx, userId) => Effect.succeed({ threadId: "test-thread-id" }),
});
// Test with mock
describe("Support Agent", () => {
it("should create support ticket", async () => {
const program = createSupportTicket({ userId: "123", issue: "Help!" });
const result = await Effect.runPromise(
program.pipe(
Effect.provide(
Layer.merge(
TestAgentServiceLive,
TestRAGServiceLive,
TestRateLimitServiceLive,
),
),
),
);
expect(result.threadId).toBe("test-thread-id");
});
});
```
### Resource Management
**Pattern: Scoped Resources with Components**
```typescript
// Use Effect.Scope for resource lifecycle
const withAgentPool = <A, E>(
program: Effect.Effect<A, E, AgentService>,
): Effect.Effect<A, E> =>
Effect.scoped(
Effect.gen(function* () {
// Acquire agents
const agentPool = yield* Effect.acquireRelease(
Effect.sync(() => createAgentPool(10)),
(pool) => Effect.sync(() => pool.shutdown()),
);
// Provide service
const service = yield* AgentService;
// Run program
return yield* program;
}),
);
```
### Performance Optimization
**Pattern: Parallel Execution with Concurrency Control**
```typescript
// Process multiple items with controlled parallelism
export const processBatchWithEffect = action({
args: { items: v.array(v.any()) },
handler: (ctx, args) =>
Effect.gen(function* () {
const results = yield* Effect.all(
args.items.map((item) => processItemWithEffect(ctx, item)),
{
concurrency: 5, // Limit concurrent operations
mode: "default", // Fail fast on first error
},
);
return { results, count: results.length };
}).pipe(Effect.provide(/* ... */), Effect.runPromise),
});
```
---
## Key Recommendations for ONE Network
### Architecture Decisions
1. **Start with Manual Effect Integration**: Don't adopt Confect initially. Use the wrapper patterns shown above to integrate Effect gradually.
2. **Use Components for Infrastructure**: Let Convex components handle durability, retries, and rate limiting. Use Effect for business logic.
3. **Service Layer with Effect**: Build a service layer using Effect's Context and Layer patterns for testability and composition.
4. **Agent Component + Workflow**: Use @convex-dev/agent for AI capabilities and @convex-dev/workflow for long-running operations.
5. **RAG with Effect Services**: Wrap @convex-dev/rag in an Effect service for composable context retrieval.
### Astro Integration
```typescript
// src/lib/convex-provider.tsx
import { ConvexReactClient, ConvexProvider } from "convex/react"
import { type FunctionComponent } from "react"
const convexClient = new ConvexReactClient(import.meta.env.PUBLIC_CONVEX_URL)
export function withConvexProvider<Props>(
Component: FunctionComponent<Props>
) {
return function WithConvexProvider(props: Props) {
return (
<ConvexProvider client={convexClient}>
<Component {...props} />
</ConvexProvider>
)
}
}
// Use in components
export default withConvexProvider(function ChatInterface() {
const messages = useQuery(api.chat.listMessages, { threadId })
const sendMessage = useMutation(api.chat.sendMessage)
// Your UI code
})
```
### Deployment
1. **Convex Deploy**: `npx convex deploy` for backend
2. **Vercel**: Use Vercel for Astro frontend with automatic Convex integration
3. **Environment Variables**: Manage through Convex Dashboard
4. **Monitoring**: Use Convex Dashboard + custom monitoring service
---
## Conclusion
Effect.ts and Confect enhance Convex's already power