Immutable. This exact content is served forever at /api/v1/blob/8426de09011940c0.
---
name: effect-ts-patterns
description: Mandatory Effect-TS coding patterns for the Reactive Agents framework. Use when writing any TypeScript code, creating services, defining types, handling errors, or composing layers in this project.
user-invocable: false
---
# Effect-TS Patterns — Reactive Agents
Every file in this project MUST follow these patterns. Violations will break the build and type system.
## Types: Schema.Struct (NEVER plain interfaces)
```typescript
import { Schema } from "effect";
// Branded IDs
export const AgentId = Schema.String.pipe(Schema.brand("AgentId"));
export type AgentId = typeof AgentId.Type;
// Struct definitions
export const AgentSchema = Schema.Struct({
id: AgentId,
name: Schema.String,
status: Schema.Literal("idle", "running", "completed", "failed"),
description: Schema.optional(Schema.String),
tags: Schema.Array(Schema.String),
metadata: Schema.optional(
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
),
});
export type Agent = typeof AgentSchema.Type;
```
**Rules:**
- ALWAYS use `Schema.Struct` for data shapes — never `interface` or `type { ... }`
- ALWAYS use `Schema.brand()` for ID types
- ALWAYS use `Schema.Literal()` for union/enum values
- ALWAYS use `Schema.optional()` for optional fields — never `?:`
- ALWAYS derive the TypeScript type with `typeof XxxSchema.Type`
## Errors: Data.TaggedError (NEVER throw)
```typescript
import { Data } from "effect";
export class AgentError extends Data.TaggedError("AgentError")<{
readonly message: string;
}> {}
export class TaskError extends Data.TaggedError("TaskError")<{
readonly message: string;
readonly taskId: string;
}> {}
// Union type for service signatures
export type CoreErrors = AgentError | TaskError;
```
**Rules:**
- NEVER use `throw new Error()`
- ALWAYS use `Data.TaggedError("UniqueTag")<{ ... }>`
- The tag string MUST match the class name exactly
- All fields MUST be `readonly`
- ALWAYS create a union type for each package's errors
## Services: Context.Tag + Layer.effect (NEVER OOP classes)
```typescript
import { Effect, Context, Layer, Ref } from "effect";
// Step 1: Define the service tag with its interface
export class MyService extends Context.Tag("MyService")<
MyService,
{
readonly doWork: (input: string) => Effect.Effect<string, MyError>;
readonly getState: () => Effect.Effect<ReadonlyMap<string, string>, never>;
}
>() {}
// Step 2: Create the Live layer
export const MyServiceLive = Layer.effect(
MyService,
Effect.gen(function* () {
// Resolve dependencies
const dep = yield* OtherService;
// Create mutable state via Ref
const state = yield* Ref.make(new Map<string, string>());
// Return the service implementation
return {
doWork: (input) =>
Effect.gen(function* () {
yield* Ref.update(state, (m) => new Map(m).set(input, input));
return yield* dep.process(input);
}),
getState: () => Ref.get(state),
};
}),
);
```
**Rules:**
- Services ALWAYS extend `Context.Tag("ServiceName")<ServiceTag, Interface>()`
- The tag string MUST match the class name
- Implementations ALWAYS use `Layer.effect(Tag, Effect.gen(...))`
- State ALWAYS uses `Ref` — never mutable variables
- Dependencies are resolved with `yield* OtherService` inside `Effect.gen`
- All methods return `Effect.Effect<Success, Error>`
## Scoped Resources: Layer.scoped for cleanup
```typescript
export const MemoryDatabaseLive = Layer.scoped(
MemoryDatabase,
Effect.acquireRelease(
Effect.sync(() => {
const db = new Database(dbPath, { create: true });
db.exec("PRAGMA journal_mode=WAL");
return db;
}),
(db) => Effect.sync(() => db.close()),
).pipe(
Effect.map((db) => ({
query: db.query.bind(db),
exec: db.exec.bind(db),
close: () => Effect.sync(() => db.close()),
})),
),
);
```
**Rules:**
- Use `Layer.scoped` + `Effect.acquireRelease` for resources needing cleanup (DB, files, connections)
- Acquire returns the resource; release cleans it up
- Both acquire and release must be wrapped in `Effect.sync` or `Effect.tryPromise`
## Synchronous Operations: Effect.sync
```typescript
// bun:sqlite is synchronous — use Effect.sync
const rows = yield * Effect.sync(() => db.query("SELECT * FROM t").all());
// Pure computations
const value = yield * Effect.sync(() => computeHash(input));
```
## Async Operations: Effect.tryPromise
```typescript
// HTTP calls, file I/O, external APIs
const data =
yield *
Effect.tryPromise({
try: () => fetch(url).then((r) => r.json()),
catch: (e) => new FetchError({ message: String(e) }),
});
```
**Rules:**
- ALWAYS provide a `catch` function that returns one of your `Data.TaggedError` types
- NEVER use raw `await`
## Layer Composition: Layer.mergeAll + Layer.provide
```typescript
// Merge independent layers
export const createMyLayer = () => Layer.mergeAll(ServiceALive, ServiceBLive);
// Provide dependencies from one layer to another
export const createMyLayer = () =>
Layer.mergeAll(ServiceALive, ServiceBLive.pipe(Layer.provide(ServiceALive)));
// Full package layer factory
export const createCoreLayer = () =>
Layer.mergeAll(
EventBusLive,
AgentServiceLive.pipe(Layer.provide(EventBusLive)),
TaskServiceLive.pipe(Layer.provide(EventBusLive)),
ContextWindowManagerLive,
);
```
**Rules:**
- Every package exports a `createXxxLayer()` factory function
- Use `Layer.provide()` to wire dependencies between services
- Use `Layer.mergeAll()` to combine independent services
- The factory function takes configuration params if needed
## Optional Dependencies: Effect.serviceOption
```typescript
import { Context } from "effect";
// When a dependency may not be present in the runtime
const maybeReasoning =
yield *
Effect.serviceOption(
Context.GenericTag<ReasoningServiceInterface>("ReasoningService"),
);
const result = Option.match(maybeReasoning, {
onNone: () => executeDefaultPath(),
onSome: (svc) => svc.selectStrategy(context),
});
```
## Pattern Checklist
Before committing any file, verify:
- [ ] All types use `Schema.Struct`, not interfaces (exception: strategy input interfaces with Effect types)
- [ ] All IDs use `Schema.brand()`
- [ ] All errors use `Data.TaggedError`
- [ ] All services use `Context.Tag` + `Layer.effect`
- [ ] All state uses `Ref`
- [ ] No `throw`, no raw `await`, no mutable variables
- [ ] No `@ts-ignore` or `@ts-expect-error` — ever
- [ ] `as any` only in test mocks or untyped meta bags (comment why)
- [ ] New behavior emits EventBus events
- [ ] Computable fields are deterministic (not LLM-generated)
- [ ] Package exports a `createXxxLayer()` factory
- [ ] `index.ts` re-exports all public types, errors, services, and the layer factory
- [ ] File names kebab-case, types PascalCase, functions camelCase
See `CODING_STANDARDS.md` for the full authoritative reference.