Repository navigation
refactor: build services with make and layer, and never pass a service as an argument - #24
Merged
humanlayerdev[bot] merged 4 commits intoOct 15, 2026
Conversation
…e as an argument
Each service now gets its dependencies with `yield*` in a `make`, and its layer is built from that
make. No function takes a service instance as a parameter; a make's arguments are only plain config,
such as options, handler functions, and provider processors.
Delivery:
- `MailboxProcessing.make(options, processors)` and `MailboxProcessing.layer(options, processors)`
replace `MailboxProcessingLive`, `makeMailboxProcessing`, and `makeMailboxProcessingWith`. The make
leaves what the processors need open, for hosts that supply it per call; the layer takes it once.
- The `ProviderEventDispatcher` and `ProviderOutputDispatcher` tags, with their `Live` and `make`
versions, are gone. `processProviderOutput(processors)` sits beside `processProviderEvent`.
- `processClaim`, `processOutputClaim`, and `claimOrDeferReadyMailbox` read the backend and `Crypto`
from the context; the `*With` variants that took them as arguments are gone, as are
`makeBatchIdWith`, `makeDeliveryAccessTokenWith`, and `makeDeliveryIdempotencyKeyWith`.
- `ChannelsProvider` loses `runtimeEventProcessor` and `runtimeOutputProcessor`: providers have one
event processor and one output processor, which leave `process` requirements open.
- `DeliveryControlLive` is now `DeliveryControl.layer`, built from the class's `make`.
Providers:
- `GitHubBot.make`, `SlackBot.make`, and `LinearBot.make` no longer build their API layer: the
`gitHubApi`, `slackApi`, and `linearApi` options are gone, and the application provides
`GitHubApiLive`, `SlackApiLive`, or `LinearApiLive` once.
- The `GitHubCallbacks`, `SlackCallbacks`, and `LinearCallbacks` tags are gone. Event processors take
the handlers (`make*EventProcessor({ ..., handlers })`) and wrap them with `make*Callbacks`.
- Output processors read the provider's API on each call instead of when they are built.
- `GitHubApiClient`, `GitHubAppCredentials`, `GitHubGitCredentials`, `SlackHttpClient`, and
`LinearHttpClient` carry their `make` and `static layer`; the `*Live` constants are gone.
- `makeFixedCredentialLinearHttpClient({ organizationId, accessToken })` reads `HttpClient` from the
context. `LinearCredentialResolver` and `LinearVerifiedCredential` move to `LinearCredential.ts`
(still re-exported) to break an import loop.
Cloudflare:
- `makeMailboxAlarmHandler(options)` reads `MailboxProcessing` on each pass; the
`makeMailboxAlarmHandlerWith(options, processing)` variant is gone. `ChannelsCloudflare.make`'s
`mailbox()` supplies processing to each alarm from that call's services.
- The example's Worker already provided `GitHubApiLive`; it is now the only GitHub client. The
mailbox object logs why it could not start before it dies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a1418a-45ea-7a36-87bf-d2e8c4e465fe
`bun run format` now passes across the repo. Layout only; no code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a1418a-45ea-7a36-87bf-d2e8c4e465fe
A single-implementation service now builds itself in its tag's `make` option, with `static readonly layer = Layer.effect(this, this.make)`. No module-level constant or layer builds a service that the tag then wraps. - GitHubApi, SlackApi, LinearApi: the implementations that lived in GitHubApiOperationsLive, SlackApiService, and makeLinearApi move into the tags. Each tag module's errors, schemas, and request types move to GitHubApiContract, SlackApiContract, and LinearApiContract, which the operation modules import, so the tag module can import the operations without a cycle; the tag modules re-export them. SlackApiWire holds the Slack response schemas and plain helpers. - The Linear and Slack resource classes read their API through LinearApiHandle and SlackApiHandle, which share the tag's key, since the tags' constructors build those classes. - SlackHttpClient and LinearHttpClient build in `make`. A fixed-credential Linear client is `LinearHttpClient.makeFixedCredential` and `layerFixedCredential`, replacing `makeFixedCredentialLinearHttpClient`. - GitHubAppSigner builds in `make`; `layerWebCrypto` is now `layer`. - MailboxStorage builds in `make`; `MailboxStorageFromDurableObjectState` is now `MailboxStorage.layer`. - Example: Workspace, AutoLabel, RunRecovery, ActiveDelivery, DeliveryTurns, SessionExpiry, DeliveryApi, AgentSessions, and AgentConversation build in `make`. AgentConversation's implementation moves from AgentSessionDO into AgentConversation.ts with its prompt and tools; the message schema moves to AgentSessionMessage.ts and ActiveDelivery to ActiveDelivery.ts, so the example has no import cycles. Tags with several implementations (the storage ports, LinearCredentialResolver) keep a named constructor and layer per implementation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a1418a-45ea-7a36-87bf-d2e8c4e465fe
…xStorage.layer's type named The alchemy preview build pinned for auto-labeling (pkg.alchemy.run pr:2064:a8c9ecc) is gone, so `bun install --frozen-lockfile` failed on every CI job. Use the published 2.0.0-beta.83. `MailboxStorage.layer` lost the named type the old `MailboxStorageFromDurableObjectState` carried, so the emitted declarations referred to `alchemy/Cloudflare/index`, which alchemy does not export, and release validation failed. `MailboxStorageLayer` names it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a1418a-45ea-7a36-87bf-d2e8c4e465fe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every service now follows one pattern: a
makegets its dependencies withyield*, and its layer is built from that make (Layer.effect(this, this.make)). No function takes a service instance as an argument. A make's arguments are only plain config, such as options, the app's handler functions, and the providers' processors.Each bot used to build its own API client (
Layer.build(GitHubApiLive)insideGitHubBot.make), so one app could hold several GitHub clients, each with its own installation-token cache. Now the app provides each API once.Breaking changes
Delivery
MailboxProcessing.make(options, processors)andMailboxProcessing.layer(options, processors)replaceMailboxProcessingLive,makeMailboxProcessing, andmakeMailboxProcessingWith. The make leaves what the processors need inprocessReady's requirements, so a host can supply it on each call, as a Durable Object alarm must. The layer takes it once, when built.ProviderEventDispatcherandProviderOutputDispatcherservices, with theirLiveandmakeversions, are removed.processProviderOutput(processors)sits besideprocessProviderEvent.processClaimandprocessOutputClaimtakeprocessorsin their input and read the backend andCryptofrom the context. Every*With(service, …)function is removed, includingmakeBatchIdWith,makeDeliveryAccessTokenWith, andmakeDeliveryIdempotencyKeyWith.ChannelsProviderdropsruntimeEventProcessorandruntimeOutputProcessor. Each provider has one event processor and one output processor, and both leaveprocessrequirements open.DeliveryControlLiveis nowDeliveryControl.layer.Providers (GitHub, Slack, Linear)
gitHubApi,slackApi, andlinearApioptions are removed. The app providesGitHubApiLive,SlackApiLive, orLinearApiLive(ormakeLinearApiLive(...)). The README samples show where.GitHubCallbacks,SlackCallbacks, andLinearCallbacksservices are removed. Event processors take the handlers, as inmakeGitHubEventProcessor({ namespace, bot, handlers }), and wrap them withmakeGitHubCallbacksand its Slack and Linear versions.GitHubApiClient,GitHubAppCredentials,GitHubGitCredentials,SlackHttpClient, andLinearHttpClientcarry their ownmakeandstatic layer. Their*Liveconstants are removed;GitHubApiLive,SlackApiLive, andLinearApiLivestay.makeFixedCredentialLinearHttpClient({ organizationId, accessToken })readsHttpClientfrom the context.LinearCredentialResolvermoved toLinearCredential.tsto break an import loop, and is still re-exported.SlackApiis abuildrequirement.Cloudflare
makeMailboxAlarmHandler(options)readsMailboxProcessingon each pass;makeMailboxAlarmHandlerWithis removed.mailbox()supplies processing to each alarm from that call's services.layers.mailbox.processingnow needs the providers' APIs when built. A split Durable Object doesn't use it; the example and README don't either.Constructors live on the tag
Every single-implementation service builds itself in its tag's
makeoption, withstatic readonly layer = Layer.effect(this, this.make). No module-level constant or layer builds a service that the tag then wraps.GitHubApi,SlackApi,LinearApi: their implementations moved into the tags. Each tag module's errors, schemas, and request types moved to*ApiContract.tsmodules, which the operation modules import, so the tag module can import the operations without a cycle. The tag modules re-export the contracts, so public exports don't change.LinearApiHandleandSlackApiHandle, which share the tag's key, because the tags' constructors build those classes. GitHub's resource module still forms a cycle withGitHubApi, but every module loads safely whichever is imported first.LinearHttpClient.makeFixedCredential/layerFixedCredentialreplacemakeFixedCredentialLinearHttpClient.GitHubAppSigner.layerreplaceslayerWebCrypto.MailboxStorage.layerreplacesMailboxStorageFromDurableObjectState.make:Workspace,AutoLabel,RunRecovery,ActiveDelivery,DeliveryTurns,SessionExpiry,DeliveryApi,AgentSessions, andAgentConversation.AgentConversation's implementation moved fromAgentSessionDO.tsintoAgentConversation.ts, with its prompt and tools.AgentSessionMessage.tsandActiveDeliverytoActiveDelivery.ts, so the example has no import cycles.LinearCredentialResolver) keep a named constructor and layer per implementation.main: a load-order cycle betweenSlackApiContractandSlackModels(formerlySlackApiandSlackModels). ImportingSlackModelsfirst fails there too. This PR doesn't change it.Example app
GitHubApiLive, and that is now the app's only GitHub client.CI fix: Alchemy version
The Alchemy preview build pinned for auto-labeling (
pkg.alchemy.run/alchemy/pr:2064:a8c9ecc) is gone, sobun install --frozen-lockfilefailed on every job. The catalog now pins the publishedalchemy@2.0.0-beta.83.MailboxStorage.layeris typedMailboxStorageLayer, so the built type files keep referring toalchemy/Cloudflare, not to its internalalchemy/Cloudflare/indexpath.Also in this PR
A second commit formats the 25 files
bun run formatalready flagged before this change, so the format check passes. It changes layout only.Checks
bun run check(lint, typecheck, scripts typecheck, typed-lint): passes.bun run format: passes.bun run test: 742 tests pass.🤖 Generated with Claude Code