1import * as bedrock from "@distilled.cloud/aws/bedrock-agent";
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 { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
11
12/**
13 * A rule pointing an alias at a specific agent version (optionally with
14 * provisioned throughput). Omit `routingConfiguration` entirely to have
15 * Bedrock snapshot the current DRAFT into a new version and point the alias
16 * at it — the common "deploy the current agent" case.
17 */
18export interface AgentAliasRoutingConfig {
19 /** The agent version this alias routes invocations to. */
20 agentVersion?: string;
21 /** The ARN of a provisioned-throughput commitment to use. */
22 provisionedThroughput?: string;
23}
24
25export interface AgentAliasProps {
26 /**
27 * The id of the {@link Agent} this alias belongs to. Accepts an agent's
28 * `agentId` output. Changing the agent triggers a replacement.
29 */
30 agentId: string;
31 /**
32 * Name of the alias (1-100 characters; letters, digits, and the
33 * characters `_-`). If omitted, a deterministic physical name is
34 * generated from the app, stage, and logical ID. Changing the name
35 * triggers a replacement.
36 */
37 agentAliasName?: string;
38 /**
39 * A description of the alias.
40 */
41 description?: string;
42 /**
43 * Which agent version(s) the alias routes to. When omitted, Bedrock
44 * snapshots the current DRAFT into a new version and routes the alias to
45 * it on create.
46 */
47 routingConfiguration?: AgentAliasRoutingConfig[];
48 /**
49 * Tags to apply to the alias. Merged with internal Alchemy tags.
50 */
51 tags?: Record<string, string>;
52}
53
54export interface AgentAlias extends Resource<
55 "AWS.Bedrock.AgentAlias",
56 AgentAliasProps,
57 {
58 /**
59 * The unique identifier of the agent the alias routes to.
60 */
61 agentId: string;
62 /**
63 * The unique identifier of the alias.
64 */
65 agentAliasId: string;
66 /**
67 * The ARN of the alias.
68 */
69 agentAliasArn: string;
70 /**
71 * Name of the alias.
72 */
73 agentAliasName: string;
74 }
75> {}
76
77/**
78 * An alias for an Amazon Bedrock {@link Agent} — a stable, invocable pointer
79 * to one or more agent versions.
80 *
81 * An alias is what applications invoke (via `bedrock-agent-runtime`
82 * `InvokeAgent`). Creating an alias with no `routingConfiguration` snapshots
83 * the agent's current DRAFT into a new immutable version and routes the alias
84 * to it, so redeploying an updated + prepared agent and recreating the alias
85 * publishes a new version.
86 *
87 * ### Creating Aliases
88 * **Example:** Alias Pointing at the Current Agent
89 * ```typescript
90 * import * as Bedrock from "alchemy/AWS/Bedrock";
91 *
92 * const agent = yield* Bedrock.Agent("assistant", {
93 * foundationModel: "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
94 * instruction: "You are a helpful assistant.",
95 * });
96 *
97 * const alias = yield* Bedrock.AgentAlias("prod", {
98 * agentId: agent.agentId,
99 * });
100 * ```
101 *
102 * **Example:** Alias Pinned to a Specific Version
103 * ```typescript
104 * const alias = yield* Bedrock.AgentAlias("prod", {
105 * agentId: agent.agentId,
106 * routingConfiguration: [{ agentVersion: "3" }],
107 * });
108 * ```
109 *
110 * @resource
111 */
112export const AgentAlias = Resource<AgentAlias>("AWS.Bedrock.AgentAlias");
113
114/** Alias status values indicating an in-flight transition to wait out. */
115const ALIAS_TRANSIENT = new Set(["CREATING", "UPDATING"]);
116
117export const AgentAliasProvider = () =>
118 Provider.effect(
119 AgentAlias,
120 Effect.gen(function* () {
121 const createName = Effect.fn(function* (
122 id: string,
123 props: Pick<AgentAliasProps, "agentAliasName">,
124 ) {
125 return (
126 props.agentAliasName ??
127 (yield* createPhysicalName({ id, maxLength: 100 }))
128 );
129 });
130
131 const getAliasOrUndefined = Effect.fn(function* (
132 agentId: string,
133 agentAliasId: string,
134 ) {
135 return yield* bedrock.getAgentAlias({ agentId, agentAliasId }).pipe(
136 Effect.map((r) => r.agentAlias),
137 Effect.catchTag("ResourceNotFoundException", () =>
138 Effect.succeed(undefined),
139 ),
140 );
141 });
142
143 const findByName = Effect.fn(function* (agentId: string, name: string) {
144 const pages = yield* bedrock.listAgentAliases
145 .pages({ agentId })
146 .pipe(Stream.runCollect);
147 return Array.from(pages)
148 .flatMap((page) => page.agentAliasSummaries ?? [])
149 .find((s) => s.agentAliasName === name)?.agentAliasId;
150 });
151
152 const fetchObservedTags = Effect.fn(function* (resourceArn: string) {
153 return yield* bedrock.listTagsForResource({ resourceArn }).pipe(
154 Effect.map((r) => (r.tags ?? {}) as Record<string, string>),
155 Effect.catchTag("ResourceNotFoundException", () =>
156 Effect.succeed({} as Record<string, string>),
157 ),
158 );
159 });
160
161 const waitForSettled = Effect.fn(function* (
162 agentId: string,
163 agentAliasId: string,
164 ) {
165 return yield* bedrock.getAgentAlias({ agentId, agentAliasId }).pipe(
166 Effect.map((r) => r.agentAlias),
167 Effect.catchTag("ResourceNotFoundException", () =>
168 Effect.succeed(undefined),
169 ),
170 Effect.repeat({
171 schedule: Schedule.fixed("3 seconds"),
172 until: (alias) =>
173 alias === undefined ||
174 !ALIAS_TRANSIENT.has(alias.agentAliasStatus),
175 times: 40,
176 }),
177 );
178 });
179
180 return AgentAlias.Provider.of({
181 stables: ["agentId", "agentAliasId", "agentAliasArn", "agentAliasName"],
182
183 // Aliases are scoped to a parent agent — enumeration requires the
184 // agent id, so this returns empty (the engine keys off state).
185 list: () => Effect.succeed([]),
186
187 read: Effect.fn(function* ({ id, olds, output }) {
188 const agentId = output?.agentId ?? olds?.agentId;
189 if (agentId === undefined) return undefined;
190 const aliasId =
191 output?.agentAliasId ??
192 (yield* findByName(
193 agentId,
194 output?.agentAliasName ??
195 (yield* createName(id, olds ?? ({} as AgentAliasProps))),
196 ));
197 if (aliasId === undefined) return undefined;
198 const alias = yield* getAliasOrUndefined(agentId, aliasId);
199 if (alias === undefined || alias.agentAliasStatus === "DELETING") {
200 return undefined;
201 }
202 const attrs = {
203 agentId: alias.agentId,
204 agentAliasId: alias.agentAliasId,
205 agentAliasArn: alias.agentAliasArn,
206 agentAliasName: alias.agentAliasName,
207 };
208 const tags = yield* fetchObservedTags(alias.agentAliasArn);
209 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
210 }),
211
212 diff: Effect.fn(function* ({ id, news, olds }) {
213 if (!isResolved(news)) return undefined;
214 if ((olds?.agentId ?? undefined) !== (news?.agentId ?? undefined)) {
215 return { action: "replace" } as const;
216 }
217 const oldName = yield* createName(
218 id,
219 olds ?? ({} as AgentAliasProps),
220 );
221 const newName = yield* createName(
222 id,
223 news ?? ({} as AgentAliasProps),
224 );
225 if (oldName !== newName) {
226 return { action: "replace" } as const;
227 }
228 // description, routingConfiguration, and tags converge via update.
229 }),
230
231 reconcile: Effect.fn(function* ({
232 id,
233 news = {} as AgentAliasProps,
234 output,
235 session,
236 }) {
237 const name = output?.agentAliasName ?? (yield* createName(id, news));
238 const agentId = news.agentId;
239 const internalTags = yield* createInternalTags(id);
240 const desiredTags = { ...news.tags, ...internalTags };
241
242 // 1. OBSERVE
243 let alias = output?.agentAliasId
244 ? yield* getAliasOrUndefined(agentId, output.agentAliasId)
245 : undefined;
246 if (alias === undefined) {
247 const foundId = yield* findByName(agentId, name);
248 if (foundId !== undefined) {
249 alias = yield* getAliasOrUndefined(agentId, foundId);
250 }
251 }
252
253 if (alias === undefined) {
254 // 2. ENSURE
255 const created = yield* bedrock.createAgentAlias({
256 agentId,
257 agentAliasName: name,
258 description: news.description,
259 routingConfiguration: news.routingConfiguration,
260 tags: desiredTags,
261 });
262 alias = created.agentAlias;
263 alias =
264 (yield* waitForSettled(agentId, alias.agentAliasId)) ?? alias;
265 } else {
266 // 3. SYNC
267 alias =
268 (yield* waitForSettled(agentId, alias.agentAliasId)) ?? alias;
269 yield* bedrock.updateAgentAlias({
270 agentId,
271 agentAliasId: alias.agentAliasId,
272 agentAliasName: name,
273 description: news.description,
274 routingConfiguration: news.routingConfiguration,
275 });
276 alias =
277 (yield* waitForSettled(agentId, alias.agentAliasId)) ?? alias;
278 }
279
280 const agentAliasId = alias.agentAliasId;
281 const agentAliasArn = alias.agentAliasArn;
282
283 // 3b. SYNC TAGS against observed cloud tags.
284 const observedTags = yield* fetchObservedTags(agentAliasArn);
285 const { upsert, removed } = diffTags(observedTags, desiredTags);
286 if (upsert.length > 0) {
287 yield* bedrock.tagResource({
288 resourceArn: agentAliasArn,
289 tags: Object.fromEntries(
290 upsert.map(({ Key, Value }) => [Key, Value]),
291 ),
292 });
293 }
294 if (removed.length > 0) {
295 yield* bedrock.untagResource({
296 resourceArn: agentAliasArn,
297 tagKeys: removed,
298 });
299 }
300
301 yield* session.note(agentAliasArn);
302 return {
303 agentId,
304 agentAliasId,
305 agentAliasArn,
306 agentAliasName: name,
307 };
308 }),
309
310 delete: Effect.fn(function* ({ output }) {
311 yield* bedrock
312 .deleteAgentAlias({
313 agentId: output.agentId,
314 agentAliasId: output.agentAliasId,
315 })
316 .pipe(
317 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
318 );
319 }),
320 });
321 }),
322 );