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} from "./internal.ts";
18
19export interface CodeInterpreterProps {
20 /**
21 * Name of the code interpreter. Must match `[a-zA-Z][a-zA-Z0-9_]{0,47}`
22 * (underscores, no hyphens). If omitted, a deterministic physical name is
23 * generated from the app, stage, and logical ID. Changing the name triggers
24 * a replacement.
25 */
26 name?: string;
27 /**
28 * A description of the code interpreter. Changing it triggers a
29 * replacement (the API has no update operation).
30 */
31 description?: string;
32 /**
33 * The ARN of an IAM role sessions assume to access AWS resources (e.g. S3
34 * buckets) from executed code. Changing it triggers a replacement.
35 */
36 executionRoleArn?: string;
37 /**
38 * Network access for executed code: `SANDBOX` (no egress), `PUBLIC`
39 * (internet egress), or `VPC`. Changing it triggers a replacement.
40 * @default { networkMode: "SANDBOX" }
41 */
42 networkConfiguration?: control.CodeInterpreterNetworkConfiguration;
43 /**
44 * Tags to apply to the code interpreter. Merged with internal Alchemy tags.
45 */
46 tags?: Record<string, string>;
47}
48
49export interface CodeInterpreter extends Resource<
50 "AWS.BedrockAgentCore.CodeInterpreter",
51 CodeInterpreterProps,
52 {
53 /**
54 * The unique identifier of the code interpreter.
55 */
56 codeInterpreterId: string;
57 /**
58 * The ARN of the code interpreter.
59 */
60 codeInterpreterArn: string;
61 /**
62 * Name of the code interpreter.
63 */
64 name: string;
65 /**
66 * Current status of the code interpreter (e.g. `READY`).
67 */
68 status: string;
69 }
70> {}
71
72/**
73 * A custom Amazon Bedrock AgentCore Code Interpreter — an isolated sandbox
74 * where agents execute Python/JavaScript/TypeScript code.
75 *
76 * A custom interpreter controls the sandbox's network mode and execution
77 * role. All configuration is create-only (the API has no update operation);
78 * property changes trigger a replacement.
79 *
80 * ### Creating Code Interpreters
81 * **Example:** Sandboxed Interpreter (no network egress)
82 * ```typescript
83 * import * as AgentCore from "alchemy/AWS/BedrockAgentCore";
84 *
85 * const interpreter = yield* AgentCore.CodeInterpreter("Sandbox", {});
86 * ```
87 *
88 * **Example:** Interpreter with Public Egress
89 * ```typescript
90 * const interpreter = yield* AgentCore.CodeInterpreter("PublicSandbox", {
91 * networkConfiguration: { networkMode: "PUBLIC" },
92 * });
93 * ```
94 *
95 * ### Executing Code from a Function
96 * **Example:** Start a Session and Run Code
97 * ```typescript
98 * // init
99 * const startSession = yield* AgentCore.StartCodeInterpreterSession(interpreter);
100 * const invoke = yield* AgentCore.InvokeCodeInterpreter(interpreter);
101 * const stopSession = yield* AgentCore.StopCodeInterpreterSession(interpreter);
102 *
103 * return {
104 * fetch: Effect.gen(function* () {
105 * // runtime
106 * const session = yield* startSession({ sessionTimeout: "5 minutes" });
107 * const result = yield* invoke({
108 * sessionId: session.sessionId,
109 * name: "executeCode",
110 * arguments: { language: "python", code: "print(1 + 1)" },
111 * });
112 * const output = yield* Stream.runCollect(result.stream);
113 * yield* stopSession({ sessionId: session.sessionId });
114 * return HttpServerResponse.json({ output: Array.from(output) });
115 * }),
116 * };
117 * ```
118 *
119 * @resource
120 */
121export const CodeInterpreter = Resource<CodeInterpreter>(
122 "AWS.BedrockAgentCore.CodeInterpreter",
123);
124
125export const CodeInterpreterProvider = () =>
126 Provider.effect(
127 CodeInterpreter,
128 Effect.gen(function* () {
129 const createName = Effect.fn(function* (
130 id: string,
131 props: Pick<CodeInterpreterProps, "name">,
132 ) {
133 return props.name ?? (yield* createAgentCoreName(id));
134 });
135
136 // getCodeInterpreter still resolves soft-deleted interpreters —
137 // DELETED/DELETING count as gone.
138 const getLiveOrUndefined = Effect.fn(function* (
139 codeInterpreterId: string,
140 ) {
141 const found = yield* control
142 .getCodeInterpreter({ codeInterpreterId })
143 .pipe(
144 Effect.catchTag("ResourceNotFoundException", () =>
145 Effect.succeed(undefined),
146 ),
147 );
148 return found === undefined ||
149 found.status === "DELETED" ||
150 found.status === "DELETING"
151 ? undefined
152 : found;
153 });
154
155 const findByName = Effect.fn(function* (name: string) {
156 const pages = yield* control.listCodeInterpreters
157 .pages({})
158 .pipe(Stream.runCollect);
159 const summary = Array.from(pages)
160 .flatMap((page) => page.codeInterpreterSummaries ?? [])
161 .find(
162 (s) =>
163 s.name === name &&
164 s.status !== "DELETED" &&
165 s.status !== "DELETING",
166 );
167 return summary === undefined
168 ? undefined
169 : yield* getLiveOrUndefined(summary.codeInterpreterId);
170 });
171
172 const toAttributes = (ci: control.GetCodeInterpreterResponse) => ({
173 codeInterpreterId: ci.codeInterpreterId,
174 codeInterpreterArn: ci.codeInterpreterArn,
175 name: ci.name,
176 status: ci.status,
177 });
178
179 return CodeInterpreter.Provider.of({
180 stables: ["codeInterpreterId", "codeInterpreterArn", "name"],
181
182 list: () =>
183 Effect.gen(function* () {
184 const pages = yield* control.listCodeInterpreters
185 .pages({})
186 .pipe(Stream.runCollect);
187 const summaries = Array.from(pages)
188 .flatMap((page) => page.codeInterpreterSummaries ?? [])
189 .filter((s) => s.status !== "DELETED" && s.status !== "DELETING");
190 const hydrated = yield* Effect.forEach(
191 summaries,
192 (s) => getLiveOrUndefined(s.codeInterpreterId),
193 { concurrency: 5 },
194 );
195 return hydrated.filter((c) => c !== undefined).map(toAttributes);
196 }),
197
198 read: Effect.fn(function* ({ id, olds, output }) {
199 const ci = output?.codeInterpreterId
200 ? yield* getLiveOrUndefined(output.codeInterpreterId)
201 : yield* findByName(
202 yield* createName(id, olds ?? ({} as CodeInterpreterProps)),
203 );
204 if (ci === undefined) return undefined;
205 const attrs = toAttributes(ci);
206 const tags = yield* readAgentCoreTags(ci.codeInterpreterArn);
207 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
208 }),
209
210 // Everything except tags is create-only — any change replaces.
211 diff: Effect.fn(function* ({ id, news, olds }) {
212 if (!isResolved(news)) return undefined;
213 const oldProps = olds ?? ({} as CodeInterpreterProps);
214 const newProps = news ?? ({} as CodeInterpreterProps);
215 const oldName = yield* createName(id, oldProps);
216 const newName = yield* createName(id, newProps);
217 if (
218 oldName !== newName ||
219 (oldProps.description ?? undefined) !==
220 (newProps.description ?? undefined) ||
221 (oldProps.executionRoleArn ?? undefined) !==
222 (newProps.executionRoleArn ?? undefined) ||
223 JSON.stringify(
224 oldProps.networkConfiguration ?? { networkMode: "SANDBOX" },
225 ) !==
226 JSON.stringify(
227 newProps.networkConfiguration ?? { networkMode: "SANDBOX" },
228 )
229 ) {
230 return { action: "replace" } as const;
231 }
232 }),
233
234 reconcile: Effect.fn(function* ({ id, news, output, session }) {
235 const props = news ?? ({} as CodeInterpreterProps);
236 const name = output?.name ?? (yield* createName(id, props));
237 const internalTags = yield* createInternalTags(id);
238 const desiredTags = { ...props.tags, ...internalTags };
239
240 // 1. OBSERVE
241 let ci = output?.codeInterpreterId
242 ? yield* getLiveOrUndefined(output.codeInterpreterId)
243 : undefined;
244 if (ci === undefined) {
245 ci = yield* findByName(name);
246 }
247
248 // 2. ENSURE — creation is synchronous (READY on return); tolerate
249 // the name-exists race and transient IAM propagation.
250 if (ci === undefined) {
251 const created = yield* control
252 .createCodeInterpreter({
253 name,
254 description: props.description,
255 executionRoleArn: props.executionRoleArn,
256 networkConfiguration: props.networkConfiguration ?? {
257 networkMode: "SANDBOX",
258 },
259 tags: desiredTags,
260 })
261 .pipe(
262 retryWhileValidation,
263 Effect.catchTag("ConflictException", () =>
264 Effect.succeed(undefined),
265 ),
266 );
267 ci =
268 created === undefined
269 ? yield* findByName(name)
270 : yield* getLiveOrUndefined(created.codeInterpreterId);
271 }
272 if (ci === undefined) {
273 return yield* new AgentCoreProvisioningFailed({
274 message: `code interpreter '${name}' was neither created nor found`,
275 });
276 }
277 if (ci.status === "CREATE_FAILED") {
278 return yield* new AgentCoreProvisioningFailed({
279 message: `code interpreter '${name}' failed: ${ci.failureReason ?? "unknown"}`,
280 });
281 }
282
283 // 3. SYNC — only tags are mutable.
284 yield* syncAgentCoreTags(ci.codeInterpreterArn, desiredTags);
285
286 // 4. RETURN fresh attributes.
287 yield* session.note(ci.codeInterpreterId);
288 return toAttributes(ci);
289 }),
290
291 delete: Effect.fn(function* ({ output }) {
292 yield* control
293 .deleteCodeInterpreter({
294 codeInterpreterId: output.codeInterpreterId,
295 })
296 .pipe(
297 retryWhileConflict,
298 Effect.catchTag("ResourceNotFoundException", () =>
299 Effect.succeed(undefined),
300 ),
301 );
302 // Deletion completes quickly (soft-delete to DELETED); wait until
303 // it is out of the live set so a re-create of the same name works.
304 yield* control
305 .getCodeInterpreter({
306 codeInterpreterId: output.codeInterpreterId,
307 })
308 .pipe(
309 Effect.map((ci) => ci.status as string),
310 Effect.catchTag("ResourceNotFoundException", () =>
311 Effect.succeed("DELETED" as string),
312 ),
313 Effect.repeat({
314 schedule: Schedule.fixed("3 seconds"),
315 until: (status) =>
316 status === "DELETED" || status === "DELETE_FAILED",
317 times: 20,
318 }),
319 );
320 }),
321 });
322 }),
323 );