1import * as control from "@distilled.cloud/aws/bedrock-agentcore-control";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Redacted from "effect/Redacted";
5import * as Schedule from "effect/Schedule";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import { diffTags } from "../../Tags.ts";
8
9/**
10 * Raised when an AgentCore resource lands in a terminal failure state
11 * (`FAILED`, `CREATE_FAILED`, `UPDATE_FAILED`) during reconciliation.
12 */
13export class AgentCoreProvisioningFailed extends Data.TaggedError(
14 "AgentCoreProvisioningFailed",
15)<{ message: string }> {}
16
17/**
18 * Unwrap an AgentCore `SensitiveString` (decoded as `Redacted`) to its plain
19 * string value. `String(redacted)` would yield `"<redacted>"`, not the value.
20 */
21export const unredact = (
22 value: string | Redacted.Redacted<string> | undefined,
23): string | undefined =>
24 value === undefined
25 ? undefined
26 : Redacted.isRedacted(value)
27 ? Redacted.value(value)
28 : value;
29
30/**
31 * AgentCore Memory / Runtime / CodeInterpreter / Browser names must match
32 * `[a-zA-Z][a-zA-Z0-9_]{0,47}` — underscores, no hyphens. Derive a
33 * deterministic physical name and swap hyphens for underscores.
34 */
35export const createAgentCoreName = Effect.fn(function* (id: string) {
36 const name = yield* createPhysicalName({
37 id,
38 maxLength: 48,
39 delimiter: "_",
40 });
41 return name.replaceAll("-", "_");
42});
43
44/**
45 * Gateway names must match `([0-9a-zA-Z][-]?){1,48}` — hyphens allowed but
46 * never consecutive, no underscores. Collapse runs of hyphens.
47 */
48export const createGatewayName = Effect.fn(function* (id: string) {
49 const name = yield* createPhysicalName({ id, maxLength: 48 });
50 return name.replaceAll(/-+/g, "-");
51});
52
53/**
54 * Read the observed tags of an AgentCore resource by ARN. Best-effort — a
55 * failure (e.g. a race with deletion) reports no tags.
56 */
57export const readAgentCoreTags = Effect.fn(function* (resourceArn: string) {
58 const response = yield* control
59 .listTagsForResource({ resourceArn })
60 .pipe(Effect.catch(() => Effect.succeed(undefined)));
61 const tags: Record<string, string> = {};
62 for (const [key, value] of Object.entries(response?.tags ?? {})) {
63 if (value !== undefined) tags[key] = value;
64 }
65 return tags;
66});
67
68/**
69 * Sync tags on an AgentCore resource: diff the OBSERVED cloud tags against
70 * the desired set and apply only the delta.
71 */
72export const syncAgentCoreTags = Effect.fn(function* (
73 resourceArn: string,
74 desired: Record<string, string>,
75) {
76 const observed = yield* readAgentCoreTags(resourceArn);
77 const { upsert, removed } = diffTags(observed, desired);
78 if (upsert.length > 0) {
79 yield* control.tagResource({
80 resourceArn,
81 tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
82 });
83 }
84 if (removed.length > 0) {
85 yield* control.untagResource({ resourceArn, tagKeys: removed });
86 }
87});
88
89/**
90 * Bounded retry through transient `ConflictException`s (e.g. deleting a
91 * gateway whose targets are still tearing down, or mutating a resource that
92 * is mid-transition). Explicitly typed so the conditional `Retry.Return`
93 * type never widens the provider layer's requirements in declaration emit.
94 */
95export const retryWhileConflict = <A, E extends { readonly _tag: string }, R>(
96 self: Effect.Effect<A, E, R>,
97): Effect.Effect<A, E, R> =>
98 Effect.retry(self, {
99 while: (e) => e._tag === "ConflictException",
100 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(15)]),
101 });
102
103/**
104 * Bounded retry through `ValidationException` during create — a freshly
105 * created IAM role referenced by `roleArn` is not instantly assumable by the
106 * AgentCore service principal (IAM eventual consistency).
107 */
108export const retryWhileValidation = <A, E extends { readonly _tag: string }, R>(
109 self: Effect.Effect<A, E, R>,
110): Effect.Effect<A, E, R> =>
111 Effect.retry(self, {
112 while: (e) => e._tag === "ValidationException",
113 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
114 });