Lesson 10 · Effect AgentCore · Appendix

Authoring the AgentCore resource

Every deploy lesson yield*-ed a given resource — Runtime, Gateway, Memory — as if Alchemy had always shipped them. It doesn't; the course does. This appendix opens the hood: how an Alchemy resource is authored against the AWS control-plane SDK, the same way Alchemy authors its own.

01Why this is given, and why it's worth seeing

Alchemy has no AgentCore resource yet, and no CloudFormation escape hatch to borrow one — its resources are hand-authored against the AWS API. So the course ships AgentCore.Runtime as library code you consume like any other resource. You never have to read this lesson to finish the course. But authoring a resource is the moment Alchemy stops being magic: a resource is just an Effect that reconciles desired state against what AWS actually has. Once you've seen it, the alpha frontier stops being a wall.

given/agentcore/Runtime.ts

02The shape: a Resource and its Provider

Every Alchemy resource is two values. A Resource is a typed handle — a name, its props, its output attributes. A Provider is the lifecycle that backs it: how to read current state, decide what changed, converge it, and delete it. This is exactly how Alchemy authors its own resources; the canonical reference is its SQS queue:

// the framework's own pattern — alchemy/AWS/SQS/Queue.ts
export const Queue = Resource<Queue>("AWS.SQS.Queue");

export const QueueProvider = () =>
  Provider.effect(Queue, Effect.gen(function* () {
    const region = yield* Region;
    return Queue.Provider.of({
      stables: ["queueName", "queueUrl", "queueArn"],
      read:      Effect.fn(function* ({ output }) { /* GetQueueUrl */ }),
      diff:      Effect.fn(function* ({ news, olds }) { /* replace vs update */ }),
      reconcile: Effect.fn(function* ({ news, output }) { /* observe → ensure → sync */ }),
      delete:    Effect.fn(function* ({ output }) { /* DeleteQueue */ }),
    });
  }));

Our AgentCore.Runtime is the same skeleton with a different SDK underneath. The RuntimeProps (container URI, role ARN, network) and RuntimeOutput (agentRuntimeArn, agentRuntimeId, status) are already defined in the given file — what remains is the provider.

alchemy/AWS/SQS/Queue.ts — the canonical Resource+Provider shape

03The control plane is native Effect

The SDK we author against — @distilled.cloud/aws/bedrock-agentcore-control — returns Effects, not Promises. Each operation is an OperationMethod requiring Credentials | Region | HttpClient, which the provider already has in scope:

import * as control from "@distilled.cloud/aws/bedrock-agentcore-control";

control.createAgentRuntime(req)  // Effect<CreateAgentRuntimeResponse, CreateAgentRuntimeError, Credentials | Region | HttpClient>
control.getAgentRuntime(req)     // Effect<GetAgentRuntimeResponse, ...>
control.deleteAgentRuntime(req)  // Effect<DeleteAgentRuntimeResponse, ...>
A deliberate divergence from the "use pattern"

This course's use pattern exists to wrap Promise-based SDKs — Effect.tryPromise, a service tag, a layer. Here there is nothing to wrap: createAgentRuntime is already an Effect with typed errors and its dependencies in the requirement channel. So we call it directly. The pattern still applies the moment you reach a Promise SDK; this SDK simply spares you it. Worth saying out loud, because it's the rare case where "wrap your client" is the wrong advice.

04Reconcile: create, read, converge

The heart of a provider is reconcile — given the desired props and any prior output, make AWS match. The shape for the Runtime: look it up; if it doesn't exist, create it; if it does, it's already converged. createAgentRuntime takes the container artifact, the role, and a network configuration:

reconcile: Effect.fn(function* ({ id, news, output }) {
  // observe: does it already exist?
  if (output?.agentRuntimeId) {
    const got = yield* control.getAgentRuntime({ agentRuntimeId: output.agentRuntimeId });
    return { agentRuntimeArn: got.agentRuntimeArn, agentRuntimeId: got.agentRuntimeId, status: got.status };
  }
  // ensure: create it
  const created = yield* control.createAgentRuntime({
    agentRuntimeName: id,
    agentRuntimeArtifact: { containerConfiguration: { containerUri: news.containerUri } },
    roleArn: news.roleArn,
    networkConfiguration: { networkMode: news.network ?? "PUBLIC" },
    environmentVariables: news.environment,
  });
  return {
    agentRuntimeArn: created.agentRuntimeArn,
    agentRuntimeId: created.agentRuntimeId,
    status: created.status,             // CREATING → READY
  };
}),

read (used to detect drift outside Alchemy) is just the getAgentRuntime branch; delete is deleteAgentRuntime({ agentRuntimeId }), catching "already gone" the way Queue catches QueueDoesNotExist.

Alpha — verify the request shape

agentRuntimeArtifact, networkConfiguration, and the exact create/get/delete field names are the young part of this API. The names above follow @distilled.cloud/aws's CreateAgentRuntimeRequest (agentRuntimeName, agentRuntimeArtifact, roleArn, networkConfiguration) — but pin the version and read the installed .d.ts before trusting the nested shapes. This is exactly why the resource is given and dated: you patch one file, not ten lessons.

05diff and stables: when to replace

Some changes can be applied in place; others require tearing the resource down and rebuilding it. stables lists the output attributes that identify the resource — change an input that would alter them, and Alchemy must replace rather than update. For the Runtime:

stables: ["agentRuntimeArn", "agentRuntimeId"],

diff: Effect.fn(function* ({ news, olds }) {
  // a new container image is an in-place update; changing the role identity replaces
  if (news.roleArn !== olds.roleArn) return { action: "replace" } as const;
  return undefined;   // otherwise: update in place (new image digest, env vars)
}),

This is what made "re-run alchemy deploy" behave sensibly back in lesson 05: an unchanged stack is a no-op because diff returns nothing; a new image updates in place; a change to an identity-bearing input forces a clean replace. The idempotency you relied on is this function.

control ops: @distilled.cloud/aws/bedrock-agentcore-controlcreateAgentRuntime, getAgentRuntime, deleteAgentRuntime (+ createGateway, createMemory for the sibling resources)

06Registering it — full circle

A provider is a Layer. To make yield* Runtime("Agent", {...}) resolve inside the stack, its provider joins the collection alongside the AWS providers:

// the stack from lessons 04–08
export default Alchemy.Stack("EffectAgentCore",
  {
    providers: Layer.mergeAll(AWS.providers(), RuntimeProvider(), GatewayProvider(), MemoryProvider()),
    state: Alchemy.localState(),
  },
  Effect.gen(function* () {
    const runtime = yield* Runtime("Agent", { containerUri, roleArn });   // ← now resolvable
    // ...
  }),
);
Aha

There was never a privileged layer. The AgentCore.Runtime you've been deploying since lesson 05 is the same kind of value as AWS.ECR.Repository — a Resource plus a Provider that reconciles desired state against the SDK. "Given" meant "pre-written," not "special." You could author the next AWS resource AWS hasn't gotten to yet, the same way.

That closes the course. You built an Effect-docs agent as an HttpApi, gave it a model, deployed it to AgentCore, grew it with a Gateway tool and Memory, made it observable, and shipped the whole thing — site included — from one Effect program. And now the one piece that looked like infrastructure-magic is just more of the same program.