1import * as control from "@distilled.cloud/aws/bedrock-agentcore-control";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import {
11 AgentCoreProvisioningFailed,
12 createAgentCoreName,
13 readAgentCoreTags,
14 retryWhileConflict,
15 retryWhileValidation,
16 syncAgentCoreTags,
17 unredact,
18} from "./internal.ts";
19
20/**
21 * The deployable artifact of an agent runtime — a container image in ECR or
22 * a managed-runtime code bundle in S3. Passed through to the AgentCore API
23 * unchanged — see the AWS SDK `AgentRuntimeArtifact` shape.
24 */
25export type RuntimeArtifact = control.AgentRuntimeArtifact;
26
27export interface RuntimeProps {
28 /**
29 * Name of the agent runtime. Must match `[a-zA-Z][a-zA-Z0-9_]{0,47}`
30 * (underscores, no hyphens). If omitted, a deterministic physical name is
31 * generated from the app, stage, and logical ID. Changing the name triggers
32 * a replacement.
33 */
34 agentRuntimeName?: string;
35 /**
36 * A description of the agent runtime.
37 */
38 description?: string;
39 /**
40 * The deployable artifact: `{ containerConfiguration: { containerUri } }`
41 * for an ECR image, or `{ codeConfiguration: ... }` for a managed-runtime
42 * code bundle.
43 */
44 agentRuntimeArtifact: RuntimeArtifact;
45 /**
46 * The ARN of an IAM role the runtime assumes. Must trust
47 * `bedrock-agentcore.amazonaws.com`.
48 */
49 roleArn: string;
50 /**
51 * Network access for the runtime.
52 * @default { networkMode: "PUBLIC" }
53 */
54 networkConfiguration?: control.NetworkConfiguration;
55 /**
56 * The protocol the hosted agent serves (`HTTP`, `MCP`, or `A2A`).
57 */
58 protocolConfiguration?: control.ProtocolConfiguration;
59 /**
60 * Inbound authorization for `InvokeAgentRuntime` (JWT bearer tokens).
61 * Omit for SigV4 (IAM).
62 */
63 authorizerConfiguration?: control.AuthorizerConfiguration;
64 /**
65 * Request headers forwarded to the agent container.
66 */
67 requestHeaderConfiguration?: control.RequestHeaderConfiguration;
68 /**
69 * Idle-session and max-lifetime configuration.
70 */
71 lifecycleConfiguration?: control.LifecycleConfiguration;
72 /**
73 * Environment variables injected into the agent container.
74 */
75 environmentVariables?: Record<string, string>;
76 /**
77 * Tags to apply to the runtime. Merged with internal Alchemy tags.
78 */
79 tags?: Record<string, string>;
80}
81
82export interface Runtime extends Resource<
83 "AWS.BedrockAgentCore.Runtime",
84 RuntimeProps,
85 {
86 /**
87 * The unique identifier of the agent runtime.
88 */
89 agentRuntimeId: string;
90 /**
91 * The ARN of the agent runtime.
92 */
93 agentRuntimeArn: string;
94 /**
95 * The current version of the agent runtime (bumped on every update).
96 */
97 agentRuntimeVersion: string;
98 /**
99 * Name of the agent runtime.
100 */
101 agentRuntimeName: string;
102 /**
103 * Current status of the agent runtime (e.g. `READY`).
104 */
105 status: string;
106 }
107> {}
108
109/**
110 * An Amazon Bedrock AgentCore Runtime — serverless hosting for containerized
111 * AI agents.
112 *
113 * A runtime deploys an agent (an ECR container image or managed-runtime code
114 * bundle) behind the `InvokeAgentRuntime` data-plane API with session
115 * isolation, scaling, and identity built in. Each configuration change
116 * publishes a new immutable runtime version.
117 *
118 * ### Creating Runtimes
119 * **Example:** Container-Backed Agent Runtime
120 * ```typescript
121 * import * as AgentCore from "alchemy/AWS/BedrockAgentCore";
122 *
123 * const runtime = yield* AgentCore.Runtime("MyAgent", {
124 * agentRuntimeArtifact: {
125 * containerConfiguration: {
126 * containerUri: `${account}.dkr.ecr.us-west-2.amazonaws.com/my-agent:latest`,
127 * },
128 * },
129 * roleArn: role.roleArn,
130 * });
131 * ```
132 *
133 * ### Invoking from a Function
134 * **Example:** Invoke the Agent
135 * ```typescript
136 * // init
137 * const invoke = yield* AgentCore.InvokeAgentRuntime(runtime);
138 *
139 * return {
140 * fetch: Effect.gen(function* () {
141 * // runtime
142 * const response = yield* invoke({
143 * runtimeSessionId: "session-0000000000000000000000000000000001",
144 * payload: JSON.stringify({ prompt: "hello" }),
145 * });
146 * return HttpServerResponse.json({ contentType: response.contentType });
147 * }),
148 * };
149 * ```
150 *
151 * @resource
152 */
153export const Runtime = Resource<Runtime>("AWS.BedrockAgentCore.Runtime");
154
155/** Statuses indicating an in-flight transition to wait out. */
156const RUNTIME_TRANSIENT = new Set(["CREATING", "UPDATING"]);
157
158export const RuntimeProvider = () =>
159 Provider.effect(
160 Runtime,
161 Effect.gen(function* () {
162 const createName = Effect.fn(function* (
163 id: string,
164 props: Pick<RuntimeProps, "agentRuntimeName">,
165 ) {
166 return props.agentRuntimeName ?? (yield* createAgentCoreName(id));
167 });
168
169 const getRuntimeOrUndefined = Effect.fn(function* (
170 agentRuntimeId: string,
171 ) {
172 return yield* control
173 .getAgentRuntime({ agentRuntimeId })
174 .pipe(
175 Effect.catchTag("ResourceNotFoundException", () =>
176 Effect.succeed(undefined),
177 ),
178 );
179 });
180
181 const findByName = Effect.fn(function* (name: string) {
182 const pages = yield* control.listAgentRuntimes
183 .pages({})
184 .pipe(Stream.runCollect);
185 const summary = Array.from(pages)
186 .flatMap((page) => page.agentRuntimes ?? [])
187 .find((s) => s.agentRuntimeName === name);
188 return summary === undefined
189 ? undefined
190 : yield* getRuntimeOrUndefined(summary.agentRuntimeId);
191 });
192
193 const waitForSettled = Effect.fn(function* (agentRuntimeId: string) {
194 return yield* getRuntimeOrUndefined(agentRuntimeId).pipe(
195 Effect.repeat({
196 schedule: Schedule.fixed("5 seconds"),
197 until: (r) => r === undefined || !RUNTIME_TRANSIENT.has(r.status),
198 times: 60,
199 }),
200 );
201 });
202
203 const toAttributes = (runtime: control.GetAgentRuntimeResponse) => ({
204 agentRuntimeId: runtime.agentRuntimeId,
205 agentRuntimeArn: runtime.agentRuntimeArn,
206 agentRuntimeVersion: runtime.agentRuntimeVersion,
207 agentRuntimeName: runtime.agentRuntimeName,
208 status: runtime.status,
209 });
210
211 return Runtime.Provider.of({
212 stables: ["agentRuntimeId", "agentRuntimeArn", "agentRuntimeName"],
213
214 list: () =>
215 Effect.gen(function* () {
216 const pages = yield* control.listAgentRuntimes
217 .pages({})
218 .pipe(Stream.runCollect);
219 const summaries = Array.from(pages).flatMap(
220 (page) => page.agentRuntimes ?? [],
221 );
222 const hydrated = yield* Effect.forEach(
223 summaries,
224 (s) => getRuntimeOrUndefined(s.agentRuntimeId),
225 { concurrency: 5 },
226 );
227 return hydrated.filter((r) => r !== undefined).map(toAttributes);
228 }),
229
230 read: Effect.fn(function* ({ id, olds, output }) {
231 const runtime = output?.agentRuntimeId
232 ? yield* getRuntimeOrUndefined(output.agentRuntimeId)
233 : yield* findByName(
234 yield* createName(id, olds ?? ({} as RuntimeProps)),
235 );
236 if (runtime === undefined || runtime.status === "DELETING") {
237 return undefined;
238 }
239 const attrs = toAttributes(runtime);
240 const tags = yield* readAgentCoreTags(runtime.agentRuntimeArn);
241 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
242 }),
243
244 diff: Effect.fn(function* ({ id, news, olds }) {
245 if (!isResolved(news)) return undefined;
246 const oldName = yield* createName(id, olds ?? ({} as RuntimeProps));
247 const newName = yield* createName(id, news ?? ({} as RuntimeProps));
248 if (oldName !== newName) {
249 return { action: "replace" } as const;
250 }
251 // artifact, role, network, protocol, env vars, and tags converge
252 // via update (each update publishes a new runtime version).
253 }),
254
255 reconcile: Effect.fn(function* ({ id, news, output, session }) {
256 const props = news ?? ({} as RuntimeProps);
257 const name =
258 output?.agentRuntimeName ?? (yield* createName(id, props));
259 const internalTags = yield* createInternalTags(id);
260 const desiredTags = { ...props.tags, ...internalTags };
261 const networkConfiguration = props.networkConfiguration ?? {
262 networkMode: "PUBLIC",
263 };
264
265 // 1. OBSERVE
266 let runtime = output?.agentRuntimeId
267 ? yield* getRuntimeOrUndefined(output.agentRuntimeId)
268 : undefined;
269 if (runtime === undefined) {
270 runtime = yield* findByName(name);
271 }
272
273 // 2. ENSURE — a freshly created IAM role is not instantly
274 // assumable; ride out the transient ValidationException and the
275 // name-exists conflict race.
276 if (runtime === undefined) {
277 const created = yield* control
278 .createAgentRuntime({
279 agentRuntimeName: name,
280 description: props.description,
281 agentRuntimeArtifact: props.agentRuntimeArtifact,
282 roleArn: props.roleArn,
283 networkConfiguration,
284 protocolConfiguration: props.protocolConfiguration,
285 authorizerConfiguration: props.authorizerConfiguration,
286 requestHeaderConfiguration: props.requestHeaderConfiguration,
287 lifecycleConfiguration: props.lifecycleConfiguration,
288 environmentVariables: props.environmentVariables,
289 tags: desiredTags,
290 })
291 .pipe(
292 retryWhileValidation,
293 Effect.catchTag("ConflictException", () =>
294 Effect.succeed(undefined),
295 ),
296 );
297 runtime =
298 created === undefined
299 ? yield* findByName(name)
300 : yield* getRuntimeOrUndefined(created.agentRuntimeId);
301 }
302 if (runtime === undefined) {
303 return yield* new AgentCoreProvisioningFailed({
304 message: `agent runtime '${name}' was neither created nor found`,
305 });
306 }
307
308 runtime = (yield* waitForSettled(runtime.agentRuntimeId)) ?? runtime;
309 if (
310 runtime.status === "CREATE_FAILED" ||
311 runtime.status === "UPDATE_FAILED"
312 ) {
313 return yield* new AgentCoreProvisioningFailed({
314 message: `agent runtime '${name}' failed: ${runtime.failureReason ?? "unknown"}`,
315 });
316 }
317
318 // 3. SYNC — converge mutable settings from OBSERVED state. Each
319 // update publishes a new immutable runtime version.
320 const drifted =
321 (unredact(runtime.description) ?? undefined) !==
322 (props.description ?? unredact(runtime.description)) ||
323 runtime.roleArn !== props.roleArn ||
324 JSON.stringify(runtime.agentRuntimeArtifact ?? null) !==
325 JSON.stringify(props.agentRuntimeArtifact) ||
326 JSON.stringify(runtime.networkConfiguration) !==
327 JSON.stringify(networkConfiguration) ||
328 JSON.stringify(runtime.environmentVariables ?? {}) !==
329 JSON.stringify(props.environmentVariables ?? {}) ||
330 (props.protocolConfiguration !== undefined &&
331 JSON.stringify(runtime.protocolConfiguration ?? null) !==
332 JSON.stringify(props.protocolConfiguration)) ||
333 (props.lifecycleConfiguration !== undefined &&
334 JSON.stringify(runtime.lifecycleConfiguration ?? null) !==
335 JSON.stringify(props.lifecycleConfiguration));
336 if (drifted) {
337 yield* control
338 .updateAgentRuntime({
339 agentRuntimeId: runtime.agentRuntimeId,
340 description: props.description,
341 agentRuntimeArtifact: props.agentRuntimeArtifact,
342 roleArn: props.roleArn,
343 networkConfiguration,
344 protocolConfiguration: props.protocolConfiguration,
345 authorizerConfiguration: props.authorizerConfiguration,
346 requestHeaderConfiguration: props.requestHeaderConfiguration,
347 lifecycleConfiguration: props.lifecycleConfiguration,
348 environmentVariables: props.environmentVariables,
349 })
350 .pipe(retryWhileConflict);
351 runtime =
352 (yield* waitForSettled(runtime.agentRuntimeId)) ?? runtime;
353 }
354
355 // 3b. SYNC TAGS against observed cloud tags.
356 yield* syncAgentCoreTags(runtime.agentRuntimeArn, desiredTags);
357
358 // 4. RETURN fresh attributes.
359 yield* session.note(runtime.agentRuntimeId);
360 return toAttributes(runtime);
361 }),
362
363 // A runtime with live endpoints rejects deletion with a
364 // ConflictException until they are gone.
365 delete: Effect.fn(function* ({ output }) {
366 yield* control
367 .deleteAgentRuntime({ agentRuntimeId: output.agentRuntimeId })
368 .pipe(
369 retryWhileConflict,
370 Effect.catchTag("ResourceNotFoundException", () =>
371 Effect.succeed(undefined),
372 ),
373 );
374 yield* control
375 .getAgentRuntime({ agentRuntimeId: output.agentRuntimeId })
376 .pipe(
377 Effect.map((r) => r.status as string),
378 Effect.catchTag("ResourceNotFoundException", () =>
379 Effect.succeed("GONE" as string),
380 ),
381 Effect.repeat({
382 schedule: Schedule.fixed("5 seconds"),
383 until: (status) => status === "GONE",
384 times: 36,
385 }),
386 );
387 }),
388 });
389 }),
390 );