How Datapizza uses Services and Layers to make Dual adapt to real enterprise systems.
They fit the company: data, processes, permissions, compliance, internal systems.
They already (not really?) have agents, workflows, guardrails, observability, deployment.
Dependency Injection (DI) is a software design pattern used to create loosely coupled code.
With Effect, your dependencies become explicit, type-safe, testable, and swappable.
export class CapabilityRegistry extends Context.Service< CapabilityRegistry, { readonly list: () => Effect.Effect<ReadonlyArray<Capability>, RegistryUnavailable> readonly search: (query: string) => Effect.Effect<ReadonlyArray<Capability>, RegistryUnavailable> readonly invoke: (request: InvokeCapabilityById) => Effect.Effect<CapabilityResult, CapabilityNotFound | CapabilityInputError | CapabilityInvokeError> }>()("dual/CapabilityRegistry") {}
export const invokeCapabilityById = Effect.fn("invokeCapabilityById")( function* (id: CapabilityId, input: unknown) { const registry = yield* CapabilityRegistry return yield* registry.invoke({ id, input, context: { source: "agent" } }) })// Effect<CapabilityResult, CapabilityError, CapabilityRegistry>
const program = invokeCapabilityById( "google.gmail.search", { q: "from:finance has:attachment" })Effect.runPromise(program)// ^^^^^^^^^// Type 'CapabilityRegistry' is not assignable to type 'never'.
const CapabilityRegistryLive = Layer.effect( CapabilityRegistry, Effect.gen(function* () { const db = yield* Db const dual = yield* Dual.Service return CapabilityRegistry.of({ list: () => listCapabilities(db), search: (query) => searchCapabilities(db, query), invoke: ({ id, input, context }) => Effect.gen(function* () { const capability = yield* findCapability(db, id) const decoded = yield* decodeInput(capability, input) return yield* dual.invoke({ actionId: capability.actionId, input: decoded, context }) }) }) }))
const Live = CapabilityRegistryLive.pipe( Layer.provide(Layer.mergeAll(DbLive, DualLive)))const runnable = program.pipe( Effect.provide(Live))await Effect.runPromise(runnable)
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
// contract: route + payload + success + errorsexport class UsersApiGroup extends HttpApiGroup.make("users") .add( HttpApiEndpoint.get("search", "/search", { payload: { search: Schema.String }, success: Schema.Array(User), error: SearchQueryTooShort }) ) .prefix("/users") {}
// implementation: handler group as a Layerexport const UsersApiHandlers = HttpApiBuilder.group( Api, "users", Effect.fn(function* (handlers) { const users = yield* Users return handlers.handle("search", ({ payload }) => users.search(payload.search) ) }))const ApiRoutes = HttpApiBuilder.layer(Api).pipe( Layer.provide(UsersApiHandlers))
// contract: provider + Gmail send capabilityexport const gmailSend = Action.make("send", { name: "Send Gmail message", description: "Send an email through Gmail"}).pipe( Action.withVersion("v1", { input: Schema.Struct({ to: Schema.String, subject: Schema.String, body: Schema.String }), output: Schema.Struct({ messageId: Schema.String }) }))export const googleProvider = Provider.make("google").pipe( Provider.withAuthMethod(googleOauth), Provider.add(Group.make("gmail").pipe(Group.add(gmailSend))))
// implementation: Gmail handlers provided as a Layerexport const GoogleLive = Provider.toLayer( googleProvider, googleProvider.of({ actions: { gmail: { send: { v1: Effect.fn("@dual/google/gmail.send")( function* ({ to, subject, body }, context) { const gmail = yield* GmailClient const messageId = yield* gmail.sendMessage({ accessToken: context.auth.current.value.accessToken, to, subject, body }) return { messageId } } ) } } } }))
Enterprise AI is mostly about integrating with the boundaries a company already has.
Great. Now build it around our systems.
if (client === "azure-bank") { queue = new RedisQueue() secrets = new KeyVault() sandbox = new PrivateSandbox() model = new AzureOpenAI()}if (client === "another-enterprise") { // repeat forever...}
import { Dual } from "@dual/sdk"const dual = Dual.make(app)const runServer = dual.runServer({ port: 3825 })await Effect.runPromise(runServer)// ~~~~~~~~~// ts(2345) Argument of type// Effect<never, ServerStartError,// QueueService | CredentialsService | ModelGateway |// SandboxService | StaticServing | Telemetry// > is not assignable to Effect<never, unknown, never>.//// Missing requirements:// QueueService | CredentialsService | ModelGateway |// SandboxService | StaticServing | Telemetry
const ClientLive = Layer.mergeAll( RedisQueue.layer, KeyVaultCredentials.layer, AzureModelGateway.layer, PrivateSandbox.layer, S3StaticServing.layer, SentryTelemetry.layer,)await Effect.runPromise( runServer.pipe( Effect.provide(ClientLive), Effect.orDie, ))