Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Effect-TS/discord-bot/llms.txt
Use this file to discover all available pages before exploring further.
The codebase uses Effect 4.x idioms consistently across every feature. This page catalogs each pattern with real examples from the source.
ServiceMap.Service — defining services
Every injectable service is a class that extends ServiceMap.Service. The class body holds the make factory (an Effect.gen block), and a static layer property that wraps it in a Layer.
// packages/discord-bot/src/Ai.ts
export class AiHelpers extends ServiceMap.Service<AiHelpers>()(
"app/AiHelpers",
{
make: Effect.gen(function* () {
const rest = yield* DiscordREST
const model = yield* ChatModel
const application = yield* DiscordApplication
// ...
return { generateTitle, generateDocs, generateSummary, generateAiInput } as const
}),
},
) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(OpenAiLive),
)
}
Other services that follow the same pattern: Github, EffectRepo, Summarizer, ChannelsCache, MemberCache, Messages, DiscordApplication.
Layer — dependency injection and composition
Effect Layers are the dependency-injection mechanism. The bot uses several Layer combinators.
Layer.effectDiscard — for features that register gateway handlers or slash commands and return nothing:
// packages/discord-bot/src/AutoThreads.ts
export const AutoThreadsLive = Layer.effectDiscard(make).pipe(
Layer.provide(ChannelsCache.layer),
Layer.provide(AiHelpers.layer),
Layer.provide(DiscordGatewayLayer),
)
Layer.effect — for services whose value is consumed by other layers:
// packages/discord-bot/src/Summarizer.ts
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(ChannelsCache.layer),
Layer.provide(MemberCache.layer),
Layer.provide(Messages.layer),
Layer.provide(DiscordGatewayLayer),
)
Layer.provide — wires a dependency into a layer:
// packages/discord/src/DiscordGateway.ts
const DiscordLayer = DiscordIxLive.pipe(
Layer.provideMerge(NodeHttpClient.layerUndici),
Layer.provide(NodeSocket.layerWebSocketConstructor),
Layer.provide(DiscordConfigLayer),
)
Layer.mergeAll — combines multiple independent feature layers in main.ts:
const MainLive = Layer.mergeAll(
AiResponse,
AutoThreadsLive,
DocsLookupLive,
Summarizer.layer,
// ...
).pipe(Layer.provide(TracerLayer("discord-bot")), Layer.provide(LogLevelLive))
Effect.gen — generator-style composition
Effect.gen lets you write sequential Effect logic using yield* for a readable, imperative style. Every make factory and most handlers use it.
// packages/discord-bot/src/Summarizer.ts
make: Effect.gen(function* () {
const rest = yield* DiscordREST
const channels = yield* ChannelsCache
const registry = yield* InteractionsRegistry
const members = yield* MemberCache
const messagesService = yield* Messages
// ...
yield* registry.register(ix)
return { thread: summarizeThread, messages: summarizeWithMessages, message: summarizeMessage } as const
}),
Effect.fn / Effect.fnUntraced — named traced functions
Effect.fn wraps a generator function and attaches an OpenTelemetry span automatically. Effect.fnUntraced skips the span for hot-path helpers.
// packages/discord-bot/src/Ai.ts
const getOpeningMessage = Effect.fn("AiHelpers.getOpeningMessage")(
function* (thread: Discord.GuildChannelResponse | Discord.ThreadResponse) {
if (thread.parent_id == null) {
return yield* rest.getMessage(thread.id, thread.id)
}
return yield* rest
.getMessage(thread.parent_id, thread.id)
.pipe(Effect.catch(() => rest.getMessage(thread.id, thread.id)))
},
)
// packages/discord-bot/src/Summarizer.ts
const summarizeThread = Effect.fn("Summarizer.summarizeThread")(
function* (channel: Discord.ThreadResponse, small: boolean = true) {
const parentChannel = yield* channels.get(channel.guild_id!, channel.parent_id!)
const threadMessages = yield* Stream.runCollect(
messagesService.cleanForChannel(channel),
).pipe(Effect.map((items) => [...items].toReversed()))
return yield* summarize(parentChannel, channel, threadMessages, small)
},
)
Effect.fnUntraced is used for gateway event handlers where the outer span comes from Effect.withSpan:
// packages/discord-bot/src/AutoThreads.ts
const handleMessages = gateway.handleDispatch(
"MESSAGE_CREATE",
Effect.fnUntraced(
function* (event) { /* ... */ },
(effect, event) =>
Effect.withSpan(effect, "AutoThreads.handleMessages", {
attributes: { messageId: event.id },
}),
Effect.catchCause(Effect.logError),
),
)
Config / ConfigProvider — environment variables
All configuration is read through Effect’s Config module, keeping secrets out of code.
// packages/discord/src/DiscordConfig.ts
export const DiscordConfigLayer = DiscordConfig.layerConfig({
token: Config.redacted("DISCORD_BOT_TOKEN"),
gateway: {
intents: Config.succeed(
Intents.fromList(["GuildMessages", "MessageContent", "Guilds"]),
),
},
})
// packages/discord-bot/src/Ai.ts
export const OpenAiLive = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENAI_API_KEY"),
// ...
})
ConfigProvider.nested + ConfigProvider.constantCase — AutoThreads scopes its config under a namespace so all variables are prefixed AUTOTHREADS_:
// packages/discord-bot/src/AutoThreads.ts
Effect.provideService(
ConfigProvider.ConfigProvider,
ConfigProvider.fromEnv().pipe(
ConfigProvider.nested("autothreads"),
ConfigProvider.constantCase,
),
)
// reads AUTOTHREADS_KEYWORD from the environment
Data.TaggedError — typed error classes
Data.TaggedError creates nominal error types with a _tag discriminant, making error handling exhaustive and precise.
// packages/discord-bot/src/AutoThreads.ts
export class NotValidMessageError extends Data.TaggedError(
"NotValidMessageError",
)<{
readonly reason: "non-default" | "from-bot" | "non-text-channel" | "disabled"
}> {}
export class PermissionsError extends Data.TaggedError("PermissionsError")<{
readonly action: string
readonly subject: string
}> {}
// packages/discord-bot/src/Summarizer.ts
export class NotInThreadError extends Data.TaggedError("NotInThreadError")<{}> {}
Tagged errors are caught by tag in Ix.builder:
Ix.builder
.add(command)
.catchTagRespond("NotInThreadError", () =>
Effect.succeed(
Ix.response({
type: Discord.InteractionCallbackTypes.CHANNEL_MESSAGE_WITH_SOURCE,
data: { content: "This command can only be used in a thread", flags: Discord.MessageFlags.Ephemeral },
}),
),
)
.catchAllCause(Effect.logError)
Effect.withSpan — OpenTelemetry tracing
Effect.withSpan manually annotates an effect with a span name and optional attributes. It composes with Effect.fn (which adds spans automatically) for fine-grained traces.
// packages/discord-bot/src/Summarizer.ts
const summarizeWithMessages = (
channel: Discord.ThreadResponse,
messages: Array<Discord.MessageResponse>,
small = true,
) =>
pipe(
channels.get(channel.guild_id!, channel.parent_id!),
Effect.flatMap((parentChannel) =>
summarize(parentChannel, channel, messages, small),
),
Effect.withSpan("Summarizer.summarizeWithMessages"),
)
// packages/discord-bot/src/AiResponse.ts
Effect.withSpan(effect, "AiResponse.generate (inner)"),
Span attributes are annotated on the current span with Effect.annotateCurrentSpan:
yield* Effect.annotateCurrentSpan({ channelId: channel.id, small })
FiberMap — managing concurrent fibers by key
FiberMap tracks running fibers by a key, preventing duplicate work and allowing cancellation by key.
// packages/discord-bot/src/AiResponse.ts
const fiberMap = yield* FiberMap.make<Discord.Snowflake>()
// Start (or replace) the AI generation fiber for this interaction
yield* FiberMap.run(fiberMap, context.id, generate(context, history, reasoning))
// Cancel the fiber when the user clicks "Cancel"
yield* FiberMap.remove(fiberMap, interactionId)
Schedule — retry and recurrence
Schedule drives retries and periodic tasks.
// packages/discord-bot/src/Ai.ts — exponential backoff on transient HTTP errors
export const OpenAiLive = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENAI_API_KEY"),
transformClient: HttpClient.retryTransient({
times: 3,
schedule: Schedule.exponential(500),
}),
})
// packages/discord-bot/src/EffectRepo.ts — pull the git repo every 15 minutes
yield* Effect.gen(function* () {
while (true) {
yield* Effect.sleep("15 minutes")
yield* git.pull(repoPath)
yield* RcRef.invalidate(llmsMd)
}
}).pipe(Effect.retry(Schedule.forever), Effect.forkScoped)